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

feat: fcm 토큰 관련 로직 추가

parent b3bea64d
No related branches found
No related tags found
2 merge requests!31Develop,!25[#17] 채팅 푸시알림 추가
This commit is part of merge request !25. Comments created here will be created in the context of that merge request.
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
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
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
......@@ -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,
};
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
// 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
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment