Skip to content
Snippets Groups Projects
Commit b3d9c108 authored by tpgus2603's avatar tpgus2603
Browse files

merge: merge컨플릭트 해결

parents 67c2a54b 2235263b
No related branches found
No related tags found
1 merge request!42[#25] 배포코드 master브랜치로 이동
...@@ -16,13 +16,14 @@ const app = express(); ...@@ -16,13 +16,14 @@ const app = express();
app.use(morgan('dev')); //로깅용 app.use(morgan('dev')); //로깅용
// CORS 설정
// CORS 설정 (로컬 환경용)
app.use( app.use(
cors({ cors({
origin:[ process.env.FROENT_URL,'https://yanawa.shop'], origin:[ process.env.FROENT_URL,'https://yanawa.shop'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'], allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, credentials: true,
}) })
); );
// //
...@@ -59,10 +60,10 @@ console.log('MongoDB URI:', process.env.MONGO_URI); ...@@ -59,10 +60,10 @@ console.log('MongoDB URI:', process.env.MONGO_URI);
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
const scheduleRoutes = require('./routes/schedule'); const scheduleRoutes = require('./routes/scheduleRoute');
app.use('/api/schedule', scheduleRoutes); app.use('/api/schedule', scheduleRoutes);
const friendRoutes = require('./routes/friend'); const friendRoutes = require('./routes/friendRoute');
app.use('/api/friend', friendRoutes); app.use('/api/friend', friendRoutes);
const meetingRoutes = require('./routes/meetingRoute'); const meetingRoutes = require('./routes/meetingRoute');
...@@ -74,7 +75,7 @@ app.use('/api/chat', chatRoutes); ...@@ -74,7 +75,7 @@ app.use('/api/chat', chatRoutes);
const memberRoutes = require('./routes/memberRoute'); const memberRoutes = require('./routes/memberRoute');
app.use('/api/member', memberRoutes); app.use('/api/member', memberRoutes);
const sessionRouter = require('./routes/session'); const sessionRouter = require('./routes/sessionRoute');
app.use('/api/session', sessionRouter); app.use('/api/session', sessionRouter);
// 스케줄 클리너 초기화 // 스케줄 클리너 초기화
......
...@@ -91,4 +91,67 @@ exports.updateStatusAndLogId = async (req, res) => { ...@@ -91,4 +91,67 @@ exports.updateStatusAndLogId = async (req, res) => {
console.error('Error updating user status and lastReadLogId:', err); console.error('Error updating user status and lastReadLogId:', err);
res.status(500).json({ error: 'Failed to update user status and lastReadLogId' }); 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
// middlewares/auth.js // middlewares/auth.js
exports.isLoggedIn = (req, res, next) => { // 로그인된 사용자만 접근 허용
exports.isLoggedIn = (req, res, next) => { // 로그인된 사용자만 접근 허용 exports.isLoggedIn = (req, res, next) => { // 로그인된 사용자만 접근 허용
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
return next(); return next();
......
...@@ -36,7 +36,7 @@ User.hasMany(Friend, { ...@@ -36,7 +36,7 @@ User.hasMany(Friend, {
Meeting.belongsTo(User, { Meeting.belongsTo(User, {
foreignKey: 'created_by', foreignKey: 'created_by',
as: 'creator', as: 'creator',
onDelete: 'SET NULL', // Meetings might persist even if the creator is deleted onDelete: 'SET NULL',
}); });
User.hasMany(Meeting, { User.hasMany(Meeting, {
foreignKey: 'created_by', foreignKey: 'created_by',
......
// passport/googleStrategy.js // passport/googleStrategy.js
const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const User = require('../models/user'); // 사용자 모델을 가져옵니다. const User = require('../models/user');
module.exports = new GoogleStrategy( module.exports = new GoogleStrategy(
{ {
clientID: process.env.GOOGLE_CLIENT_ID, clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL, callbackURL: process.env.CALLBACK_URL,
passReqToCallback: true, // req 객체를 콜백에 전달
}, },
async (req, accessToken, refreshToken, profile, done) => { async (req, accessToken, refreshToken, profile, done) => {
try { try {
......
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
...@@ -10,5 +10,9 @@ router.get('/unread-messages/:nickname', chatController.getUnreadMessages); ...@@ -10,5 +10,9 @@ router.get('/unread-messages/:nickname', chatController.getUnreadMessages);
router.get('/unread-count/:chatRoomId', chatController.getUnreadCount); router.get('/unread-count/:chatRoomId', chatController.getUnreadCount);
router.post('/update-status-and-logid', chatController.updateStatusAndLogId); router.post('/update-status-and-logid', chatController.updateStatusAndLogId);
router.post('/update-read-log-id', chatController.updateReadLogId); 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; module.exports = router;
File moved
File moved
File moved
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
//schemas/chatRooms.js
const mongoose = require('mongoose'); const mongoose = require('mongoose');
// MongoDB 채팅방 스키마 수정 (FCM 토큰을 배열로 관리)
const chatRoomsSchema = new mongoose.Schema({ const chatRoomsSchema = new mongoose.Schema({
chatRoomId: { type: String, required: true, unique: true }, chatRoomId: { type: String, required: true, unique: true },
chatRoomName: { type: String, required: true }, chatRoomName: { type: String, required: true },
...@@ -18,6 +16,11 @@ const chatRoomsSchema = new mongoose.Schema({ ...@@ -18,6 +16,11 @@ const chatRoomsSchema = new mongoose.Schema({
lastReadAt: { type: Map, of: Date }, lastReadAt: { type: Map, of: Date },
lastReadLogId: { type: Map, of: String }, lastReadLogId: { type: Map, of: String },
isOnline: { type: Map, of: Boolean }, isOnline: { type: Map, of: Boolean },
notices: [{
sender: { type: String },
message: { type: String },
timestamp: { type: Date, default: Date.now },
}]
}, { collection: 'chatrooms' }); }, { collection: 'chatrooms' });
const ChatRooms = mongoose.models.ChatRooms || mongoose.model('ChatRooms', chatRoomsSchema); const ChatRooms = mongoose.models.ChatRooms || mongoose.model('ChatRooms', chatRoomsSchema);
......
...@@ -209,6 +209,87 @@ class ChatService { ...@@ -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(); module.exports = new ChatService();
\ No newline at end of file
...@@ -5,22 +5,21 @@ const mongoose = require('mongoose'); ...@@ -5,22 +5,21 @@ const mongoose = require('mongoose');
const admin = require('firebase-admin'); const admin = require('firebase-admin');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const amqp = require('amqplib'); // RabbitMQ 연결 const amqp = require('amqplib'); // RabbitMQ 연결
const ChatRoom = require('./schemas/chatRooms'); const ChatRoom = require('./schemas/ChatRooms');
// .env 파일 로드 // .env 파일 로드
dotenv.config(); dotenv.config();
// 서비스 계정 키 파일 경로를 환경 변수에서 가져오기 const HEARTBEAT_TIMEOUT = 10000; // 10초 후 타임아웃
const serviceAccountPath = process.env.FIREBASE_CREDENTIAL_PATH;
// Firebase Admin SDK 초기화 // RabbitMQ 연결 풀 생성
admin.initializeApp({ let amqpConnection, amqpChannel;
credential: admin.credential.cert(require(serviceAccountPath)),
});
// WebSocket 관련 데이터 // WebSocket 관련 데이터
let clients = []; let clients = [];
let chatRooms = {};
// 클라이언트 상태를 저장하는 Map
const clientHeartbeats = new Map();
// MongoDB 연결 설정 // MongoDB 연결 설정
async function connectMongoDB() { async function connectMongoDB() {
...@@ -39,14 +38,35 @@ 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) { async function publishToQueue(queue, message) {
const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost'); try {
const channel = await connection.createChannel(); await amqpChannel.assertQueue(queue, { durable: true });
await channel.assertQueue(queue, { durable: true }); amqpChannel.sendToQueue(queue, Buffer.from(JSON.stringify(message)));
channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); console.log(`Message sent to queue ${queue}:`, message);
console.log(`Message sent to queue ${queue}:`, message); } catch (err) {
setTimeout(() => connection.close(), 500); // 연결 닫기 logError('RabbitMQ Publish', err);
}
} }
// RabbitMQ를 통해 푸시 알림 요청을 전송하는 함수 // RabbitMQ를 통해 푸시 알림 요청을 전송하는 함수
...@@ -67,237 +87,355 @@ async function getChatHistory(chatRoomId) { ...@@ -67,237 +87,355 @@ async function getChatHistory(chatRoomId) {
return chatRoom ? chatRoom.messages : []; return chatRoom ? chatRoom.messages : [];
} }
// WebSocket 서버 생성 및 핸드셰이크 처리
function startWebSocketServer() { function startWebSocketServer() {
const wsServer = http.createServer((req, res) => { const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' }); res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket server is running'); res.end('WebSocket server is running');
}); });
wsServer.on('upgrade', (req, socket, head) => { server.on('upgrade', (req, socket, head) => {
const key = req.headers['sec-websocket-key']; handleWebSocketUpgrade(req, socket);
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);
}
});
socket.on('close', async () => { server.listen(8081, () => {
if (nickname && chatRoomId) { console.log('WebSocket 채팅 서버가 8081 포트에서 실행 중입니다.');
await ChatRoom.updateOne( });
{ chatRoomId }, }
{ $set: { [`isOnline.${nickname}`]: false } }
); function handleWebSocketUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
const statusMessage = { const acceptKey = generateAcceptValue(key);
type: 'status', const responseHeaders = [
chatRoomId, 'HTTP/1.1 101 Switching Protocols',
nickname, 'Upgrade: websocket',
isOnline: false, 'Connection: Upgrade',
}; `Sec-WebSocket-Accept: ${acceptKey}`
];
clients.forEach(client => {
client.write(constructReply(JSON.stringify(statusMessage))); 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) => { // 오프라인 사용자에게 FCM 푸시 알림 전송
console.error(`WebSocket error: ${err}`); const chatRoom = await ChatRoom.findOne({ chatRoomId });
clients = clients.filter(client => client !== socket); 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, () => { const leaveMessage = { message: `${nickname} 님이 퇴장했습니다.`, timestamp: new Date(), type: 'leave' };
console.log('WebSocket 채팅 서버가 8081 포트에서 실행 중입니다.'); 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처리 // Sec-WebSocket-Accept 헤더 값 생성 -> env처리
function generateAcceptValue(key) { function generateAcceptValue(key) {
return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64'); return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64');
...@@ -305,27 +443,37 @@ function generateAcceptValue(key) { ...@@ -305,27 +443,37 @@ function generateAcceptValue(key) {
// WebSocket 메시지 파싱 함수 // WebSocket 메시지 파싱 함수
function parseMessage(buffer) { function parseMessage(buffer) {
const byteArray = [...buffer]; try {
const secondByte = byteArray[1]; const byteArray = [...buffer];
let length = secondByte & 127; const secondByte = byteArray[1];
let maskStart = 2; let length = secondByte & 127;
let maskStart = 2;
if (length === 126) {
length = (byteArray[2] << 8) + byteArray[3]; if (length === 126) {
maskStart = 4; length = (byteArray[2] << 8) + byteArray[3];
} else if (length === 127) { maskStart = 4;
length = 0; } else if (length === 127) {
for (let i = 0; i < 8; i++) { length = 0;
length = (length << 8) + byteArray[2 + i]; for (let i = 0; i < 8; i++) {
length = (length << 8) + byteArray[2 + i];
}
maskStart = 10;
} }
maskStart = 10;
}
const dataStart = maskStart + 4; const dataStart = maskStart + 4;
const mask = byteArray.slice(maskStart, dataStart); const mask = byteArray.slice(maskStart, dataStart);
const data = byteArray.slice(dataStart, dataStart + length).map((byte, i) => byte ^ mask[i % 4]); 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) { ...@@ -353,5 +501,8 @@ function constructReply(message) {
return Buffer.concat([Buffer.from(reply), messageBuffer]); return Buffer.concat([Buffer.from(reply), messageBuffer]);
} }
// 서버 시작 시 RabbitMQ 설정
setupRabbitMQ();
// MongoDB 연결 후 WebSocket 서버 시작 // MongoDB 연결 후 WebSocket 서버 시작
connectMongoDB(); connectMongoDB();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment