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

Merge remote-tracking branch 'origin/develop' into feature/#12

parents 72516146 40950690
No related branches found
No related tags found
2 merge requests!31Develop,!25[#17] 채팅 푸시알림 추가
This diff is collapsed.
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
"firebase-admin": "^13.0.1", "firebase-admin": "^13.0.1",
"jest": "^29.7.0", "jest": "^29.7.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongoose": "^8.8.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.4", "mysql2": "^3.11.4",
...@@ -36,6 +35,10 @@ ...@@ -36,6 +35,10 @@
"devDependencies": { "devDependencies": {
"artillery": "^2.0.21", "artillery": "^2.0.21",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^3.1.7" "jest": "^29.7.0",
"jest-mock": "^29.7.0",
"mongoose": "^8.8.2",
"nodemon": "^3.1.7",
"sequelize-mock": "^0.10.2"
} }
} }
// routes/inviteRoutes.js
const express = require('express');
const router = express.Router();
const inviteController = require('../controllers/inviteController');
const { isLoggedIn } = require('../middlewares/auth');
router.use(isLoggedIn);
// 초대 응답
router.post('/respond', async (req, res) => {
const { inviteId, response } = req.body;
const userId = req.user.id; // 인증된 사용자 ID
try {
const result = await inviteController.respondToInvite(inviteId, userId, response);
res.status(200).json({ success: true, result });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 받은 초대 조회
router.get('/received', async (req, res) => {
const userId = req.user.id; // 인증된 사용자 ID
try {
const invites = await inviteController.getReceivedInvites(userId);
res.status(200).json({ success: true, invites });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;
const ChatRoom = require('../models/chatRooms'); const ChatRoom = require('../models/ChatRooms');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
class ChatService { class ChatService {
......
// services/friendService.js // services/friendService.js
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const Friend = require('../models/Friend'); const { Friend,User} = require('../models');
const User = require('../models/User');
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
// DTO 임포트 // DTO 임포트
......
// test/friendService.test.js // test/friendService.test.js
const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임포트 const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임포트
const User = require('../models/User'); // const User = require('../models/User');
const Friend = require('../models/Friend'); // const Friend = require('../models/Friend');
const { Friend,User} = require('../models');
const friendService = require('./friendService'); // FriendService 임포트 const friendService = require('./friendService'); // FriendService 임포트
// Sequelize의 Op를 가져오기 위해 추가 // Sequelize의 Op를 가져오기 위해 추가
......
// test/friend.test.js // test/friend.test.js
const sequelize = require('../config/sequelize'); // 환경 변수에 따라 다른 인스턴스 사용 const sequelize = require('../config/sequelize'); // 환경 변수에 따라 다른 인스턴스 사용
const User = require('../models/User'); const { Friend,User} = require('../models');
const Friend = require('../models/Friend');
beforeAll(async () => { beforeAll(async () => {
// 데이터베이스 동기화 // 데이터베이스 동기화
......
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); const { Meeting, MeetingParticipant, User, Schedule } = require('../models');
const ChatRoom = require('../models/chatRooms'); const ChatRoom = require('../models/chatRooms');
const FcmToken = require('../models/fcmToken'); const FcmToken = require('../models/fcmToken');
// services/meetingService.js
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('../models/ChatRooms');
const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO');
const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO');
const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO');
...@@ -10,14 +16,36 @@ const chatService = require('./chatService'); ...@@ -10,14 +16,36 @@ const chatService = require('./chatService');
class MeetingService { class MeetingService {
/** /**
* 번개 모임 생성 * 현재 시간을 time_idx로 변환하는 유틸리티 함수
* @returns 생성된 모임 ID와 채팅방 ID * 월요일부터 일요일까지 15분 단위로 타임 인덱스를 할당
* 현재 시간의 타임 인덱스 (0 ~ 671)
*/ */
getCurrentTimeIdx() {
const today = new Date();
const jsDayOfWeek = today.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
const adjustedDayOfWeek = (jsDayOfWeek + 6) % 7; // 0=Monday, ..., 6=Sunday
const hours = today.getHours();
const minutes = today.getMinutes();
const timeIdx = hours * 4 + Math.floor(minutes / 15); // 15분 단위 인덱스
const totalIdx = adjustedDayOfWeek * 96 + timeIdx; // 주 전체 인덱스
return totalIdx;
}
async createMeeting(meetingData) { async createMeeting(meetingData) {
const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); const createMeetingDTO = new CreateMeetingRequestDTO(meetingData);
createMeetingDTO.validate(); createMeetingDTO.validate();
const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; const {
title,
description,
time_idx_start,
time_idx_end,
location,
time_idx_deadline,
type,
created_by,
max_num,
} = meetingData;
// 사용자와 FCM 토큰 조회 // 사용자와 FCM 토큰 조회
const user = await this._findUserWithFcmTokens(created_by); const user = await this._findUserWithFcmTokens(created_by);
...@@ -45,23 +73,55 @@ class MeetingService { ...@@ -45,23 +73,55 @@ class MeetingService {
const chatRoomId = chatRoomResponse.chatRoomId; const chatRoomId = chatRoomResponse.chatRoomId;
const newMeeting = await Meeting.create({ // 모임 생성
title, const newMeeting = await Meeting.create(
description, {
start_time, title,
end_time, description,
location, time_idx_start,
deadline, time_idx_end,
type, location,
created_by, time_idx_deadline,
chatRoomId, type,
}, { transaction }); created_by,
chatRoomId,
max_num, // max_num 추가
cur_num: 1, // 생성자 자신 포함
},
{ transaction }
);
await MeetingParticipant.create({ // 모임 참가자 추가 (생성자 자신)
meeting_id: newMeeting.id, await MeetingParticipant.create(
user_id: created_by, {
}, { transaction }); meeting_id: newMeeting.id,
user_id: created_by,
},
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (let idx = time_idx_start; idx <= time_idx_end; idx++) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: created_by,
title: `번개 모임: ${title}`,
is_fixed: false,
events: events,
},
transaction
);
// 친구 초대 로직 호출
const invitedFriendIds = await this.sendInvites({
meetingId: newMeeting.id,
creatorId: created_by,
time_idx_start,
time_idx_end,
}, transaction);
await ScheduleService.createSchedule({ await ScheduleService.createSchedule({
userId: created_by, userId: created_by,
title: `번개 모임: ${title}`, title: `번개 모임: ${title}`,
...@@ -77,70 +137,284 @@ class MeetingService { ...@@ -77,70 +137,284 @@ class MeetingService {
this._addParticipantToChatRoom(chatRoom, user, userFcmTokens); this._addParticipantToChatRoom(chatRoom, user, userFcmTokens);
} }
return { meeting_id: newMeeting.id, chatRoomId }; return { meeting_id: newMeeting.id, chatRoomId, invitedFriendIds };
}); });
} }
/** async sendInvites({ meetingId, creatorId, time_idx_start, time_idx_end }, transaction) {
* 번개 모임 목록 조회 // 1. 친구 목록 가져오기 (ACCEPTED 상태)
* @return 모임 목록 DTO 배열 const friends = await Friend.findAll({
*/ where: {
[Op.or]: [
{ requester_id: creatorId, status: 'ACCEPTED' },
{ receiver_id: creatorId, status: 'ACCEPTED' },
],
},
transaction,
});
const friendIds = friends.map(friend =>
friend.requester_id === creatorId ? friend.receiver_id : friend.requester_id
);
if (friendIds.length === 0) {
// 친구가 없거나 모든 친구가 초대받지 못함
return [];
}
const schedules = await Schedule.findAll({
where: {
user_id: { [Op.in]: friendIds },
time_idx: {
[Op.between]: [time_idx_start, time_idx_end],
},
},
transaction,
});
// 스케줄이 겹치는 친구 ID를 추출
const conflictedFriendIds = schedules.map(schedule => schedule.user_id);
// 스케줄이 겹치지 않는 친구 ID 필터링
const availableFriendIds = friendIds.filter(friendId => !conflictedFriendIds.includes(friendId));
if (availableFriendIds.length === 0) {
// 스케줄이 겹치는 친구가 모두 있음
return [];
}
const invitePromises = availableFriendIds.map(inviteeId => {
return Invite.create({
meeting_id: meetingId,
inviter_id: creatorId,
invitee_id: inviteeId,
status: 'PENDING',
}, { transaction });
});
await Promise.all(invitePromises);
return availableFriendIds;
}
async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`);
if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.');
}
if (meeting.type === 'CLOSE') {
throw new Error('이미 마감된 모임입니다.');
}
if (meeting.time_idx_deadline !== undefined) {
const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스
if (currentTimeIdx >= meeting.time_idx_deadline) {
throw new Error('참가 신청이 마감되었습니다.');
}
}
const existingParticipant = await MeetingParticipant.findOne({
where: { meeting_id: meetingId, user_id: userId },
});
if (existingParticipant) {
throw new Error('이미 참가한 사용자입니다.');
}
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await sequelize.transaction(async (transaction) => {
if (meeting.cur_num >= meeting.max_num) {
throw new Error("모임 인원이 모두 찼습니다.");
}
// 스케줄 충돌 확인
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end,
transaction
);
console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) {
throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요.");
}
await MeetingParticipant.create(
{ meeting_id: meetingId, user_id: userId },
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (
let idx = meeting.time_idx_start;
idx <= meeting.time_idx_end;
idx++
) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: true,
events: events,
},
transaction
);
// 채팅방 참가 (MongoDB)
const user = await User.findOne({
where: { id: userId },
transaction,
});
const chatRoom = await ChatRooms.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();
}
// 현재 인원 수 증가
await meeting.increment("cur_num", { by: 1, transaction });
});
}
async getMeetings(userId) { async getMeetings(userId) {
const meetings = await Meeting.findAll({ const meetings = await Meeting.findAll({
attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], attributes: [
'id',
'title',
'description',
'time_idx_start',
'time_idx_end',
'location',
'time_idx_deadline',
'type',
'max_num',
'cur_num',
],
include: [ include: [
{
model: User,
as: 'creator',
attributes: ['name'],
},
{ {
model: MeetingParticipant, model: MeetingParticipant,
as: 'participants', as: 'participants',
attributes: ['user_id'], where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴
attributes: [],
},
{
model: User,
as: 'creator',
attributes: ['name'], // 미팅 생성자의 이름만 필요
}, },
], ],
}); });
return meetings.map((meeting) => { return meetings.map((meeting) => {
const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; const creatorName = meeting.creator ? meeting.creator.name : 'Unknown';
const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); return new MeetingResponseDTO(meeting, true, false, creatorName);
return new MeetingResponseDTO(
meeting,
isParticipant,
false, // isScheduleConflict: 필요 시 추가 로직 구현
creatorName
);
}); });
} }
/**
* 번개 모임 참가 async closeMeeting(meetingId) {
*/
async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId); const meeting = await Meeting.findByPk(meetingId);
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error('모임을 찾을 수 없습니다.');
} }
if (meeting.type === 'CLOSE') { if (meeting.type === 'CLOSE') {
throw new Error('이미 마감된 모임입니다.'); throw new Error('이미 마감된 모임입니다.');
} }
meeting.type = 'CLOSE';
await meeting.save();
return meeting;
}
if (new Date() > new Date(meeting.deadline)) {
throw new Error('참가 신청이 마감되었습니다.'); async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`);
if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.');
} }
if (meeting.type === 'CLOSE') {
const existingParticipant = await MeetingParticipant.findOne({ throw new Error('이미 마감된 모임입니다.');
where: { meeting_id: meetingId, user_id: userId } }
if (meeting.time_idx_deadline !== undefined) {
const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스
if (currentTimeIdx >= meeting.time_idx_deadline) {
throw new Error('참가 신청이 마감되었습니다.');
}
}
const existingParticipant = await MeetingParticipant.findOne({
where: { meeting_id: meetingId, user_id: userId },
}); });
if (existingParticipant) { if (existingParticipant) {
throw new Error('이미 참가한 사용자입니다.'); throw new Error('이미 참가한 사용자입니다.');
} }
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await sequelize.transaction(async (transaction) => {
// 스케줄 충돌 확인
// 현재 인원 수 확인
if (meeting.cur_num >= meeting.max_num) {
throw new Error("모임 인원이 모두 찼습니다.");
}
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end,
transaction
);
console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) {
throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요.");
}
// 참가자 추가
await MeetingParticipant.create(
{ meeting_id: meetingId, user_id: userId },
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (
let idx = meeting.time_idx_start;
idx <= meeting.time_idx_end;
idx++
) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: true,
events: events,
},
transaction
);
// 채팅방 참가 (MongoDB)
const user = await User.findOne({
where: { id: userId },
transaction,
});
const chatRoom = await ChatRooms.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();
}
// 현재 인원 수 증가
await meeting.increment("cur_num", { by: 1, transaction });
await Meeting.sequelize.transaction(async (transaction) => { await Meeting.sequelize.transaction(async (transaction) => {
const hasConflict = await ScheduleService.checkScheduleOverlap( const hasConflict = await ScheduleService.checkScheduleOverlap(
userId, userId,
...@@ -172,28 +446,27 @@ class MeetingService { ...@@ -172,28 +446,27 @@ class MeetingService {
this._addParticipantToChatRoom(chatRoom, user, userFcmTokens); this._addParticipantToChatRoom(chatRoom, user, userFcmTokens);
} }
}); });
});
} }
/**
* 번개 모임 상세 조회
* @return 모임 상세 DTO
*/
async getMeetingDetail(meetingId) { async getMeetingDetail(meetingId) {
const meeting = await Meeting.findByPk(meetingId, { const meeting = await Meeting.findByPk(meetingId, {
include: [ include: [
{ {
model: User, model: User,
as: 'creator', as: "creator",
attributes: ['name'], attributes: ["name"],
}, },
{ {
model: MeetingParticipant, model: MeetingParticipant,
as: 'participants', as: "participants",
include: [ include: [
{ {
model: User, model: User,
as: 'participantUser', as: "user", // 'participantUser'에서 'user'로 수정
attributes: ['name', 'email'], attributes: ["name", "email"],
}, },
], ],
}, },
...@@ -201,7 +474,7 @@ class MeetingService { ...@@ -201,7 +474,7 @@ class MeetingService {
}); });
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error("모임을 찾을 수 없습니다.");
} }
return new MeetingDetailResponseDTO(meeting); return new MeetingDetailResponseDTO(meeting);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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