From b6dc11f7c673f08e6139db44b0a466a75c4d910a 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: Mon, 25 Nov 2024 17:34:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20fcm=20=ED=86=A0=ED=81=B0=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/memberController.js | 17 +++ models/chatRooms.js | 22 ++-- models/fcmToken.js | 29 ++++++ models/index.js | 2 + routes/memberRoute.js | 8 ++ services/meetingService.js | 178 ++++++++++++++++++++------------ services/memberService.js | 47 +++++++++ 7 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 controllers/memberController.js create mode 100644 models/fcmToken.js create mode 100644 routes/memberRoute.js create mode 100644 services/memberService.js diff --git a/controllers/memberController.js b/controllers/memberController.js new file mode 100644 index 0000000..3ec5af6 --- /dev/null +++ b/controllers/memberController.js @@ -0,0 +1,17 @@ +const MemberService = require('../services/memberService'); + +class MemberController { + async registerToken(req, res) { + const { email, fcmToken } = req.body; + + try { + const result = await MemberService.registerToken(email, fcmToken); + res.status(200).json(result); + } catch (error) { + console.error('Error registering FCM token:', error); + res.status(500).json({ message: error.message || 'Internal server error' }); + } + } +} + +module.exports = new MemberController(); \ No newline at end of file diff --git a/models/chatRooms.js b/models/chatRooms.js index ca68c39..6c50957 100644 --- a/models/chatRooms.js +++ b/models/chatRooms.js @@ -1,21 +1,23 @@ const mongoose = require('mongoose'); -// MongoDB 채팅방 스키마 수정 (현재 참가 중인 유저 목록 추가) +// MongoDB 채팅방 스키마 수정 (FCM 토큰을 배열로 관리) const chatRoomsSchema = new mongoose.Schema({ chatRoomId: { type: String, required: true, unique: true }, + chatRoomName: { type: String, required: true }, messages: [{ sender: String, message: String, timestamp: Date, - type: { type: String, default: 'message' } // 기본값은 'message', 다른 값으로 'join', 'leave' 가능 + type: { type: String, default: 'message' }, // 기본값은 'message', 다른 값으로 'join', 'leave' 가능 }], - participants: [{ type: String }], - lastReadAt: { type: Map, of: Date }, // 각 참가자의 마지막 읽은 메시지 시간 기록 - lastReadLogId: { type: Map, of: String }, // 각 참가자의 마지막으로 읽은 logID 기록 - isOnline: { type: Map, of: Boolean } // 각 참가자의 온라인 상태 + participants: [{ + name: { type: String, required: true }, + fcmTokens: { type: [String], default: [] }, // FCM 토큰 배열 + }], + lastReadAt: { type: Map, of: Date }, + lastReadLogId: { type: Map, of: String }, + isOnline: { type: Map, of: Boolean }, }, { collection: 'chatrooms' }); -// 모델이 이미 정의되어 있는 경우 재정의하지 않음 -const ChatRooms = mongoose.models.ChatRooms || mongoose.model('ChatRooms', chatRoomsSchema); - -module.exports = ChatRooms; \ No newline at end of file +const ChatRoom = mongoose.model('ChatRooms', chatRoomsSchema); +module.exports = ChatRoom; \ No newline at end of file diff --git a/models/fcmToken.js b/models/fcmToken.js new file mode 100644 index 0000000..0691d46 --- /dev/null +++ b/models/fcmToken.js @@ -0,0 +1,29 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/sequelize'); +const User = require('./User'); // 올바른 경로 확인 + +const FcmToken = sequelize.define('FcmToken', { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: User, // 문자열 대신 모델 객체를 참조 + key: 'id', + }, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, +}, { + tableName: 'FcmTokens', + timestamps: true, +}); + +// 관계 설정 +FcmToken.belongsTo(User, { foreignKey: 'userId', as: 'user' }); +User.hasMany(FcmToken, { foreignKey: 'userId', as: 'fcmTokenList' }); + + + +module.exports = FcmToken; \ No newline at end of file diff --git a/models/index.js b/models/index.js index ec18287..b9b463f 100644 --- a/models/index.js +++ b/models/index.js @@ -7,6 +7,7 @@ const Schedule = require('./Schedule'); const Meeting = require('./Meeting'); const MeetingParticipant = require('./MeetingParticipant'); //폴더명수정 const Friend = require('./Friend'); +const FcmToken = require('./fcmToken'); module.exports = { sequelize, @@ -15,4 +16,5 @@ module.exports = { Meeting, MeetingParticipant, Friend, + FcmToken, }; diff --git a/routes/memberRoute.js b/routes/memberRoute.js new file mode 100644 index 0000000..fe5cacc --- /dev/null +++ b/routes/memberRoute.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const MemberController = require('../controllers/memberController'); + +// FCM 토큰 저장 +router.post('/register-token', MemberController.registerToken); + +module.exports = router; \ No newline at end of file diff --git a/services/meetingService.js b/services/meetingService.js index c0b3f43..caa0988 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -1,14 +1,12 @@ -// services/meetingService.js -const { v4: uuidv4 } = require('uuid'); -const { Op } = require('sequelize'); const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); const ChatRoom = require('../models/chatRooms'); -const chatController = require('../controllers/chatController'); +const FcmToken = require('../models/fcmToken'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const ScheduleService = require('./scheduleService'); // ScheduleService 임포트 +const chatService = require('./chatService'); class MeetingService { /** @@ -16,17 +14,14 @@ class MeetingService { * @returns 생성된 모임 ID와 채팅방 ID */ async createMeeting(meetingData) { - // DTO를 사용하여 요청 데이터 검증 const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); createMeetingDTO.validate(); const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; - // 사용자 존재 여부 확인 - const user = await User.findOne({ where: { id: created_by } }); - if (!user) { - throw new Error('사용자를 찾을 수 없습니다.'); - } + // 사용자와 FCM 토큰 조회 + const user = await this._findUserWithFcmTokens(created_by); + const userFcmTokens = user.fcmTokenList.map((fcmToken) => fcmToken.token); // 스케줄 충돌 확인 const hasConflict = await ScheduleService.checkScheduleOverlap( @@ -34,17 +29,15 @@ class MeetingService { new Date(start_time), new Date(end_time) ); + if (hasConflict) { throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.'); } // 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리 - const result = await Meeting.sequelize.transaction(async (transaction) => { - // 채팅방 생성 - const chatRoomData = { - participants: [user.name], - }; - const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); + return await Meeting.sequelize.transaction(async (transaction) => { + const chatRoomData = this._constructChatRoomData(title, user, userFcmTokens); + const chatRoomResponse = await chatService.createChatRoom(chatRoomData); if (!chatRoomResponse.success) { throw new Error('채팅방 생성 실패'); @@ -52,7 +45,6 @@ class MeetingService { const chatRoomId = chatRoomResponse.chatRoomId; - // 모임 생성 const newMeeting = await Meeting.create({ title, description, @@ -65,13 +57,11 @@ class MeetingService { chatRoomId, }, { transaction }); - // 모임 참가자 추가 (생성자 자신) await MeetingParticipant.create({ meeting_id: newMeeting.id, user_id: created_by, }, { transaction }); - // 스케줄 추가 await ScheduleService.createSchedule({ userId: created_by, title: `번개 모임: ${title}`, @@ -80,15 +70,20 @@ class MeetingService { is_fixed: true, }); + const chatRoom = await ChatRoom.findOne({ chatRoomId: chatRoomId }); + + if (chatRoom) { + console.log("채팅방 찾음"); + this._addParticipantToChatRoom(chatRoom, user, userFcmTokens); + } + return { meeting_id: newMeeting.id, chatRoomId }; }); - - return result; } /** * 번개 모임 목록 조회 - * @return:모임 목록 DTO 배열 + * @return 모임 목록 DTO 배열 */ async getMeetings(userId) { const meetings = await Meeting.findAll({ @@ -121,28 +116,11 @@ class MeetingService { } /** - * 번개 모임 마감 - * @returns 마감된 모임 객체 + * 번개 모임 참가 */ - async closeMeeting(meetingId) { - const meeting = await Meeting.findByPk(meetingId); - if (!meeting) { - throw new Error('모임을 찾을 수 없습니다.'); - } - - if (meeting.type === 'CLOSE') { - throw new Error('이미 마감된 모임입니다.'); - } - - meeting.type = 'CLOSE'; - await meeting.save(); - return meeting; - } - - - //번개모임 참가 async joinMeeting(meetingId, userId) { const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { throw new Error('모임을 찾을 수 없습니다.'); } @@ -155,20 +133,15 @@ class MeetingService { throw new Error('참가 신청이 마감되었습니다.'); } - const existingParticipant = await MeetingParticipant.findOne({ - where: { meeting_id: meetingId, user_id: userId } + const existingParticipant = await MeetingParticipant.findOne({ + where: { meeting_id: meetingId, user_id: userId } }); if (existingParticipant) { throw new Error('이미 참가한 사용자입니다.'); } - // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 await Meeting.sequelize.transaction(async (transaction) => { - // 참가자 추가 - await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction }); - - // 스케줄 충돌 확인 const hasConflict = await ScheduleService.checkScheduleOverlap( userId, new Date(meeting.start_time), @@ -178,25 +151,25 @@ class MeetingService { throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); } - // 스케줄 추가 + await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction }); + await ScheduleService.createSchedule({ - userId: userId, + userId, title: `번개 모임: ${meeting.title}`, start_time: new Date(meeting.start_time), end_time: new Date(meeting.end_time), is_fixed: true, }); - // 채팅방 참가 - const user = await User.findOne({ where: { id: userId } }); - const chatRoom = await ChatRoom.findOne({ where: { meeting_id: meetingId } }); + // 사용자와 FCM 토큰 조회 + const user = await this._findUserWithFcmTokens(userId); + const userFcmTokens = user.fcmTokenList.map((fcmToken) => fcmToken.token); + + const chatRoom = await ChatRoom.findOne({ chatRoomId: meeting.chatRoomId }); - if (chatRoom && !chatRoom.participants.includes(user.name)) { - chatRoom.participants.push(user.name); - chatRoom.isOnline.set(user.name, true); - chatRoom.lastReadAt.set(user.name, new Date()); - chatRoom.lastReadLogId.set(user.name, null); - await chatRoom.save(); + if (chatRoom) { + console.log("채팅방 찾음"); + this._addParticipantToChatRoom(chatRoom, user, userFcmTokens); } }); } @@ -211,7 +184,7 @@ class MeetingService { { model: User, as: 'creator', - attributes: ['name'] + attributes: ['name'], }, { model: MeetingParticipant, @@ -220,11 +193,11 @@ class MeetingService { { model: User, as: 'participantUser', - attributes: ['name', 'email'] - } - ] - } - ] + attributes: ['name', 'email'], + }, + ], + }, + ], }); if (!meeting) { @@ -233,6 +206,79 @@ class MeetingService { return new MeetingDetailResponseDTO(meeting); } + + /** + * 번개 모임 마감 + */ + async closeMeeting(meetingId) { + const meeting = await Meeting.findByPk(meetingId); + + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + if (meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } + + meeting.type = 'CLOSE'; + await meeting.save(); + return meeting; + } + + // Helper functions + async _findUserWithFcmTokens(userId) { + const user = await User.findOne({ + where: { id: userId }, + include: [ + { + model: FcmToken, + as: 'fcmTokenList', + attributes: ['token'], + }, + ], + }); + + if (!user) { + throw new Error('사용자를 찾을 수 없습니다.'); + } + + return user; + } + + _constructChatRoomData(title, user, userFcmTokens) { + return { + meeting_id: null, + participants: [ + { + name: user.name, + fcmTokens: userFcmTokens || [], + isOnline: true, + lastReadAt: new Date(), + lastReadLogId: null, + }, + ], + chatRoomName: title, + }; + } + + _addParticipantToChatRoom(chatRoom, user, userFcmTokens) { + // Map 필드가 초기화되지 않은 경우 기본값 설정 + if (!chatRoom.isOnline) chatRoom.isOnline = new Map(); + if (!chatRoom.lastReadAt) chatRoom.lastReadAt = new Map(); + if (!chatRoom.lastReadLogId) chatRoom.lastReadLogId = new Map(); + + // 참가자 추가 로직 + if (!chatRoom.participants.some(participant => participant.name === user.name)) { + chatRoom.participants.push({ name: user.name, fcmTokens: userFcmTokens }); + chatRoom.isOnline.set(user.name, true); + chatRoom.lastReadAt.set(user.name, new Date()); + chatRoom.lastReadLogId.set(user.name, null); + } + + // 저장 + chatRoom.save(); + } } -module.exports = new MeetingService(); +module.exports = new MeetingService(); \ No newline at end of file diff --git a/services/memberService.js b/services/memberService.js new file mode 100644 index 0000000..bf99182 --- /dev/null +++ b/services/memberService.js @@ -0,0 +1,47 @@ +const User = require('../models/User'); +const FcmToken = require('../models/fcmToken'); +const ChatRoom = require('../models/chatRooms'); + +class MemberService { + async registerToken(email, fcmToken) { + console.log(`Registering FCM token for email: ${email}, token: ${fcmToken}`); + + // 1. RDB에서 사용자 검색 + const user = await User.findOne({ where: { email } }); + if (!user) throw new Error('User not found'); + + console.log(`User found: ${user.name}`); + + // 2. RDB의 FcmTokens 테이블에 저장 + const existingToken = await FcmToken.findOne({ + where: { userId: user.id, token: fcmToken }, + }); + + if (!existingToken) { + await FcmToken.create({ userId: user.id, token: fcmToken }); + console.log(`FCM token ${fcmToken} saved to FcmTokens table`); + } else { + console.log(`FCM token ${fcmToken} already exists for user ${user.name}`); + } + + // 3. MongoDB에서 관련 채팅방의 FCM 토큰 업데이트 + const existingChatRooms = await ChatRoom.find({ "participants.name": user.name }); + for (const room of existingChatRooms) { + room.participants = room.participants.map((participant) => { + if (participant.name === user.name) { + const currentFcmTokens = participant.fcmTokens || []; + if (!currentFcmTokens.includes(fcmToken)) { + participant.fcmTokens = Array.from(new Set([...currentFcmTokens, fcmToken])); + } + } + return participant; + }); + await room.save(); + } + + console.log(`FCM token registration process completed for email: ${email}`); + return { message: 'FCM token registered successfully' }; + } +} + +module.exports = new MemberService(); \ No newline at end of file -- GitLab