diff --git a/app.js b/app.js index 0bbd702a7db8f09349bd753fe2d2b361e64d3068..c01d3400fd1c770ad31aca180ffd61d614f7891b 100644 --- a/app.js +++ b/app.js @@ -16,13 +16,14 @@ const app = express(); app.use(morgan('dev')); //濡쒓퉭�� -// CORS �ㅼ젙 + +// CORS �ㅼ젙 (濡쒖뺄 �섍꼍��) app.use( cors({ origin:[ process.env.FROENT_URL,'https://yanawa.shop'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true, + credentials: true, }) ); // @@ -59,10 +60,10 @@ console.log('MongoDB URI:', process.env.MONGO_URI); const authRoutes = require('./routes/auth'); app.use('/api/auth', authRoutes); -const scheduleRoutes = require('./routes/schedule'); +const scheduleRoutes = require('./routes/scheduleRoute'); app.use('/api/schedule', scheduleRoutes); -const friendRoutes = require('./routes/friend'); +const friendRoutes = require('./routes/friendRoute'); app.use('/api/friend', friendRoutes); const meetingRoutes = require('./routes/meetingRoute'); @@ -74,7 +75,7 @@ app.use('/api/chat', chatRoutes); const memberRoutes = require('./routes/memberRoute'); app.use('/api/member', memberRoutes); -const sessionRouter = require('./routes/session'); +const sessionRouter = require('./routes/sessionRoute'); app.use('/api/session', sessionRouter); // �ㅼ�以� �대━�� 珥덇린�� diff --git a/controllers/chatController.js b/controllers/chatController.js index 41a2d3c6f05d4f021964785d98414452322050ec..bfcca38df3a7ab0b513d3c4bee2c46533226b09b 100644 --- a/controllers/chatController.js +++ b/controllers/chatController.js @@ -91,4 +91,67 @@ exports.updateStatusAndLogId = async (req, res) => { console.error('Error updating user status and lastReadLogId:', err); res.status(500).json({ error: 'Failed to update user status and lastReadLogId' }); } +}; + +// 怨듭� �깅줉 +exports.addNotice = async (req, res) => { + const { chatRoomId } = req.params; + const { sender, message } = req.body; + + try { + const notice = await chatService.addNotice(chatRoomId, sender, message); + res.status(200).json(notice); + } catch (error) { + console.error('Error adding notice:', error.message); + res.status(500).json({ error: 'Failed to add notice' }); + } +}; + +// 理쒖떊 怨듭� 議고쉶 +exports.getLatestNotice = async (req, res) => { + const { chatRoomId } = req.params; + + try { + const latestNotice = await chatService.getLatestNotice(chatRoomId); + if (latestNotice) { + res.status(200).json(latestNotice); + } else { + res.status(404).json({ message: 'No notices found' }); + } + } catch (error) { + console.error('Error fetching latest notice:', error.message); + res.status(500).json({ error: 'Failed to fetch latest notice' }); + } +}; + +// 怨듭� �꾩껜 議고쉶 +exports.getAllNotices = async (req, res) => { + const { chatRoomId } = req.params; + + try { + const notices = await chatService.getAllNotices(chatRoomId); + console.log(`[getAllNotices] Notices for chatRoomId ${chatRoomId}:`, notices); // 濡쒓렇 異붽� + res.status(200).json(notices); + } catch (error) { + console.error('Error fetching all notices:', error.message); + res.status(500).json({ error: 'Failed to fetch all notices' }); + } +}; + +// 怨듭��ы빆 �곸꽭 議고쉶 +exports.getNoticeById = async (req, res) => { + const { chatRoomId, noticeId } = req.params; + + try { + const notice = await chatService.getNoticeById(chatRoomId, noticeId); + + if (!notice) { + return res.status(404).json({ error: 'Notice not found' }); + } + + res.status(200).json(notice); + } catch (error) { + console.error('Error fetching notice by ID:', error.message); + res.status(500).json({ error: 'Failed to fetch notice by ID' }); + } }; \ No newline at end of file diff --git a/middlewares/auth.js b/middlewares/auth.js index 52eb397476e16fa8c98e7d2e29a15973d76215bc..10118d7670cc4e393aa23600d4d659668bd8c606 100644 --- a/middlewares/auth.js +++ b/middlewares/auth.js @@ -1,4 +1,5 @@ // middlewares/auth.js +exports.isLoggedIn = (req, res, next) => { // 濡쒓렇�몃맂 �ъ슜�먮쭔 �묎렐 �덉슜 exports.isLoggedIn = (req, res, next) => { // 濡쒓렇�몃맂 �ъ슜�먮쭔 �묎렐 �덉슜 if (req.isAuthenticated()) { return next(); diff --git a/models/index.js b/models/index.js index e76f6335b2a0bccafc4faf8546995a203e7de6ee..7caa67277dc86e3f1dcc043a15e8897cbc7ee641 100644 --- a/models/index.js +++ b/models/index.js @@ -36,7 +36,7 @@ User.hasMany(Friend, { Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator', - onDelete: 'SET NULL', // Meetings might persist even if the creator is deleted + onDelete: 'SET NULL', }); User.hasMany(Meeting, { foreignKey: 'created_by', diff --git a/passport/googleStrategy.js b/passport/googleStrategy.js index 6926deeb8ef2f6809d99e1f11b264a2ba08d9959..f6698b36fab68e39e1d5b078802a20e197594797 100644 --- a/passport/googleStrategy.js +++ b/passport/googleStrategy.js @@ -1,12 +1,13 @@ // passport/googleStrategy.js const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); -const User = require('../models/user'); // �ъ슜�� 紐⑤뜽�� 媛��몄샃�덈떎. +const User = require('../models/user'); module.exports = new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.CALLBACK_URL, + passReqToCallback: true, // req 媛앹껜瑜� 肄쒕갚�� �꾨떖 }, async (req, accessToken, refreshToken, profile, done) => { try { diff --git a/routes/authRoute.js b/routes/authRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..e6f2d553e56f361aec5c9f6ca82d664c1b5034ff --- /dev/null +++ b/routes/authRoute.js @@ -0,0 +1,84 @@ + const express = require('express'); + const passport = require('passport'); + + const router = express.Router(); + + // Google OAuth 濡쒓렇�� �쇱슦�� + router.get( + '/login', + passport.authenticate('google', { + scope: ['profile', 'email'], // �ъ슜�� �뺣낫 �붿껌�� �꾪븳 scope + failureRedirect: `${process.env.FRONT_URL}/login` + }) + ); + + router.get( + '/google/callback', + passport.authenticate('google', { + failureRedirect: `${process.env.FRONT_URL}/login` + }), + (req, res) => { + const redirectUrl = process.env.FRONT_URL; + req.session.save((err) => { + if (err) { + console.error('�몄뀡 ���� �ㅻ쪟:', err); + return res.status(500).json({ error: '�쒕쾭 �ㅻ쪟' }); + } + res.redirect(redirectUrl); + }); + } + ); + + // 濡쒓렇�꾩썐 �쇱슦�� + router.get('/logout', (req, res) => { + if (req.session) { + req.session.destroy((err) => { + if (err) { + console.error('�몄뀡 ��젣 �ㅻ쪟:', err); + return res.status(500).json({ error: '�쒕쾭 �ㅻ쪟' }); + } + const redirectUrl = process.env.FRONT_URL; + res.redirect(redirectUrl); + }); + } else { + // �몄뀡�� �녿뒗 寃쎌슦�먮룄 由щ떎�대젆�� + const redirectUrl = process.env.FRONT_URL; + res.redirect(redirectUrl); + } + }); + + // �ъ슜�� ��젣 �쇱슦�� +router.delete('/leave', async (req, res) => { + try { + // �몄쬆�� �ъ슜�� �뺤씤 + if (!req.user) { + return res.status(401).json({ error: '�몄쬆�섏� �딆� �ъ슜�먯엯�덈떎.' }); + } + + const userId = req.user.id; + + // �ъ슜�� ��젣 + const deleted = await User.destroy({ + where: { id: userId } + }); + + if (!deleted) { + return res.status(404).json({ error: '�ъ슜�먮� 李얠쓣 �� �놁뒿�덈떎.' }); + } + + // �몄뀡 ��젣 + req.session.destroy((err) => { + if (err) { + console.error('�몄뀡 ��젣 �ㅻ쪟:', err); + return res.status(500).json({ error: '�쒕쾭 �ㅻ쪟' }); + } + // �깃났 硫붿떆吏� 諛섑솚 (由щ떎�대젆�� ���� JSON �묐떟) + res.status(200).json({ message: '�ъ슜�� 怨꾩젙�� �깃났�곸쑝濡� ��젣�섏뿀�듬땲��.' }); + }); + } catch (error) { + console.error('�ъ슜�� ��젣 �ㅻ쪟:', error); + res.status(500).json({ error: '�쒕쾭 �ㅻ쪟' }); + } +}); + + module.exports = router; \ No newline at end of file diff --git a/routes/chatRoute.js b/routes/chatRoute.js index 6ae03f13f5e429e9c245c289d859400f99ac799a..e7c2769ca7ec5a4cf449694190885d2f19333046 100644 --- a/routes/chatRoute.js +++ b/routes/chatRoute.js @@ -10,5 +10,9 @@ router.get('/unread-messages/:nickname', chatController.getUnreadMessages); router.get('/unread-count/:chatRoomId', chatController.getUnreadCount); router.post('/update-status-and-logid', chatController.updateStatusAndLogId); router.post('/update-read-log-id', chatController.updateReadLogId); +router.post('/:chatRoomId/notices', chatController.addNotice); +router.get('/:chatRoomId/notices/latest', chatController.getLatestNotice); +router.get('/:chatRoomId/notices', chatController.getAllNotices); +router.get('/:chatRoomId/notices/:noticeId', chatController.getNoticeById); module.exports = router; diff --git a/routes/friend.js b/routes/friendRoute.js similarity index 100% rename from routes/friend.js rename to routes/friendRoute.js diff --git a/routes/inviteRoutes.js b/routes/inviteRoute.js similarity index 100% rename from routes/inviteRoutes.js rename to routes/inviteRoute.js diff --git a/routes/schedule.js b/routes/scheduleRoute.js similarity index 100% rename from routes/schedule.js rename to routes/scheduleRoute.js diff --git a/routes/sessionRoute.js b/routes/sessionRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..77a3b118a8050b3167a10494921698230699b5b6 --- /dev/null +++ b/routes/sessionRoute.js @@ -0,0 +1,26 @@ +const express = require('express'); +const router = express.Router(); + +// GET /api/session/info +router.get('/info', (req, res) => { + if (req.user) { + const { email, name } = req.user; + // 罹먯떛 鍮꾪솢�깊솕 + res.set('Cache-Control', 'no-store'); + res.set('Pragma', 'no-cache'); + return res.status(200).json({ + user: { + email, + name, + }, + }); + } + // �몄뀡�� 留뚮즺�섏뿀嫄곕굹 �ъ슜�� �뺣낫媛� �녿뒗 寃쎌슦 + res.set('Cache-Control', 'no-store'); + res.set('Pragma', 'no-cache'); + res.status(401).json({ + message: '�몄뀡�� 留뚮즺�섏뿀嫄곕굹 �ъ슜�� �뺣낫媛� �놁뒿�덈떎.', + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/schemas/chatRooms.js b/schemas/chatRooms.js index 323bb94c06f5487534c8b422990759dca9b752e0..beaba10e774b5036b2206fc0a4152b5fc105f9a6 100644 --- a/schemas/chatRooms.js +++ b/schemas/chatRooms.js @@ -1,7 +1,5 @@ -//schemas/chatRooms.js const mongoose = require('mongoose'); -// MongoDB 梨꾪똿諛� �ㅽ궎留� �섏젙 (FCM �좏겙�� 諛곗뿴濡� 愿�由�) const chatRoomsSchema = new mongoose.Schema({ chatRoomId: { type: String, required: true, unique: true }, chatRoomName: { type: String, required: true }, @@ -18,6 +16,11 @@ const chatRoomsSchema = new mongoose.Schema({ lastReadAt: { type: Map, of: Date }, lastReadLogId: { type: Map, of: String }, isOnline: { type: Map, of: Boolean }, + notices: [{ + sender: { type: String }, + message: { type: String }, + timestamp: { type: Date, default: Date.now }, + }] }, { collection: 'chatrooms' }); const ChatRooms = mongoose.models.ChatRooms || mongoose.model('ChatRooms', chatRoomsSchema); diff --git a/services/chatService.js b/services/chatService.js index 0d462a5da57a7d0ce940cbafa7dbe7eee295828b..f67ee428d4efc761df9dd467600ec25eb93e4cbc 100644 --- a/services/chatService.js +++ b/services/chatService.js @@ -209,6 +209,87 @@ class ChatService { } } + // 怨듭��ы빆 異붽� + async addNotice(chatRoomId, sender, message) { + try { + const newNotice = { + sender, + message, + timestamp: new Date(), + }; + + const updatedChatRoom = await ChatRooms.findOneAndUpdate( + { chatRoomId }, + { $push: { notices: newNotice } }, // 怨듭��ы빆 諛곗뿴�� 異붽� + { new: true } + ); + + if (!updatedChatRoom) { + throw new Error('Chat room not found'); + } + + return newNotice; + } catch (error) { + console.error('Error adding notice:', error.message); + throw new Error('Failed to add notice'); + } + } + + // 理쒖떊 怨듭��ы빆 議고쉶 + async getLatestNotice(chatRoomId) { + try { + const chatRoom = await ChatRooms.findOne( + { chatRoomId }, + { notices: { $slice: -1 } } // 理쒖떊 怨듭� 1媛쒕쭔 媛��몄삤湲� + ); + + if (!chatRoom || chatRoom.notices.length === 0) { + return null; + } + + return chatRoom.notices[0]; + } catch (error) { + console.error('Error fetching latest notice:', error.message); + throw new Error('Failed to fetch latest notice'); + } + } + + // 怨듭��ы빆 �꾩껜 議고쉶 + async getAllNotices(chatRoomId) { + try { + const chatRoom = await ChatRooms.findOne({ chatRoomId }, { notices: 1 }); + + if (!chatRoom) { + throw new Error('Chat room not found'); + } + + return chatRoom.notices; + } catch (error) { + console.error('Error fetching all notices:', error.message); + throw new Error('Failed to fetch all notices'); + } + } + + // 怨듭��ы빆 �곸꽭 議고쉶 + async getNoticeById(chatRoomId, noticeId) { + try { + const chatRoom = await ChatRooms.findOne({ chatRoomId }); + if (!chatRoom) { + throw new Error('Chat room not found'); + } + + const notice = chatRoom.notices.find(notice => notice._id.toString() === noticeId); + if (!notice) { + throw new Error('Notice not found'); + } + + return notice; + } catch (error) { + console.error('Error in getNoticeById:', error.message); + throw error; + } + } + } module.exports = new ChatService(); \ No newline at end of file diff --git a/wsServer.js b/wsServer.js index 16e8bf651815e0d76f1d433cd89124ca49e3d417..26b8d3e59dd7cc0bdccbad820f826055c2342777 100644 --- a/wsServer.js +++ b/wsServer.js @@ -5,22 +5,21 @@ const mongoose = require('mongoose'); const admin = require('firebase-admin'); const dotenv = require('dotenv'); const amqp = require('amqplib'); // RabbitMQ �곌껐 -const ChatRoom = require('./schemas/chatRooms'); +const ChatRoom = require('./schemas/ChatRooms'); // .env �뚯씪 濡쒕뱶 dotenv.config(); -// �쒕퉬�� 怨꾩젙 �� �뚯씪 寃쎈줈瑜� �섍꼍 蹂��섏뿉�� 媛��몄삤湲� -const serviceAccountPath = process.env.FIREBASE_CREDENTIAL_PATH; +const HEARTBEAT_TIMEOUT = 10000; // 10珥� �� ���꾩븘�� -// Firebase Admin SDK 珥덇린�� -admin.initializeApp({ - credential: admin.credential.cert(require(serviceAccountPath)), -}); +// RabbitMQ �곌껐 �� �앹꽦 +let amqpConnection, amqpChannel; // WebSocket 愿��� �곗씠�� let clients = []; -let chatRooms = {}; + +// �대씪�댁뼵�� �곹깭瑜� ���ν븯�� Map +const clientHeartbeats = new Map(); // MongoDB �곌껐 �ㅼ젙 async function connectMongoDB() { @@ -39,14 +38,35 @@ async function connectMongoDB() { } } -// RabbitMQ 硫붿떆吏� 諛쒗뻾 �⑥닔 +// // RabbitMQ 硫붿떆吏� 諛쒗뻾 �⑥닔 +// async function publishToQueue(queue, message) { +// const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost'); +// const channel = await connection.createChannel(); +// await channel.assertQueue(queue, { durable: true }); +// channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); +// console.log(`Message sent to queue ${queue}:`, message); +// setTimeout(() => connection.close(), 500); // �곌껐 �リ린 +// } + +async function setupRabbitMQ() { + try { + amqpConnection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost'); + amqpChannel = await amqpConnection.createChannel(); + console.log('RabbitMQ connection established'); + } catch (err) { + logError('RabbitMQ Setup', err); + process.exit(1); + } +} + async function publishToQueue(queue, message) { - const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost'); - const channel = await connection.createChannel(); - await channel.assertQueue(queue, { durable: true }); - channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); - console.log(`Message sent to queue ${queue}:`, message); - setTimeout(() => connection.close(), 500); // �곌껐 �リ린 + try { + await amqpChannel.assertQueue(queue, { durable: true }); + amqpChannel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + console.log(`Message sent to queue ${queue}:`, message); + } catch (err) { + logError('RabbitMQ Publish', err); + } } // RabbitMQ瑜� �듯빐 �몄떆 �뚮┝ �붿껌�� �꾩넚�섎뒗 �⑥닔 @@ -67,237 +87,355 @@ async function getChatHistory(chatRoomId) { return chatRoom ? chatRoom.messages : []; } -// WebSocket �쒕쾭 �앹꽦 諛� �몃뱶�곗씠�� 泥섎━ function startWebSocketServer() { - const wsServer = http.createServer((req, res) => { + const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('WebSocket server is running'); }); - wsServer.on('upgrade', (req, socket, head) => { - const key = req.headers['sec-websocket-key']; - const acceptKey = generateAcceptValue(key); - const responseHeaders = [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade', - `Sec-WebSocket-Accept: ${acceptKey}` - ]; - socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); - - // �대씪�댁뼵�몃� clients 諛곗뿴�� 異붽� - clients.push(socket); - - let chatRoomId = null; - let nickname = null; - - socket.on('data', async buffer => { - let message; - try { - message = parseMessage(buffer); - const parsedData = JSON.parse(message); - const { type, chatRoomId: clientChatRoomId, nickname: clientNickname, text } = parsedData; - - console.log('�쒕쾭�먯꽌 �섏떊�� 硫붿떆吏�:', { type, clientChatRoomId, clientNickname, text }); - - if (type === 'join' || type === 'leave') { - await ChatRoom.updateOne( - { chatRoomId: clientChatRoomId }, - { $set: { [`isOnline.${clientNickname}`]: type === 'join' } } - ); - - const statusMessage = { - type: 'status', - chatRoomId: clientChatRoomId, - nickname: clientNickname, - isOnline: type === 'join', - }; - - clients.forEach(client => { - client.write(constructReply(JSON.stringify(statusMessage))); - }); - } - - if (type === 'join') { - chatRoomId = clientChatRoomId; - nickname = clientNickname; - - await ChatRoom.updateOne( - { chatRoomId }, - { - $set: { - [`isOnline.${nickname}`]: true, - [`lastReadLogId.${nickname}`]: null, - }, - } - ); - - if (!chatRooms[chatRoomId]) { - chatRooms[chatRoomId] = []; - } - - const chatRoom = await ChatRoom.findOne({ chatRoomId }); - - // 李멸��� �뺤씤 - const participantIndex = chatRoom.participants.findIndex(participant => participant.name === nickname); - if (participantIndex !== -1) { - const existingParticipant = chatRoom.participants[participantIndex]; - - // 李멸��� �곹깭 �낅뜲�댄듃 - existingParticipant.isOnline = true; - existingParticipant.lastReadAt = new Date(); - - await chatRoom.save(); - } else { - // �� 李멸��� 異붽� - const joinMessage = { - message: `${nickname}�섏씠 李멸��덉뒿�덈떎.`, - timestamp: new Date(), - type: 'join' - }; - - chatRoom.participants.push({ - name: nickname, - fcmTokens: parsedData.fcmToken ? [parsedData.fcmToken] : [], - lastReadAt: new Date(), - lastReadLogId: null, - isOnline: true, - }); - - chatRoom.messages.push(joinMessage); - - await chatRoom.save(); - - clients.forEach(client => { - client.write(constructReply(JSON.stringify(joinMessage))); - }); - - console.log(`${nickname} �� 李멸��먮줈 異붽�`); - } - - try { - const previousMessages = await getChatHistory(chatRoomId); - if (previousMessages.length > 0) { - socket.write(constructReply(JSON.stringify({ type: 'previousMessages', messages: previousMessages }))); - console.log(`�댁쟾 硫붿떆吏� �꾩넚: ${previousMessages.length}媛�`); - } - } catch (err) { - console.error('�댁쟾 梨꾪똿 湲곕줉 遺덈윭�ㅺ린 以� �ㅻ쪟 諛쒖깮:', err); - } - - } else if (type === 'message') { - const chatMessage = { - message: text, - timestamp: new Date(), - type: 'message', - sender: nickname - }; - - - chatRooms[chatRoomId].push(chatMessage); - - try { - // �덈줈�� 硫붿떆吏�瑜� messages 諛곗뿴�� 異붽� - const updatedChatRoom = await ChatRoom.findOneAndUpdate( - { chatRoomId }, - { $push: { messages: chatMessage } }, - { new: true, fields: { "messages": { $slice: -1 } } } // 留덉�留� 異붽��� 硫붿떆吏�留� 媛��몄샂 - ); - - // 留덉�留됱뿉 異붽��� 硫붿떆吏��� _id瑜� 媛��몄삤湲� - const savedMessage = updatedChatRoom.messages[updatedChatRoom.messages.length - 1]; - - // �덈줈�� 硫붿떆吏� �꾩넚: �대씪�댁뼵�몃줈 硫붿떆吏� 釉뚮줈�쒖틦�ㅽ듃 - const messageData = { - type: 'message', - chatRoomId, - sender: nickname, - message: text, - timestamp: chatMessage.timestamp, - _id: savedMessage._id // ���λ맂 硫붿떆吏��� _id �ъ슜 - }; - - clients.forEach(client => { - client.write(constructReply(JSON.stringify(messageData))); - console.log('梨꾪똿 硫붿떆吏� �꾩넚:', messageData); - }); - - // �ㅽ봽�쇱씤 �ъ슜�먯뿉寃� FCM �몄떆 �뚮┝ �꾩넚 - const chatRoom = await ChatRoom.findOne({ chatRoomId }); - const offlineParticipants = chatRoom.participants.filter(participant => { - // isOnline �곹깭瑜� Map�먯꽌 媛��몄삤湲� - const isOnline = chatRoom.isOnline.get(participant.name); - return isOnline === false; // �뺥솗�� false�� �ъ슜�먮쭔 �꾪꽣留� - }); - - console.log("offlineParticipants", offlineParticipants); - - // RabbitMQ�� �몄떆 �뚮┝ �붿껌 諛쒗뻾 - await sendPushNotificationRequest(chatRoom.chatRoomName, clientNickname, text, offlineParticipants, chatRoomId); - } catch (err) { - console.error('MongoDB 梨꾪똿 硫붿떆吏� ���� �ㅻ쪟:', err); - } - } else if (type === 'leave') { - const leaveMessage = { - message: `${nickname}�섏씠 �댁옣�덉뒿�덈떎.`, - timestamp: new Date(), - type: 'leave' - }; - - chatRooms[chatRoomId].push(leaveMessage); - - await ChatRoom.updateOne( - { chatRoomId }, - { $set: { [`isOnline.${nickname}`]: false } } - ); - - await ChatRoom.updateOne({ chatRoomId }, { - $push: { messages: leaveMessage }, - $pull: { participants: nickname } - }); - - clients.forEach(client => { - client.write(constructReply(JSON.stringify(leaveMessage))); - }); - - clients = clients.filter(client => client !== socket); - } - } catch (err) { - console.error('硫붿떆吏� 泥섎━ 以� �ㅻ쪟 諛쒖깮:', err); - } - }); + server.on('upgrade', (req, socket, head) => { + handleWebSocketUpgrade(req, socket); + }); - socket.on('close', async () => { - if (nickname && chatRoomId) { - await ChatRoom.updateOne( - { chatRoomId }, - { $set: { [`isOnline.${nickname}`]: false } } - ); - - const statusMessage = { - type: 'status', - chatRoomId, - nickname, - isOnline: false, - }; - - clients.forEach(client => { - client.write(constructReply(JSON.stringify(statusMessage))); - }); - } + server.listen(8081, () => { + console.log('WebSocket 梨꾪똿 �쒕쾭媛� 8081 �ы듃�먯꽌 �ㅽ뻾 以묒엯�덈떎.'); + }); +} + +function handleWebSocketUpgrade(req, socket) { + const key = req.headers['sec-websocket-key']; + const acceptKey = generateAcceptValue(key); + const responseHeaders = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${acceptKey}` + ]; + + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); + + // �대씪�댁뼵�몃� clients 諛곗뿴�� 異붽� + clients.push(socket); + + socket.on('data', async buffer => { + try { + message = parseMessage(buffer); + if (!message) return; // 硫붿떆吏�媛� 鍮꾩뼱 �덈뒗 寃쎌슦 臾댁떆 + + const parsedData = JSON.parse(message); + const { type, chatRoomId: clientChatRoomId, nickname: clientNickname, text } = parsedData; + await handleClientMessage(socket, parsedData); + } catch (err) { + console.error('Error processing message:', err); + } + }); + + socket.on('close', async () => { + + console.log(`WebSocket �곌껐�� 醫낅즺�섏뿀�듬땲��: ${socket.nickname}, ${socket.chatRoomId}`); + + // �대씪�댁뼵�� Heartbeat 留듭뿉�� �쒓굅 + clientHeartbeats.delete(socket); + + // �대씪�댁뼵�� 紐⑸줉�먯꽌 �쒓굅 + clients = clients.filter((client) => client !== socket); + + + // �뚯폆 醫낅즺 ��, 李� �リ린 or hidden �뚮Ц�� �대� �⑤씪�� �곹깭 false濡� �� (以묐났 濡쒖쭅 二쇱꽍 泥섎━) + // if (socket.nickname && socket.chatRoomId) { + // await ChatRoom.updateOne( + // { chatRoomId: socket.chatRoomId }, + // { $set: { [`isOnline.${socket.nickname}`]: false } } + // ); + // } + }); + + socket.on('error', (err) => { + console.error(`WebSocket error: ${err}`); + clients = clients.filter((client) => client !== socket); + }); + +} + +// 硫붿떆吏� ���� 泥섎━ +async function handleClientMessage(socket, data) { + const { type, chatRoomId, nickname, text, fcmToken } = data; + + // ���꾩븘�껊맂 �뚯폆 李⑤떒 + if (socket.isTimedOut) { + console.log(`���꾩븘�껊맂 �대씪�댁뼵�몄쓽 �ъ뿰寃곗쓣 李⑤떒: ${nickname}`); + return; + } + + switch (type) { + case 'heartbeat': + // console.log(`Heartbeat received from ${nickname} in room ${chatRoomId}`); + clientHeartbeats.set(socket, Date.now()); + break; + case 'join': + // WebSocket�� �ъ슜�� �뺣낫 ���� + // socket.nickname = nickname; + // socket.chatRoomId = chatRoomId; + await handleJoin(socket, chatRoomId, nickname, fcmToken); + break; + case 'message': + await handleMessage(chatRoomId, nickname, text); + break; + case 'leave': + await handleLeave(chatRoomId, nickname); + break; + case 'notice': + await handleSetNotice(chatRoomId, nickname, text); + break; + default: + console.log(`Unknown message type: ${type}`); + } +} + +// join - 李멸� 硫붿떆吏� +async function handleJoin(socket, chatRoomId, nickname) { + if (socket.isTimedOut) { + console.log(`���꾩븘�껊맂 �대씪�댁뼵�몄쓽 �ъ갭�щ� 李⑤떒: ${nickname}`); + return; + } + + // Set client properties + socket.chatRoomId = chatRoomId; + socket.nickname = nickname; + + console.log(`Client joined room: ${chatRoomId}, nickname: ${nickname}`); + + await ChatRoom.updateOne( + { chatRoomId: chatRoomId }, + { $set: { [`isOnline.${nickname}`]:true } } + ); + + const statusMessage = { + type: 'status', + chatRoomId: chatRoomId, + nickname: nickname, + isOnline: true, + }; + + broadcastMessage(chatRoomId, statusMessage); + + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { + [`isOnline.${nickname}`]: true, + [`lastReadLogId.${nickname}`]: null, + }, + } + ); + + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + + // 李멸��� �뺤씤 + const participantIndex = chatRoom.participants.findIndex(participant => participant.name === nickname); + + if (participantIndex !== -1) { + const existingParticipant = chatRoom.participants[participantIndex]; + + // 李멸��� �곹깭 �낅뜲�댄듃 + existingParticipant.isOnline = true; + existingParticipant.lastReadAt = new Date(); + + await chatRoom.save(); + } else { + // �� 李멸��� 異붽� + const joinMessage = { + message: `${nickname}�섏씠 李멸��덉뒿�덈떎.`, + timestamp: new Date(), + type: 'join' + }; + + chatRoom.participants.push({ + name: nickname, + fcmTokens: parsedData.fcmToken ? [parsedData.fcmToken] : [], + lastReadAt: new Date(), + lastReadLogId: null, + isOnline: true, + }); + + chatRoom.messages.push(joinMessage); + + await chatRoom.save(); + + broadcastMessage(chatRoomId, joinMessage); + + console.log(`${nickname} �� 李멸��먮줈 異붽�`); + } + + const previousMessages = await getChatHistory(chatRoomId); + if (previousMessages.length > 0) { + socket.write(constructReply(JSON.stringify({ type: 'previousMessages', messages: previousMessages }))); + } +} + +// meessage - �쇰컲 硫붿떆吏� +async function handleMessage(chatRoomId, nickname, text) { + const chatMessage = { message: text, timestamp: new Date(), type: 'message', sender: nickname }; + + try { + const updatedChatRoom = await ChatRoom.findOneAndUpdate( + { chatRoomId }, + { $push: { messages: chatMessage } }, + { new: true, fields: { messages: { $slice: -1 } } } + ); + + // 留덉�留됱뿉 異붽��� 硫붿떆吏��� _id瑜� 媛��몄삤湲� + const savedMessage = updatedChatRoom.messages[updatedChatRoom.messages.length - 1]; + + // �덈줈�� 硫붿떆吏� �꾩넚: �대씪�댁뼵�몃줈 硫붿떆吏� 釉뚮줈�쒖틦�ㅽ듃 + const messageData = { + type: 'message', + chatRoomId, + sender: nickname, + message: text, + timestamp: chatMessage.timestamp, + _id: savedMessage._id // ���λ맂 硫붿떆吏��� _id �ъ슜 + }; + + console.log('梨꾪똿�먯꽌 Current clients:', clients.map(client => client.chatRoomId)); + + // broadcastMessage(chatRoomId, messageData); + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(messageData))); + console.log('梨꾪똿 硫붿떆吏� �꾩넚:', messageData); }); - socket.on('error', (err) => { - console.error(`WebSocket error: ${err}`); - clients = clients.filter(client => client !== socket); + // �ㅽ봽�쇱씤 �ъ슜�먯뿉寃� FCM �몄떆 �뚮┝ �꾩넚 + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + const offlineParticipants = chatRoom.participants.filter(participant => { + // isOnline �곹깭瑜� Map�먯꽌 媛��몄삤湲� + const isOnline = chatRoom.isOnline.get(participant.name); + return isOnline === false; // �뺥솗�� false�� �ъ슜�먮쭔 �꾪꽣留� }); + + console.log("offlineParticipants", offlineParticipants); + + // RabbitMQ�� �몄떆 �뚮┝ �붿껌 諛쒗뻾 + await sendPushNotificationRequest(chatRoom.chatRoomName, nickname, text, offlineParticipants, chatRoomId); + + } catch (err) { + console.error('Error saving message to MongoDB:', err); + } +} + +// leave - �댁옣 硫붿떆吏� +async function handleLeave(chatRoomId, nickname) { + await ChatRoom.updateOne( + { chatRoomId: clientChatRoomId }, + { $set: { [`isOnline.${clientNickname}`]: type === 'leave' } } + ); + + const statusMessage = { + type: 'status', + chatRoomId: clientChatRoomId, + nickname: clientNickname, + isOnline: type === 'leave', + }; + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(statusMessage))); }); - wsServer.listen(8081, () => { - console.log('WebSocket 梨꾪똿 �쒕쾭媛� 8081 �ы듃�먯꽌 �ㅽ뻾 以묒엯�덈떎.'); + const leaveMessage = { message: `${nickname} �섏씠 �댁옣�덉뒿�덈떎.`, timestamp: new Date(), type: 'leave' }; + await ChatRoom.updateOne({ chatRoomId }, { $push: { messages: leaveMessage } }); + broadcastMessage(chatRoomId, leaveMessage); +} + +async function handleSetNotice(chatRoomId, sender, message) { + const notice = { + sender, + message, + timestamp: new Date(), + }; + + try { + // MongoDB�� 理쒖떊 怨듭� ���� + await ChatRoom.updateOne( + { chatRoomId }, + { $push: { notices: notice } } + ); + + // 紐⑤뱺 �대씪�댁뼵�몄뿉寃� 怨듭��ы빆 �낅뜲�댄듃 硫붿떆吏� �꾩넚 + const noticeMessage = { + type: 'notice', + chatRoomId, + sender, + message, + }; + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(noticeMessage))); + }); + + // broadcastMessage(chatRoomId, noticeMessage); + + console.log('怨듭��ы빆 �낅뜲�댄듃:', noticeMessage); + } catch (error) { + console.error('怨듭��ы빆 �낅뜲�댄듃 �ㅽ뙣:', error); + } +} + +// Broadcast message to clients in the same chat room +function broadcastMessage(chatRoomId, message) { + clients.forEach((client) => { + if (client.chatRoomId === chatRoomId) { + client.write(constructReply(JSON.stringify(message))); + } }); } +// 二쇨린�곸쑝濡� Heartbeat �곹깭 �뺤씤 +setInterval(async () => { + const now = Date.now(); + for (const [socket, lastHeartbeat] of clientHeartbeats.entries()) { + if (now - lastHeartbeat > HEARTBEAT_TIMEOUT) { + console.log('���꾩븘�� ���� �대씪�댁뼵��:', { + nickname: socket.nickname, + chatRoomId: socket.chatRoomId, + lastHeartbeat: new Date(lastHeartbeat).toISOString(), + }); + + // Heartbeat 留듭뿉�� �쒓굅 + clientHeartbeats.delete(socket); + + // �곹깭 �뚮옒洹� �ㅼ젙 + socket.isTimedOut = true; + + // �뚯폆 �곌껐 醫낅즺 + socket.end(); + + // �대씪�댁뼵�� 紐⑸줉�먯꽌 �쒓굅 + clients = clients.filter((client) => client !== socket); + + // �대씪�댁뼵�몃� �ㅽ봽�쇱씤�쇰줈 �ㅼ젙 + console.log("Client timed out �� �ㅽ봽�쇱씤 �ㅼ젙"); + await ChatRoom.updateOne( + { [`isOnline.${socket.nickname}`]: false }, + { [`lastReadAt.${socket.nickname}`]: new Date() } + ); + + // �대씪�댁뼵�몄뿉寃� �곌껐 醫낅즺 硫붿떆吏� �꾩넚 + const timeoutMessage = JSON.stringify({ + type: 'status', + nickname: socket.nickname, + chatRoomId: socket.chatRoomId, + isOnline: false, + }); + + clients.forEach(client => { + client.write(constructReply(timeoutMessage)); + }); + + + } + } +}, 5000); // 5珥덈쭏�� �곹깭 �뺤씤 + // Sec-WebSocket-Accept �ㅻ뜑 媛� �앹꽦 -> env泥섎━ function generateAcceptValue(key) { return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64'); @@ -305,27 +443,37 @@ function generateAcceptValue(key) { // WebSocket 硫붿떆吏� �뚯떛 �⑥닔 function parseMessage(buffer) { - const byteArray = [...buffer]; - const secondByte = byteArray[1]; - let length = secondByte & 127; - let maskStart = 2; - - if (length === 126) { - length = (byteArray[2] << 8) + byteArray[3]; - maskStart = 4; - } else if (length === 127) { - length = 0; - for (let i = 0; i < 8; i++) { - length = (length << 8) + byteArray[2 + i]; + try { + const byteArray = [...buffer]; + const secondByte = byteArray[1]; + let length = secondByte & 127; + let maskStart = 2; + + if (length === 126) { + length = (byteArray[2] << 8) + byteArray[3]; + maskStart = 4; + } else if (length === 127) { + length = 0; + for (let i = 0; i < 8; i++) { + length = (length << 8) + byteArray[2 + i]; + } + maskStart = 10; } - maskStart = 10; - } - const dataStart = maskStart + 4; - const mask = byteArray.slice(maskStart, dataStart); - const data = byteArray.slice(dataStart, dataStart + length).map((byte, i) => byte ^ mask[i % 4]); + const dataStart = maskStart + 4; + const mask = byteArray.slice(maskStart, dataStart); + const data = byteArray.slice(dataStart, dataStart + length).map((byte, i) => byte ^ mask[i % 4]); + + const decodedMessage = new TextDecoder('utf-8').decode(Uint8Array.from(data)); + + // JSON �좏슚�� 寃��� + JSON.parse(decodedMessage); - return new TextDecoder('utf-8').decode(Uint8Array.from(data)); + return decodedMessage; + } catch (err) { + console.error('Error parsing WebSocket message:', err.message); + return null; // �좏슚�섏� �딆� 硫붿떆吏��� 臾댁떆 + } } // �대씪�댁뼵�� 硫붿떆吏� �묐떟 �앹꽦 �⑥닔 @@ -353,5 +501,8 @@ function constructReply(message) { return Buffer.concat([Buffer.from(reply), messageBuffer]); } +// �쒕쾭 �쒖옉 �� RabbitMQ �ㅼ젙 +setupRabbitMQ(); + // MongoDB �곌껐 �� WebSocket �쒕쾭 �쒖옉 connectMongoDB();