Language/NodeJS

NodeJS/채팅 만들기 (BackEnd)

건담아빠 2022. 9. 2. 10:41

frontend는 별도 backend는 node js로 구성된 서비스

실질적인 작업은 일주일도 하지 못하고 완성된 작품이라서 부족한게 많지만 다음에는 좀더 좋게 만들기 위해서 간단히 기록해 둔다.

다음에는 타입스크립트 및 세션베이스로 고려해보자.

 

package.json

{
  "name": "test-nodejs",
  "version": "1.0.0",
  "description": "test nodejs",
  "main": "index.js",
  "author": "kand.deokjoon",
  "license": "ISC",
  "type": "module",
  "scripts": {
    "build": "babel . --ignore node_modules,build --out-dir build",
    "start:local": "MODE=local nodemon --watch src/ src/server.js"
  },
  "dependencies": {
    "chalk-template": "^0.2.0",
    "cors": "^2.8.5",
    "express": "^4.17.2",
    "http": "^0.0.1-security",
    "module": "^1.2.5",
    "moment": "^2.29.1",
    "sockjs": "^0.3.24",
    "url": "^0.11.0"
  },
  "devDependencies": {
    "@babel/cli": "^7.16.0",
    "@babel/core": "^7.16.5",
    "@babel/node": "^7.16.5",
    "@babel/preset-env": "^7.16.5",
    "nodemon": "^2.0.15"
  }
}

 

server.js

import Server from './lib/server.js'

global.g_channels = {}
global.g_clients = {}

const server = new Server()

 

socket-events.js

import chalk from 'chalk'

import constants from './lib/constants.js'
import log from './lib/log.js'
import socketService from './services/socket-service.js'
import chatService from './services/chat-service.js'

const socketEvents = {
  /**
   * sockjs - connection event
   * 
   * @param {*} socket 
   */
  connection: (socket) => {
    log.socket(`${chalk.underline(socket.id)} : connected (${socket.headers['x-forwarded-for']})`)
    // const params = url.parse(socket.url, true).query
    // log.info(`seller_no : ${params.seller_no}`)

    /**
     * sockjs - connection data event
     */
    socket.on('data', (message) => {
      try {
        const params = JSON.parse(message)

        console.log('\r\n\r\n\r\n')
        log.info('######################################## DATA STATED ########################################')
        log.info(`${chalk.bold.magenta.dim('[DATA-TYPE]')} [${params.type}] - (${params.action})`)

        if (params.type === constants.SOCKET.TYPE) {
          switch (params.action) {
            case constants.SOCKET.ACTION.CONNECT:
              socketService.connect(socket, params)
              break
          }
        } else if (params.type === constants.EASYTALK.TYPE) {
          switch (params.action) {
            case constants.EASYTALK.ACTION.JOIN_CHANNEL:
              if (chatService.isValidUserParams(socket.id, params) === false) {
                return log.error('권한없음')
              }
              chatService.join(socket, params)
              break
            case constants.EASYTALK.ACTION.SEND_MESSAGE:
              if (chatService.isValidChannelParams(socket.id, params) === false) {
                return log.error('권한없음')
              }
              chatService.send(socket.id, params)
              break
            case constants.EASYTALK.ACTION.EXIT_CHANNEL:
              chatService.exit(socket.id, params.is_forever)
              break
            case constants.EASYTALK.ACTION.EMIT_CHANNEL_MESSAGE:
              chatService.emitChannelMessage(params)
              break
          }
        }
      } catch (err) {
        return log.error(err)
      }
    })

    /**
     * sockjs - connection close event
     */
    socket.on('close', () => {
      try {
        if (g_clients.hasOwnProperty(socket.id) === true) {
          log.socket(`${chalk.underline(socket.id)} : disconnected (${g_clients[socket.id].ip})`)
          socketService.close(socket.id)
        } else {
          log.socket(`${chalk.underline(socket.id)} : disconnected (${socket.headers['x-forwarded-for']})`)
        }
      } catch (err) {
        return log.error(err)
      }
    })
  },

}

export default socketEvents

 

Config

config/config.js

import baseConfig from './base-config.js'

import prdConfig from './prd-config.js'
import qaConfig from './qa-config.js'
import devConfig from './dev-config.js'
import demoConfig from './demo-config.js'
import localConfig from './local-config.js'

let config = baseConfig;

const SERVER_HOSTS = Object.freeze({
  dev: {
    'test-dev-001': {
      EZP_API_HOST: 'dev-001.test.co.kr'
    },
    ...
  },
  qa: {
    'test-qa-001': {
      EZP_API_HOST: 'qa-001.test.co.kr'
    },
    ...
  },
  ...
})

console.log('TEST process.env.MODE : ', process.env.MODE)
console.log('TEST process.env.HOST_NAME : ', process.env.HOST_NAME)

if (process.env.MODE == 'local') {
  config = Object.assign({}, config, localConfig);
} else {
  if (process.env.MODE == 'prd') {
    config = Object.assign({}, config, prdConfig);
  } else if (process.env.MODE == 'qa') {
    config = Object.assign({}, config, qaConfig);
  } else if (process.env.MODE == 'dev') {
    config = Object.assign({}, config, devConfig);
  } else if (process.env.MODE == 'demo') {
    config = Object.assign({}, config, demoConfig);
  }
  
  // 서버별 URL 틀려서 API 주소 변경
  config.api.host = SERVER_HOSTS[process.env.MODE][process.env.HOST_NAME]['EZP_API_HOST']
  console.log('TEST config.api.host : ', config.api.host)
}


export default config

 

config/base-config.js

export default {
  mode: "common",
  log: true,
  port: 10002,
  url: "/",
  ssl: {
    use: false,
    key: "/path/to/your/ssl.key",
    cert: "/path/to/your/ssl.crt"
  }
}

 

config/local-config.js

export default {
  mode: "local",
  api: {
    host: 'test.apitest.com',
    port: 80,
    serviceId: "test-node",
    signature: "sdas"
  }
}

 

Library

lib/server.js

import express from 'express'
import sockjs from 'sockjs'
import fs from 'fs'
import chalk from 'chalk'
import * as http from 'http'
import https from 'https'
import cors from 'cors'

import config from '../config/config.js'
import log from './log.js'
import socketEvents from '../socket-events.js'
import routers from '../routes/index.js'
// import session from "express-session"

export default class Server {
  constructor() {
    log.start(`mode ${chalk.bold.red.dim(config.mode.toUpperCase())}`)

    this.port = process.env.PORT || config.port

    this.app = express()
    this.server = null
    this.sockjs_server = null

    this.configuration()
  }

  /**
   * configuration
   */
  configuration() {
    this.setRouter()
    this.createServer()
    this.setServerEvent()
    this.createSocketServer()

    // this.sessionTest()
  }

  /**
   * routers setting
   */
  setRouter() {
    this.app.use('/', routers.index)
  }

  /**
   * create server
   */
  createServer() {
    // http or https setting
    if (config.ssl.use === false) {
      log.start('no ssl')
      this.server = http.createServer(this.app)
    } else {
      log.start('ssl')
      const opt = {
        key: fs.readFileSync(config.ssl.key),
        cert: fs.readFileSync(config.ssl.cert)
      }
      this.server = https.createServer(opt, this.app)
    }
  }

  /**
   * set server event
   */
  setServerEvent() {
    // this.server.listen(this.port)
    this.server.listen(this.port, '0.0.0.0', () => {
      // log.start(`server on port ${this.port}!`)
    })

    // server - listening event
    this.server.on('listening', (args) => {
      // log.start(`server listening ${this.port}`)
      const addr = this.server.address()
      const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
      log.start('Listening at ' + bind)
    })

    // server - error event
    this.server.on('error', (error) => {
      log.error('server error', error)
    })
  }

  /**
   * sockjs - create server
   */
  createSocketServer() {
    // sockjs - create server
    this.sockjs_server = sockjs.createServer()
    this.sockjs_server.installHandlers(this.server,
      {
        prefix: '/socket.io',
        log: function () { }
      }
    )

    // sockjs - connection event
    this.sockjs_server.on('connection', socketEvents.connection)
  }
}

 

lib/request.js

import * as http from 'http'
import https from 'https'
import log from './log.js'

class Request {
  constructor() {

  }

  setOptions(options = {}) {
    this.options = options
  }
 
  send(options) {
    try {
      // log.test('api url', this.options.path)
      log.test('this.options', this.options)
      log.test('this.options.host', this.options.host)

      // TODO. test 및 status code에 따른 분기처리
      let req = null
      if (this.options.port === 443) {
        req = https.request(this.options, res => {
          res.setEncoding("utf8")
          res.on('data', options.ondata)
        })
      } else {
        req = http.request(this.options, res => {
          res.setEncoding("utf8")
          res.on('data', options.ondata)
        })
      }
    
      req.on('error', e => {
        log.error(e)
      })
    
      req.write(JSON.stringify(options.data))
      req.end()
    } catch(err) {
      log.error(err)
    }
  }

  
}

export default Request

 

lib/log.js

import chalk from 'chalk'
import moment from 'moment'

moment.locale('ko')

const log = {
    test: (title, message = '') => {
        if (message === '') {
            message = title
            log.print(chalk.rgb(255, 255, 0).bold.dim('[Test] '), message)
        } else {
            const type = chalk.rgb(255, 255, 0).bold.dim('[Test] ') + chalk.rgb(255, 255, 0).bold.dim(title)
            log.print(type, message)
        }
    },

    socket: message => log.print(chalk.bold.magenta('[Socket] '), message),

    start: message => log.print(chalk.bold.green.dim('[Node Start] '), message),
    stop: message => log.print(chalk.bold.red.dim('[Node Stop] '), message),

    error: message => log.print(chalk.bold.red.dim('[Error] '), message),
    info: message => log.print(chalk.bold.blue('[Info] '), message),
    message: message => log.print(chalk.bold.cyan.dim('[Message] '), message),

    print: (type, message) => console.log(`[${moment(new Date()).format('HH:mm:ss')}] ${type} : `, message)
}

export default log

 

lib/constants.js

const constants = {
  SOCKET: {
    TYPE: 'socket',
    ACTION: {
      CONNECT: 'socket_connect',
      REFRESH_EASY_TALK_TOP: 'refresh_easy_talk_top',
      RELOAD_PAGE: 'reload_page'
    },
    TARGET: {
      SELF: 'self',
      OTHER: 'other'
    }
  },
  EASYTALK: {
    TYPE: 'talk',
    ACTION: {
      JOIN_CHANNEL: 'join_channel',
      SEND_MESSAGE: 'send_message',
      EXIT_CHANNEL: 'exit_channel',
      EMIT_CHANNEL_MESSAGE: 'emit_channel_message',

      RECEIVE_MESSAGE: 'receive_message',
      REFRESH_CHANNEL: 'refresh_channel',
      RESET_CHANNEL: 'reset_channel'
    }    
  },
  COMMON: {
    USER_TYPE_BUYER: 'B', // 쇼핑몰
    USER_TYPE_SELLER: 'S',  // 도매처
    SEND_TYPE_MESSAGE: '10' // 전송 유형 (10: 문자,  20: 주문문의, 21: 주문 문의 (일반주문 상품), 22: 주문 문의 (바로주문 상품) , 30:  상품 문의 (일반주문)
  }
}

const deepFreeze = obj => {
  const propNames = Object.getOwnPropertyNames(obj)

  for (const name of propNames) {
    const value = obj[name];

    if (value && typeof value === "object") {
      deepFreeze(value);
    }
  }

  return Object.freeze(obj);
}


export default deepFreeze(constants)

 

lib/util/socket-utils.js

import moment from 'moment'

import constants from '../constants.js'
import log from '../log.js'

moment.locale('ko')

