Skip to content
Snippets Groups Projects
Commit 99eee24b authored by 심재엽's avatar 심재엽
Browse files

[#20] 번개모임 푸시알림(fcm) 추가, 메시지큐 추가

parents d8c72067 4fe22e0b
No related branches found
No related tags found
2 merge requests!31Develop,!30[#20] 번개모임 푸시알림(fcm) 추가, 메시지큐 추가
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
// 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);
// 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);
const ChatRooms = require('../schemas/ChatRooms');
const ChatRooms = require('../schemas/chatRooms');
const { v4: uuidv4 } = require('uuid');
class ChatService {
......
......@@ -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);
......
......@@ -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);
}
......
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