diff --git a/controllers/friendController.js b/controllers/friendController.js index 8f385c7cf1a01cdfbf55ab5afea27df9a193ecbe..c3eb2515eb46ef2a7eadebe5f0294f6698db3536 100644 --- a/controllers/friendController.js +++ b/controllers/friendController.js @@ -137,15 +137,27 @@ class friendController { /** * 친구 목록 조회 - * GET /api/friend/all/:offset + * GET /api/friend/all?page=1&size=20 */ async getFriendList(req, res) { try { const userId = req.user.id; - const friends = await FriendService.getFriendList(userId,20,req.param); + const page = parseInt(req.query.page) || 0; + const size = parseInt(req.query.size) || 20; + + const friends = await FriendService.getFriendList(userId, { + limit: size, + offset: page * size + }); + return res.status(200).json({ success: true, - data: friends + data: { + content: friends, + page: page, + size: size, + hasNext: friends.length === size + } }); } catch (error) { return res.status(500).json({ diff --git a/controllers/meetingController.js b/controllers/meetingController.js index d85e78864e0106b054239c6cc3946d19daaa656b..4363b41365ea22d274b6cc14526bd1669fc7da09 100644 --- a/controllers/meetingController.js +++ b/controllers/meetingController.js @@ -21,9 +21,9 @@ class MeetingController { async createMeeting(req, res) { try { const userId = req.user.id; - const meetingData = { - ...req.body, - created_by: userId + const meetingData = { + ...req.body, + created_by: userId }; const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); createMeetingDTO.validate(); @@ -43,8 +43,23 @@ class MeetingController { async getMeetings(req, res) { try { const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID - const meetings = await MeetingService.getMeetings(userId); - res.status(200).json(meetings); + const page = parseInt(req.query.page) || 0; + const size = parseInt(req.query.size) || 20; + + const meetings = await MeetingService.getMeetings(userId, { + limit: size, + offset: page * size + }); + + res.status(200).json({ + success: true, + data: { + content: meetings.content, + page: page, + size: size, + hasNext: meetings.hasNext + } + }); } catch (err) { console.error('모임 목록 조회 오류:', err); res.status(500).json({ error: err.message || '모임 목록 조회 실패' }); @@ -77,7 +92,7 @@ class MeetingController { const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID await MeetingService.joinMeeting(meetingId, userId); - + res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); } catch (err) { console.error('모임 참가 오류:', err); diff --git a/dtos/MeetingDetailResponseDTO.js b/dtos/MeetingDetailResponseDTO.js index 5d4ac75f7dbc729ccc4235e5d27abe247ddf4c3f..c9a07edd124db0d59702cc02460122e4a9f23d40 100644 --- a/dtos/MeetingDetailResponseDTO.js +++ b/dtos/MeetingDetailResponseDTO.js @@ -1,6 +1,6 @@ // dtos/MeetingResponseDTO.js class MeetingDetailResponseDTO { - constructor(meeting) { + constructor(meeting, isScheduleConflict) { this.id = meeting.id; this.title = meeting.title; this.description = meeting.description; @@ -10,6 +10,7 @@ class MeetingDetailResponseDTO { this.time_idx_deadline = meeting.time_idx_deadline; this.type = meeting.type; this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + this.isScheduleConflict = isScheduleConflict; this.participants = meeting.participants.map(participant => ({ userId: participant.user_id, name: participant.participantUser ? participant.participantUser.name : 'Unknown', diff --git a/models/index.js b/models/index.js index 9ad167a836d1ae5924d17bfd1d1f4671a2ab8916..94c499b692695e50655f62d04237f64ae8a8962e 100644 --- a/models/index.js +++ b/models/index.js @@ -2,10 +2,8 @@ const sequelize = require('../config/sequelize'); const User = require('./user'); -const Friend = require('./Friend'); const Schedule = require('./Schedule'); const Meeting = require('./Meeting'); -const MeetingParticipant = require('./MeetingParticipant'); //폴더명수정 const Friend = require('./Friend'); const FcmToken = require('./fcmToken'); const MeetingParticipant = require('./MeetingParticipant'); diff --git a/routes/friend.js b/routes/friend.js index 53145a9930403881d542d7fad15a27d9d485edbc..6061959eff8d2a4fffd78ebba2226deef8982816 100644 --- a/routes/friend.js +++ b/routes/friend.js @@ -37,7 +37,7 @@ router.post('/request/:friendId/reject', FriendController.rejectRequest); /** * 친구 목록 조회 - * GET /api/friend/all + * GET /api/friend/all?page=1&size=20 */ router.get('/all', FriendController.getFriendList); diff --git a/services/friendService.js b/services/friendService.js index bb995bb9ef69f4221f2ff40dbc452f21ba75fba4..d35b3c700c9b4b1a1b7a18326eafb2eace829478 100644 --- a/services/friendService.js +++ b/services/friendService.js @@ -191,7 +191,9 @@ class FriendService { * @param {number} offset - 페이징 오프셋 * @returns {Promise<Array<FriendListDTO>>} - 친구 목록 DTO 배열 */ - async getFriendList(userId, limit = 20, offset = 0) { + async getFriendList(userId, pagination) { + const { limit = 20, offset = 0 } = pagination; + const friends = await Friend.findAll({ where: { [Op.or]: [ @@ -212,13 +214,20 @@ class FriendService { attributes: ['id', 'name', 'email'] } ], - order: [['id', 'ASC']], - limit, + order: [['id', 'ASC']], + limit: limit + 1, // 다음 페이지 존재 여부 확인을 위해 1개 더 조회 offset }); - - - return friends.map(friend => new FriendListDTO(friend, userId)); + + const hasNext = friends.length > limit; + const content = friends.slice(0, limit).map(friend => new FriendListDTO(friend, userId)); + + return { + content, + page: offset / limit, + size: limit, + hasNext + }; } /** diff --git a/services/friendService.test.js b/services/friendService.test.js index 02394ee0066ee60c52fada5fc48149148c0e1bad..2822a25a497848c48529363a1f8bda18f09fc0bd 100644 --- a/services/friendService.test.js +++ b/services/friendService.test.js @@ -143,50 +143,77 @@ describe('Friend Service', () => { }); describe('getFriendList', () => { - test('should retrieve friend list with correct pagination', async () => { + beforeEach(async () => { await friendService.sendFriendRequest(1, 2); await friendService.acceptFriendRequest(2, 1); - await friendService.sendFriendRequest(1, 3); await friendService.acceptFriendRequest(3, 1); - + // 추가 더미데이터 생성 for (let i = 4; i <= 23; i++) { - // Create dummy users await User.create({ id: i, name: `User${i}`, email: `user${i}@example.com`, }); - - // Alice랑 친구맺기 await friendService.sendFriendRequest(1, i); await friendService.acceptFriendRequest(i, 1); } - - // Alice 친구: Bob (2), Charlie (3), User4부터 User23까지 (총 22명) - const limit = 5; - const offset = 0; - const friendsPage1 = await friendService.getFriendList(1, limit, offset); - expect(friendsPage1.length).toBe(limit); - const expectedNamesPage1 = ['Bob', 'Charlie', 'User4', 'User5', 'User6']; - const receivedNamesPage1 = friendsPage1.map(friend => friend.friendInfo.name); - expectedNamesPage1.forEach(name => { - expect(receivedNamesPage1).toContain(name); + }); + + test('작은 size로 여러 페이지 조회', async () => { + const size = 10; + + // 첫 페이지 + const page1 = await friendService.getFriendList(1, { + limit: size, + offset: 0 }); - - const friendsPage2 = await friendService.getFriendList(1, limit, limit); - expect(friendsPage2.length).toBe(limit); - const expectedNamesPage2 = ['User7', 'User8', 'User9', 'User10', 'User11']; - const receivedNamesPage2 = friendsPage2.map(friend => friend.friendInfo.name); - expectedNamesPage2.forEach(name => { - expect(receivedNamesPage2).toContain(name); + expect(page1.content.length).toBe(size); + expect(page1.hasNext).toBe(true); + + // 두 번째 페이지 + const page2 = await friendService.getFriendList(1, { + limit: size, + offset: size + }); + expect(page2.content.length).toBe(size); + expect(page2.hasNext).toBe(true); + + // 마지막 페이지 + const page3 = await friendService.getFriendList(1, { + limit: size, + offset: size * 2 }); + expect(page3.content.length).toBe(2); + expect(page3.hasNext).toBe(false); }); - - test('should return empty array when user has no friends', async () => { - const friends = await friendService.getFriendList(999); // Non-existing user - expect(friends.length).toBe(0); + + test('페이지 순서 검증', async () => { + const size = 5; + const page1 = await friendService.getFriendList(1, { + limit: size, + offset: 0 + }); + const page2 = await friendService.getFriendList(1, { + limit: size, + offset: size + }); + + const names1 = page1.content.map(friend => friend.friendInfo.name); + const names2 = page2.content.map(friend => friend.friendInfo.name); + + expect(names1).toEqual(['Bob', 'Charlie', 'User4', 'User5', 'User6']); + expect(names2).toEqual(['User7', 'User8', 'User9', 'User10', 'User11']); + }); + + test('존재하지 않는 페이지 조회', async () => { + const response = await friendService.getFriendList(1, { + limit: 20, + offset: 100 + }); + expect(response.content).toHaveLength(0); + expect(response.hasNext).toBe(false); }); }); diff --git a/services/integration.test.js b/services/integration.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f004ce1a6359d07efa90d706d40246e819d88d8b --- /dev/null +++ b/services/integration.test.js @@ -0,0 +1,275 @@ +// test/integration.test.js +const sequelize = require('../config/sequelize'); +const { User, Friend, Meeting, Schedule, MeetingParticipant, ChatRooms } = require('../models'); +const FriendService = require('../services/friendService'); +const MeetingService = require('../services/meetingService'); +const ScheduleService = require('../services/scheduleService'); + +describe('System Integration Test', () => { + beforeAll(async () => { + await sequelize.sync({ force: true }); + + jest.spyOn(ChatRooms.prototype, 'save').mockResolvedValue(undefined); + jest.spyOn(ChatRooms, 'findOne').mockResolvedValue({ + participants: [], + isOnline: new Map(), + lastReadAt: new Map(), + lastReadLogId: new Map(), + save: jest.fn().mockResolvedValue(true), + }); + + // 테스트용 사용자 생성 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' } + ]); + + // 성능 측정을 위한 시작 시간 기록 + console.time('Complete User Journey'); + }); + + afterAll(async () => { + console.timeEnd('Complete User Journey'); + jest.restoreAllMocks(); + await sequelize.close(); + }); + + /** + * 시나리오 1 + * 1. 친구 + * 2. 스케줄 관리 + * 3. 미팅 + * 4. 조회 + */ + + test('Complete User Journey Scenario', async () => { + const pagination = { limit: 20, offset: 0 }; + /** + * 1. 친구 + * 친구 요청/수락/거절 + * 친구 목록 조회 + * 중복 친구 요청 방지 + * ++ 친구 스케줄 보기 + */ + console.time('Friend Operations'); + const aliceId = 1, bobId = 2, charlieId = 3; + + await FriendService.sendFriendRequest(aliceId, bobId); + await FriendService.sendFriendRequest(aliceId, charlieId); + + await FriendService.acceptFriendRequest(bobId, aliceId); + await FriendService.rejectFriendRequest(charlieId, aliceId); + + const aliceFriends = await FriendService.getFriendList(aliceId, pagination); + expect(aliceFriends.content.length).toBe(1); + expect(aliceFriends.content[0].friendInfo.name).toBe('Bob'); + + await expect( + FriendService.sendFriendRequest(aliceId, bobId) + ).rejects.toThrow('Friend request already exists'); + + /** + * 2. 스케줄 관리 + * 스케줄 생성/수정/삭제 + * 전체 스케줄 조회 + * 특정 스케줄 조회 + * ++ 페이지네이션 확인 + */ + console.time('Schedule Operations'); + + // 2-1. 스케줄 생성 + const aliceSchedule = { + userId: aliceId, + title: '수업', + is_fixed: true, + events: [ + { time_idx: 36 }, + { time_idx: 37 }, + { time_idx: 38 } + ] + }; + const createdSchedules = await ScheduleService.createSchedules(aliceSchedule); + expect(createdSchedules.length).toBe(3); + + // 2-2. 특정 스케줄 조회 + const specificSchedule = await ScheduleService.getScheduleByTimeIdx(aliceId, 36); + expect(specificSchedule.title).toBe('수업'); + expect(specificSchedule.is_fixed).toBe(true); + + // 2-3. 전체 스케줄 조회 + const allSchedules = await ScheduleService.getAllSchedules(aliceId); + expect(allSchedules.length).toBe(3); + expect(allSchedules.every(s => s.title === '수업')).toBe(true); + + // 2-4. 스케줄 수정 + const scheduleUpdates = [ + { time_idx: 36, title: '중요 수업', is_fixed: true } + ]; + const updatedSchedules = await ScheduleService.updateSchedules(aliceId, scheduleUpdates); + expect(updatedSchedules[0].title).toBe('중요 수업'); + + // 2-5. 수정된 스케줄 확인 + const updatedAllSchedules = await ScheduleService.getAllSchedules(aliceId); + const updatedSchedule = updatedAllSchedules.find(s => s.time_idx === 36); + expect(updatedSchedule.title).toBe('중요 수업'); + + // 2-6. 스케줄 삭제 + const deleteResult = await ScheduleService.deleteSchedules(aliceId, [37]); + expect(deleteResult.deleted_time_idxs).toContain(37); + + // 2-7. 삭제 확인 + const remainingSchedules = await ScheduleService.getAllSchedules(aliceId); + expect(remainingSchedules.length).toBe(2); + expect(remainingSchedules.every(s => s.time_idx !== 37)).toBe(true); + + // 2-8. 중복 스케줄 생성 시도 + await expect( + ScheduleService.createSchedules({ + userId: aliceId, + title: '중복 스케줄', + is_fixed: true, + events: [{ time_idx: 36 }] + }) + ).rejects.toThrow('Schedule overlaps with existing schedule'); + + console.timeEnd('Schedule Operations'); + + + /** + * 3. 미팅 참가 + * 미팅 생성/스케줄 자동 등록 ++ create 시 생성자의 스케줄 확인 및 중복 체크 + * 중복된 시간 참여 불가 + * 미팅 close (생성자) + * 미팅 탈퇴 + * ++ 친구 초대 + */ + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(30); + + /** + * 3. 미팅 참가 시나리오 + */ + console.time('Meeting Operations'); + + // 3-1. 미팅 생성 및 스케줄 자동 등록 + const aliceConflictMeeting = { + title: '스터디 모임', + time_idx_start: 36, + time_idx_end: 38, + created_by: aliceId, + type: 'OPEN', + time_idx_deadline: 35, + location: 'Room A' + }; + + await expect( + MeetingService.createMeeting(aliceConflictMeeting) + ).rejects.toThrow('해당 시간에 이미 다른 스케줄이 있습니다'); + + const meetingData = { + title: '스터디 모임', + time_idx_start: 36, + time_idx_end: 38, + created_by: bobId, + type: 'OPEN', + time_idx_deadline: 35, + location: 'Room A' + }; + + const meeting = await MeetingService.createMeeting(meetingData); + + const bobSchedules = await Schedule.findAll({ + where: { + user_id: bobId, + title: `번개 모임: ${meetingData.title}` + } + }); + expect(bobSchedules.length).toBe(3); // 36-38 시간대 + + // 3-2. 스케줄 충돌로 인한 참가 실패 (Alice) + await expect( + MeetingService.joinMeeting(meeting.meeting_id, aliceId) + ).rejects.toThrow('스케줄이 겹칩니다'); + + // 3-3. Charlie 참가 성공 + await MeetingService.joinMeeting(meeting.meeting_id, charlieId); + const charlieSchedules = await Schedule.findAll({ + where: { + user_id: charlieId, + title: `번개 모임: ${meetingData.title}` + } + }); + expect(charlieSchedules.length).toBe(3); + + // 3-4. Charlie의 미팅 목록 조회 + const charlieMyMeetings = await MeetingService.getMyMeetings(charlieId, pagination); + expect(charlieMyMeetings.content.length).toBe(1); + expect(charlieMyMeetings.content[0].isParticipant).toBe(true); + expect(charlieMyMeetings.content[0].title).toBe('스터디 모임'); + + // 3-5. Charlie 미팅 탈퇴 + await MeetingService.leaveMeeting(meeting.meeting_id, charlieId); + + // 탈퇴 후 스케줄 삭제 확인 + const remainingCharlieSchedules = await Schedule.findAll({ + where: { + user_id: charlieId, + title: `번개 모임: ${meetingData.title}` + } + }); + expect(remainingCharlieSchedules.length).toBe(0); + + // 탈퇴 후 미팅 목록 확인 + const charlieMyMeetingsAfterLeave = await MeetingService.getMyMeetings(charlieId, pagination); + expect(charlieMyMeetingsAfterLeave.content.length).toBe(0); + + // 3-6. 생성자 탈퇴 시도 (실패) + await expect( + MeetingService.leaveMeeting(meeting.meeting_id, bobId) + ).rejects.toThrow('모임 생성자는 탈퇴할 수 없습니다'); + + // 3-7. 미팅 마감 + await MeetingService.closeMeeting(meeting.meeting_id); + const closedMeeting = await Meeting.findByPk(meeting.meeting_id); + expect(closedMeeting.type).toBe('CLOSE'); + + // 3-8. 마감된 미팅 참가 시도 (실패) + await expect( + MeetingService.joinMeeting(meeting.meeting_id, charlieId) + ).rejects.toThrow('이미 마감된 모임입니다'); + + /** + * 4. 미팅 조회 시나리오 + * 전체 미팅 목록 조회 + * 참여하고 있는 미팅 목록 조회 + * 상세 정보 조회 + */ + console.time('Meeting Queries'); + + // 4-1. 전체 미팅 목록 조회 + const allMeetings = await MeetingService.getMeetings(aliceId, pagination); + expect(allMeetings.content.length).toBe(1); + expect(allMeetings.content[0].isScheduleConflict).toBe(true); + + // 4-2. Bob의 미팅 목록 조회 (생성자) + const bobMyMeetings = await MeetingService.getMyMeetings(bobId, pagination); + expect(bobMyMeetings.content.length).toBe(1); + expect(bobMyMeetings.content[0].isParticipant).toBe(true); + expect(bobMyMeetings.content[0].creatorName).toBe('Bob'); + + // 4-2. Alice의 미팅 목록 조회 (참여 x, 생성 x) + const aliceMyMeetings = await MeetingService.getMyMeetings(aliceId, pagination); + expect(aliceMyMeetings.content.length).toBe(0); + // expect(aliceMyMeetings.content[0].isParticipant).toBe(true); + // expect(aliceMyMeetings.content[0].creatorName).toBe('Bob'); + + // 4-3. 상세 정보 조회 + const meetingDetail = await MeetingService.getMeetingDetail(meeting.meeting_id, aliceId); + expect(meetingDetail.isScheduleConflict).toBe(true); + expect(meetingDetail.creatorName).toBe('Bob'); + expect(meetingDetail.participants).toBeDefined(); + + console.timeEnd('Meeting Queries'); + + }, 10000); +}); diff --git a/services/meetingService.js b/services/meetingService.js index d7b6023a2ecfebfe98d55ca4a7d43496c3bf3f2b..f3d5e921dcd3b9c612ab1be39b57a421b073fddd 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -1,7 +1,7 @@ -const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); -const ChatRoom = require('../models/chatRooms'); -const FcmToken = require('../models/fcmToken'); +// const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); +// const ChatRoom = require('../models/chatRooms'); +// const FcmToken = require('../models/fcmToken'); // services/meetingService.js const { v4: uuidv4 } = require('uuid'); const { Op } = require('sequelize'); @@ -52,14 +52,23 @@ class MeetingService { const userFcmTokens = user.fcmTokenList.map((fcmToken) => fcmToken.token); // 스케줄 충돌 확인 - const hasConflict = await ScheduleService.checkScheduleOverlap( + // const hasConflict = await ScheduleService.checkScheduleOverlap( + // created_by, + // new Date(start_time), + // new Date(end_time) + // ); + + // if (hasConflict) { + // throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.'); + // } + + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( created_by, - new Date(start_time), - new Date(end_time) + time_idx_start, + time_idx_end ); - if (hasConflict) { - throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.'); + throw new Error('해당 시간에 이미 다른 스케줄이 있습니다.'); } // 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리 @@ -253,7 +262,7 @@ class MeetingService { { userId: userId, title: `번개 모임: ${meeting.title}`, - is_fixed: true, + is_fixed: false, events: events, }, transaction @@ -281,7 +290,9 @@ class MeetingService { } - async getMeetings(userId) { + async getMeetings(userId, pagination) { + const { limit = 20, offset = 0 } = pagination; + const meetings = await Meeting.findAll({ attributes: [ 'id', @@ -299,159 +310,113 @@ class MeetingService { { model: MeetingParticipant, as: 'participants', - where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴 - attributes: [], + required: false, + attributes: [], }, { model: User, as: 'creator', - attributes: ['name'], // 미팅 생성자의 이름만 필요 - }, + attributes: ['name'], + } ], + order: [['createdAt', 'DESC']], + offset }); - - return meetings.map((meeting) => { - const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - return new MeetingResponseDTO(meeting, true, false, creatorName); - }); - } - - - 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; + + const hasNext = meetings.length > limit; + const content = await Promise.all( + meetings.slice(0, limit).map(async (meeting) => { + const isParticipant = await MeetingParticipant.findOne({ + where: { + meeting_id: meeting.id, + user_id: userId + } + }); + + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( + userId, + meeting.time_idx_start, + meeting.time_idx_end + ); + + const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + return new MeetingResponseDTO(meeting, !!isParticipant, hasConflict, creatorName); + }) + ); + + return { + content, + hasNext + }; } - - 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, + async getMyMeetings(userId, pagination) { + const { limit = 20, offset = 0 } = pagination; + + const meetings = await Meeting.findAll({ + attributes: [ + 'id', + 'title', + 'description', + 'time_idx_start', + 'time_idx_end', + 'location', + 'time_idx_deadline', + 'type', + 'max_num', + 'cur_num', + ], + include: [ + { + model: MeetingParticipant, + as: 'participants', + where: { user_id: userId }, + attributes: [], + }, + { + model: User, + as: 'creator', + attributes: ['name'], + } + ], + where: { + [Op.or]: [ + { created_by: userId }, + { '$participants.user_id$': userId } + ] }, - 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) => { - const hasConflict = await ScheduleService.checkScheduleOverlap( - userId, - new Date(meeting.start_time), - new Date(meeting.end_time) - ); - if (hasConflict) { - throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); - } - - await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction }); - - await ScheduleService.createSchedule({ - userId, - title: `번개 모임: ${meeting.title}`, - start_time: new Date(meeting.start_time), - end_time: new Date(meeting.end_time), - is_fixed: true, - }); - - // 사용자와 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) { - console.log("채팅방 찾음"); - this._addParticipantToChatRoom(chatRoom, user, userFcmTokens); - } + order: [['createdAt', 'DESC']], + offset }); - }); - } - + const hasNext = meetings.length > limit; + const content = await Promise.all( + meetings.slice(0, limit).map(async (meeting) => { + const isParticipant = await MeetingParticipant.findOne({ + where: { + meeting_id: meeting.id, + user_id: userId + } + }); + + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( + userId, + meeting.time_idx_start, + meeting.time_idx_end + ); - async getMeetingDetail(meetingId) { + const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + return new MeetingResponseDTO(meeting, !!isParticipant, hasConflict, creatorName); + }) + ); + + return { + content, + hasNext + }; + } + + async getMeetingDetail(meetingId, userId) { const meeting = await Meeting.findByPk(meetingId, { include: [ { @@ -465,19 +430,25 @@ class MeetingService { include: [ { model: User, - as: "user", // 'participantUser'에서 'user'로 수정 + as: "user", attributes: ["name", "email"], - }, - ], - }, - ], + } + ] + } + ] }); - + if (!meeting) { throw new Error("모임을 찾을 수 없습니다."); } - - return new MeetingDetailResponseDTO(meeting); + + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( + userId, + meeting.time_idx_start, + meeting.time_idx_end + ); + + return new MeetingDetailResponseDTO(meeting, hasConflict); } /** @@ -552,6 +523,67 @@ class MeetingService { // 저장 chatRoom.save(); } + + async leaveMeeting(meetingId, userId) { + const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + await sequelize.transaction(async (transaction) => { + // 참가자 확인 + const participant = await MeetingParticipant.findOne({ + where: { + meeting_id: meetingId, + user_id: userId + }, + transaction + }); + + if (!participant) { + throw new Error('참가하지 않은 모임입니다.'); + } + + // 생성자는 탈퇴할 수 없음 + if (meeting.created_by === userId) { + throw new Error('모임 생성자는 탈퇴할 수 없습니다.'); + } + + // 참가자 제거 + await MeetingParticipant.destroy({ + where: { + meeting_id: meetingId, + user_id: userId + }, + transaction + }); + + // 관련 스케줄 삭제 + await Schedule.destroy({ + where: { + user_id: userId, + title: `번개 모임: ${meeting.title}`, + time_idx: { + [Op.between]: [meeting.time_idx_start, meeting.time_idx_end] + } + }, + transaction + }); + + // 채팅방에서 제거 + const chatRoom = await ChatRooms.findOne({ + chatRoomId: meeting.chatRoomId + }); + if (chatRoom) { + const user = await User.findByPk(userId); + chatRoom.participants = chatRoom.participants.filter(p => p !== user.name); + await chatRoom.save(); + } + + // 현재 인원 수 감소 + await meeting.decrement('cur_num', { by: 1, transaction }); + }); + } } module.exports = new MeetingService(); \ No newline at end of file diff --git a/services/meetingService.test.js b/services/meetingService.test.js index 1dc2f055a01fe2e263cdbe8715a14f75189ab008..0ba48c6252b827a13df842d75f5b5a6de40c8a0d 100644 --- a/services/meetingService.test.js +++ b/services/meetingService.test.js @@ -1,10 +1,10 @@ // test/meetingService.test.js -const sequelize = require('../config/sequelize'); +const sequelize = require('../config/sequelize'); const { Op } = require('sequelize'); const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); const MeetingService = require('../services/meetingService'); const ScheduleService = require('../services/scheduleService'); -const ChatRooms = require('../models/ChatRooms'); +const ChatRooms = require('../models/ChatRooms'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); @@ -12,486 +12,471 @@ const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); beforeAll(async () => { - // 데이터베이스 초기화 및 동기화 - await sequelize.sync({ force: true }); + // 데이터베이스 초기화 및 동기화 + await sequelize.sync({ force: true }); }); beforeEach(async () => { - // 외래 키 순서에 따라 데이터 삭제 - await MeetingParticipant.destroy({ where: {}, truncate: true }); - await Meeting.destroy({ where: {}, truncate: true }); - await Schedule.destroy({ where: {}, truncate: true }); - await User.destroy({ where: {}, truncate: true }); - - // 더미 사용자 데이터 삽입 - await User.bulkCreate([ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' }, - { id: 3, name: 'Charlie', email: 'charlie@example.com' }, - ]); - - // ChatRooms Mock 설정 - jest.spyOn(ChatRooms.prototype, 'save').mockResolvedValue(undefined); - jest.spyOn(ChatRooms, 'findOne').mockResolvedValue({ - participants: [], - isOnline: new Map(), - lastReadAt: new Map(), - lastReadLogId: new Map(), - save: jest.fn().mockResolvedValue(true), - }); + // 외래 키 순서에 따라 데이터 삭제 + await MeetingParticipant.destroy({ where: {}, truncate: true }); + await Meeting.destroy({ where: {}, truncate: true }); + await Schedule.destroy({ where: {}, truncate: true }); + await User.destroy({ where: {}, truncate: true }); + + // 더미 사용자 데이터 삽입 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' }, + ]); + + // ChatRooms Mock 설정 + jest.spyOn(ChatRooms.prototype, 'save').mockResolvedValue(undefined); + jest.spyOn(ChatRooms, 'findOne').mockResolvedValue({ + participants: [], + isOnline: new Map(), + lastReadAt: new Map(), + lastReadLogId: new Map(), + save: jest.fn().mockResolvedValue(true), + }); }); afterEach(() => { - // Mock 복원 및 초기화 - jest.restoreAllMocks(); - jest.clearAllMocks(); + // Mock 복원 및 초기화 + jest.restoreAllMocks(); + jest.clearAllMocks(); }); afterAll(async () => { - // 데이터베이스 연결 종료 - await sequelize.close(); + // 데이터베이스 연결 종료 + await sequelize.close(); }); -describe('MeetingService - getMeetings', () => { - beforeEach(async () => { - await MeetingParticipant.destroy({ where: {} }); - await Meeting.destroy({ where: {} }); - await Schedule.destroy({ where: {} }); - await User.destroy({ where: {} }); - - // Create dummy users - await User.bulkCreate([ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' }, - { id: 3, name: 'Charlie', email: 'charlie@example.com' }, - ]); - }); - - test('should retrieve meetings where the user is a participant', async () => { - const meetingData = { - title: 'Meeting with Alice', - description: 'Discuss project.', - time_idx_start: 10, - time_idx_end: 20, - location: 'Room A', - time_idx_deadline: 8, - type: 'OPEN', - created_by: 1, - }; - - const createdMeeting = await MeetingService.createMeeting(meetingData); - - await MeetingParticipant.create({ - meeting_id: createdMeeting.meeting_id, - user_id: 2, - }); - - const meetings = await MeetingService.getMeetings(2); // Bob's user ID - - expect(meetings).toBeDefined(); - expect(Array.isArray(meetings)).toBe(true); - expect(meetings.length).toBe(1); - - const [meeting] = meetings; - expect(meeting.title).toBe('Meeting with Alice'); - expect(meeting.creatorName).toBe('Alice'); - expect(meeting.isParticipant).toBe(true); - }); - - test('should retrieve meetings where the user is the creator', async () => { - const meetingData = { - title: 'Alice-created Meeting', - description: 'Team discussion.', - time_idx_start: 15, - time_idx_end: 25, - location: 'Room B', - time_idx_deadline: 12, - type: 'OPEN', - created_by: 1, - }; - - await MeetingService.createMeeting(meetingData); - - const meetings = await MeetingService.getMeetings(1); // Alice's user ID - - expect(meetings).toBeDefined(); - expect(Array.isArray(meetings)).toBe(true); - expect(meetings.length).toBe(1); - - const [meeting] = meetings; - expect(meeting.title).toBe('Alice-created Meeting'); - expect(meeting.creatorName).toBe('Alice'); - expect(meeting.isParticipant).toBe(true); - }); - - test('should not include meetings where the user is neither a participant nor the creator', async () => { - const meetingData = { - title: 'Meeting with Bob', - description: 'General discussion.', - time_idx_start: 30, - time_idx_end: 40, - location: 'Room C', - time_idx_deadline: 28, - type: 'OPEN', - created_by: 2, - }; - - await MeetingService.createMeeting(meetingData); - - const meetings = await MeetingService.getMeetings(1); // Alice's user ID - - expect(meetings).toBeDefined(); - expect(Array.isArray(meetings)).toBe(true); - expect(meetings.length).toBe(0); // Alice is not a participant or the creator - }); - - test('should retrieve multiple meetings correctly', async () => { - const meetingData1 = { - title: 'Meeting 1', - description: 'First meeting.', - time_idx_start: 50, - time_idx_end: 60, - location: 'Room D', - time_idx_deadline: 48, - type: 'OPEN', - created_by: 1, - }; - - const meetingData2 = { - title: 'Meeting 2', - description: 'Second meeting.', - time_idx_start: 70, - time_idx_end: 80, - location: 'Room E', - time_idx_deadline: 68, - type: 'OPEN', - created_by: 2, - }; - - await MeetingService.createMeeting(meetingData1); - const meeting2 = await MeetingService.createMeeting(meetingData2); - - - await MeetingParticipant.create({ - meeting_id: meeting2.meeting_id, - user_id: 1, - }); - - const meetings = await MeetingService.getMeetings(1); // Alice's user ID - - expect(meetings).toBeDefined(); - expect(Array.isArray(meetings)).toBe(true); - expect(meetings.length).toBe(2); // Alice is either the creator or a participant in two meetings - - const meetingTitles = meetings.map((m) => m.title); - expect(meetingTitles).toContain('Meeting 1'); - expect(meetingTitles).toContain('Meeting 2'); - }); - - test('should return an empty array if the user has no meetings', async () => { - const meetings = await MeetingService.getMeetings(3); - expect(meetings).toBeDefined(); - expect(Array.isArray(meetings)).toBe(true); - expect(meetings.length).toBe(0); - }); +describe('MeetingService - 전체 미팅 조회 테스트', () => { + beforeEach(async () => { + await MeetingParticipant.destroy({ where: {} }); + await Meeting.destroy({ where: {} }); + await Schedule.destroy({ where: {} }); + await User.destroy({ where: {} }); + + // 더미 사용자 생성 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' } + ]); + + // 스케줄 충돌 체크 Mock 설정 + jest.spyOn(ScheduleService, 'checkScheduleOverlapByTime') + .mockImplementation(async (userId, start, end) => { + // 36-38 시간대 충돌 + if (start <= 38 && end >= 36) return true; + // 51 시간대 충돌 (Bob의 개인 일정) + if (userId === 2 && start <= 51 && end >= 51) return true; + return false; + }); + }); + + test('모든 미팅 목록 조회 및 스케줄 충돌 확인', async () => { + // 1. Alice의 스케줄 등록 + await ScheduleService.createSchedules({ + userId: 1, + title: '기존 일정', + is_fixed: true, + events: [ + { time_idx: 36 }, + { time_idx: 37 }, + { time_idx: 38 } + ] + }); + + // 2. getCurrentTimeIdx Mock 설정 + jest.spyOn(MeetingService, 'getCurrentTimeIdx') + .mockReturnValue(30); + + const meetingData1 = { + title: '아침 미팅', + time_idx_start: 36, + time_idx_end: 38, + created_by: 2, + type: 'OPEN', + time_idx_deadline: 35, + location: 'Room A' + }; + + const meetingData2 = { + title: '점심 미팅', + time_idx_start: 44, + time_idx_end: 46, + created_by: 3, + type: 'OPEN', + time_idx_deadline: 43, + location: 'Room B' + }; + + await MeetingService.createMeeting(meetingData1); + await MeetingService.createMeeting(meetingData2); + + const {content: meetings} = await MeetingService.getMeetings(1, {limit: 20, offset: 0}); + + expect(meetings.length).toBe(2); + + const morningMeeting = meetings.find(m => m.title === '아침 미팅'); + expect(morningMeeting.isScheduleConflict).toBe(true); + expect(morningMeeting.creatorName).toBe('Bob'); + expect(morningMeeting.isParticipant).toBe(false); + + const lunchMeeting = meetings.find(m => m.title === '점심 미팅'); + expect(lunchMeeting.isScheduleConflict).toBe(false); + expect(lunchMeeting.creatorName).toBe('Charlie'); + expect(lunchMeeting.isParticipant).toBe(false); + }); + + test('미팅 상세 정보 조회 및 충돌 확인', async () => { + jest.spyOn(MeetingService, 'getCurrentTimeIdx') + .mockReturnValue(35); + + jest.spyOn(ScheduleService, 'checkScheduleOverlapByTime') + .mockImplementation(async (userId, start, end) => { + return start === 40 && end === 42 && userId === 1; + }); + + const meeting = await MeetingService.createMeeting({ + title: '팀 미팅', + time_idx_start: 40, + time_idx_end: 42, + created_by: 2, + type: 'OPEN', + time_idx_deadline: 39, + location: 'Room A' + }); + + await MeetingService.joinMeeting(meeting.meeting_id, 3); + + const meetingDetail = await MeetingService.getMeetingDetail(meeting.meeting_id, 1); + + expect(meetingDetail.title).toBe('팀 미팅'); + expect(meetingDetail.isScheduleConflict).toBe(true); + expect(meetingDetail.creatorName).toBe('Bob'); + }); + + test('여러 사용자 관점에서의 미팅 조회', async () => { + const meeting = await MeetingService.createMeeting({ + title: '공통 미팅', + time_idx_start: 50, + time_idx_end: 52, + created_by: 1, + type: 'OPEN', + location: 'Room A', + time_idx_deadline: 49 + }); + + await ScheduleService.createSchedules({ + userId: 2, + title: '개인 일정', + is_fixed: true, + events: [{ time_idx: 51 }] + }); + + const {content: aliceMeetings } = await MeetingService.getMeetings(1, {limit: 20, offset: 0}); + const {content: bobMeetings } = await MeetingService.getMeetings(2, {limit: 20, offset: 0}); + const {content: charlieMeetings } = await MeetingService.getMeetings(3, {limit: 20, offset: 0}); + + expect(aliceMeetings[0].isScheduleConflict).toBe(false); + expect(bobMeetings[0].isScheduleConflict).toBe(true); + expect(charlieMeetings[0].isScheduleConflict).toBe(false); + + expect(aliceMeetings[0].isParticipant).toBe(true); + expect(bobMeetings[0].isParticipant).toBe(false); + expect(charlieMeetings[0].isParticipant).toBe(false); + }); }); - describe('MeetingService - Integration: createMeeting, joinMeeting, getMeetings', () => { - beforeEach(async () => { - await MeetingParticipant.destroy({ where: {} }); - await Meeting.destroy({ where: {} }); - await Schedule.destroy({ where: {} }); - await User.destroy({ where: {} }); - - // Create dummy users - await User.bulkCreate([ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' }, - { id: 3, name: 'Charlie', email: 'charlie@example.com' }, - ]); - }); - - test('should create a meeting, allow multiple users to join, and retrieve them correctly', async () => { - // Step 1: Create a meeting - const meetingData = { - title: 'Integration Test Meeting', - description: 'Test meeting for integration.', - time_idx_start: 10, - time_idx_end: 20, - location: 'Conference Room A', - time_idx_deadline: 8, - type: 'OPEN', - created_by: 1, - }; - - const createdMeeting = await MeetingService.createMeeting(meetingData); - - expect(createdMeeting).toBeDefined(); - expect(createdMeeting.meeting_id).toBeDefined(); - expect(createdMeeting.chatRoomId).toBeDefined(); - - // Step 2: Bob and Charlie join the meeting - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); // Ensure deadline is not passed - await MeetingService.joinMeeting(createdMeeting.meeting_id, 2); // Bob joins - await MeetingService.joinMeeting(createdMeeting.meeting_id, 3); // Charlie joins - - // Step 3: Retrieve meetings for Alice (creator) - const aliceMeetings = await MeetingService.getMeetings(1); - expect(aliceMeetings).toBeDefined(); - expect(aliceMeetings.length).toBe(1); - - const aliceMeeting = aliceMeetings[0]; - expect(aliceMeeting.title).toBe('Integration Test Meeting'); - expect(aliceMeeting.creatorName).toBe('Alice'); - expect(aliceMeeting.isParticipant).toBe(true); - - // Step 4: Retrieve meetings for Bob (participant) - const bobMeetings = await MeetingService.getMeetings(2); - expect(bobMeetings).toBeDefined(); - expect(bobMeetings.length).toBe(1); - - const bobMeeting = bobMeetings[0]; - expect(bobMeeting.title).toBe('Integration Test Meeting'); - expect(bobMeeting.creatorName).toBe('Alice'); - expect(bobMeeting.isParticipant).toBe(true); - - // Step 5: Retrieve meetings for Charlie (participant) - const charlieMeetings = await MeetingService.getMeetings(3); - expect(charlieMeetings).toBeDefined(); - expect(charlieMeetings.length).toBe(1); - - const charlieMeeting = charlieMeetings[0]; - expect(charlieMeeting.title).toBe('Integration Test Meeting'); - expect(charlieMeeting.creatorName).toBe('Alice'); - expect(charlieMeeting.isParticipant).toBe(true); - }); - - test('should not allow joining a meeting after the deadline', async () => { - const meetingData = { - title: 'Deadline Test Meeting', - description: 'Meeting to test deadlines.', - time_idx_start: 30, - time_idx_end: 40, - location: 'Conference Room B', - time_idx_deadline: 25, - type: 'OPEN', - created_by: 1, // Alice creates the meeting - }; - - const createdMeeting = await MeetingService.createMeeting(meetingData); - - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(26); // Simulate time after the deadline - - await expect( - MeetingService.joinMeeting(createdMeeting.meeting_id, 2) - ).rejects.toThrow('참가 신청이 마감되었습니다.'); - }); - - test('should prevent duplicate joining of a meeting', async () => { - const meetingData = { - title: 'Duplicate Join Test Meeting', - description: 'Meeting to test duplicate join handling.', - time_idx_start: 50, - time_idx_end: 60, - location: 'Conference Room C', - time_idx_deadline: 48, - type: 'OPEN', - created_by: 1, // Alice creates the meeting - }; - - const createdMeeting = await MeetingService.createMeeting(meetingData); - - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(45); // Ensure deadline is not passed - await MeetingService.joinMeeting(createdMeeting.meeting_id, 2); // Bob joins - - // Attempt duplicate join - await expect( - MeetingService.joinMeeting(createdMeeting.meeting_id, 2) - ).rejects.toThrow('이미 참가한 사용자입니다.'); - }); - - test('should prevent joining when schedule conflicts', async () => { - const meetingData = { - title: 'Conflict Test Meeting', - description: 'Meeting to test schedule conflict.', - time_idx_start: 70, - time_idx_end: 80, - location: 'Conference Room D', - time_idx_deadline: 68, - type: 'OPEN', - created_by: 1, // Alice creates the meeting - }; - - const createdMeeting = await MeetingService.createMeeting(meetingData); - - // Step 1: Virtually set current time before the deadline - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(65); // 현재 시간이 데드라인보다 작음 - - // Step 2: Simulate schedule conflict - jest.spyOn(ScheduleService, 'checkScheduleOverlapByTime').mockResolvedValue(true); // 스케줄 충돌 발생 - - // Step 3: Expect schedule conflict error - await expect( - MeetingService.joinMeeting(createdMeeting.meeting_id, 2) - ).rejects.toThrow('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); -}); + beforeEach(async () => { + await MeetingParticipant.destroy({ where: {} }); + await Meeting.destroy({ where: {} }); + await Schedule.destroy({ where: {} }); + await User.destroy({ where: {} }); + + // Create dummy users + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' }, + ]); + }); + + test('should create a meeting, allow multiple users to join, and retrieve them correctly', async () => { + // Step 1: Create a meeting + const meetingData = { + title: 'Integration Test Meeting', + description: 'Test meeting for integration.', + time_idx_start: 10, + time_idx_end: 20, + location: 'Conference Room A', + time_idx_deadline: 8, + type: 'OPEN', + created_by: 1, + }; + + const createdMeeting = await MeetingService.createMeeting(meetingData); + + expect(createdMeeting).toBeDefined(); + expect(createdMeeting.meeting_id).toBeDefined(); + expect(createdMeeting.chatRoomId).toBeDefined(); + + // Step 2: Bob and Charlie join the meeting + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); // Ensure deadline is not passed + await MeetingService.joinMeeting(createdMeeting.meeting_id, 2); // Bob joins + await MeetingService.joinMeeting(createdMeeting.meeting_id, 3); // Charlie joins + + // Step 3: Retrieve meetings for Alice (creator) + const {content: aliceMeetings } = await MeetingService.getMeetings(1, {limit: 20, offset: 0}); + expect(aliceMeetings).toBeDefined(); + expect(aliceMeetings.length).toBe(1); + + const aliceMeeting = aliceMeetings[0]; + expect(aliceMeeting.title).toBe('Integration Test Meeting'); + expect(aliceMeeting.creatorName).toBe('Alice'); + expect(aliceMeeting.isParticipant).toBe(true); + + // Step 4: Retrieve meetings for Bob (participant) + const {content: bobMeetings } = await MeetingService.getMeetings(2, {limit: 20, offset: 0}); + expect(bobMeetings).toBeDefined(); + expect(bobMeetings.length).toBe(1); + + const bobMeeting = bobMeetings[0]; + expect(bobMeeting.title).toBe('Integration Test Meeting'); + expect(bobMeeting.creatorName).toBe('Alice'); + expect(bobMeeting.isParticipant).toBe(true); + + // Step 5: Retrieve meetings for Charlie (participant) + const {content: charlieMeetings } = await MeetingService.getMeetings(3, {limit: 20, offset: 0}); + expect(charlieMeetings).toBeDefined(); + expect(charlieMeetings.length).toBe(1); + + const charlieMeeting = charlieMeetings[0]; + expect(charlieMeeting.title).toBe('Integration Test Meeting'); + expect(charlieMeeting.creatorName).toBe('Alice'); + expect(charlieMeeting.isParticipant).toBe(true); + }); + + test('should not allow joining a meeting after the deadline', async () => { + const meetingData = { + title: 'Deadline Test Meeting', + description: 'Meeting to test deadlines.', + time_idx_start: 30, + time_idx_end: 40, + location: 'Conference Room B', + time_idx_deadline: 25, + type: 'OPEN', + created_by: 1, // Alice creates the meeting + }; + + const createdMeeting = await MeetingService.createMeeting(meetingData); + + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(26); // Simulate time after the deadline + + await expect( + MeetingService.joinMeeting(createdMeeting.meeting_id, 2) + ).rejects.toThrow('참가 신청이 마감되었습니다.'); + }); + + test('should prevent duplicate joining of a meeting', async () => { + const meetingData = { + title: 'Duplicate Join Test Meeting', + description: 'Meeting to test duplicate join handling.', + time_idx_start: 50, + time_idx_end: 60, + location: 'Conference Room C', + time_idx_deadline: 48, + type: 'OPEN', + created_by: 1, // Alice creates the meeting + }; + + const createdMeeting = await MeetingService.createMeeting(meetingData); + + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(45); // Ensure deadline is not passed + await MeetingService.joinMeeting(createdMeeting.meeting_id, 2); // Bob joins + + // Attempt duplicate join + await expect( + MeetingService.joinMeeting(createdMeeting.meeting_id, 2) + ).rejects.toThrow('이미 참가한 사용자입니다.'); + }); + + test('should prevent joining when schedule conflicts', async () => { + const meetingData = { + title: 'Conflict Test Meeting', + description: 'Meeting to test schedule conflict.', + time_idx_start: 70, + time_idx_end: 80, + location: 'Conference Room D', + time_idx_deadline: 68, + type: 'OPEN', + created_by: 1, // Alice creates the meeting + }; + + const createdMeeting = await MeetingService.createMeeting(meetingData); + + // Step 1: Virtually set current time before the deadline + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(65); // 현재 시간이 데드라인보다 작음 + + // Step 2: Simulate schedule conflict + jest.spyOn(ScheduleService, 'checkScheduleOverlapByTime').mockResolvedValue(true); // 스케줄 충돌 발생 + + // Step 3: Expect schedule conflict error + await expect( + MeetingService.joinMeeting(createdMeeting.meeting_id, 2) + ).rejects.toThrow('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); + }); }); describe('MeetingService2', () => { - beforeEach(async () => { - // 데이터베이스 초기화 - await MeetingParticipant.destroy({ where: {} }); - await Meeting.destroy({ where: {} }); - await Schedule.destroy({ where: {} }); - await User.destroy({ where: {} }); - - // 더미 사용자 생성 - await User.bulkCreate([ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' }, - { id: 3, name: 'Charlie', email: 'charlie@example.com' }, - ]); - }); - - test('사용자가 여러 모임에 참여하고 이를 정확히 조회할 수 있어야 한다', async () => { - // 1단계: 겹치지 않는 시간대의 모임 생성 - const meetingData1 = { - title: 'Morning Meeting', - description: 'Morning planning meeting.', - time_idx_start: 10, - time_idx_end: 20, - location: 'Room A', - time_idx_deadline: 8, - type: 'OPEN', - created_by: 1, // Alice가 모임 생성 - }; - - const meetingData2 = { - title: 'Lunch Meeting', - description: 'Lunch and discussion.', - time_idx_start: 30, - time_idx_end: 40, - location: 'Room B', - time_idx_deadline: 28, - type: 'OPEN', - created_by: 2, // Bob이 모임 생성 - }; - - const meeting1 = await MeetingService.createMeeting(meetingData1); - const meeting2 = await MeetingService.createMeeting(meetingData2); - - // 모임 생성 확인 - expect(meeting1).toBeDefined(); - expect(meeting2).toBeDefined(); - - // 2단계: Charlie가 두 모임에 참여 - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); // 마감 시간을 초과하지 않도록 설정 - await MeetingService.joinMeeting(meeting1.meeting_id, 3); // Charlie가 Morning Meeting 참여 - await MeetingService.joinMeeting(meeting2.meeting_id, 3); // Charlie가 Lunch Meeting 참여 - - // 3단계: Charlie의 참여 모임 조회 - const charlieMeetings = await MeetingService.getMeetings(3); // Charlie의 사용자 ID - expect(charlieMeetings).toBeDefined(); - expect(Array.isArray(charlieMeetings)).toBe(true); - expect(charlieMeetings.length).toBe(2); // Charlie는 2개의 모임에 참여 - - // 각 모임의 세부 정보 확인 - const morningMeeting = charlieMeetings.find(meeting => meeting.title === 'Morning Meeting'); - const lunchMeeting = charlieMeetings.find(meeting => meeting.title === 'Lunch Meeting'); - - expect(morningMeeting).toBeDefined(); - expect(morningMeeting.creatorName).toBe('Alice'); - expect(morningMeeting.isParticipant).toBe(true); - - expect(lunchMeeting).toBeDefined(); - expect(lunchMeeting.creatorName).toBe('Bob'); - expect(lunchMeeting.isParticipant).toBe(true); - - // 추가 검증: 각 모임에 대한 Charlie의 스케줄이 올바르게 생성되었는지 확인 - const charlieSchedules = await Schedule.findAll({ where: { user_id: 3 } }); - expect(charlieSchedules.length).toBe(2 * (20 - 10 + 1)); // 두 모임, 각 모임마다 11개의 스케줄 (10~20, 30~40) - - // 중복 스케줄이 없는지 확인 - const timeIndicesMorning = charlieSchedules - .filter(schedule => schedule.title === `번개 모임: ${meetingData1.title}`) - .map(schedule => schedule.time_idx); - const timeIndicesLunch = charlieSchedules - .filter(schedule => schedule.title === `번개 모임: ${meetingData2.title}`) - .map(schedule => schedule.time_idx); - - // Morning Meeting의 시간대 확인 - for (let i = 10; i <= 20; i++) { - expect(timeIndicesMorning).toContain(i); - } - - // Lunch Meeting의 시간대 확인 - for (let i = 30; i <= 40; i++) { - expect(timeIndicesLunch).toContain(i); - } - }); - - test('각 사용자의 모임을 정확히 조회해야 한다', async () => { - // 1단계: 겹치지 않는 시간대의 모임 생성 - const meetingData1 = { - title: 'Morning Meeting', - description: 'Morning planning meeting.', - time_idx_start: 10, - time_idx_end: 20, - location: 'Room A', - time_idx_deadline: 8, - type: 'OPEN', - created_by: 1, // Alice가 모임 생성 - }; - - const meetingData2 = { - title: 'Lunch Meeting', - description: 'Lunch and discussion.', - time_idx_start: 30, - time_idx_end: 40, - location: 'Room B', - time_idx_deadline: 28, - type: 'OPEN', - created_by: 2, // Bob이 모임 생성 - }; - - const meeting1 = await MeetingService.createMeeting(meetingData1); - const meeting2 = await MeetingService.createMeeting(meetingData2); - - // 2단계: Charlie가 두 모임에 참여 - jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); // 마감 시간을 초과하지 않도록 설정 - await MeetingService.joinMeeting(meeting1.meeting_id, 3); // Charlie가 Morning Meeting 참여 - await MeetingService.joinMeeting(meeting2.meeting_id, 3); // Charlie가 Lunch Meeting 참여 - - // 3단계: Alice의 모임 조회 - const aliceMeetings = await MeetingService.getMeetings(1); // Alice의 사용자 ID - expect(aliceMeetings.length).toBe(1); // Alice는 하나의 모임 생성 - expect(aliceMeetings[0].title).toBe('Morning Meeting'); - expect(aliceMeetings[0].isParticipant).toBe(true); - - // 4단계: Bob의 모임 조회 - const bobMeetings = await MeetingService.getMeetings(2); // Bob의 사용자 ID - expect(bobMeetings.length).toBe(1); // Bob은 하나의 모임 생성 - expect(bobMeetings[0].title).toBe('Lunch Meeting'); - expect(bobMeetings[0].isParticipant).toBe(true); - - // 5단계: Charlie의 모임 조회 - const charlieMeetings = await MeetingService.getMeetings(3); // Charlie의 사용자 ID - expect(charlieMeetings.length).toBe(2); // Charlie는 두 모임에 참여 - const meetingTitles = charlieMeetings.map(meeting => meeting.title); - expect(meetingTitles).toContain('Morning Meeting'); - expect(meetingTitles).toContain('Lunch Meeting'); - - // 추가 검증: 각 사용자의 스케줄을 확인하여 충돌이 없는지 확인 - const aliceSchedules = await Schedule.findAll({ where: { user_id: 1 } }); - expect(aliceSchedules.length).toBe(11); // Morning Meeting: 10-20 - - const bobSchedules = await Schedule.findAll({ where: { user_id: 2 } }); - expect(bobSchedules.length).toBe(11); // Lunch Meeting: 30-40 - - const charlieSchedules = await Schedule.findAll({ where: { user_id: 3 } }); - expect(charlieSchedules.length).toBe(22); // 두 모임, 각 모임마다 11개의 스케줄 (10~20, 30~40) - }); + beforeEach(async () => { + // 데이터베이스 초기화 + await MeetingParticipant.destroy({ where: {} }); + await Meeting.destroy({ where: {} }); + await Schedule.destroy({ where: {} }); + await User.destroy({ where: {} }); + + // 더미 사용자 생성 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' }, + ]); + }); + + test('사용자가 여러 모임에 참여하고 이를 정확히 조회할 수 있어야 한다', async () => { + // 1단계: 겹치지 않는 시간대의 모임 생성 + const meetingData1 = { + title: 'Morning Meeting', + description: 'Morning planning meeting.', + time_idx_start: 10, + time_idx_end: 20, + location: 'Room A', + time_idx_deadline: 8, + type: 'OPEN', + created_by: 1, // Alice가 모임 생성 + }; + + const meetingData2 = { + title: 'Lunch Meeting', + description: 'Lunch and discussion.', + time_idx_start: 30, + time_idx_end: 40, + location: 'Room B', + time_idx_deadline: 28, + type: 'OPEN', + created_by: 2, // Bob이 모임 생성 + }; + + const meeting1 = await MeetingService.createMeeting(meetingData1); + const meeting2 = await MeetingService.createMeeting(meetingData2); + + // 모임 생성 확인 + expect(meeting1).toBeDefined(); + expect(meeting2).toBeDefined(); + + // 2단계: Charlie가 두 모임에 참여 + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); // 마감 시간을 초과하지 않도록 설정 + await MeetingService.joinMeeting(meeting1.meeting_id, 3); // Charlie가 Morning Meeting 참여 + await MeetingService.joinMeeting(meeting2.meeting_id, 3); // Charlie가 Lunch Meeting 참여 + + // 3단계: Charlie의 참여 모임 조회 + const {content: charlieMeetings } = await MeetingService.getMeetings(3, {limit: 20, offset: 0}); // Charlie의 사용자 ID + expect(charlieMeetings).toBeDefined(); + expect(Array.isArray(charlieMeetings)).toBe(true); + expect(charlieMeetings.length).toBe(2); // Charlie는 2개의 모임에 참여 + + // 각 모임의 세부 정보 확인 + const morningMeeting = charlieMeetings.find(meeting => meeting.title === 'Morning Meeting'); + const lunchMeeting = charlieMeetings.find(meeting => meeting.title === 'Lunch Meeting'); + + expect(morningMeeting).toBeDefined(); + expect(morningMeeting.creatorName).toBe('Alice'); + expect(morningMeeting.isParticipant).toBe(true); + + expect(lunchMeeting).toBeDefined(); + expect(lunchMeeting.creatorName).toBe('Bob'); + expect(lunchMeeting.isParticipant).toBe(true); + + // 추가 검증: 각 모임에 대한 Charlie의 스케줄이 올바르게 생성되었는지 확인 + const charlieSchedules = await Schedule.findAll({ where: { user_id: 3 } }); + expect(charlieSchedules.length).toBe(2 * (20 - 10 + 1)); // 두 모임, 각 모임마다 11개의 스케줄 (10~20, 30~40) + + // 중복 스케줄이 없는지 확인 + const timeIndicesMorning = charlieSchedules + .filter(schedule => schedule.title === `번개 모임: ${meetingData1.title}`) + .map(schedule => schedule.time_idx); + const timeIndicesLunch = charlieSchedules + .filter(schedule => schedule.title === `번개 모임: ${meetingData2.title}`) + .map(schedule => schedule.time_idx); + + // Morning Meeting의 시간대 확인 + for (let i = 10; i <= 20; i++) { + expect(timeIndicesMorning).toContain(i); + } + + // Lunch Meeting의 시간대 확인 + for (let i = 30; i <= 40; i++) { + expect(timeIndicesLunch).toContain(i); + } + }); + + test('각 사용자의 모임을 정확히 조회해야 한다', async () => { + // 1단계: 겹치지 않는 시간대의 모임 생성 + const meetingData1 = { + title: 'Morning Meeting', + time_idx_start: 10, + time_idx_end: 20, + location: 'Room A', + time_idx_deadline: 8, + type: 'OPEN', + created_by: 1 + }; + + const meetingData2 = { + title: 'Lunch Meeting', + time_idx_start: 30, + time_idx_end: 40, + location: 'Room B', + time_idx_deadline: 28, + type: 'OPEN', + created_by: 2 + }; + + const meeting1 = await MeetingService.createMeeting(meetingData1); + const meeting2 = await MeetingService.createMeeting(meetingData2); + + // 2단계: Charlie가 두 모임에 참여 + jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(5); + await MeetingService.joinMeeting(meeting1.meeting_id, 3); + await MeetingService.joinMeeting(meeting2.meeting_id, 3); + + // 3단계: 각 사용자의 모임 조회 + const pagination = {limit: 20, offset:0 }; + const {content: aliceMeetings} = await MeetingService.getMeetings(1, pagination); + const {content: bobMeetings} = await MeetingService.getMeetings(2, pagination); + const {content: charlieMeetings} = await MeetingService.getMeetings(3, pagination); + + // 모든 미팅이 조회되어야 함 + expect(aliceMeetings.length).toBe(2); + expect(bobMeetings.length).toBe(2); + expect(charlieMeetings.length).toBe(2); + + // 참가 여부 확인 + const aliceMorningMeeting = aliceMeetings.find(m => m.title === 'Morning Meeting'); + expect(aliceMorningMeeting.isParticipant).toBe(true); + + const bobLunchMeeting = bobMeetings.find(m => m.title === 'Lunch Meeting'); + expect(bobLunchMeeting.isParticipant).toBe(true); + + // Charlie는 두 미팅 모두 참가 + expect(charlieMeetings.every(m => m.isParticipant)).toBe(true); + }); });