diff --git a/services/scheduleService.js b/services/scheduleService.js new file mode 100644 index 0000000000000000000000000000000000000000..79a787f9e7e3c9358a3c8e73888d75fd501879a3 --- /dev/null +++ b/services/scheduleService.js @@ -0,0 +1,230 @@ +const { Op } = require('sequelize'); +const Schedule = require('../models/Schedule'); + +class schedulService { + + /** + * transactin wrapper 함수 + */ + async withTransaction(callback) { + const transaction = await Schedule.sequelize.transaction(); + try { + const result = await callback(transaction); + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + /** + * 공통 where 절 생성 + */ + getScheduleWhereClause(userId, id = null) { + const where = { + user_id: userId, + [Op.or]: [ + { is_fixed: true }, + { + is_fixed: false, + expiry_date: { + [Op.gt]: new Date() + } + } + ] + }; + + if (id) { + where.id = id; + } + + return where; + } + + /** + * 스케줄 유효성 검사 + */ + 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() { + const date = new Date(); + const day = date.getDay(); + const daysUntilNextMonday = (8 - day) % 7; + date.setDate(date.getDate() + daysUntilNextMonday); + date.setHours(0, 0, 0, 0); + return date; + } + + /** + * 사용자 스케줄 생성 + */ + async createSchedule({ userId, title, start_time, end_time, is_fixed }) { + return this.withTransaction(async (transaction) => { + this.validateScheduleTime(start_time, end_time); + + const overlap = await this.checkScheduleOverlap(userId, start_time, end_time); + if (overlap) { + throw new Error('Schedule overlaps with existing schedule'); + } + + const scheduleData = { + user_id: userId, + title, + start_time, + end_time, + is_fixed, + expiry_date: is_fixed ? null : this.getNextMonday() + }; + + return Schedule.create(scheduleData, { transaction }); + }); + } + + /** + * 사용자 스케줄 수정 + */ + async updateSchedule(id, userId, updateData) { + return this.withTransaction(async (transaction) => { + const schedule = await Schedule.findOne({ + where: { id, user_id: userId }, + transaction + }); + + if (!schedule) { + throw new Error('Schedule not found'); + } + + this.validateScheduleTime(updateData.start_time, updateData.end_time); + + const overlap = await this.checkScheduleOverlap( + userId, + updateData.start_time, + updateData.end_time, + id + ); + if (overlap) { + throw new Error('Schedule overlaps with existing schedule'); + } + + delete updateData.is_fixed; + return schedule.update(updateData, { transaction }); + }); + } + + /** + * 사용자 스케줄 삭제 + */ + async deleteSchedule(id, userId) { + return this.withTransaction(async (transaction) => { + const result = await Schedule.destroy({ + where: { id, user_id: userId }, + transaction + }); + + if (!result) { + throw new Error('Schedule not found'); + } + + return true; + }); + } + + /** + * 해당 사용자의 스케줄 정보 조회 + */ + async getAllSchedules(userId) { + try { + return Schedule.findAll({ + where: this.getScheduleWhereClause(userId), + order: [['start_time', 'ASC']] + }); + } catch (error) { + throw new Error(`Failed to fetch schedules: ${error.message}`); + } + } + + /** + * 해당 사용자의 특정 스케줄 조회 + */ + async getScheduleById(id, userId) { + try { + const schedule = await Schedule.findOne({ + where: this.getScheduleWhereClause(userId, id) + }); + + if (!schedule) { + throw new Error('Schedule not found'); + } + + return schedule; + } catch (error) { + throw new Error(`Failed to fetch schedule: ${error.message}`); + } + } + + + /** + * 만료된 유동 스케줄 정리 -> utils에 cron job 추가해서 실행하도록 설정 + */ + async cleanExpiredSchedules() { + try { + await Schedule.destroy({ + where: { + is_fixed: false, + expiry_date: { + [Op.lte]: new Date() + } + } + }); + } catch (error) { + throw new Error(`Failed to clean expired schedules: ${error.message}`); + } + } + + + /** + * 스케줄 중복 검사 -> 기존 스케줄 시간대에 추가 못하도록 + */ + async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { + try { + const where = { + user_id: userId, + [Op.or]: [ + { + // 새로운 스케줄이 기존 스케줄 내 존재 + [Op.and]: [ + { start_time: { [Op.lte]: start_time } }, + { end_time: { [Op.gte]: start_time } } + ] + }, + { + // 새로운 스케줄이 기존 스케줄을 포함 + [Op.and]: [ + { start_time: { [Op.gte]: start_time } }, + { start_time: { [Op.lte]: end_time } } + ] + } + ] + }; + + if (excludeId) { + where.id = { [Op.ne]: excludeId }; + } + + const overlappingSchedule = await Schedule.findOne({ where }); + return overlappingSchedule; + } catch (error) { + throw new Error(`Failed to check schedule overlap: ${error.message}`); + } + } +} + +module.exports = new scheduleService();