diff --git a/dtos/CreateMeetingRequestDTO.js b/dtos/CreateMeetingRequestDTO.js index 2c106f1b2826e1dfce53ce85d62ebe614b26fc64..c61b0691683a7b2bdd7e96de219c732072065ec1 100644 --- a/dtos/CreateMeetingRequestDTO.js +++ b/dtos/CreateMeetingRequestDTO.js @@ -3,14 +3,16 @@ const Joi = require('joi'); class CreateMeetingRequestDTO { constructor({ title, description, time_idx_start, time_idx_end, location, time_idx_deadline, type, created_by }) { - this.title = title; - this.description = description; - this.time_idx_start = time_idx_start; - this.time_idx_end = time_idx_end; - this.location = location; - this.time_idx_deadline = time_idx_deadline; - this.type = type; - this.created_by = created_by; + this.data = { + title, + description, + time_idx_start, + time_idx_end, + location, + time_idx_deadline, + type, + created_by, + }; } validate() { @@ -22,13 +24,13 @@ class CreateMeetingRequestDTO { location: Joi.string().allow('', null).optional(), time_idx_deadline: Joi.number().integer().min(0).less(Joi.ref('time_idx_start')).optional(), type: Joi.string().valid('OPEN', 'CLOSE').required(), - created_by: Joi.number().integer().positive().required() + created_by: Joi.number().integer().positive().required(), }); - const { error } = schema.validate(this, { abortEarly: false }); + const { error } = schema.validate(this.data, { abortEarly: false }); if (error) { - const errorMessages = error.details.map(detail => detail.message).join(', '); + const errorMessages = error.details.map((detail) => detail.message).join(', '); throw new Error(`Validation error: ${errorMessages}`); } diff --git a/services/meetingService.js b/services/meetingService.js index bd7da9e39b480befca478a541547b53d055c26e2..0d87a0c3026ddbf822ad70d09cd2ccb6ac8be674 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -3,17 +3,18 @@ const { v4: uuidv4 } = require('uuid'); const { Op } = require('sequelize'); const sequelize = require('../config/sequelize'); // 트랜잭션 관리를 위해 sequelize 인스턴스 필요 const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); -const ChatRooms = require('../models/ChatRooms'); +const ChatRooms = require('../models/ChatRooms'); + const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); -const ScheduleService = require('./scheduleService'); +const ScheduleService = require('./scheduleService'); class MeetingService { /** * 현재 시간을 time_idx로 변환하는 유틸리티 함수 * 월요일부터 일요일까지 15분 단위로 타임 인덱스를 할당 - * 현재 시간의 타임 인덱스 (0 ~ 671) + * 현재 시간의 타임 인덱스 (0 ~ 671) */ getCurrentTimeIdx() { const today = new Date(); @@ -36,7 +37,16 @@ class MeetingService { const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); createMeetingDTO.validate(); - const { title, description, time_idx_start, time_idx_end, location, time_idx_deadline, type, created_by } = meetingData; + const { + title, + description, + time_idx_start, + time_idx_end, + location, + time_idx_deadline, + type, + created_by, + } = meetingData; // 사용자 존재 여부 확인 const user = await User.findOne({ where: { id: created_by } }); @@ -54,40 +64,51 @@ class MeetingService { messages: [], lastReadAt: {}, lastReadLogId: {}, - isOnline: {} + isOnline: {}, }; const chatRoom = new ChatRooms(chatRoomData); await chatRoom.save(); // 모임 생성 - const newMeeting = await Meeting.create({ - title, - description, - time_idx_start, - time_idx_end, - location, - time_idx_deadline, - type, - created_by, - chatRoomId, - }, { transaction }); + const newMeeting = await Meeting.create( + { + title, + description, + time_idx_start, + time_idx_end, + location, + time_idx_deadline, + type, + created_by, + chatRoomId, + }, + { transaction } + ); // 모임 참가자 추가 (생성자 자신) - await MeetingParticipant.create({ - meeting_id: newMeeting.id, - user_id: created_by, - }, { transaction }); - - // 스케줄 생성 - await ScheduleService.createSchedules({ - userId: created_by, - title: `번개 모임: ${title}`, - is_fixed: true, - events: [ - { time_idx: time_idx_start }, - { time_idx: time_idx_end }, - ], - }, transaction); + await MeetingParticipant.create( + { + meeting_id: newMeeting.id, + user_id: created_by, + }, + { transaction } + ); + + // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) + const events = []; + for (let idx = time_idx_start; idx <= time_idx_end; idx++) { + events.push({ time_idx: idx }); + } + + await ScheduleService.createSchedules( + { + userId: created_by, + title: `번개 모임: ${title}`, + is_fixed: false, + events: events, + }, + transaction + ); return { meeting_id: newMeeting.id, chatRoomId }; }); @@ -102,7 +123,16 @@ class MeetingService { */ async getMeetings(userId) { const meetings = await Meeting.findAll({ - attributes: ['id', 'title', 'description', 'time_idx_start', 'time_idx_end', 'location', 'time_idx_deadline', 'type'], + attributes: [ + 'id', + 'title', + 'description', + 'time_idx_start', + 'time_idx_end', + 'location', + 'time_idx_deadline', + 'type', + ], include: [ { model: User, @@ -119,14 +149,11 @@ class MeetingService { return meetings.map((meeting) => { const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); - - return new MeetingResponseDTO( - meeting, - isParticipant, - false, - creatorName + const isParticipant = meeting.participants.some( + (participant) => participant.user_id === parseInt(userId, 10) ); + + return new MeetingResponseDTO(meeting, isParticipant, false, creatorName); }); } @@ -168,13 +195,13 @@ class MeetingService { if (meeting.time_idx_deadline !== undefined) { const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스 - if (currentTimeIdx > meeting.time_idx_deadline) { + if (currentTimeIdx >= meeting.time_idx_deadline) { throw new Error('참가 신청이 마감되었습니다.'); } } const existingParticipant = await MeetingParticipant.findOne({ - where: { meeting_id: meetingId, user_id: userId } + where: { meeting_id: meetingId, user_id: userId }, }); if (existingParticipant) { @@ -183,9 +210,6 @@ class MeetingService { // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 await sequelize.transaction(async (transaction) => { - // 참가자 추가 - await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction }); - // 스케줄 충돌 확인 const hasConflict = await ScheduleService.checkScheduleOverlapByTime( userId, @@ -197,16 +221,27 @@ class MeetingService { throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); } - // 스케줄 추가 - await ScheduleService.createSchedules({ - userId: userId, - title: `번개 모임: ${meeting.title}`, - is_fixed: true, - events: [ - { time_idx: meeting.time_idx_start }, - { time_idx: meeting.time_idx_end }, - ], - }, transaction); + // 참가자 추가 + await MeetingParticipant.create( + { meeting_id: meetingId, user_id: userId }, + { transaction } + ); + + // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) + const events = []; + for (let idx = meeting.time_idx_start; idx <= meeting.time_idx_end; idx++) { + events.push({ time_idx: idx }); + } + + await ScheduleService.createSchedules( + { + userId: userId, + title: `번개 모임: ${meeting.title}`, + is_fixed: true, + events: events, + }, + transaction + ); // 채팅방 참가 (MongoDB) const user = await User.findOne({ where: { id: userId }, transaction }); @@ -217,7 +252,7 @@ class MeetingService { chatRoom.isOnline.set(user.name, true); chatRoom.lastReadAt.set(user.name, new Date()); chatRoom.lastReadLogId.set(user.name, null); - await chatRoom.save(); + await chatRoom.save(); } }); } @@ -233,7 +268,7 @@ class MeetingService { { model: User, as: 'creator', - attributes: ['name'] + attributes: ['name'], }, { model: MeetingParticipant, @@ -242,11 +277,11 @@ class MeetingService { { model: User, as: 'participantUser', - attributes: ['name', 'email'] - } - ] - } - ] + attributes: ['name', 'email'], + }, + ], + }, + ], }); if (!meeting) { diff --git a/services/meetingService.test.js b/services/meetingService.test.js index 7515718463fdbdeba6370b207d2e1f940b59f0a1..6471bba41056871164ed79e86ce6f2ea6cd5c6ab 100644 --- a/services/meetingService.test.js +++ b/services/meetingService.test.js @@ -1,161 +1,375 @@ // test/meetingService.test.js - -// 1. ScheduleService 모킹 -jest.mock('../services/scheduleService', () => ({ - createSchedules: jest.fn(), - checkScheduleOverlapByTime: jest.fn(), -})); - -// 2. ChatRooms 모킹 -jest.mock('../models/ChatRooms', () => { - const mockChatRooms = jest.fn().mockImplementation((chatRoomData) => { - return { - chatRoomId: chatRoomData.chatRoomId || 'chatroom-1234', - participants: chatRoomData.participants || [], - messages: chatRoomData.messages || [], - lastReadAt: chatRoomData.lastReadAt || {}, - lastReadLogId: chatRoomData.lastReadLogId || {}, - isOnline: chatRoomData.isOnline || {}, - save: jest.fn().mockResolvedValue(true), // save 메서드 모킹 - }; - }); - - mockChatRooms.findOne = jest.fn().mockResolvedValue({ - chatRoomId: 'chatroom-1234', - participants: ['Alice'], - messages: [], - lastReadAt: {}, - lastReadLogId: {}, - isOnline: {}, - save: jest.fn().mockResolvedValue(true), - }); - - return mockChatRooms; -}); - -// 3. 모킹 이후에 서비스와 의존성 임포트 -const models = require('../models'); // models/index.js에서 모델과 관계를 가져옴 -const { User, Meeting, MeetingParticipant } = models; -const sequelize = models.sequelize; -const MeetingService = require('../services/meetingService'); // 테스트할 서비스 +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'); // MongoDB 모델 +const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); +const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); +const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); +// ChatRooms 모듈 전체를 모킹하지 않고, 필요한 메서드만 선택적으로 모킹합니다. beforeAll(async () => { - await sequelize.sync({ force: true }); // 테스트 데이터베이스 초기화 + // 테스트 스위트가 시작되기 전에 데이터베이스를 동기화합니다. + await sequelize.sync({ force: true }); }); beforeEach(async () => { - // 각 테스트 전에 데이터베이스 초기화 - await sequelize.sync({ force: true }); + // 각 테스트가 시작되기 전에 기존 데이터를 삭제합니다. + 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' }, + ]); - // 더미 사용자 생성 (ID 자동 증가) - await User.create({ name: 'Alice', email: 'alice@example.com' }); - await User.create({ name: 'Bob', email: 'bob@example.com' }); + // 더미 스케줄 생성 (이미 존재하는 스케줄로 스케줄 겹침 테스트에 사용) + await Schedule.bulkCreate([ + { id: 1, user_id: 1, title: 'Alice Schedule', time_idx: 50, is_fixed: true }, + { id: 2, user_id: 2, title: 'Bob Schedule', time_idx: 60, is_fixed: true }, + ]); - // 생성된 사용자 ID 가져오기 - const alice = await User.findOne({ where: { email: 'alice@example.com' } }); - const bob = await User.findOne({ where: { email: 'bob@example.com' } }); + // ChatRooms 모킹 설정 + jest.clearAllMocks(); - // 사용자 ID를 테스트에서 사용하기 위해 저장 - global.aliceId = alice.id; - global.bobId = bob.id; + // ChatRooms 인스턴스 메서드 모킹 + jest.spyOn(ChatRooms.prototype, 'save').mockImplementation(() => Promise.resolve()); + + // ChatRooms 스태틱 메서드 모킹 + jest.spyOn(ChatRooms, 'findOne').mockImplementation(async () => { + return { + participants: [], + isOnline: new Map(), + lastReadAt: new Map(), + lastReadLogId: new Map(), + save: jest.fn().mockResolvedValue(true), + }; + }); }); afterAll(async () => { - await sequelize.close(); // 테스트 후 Sequelize 연결 종료 + await sequelize.close(); }); -describe('Meeting Service', () => { - describe('createMeeting', () => { - test('should create a new meeting successfully', async () => { - // Arrange - const meetingData = { - title: '팀 동기화 미팅', - description: '월간 팀 동기화 회의입니다.', - time_idx_start: 40, - time_idx_end: 42, - location: '회의실 A', - time_idx_deadline: 38, - type: 'OPEN', - created_by: global.aliceId, - }; - - // Mock ScheduleService.createSchedules가 성공적으로 동작하도록 설정 - ScheduleService.createSchedules.mockResolvedValue(true); - - // Act - const result = await MeetingService.createMeeting(meetingData); - - // Assert - expect(result).toHaveProperty('meeting_id'); - expect(result).toHaveProperty('chatRoomId'); - - // ChatRooms가 올바르게 호출되었는지 확인 - expect(ChatRooms).toHaveBeenCalledWith({ - chatRoomId: expect.any(String), - participants: ['Alice'], - messages: [], - lastReadAt: {}, - lastReadLogId: {}, - isOnline: {}, - }); - - // ChatRooms 인스턴스의 save 메서드가 호출되었는지 확인 - const chatRoomInstance = ChatRooms.mock.instances[0]; - expect(chatRoomInstance).toBeDefined(); - expect(jest.isMockFunction(chatRoomInstance.save)).toBe(true); - expect(chatRoomInstance.save).toHaveBeenCalled(); - - // Meeting이 올바르게 생성되었는지 확인 - const createdMeeting = await Meeting.findOne({ where: { id: result.meeting_id } }); - expect(createdMeeting).toBeDefined(); - expect(createdMeeting.title).toBe('팀 동기화 미팅'); - - // MeetingParticipant가 올바르게 생성되었는지 확인 - const participant = await MeetingParticipant.findOne({ where: { meeting_id: result.meeting_id, user_id: global.aliceId } }); - expect(participant).toBeDefined(); - expect(participant.user_id).toBe(global.aliceId); - - // ScheduleService.createSchedules가 올바르게 호출되었는지 확인 - expect(ScheduleService.createSchedules).toHaveBeenCalledWith( - { - userId: global.aliceId, - title: '번개 모임: 팀 동기화 미팅', - is_fixed: true, - events: [{ time_idx: 40 }, { time_idx: 42 }], - }, - expect.any(Object) - ); +describe('MeetingService', () => { + describe('createMeeting', () => { + test('should create a new meeting successfully', async () => { + const meetingData = { + title: 'Team Meeting', + description: 'Weekly sync-up meeting.', + time_idx_start: 70, + time_idx_end: 72, + location: 'Conference Room A', + time_idx_deadline: 68, + type: 'OPEN', + created_by: 1, + }; + + const result = await MeetingService.createMeeting(meetingData); + + expect(result).toBeDefined(); + expect(result.meeting_id).toBeDefined(); + expect(result.chatRoomId).toBeDefined(); + + // 데이터베이스에서 모임 확인 + const meeting = await Meeting.findByPk(result.meeting_id); + expect(meeting).toBeDefined(); + expect(meeting.title).toBe('Team Meeting'); + expect(meeting.created_by).toBe(1); + + // 모임 참가자 확인 + const participants = await MeetingParticipant.findAll({ where: { meeting_id: result.meeting_id } }); + expect(participants.length).toBe(1); + expect(participants[0].user_id).toBe(1); + + // 스케줄 생성 확인 + const schedules = await Schedule.findAll({ where: { user_id: 1, title: '번개 모임: Team Meeting' } }); + expect(schedules.length).toBe(3); // time_idx_start부터 time_idx_end까지 + schedules.forEach(schedule => { + expect(schedule.is_fixed).toBe(false); // 유동 시간대 + expect(schedule.time_idx).toBeGreaterThanOrEqual(70); + expect(schedule.time_idx).toBeLessThanOrEqual(72); + }); + + // ChatRooms 모킹 확인 + expect(ChatRooms.prototype.save).toHaveBeenCalledTimes(1); + }); + + test('should throw error when user does not exist', async () => { + const meetingData = { + title: 'Invalid User Meeting', + description: 'This should fail.', + time_idx_start: 70, + time_idx_end: 72, + location: 'Conference Room A', + time_idx_deadline: 68, + type: 'OPEN', + created_by: 999, // 존재하지 않는 사용자 ID + }; + + await expect(MeetingService.createMeeting(meetingData)).rejects.toThrow('사용자를 찾을 수 없습니다.'); + }); + + test('should throw error when schedule overlaps', async () => { + // Alice는 이미 time_idx 50에 스케줄이 있음 + const meetingData = { + title: 'Overlapping Meeting', + description: 'This should fail due to schedule overlap.', + time_idx_start: 49, + time_idx_end: 51, // time_idx 50 포함 + location: 'Conference Room B', + time_idx_deadline: 48, + type: 'OPEN', + created_by: 1, + }; + + await expect(MeetingService.createMeeting(meetingData)).rejects.toThrow( + 'Schedule overlaps with existing schedule at time_idx 50' + ); + }); }); - test('should throw error when user does not exist', async () => { - // Arrange - const meetingData = { - title: '팀 동기화 미팅', - description: '월간 팀 동기화 회의입니다.', - time_idx_start: 40, - time_idx_end: 42, - location: '회의실 A', - time_idx_deadline: 38, - type: 'OPEN', - created_by: 9999, // 존재하지 않는 사용자 ID - }; - - // Act & Assert - await expect(MeetingService.createMeeting(meetingData)).rejects.toThrow('사용자를 찾을 수 없습니다.'); - - // Meeting이 생성되지 않았는지 확인 - const createdMeeting = await Meeting.findOne({ where: { title: '팀 동기화 미팅' } }); - expect(createdMeeting).toBeNull(); - - // ChatRooms과 ScheduleService.createSchedules가 호출되지 않았는지 확인 - expect(ChatRooms).not.toHaveBeenCalled(); - expect(ScheduleService.createSchedules).not.toHaveBeenCalled(); + describe('getMeetings', () => { + test('should retrieve all meetings', async () => { + // 미팅 생성 + const meetingData1 = { + title: 'Meeting 1', + description: 'First meeting.', + time_idx_start: 70, + time_idx_end: 72, + location: 'Conference Room A', + time_idx_deadline: 68, + type: 'OPEN', + created_by: 1, + }; + + const meetingData2 = { + title: 'Meeting 2', + description: 'Second meeting.', + time_idx_start: 80, + time_idx_end: 82, + location: 'Conference Room B', + time_idx_deadline: 78, + type: 'OPEN', + created_by: 2, + }; + + await MeetingService.createMeeting(meetingData1); + await MeetingService.createMeeting(meetingData2); + + const meetings = await MeetingService.getMeetings(1); // Alice의 사용자 ID + + expect(meetings).toBeDefined(); + expect(Array.isArray(meetings)).toBe(true); + expect(meetings.length).toBe(2); + + meetings.forEach(meeting => { + expect(meeting).toBeInstanceOf(MeetingResponseDTO); + expect(['Meeting 1', 'Meeting 2']).toContain(meeting.title); + expect(['OPEN']).toContain(meeting.type); + if (meeting.id === 1) { + expect(meeting.creatorName).toBe('Alice'); + expect(meeting.isParticipant).toBe(true); + } else { + expect(meeting.creatorName).toBe('Bob'); + expect(meeting.isParticipant).toBe(false); + } + }); + }); }); - // 나머지 테스트 케이스도 동일한 방식으로 수정 - }); + describe('closeMeeting', () => { + test('should close an open meeting successfully', async () => { + const meetingData = { + title: 'Meeting to Close', + description: 'This meeting will be closed.', + time_idx_start: 90, + time_idx_end: 92, + location: 'Conference Room C', + time_idx_deadline: 88, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + const closedMeeting = await MeetingService.closeMeeting(meeting_id); + + expect(closedMeeting).toBeDefined(); + expect(closedMeeting.type).toBe('CLOSE'); + + // 데이터베이스에서 확인 + const meeting = await Meeting.findByPk(meeting_id); + expect(meeting.type).toBe('CLOSE'); + }); + + test('should throw error when closing a non-existing meeting', async () => { + await expect(MeetingService.closeMeeting(999)).rejects.toThrow('모임을 찾을 수 없습니다.'); + }); + + test('should throw error when closing an already closed meeting', async () => { + const meetingData = { + title: 'Already Closed Meeting', + description: 'This meeting is already closed.', + time_idx_start: 100, + time_idx_end: 102, + location: 'Conference Room D', + time_idx_deadline: 98, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + await MeetingService.closeMeeting(meeting_id); - // 다른 테스트 케이스... + await expect(MeetingService.closeMeeting(meeting_id)).rejects.toThrow('이미 마감된 모임입니다.'); + }); + }); + + describe('joinMeeting', () => { + test('should allow a user to join an open meeting', async () => { + const meetingData = { + title: 'Open Meeting', + description: 'Users can join this meeting.', + time_idx_start: 110, + time_idx_end: 112, + location: 'Conference Room E', + time_idx_deadline: 108, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + // Bob이 참가 + await MeetingService.joinMeeting(meeting_id, 2); + + // 참가자 확인 + const participants = await MeetingParticipant.findAll({ where: { meeting_id } }); + expect(participants.length).toBe(2); // Alice와 Bob + + const participantIds = participants.map((p) => p.user_id); + expect(participantIds).toContain(1); + expect(participantIds).toContain(2); + + // Bob의 스케줄 생성 확인 + const schedules = await Schedule.findAll({ + where: { user_id: 2, title: `번개 모임: ${meetingData.title}` }, + }); + expect(schedules.length).toBe(3); // time_idx_start부터 time_idx_end까지 + schedules.forEach(schedule => { + expect(schedule.is_fixed).toBe(true); // 고정 시간대 + expect(schedule.time_idx).toBeGreaterThanOrEqual(110); + expect(schedule.time_idx).toBeLessThanOrEqual(112); + }); + + // ChatRooms 모킹 확인 + expect(ChatRooms.findOne).toHaveBeenCalledTimes(1); + const chatRoom = await ChatRooms.findOne(); + expect(chatRoom.save).toHaveBeenCalledTimes(1); + }); + + test('should throw error when joining a closed meeting', async () => { + const meetingData = { + title: 'Closed Meeting', + description: 'This meeting is closed.', + time_idx_start: 120, + time_idx_end: 122, + location: 'Conference Room F', + time_idx_deadline: 118, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + await MeetingService.closeMeeting(meeting_id); + + await expect(MeetingService.joinMeeting(meeting_id, 2)).rejects.toThrow('이미 마감된 모임입니다.'); + }); + + test('should throw error when user already joined', async () => { + const meetingData = { + title: 'Meeting with Bob', + description: 'Bob will join this meeting.', + time_idx_start: 130, + time_idx_end: 132, + location: 'Conference Room G', + time_idx_deadline: 128, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + await MeetingService.joinMeeting(meeting_id, 2); + + await expect(MeetingService.joinMeeting(meeting_id, 2)).rejects.toThrow('이미 참가한 사용자입니다.'); + }); + + test('should throw error when schedule overlaps', async () => { + // Bob은 이미 time_idx 60에 스케줄이 있음 + const meetingData = { + title: 'Overlapping Schedule Meeting', + description: 'Bob has a conflicting schedule.', + time_idx_start: 59, + time_idx_end: 61, // time_idx 60 포함 + location: 'Conference Room H', + time_idx_deadline: 58, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + await expect(MeetingService.joinMeeting(meeting_id, 2)).rejects.toThrow( + '스케줄이 겹칩니다. 다른 모임에 참가하세요.' + ); + }); + }); + + describe('getMeetingDetail', () => { + test('should retrieve meeting details', async () => { + const meetingData = { + title: 'Detailed Meeting', + description: 'This meeting has details.', + time_idx_start: 140, + time_idx_end: 142, + location: 'Conference Room I', + time_idx_deadline: 138, + type: 'OPEN', + created_by: 1, + }; + + const { meeting_id } = await MeetingService.createMeeting(meetingData); + + // Bob과 Charlie 참가 + await MeetingService.joinMeeting(meeting_id, 2); + await MeetingService.joinMeeting(meeting_id, 3); + + const meetingDetail = await MeetingService.getMeetingDetail(meeting_id); + + expect(meetingDetail).toBeDefined(); + expect(meetingDetail).toBeInstanceOf(MeetingDetailResponseDTO); + expect(meetingDetail.title).toBe('Detailed Meeting'); + expect(meetingDetail.creatorName).toBe('Alice'); + expect(meetingDetail.participants.length).toBe(3); // Alice, Bob, Charlie + + const participantNames = meetingDetail.participants.map((p) => p.name); + expect(participantNames).toContain('Alice'); + expect(participantNames).toContain('Bob'); + expect(participantNames).toContain('Charlie'); + }); + + test('should throw error when meeting does not exist', async () => { + await expect(MeetingService.getMeetingDetail(999)).rejects.toThrow('모임을 찾을 수 없습니다.'); + }); + }); }); diff --git a/services/scheduleService.js b/services/scheduleService.js index 71f99dfe664e634f2cbe1a3a9250e87318eebdd2..227bca4feafb9b68257e2454855b919211a1199e 100644 --- a/services/scheduleService.js +++ b/services/scheduleService.js @@ -131,31 +131,21 @@ class ScheduleService { return !!overlappingSchedule; } - + async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) { const overlappingSchedule = await Schedule.findOne({ where: { user_id: userId, - [Op.or]: [ - { - time_idx_start: { [Op.between]: [time_idx_start, time_idx_end] } - }, - { - time_idx_end: { [Op.between]: [time_idx_start, time_idx_end] } - }, - { - [Op.and]: [ - { time_idx_start: { [Op.lte]: time_idx_start } }, - { time_idx_end: { [Op.gte]: time_idx_end } } - ] - } - ] + time_idx: { + [Op.between]: [time_idx_start, time_idx_end] + } }, transaction, }); - + return !!overlappingSchedule; } + /** * 만료된 스케줄 삭제