From b6719c5fa233f822dd111d37683026fb9918ffb4 Mon Sep 17 00:00:00 2001 From: tpgus2603 <kakaneymar2424@gmail.com> Date: Fri, 22 Nov 2024 20:28:13 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A5=B4=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8,=EC=84=9C=EB=B9=84=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=8C=80=ED=8F=AD=EC=88=98=EC=A0=95(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/meetingController.js | 6 +- controllers/scheduleController.js | 116 +++++---- dtos/FriendListDTO.js | 5 +- dtos/FriendResponseDTO.js | 32 ++- dtos/ScheduleRequestDTO.js | 44 ++-- dtos/ScheduleResponseDTO.js | 6 +- models/schedule.js | 34 +-- services/friendService.test.js | 1 - services/schedule.test.js | 407 +++++++++++++++--------------- services/scheduleService.js | 354 +++++++++----------------- 10 files changed, 463 insertions(+), 542 deletions(-) diff --git a/controllers/meetingController.js b/controllers/meetingController.js index 182c25b..6775b59 100644 --- a/controllers/meetingController.js +++ b/controllers/meetingController.js @@ -10,7 +10,7 @@ class MeetingController { */ async createMeeting(req, res) { try { - const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID + const userId = req.user.id; const meetingData = { ...req.body, created_by: userId }; // CreateMeetingRequestDTO를 사용하여 요청 데이터 검증 @@ -31,7 +31,7 @@ class MeetingController { */ async getMeetings(req, res) { try { - const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID + const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID const meetings = await MeetingService.getMeetings(userId); res.status(200).json(meetings); @@ -64,7 +64,7 @@ class MeetingController { async joinMeeting(req, res) { try { const { meetingId } = req.params; - const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID + const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID await MeetingService.joinMeeting(meetingId, userId); res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); diff --git a/controllers/scheduleController.js b/controllers/scheduleController.js index 0d3f1c9..4d43ee1 100644 --- a/controllers/scheduleController.js +++ b/controllers/scheduleController.js @@ -6,28 +6,46 @@ class scheduleController { /** * 스케줄 생성 * POST /api/schedule + * 해당 사용자 id는 auth 미들웨어에서 설정된 사용자 정보 이용 + * req.user = User 모델의 인스턴스 + * 요청 본문 예시: + * { + * title: 'Schedule Title', + * is_fixed: true, + * events: [ + * { time_idx: 36 }, + * { time_idx: 37 }, + * // ... + * ] + * } */ async createSchedule(req, res) { try { const userId = req.user.id; const scheduleRequestDTO = new ScheduleRequestDTO(req.body); - const validatedData = scheduleRequestDTO.validate('create'); + const validatedData = scheduleRequestDTO.validate('create'); // 'create' 타입 검증 - const scheduleDTO = await ScheduleService.createSchedule({ + const { title, is_fixed, events } = validatedData; + + const schedules = await ScheduleService.createSchedules({ userId, - ...validatedData + title, + is_fixed, + events }); return res.status(201).json({ success: true, - data: scheduleDTO + data: { + schedules + } }); } catch (error) { return res.status(400).json({ success: false, error: { - message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR', - code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR' + message: error.message, + code: 'SCHEDULE_CREATE_ERROR' } }); } @@ -35,23 +53,35 @@ class scheduleController { /** * 스케줄 수정 - * PUT /api/schedule/:id + * PUT /api/schedule + * Bulk update 지원 + * 요청 본문 예시: + * { + * updates: [ + * { time_idx: 36, title: 'New Title', is_fixed: true }, + * { time_idx: 44, title: 'Another Title' }, + * // ... + * ] + * } */ - async updateSchedule(req, res) { + async updateSchedules(req, res) { try { - const { id } = req.params; const userId = req.user.id; const scheduleRequestDTO = new ScheduleRequestDTO(req.body); - const validatedData = scheduleRequestDTO.validate('update'); + const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증 + + const { updates } = validatedData; - const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData); + const updatedSchedules = await ScheduleService.updateSchedules(userId, updates); return res.status(200).json({ success: true, - data: scheduleDTO + data: { + schedules: updatedSchedules + } }); } catch (error) { - if (error.message === 'Schedule not found') { + if (error.code === 'SCHEDULE_NOT_FOUND') { return res.status(404).json({ success: false, error: { @@ -59,14 +89,6 @@ class scheduleController { code: 'SCHEDULE_NOT_FOUND' } }); - } else if (error.message.includes('Validation error')) { - return res.status(400).json({ - success: false, - error: { - message: error.message, - code: 'VALIDATION_ERROR' - } - }); } return res.status(400).json({ success: false, @@ -80,34 +102,36 @@ class scheduleController { /** * 스케줄 삭제 - * DELETE /api/schedule/:id + * DELETE /api/schedule + * Bulk delete 지원 + * 요청 본문 예시: + * { + * time_idxs: [36, 44, ...] + * } */ - async deleteSchedule(req, res) { + async deleteSchedules(req, res) { try { - const { id } = req.params; const userId = req.user.id; + const scheduleRequestDTO = new ScheduleRequestDTO(req.body); + const validatedData = scheduleRequestDTO.validate('bulk_delete'); // 'bulk_delete' 타입 검증 + + const { time_idxs } = validatedData; - const deleteResult = await ScheduleService.deleteSchedule(id, userId); + const result = await ScheduleService.deleteSchedules(userId, time_idxs); return res.status(200).json({ success: true, - data: deleteResult + data: { + message: 'Schedules successfully deleted', + deleted_time_idxs: result.deleted_time_idxs + } }); } catch (error) { - if (error.message === 'Schedule not found') { - return res.status(404).json({ - success: false, - error: { - message: error.message, - code: 'SCHEDULE_NOT_FOUND' - } - }); - } - return res.status(500).json({ + return res.status(404).json({ success: false, error: { - message: 'Failed to delete schedule', - code: 'DELETE_ERROR' + message: error.message, + code: 'SCHEDULE_DELETE_ERROR' } }); } @@ -120,11 +144,11 @@ class scheduleController { async getAllSchedules(req, res) { try { const userId = req.user.id; - const schedulesDTO = await ScheduleService.getAllSchedules(userId); + const schedules = await ScheduleService.getAllSchedules(userId); return res.status(200).json({ success: true, - data: schedulesDTO + data: schedules }); } catch (error) { return res.status(500).json({ @@ -139,17 +163,19 @@ class scheduleController { /** * 해당 사용자 특정 스케줄 조회 - * GET /api/schedule/:id + * GET /api/schedule/:time_idx + * 예: GET /api/schedule/36 */ - async getScheduleById(req, res) { + async getScheduleByTimeIdx(req, res) { try { - const { id } = req.params; + const { time_idx } = req.params; const userId = req.user.id; - const scheduleDTO = await ScheduleService.getScheduleById(id, userId); + + const schedule = await ScheduleService.getScheduleByTimeIdx(userId, parseInt(time_idx, 10)); return res.status(200).json({ success: true, - data: scheduleDTO + data: schedule }); } catch (error) { if (error.message === 'Schedule not found') { diff --git a/dtos/FriendListDTO.js b/dtos/FriendListDTO.js index 1768c74..3dd8b22 100644 --- a/dtos/FriendListDTO.js +++ b/dtos/FriendListDTO.js @@ -1,10 +1,7 @@ // dto/FriendListDTO.js class FriendListDTO { - /** - * @param {object} friend - Friend relationship object retrieved from the database. - * @param {number} userId - The ID of the user whose friend list is being retrieved. - */ + constructor(friend, userId) { this.id = friend.id; this.status = friend.status; diff --git a/dtos/FriendResponseDTO.js b/dtos/FriendResponseDTO.js index 8a7f365..050786a 100644 --- a/dtos/FriendResponseDTO.js +++ b/dtos/FriendResponseDTO.js @@ -1,25 +1,23 @@ -// dto/FriendRequestDTO.js +// dto/FriendResponseDTO.js -class FriendRequestDTO { - /** - * @param {object} friendRequest - Friend request object retrieved from the database. - */ - constructor(friendRequest) { - this.id = friendRequest.id; +class FriendResponseDTO { + + constructor(friendResponse) { + this.id = friendResponse.id; this.requester = { - id: friendRequest.requester.id, - name: friendRequest.requester.name, - email: friendRequest.requester.email + id: friendResponse.requester.id, + name: friendResponse.requester.name, + email: friendResponse.requester.email }; this.receiver = { - id: friendRequest.receiver.id, - name: friendRequest.receiver.name, - email: friendRequest.receiver.email + id: friendResponse.receiver.id, + name: friendResponse.receiver.name, + email: friendResponse.receiver.email }; - this.status = friendRequest.status; - this.createdAt = friendRequest.createdAt; - this.updatedAt = friendRequest.updatedAt; + this.status = friendResponse.status; + this.createdAt = friendResponse.createdAt; + this.updatedAt = friendResponse.updatedAt; } } -module.exports = FriendRequestDTO; +module.exports = FriendResponseDTO; diff --git a/dtos/ScheduleRequestDTO.js b/dtos/ScheduleRequestDTO.js index a332534..854bd3f 100644 --- a/dtos/ScheduleRequestDTO.js +++ b/dtos/ScheduleRequestDTO.js @@ -1,39 +1,49 @@ // dtos/ScheduleRequestDTO.js - const Joi = require('joi'); class ScheduleRequestDTO { constructor(data) { this.data = data; } + validate(type = 'create') { - // 기본 스키마 정의 - let schema = Joi.object({ - title: Joi.string().min(1).max(255).required(), - start_time: Joi.date().iso().required(), - end_time: Joi.date().iso().required(), - is_fixed: Joi.boolean().required() - }); + let schema; - // 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음 - if (type === 'update') { + if (type === 'create') { + schema = Joi.object({ + title: Joi.string().min(1).max(255).required(), + is_fixed: Joi.boolean().required(), + events: Joi.array().items( + Joi.object({ + time_idx: Joi.number().integer().min(0).max(671).required(), + }) + ).min(1).required() + }); + } else if (type === 'bulk_update') { + schema = Joi.object({ + updates: Joi.array().items( + Joi.object({ + time_idx: Joi.number().integer().min(0).max(671).required(), + title: Joi.string().min(1).max(255).optional(), + is_fixed: Joi.boolean().optional(), + }) + ).min(1).required() + }); + } else if (type === 'bulk_delete') { schema = Joi.object({ - title: Joi.string().min(1).max(255).optional(), - start_time: Joi.date().iso().optional(), - end_time: Joi.date().iso().optional(), - is_fixed: Joi.boolean().optional() - }).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요 + time_idxs: Joi.array().items( + Joi.number().integer().min(0).max(671).required() + ).min(1).required() + }); } const { error, value } = schema.validate(this.data, { abortEarly: false }); if (error) { - // 모든 에러 메시지를 하나의 문자열로 결합 const errorMessages = error.details.map(detail => detail.message).join(', '); throw new Error(`Validation error: ${errorMessages}`); } - // 검증된 데이터를 반환 return value; } } diff --git a/dtos/ScheduleResponseDTO.js b/dtos/ScheduleResponseDTO.js index b8717cf..a75316e 100644 --- a/dtos/ScheduleResponseDTO.js +++ b/dtos/ScheduleResponseDTO.js @@ -3,12 +3,10 @@ class ScheduleResponseDTO { constructor(schedule) { this.id = schedule.id; - this.user_id = schedule.user_id; + this.user_id = schedule.user_id; this.title = schedule.title; - this.start_time = schedule.start_time; - this.end_time = schedule.end_time; + this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가 this.is_fixed = schedule.is_fixed; - this.expiry_date = schedule.expiry_date; this.createdAt = schedule.createdAt; this.updatedAt = schedule.updatedAt; } diff --git a/models/schedule.js b/models/schedule.js index 92aca80..a7c4a42 100644 --- a/models/schedule.js +++ b/models/schedule.js @@ -8,29 +8,33 @@ const Schedule = sequelize.define('Schedule', { type: DataTypes.STRING, allowNull: false, }, - start_time: { - type: DataTypes.DATE, - allowNull: false, - }, - end_time: { - type: DataTypes.DATE, + time_idx: { // 일주일을 15분 단위로 나눈 시간 인덱스 + type: DataTypes.INTEGER, allowNull: false, + validate: { + min: 0, + max: 671, // 7일 * 24시간 * 4 (15분 단위) - 1 + }, }, is_fixed: { type: DataTypes.BOOLEAN, allowNull: false, - defaultValue: false + defaultValue: false, }, - expiry_date: { - type: DataTypes.DATE, - allowNull: true - } }, { tableName: 'Schedules', - timestamps: true, // created_at과 updated_at 자동 관리 + timestamps: true, // createdAt과 updatedAt 자동 관리 + indexes: [ + { + unique: true, + fields: ['user_id', 'time_idx'], + name: 'unique_schedule_per_user_time', + }, + { + fields: ['time_idx'], + }, + ], }); -// Schedule.belongsTo(User, { foreignKey: 'user_id', as: 'user' }); -// User.hasMany(Schedule, { foreignKey: 'user_id', as: 'schedules' }); -module.exports = Schedule; \ No newline at end of file +module.exports = Schedule; diff --git a/services/friendService.test.js b/services/friendService.test.js index db3e03e..02394ee 100644 --- a/services/friendService.test.js +++ b/services/friendService.test.js @@ -4,7 +4,6 @@ const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임 // const User = require('../models/User'); // const Friend = require('../models/Friend'); const { Friend,User} = require('../models'); - const friendService = require('./friendService'); // FriendService 임포트 // Sequelize의 Op를 가져오기 위해 추가 diff --git a/services/schedule.test.js b/services/schedule.test.js index d1cc0f2..b5d5626 100644 --- a/services/schedule.test.js +++ b/services/schedule.test.js @@ -1,244 +1,237 @@ // test/schedule.test.js - const sequelize = require('../config/sequelize'); -const User = require('../models/User'); -const Friend = require('../models/Friend'); -const Schedule = require('../models/Schedule'); -const scheduleService = require('./scheduleService'); +const {User, Friend, Schedule,} = require('../models'); +const scheduleService = require('../services/scheduleService'); // 경로 수정 beforeAll(async () => { - await sequelize.sync({ force: true }); - // 더미 사용자 생성 - await User.bulkCreate([ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' }, - ]); - - // 더미 친구 관계 생성 - await Friend.create({ - id: 1, - requester_id: 1, - receiver_id: 2, - status: 'ACCEPTED', - }); - - // 더미 스케줄 생성 - await Schedule.create({ - id: 1, - user_id: 1, - title: 'Alice\'s Fixed Schedule', - start_time: new Date('2024-05-01T09:00:00Z'), - end_time: new Date('2024-05-01T10:00:00Z'), - is_fixed: true, - expiry_date: null, - }); - - await Schedule.create({ - id: 2, - user_id: 1, - title: 'Alice\'s Flexible Schedule', - start_time: new Date('2024-05-02T11:00:00Z'), - end_time: new Date('2024-05-02T12:00:00Z'), - is_fixed: false, - expiry_date: new Date('2024-05-08T00:00:00Z'), // 다음 월요일 - }); -}); - -afterAll(async () => { - // 데이터베이스 연결 종료 - await sequelize.close(); -}); + await sequelize.sync({ force: true }); + + // 더미 사용자 생성 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + ]); + + // 더미 친구 관계 생성 + await Friend.create({ + id: 1, + requester_id: 1, + receiver_id: 2, + status: 'ACCEPTED', + }); -describe('Schedule Service', () => { - describe('createSchedule', () => { - test('should create a new fixed schedule successfully', async () => { - const scheduleData = { - userId: 2, - title: 'Bob\'s Fixed Schedule', - start_time: new Date('2024-05-03T14:00:00Z'), - end_time: new Date('2024-05-03T15:00:00Z'), + // 더미 스케줄 생성 (day_of_week과 TIME 형식 사용) + await Schedule.create({ + id: 1, + user_id: 1, + title: 'Alice\'s Fixed Schedule', + day_of_week: 'Monday', + start_time: '09:00:00', // 'HH:MM:SS' 형식 + end_time: '10:00:00', is_fixed: true, - }; - - const schedule = await scheduleService.createSchedule(scheduleData); - - - expect(schedule).toBeDefined(); - expect(schedule.user_id).toBe(2); - expect(schedule.title).toBe('Bob\'s Fixed Schedule'); - expect(schedule.is_fixed).toBe(true); - expect(schedule.expiry_date).toBeNull(); }); - test('should create a new flexible schedule with expiry date', async () => { - const scheduleData = { - userId: 2, - title: 'Bob\'s Flexible Schedule', - start_time: new Date('2024-05-04T16:00:00Z'), - end_time: new Date('2024-05-04T17:00:00Z'), + await Schedule.create({ + id: 2, + user_id: 1, + title: 'Alice\'s Flexible Schedule', + day_of_week: 'Tuesday', + start_time: '11:00:00', + end_time: '12:00:00', is_fixed: false, - }; + }); +}); - const schedule = await scheduleService.createSchedule(scheduleData); +afterAll(async () => { + // 데이터베이스 연결 종료 + await sequelize.close(); +}); - expect(schedule).toBeDefined(); - expect(schedule.user_id).toBe(2); - expect(schedule.title).toBe('Bob\'s Flexible Schedule'); - expect(schedule.is_fixed).toBe(false); - expect(schedule.expiry_date).toBeInstanceOf(Date); +describe('Schedule Service', () => { + describe('createSchedules', () => { + test('should create new fixed schedules successfully', async () => { + const scheduleData = { + userId: 2, + title: 'Bob\'s Fixed Schedule', + is_fixed: true, + events: [ + { + day_of_week: 'Wednesday', + start_time: '14:00:00', + end_time: '15:00:00', + }, + ], + }; + + const schedules = await scheduleService.createSchedules(scheduleData); + + expect(schedules).toBeDefined(); + expect(Array.isArray(schedules)).toBe(true); + expect(schedules.length).toBe(1); + + const schedule = schedules[0]; + expect(schedule.user_id).toBe(2); + expect(schedule.title).toBe('Bob\'s Fixed Schedule'); + expect(schedule.is_fixed).toBe(true); + expect(schedule.day_of_week).toBe('Wednesday'); + expect(schedule.start_time).toBe('14:00'); + expect(schedule.end_time).toBe('15:00'); + }); - // expiry_date가 다음 월요일로 설정되었는지 확인 - const expectedExpiryDate = new Date('2024-05-06T00:00:00Z'); // 2024-05-06은 다음 월요일 - expect(schedule.expiry_date.toISOString()).toBe(expectedExpiryDate.toISOString()); - }); + test('should create new flexible schedules successfully', async () => { + const scheduleData = { + userId: 2, + title: 'Bob\'s Flexible Schedule', + is_fixed: false, + events: [ + { + day_of_week: 'Thursday', + start_time: '16:00:00', + end_time: '17:00:00', + }, + ], + }; + + const schedules = await scheduleService.createSchedules(scheduleData); + + expect(schedules).toBeDefined(); + expect(Array.isArray(schedules)).toBe(true); + expect(schedules.length).toBe(1); + + const schedule = schedules[0]; + expect(schedule.user_id).toBe(2); + expect(schedule.title).toBe('Bob\'s Flexible Schedule'); + expect(schedule.is_fixed).toBe(false); + expect(schedule.day_of_week).toBe('Thursday'); + expect(schedule.start_time).toBe('16:00'); + expect(schedule.end_time).toBe('17:00'); + }); - test('should throw error when schedule times overlap with existing schedule', async () => { - const scheduleData = { - userId: 1, - title: 'Alice\'s Overlapping Schedule', - start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침 - end_time: new Date('2024-05-01T10:30:00Z'), - is_fixed: false, - }; + test('should throw error when schedule times overlap with existing schedule', async () => { + const scheduleData = { + userId: 1, + title: 'Alice\'s Overlapping Schedule', + is_fixed: false, + events: [ + { + day_of_week: 'Monday', // 기존 스케줄과 동일한 요일 + start_time: '09:30:00', // 기존 스케줄과 겹치는 시간 + end_time: '10:30:00', + }, + ], + }; + + await expect(scheduleService.createSchedules(scheduleData)) + .rejects + .toThrow('Schedule overlaps with existing schedule on Monday'); + }); - await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Schedule overlaps with existing schedule'); + test('should throw error when start_time is after end_time', async () => { + const scheduleData = { + userId: 1, + title: 'Invalid Schedule', + is_fixed: false, + events: [ + { + day_of_week: 'Friday', + start_time: '18:00:00', + end_time: '17:00:00', // start_time이 더 늦음 + }, + ], + }; + + await expect(scheduleService.createSchedules(scheduleData)) + .rejects + .toThrow('Start time must be before end time'); + }); }); - test('should throw error when start_time is after end_time', async () => { - const scheduleData = { - userId: 1, - title: 'Invalid Schedule', - start_time: new Date('2024-05-05T18:00:00Z'), - end_time: new Date('2024-05-05T17:00:00Z'), // start_time이 더 나중 - is_fixed: false, - }; + describe('updateSchedule', () => { + test('should update an existing schedule successfully', async () => { + const updateData = { + title: 'Alice\'s Updated Flexible Schedule', + start_time: '11:30:00', + end_time: '12:30:00', + }; - await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Start time must be before end time'); - }); - }); - - describe('updateSchedule', () => { - test('should update an existing schedule successfully', async () => { - const updateData = { - title: 'Alice\'s Updated Flexible Schedule', - start_time: new Date('2024-05-02T11:30:00Z'), - end_time: new Date('2024-05-02T12:30:00Z'), - }; - - const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData); - - expect(updatedSchedule).toBeDefined(); - expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule'); - expect(updatedSchedule.start_time.toISOString()).toBe(new Date('2024-05-02T11:30:00Z').toISOString()); - expect(updatedSchedule.end_time.toISOString()).toBe(new Date('2024-05-02T12:30:00Z').toISOString()); - expect(updatedSchedule.expiry_date).toBeInstanceOf(Date); - }); + const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData); + + expect(updatedSchedule).toBeDefined(); + expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule'); + expect(updatedSchedule.start_time).toBe('11:30'); + expect(updatedSchedule.end_time).toBe('12:30'); + }); - test('should throw error when updating a non-existing schedule', async () => { - const updateData = { - title: 'Non-existing Schedule', - start_time: new Date('2024-05-06T10:00:00Z'), - end_time: new Date('2024-05-06T11:00:00Z'), - }; + test('should throw error when updating a non-existing schedule', async () => { + const updateData = { + title: 'Non-existing Schedule', + start_time: '10:00:00', + end_time: '11:00:00', + }; - await expect(scheduleService.updateSchedule(999, 1, updateData)).rejects.toThrow('Schedule not found'); - }); + await expect(scheduleService.updateSchedule(999, 1, updateData)) + .rejects + .toThrow('Schedule not found'); + }); - test('should throw error when updated schedule overlaps with existing schedule', async () => { - const updateData = { - title: 'Alice\'s Overlapping Update', - start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침 - end_time: new Date('2024-05-01T10:30:00Z'), - }; + test('should throw error when updated schedule overlaps with existing schedule', async () => { + const updateData = { + title: 'Alice\'s Overlapping Update', + start_time: '09:30:00', // 기존 스케줄과 겹침 + end_time: '10:30:00', + }; - await expect(scheduleService.updateSchedule(2, 1, updateData)).rejects.toThrow('Schedule overlaps with existing schedule'); + await expect(scheduleService.updateSchedule(2, 1, updateData)) + .rejects + .toThrow('Schedule overlaps with existing schedule'); + }); }); - }); - describe('deleteSchedule', () => { - test('should delete an existing schedule successfully', async () => { - const result = await scheduleService.deleteSchedule(2, 1); + describe('deleteSchedule', () => { + test('should delete an existing schedule successfully', async () => { + const result = await scheduleService.deleteSchedule(2, 1); - expect(result).toEqual({ message: 'Schedule successfully deleted' }); + 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'); + test('should throw error when deleting a non-existing schedule', async () => { + await expect(scheduleService.deleteSchedule(999, 1)) + .rejects + .toThrow('Schedule not found'); + }); }); -}); - describe('getAllSchedules', () => { - test('should retrieve all valid schedules for a user', async () => { - // 사용자 Alice의 모든 스케줄 조회 - const schedules = await scheduleService.getAllSchedules(1); + describe('getAllSchedules', () => { + test('should retrieve all valid schedules for a user', async () => { + // 사용자 Alice의 모든 스케줄 조회 (user_id: 1) + const schedules = await scheduleService.getAllSchedules(1); - expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true - expect(schedules[0].title).toBe('Alice\'s Fixed Schedule'); + expect(schedules).toBeDefined(); + expect(Array.isArray(schedules)).toBe(true); + expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true + expect(schedules[0].title).toBe('Alice\'s Fixed Schedule'); + expect(schedules[0].day_of_week).toBe('Monday'); + }); }); - }); - - describe('getScheduleById', () => { - test('should retrieve a specific schedule by ID', async () => { - const schedule = await scheduleService.getScheduleById(1, 1); - expect(schedule).toBeDefined(); - expect(schedule.title).toBe('Alice\'s Fixed Schedule'); - }); + describe('getScheduleById', () => { + test('should retrieve a specific schedule by ID', async () => { + const schedule = await scheduleService.getScheduleById(1, 1); - test('should throw error when retrieving a non-existing schedule', async () => { - await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found'); - }); - }); - // test/schedule.test.js - - describe('cleanExpiredSchedules', () => { - test('should delete expired flexible schedules', async () => { - // 현재 날짜를 기준으로 만료된 스케줄과 만료되지 않은 스케줄 생성 - 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'), // 이미 만료됨 + expect(schedule).toBeDefined(); + expect(schedule.title).toBe('Alice\'s Fixed Schedule'); + expect(schedule.day_of_week).toBe('Monday'); }); - // 만료되지 않은 유동 스케줄 생성 - 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'), // 아직 만료되지 않음 + test('should throw error when retrieving a non-existing schedule', async () => { + await expect(scheduleService.getScheduleById(999, 1)) + .rejects + .toThrow('Schedule not found'); }); - - // 만료된 스케줄 정리 - 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 e7c5a3b..419b49a 100644 --- a/services/scheduleService.js +++ b/services/scheduleService.js @@ -1,261 +1,157 @@ // services/scheduleService.js const sequelize = require('../config/sequelize'); const { Op } = require('sequelize'); -const {Schedule} = require('../models'); +const Schedule = require('../models/Schedule'); const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO'); -class scheduleService { - /** - * 트랜잭션 래퍼 함수 - */ - async withTransaction(callback) { - const transaction = await sequelize.transaction(); // 직접 sequelize 사용 - 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; +class ScheduleService { + /** + * 스케줄 생성 (벌크) + */ + async createSchedules({ userId, title, is_fixed, events }) { + return await sequelize.transaction(async (transaction) => { + const scheduleDTOs = []; + + for (const event of events) { + const { time_idx } = event; + + // 중복 스케줄 검사 + const overlap = await this.checkScheduleOverlap( + userId, + time_idx, + transaction + ); + + if (overlap) { + throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`); + } + + const scheduleData = { + user_id: userId, + title, + time_idx, + is_fixed, + }; + + const schedule = await Schedule.create(scheduleData, { transaction }); + scheduleDTOs.push(new ScheduleResponseDTO(schedule)); + } + + return scheduleDTOs; + }); } - return where; - } - - /** - * 스케줄 유효성 검사 - * 이미 컨트롤러에서 검증했으므로, 추가 검증 필요 시 수행 - */ - validateScheduleTime(start_time, end_time) { - if (new Date(start_time) >= new Date(end_time)) { - throw new Error("Start time must be before end time"); - } - } - - /** - * 유동 스케줄 만료일 구하기 - */ - 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); + /** + * 스케줄 수정 (벌크) + */ + async updateSchedules(userId, updates) { + return await sequelize.transaction(async (transaction) => { + const updatedSchedules = []; - const overlap = await this.checkScheduleOverlap( - userId, - start_time, - end_time - ); - if (overlap) { - throw new Error("Schedule overlaps with existing schedule"); - } + for (const update of updates) { + const { time_idx, title, is_fixed } = update; - const scheduleData = { - user_id: userId, - title, - start_time, - end_time, - is_fixed, - expiry_date: is_fixed ? null : this.getNextMonday(start_time), - }; + const schedule = await Schedule.findOne({ + where: { user_id: userId, time_idx }, + transaction, + }); - return Schedule.create(scheduleData, { transaction }); - }); + if (!schedule) { + throw { code: 'SCHEDULE_NOT_FOUND', message: `Schedule not found at time_idx ${time_idx}` }; + } - return new ScheduleResponseDTO(schedule); - } + // 중복 스케줄 검사 (time_idx는 고유하므로 필요 없음) + // 만약 다른 필드를 기반으로 중복을 검사한다면 추가 로직 필요 - /** - * 사용자 스케줄 수정 - */ - async updateSchedule(id, userId, updateData) { - const updatedSchedule = await this.withTransaction(async (transaction) => { - const schedule = await Schedule.findOne({ - where: { id, user_id: userId }, - transaction, - }); + const updatedData = {}; + if (title !== undefined) updatedData.title = title; + if (is_fixed !== undefined) updatedData.is_fixed = is_fixed; - if (!schedule) { - throw new Error("Schedule not found"); - } + const updatedSchedule = await schedule.update(updatedData, { transaction }); + updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule)); + } - // 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행 - 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 updatedSchedules; + }); + } - return new ScheduleResponseDTO(updatedSchedule); - } + /** + * 스케줄 삭제 (벌크) + */ + async deleteSchedules(userId, time_idxs) { + return await sequelize.transaction(async (transaction) => { + const deleted_time_idxs = []; - /** - * 사용자 스케줄 삭제 - */ - async deleteSchedule(id, userId) { - return this.withTransaction(async (transaction) => { - const result = await Schedule.destroy({ - where: { id, user_id: userId }, - transaction, - }); + for (const time_idx of time_idxs) { + const deletedCount = await Schedule.destroy({ + where: { user_id: userId, time_idx }, + transaction, + }); - if (!result) { - throw new Error("Schedule not found"); - } + if (deletedCount === 0) { + throw new Error(`Schedule not found at time_idx ${time_idx}`); + } - // 삭제 성공 메시지 반환 - return { message: "Schedule successfully deleted" }; - }); - } + deleted_time_idxs.push(time_idx); + } - /** - * 해당 사용자의 스케줄 정보 조회 - */ - 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}`); + return { deleted_time_idxs }; + }); } - } - /** - * 해당 사용자의 특정 스케줄 조회 - */ - async getScheduleById(id, userId) { - try { - const schedule = await Schedule.findOne({ - where: this.getScheduleWhereClause(userId, id), - }); + /** + * 특정 time_idx로 스케줄 조회 + */ + async getScheduleByTimeIdx(userId, time_idx) { + const schedule = await Schedule.findOne({ + where: { user_id: userId, time_idx }, + }); - if (!schedule) { - throw new Error("Schedule not found"); - } + if (!schedule) { + throw new Error('Schedule not found'); + } - return new ScheduleResponseDTO(schedule); - } catch (error) { - throw new Error(`Failed to fetch schedule: ${error.message}`); + return new ScheduleResponseDTO(schedule); } - } - /** - * 만료된 유동 스케줄 정리 - */ - 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: { user_id: userId }, + order: [['time_idx', 'ASC']], + }); + return schedules.map((schedule) => new ScheduleResponseDTO(schedule)); + } catch (error) { + throw new Error(`Failed to fetch 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 } }, - ], - }, - ], - }; + /** + * 중복 스케줄 검사 + */ + async checkScheduleOverlap(userId, time_idx, transaction) { + const overlappingSchedule = await Schedule.findOne({ + where: { user_id: userId, time_idx }, + transaction, + }); - if (excludeId) { - where.id = { [Op.ne]: excludeId }; - } + return !!overlappingSchedule; + } - const overlappingSchedule = await Schedule.findOne({ where }); - return overlappingSchedule; - } catch (error) { - throw new Error(`Failed to check schedule overlap: ${error.message}`); + async cleanExpiredSchedules() { + try { + const deletedCount = await Schedule.destroy({ + where: { is_fixed: false }, + }); + //console.log(`Deleted ${deletedCount} flexible schedules.`); + } catch (error) { + console.error('Failed to clean expired schedules:', error); + throw error; + } } - } } -module.exports = new scheduleService(); +module.exports = new ScheduleService(); -- GitLab