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

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

parent ce8ae990
No related branches found
No related tags found
2 merge requests!31Develop,!22[#13] 서비스로직에 인덱스 도입하여 로직 최적화 안료
......@@ -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: '모임 및 채팅방 참가 완료' });
......
......@@ -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') {
......
// 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;
......
// 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;
// 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;
}
}
......
......@@ -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;
}
......
......@@ -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;
......@@ -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를 가져오기 위해 추가
......
// 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();
});
});
});
// 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();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment