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); + }); });