const socketUtils = {
  /**
   * send to clients
   * 
   * @param {*} to_clients 
   * @param {*} customParams 
   */
  sendToClients: (to_clients, customParams) => {
    for (const [key, to_client] of Object.entries(to_clients)) {
      to_client.socket.write(JSON.stringify({ ...customParams }));
    }
  },

  /**
   * send to join clients by socket id
   * 
   * @param {*} from_socket_id 
   * @param {*} customParams 
   */
  sendToJoinClientsBySocketId: (from_socket_id, customParams) => {
    const from_client = g_clients[from_socket_id]
    const to_sockets = g_channels[from_client.channel_id].sockets

    for (const [key, to_socket] of Object.entries(to_sockets)) {
      const to_client = g_clients[key]
      const target = from_client.user_type === to_client.user_type ? constants.SOCKET.TARGET.SELF : constants.SOCKET.TARGET.OTHER

      to_socket.write(JSON.stringify({ type: constants.EASYTALK.TYPE, target: target, ...customParams }));
    }
  },

  /**
   * get join clients by user type
   * 
   * @param {*} user_type 
   * @param {*} buyer_no 
   * @param {*} seller_no 
   * @returns 
   */
  getJoinClientsByUserType: (user_type, buyer_no, seller_no) => {
    return Object.values(g_clients).filter(client => {
      if (
        client.user_type === user_type && client.buyer_no === buyer_no && client.seller_no === seller_no
      ) {
        return true
      }
    })
  },

  /**
   * get all clients by socket id
   * 
   * @param {*} from_socket_id 
   * @returns 
   */
  getAllClientsBySocketId: from_socket_id => {
    const { buyer_no, seller_no } = g_clients[from_socket_id]

    return Object.values(g_clients).filter(client => {
      if (
        client.user_type === constants.COMMON.USER_TYPE_BUYER && client.buyer_no === buyer_no ||
        client.user_type === constants.COMMON.USER_TYPE_SELLER && client.seller_no === seller_no
      ) {
        return true
      }
    })
  },

  /**
   * get all clients by user no
   * 
   * @param {*} buyer_no 
   * @param {*} seller_no 
   * @returns 
   */
  getAllClientsByUserNo: (buyer_no, seller_no) => {
    return Object.values(g_clients).filter(client => {
      if (
        client.user_type === constants.COMMON.USER_TYPE_BUYER && client.buyer_no === buyer_no ||
        client.user_type === constants.COMMON.USER_TYPE_SELLER && client.seller_no === seller_no
      ) {
        return true
      }
    })
  },

  /**
   * get self clients by socket id
   * 
   * @param {*} from_socket_id 
   * @returns 
   */
  getSelfClientsBySocketId: from_socket_id => {
    const { user_type, buyer_no, seller_no } = g_clients[from_socket_id]

    return Object.values(g_clients).filter(client => {
      if (
        (user_type === constants.COMMON.USER_TYPE_BUYER && client.buyer_no === buyer_no) ||
        (user_type === constants.COMMON.USER_TYPE_SELLER && client.seller_no === seller_no)) {
        return true
      }
    })
  },

  /**
   * get other clients by socket id
   * 
   * @param {*} from_socket_id 
   * @returns 
   */
  getOtherClientsBySocketId: from_socket_id => {
    const { user_type, buyer_no, seller_no } = g_clients[from_socket_id]

    return Object.values(g_clients).filter(client => {
      if (
        (user_type === constants.COMMON.USER_TYPE_BUYER && client.user_type === constants.COMMON.USER_TYPE_SELLER && client.seller_no === seller_no) ||
        (user_type === constants.COMMON.USER_TYPE_SELLER && client.user_type === constants.COMMON.USER_TYPE_BUYER && client.buyer_no === buyer_no)) {
        return true
      }
    })
  },

  /**
   * is target join by socket id
   * 
   * @param {*} from_socket_id 
   * @returns 
   */
  isJoinTargetBySocketId: from_socket_id => {
    const { user_type, buyer_no, seller_no, channel_id } = g_clients[from_socket_id]

    for (const socket_id of Object.keys(g_channels[channel_id].sockets)) {
      const client = g_clients[socket_id]

      if (user_type === constants.COMMON.USER_TYPE_BUYER && client.user_type === constants.COMMON.USER_TYPE_SELLER && client.buyer_no === buyer_no && client.seller_no === seller_no) {
        return 'T'
      } else if (user_type === constants.COMMON.USER_TYPE_SELLER && client.user_type === constants.COMMON.USER_TYPE_BUYER && client.buyer_no === buyer_no && client.seller_no === seller_no) {
        return 'T'
      }
    }

    return 'F'
  },

  /**
   * is join channel
   * 
   * @param {*} user_type 
   * @param {*} buyer_no 
   * @param {*} seller_no 
   * @returns 
   */
  isJoinChannel: (user_type, buyer_no, seller_no) => {
    const channel_id = `${buyer_no}-${seller_no}`

    if (Object.keys(g_channels).includes(channel_id) === true) {
      for (const socket_id of Object.keys(g_channels[channel_id].sockets)) {
        const client = g_clients[socket_id]

        if (client.user_type === user_type && client.buyer_no === buyer_no && client.seller_no === seller_no) {
          return 'T'
        }
      }
    }

    return 'F'
  },

  /**
   * add channel and socket by channel id
   * 
   * @param {*} channel_id 
   * @param {*} socket 
   */
  addChannelAndSocketByChannelId: (channel_id, socket) => {
    if (Object.keys(g_channels).includes(channel_id) === true) {
      // 기존 채널
      let channelSockets = g_channels[channel_id].sockets
      if (Object.keys(channelSockets).includes(socket.id) === false) {
        channelSockets[socket.id] = socket
      }
    } else {
      // 신규 채널
      g_channels[channel_id] = { sockets: [] }
      g_channels[channel_id].sockets[socket.id] = socket
    }
  },

  /**
   * add client
   * 
   * @param {*} channel_id 
   * @param {*} user_type 
   * @param {*} buyer_no 
   * @param {*} seller_no 
   * @param {*} socket 
   */
  addClient: (channel_id, user_type, buyer_no, seller_no, socket) => {
    g_clients[socket.id] = {
      user_type: user_type,
      buyer_no: buyer_no,
      seller_no: seller_no,
      socket: socket,
      ip: socket.headers['x-forwarded-for']
    }

    if (channel_id !== null) {
      g_clients[socket.id].channel_id = channel_id
    }
  },

  /**
   * delete channel by socket id
   * 
   * @param {*} delete_socket_id 
   */
  deleteChannelBySocketId: delete_socket_id => {
    if (Object.keys(g_clients).includes(delete_socket_id) === true) {
      if (g_clients[delete_socket_id].hasOwnProperty('channel_id') === true) {

        const before_channel_id = g_clients[delete_socket_id].channel_id
        log.test('before_channel_id', before_channel_id)

        delete g_channels[before_channel_id].sockets[delete_socket_id]
        if (Object.keys(g_channels[before_channel_id].sockets).length === 0) {
          delete g_channels[before_channel_id]
        }
      }
    }
  },

  /**
   * delete client by socket id
   * 
   * @param {*} delete_socket_id 
   */
  deleteClientBySocketId: delete_socket_id => {
    delete g_clients[delete_socket_id]
  },

  /**
   * delete join client by socket id
   * 
   * @param {*} delete_socket_id 
   */
  deleteJoinClientBySocketId: delete_socket_id => {
    delete g_clients[delete_socket_id].channel_id
    if (g_clients[delete_socket_id].user_type === constants.COMMON.USER_TYPE_BUYER) {
      delete g_clients[delete_socket_id].seller_no
    } else if (g_clients[delete_socket_id].user_type === constants.COMMON.USER_TYPE_SELLER) {
      delete g_clients[delete_socket_id].buyer_no
    }
  }
}

export default socketUtils

 

Services

services/socket-service.js

import moment from 'moment'

import log from '../lib/log.js'
import socketUtils from '../lib/util/socket-utils.js'

moment.locale('ko')

const socketService = {
  /**
   * connect
   * 
   * @param {*} socket 
   * @param {*} params 
   */
  connect: (socket, params) => {
    const { buyer_no, seller_no, user_type } = params

    socketUtils.addClient(null, user_type, buyer_no, seller_no, socket)

    log.test('channels', g_channels)
    log.test('clients', g_clients)
  },
  /**
   * close
   * 
   * @param {*} socket 
   */
  close: (from_socket_id) => {
    socketUtils.deleteChannelBySocketId(from_socket_id)
    socketUtils.deleteClientBySocketId(from_socket_id)    

    log.test('close from_socket_id', from_socket_id)
    log.test('close channels', g_channels)
    log.test('close clients', g_clients)
  }
}
export default socketService

 

services/chat-service.js

import moment from 'moment'

import constants from '../lib/constants.js'
import log from '../lib/log.js'
import ezpApi from '../api/ezp-api.js'
import socketUtils from '../lib/util/socket-utils.js'

moment.locale('ko')

const chatService = {
  /**
   * is valid user params
   * 
   * @param {*} from_socket_id 
   * @param {*} params 
   * @returns 
   */
  isValidUserParams: (from_socket_id, params) => {
    if (Object.keys(g_clients).includes(from_socket_id) === true) {
      const from_client = g_clients[from_socket_id]

      if (from_client.user_type === constants.COMMON.USER_TYPE_BUYER && from_client.user_type === params.user_type && from_client.buyer_no === params.buyer_no) {
        return true
      } else if (from_client.user_type === constants.COMMON.USER_TYPE_SELLER && from_client.user_type === params.user_type && from_client.seller_no === params.seller_no) {
        return true
      } else {
        return false
      }
    }

    return true
  },
  /**
   * is valid channel  params
   * @param {*} from_socket_id 
   * @param {*} params 
   * @returns 
   */
  isValidChannelParams: (from_socket_id, params) => {
    if (Object.keys(g_clients).includes(from_socket_id) === true) {
      const from_client = g_clients[from_socket_id]

      if (from_client.user_type === params.user_type && from_client.buyer_no === params.buyer_no && from_client.seller_no === params.seller_no) {
        return true
      } else {
        return false
      }
    }

    return false
  },
  /**
   * join
   * 
   * @param {*} socket
   * @param {*} params
   */
  join: (socket, params) => {
    const { user_type, buyer_no, seller_no } = params
    const channel_id = `${buyer_no}-${seller_no}`

    socketUtils.deleteChannelBySocketId(socket.id)
    socketUtils.addChannelAndSocketByChannelId(channel_id, socket)
    socketUtils.addClient(channel_id, user_type, buyer_no, seller_no, socket)

    ezpApi.join({
      user_type: user_type,
      buyer_no: buyer_no,
      seller_no: seller_no,
      success: res => {
        try {
          if (res.meta.code === 200) {
            log.test('SUCCESS res.code', res.meta.code)

            // TOP 새로고침
            const self_clients = socketUtils.getSelfClientsBySocketId(socket.id)
            /*
            socketUtils.sendToClients(self_clients, {
              type: constants.SOCKET.TYPE,
              action: constants.SOCKET.ACTION.REFRESH_EASY_TALK_TOP,
              unread_count: Number(res.response.target_unread_count)
            })
            */

            const self_other_clients = Object.values(self_clients).filter(client => {
              // 자신 제외
              if (client.socket.id !== socket.id) {
                return true
              }
            })

            // 채널 새로고침
            socketUtils.sendToClients(self_other_clients, {
              type: constants.EASYTALK.TYPE,
              action: constants.EASYTALK.ACTION.REFRESH_CHANNEL
            })
          } else {
            log.test('ERROR res.code', res.meta.code)
          }
        } catch (err) {
          return log.error(err);
        }
      }
    })

    log.test('join channels', g_channels)
    log.test('join clients', g_clients)
  },

  /**
   * send
   * 
   * @param {*} socket 
   * @param {*} params 
   */
  send: (from_socket_id, params) => {
    const { buyer_no, seller_no, message } = params
    const send_ts = moment(new Date()).format('YYYY.MM.DD HH:mm:ss')
    const is_join_target = socketUtils.isJoinTargetBySocketId(from_socket_id)

    log.test('is_join_target', is_join_target)

    ezpApi.send({
      ...params, send_ts, is_join_target,
      success: res => {
        try {
          if (res.meta.code === 200) {
            log.test('SUCCESS res.code', res.meta.code)
            /*
            if (is_join_target === 'F') {
              // TOP 새로고침
              const other_clients = socketUtils.getOtherClientsBySocketId(from_socket_id)
              socketUtils.sendToClients(other_clients, {
                type: constants.SOCKET.TYPE,
                action: constants.SOCKET.ACTION.REFRESH_EASY_TALK_TOP,
                unread_count: Number(res.response.target_unread_count)
              })
            }
            */

            // 채널 새로고침
            const all_clients = socketUtils.getAllClientsBySocketId(from_socket_id)
            log.test('all_clients', all_clients);
            socketUtils.sendToClients(all_clients, {
              type: constants.EASYTALK.TYPE,
              action: constants.EASYTALK.ACTION.REFRESH_CHANNEL,
              buyer_no: buyer_no,
              seller_no: seller_no
            })
          } else {
            log.test('ERROR res.code', res.meta.code)
          }
        } catch (err) {
          return log.error(err);
        }
      }
    })

    // 채널 접속자들 대상으로 메세지 전송
    socketUtils.sendToJoinClientsBySocketId(from_socket_id, {
      action: constants.EASYTALK.ACTION.RECEIVE_MESSAGE,
      send_type: constants.COMMON.SEND_TYPE_MESSAGE,
      message: message,
      send_ts: send_ts
    })
  },

  /**
   * exit
   * 
   * @param {*} socket 
   */
  exit: (from_socket_id, is_forever) => {
    const { user_type, buyer_no, seller_no } = g_clients[from_socket_id]

    if (is_forever === true) {
      // 나가기
      ezpApi.exit({
        user_type, buyer_no, seller_no,
        success: res => {
          try {
            log.test('API exit', res)

            // 채널 나가기 - 대상
            const self_clients = socketUtils.getSelfClientsBySocketId(from_socket_id)

            socketUtils.deleteChannelBySocketId(from_socket_id)
            socketUtils.deleteJoinClientBySocketId(from_socket_id)

            // 채널 나가기 - 리로드
            socketUtils.sendToClients(self_clients, {
              type: constants.EASYTALK.TYPE,
              action: constants.EASYTALK.ACTION.RESET_CHANNEL,
              buyer_no: buyer_no,
              seller_no: seller_no
            })

          } catch (err) {
            return log.error(err);
          }
        }
      })
    } else {
      // 뒤로가기
      socketUtils.deleteChannelBySocketId(from_socket_id)
      socketUtils.deleteJoinClientBySocketId(from_socket_id)
    }
  },

  /**
   * 이지톡 - 메세지 조회 및 전송
   * 
   * @param {*} params 
   */
  emitChannelMessage: (params) => {
    const { buyer_no, seller_no, easytalk_message_nos } = params
    const is_join_seller = socketUtils.isJoinChannel(constants.COMMON.USER_TYPE_SELLER, buyer_no, seller_no)

    ezpApi.getEasypickMessages({
      buyer_no, seller_no, easytalk_message_nos, is_join_seller,
      success: res => {
        try {
          if (res.meta.code === 200) {
            log.test('SUCCESS res.code', res.meta.code)

            const join_buyer_clients = socketUtils.getJoinClientsByUserType(constants.COMMON.USER_TYPE_BUYER, buyer_no, seller_no)
            const join_seller_clients = socketUtils.getJoinClientsByUserType(constants.COMMON.USER_TYPE_SELLER, buyer_no, seller_no)

            for (const row of res.response.message_list) {
              let send_params = {
                action: constants.EASYTALK.ACTION.RECEIVE_MESSAGE,
                type: constants.EASYTALK.TYPE,
                send_type: row.send_type,
                message: row.send_message,
                message_json: row.message_json,
                send_ts: row.send_ts
              }

              // 쇼핑몰 전송
              socketUtils.sendToClients(join_buyer_clients, {
                target: constants.SOCKET.TARGET.SELF,
                ...send_params
              })

              // 도매처 전송
              socketUtils.sendToClients(join_seller_clients, {
                target: constants.SOCKET.TARGET.OTHER,
                ...send_params
              })
            }

            // 채널 새로고침
            const all_clients = socketUtils.getAllClientsByUserNo(buyer_no, seller_no)
            socketUtils.sendToClients(all_clients, {
              type: constants.EASYTALK.TYPE,
              action: constants.EASYTALK.ACTION.REFRESH_CHANNEL
            })
          } else {
            log.test('ERROR res.code', res.meta.code)
          }
        } catch (err) {
          return log.error(err);
        }
      }
    })
  },

}
export default chatService

 

Api

api/test-api.js

import crypto from 'crypto'

import log from '../lib/log.js'
import config from '../config/config.js'
import Request from '../lib/request.js'
import constants from '../lib/constants.js'

const ezpApi = {
  /**
   * eazy pick api - headers
   * 
   * @returns 
   */
  getHeaders: () => {
    const timestamp = new Date().getTime()
    const signature = crypto.createHmac('sha256', config.api.signature).update(`0=${config.api.serviceId}&1=${timestamp}`).digest().toString('base64')

    return {
      "Content-Type": "application/json; charset=utf-8",
      "ezp-service-id": config.api.serviceId,
      "ezp-current-timestamp": timestamp,
      "ezp-signature": signature
    }
  },

  /**
   * send message
   * 
   * @param {*} params 
   * @param {*} send_dt 
   * @returns 
   */
  send: ({ user_type, buyer_no, seller_no, message, send_ts, is_join_target, success }) => {

    try {
      const request = new Request();
      request.setOptions({
        host: config.api.host,
        port: config.api.port,
        path: '/api/v2/easytalk/send',
        method: 'POST',
        headers: ezpApi.getHeaders()
      });     

      request.send({
        data: {
          send_type: constants.COMMON.SEND_TYPE_MESSAGE,
          send_user_type: user_type,
          buyer_no: buyer_no,
          seller_no: seller_no,
          send_message: message,
          send_ts: send_ts,
          is_join_target: is_join_target
        },
        ondata: (res) => {
          success(JSON.parse(res))
        }
      })
    } catch (err) {
      return log.error(err);
    }
  },

  /**
   * exit
   * 
   * @param {*} user_type 
   * @param {*} buyer_no 
   * @param {*} seller_no 
   * @returns 
   */
  exit: ({ user_type, buyer_no, seller_no, success }) => {

    try {
      const request = new Request();
      request.setOptions({
        host: config.api.host,
        port: config.api.port,
        path: '/api/v2/easytalk/exit',
        method: 'POST',
        headers: ezpApi.getHeaders()
      });

      request.send({
        data: {
          user_type: user_type,
          buyer_no: buyer_no,
          seller_no: seller_no,
        },
        ondata: (res) => {
          success(JSON.parse(res))
        }
      })
    } catch (err) {
      return log.error(err);
    }
  },

  /**
   * join
   * 
   * @param {*} param0 
   * @returns 
   */
  join: ({ user_type, buyer_no, seller_no, success }) => {

    try {
      const request = new Request();
      request.setOptions({
        host: config.api.host,
        port: config.api.port,
        path: '/api/v2/easytalk/join',
        method: 'POST',
        headers: ezpApi.getHeaders()
      });

      request.send({
        data: {
          user_type: user_type,
          buyer_no: buyer_no,
          seller_no: seller_no
        },
        ondata: (res) => {
          success(JSON.parse(res))
        }
      })
    } catch (err) {
      return log.error(err);
    }
  },

  /**
   * 이지톡 - 메세지 조회
   * 
   * @param {*} param0 
   * @returns 
   */
  getEasypickMessages: ({ buyer_no, seller_no, easytalk_message_nos, is_join_seller, success }) => {

    try {
      const request = new Request();
      request.setOptions({
        host: config.api.host,
        port: config.api.port,
        path: '/api/v2/easytalk/getEasypickMessages',
        method: 'POST',
        headers: ezpApi.getHeaders()
      });

      request.send({
        data: {
          buyer_no: buyer_no,
          seller_no: seller_no,
          is_join_seller: is_join_seller,
          easytalk_message_nos: easytalk_message_nos
        },
        ondata: (res) => {
          success(JSON.parse(res))
        }
      })
    } catch (err) {
      return log.error(err);
    }
  }
}

