Skip to content
Snippets Groups Projects
scheduleService.js 6.89 KiB
Newer Older
// services/scheduleService.js

const { Op } = require('sequelize');
const Schedule = require('../models/Schedule');
const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
조대희's avatar
조대희 committed
class scheduleService {
     * 트랜잭션 래퍼 함수
     */
    async withTransaction(callback) {
        const transaction = await Schedule.sequelize.transaction();
        try {
            const result = await callback(transaction);
            await transaction.commit();
            return result;
        } catch (error) {
            await transaction.rollback();
            throw error;
        }
    }

    /**
     * 공통 where 절 생성
     */
    getScheduleWhereClause(userId, id = null) {
        const where = {
            user_id: userId,
            [Op.or]: [
                { is_fixed: true },
                {
                    is_fixed: false,
                    expiry_date: { [Op.gt]: new Date() }
                }
            ]
        };

        if (id) {
            where.id = id;
        }

        return where;
    }

    /**
     * 스케줄 유효성 검사
     */
    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.getDay();
        const daysUntilNextMonday = (7 - day + 1) % 7;
        const nextMonday = new Date(date);
        nextMonday.setDate(date.getDate() + daysUntilNextMonday);
        nextMonday.setHours(0, 0, 0, 0);  // 자정으로 설정
    }

    /**
     * 사용자 스케줄 생성
     */
    async createSchedule({ userId, title, start_time, end_time, is_fixed }) {
        const schedule = await this.withTransaction(async (transaction) => {
            this.validateScheduleTime(start_time, end_time);
            const overlap = await this.checkScheduleOverlap(userId, start_time, end_time);
            if (overlap) {
                throw new Error('Schedule overlaps with existing schedule');
            }

            const scheduleData = {
                user_id: userId,
                title,
                start_time,
                end_time,
                is_fixed,
                expiry_date: is_fixed ? null : this.getNextMonday(start_time)
            return Schedule.create(scheduleData, { transaction });
        });

        return new ScheduleResponseDTO(schedule);
    /**
     * 사용자 스케줄 수정
     */
    async updateSchedule(id, userId, updateData) {
        const updatedSchedule = await this.withTransaction(async (transaction) => {
            const schedule = await Schedule.findOne({
                where: { id, user_id: userId },
                transaction
            });

            if (!schedule) {
                throw new Error('Schedule not found');
            this.validateScheduleTime(updateData.start_time, updateData.end_time);
            const overlap = await this.checkScheduleOverlap(
                userId,
                updateData.start_time,
            if (overlap) {
                throw new Error('Schedule overlaps with existing schedule');
            }

            const is_fixed = schedule.is_fixed;
            const updatedDataWithExpiry = {
                ...updateData,
                expiry_date: is_fixed ? null : this.getNextMonday(updateData.start_time),
                updatedAt: new Date()
            };
            delete updatedDataWithExpiry.is_fixed;
            return schedule.update(updatedDataWithExpiry, { transaction });

        return new ScheduleResponseDTO(updatedSchedule);
    /**
     * 사용자 스케줄 삭제
     */
    async deleteSchedule(id, userId) {
        return this.withTransaction(async (transaction) => {
            const result = await Schedule.destroy({
                where: { id, user_id: userId },
                transaction
                throw new Error('Schedule not found');
            // 삭제 성공 메시지 반환
            return { message: 'Schedule successfully deleted' };
    /**
     * 해당 사용자의 스케줄 정보 조회
     */
    async getAllSchedules(userId) {
        try {
            const schedules = await Schedule.findAll({
                where: this.getScheduleWhereClause(userId),
                order: [['start_time', 'ASC']]
            });
            const schedulesDTO = schedules.map(schedule => new ScheduleResponseDTO(schedule));
            return schedulesDTO;
        } catch (error) {
            throw new Error(`Failed to fetch schedules: ${error.message}`);
        }
    }

    /**
     * 해당 사용자의 특정 스케줄 조회
     */
    async getScheduleById(id, userId) {
        try {
            const schedule = await Schedule.findOne({
                where: this.getScheduleWhereClause(userId, id)
            if (!schedule) {
                throw new Error('Schedule not found');
            }

            return new ScheduleResponseDTO(schedule);
        } catch (error) {
            throw new Error(`Failed to fetch schedule: ${error.message}`);
        }
    }
     * 만료된 유동 스케줄 정리
     */
    async cleanExpiredSchedules() {
        try {
            await Schedule.destroy({
                where: {
                    is_fixed: false,
                    expiry_date: { [Op.lte]: new Date() }
                }
            });
        } catch (error) {
            throw new Error(`Failed to clean expired schedules: ${error.message}`);
        }
    }
     * 스케줄 중복 검사
     */
    async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) {
        try {
            const where = {
                user_id: userId,
                [Op.or]: [
                    {
                        [Op.and]: [
                            { start_time: { [Op.lte]: start_time } },
                            { end_time: { [Op.gte]: start_time } }
                        ]
                    },
                    {
                        [Op.and]: [
                            { start_time: { [Op.gte]: start_time } },
                            { start_time: { [Op.lte]: end_time } }
                        ]
                    }
                ]
            };
            if (excludeId) {
                where.id = { [Op.ne]: excludeId };
            }
            const overlappingSchedule = await Schedule.findOne({ where });
            return overlappingSchedule;
        } catch (error) {
            throw new Error(`Failed to check schedule overlap: ${error.message}`);
        }
    }
}

module.exports = new scheduleService();