Skip to content
Snippets Groups Projects
Commit da135a2f authored by 세현 임's avatar 세현 임
Browse files

[#13] 서비스로직에 인덱스 도입하여 로직 최적화 안료

parents 383434ba 1abb4eea
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를 가져오기 위해 추가
......
This diff is collapsed.
// 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}` };
} }
// 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행
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