export default ezpApi

 

Docker

docker-build.sh

#!/usr/bin/env bash
docker build -t image-ezp-node:0.1 -f $PWD/docker/Dockerfile .

 

docker-run.sh

#!/usr/bin/env bash
HOSTNAME=`hostname -s`

run_script() {
    MODE='local'

    if [[ $HOSTNAME =~ ^test-dev ]]; then
        MODE='dev'
    elif [[ $HOSTNAME =~ ^test-qa ]]; then
        MODE='qa'
    elif [[ $HOSTNAME =~ ^test- || $HOSTNAME =~ ^ezp- ]]; then
        MODE='prd'
    elif [[ $HOSTNAME =~ ^test-demo ]]; then
        MODE='demo'
    fi

    if [[ $MODE == 'local' ]]; then        
        docker run --name server-ezp-node -d \
        -p 10002:10002 \
        -v /home/djkang/project-src/ezp-node/src:/home/ezp-node/src \
        --env MODE=$MODE \
        --env HOST_NAME=$HOSTNAME \
        image-ezp-node:0.1 sh /start-node.sh
    else
        docker run --name server-ezp-node -u 1000 -d \
        -p 10002:10002 \
        -v /home/mes-docker/ezp-node/src:/home/ezp-node/src \
        --read-only \
        --env MODE=$MODE \
        --env HOST_NAME=$HOSTNAME \
        --restart=always \
        image-ezp-node:0.1 sh /start-node.sh
    fi
}

docker stop server-ezp-node
docker rm server-ezp-node
run_script

 

docker/Dockerfile

FROM node:16-alpine

WORKDIR /home/ezp-node

COPY ./yarn* ./package.json ./
COPY ./docker/start-node.sh /

ENV TZ=Asia/Seoul

# RUN yarn
RUN yarn install &&\
    chmod +x /start-node.sh

 

docker/start-node.sh

#!/usr/bin/env bash

if [ $MODE = 'local' ]; then
  getent hosts host.docker.internal | awk '{ print $1  "  test.aaaa.com" }' >> /etc/hosts

  # yarn --cwd /home/djkang/project-src/ezp-node start:dev
  yarn start:local
else
  cd /home/ezp-node/
  node src/server.js
fi