From 4fe22e0bacc8997934b2d823eda0991683bcf604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=AC=EC=9E=AC=EC=97=BD?= <jysim0326@ajou.ac.kr> Date: Thu, 28 Nov 2024 22:01:15 +0900 Subject: [PATCH] =?UTF-8?q?feat,=20refactor:=20=EB=B2=88=EA=B0=9C=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC(fcm)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EB=A9=94=EC=8B=9C=EC=A7=80=ED=81=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pushServer.js | 108 ++++++++++++++++++++++++++++++++ schemas/ChatRoomParticipants.js | 23 ------- schemas/Messages.js | 23 ------- services/chatService.js | 2 +- services/meetingService.js | 70 +++++++++++++++++---- wsServer.js | 65 ++++++++----------- 6 files changed, 194 insertions(+), 97 deletions(-) create mode 100644 pushServer.js delete mode 100644 schemas/ChatRoomParticipants.js delete mode 100644 schemas/Messages.js diff --git a/pushServer.js b/pushServer.js new file mode 100644 index 0000000..76d593d --- /dev/null +++ b/pushServer.js @@ -0,0 +1,108 @@ +const amqp = require('amqplib'); +const admin = require('firebase-admin'); +const dotenv = require('dotenv'); + +// .env 파일 로드 +dotenv.config(); + +// Firebase Admin SDK 초기화 +admin.initializeApp({ + credential: admin.credential.cert(require(process.env.FIREBASE_CREDENTIAL_PATH)), +}); + +// RabbitMQ에서 메시지를 소비하고 FCM 알림 전송 +async function startPushServer() { + const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost'); + const channel = await connection.createChannel(); + + const chatQueue = 'chat_push_notifications'; + const meetingQueue = 'meeting_push_notifications'; + + await channel.assertQueue(chatQueue, { durable: true }); + await channel.assertQueue(meetingQueue, { durable: true }); + + console.log(`푸시 서버가 큐 ${chatQueue} 및 ${meetingQueue}에서 메시지를 기다리고 있습니다.`); + + // Chat Push 처리 + channel.consume(chatQueue, async (msg) => { + if (msg !== null) { + const event = JSON.parse(msg.content.toString()); + const { chatRoomName, sender, messageContent, offlineParticipants, chatRoomId } = event; + + console.log('Chat 푸시 알림 요청 수신:', event); + + + for (const participant of offlineParticipants) { + const tokens = participant.fcmTokens || []; + if (tokens.length > 0) { + const message = { + tokens, + notification: { + title: `${chatRoomName}`, + body: `${sender}: ${messageContent}`, + }, + data: { + click_action: `http://localhost:3000/chat/chatRoom/${chatRoomId}`, // 클릭 시 이동할 URL + }, + android: { priority: 'high' }, + apns: { payload: { aps: { sound: 'default' } } }, + }; + + try { + const response = await admin.messaging().sendEachForMulticast(message); + response.responses.forEach((res, index) => { + if (!res.success) { + console.error(`Chat 푸시 알림 실패 - ${tokens[index]}:`, res.error); + } else { + console.log(`Chat 푸시 알림 성공 - ${tokens[index]}`); + } + }); + } catch (error) { + console.error(`Chat 푸시 알림 전송 오류:`, error); + } + } + } + + channel.ack(msg); + } + }); + + // Meeting Push 처리 + channel.consume(meetingQueue, async (msg) => { + if (msg !== null) { + const event = JSON.parse(msg.content.toString()); + const { meetingTitle, inviterName, inviteeTokens } = event; + + console.log('Meeting 푸시 알림 요청 수신:', event); + console.log("푸시 알림 보내는 fcmToken", inviteeTokens); + + if (inviteeTokens.length > 0) { + const message = { + tokens: inviteeTokens, + notification: { + title: '번개 모임 초대', + body: `${inviterName}님이 ${meetingTitle} 모임에 초대했습니다.`, + }, + data: { + click_action: `http://localhost:3000`, // 클릭 시 이동할 URL + }, + android: { priority: 'high' }, + apns: { payload: { aps: { sound: 'default' } } }, + }; + + try { + const response = await admin.messaging().sendEachForMulticast(message); + console.log(`Meeting 푸시 알림 전송 성공:`, response.successCount); + } catch (error) { + console.error(`Meeting 푸시 알림 전송 실패:`, error); + } + } + + channel.ack(msg); + } + }); +} + +startPushServer().catch((error) => { + console.error('푸시 서버 시작 실패:', error); +}); \ No newline at end of file diff --git a/schemas/ChatRoomParticipants.js b/schemas/ChatRoomParticipants.js deleted file mode 100644 index 09279d1..0000000 --- a/schemas/ChatRoomParticipants.js +++ /dev/null @@ -1,23 +0,0 @@ -// schemas/ChatRoomParticipant.js - -const mongoose = require('mongoose'); - -const ChatRoomParticipantSchema = new mongoose.Schema({ - chat_room_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'ChatRoom', - required: true, - }, - user_id: { - type: Number, // SQL의 Users 테이블 ID 참조 - required: true, - }, - left_at: { - type: Date, - default: null, - }, -}, { - timestamps: { createdAt: 'joined_at', updatedAt: false }, -}); - -module.exports = mongoose.model('ChatRoomParticipant', ChatRoomParticipantSchema); diff --git a/schemas/Messages.js b/schemas/Messages.js deleted file mode 100644 index 7c59dcf..0000000 --- a/schemas/Messages.js +++ /dev/null @@ -1,23 +0,0 @@ -// schemas/Message.js - -const mongoose = require('mongoose'); - -const MessageSchema = new mongoose.Schema({ - chat_room_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'ChatRoom', - required: true, - }, - sender_id: { - type: Number, // SQL의 Users 테이블 ID 참조 - required: true, - }, - message: { - type: String, - required: true, - }, -}, { - timestamps: { createdAt: 'sent_at', updatedAt: false }, -}); - -module.exports = mongoose.model('Message', MessageSchema); diff --git a/services/chatService.js b/services/chatService.js index 8a47e9b..0d462a5 100644 --- a/services/chatService.js +++ b/services/chatService.js @@ -1,4 +1,4 @@ -const ChatRooms = require('../schemas/ChatRooms'); +const ChatRooms = require('../schemas/chatRooms'); const { v4: uuidv4 } = require('uuid'); class ChatService { diff --git a/services/meetingService.js b/services/meetingService.js index 573d41e..0c04014 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -6,15 +6,26 @@ const { v4: uuidv4 } = require('uuid'); const { Op } = require('sequelize'); const sequelize = require('../config/sequelize'); // 트랜잭션 관리를 위해 sequelize 인스턴스 필요 -const { Meeting, MeetingParticipant, User, Schedule, Invite, Friend } = require('../models'); -const ChatRooms = require('../schemas/ChatRooms'); +const { Meeting, MeetingParticipant, User, Schedule, Invite, Friend, FcmToken } = require('../models'); +const ChatRooms = require('../schemas/chatRooms'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const ScheduleService = require('./scheduleService'); // ScheduleService 임포트 const chatService = require('./chatService'); +const amqp = require('amqplib'); // RabbitMQ 연결 class MeetingService { + + async 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); // 연결 닫기 + } + /** * 현재 시간을 time_idx로 변환하는 유틸리티 함수 * 월요일부터 일요일까지 15분 단위로 타임 인덱스를 할당 @@ -31,6 +42,16 @@ class MeetingService { return totalIdx; } + async sendMeetingPushNotificationRequest(meetingTitle, inviterName, inviteeTokens) { + const event = { + meetingTitle, + inviterName, + inviteeTokens, + }; + await this.publishToQueue('meeting_push_notifications', event); // meeting_push_notifications 큐에 메시지 발행 + } + + async createMeeting(meetingData) { const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); createMeetingDTO.validate(); @@ -49,6 +70,7 @@ class MeetingService { // 사용자와 FCM 토큰 조회 const user = await this._findUserWithFcmTokens(created_by); + console.log("user", user); const userFcmTokens = user.fcmTokenList.map((fcmToken) => fcmToken.token); @@ -121,15 +143,21 @@ class MeetingService { time_idx_start, time_idx_end, }, transaction); - await ScheduleService.createSchedule({ - userId: created_by, - title: `번개 모임: ${title}`, - start_time: new Date(start_time), - end_time: new Date(end_time), - is_fixed: true, - }); - const chatRoom = await ChatRoom.findOne({ chatRoomId: chatRoomId }); + // 친구 목록에서 FCM 토큰 추출 + const inviteeTokens = await FcmToken.findAll({ + where: { + userId: { [Op.in]: invitedFriendIds }, + }, + attributes: ['token'], + }).then(tokens => tokens.map(token => token.token)); + + // RabbitMQ 메시지 발행 (푸시 알림 요청) + if (inviteeTokens.length > 0) { + await this.sendMeetingPushNotificationRequest(title, user.name, inviteeTokens); + } + + const chatRoom = await ChatRooms.findOne({ chatRoomId: chatRoomId }); if (chatRoom) { console.log("채팅방 찾음"); @@ -261,13 +289,31 @@ class MeetingService { // 채팅방 참가 (MongoDB) const user = await User.findOne({ where: { id: userId }, - transaction, + include: [ + { + model: FcmToken, + as: 'fcmTokenList', // FCM 토큰 가져오기 + attributes: ['token'], + }, + ], + transaction }); + + const userFcmTokens = user.fcmTokenList.map((token) => token.token); + const chatRoom = await ChatRooms.findOne({ chatRoomId: meeting.chatRoomId, }); + + console.log("여기까지"); + console.log("user.name", user.name); + console.log("참가하는 유저 fcm", userFcmTokens); if (chatRoom && !chatRoom.participants.includes(user.name)) { - chatRoom.participants.push(user.name); + // 참가자 추가 + chatRoom.participants.push({ + name: user.name, + fcmTokens: userFcmTokens, // FCM 토큰 추가 + }); chatRoom.isOnline.set(user.name, true); chatRoom.lastReadAt.set(user.name, new Date()); chatRoom.lastReadLogId.set(user.name, null); diff --git a/wsServer.js b/wsServer.js index 12ef05a..e938ef8 100644 --- a/wsServer.js +++ b/wsServer.js @@ -4,7 +4,8 @@ const crypto = require('crypto'); const mongoose = require('mongoose'); const admin = require('firebase-admin'); const dotenv = require('dotenv'); -const ChatRoom = require('./models/chatRooms'); +const amqp = require('amqplib'); // RabbitMQ 연결 +const ChatRoom = require('./schemas/chatRooms'); // .env 파일 로드 dotenv.config(); @@ -38,6 +39,28 @@ async function connectMongoDB() { } } +// 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); // 연결 닫기 +} + +// RabbitMQ를 통해 푸시 알림 요청을 전송하는 함수 +async function sendPushNotificationRequest(chatRoomName, sender, messageContent, offlineParticipants, chatRoomId) { + const event = { + chatRoomName, + sender, + messageContent, + offlineParticipants, + chatRoomId, + }; + await publishToQueue('chat_push_notifications', event); // push_notifications 큐에 메시지 발행 +} + // 채팅방 기록 불러오기 함수 async function getChatHistory(chatRoomId) { const chatRoom = await ChatRoom.findOne({ chatRoomId }); @@ -207,44 +230,10 @@ function startWebSocketServer() { return isOnline === false; // 정확히 false인 사용자만 필터링 }); + console.log("offlineParticipants", offlineParticipants); - for (const participant of offlineParticipants) { - const tokens = participant.fcmTokens || []; - // console("푸시 알림 보내는 토큰", tokens); - if (tokens.length > 0) { - const message = { - tokens, // FCM 토큰 배열 - notification: { - title: `${chatRoom.chatRoomName}`, - body: `${nickname}: ${text}`, - }, - data: { - key1: 'value1', - key2: 'value2', - }, - android: { - priority: 'high', - }, - apns: { - payload: { - aps: { - sound: 'default', - }, - }, - }, - }; - - try { - console.log(`푸시 알림 전송 중 (${participant.name}):`, message); // 디버깅 로그 추가 - const response = await admin.messaging().sendEachForMulticast(message); - console.log(`푸시 알림 전송 성공 (${participant.name}):`, response.successCount); - } catch (error) { - console.error(`푸시 알림 전송 실패 (${participant.name}):`, error); - } - } else { - console.log(`사용자 ${participant.name}의 FCM 토큰이 없습니다.`); - } - } + // RabbitMQ에 푸시 알림 요청 발행 + await sendPushNotificationRequest(chatRoom.chatRoomName, clientNickname, text, offlineParticipants, chatRoomId); } catch (err) { console.error('MongoDB 채팅 메시지 저장 오류:', err); } -- GitLab