From 8d112178d12db46aa77a56846d65b24acd5ce040 Mon Sep 17 00:00:00 2001 From: x2Skyz Date: Thu, 27 Nov 2025 21:55:02 +0700 Subject: [PATCH] -socket --- exthernal-ttc-api/package.json | 4 +- exthernal-ttc-api/src/app.js | 21 +++- .../src/controllers/socketController.js | 118 ++++++++++++++++++ .../src/services/socketService.js | 32 +++++ exthernal-ttc-api/src/socket/socketManager.js | 80 ++++++++++++ 5 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 exthernal-ttc-api/src/controllers/socketController.js create mode 100644 exthernal-ttc-api/src/services/socketService.js create mode 100644 exthernal-ttc-api/src/socket/socketManager.js diff --git a/exthernal-ttc-api/package.json b/exthernal-ttc-api/package.json index f7f1426..251fc50 100644 --- a/exthernal-ttc-api/package.json +++ b/exthernal-ttc-api/package.json @@ -1,7 +1,7 @@ { - "name": "exthernal-mobile-api", + "name": "exthernal-ttc-api", "version": "1.0.0", - "description": "External Mobile API following Nuttakit Controller Pattern vFinal", + "description": "External TTC API following Nuttakit Controller Pattern vFinal", "type": "module", "main": "src/app.js", "scripts": { diff --git a/exthernal-ttc-api/src/app.js b/exthernal-ttc-api/src/app.js index a483568..d618594 100644 --- a/exthernal-ttc-api/src/app.js +++ b/exthernal-ttc-api/src/app.js @@ -1,8 +1,11 @@ import express from 'express' import cors from 'cors' import dotenv from 'dotenv' +import { createServer } from 'http' // ✅ เพิ่ม +import { Server } from 'socket.io' // ✅ เพิ่ม import router from './routes/route.js' import { globalResponseHandler } from './middlewares/responseHandler.js' +import { SocketManager } from './socket/socketManager.js' // ✅ เพิ่ม Class ที่เราจะสร้าง dotenv.config() @@ -27,6 +30,20 @@ app.use((err, req, res, next) => { app.use('/api/ttc', router) -app.listen(process.env.PORT, () => { - console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`) +// ✅ เปลี่ยนการ Listen เป็น HTTP Server + Socket +const httpServer = createServer(app) +const io = new Server(httpServer, { + cors: { + origin: "*", // ปรับตามความเหมาะสม + methods: ["GET", "POST"] + } }) + +// ✅ เรียกใช้ Socket Manager ตาม Pattern +const socketManager = new SocketManager(io) +socketManager.initialize() + +const PORT = process.env.PORT || 3000 +httpServer.listen(PORT, () => { + console.log(`✅ ${process.env.PJ_NAME} running on port ${PORT} with WebSocket`) +}) \ No newline at end of file diff --git a/exthernal-ttc-api/src/controllers/socketController.js b/exthernal-ttc-api/src/controllers/socketController.js new file mode 100644 index 0000000..ce8582c --- /dev/null +++ b/exthernal-ttc-api/src/controllers/socketController.js @@ -0,0 +1,118 @@ +import { SocketService } from '../services/socketService.js' +import { GeneralService } from '../share/generalservice.js' +import { Interface } from '../interfaces/Interface.js' +// import { sendError } from '../utils/response.js' // Socket ส่ง error กลับคนละแบบ แต่ import ไว้ได้ + +export class SocketController { + + constructor() { + this.generalService = new GeneralService() + this.socketService = new SocketService() + this.Interface = new Interface() + } + + // ========================================================= + // FEATURE: NOTIFICATION + // ========================================================= + async onSendNotification(io, socket, data) { + this.generalService.devhint(1, 'socketController.js', 'onSendNotification() start') + let idx = -1 + let database = socket.organization + + try { + // Data: { targetUserId, title, message, type } + const { targetUserId, title, message, type } = data + + // 1. บันทึกลง Database (ใช้ Interface Pattern ถ้ามี Table รองรับ เช่น 'notimst') + // สมมติว่ามีตาราง notimst + /* + let arysave = { + methods: 'post', + notusrseq: targetUserId, + nottitle: title, + notmsg: message, + notread: 'N', + notdtm: this.socketService.getCurrentDTM() // function ใน service + } + // await this.Interface.saveInterface('notimst', arysave, { headers: { authorization: ... } }) + // *หมายเหตุ: Interface.js ต้องการ req.headers ซึ่ง socket ไม่มี ต้อง Mock หรือแก้ Interface + */ + + // หรือเรียก Service ตรงๆ เพื่อบันทึก + await this.socketService.saveNotificationLog(database, socket.user.id, targetUserId, title, message) + + // 2. ส่ง Realtime หา Target + io.to(targetUserId.toString()).emit('receive_notification', { + from: socket.user.usrnam, + title, + message, + type, + timestamp: new Date() + }) + + this.generalService.devhint(2, 'socketController.js', `Sent notify to ${targetUserId}`) + + } catch (error) { + idx = 1 + console.error(error) + } finally { + if (idx === 1) { + socket.emit('error', { message: 'Failed to send notification' }) + } + } + } + + // ========================================================= + // FEATURE: VOIP (WebRTC Signaling) + // ========================================================= + + // A โทรหา B + async onCallUser(io, socket, data) { + this.generalService.devhint(1, 'socketController.js', 'onCallUser() start') + let idx = -1 + try { + const { userToCall, signalData } = data + + // ส่ง Event 'call_incoming' ไปหาห้องของ userToCall + io.to(userToCall.toString()).emit('call_incoming', { + signal: signalData, + from: socket.user.id, + fromName: socket.user.usrnam + }) + + } catch (error) { + idx = 1 + } finally { + if (idx === 1) socket.emit('error', { message: 'Call failed' }) + } + } + + // B รับสาย A + async onAnswerCall(io, socket, data) { + this.generalService.devhint(1, 'socketController.js', 'onAnswerCall() start') + try { + const { to, signal } = data + io.to(to.toString()).emit('call_accepted', { signal }) + } catch (error) { + console.error('VoIP Error:', error) + } + } + + // แลกเปลี่ยน Network Info (ICE Candidate) + async onIceCandidate(io, socket, data) { + try { + const { targetUserId, candidate } = data + io.to(targetUserId.toString()).emit('receive_ice_candidate', { candidate }) + } catch (error) { + // silent fail for ICE + } + } + + // วางสาย + async onEndCall(io, socket, data) { + const { targetUserId } = data + if(targetUserId) { + io.to(targetUserId.toString()).emit('call_ended', { from: socket.user.id }) + } + } +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/services/socketService.js b/exthernal-ttc-api/src/services/socketService.js new file mode 100644 index 0000000..ee0bd7b --- /dev/null +++ b/exthernal-ttc-api/src/services/socketService.js @@ -0,0 +1,32 @@ +import { GeneralService } from '../share/generalservice.js' +import { getDTM } from '../utils/date.js' + +export class SocketService { + + constructor() { + this.generalService = new GeneralService() + } + + getCurrentDTM() { + return getDTM() + } + + // ตัวอย่างฟังก์ชันบันทึก Notification + async saveNotificationLog(database, fromUserSeq, toUserSeq, title, msg) { + // สมมติว่ามีตาราง comhtr + // ตรวจสอบก่อนว่ามีตารางไหม หรือข้ามไปถ้ายังไม่ได้สร้าง + /* + const sql = ` + INSERT INTO ${database}.comhtr + (from_seq, to_seq, title, message, created_dtm) + VALUES ($1, $2, $3, $4, $5) + ` + const params = [fromUserSeq, toUserSeq, title, msg, getDTM()] + await this.generalService.executeQueryParam(database, sql, params) + */ + + // Demo: แค่ Log ไว้ก่อน + this.generalService.devhint(2, 'SocketService', `Saving Log DB: [${database}] From ${fromUserSeq} to ${toUserSeq}`) + return true + } +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/socket/socketManager.js b/exthernal-ttc-api/src/socket/socketManager.js new file mode 100644 index 0000000..cc93b8b --- /dev/null +++ b/exthernal-ttc-api/src/socket/socketManager.js @@ -0,0 +1,80 @@ +import { verifyToken } from '../utils/token.js' +import { SocketController } from '../controllers/socketController.js' +import { GeneralService } from '../share/generalservice.js' +import redis from '../utils/redis.js' // ใช้ Redis ที่มีเก็บ Session + +export class SocketManager { + constructor(io) { + this.io = io + this.generalService = new GeneralService() + this.socketController = new SocketController() + } + + initialize() { + this.generalService.devhint(1, 'SocketManager.js', 'Initializing Socket.io') + + // Middleware: Authentication (เช็ค Token ก่อน Connect) + this.io.use(async (socket, next) => { + try { + const token = socket.handshake.auth.token || socket.handshake.headers.token + if (!token) return next(new Error('Authentication error')) + + const decoded = verifyToken(token) + if (!decoded) return next(new Error('Invalid Token')) + + // เก็บข้อมูล User เข้า Socket Session + socket.user = decoded + socket.organization = decoded.organization // ใช้สำหรับ Schema + next() + } catch (err) { + next(new Error('Authentication failed')) + } + }) + + this.io.on('connection', async (socket) => { + this.generalService.devhint(1, 'SocketManager.js', `User Connected: ${socket.user.usrnam}`) + + // 1. Save User Session to Redis (Pattern การเก็บ state) + // key: "online:user_id", value: socket_id + await redis.set(`online:${socket.user.id}`, socket.id) + + // Join Room ส่วนตัว (ตาม User ID) + socket.join(socket.user.id.toString()) + + // ========================================== + // Event Handlers (เรียก Controller Pattern) + // ========================================== + + // 1. Send Notification (User ส่งหา User) + socket.on('send_notification', async (data) => { + await this.socketController.onSendNotification(this.io, socket, data) + }) + + // 2. VoIP: Call Request + socket.on('call_user', async (data) => { + await this.socketController.onCallUser(this.io, socket, data) + }) + + // 3. VoIP: Answer Call + socket.on('answer_call', async (data) => { + await this.socketController.onAnswerCall(this.io, socket, data) + }) + + // 4. VoIP: ICE Candidate (Network info) + socket.on('ice_candidate', async (data) => { + await this.socketController.onIceCandidate(this.io, socket, data) + }) + + // 5. VoIP: End Call + socket.on('end_call', async (data) => { + await this.socketController.onEndCall(this.io, socket, data) + }) + + // Disconnect + socket.on('disconnect', async () => { + this.generalService.devhint(1, 'SocketManager.js', `User Disconnected: ${socket.user.usrnam}`) + await redis.del(`online:${socket.user.id}`) + }) + }) + } +} \ No newline at end of file