diff --git a/services/schedule.test.js b/services/schedule.test.js index a82562bf124a1f5044e3ecf54990531527bff78d..666dca41fdfdeec3ad6d3d7e23edb7f63e7d55b2 100644 --- a/services/schedule.test.js +++ b/services/schedule.test.js @@ -4,7 +4,7 @@ const sequelize = require('../config/sequelize'); const User = require('../models/User'); const Friend = require('../models/Friend'); const Schedule = require('../models/Schedule'); -const scheduleService = require('../services/scheduleService'); // scheduleService 임포트 +const scheduleService = require('./scheduleService'); // scheduleService 임포트 beforeAll(async () => { await sequelize.sync({ force: true }); @@ -61,6 +61,7 @@ describe('Schedule Service', () => { }; const schedule = await scheduleService.createSchedule(scheduleData); + expect(schedule).toBeDefined(); expect(schedule.user_id).toBe(2); @@ -156,19 +157,19 @@ describe('Schedule Service', () => { describe('deleteSchedule', () => { test('should delete an existing schedule successfully', async () => { - const result = await scheduleService.deleteSchedule(2, 1); + const result = await scheduleService.deleteSchedule(2, 1); - expect(result).toBe(true); + expect(result).toEqual({ message: 'Schedule successfully deleted' }); - // 삭제된 스케줄이 실제로 삭제되었는지 확인 - const schedule = await Schedule.findByPk(2); - expect(schedule).toBeNull(); + // 삭제된 스케줄이 실제로 삭제되었는지 확인 + const schedule = await Schedule.findByPk(2); + expect(schedule).toBeNull(); }); test('should throw error when deleting a non-existing schedule', async () => { - await expect(scheduleService.deleteSchedule(999, 1)).rejects.toThrow('Schedule not found'); + await expect(scheduleService.deleteSchedule(999, 1)).rejects.toThrow('Schedule not found'); }); - }); +}); describe('getAllSchedules', () => { test('should retrieve all valid schedules for a user', async () => { @@ -192,42 +193,52 @@ describe('Schedule Service', () => { await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found'); }); }); + // test/schedule.test.js describe('cleanExpiredSchedules', () => { test('should delete expired flexible schedules', async () => { - // 만료된 유동 스케줄 생성 - await Schedule.create({ - id: 3, - user_id: 1, - title: 'Expired Flexible Schedule', - start_time: new Date('2024-04-25T10:00:00Z'), - end_time: new Date('2024-04-25T11:00:00Z'), - is_fixed: false, - expiry_date: new Date('2024-04-30T00:00:00Z'), // 이미 만료됨 - }); - - // 만료되지 않은 유동 스케줄 생성 - await Schedule.create({ - id: 4, - user_id: 1, - title: 'Valid Flexible Schedule', - start_time: new Date('2024-05-07T10:00:00Z'), - end_time: new Date('2024-05-07T11:00:00Z'), - is_fixed: false, - expiry_date: new Date('2024-05-14T00:00:00Z'), // 아직 만료되지 않음 - }); - - // 만료된 스케줄 정리 - await scheduleService.cleanExpiredSchedules(); - - // 만료된 스케줄이 삭제되었는지 확인 - const expiredSchedule = await Schedule.findByPk(3); - expect(expiredSchedule).toBeNull(); - - // 만료되지 않은 스케줄은 남아있는지 확인 - const validSchedule = await Schedule.findByPk(4); - expect(validSchedule).toBeDefined(); - expect(validSchedule.title).toBe('Valid Flexible Schedule'); + // 현재 날짜를 기준으로 만료된 스케줄과 만료되지 않은 스케줄 생성 + const now = new Date('2024-05-07T00:00:00Z'); // 테스트를 위한 고정된 현재 날짜 + + // Jest의 Fake Timers를 사용하여 Date를 고정 + jest.useFakeTimers('modern'); + jest.setSystemTime(now); + + // 만료된 유동 스케줄 생성 + await Schedule.create({ + user_id: 1, + title: 'Expired Flexible Schedule', + start_time: new Date('2024-04-25T10:00:00Z'), + end_time: new Date('2024-04-25T11:00:00Z'), + is_fixed: false, + expiry_date: new Date('2024-05-06T00:00:00Z'), // 이미 만료됨 + }); + + // 만료되지 않은 유동 스케줄 생성 + await Schedule.create({ + user_id: 1, + title: 'Valid Flexible Schedule', + start_time: new Date('2024-05-07T10:00:00Z'), + end_time: new Date('2024-05-07T11:00:00Z'), + is_fixed: false, + expiry_date: new Date('2024-05-14T00:00:00Z'), // 아직 만료되지 않음 + }); + + // 만료된 스케줄 정리 + await scheduleService.cleanExpiredSchedules(); + + // 만료된 스케줄이 삭제되었는지 확인 + const expiredSchedule = await Schedule.findOne({ where: { title: 'Expired Flexible Schedule' } }); + expect(expiredSchedule).toBeNull(); + + // 만료되지 않은 스케줄은 남아있는지 확인 + const validSchedule = await Schedule.findOne({ where: { title: 'Valid Flexible Schedule' } }); + expect(validSchedule).toBeDefined(); + expect(validSchedule.title).toBe('Valid Flexible Schedule'); + + // Jest의 Fake Timers를 복구 + jest.useRealTimers(); }); - }); +}); + }); diff --git a/services/scheduleService.js b/services/scheduleService.js index f0b5e381f5e6f5039bf37722458b279afd52381a..66d0bbcc3f5d09509632cd564473b2fd14d20c31 100644 --- a/services/scheduleService.js +++ b/services/scheduleService.js @@ -5,237 +5,257 @@ const Schedule = require('../models/Schedule'); const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO'); class scheduleService { - - /** - * 트랜잭션 래퍼 함수 - */ - async withTransaction(callback) { - const transaction = await Schedule.sequelize.transaction(); - try { - const result = await callback(transaction); - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - /** - * 공통 where 절 생성 - */ - getScheduleWhereClause(userId, id = null) { - const where = { - user_id: userId, - [Op.or]: [ - { is_fixed: true }, - { - is_fixed: false, - expiry_date: { [Op.gt]: new Date() } - } - ] - }; - - if (id) { - where.id = id; - } - - return where; + /** + * 트랜잭션 래퍼 함수 + */ + async withTransaction(callback) { + const transaction = await Schedule.sequelize.transaction(); + try { + const result = await callback(transaction); + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; } - - /** - * 스케줄 유효성 검사 - */ - validateScheduleTime(start_time, end_time) { - if (new Date(start_time) >= new Date(end_time)) { - throw new Error('Start time must be before end time'); - } + } + + /** + * 공통 where 절 생성 + */ + getScheduleWhereClause(userId, id = null) { + const where = { + user_id: userId, + [Op.or]: [ + { is_fixed: true }, + { + is_fixed: false, + expiry_date: { [Op.gt]: new Date() }, + }, + ], + }; + + if (id) { + where.id = id; } - /** - * 유동 스케줄 만료일 구하기 - */ - getNextMonday(startTime) { - const date = new Date(startTime); - const day = date.getDay(); - const daysUntilNextMonday = (7 - day + 1) % 7; + return where; + } - const nextMonday = new Date(date); - nextMonday.setDate(date.getDate() + daysUntilNextMonday); - nextMonday.setHours(0, 0, 0, 0); // 자정으로 설정 - - return nextMonday; + /** + * 스케줄 유효성 검사 + * 이미 컨트롤러에서 검증했으므로, 추가 검증 필요 시 수행 + */ + validateScheduleTime(start_time, end_time) { + if (new Date(start_time) >= new Date(end_time)) { + throw new Error("Start time must be before end time"); } - - /** - * 사용자 스케줄 생성 - */ - async createSchedule({ userId, title, start_time, end_time, is_fixed }) { - const schedule = await this.withTransaction(async (transaction) => { - this.validateScheduleTime(start_time, end_time); - const overlap = await this.checkScheduleOverlap(userId, start_time, end_time); - if (overlap) { - throw new Error('Schedule overlaps with existing schedule'); - } - - const scheduleData = { - user_id: userId, - title, - start_time, - end_time, - is_fixed, - expiry_date: is_fixed ? null : this.getNextMonday(start_time) - }; - - return Schedule.create(scheduleData, { transaction }); - }); - - return new ScheduleResponseDTO(schedule); + } + + /** + * 유동 스케줄 만료일 구하기 + */ + getNextMonday(startTime) { + const date = new Date(startTime); + const day = date.getUTCDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday + const daysUntilNextMonday = (8 - day) % 7 || 7; // Ensure next Monday + + const nextMonday = new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + daysUntilNextMonday, + 0, + 0, + 0, + 0 // Set to midnight UTC + ) + ); + + return nextMonday; + } + + /** + * 사용자 스케줄 생성 + */ + async createSchedule({ userId, title, start_time, end_time, is_fixed }) { + const schedule = await this.withTransaction(async (transaction) => { + this.validateScheduleTime(start_time, end_time); + + const overlap = await this.checkScheduleOverlap( + userId, + start_time, + end_time + ); + if (overlap) { + throw new Error("Schedule overlaps with existing schedule"); + } + + const scheduleData = { + user_id: userId, + title, + start_time, + end_time, + is_fixed, + expiry_date: is_fixed ? null : this.getNextMonday(start_time), + }; + + return Schedule.create(scheduleData, { transaction }); + }); + + return new ScheduleResponseDTO(schedule); + } + + /** + * 사용자 스케줄 수정 + */ + async updateSchedule(id, userId, updateData) { + const updatedSchedule = await this.withTransaction(async (transaction) => { + const schedule = await Schedule.findOne({ + where: { id, user_id: userId }, + transaction, + }); + + if (!schedule) { + throw new Error("Schedule not found"); + } + + // 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행 + if (updateData.start_time && updateData.end_time) { + this.validateScheduleTime(updateData.start_time, updateData.end_time); + } + + const overlap = await this.checkScheduleOverlap( + userId, + updateData.start_time || schedule.start_time, + updateData.end_time || schedule.end_time, + id + ); + if (overlap) { + throw new Error("Schedule overlaps with existing schedule"); + } + + const is_fixed = schedule.is_fixed; + const updatedDataWithExpiry = { + ...updateData, + expiry_date: is_fixed + ? null + : this.getNextMonday(updateData.start_time || schedule.start_time), + updatedAt: new Date(), + }; + delete updatedDataWithExpiry.is_fixed; + + return schedule.update(updatedDataWithExpiry, { transaction }); + }); + + return new ScheduleResponseDTO(updatedSchedule); + } + + /** + * 사용자 스케줄 삭제 + */ + async deleteSchedule(id, userId) { + return this.withTransaction(async (transaction) => { + const result = await Schedule.destroy({ + where: { id, user_id: userId }, + transaction, + }); + + if (!result) { + throw new Error("Schedule not found"); + } + + // 삭제 성공 메시지 반환 + return { message: "Schedule successfully deleted" }; + }); + } + + /** + * 해당 사용자의 스케줄 정보 조회 + */ + async getAllSchedules(userId) { + try { + const schedules = await Schedule.findAll({ + where: this.getScheduleWhereClause(userId), + order: [["start_time", "ASC"]], + }); + const schedulesDTO = schedules.map( + (schedule) => new ScheduleResponseDTO(schedule) + ); + return schedulesDTO; + } catch (error) { + throw new Error(`Failed to fetch schedules: ${error.message}`); } - - /** - * 사용자 스케줄 수정 - */ - async updateSchedule(id, userId, updateData) { - const updatedSchedule = await this.withTransaction(async (transaction) => { - const schedule = await Schedule.findOne({ - where: { id, user_id: userId }, - transaction - }); - - if (!schedule) { - throw new Error('Schedule not found'); - } - - this.validateScheduleTime(updateData.start_time, updateData.end_time); - - const overlap = await this.checkScheduleOverlap( - userId, - updateData.start_time, - updateData.end_time, - id - ); - if (overlap) { - throw new Error('Schedule overlaps with existing schedule'); - } - - const is_fixed = schedule.is_fixed; - const updatedDataWithExpiry = { - ...updateData, - expiry_date: is_fixed ? null : this.getNextMonday(updateData.start_time), - updatedAt: new Date() - }; - delete updatedDataWithExpiry.is_fixed; - - return schedule.update(updatedDataWithExpiry, { transaction }); - }); - - return new ScheduleResponseDTO(updatedSchedule); + } + + /** + * 해당 사용자의 특정 스케줄 조회 + */ + async getScheduleById(id, userId) { + try { + const schedule = await Schedule.findOne({ + where: this.getScheduleWhereClause(userId, id), + }); + + if (!schedule) { + throw new Error("Schedule not found"); + } + + return new ScheduleResponseDTO(schedule); + } catch (error) { + throw new Error(`Failed to fetch schedule: ${error.message}`); } - - /** - * 사용자 스케줄 삭제 - */ - async deleteSchedule(id, userId) { - return this.withTransaction(async (transaction) => { - const result = await Schedule.destroy({ - where: { id, user_id: userId }, - transaction - }); - - if (!result) { - throw new Error('Schedule not found'); - } - - // 삭제 성공 메시지 반환 - return { message: 'Schedule successfully deleted' }; - }); + } + + /** + * 만료된 유동 스케줄 정리 + */ + async cleanExpiredSchedules() { + try { + await Schedule.destroy({ + where: { + is_fixed: false, + expiry_date: { [Op.lte]: new Date() }, + }, + }); + } catch (error) { + throw new Error(`Failed to clean expired schedules: ${error.message}`); } - - /** - * 해당 사용자의 스케줄 정보 조회 - */ - async getAllSchedules(userId) { - try { - const schedules = await Schedule.findAll({ - where: this.getScheduleWhereClause(userId), - order: [['start_time', 'ASC']] - }); - const schedulesDTO = schedules.map(schedule => new ScheduleResponseDTO(schedule)); - return schedulesDTO; - } catch (error) { - throw new Error(`Failed to fetch schedules: ${error.message}`); - } - } - - /** - * 해당 사용자의 특정 스케줄 조회 - */ - async getScheduleById(id, userId) { - try { - const schedule = await Schedule.findOne({ - where: this.getScheduleWhereClause(userId, id) - }); - - if (!schedule) { - throw new Error('Schedule not found'); - } - - return new ScheduleResponseDTO(schedule); - } catch (error) { - throw new Error(`Failed to fetch schedule: ${error.message}`); - } - } - - /** - * 만료된 유동 스케줄 정리 - */ - async cleanExpiredSchedules() { - try { - await Schedule.destroy({ - where: { - is_fixed: false, - expiry_date: { [Op.lte]: new Date() } - } - }); - } catch (error) { - throw new Error(`Failed to clean expired schedules: ${error.message}`); - } - } - - /** - * 스케줄 중복 검사 - */ - async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { - try { - const where = { - user_id: userId, - [Op.or]: [ - { - [Op.and]: [ - { start_time: { [Op.lte]: start_time } }, - { end_time: { [Op.gte]: start_time } } - ] - }, - { - [Op.and]: [ - { start_time: { [Op.gte]: start_time } }, - { start_time: { [Op.lte]: end_time } } - ] - } - ] - }; - - if (excludeId) { - where.id = { [Op.ne]: excludeId }; - } - - const overlappingSchedule = await Schedule.findOne({ where }); - return overlappingSchedule; - } catch (error) { - throw new Error(`Failed to check schedule overlap: ${error.message}`); - } + } + + /** + * 스케줄 중복 검사 + */ + async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { + try { + const where = { + user_id: userId, + [Op.or]: [ + { + [Op.and]: [ + { start_time: { [Op.lte]: start_time } }, + { end_time: { [Op.gte]: start_time } }, + ], + }, + { + [Op.and]: [ + { start_time: { [Op.gte]: start_time } }, + { start_time: { [Op.lte]: end_time } }, + ], + }, + ], + }; + + if (excludeId) { + where.id = { [Op.ne]: excludeId }; + } + + const overlappingSchedule = await Schedule.findOne({ where }); + return overlappingSchedule; + } catch (error) { + throw new Error(`Failed to check schedule overlap: ${error.message}`); } + } } module.exports = new scheduleService();