Skip to content
Snippets Groups Projects
Commit b6719c5f authored by tpgus2603's avatar tpgus2603
Browse files

refactor: 스케쥴 모델,서비스코드 대폭수정(#13)

parent ce8ae990
Branches
No related tags found
2 merge requests!31Develop,!22[#13] 서비스로직에 인덱스 도입하여 로직 최적화 안료
...@@ -10,7 +10,7 @@ class MeetingController { ...@@ -10,7 +10,7 @@ class MeetingController {
*/ */
async createMeeting(req, res) { async createMeeting(req, res) {
try { try {
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID const userId = req.user.id;
const meetingData = { ...req.body, created_by: userId }; const meetingData = { ...req.body, created_by: userId };
// CreateMeetingRequestDTO를 사용하여 요청 데이터 검증 // CreateMeetingRequestDTO를 사용하여 요청 데이터 검증
...@@ -31,7 +31,7 @@ class MeetingController { ...@@ -31,7 +31,7 @@ class MeetingController {
*/ */
async getMeetings(req, res) { async getMeetings(req, res) {
try { try {
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
const meetings = await MeetingService.getMeetings(userId); const meetings = await MeetingService.getMeetings(userId);
res.status(200).json(meetings); res.status(200).json(meetings);
...@@ -64,7 +64,7 @@ class MeetingController { ...@@ -64,7 +64,7 @@ class MeetingController {
async joinMeeting(req, res) { async joinMeeting(req, res) {
try { try {
const { meetingId } = req.params; const { meetingId } = req.params;
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
await MeetingService.joinMeeting(meetingId, userId); await MeetingService.joinMeeting(meetingId, userId);
res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); res.status(200).json({ message: '모임 및 채팅방 참가 완료' });
......
...@@ -6,28 +6,46 @@ class scheduleController { ...@@ -6,28 +6,46 @@ class scheduleController {
/** /**
* 스케줄 생성 * 스케줄 생성
* POST /api/schedule * 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) { async createSchedule(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body); 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, userId,
...validatedData title,
is_fixed,
events
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: scheduleDTO data: {
schedules
}
}); });
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: { error: {
message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR', message: error.message,
code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR' code: 'SCHEDULE_CREATE_ERROR'
} }
}); });
} }
...@@ -35,23 +53,35 @@ class scheduleController { ...@@ -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 { try {
const { id } = req.params;
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body); const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('update'); const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증
const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData); const { updates } = validatedData;
const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: scheduleDTO data: {
schedules: updatedSchedules
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.code === 'SCHEDULE_NOT_FOUND') {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
error: { error: {
...@@ -59,14 +89,6 @@ class scheduleController { ...@@ -59,14 +89,6 @@ class scheduleController {
code: 'SCHEDULE_NOT_FOUND' 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({ return res.status(400).json({
success: false, success: false,
...@@ -80,34 +102,36 @@ class scheduleController { ...@@ -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 { try {
const { id } = req.params;
const userId = req.user.id; 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({ return res.status(200).json({
success: true, success: true,
data: deleteResult data: {
message: 'Schedules successfully deleted',
deleted_time_idxs: result.deleted_time_idxs
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
error: { error: {
message: error.message, message: error.message,
code: 'SCHEDULE_NOT_FOUND' code: 'SCHEDULE_DELETE_ERROR'
}
});
}
return res.status(500).json({
success: false,
error: {
message: 'Failed to delete schedule',
code: 'DELETE_ERROR'
} }
}); });
} }
...@@ -120,11 +144,11 @@ class scheduleController { ...@@ -120,11 +144,11 @@ class scheduleController {
async getAllSchedules(req, res) { async getAllSchedules(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const schedulesDTO = await ScheduleService.getAllSchedules(userId); const schedules = await ScheduleService.getAllSchedules(userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedulesDTO data: schedules
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -139,17 +163,19 @@ class scheduleController { ...@@ -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 { try {
const { id } = req.params; const { time_idx } = req.params;
const userId = req.user.id; 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({ return res.status(200).json({
success: true, success: true,
data: scheduleDTO data: schedule
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
......
// dto/FriendListDTO.js // dto/FriendListDTO.js
class FriendListDTO { 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) { constructor(friend, userId) {
this.id = friend.id; this.id = friend.id;
this.status = friend.status; this.status = friend.status;
......
// dto/FriendRequestDTO.js // dto/FriendResponseDTO.js
class FriendRequestDTO { class FriendResponseDTO {
/**
* @param {object} friendRequest - Friend request object retrieved from the database. constructor(friendResponse) {
*/ this.id = friendResponse.id;
constructor(friendRequest) {
this.id = friendRequest.id;
this.requester = { this.requester = {
id: friendRequest.requester.id, id: friendResponse.requester.id,
name: friendRequest.requester.name, name: friendResponse.requester.name,
email: friendRequest.requester.email email: friendResponse.requester.email
}; };
this.receiver = { this.receiver = {
id: friendRequest.receiver.id, id: friendResponse.receiver.id,
name: friendRequest.receiver.name, name: friendResponse.receiver.name,
email: friendRequest.receiver.email email: friendResponse.receiver.email
}; };
this.status = friendRequest.status; this.status = friendResponse.status;
this.createdAt = friendRequest.createdAt; this.createdAt = friendResponse.createdAt;
this.updatedAt = friendRequest.updatedAt; this.updatedAt = friendResponse.updatedAt;
} }
} }
module.exports = FriendRequestDTO; module.exports = FriendResponseDTO;
// dtos/ScheduleRequestDTO.js // dtos/ScheduleRequestDTO.js
const Joi = require('joi'); const Joi = require('joi');
class ScheduleRequestDTO { class ScheduleRequestDTO {
constructor(data) { constructor(data) {
this.data = data; this.data = data;
} }
validate(type = 'create') { validate(type = 'create') {
// 기본 스키마 정의 let schema;
let schema = Joi.object({
if (type === 'create') {
schema = Joi.object({
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
start_time: Joi.date().iso().required(), is_fixed: Joi.boolean().required(),
end_time: Joi.date().iso().required(), events: Joi.array().items(
is_fixed: Joi.boolean().required() Joi.object({
time_idx: Joi.number().integer().min(0).max(671).required(),
})
).min(1).required()
}); });
} else if (type === 'bulk_update') {
// 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음
if (type === 'update') {
schema = Joi.object({ 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(), title: Joi.string().min(1).max(255).optional(),
start_time: Joi.date().iso().optional(), is_fixed: Joi.boolean().optional(),
end_time: Joi.date().iso().optional(), })
is_fixed: Joi.boolean().optional() ).min(1).required()
}).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요 });
} else if (type === 'bulk_delete') {
schema = Joi.object({
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 }); const { error, value } = schema.validate(this.data, { abortEarly: false });
if (error) { 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}`); throw new Error(`Validation error: ${errorMessages}`);
} }
// 검증된 데이터를 반환
return value; return value;
} }
} }
......
...@@ -5,10 +5,8 @@ class ScheduleResponseDTO { ...@@ -5,10 +5,8 @@ class ScheduleResponseDTO {
this.id = schedule.id; this.id = schedule.id;
this.user_id = schedule.user_id; this.user_id = schedule.user_id;
this.title = schedule.title; this.title = schedule.title;
this.start_time = schedule.start_time; this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가
this.end_time = schedule.end_time;
this.is_fixed = schedule.is_fixed; this.is_fixed = schedule.is_fixed;
this.expiry_date = schedule.expiry_date;
this.createdAt = schedule.createdAt; this.createdAt = schedule.createdAt;
this.updatedAt = schedule.updatedAt; this.updatedAt = schedule.updatedAt;
} }
......
...@@ -8,29 +8,33 @@ const Schedule = sequelize.define('Schedule', { ...@@ -8,29 +8,33 @@ const Schedule = sequelize.define('Schedule', {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
start_time: { time_idx: { // 일주일을 15분 단위로 나눈 시간 인덱스
type: DataTypes.DATE, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
validate: {
min: 0,
max: 671, // 7일 * 24시간 * 4 (15분 단위) - 1
}, },
end_time: {
type: DataTypes.DATE,
allowNull: false,
}, },
is_fixed: { is_fixed: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
expiry_date: {
type: DataTypes.DATE,
allowNull: true
}
}, { }, {
tableName: 'Schedules', 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; module.exports = Schedule;
...@@ -4,7 +4,6 @@ const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임 ...@@ -4,7 +4,6 @@ const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임
// const User = require('../models/User'); // const User = require('../models/User');
// const Friend = require('../models/Friend'); // const Friend = require('../models/Friend');
const { Friend,User} = require('../models'); const { Friend,User} = require('../models');
const friendService = require('./friendService'); // FriendService 임포트 const friendService = require('./friendService'); // FriendService 임포트
// Sequelize의 Op를 가져오기 위해 추가 // Sequelize의 Op를 가져오기 위해 추가
......
// test/schedule.test.js // test/schedule.test.js
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = require('../models/User'); const {User, Friend, Schedule,} = require('../models');
const Friend = require('../models/Friend'); const scheduleService = require('../services/scheduleService'); // 경로 수정
const Schedule = require('../models/Schedule');
const scheduleService = require('./scheduleService');
beforeAll(async () => { beforeAll(async () => {
await sequelize.sync({ force: true }); await sequelize.sync({ force: true });
// 더미 사용자 생성 // 더미 사용자 생성
await User.bulkCreate([ await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 1, name: 'Alice', email: 'alice@example.com' },
...@@ -22,25 +20,25 @@ beforeAll(async () => { ...@@ -22,25 +20,25 @@ beforeAll(async () => {
status: 'ACCEPTED', status: 'ACCEPTED',
}); });
// 더미 스케줄 생성 // 더미 스케줄 생성 (day_of_week과 TIME 형식 사용)
await Schedule.create({ await Schedule.create({
id: 1, id: 1,
user_id: 1, user_id: 1,
title: 'Alice\'s Fixed Schedule', title: 'Alice\'s Fixed Schedule',
start_time: new Date('2024-05-01T09:00:00Z'), day_of_week: 'Monday',
end_time: new Date('2024-05-01T10:00:00Z'), start_time: '09:00:00', // 'HH:MM:SS' 형식
end_time: '10:00:00',
is_fixed: true, is_fixed: true,
expiry_date: null,
}); });
await Schedule.create({ await Schedule.create({
id: 2, id: 2,
user_id: 1, user_id: 1,
title: 'Alice\'s Flexible Schedule', title: 'Alice\'s Flexible Schedule',
start_time: new Date('2024-05-02T11:00:00Z'), day_of_week: 'Tuesday',
end_time: new Date('2024-05-02T12:00:00Z'), start_time: '11:00:00',
end_time: '12:00:00',
is_fixed: false, is_fixed: false,
expiry_date: new Date('2024-05-08T00:00:00Z'), // 다음 월요일
}); });
}); });
...@@ -50,70 +48,101 @@ afterAll(async () => { ...@@ -50,70 +48,101 @@ afterAll(async () => {
}); });
describe('Schedule Service', () => { describe('Schedule Service', () => {
describe('createSchedule', () => { describe('createSchedules', () => {
test('should create a new fixed schedule successfully', async () => { test('should create new fixed schedules successfully', async () => {
const scheduleData = { const scheduleData = {
userId: 2, userId: 2,
title: 'Bob\'s Fixed Schedule', title: 'Bob\'s Fixed Schedule',
start_time: new Date('2024-05-03T14:00:00Z'),
end_time: new Date('2024-05-03T15:00:00Z'),
is_fixed: true, is_fixed: true,
events: [
{
day_of_week: 'Wednesday',
start_time: '14:00:00',
end_time: '15:00:00',
},
],
}; };
const schedule = await scheduleService.createSchedule(scheduleData); const schedules = await scheduleService.createSchedules(scheduleData);
expect(schedules).toBeDefined();
expect(Array.isArray(schedules)).toBe(true);
expect(schedules.length).toBe(1);
expect(schedule).toBeDefined(); const schedule = schedules[0];
expect(schedule.user_id).toBe(2); expect(schedule.user_id).toBe(2);
expect(schedule.title).toBe('Bob\'s Fixed Schedule'); expect(schedule.title).toBe('Bob\'s Fixed Schedule');
expect(schedule.is_fixed).toBe(true); expect(schedule.is_fixed).toBe(true);
expect(schedule.expiry_date).toBeNull(); expect(schedule.day_of_week).toBe('Wednesday');
expect(schedule.start_time).toBe('14:00');
expect(schedule.end_time).toBe('15:00');
}); });
test('should create a new flexible schedule with expiry date', async () => { test('should create new flexible schedules successfully', async () => {
const scheduleData = { const scheduleData = {
userId: 2, userId: 2,
title: 'Bob\'s Flexible Schedule', title: 'Bob\'s Flexible Schedule',
start_time: new Date('2024-05-04T16:00:00Z'),
end_time: new Date('2024-05-04T17:00:00Z'),
is_fixed: false, is_fixed: false,
events: [
{
day_of_week: 'Thursday',
start_time: '16:00:00',
end_time: '17:00:00',
},
],
}; };
const schedule = await scheduleService.createSchedule(scheduleData); const schedules = await scheduleService.createSchedules(scheduleData);
expect(schedule).toBeDefined(); 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.user_id).toBe(2);
expect(schedule.title).toBe('Bob\'s Flexible Schedule'); expect(schedule.title).toBe('Bob\'s Flexible Schedule');
expect(schedule.is_fixed).toBe(false); expect(schedule.is_fixed).toBe(false);
expect(schedule.expiry_date).toBeInstanceOf(Date); expect(schedule.day_of_week).toBe('Thursday');
expect(schedule.start_time).toBe('16:00');
// expiry_date가 다음 월요일로 설정되었는지 확인 expect(schedule.end_time).toBe('17:00');
const expectedExpiryDate = new Date('2024-05-06T00:00:00Z'); // 2024-05-06은 다음 월요일
expect(schedule.expiry_date.toISOString()).toBe(expectedExpiryDate.toISOString());
}); });
test('should throw error when schedule times overlap with existing schedule', async () => { test('should throw error when schedule times overlap with existing schedule', async () => {
const scheduleData = { const scheduleData = {
userId: 1, userId: 1,
title: 'Alice\'s Overlapping Schedule', 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, is_fixed: false,
events: [
{
day_of_week: 'Monday', // 기존 스케줄과 동일한 요일
start_time: '09:30:00', // 기존 스케줄과 겹치는 시간
end_time: '10:30:00',
},
],
}; };
await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Schedule overlaps with existing schedule'); await expect(scheduleService.createSchedules(scheduleData))
.rejects
.toThrow('Schedule overlaps with existing schedule on Monday');
}); });
test('should throw error when start_time is after end_time', async () => { test('should throw error when start_time is after end_time', async () => {
const scheduleData = { const scheduleData = {
userId: 1, userId: 1,
title: 'Invalid Schedule', 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, is_fixed: false,
events: [
{
day_of_week: 'Friday',
start_time: '18:00:00',
end_time: '17:00:00', // start_time이 더 늦음
},
],
}; };
await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Start time must be before end time'); await expect(scheduleService.createSchedules(scheduleData))
.rejects
.toThrow('Start time must be before end time');
}); });
}); });
...@@ -121,37 +150,40 @@ describe('Schedule Service', () => { ...@@ -121,37 +150,40 @@ describe('Schedule Service', () => {
test('should update an existing schedule successfully', async () => { test('should update an existing schedule successfully', async () => {
const updateData = { const updateData = {
title: 'Alice\'s Updated Flexible Schedule', title: 'Alice\'s Updated Flexible Schedule',
start_time: new Date('2024-05-02T11:30:00Z'), start_time: '11:30:00',
end_time: new Date('2024-05-02T12:30:00Z'), end_time: '12:30:00',
}; };
const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData); const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData);
expect(updatedSchedule).toBeDefined(); expect(updatedSchedule).toBeDefined();
expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule'); 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.start_time).toBe('11:30');
expect(updatedSchedule.end_time.toISOString()).toBe(new Date('2024-05-02T12:30:00Z').toISOString()); expect(updatedSchedule.end_time).toBe('12:30');
expect(updatedSchedule.expiry_date).toBeInstanceOf(Date);
}); });
test('should throw error when updating a non-existing schedule', async () => { test('should throw error when updating a non-existing schedule', async () => {
const updateData = { const updateData = {
title: 'Non-existing Schedule', title: 'Non-existing Schedule',
start_time: new Date('2024-05-06T10:00:00Z'), start_time: '10:00:00',
end_time: new Date('2024-05-06T11:00:00Z'), 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 () => { test('should throw error when updated schedule overlaps with existing schedule', async () => {
const updateData = { const updateData = {
title: 'Alice\'s Overlapping Update', title: 'Alice\'s Overlapping Update',
start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침 start_time: '09:30:00', // 기존 스케줄과 겹침
end_time: new Date('2024-05-01T10:30:00Z'), 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');
}); });
}); });
...@@ -167,17 +199,22 @@ describe('Schedule Service', () => { ...@@ -167,17 +199,22 @@ describe('Schedule Service', () => {
}); });
test('should throw error when deleting a non-existing schedule', async () => { 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', () => { describe('getAllSchedules', () => {
test('should retrieve all valid schedules for a user', async () => { test('should retrieve all valid schedules for a user', async () => {
// 사용자 Alice의 모든 스케줄 조회 // 사용자 Alice의 모든 스케줄 조회 (user_id: 1)
const schedules = await scheduleService.getAllSchedules(1); const schedules = await scheduleService.getAllSchedules(1);
expect(schedules).toBeDefined();
expect(Array.isArray(schedules)).toBe(true);
expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true
expect(schedules[0].title).toBe('Alice\'s Fixed Schedule'); expect(schedules[0].title).toBe('Alice\'s Fixed Schedule');
expect(schedules[0].day_of_week).toBe('Monday');
}); });
}); });
...@@ -187,57 +224,13 @@ describe('Schedule Service', () => { ...@@ -187,57 +224,13 @@ describe('Schedule Service', () => {
expect(schedule).toBeDefined(); expect(schedule).toBeDefined();
expect(schedule.title).toBe('Alice\'s Fixed Schedule'); expect(schedule.title).toBe('Alice\'s Fixed Schedule');
expect(schedule.day_of_week).toBe('Monday');
}); });
test('should throw error when retrieving a non-existing schedule', async () => { test('should throw error when retrieving a non-existing schedule', async () => {
await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found'); 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'), // 이미 만료됨
});
// 만료되지 않은 유동 스케줄 생성
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();
}); });
}); });
......
// services/scheduleService.js // services/scheduleService.js
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const {Schedule} = require('../models'); const Schedule = require('../models/Schedule');
const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO'); const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
class scheduleService { class ScheduleService {
/** /**
* 트랜잭션 래퍼 함수 * 스케줄 생성 (벌크)
*/ */
async withTransaction(callback) { async createSchedules({ userId, title, is_fixed, events }) {
const transaction = await sequelize.transaction(); // 직접 sequelize 사용 return await sequelize.transaction(async (transaction) => {
try { const scheduleDTOs = [];
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;
}
/**
* 스케줄 유효성 검사
* 이미 컨트롤러에서 검증했으므로, 추가 검증 필요 시 수행
*/
validateScheduleTime(start_time, end_time) {
if (new Date(start_time) >= new Date(end_time)) {
throw new Error("Start time must be before end time");
}
}
/** for (const event of events) {
* 유동 스케줄 만료일 구하기 const { time_idx } = event;
*/
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( const overlap = await this.checkScheduleOverlap(
userId, userId,
start_time, time_idx,
end_time transaction
); );
if (overlap) { if (overlap) {
throw new Error("Schedule overlaps with existing schedule"); throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`);
} }
const scheduleData = { const scheduleData = {
user_id: userId, user_id: userId,
title, title,
start_time, time_idx,
end_time,
is_fixed, is_fixed,
expiry_date: is_fixed ? null : this.getNextMonday(start_time),
}; };
return Schedule.create(scheduleData, { transaction }); const schedule = await Schedule.create(scheduleData, { transaction });
}); scheduleDTOs.push(new ScheduleResponseDTO(schedule));
}
return new ScheduleResponseDTO(schedule); return scheduleDTOs;
});
} }
/** /**
* 사용자 스케줄 수정 * 스케줄 수정 (벌크)
*/ */
async updateSchedule(id, userId, updateData) { async updateSchedules(userId, updates) {
const updatedSchedule = await this.withTransaction(async (transaction) => { return await sequelize.transaction(async (transaction) => {
const updatedSchedules = [];
for (const update of updates) {
const { time_idx, title, is_fixed } = update;
const schedule = await Schedule.findOne({ const schedule = await Schedule.findOne({
where: { id, user_id: userId }, where: { user_id: userId, time_idx },
transaction, transaction,
}); });
if (!schedule) { if (!schedule) {
throw new Error("Schedule not found"); throw { code: 'SCHEDULE_NOT_FOUND', message: `Schedule not found at time_idx ${time_idx}` };
} }
// 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행 // 중복 스케줄 검사 (time_idx는 고유하므로 필요 없음)
if (updateData.start_time && updateData.end_time) { // 만약 다른 필드를 기반으로 중복을 검사한다면 추가 로직 필요
this.validateScheduleTime(updateData.start_time, updateData.end_time);
}
const overlap = await this.checkScheduleOverlap( const updatedData = {};
userId, if (title !== undefined) updatedData.title = title;
updateData.start_time || schedule.start_time, if (is_fixed !== undefined) updatedData.is_fixed = is_fixed;
updateData.end_time || schedule.end_time,
id
);
if (overlap) {
throw new Error("Schedule overlaps with existing schedule");
}
const is_fixed = schedule.is_fixed; const updatedSchedule = await schedule.update(updatedData, { transaction });
const updatedDataWithExpiry = { updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule));
...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 deleteSchedule(id, userId) { async deleteSchedules(userId, time_idxs) {
return this.withTransaction(async (transaction) => { return await sequelize.transaction(async (transaction) => {
const result = await Schedule.destroy({ const deleted_time_idxs = [];
where: { id, user_id: userId },
for (const time_idx of time_idxs) {
const deletedCount = await Schedule.destroy({
where: { user_id: userId, time_idx },
transaction, transaction,
}); });
if (!result) { if (deletedCount === 0) {
throw new Error("Schedule not found"); throw new Error(`Schedule not found at time_idx ${time_idx}`);
} }
// 삭제 성공 메시지 반환 deleted_time_idxs.push(time_idx);
return { message: "Schedule successfully deleted" };
});
} }
/** return { deleted_time_idxs };
* 해당 사용자의 스케줄 정보 조회
*/
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}`);
}
} }
/** /**
* 해당 사용자의 특정 스케줄 조회 * 특정 time_idx로 스케줄 조회
*/ */
async getScheduleById(id, userId) { async getScheduleByTimeIdx(userId, time_idx) {
try {
const schedule = await Schedule.findOne({ const schedule = await Schedule.findOne({
where: this.getScheduleWhereClause(userId, id), where: { user_id: userId, time_idx },
}); });
if (!schedule) { if (!schedule) {
throw new Error("Schedule not found"); throw new Error('Schedule not found');
} }
return new ScheduleResponseDTO(schedule); return new ScheduleResponseDTO(schedule);
} catch (error) {
throw new Error(`Failed to fetch schedule: ${error.message}`);
}
} }
/** /**
* 만료된 유동 스케줄 정리 * 모든 스케줄 조회
*/ */
async cleanExpiredSchedules() { async getAllSchedules(userId) {
try { try {
await Schedule.destroy({ const schedules = await Schedule.findAll({
where: { where: { user_id: userId },
is_fixed: false, order: [['time_idx', 'ASC']],
expiry_date: { [Op.lte]: new Date() },
},
}); });
return schedules.map((schedule) => new ScheduleResponseDTO(schedule));
} catch (error) { } catch (error) {
throw new Error(`Failed to clean expired schedules: ${error.message}`); throw new Error(`Failed to fetch schedules: ${error.message}`);
} }
} }
/** /**
* 스케줄 중복 검사 * 중복 스케줄 검사
*/ */
async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { async checkScheduleOverlap(userId, time_idx, transaction) {
try { const overlappingSchedule = await Schedule.findOne({
const where = { where: { user_id: userId, time_idx },
user_id: userId, transaction,
[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) { return !!overlappingSchedule;
where.id = { [Op.ne]: excludeId };
} }
const overlappingSchedule = await Schedule.findOne({ where }); async cleanExpiredSchedules() {
return overlappingSchedule; try {
const deletedCount = await Schedule.destroy({
where: { is_fixed: false },
});
//console.log(`Deleted ${deletedCount} flexible schedules.`);
} catch (error) { } catch (error) {
throw new Error(`Failed to check schedule overlap: ${error.message}`); console.error('Failed to clean expired schedules:', error);
throw error;
} }
} }
} }
module.exports = new scheduleService(); module.exports = new ScheduleService();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment