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