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

Merge branch 'develop' into deploy

parents 01c9fd42 b00d1dc8
No related branches found
No related tags found
1 merge request!42[#25] 배포코드 master브랜치로 이동
...@@ -23,21 +23,17 @@ class scheduleController { ...@@ -23,21 +23,17 @@ class scheduleController {
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'); // 'create' 타입 검증 const validatedData = scheduleRequestDTO.validate('create');
const { title, is_fixed, events } = validatedData; const schedule = await ScheduleService.createSchedules({
const schedules = await ScheduleService.createSchedules({
userId, userId,
title, ...validatedData
is_fixed,
events
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: { data: {
schedules schedule
} }
}); });
} catch (error) { } catch (error) {
...@@ -49,7 +45,7 @@ class scheduleController { ...@@ -49,7 +45,7 @@ class scheduleController {
} }
}); });
} }
} }
/** /**
* 스케줄 수정 * 스케줄 수정
...@@ -57,31 +53,28 @@ class scheduleController { ...@@ -57,31 +53,28 @@ class scheduleController {
* Bulk update 지원 * Bulk update 지원
* 요청 본문 예시: * 요청 본문 예시:
* { * {
* updates: [ * "originalTitle": "알고리즘 스터디", // 기존 스케줄의 제목
* { time_idx: 36, title: 'New Title', is_fixed: true }, * "title": "알고리즘 스터디 2.0", // 변경할 제목 (제목 변경 안할거면 기존 제목을 넣어야함 * -> title로 동일 스케줄을 찾아서)
* { time_idx: 44, title: 'Another Title' }, * "is_fixed": true,
* // ... * "time_indices": [36, 37, 38, 40] // 변경할 time_indices 배열
* ]
* } * }
*/ */
async updateSchedules(req, res) { async updateSchedules(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('bulk_update'); // 'bulk_update' 타입 검증 const validatedData = scheduleRequestDTO.validate('bulk_update');
const { updates } = validatedData;
const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: {
schedules: updatedSchedules schedule: updatedSchedule
} }
}); });
} catch (error) { } catch (error) {
if (error.code === 'SCHEDULE_NOT_FOUND') { if (error.message === 'Schedule not found') {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
error: { error: {
...@@ -106,24 +99,21 @@ class scheduleController { ...@@ -106,24 +99,21 @@ class scheduleController {
* Bulk delete 지원 * Bulk delete 지원
* 요청 본문 예시: * 요청 본문 예시:
* { * {
* time_idxs: [36, 44, ...] * "title": "알고리즘 스터디"
* } * }
*/ */
async deleteSchedules(req, res) { async deleteSchedules(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('bulk_delete'); // 'bulk_delete' 타입 검증 const validatedData = scheduleRequestDTO.validate('bulk_delete');
const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
const { time_idxs } = validatedData;
const result = await ScheduleService.deleteSchedules(userId, time_idxs);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: {
message: 'Schedules successfully deleted', message: 'Schedule successfully deleted',
deleted_time_idxs: result.deleted_time_idxs deletedCount: result.deletedCount
} }
}); });
} catch (error) { } catch (error) {
...@@ -136,7 +126,6 @@ class scheduleController { ...@@ -136,7 +126,6 @@ class scheduleController {
}); });
} }
} }
/** /**
* 해당 사용자 전체 스케줄 조회 * 해당 사용자 전체 스케줄 조회
* GET /api/schedule/all * GET /api/schedule/all
...@@ -148,7 +137,9 @@ class scheduleController { ...@@ -148,7 +137,9 @@ class scheduleController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedules data: {
schedules
}
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -171,11 +162,15 @@ class scheduleController { ...@@ -171,11 +162,15 @@ class scheduleController {
const { time_idx } = req.params; const { time_idx } = req.params;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, parseInt(time_idx, 10)); const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) });
const validatedData = scheduleRequestDTO.validate('get_by_time_idx');
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: {
schedule
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
......
...@@ -13,27 +13,28 @@ class ScheduleRequestDTO { ...@@ -13,27 +13,28 @@ class ScheduleRequestDTO {
schema = Joi.object({ schema = Joi.object({
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
is_fixed: Joi.boolean().required(), is_fixed: Joi.boolean().required(),
events: Joi.array().items( time_indices: Joi.array()
Joi.object({ .items(Joi.number().integer().min(0).max(671))
time_idx: Joi.number().integer().min(0).max(671).required(), .min(1)
}) .required()
).min(1).required()
}); });
} else if (type === 'bulk_update') { } else if (type === 'bulk_update') {
schema = Joi.object({ schema = Joi.object({
updates: Joi.array().items( originalTitle: Joi.string().min(1).max(255).required(),
Joi.object({ title: Joi.string().min(1).max(255).required(),
time_idx: Joi.number().integer().min(0).max(671).required(), is_fixed: Joi.boolean().required(),
title: Joi.string().min(1).max(255).optional(), time_indices: Joi.array()
is_fixed: Joi.boolean().optional(), .items(Joi.number().integer().min(0).max(671))
}) .min(1)
).min(1).required() .required()
}); });
} else if (type === 'bulk_delete') { } else if (type === 'bulk_delete') {
schema = Joi.object({ schema = Joi.object({
time_idxs: Joi.array().items( title: Joi.string().min(1).max(255).required()
Joi.number().integer().min(0).max(671).required() });
).min(1).required() } else if (type === 'get_by_time_idx') {
schema = Joi.object({
time_idx: Joi.number().integer().min(0).max(671).required()
}); });
} }
......
// dtos/ScheduleResponseDTO.js // dtos/ScheduleResponseDTO.js
class ScheduleResponseDTO { class ScheduleResponseDTO {
constructor(schedule) { static groupSchedules(schedules) {
this.id = schedule.id; const grouped = schedules.reduce((acc, schedule) => {
this.user_id = schedule.user_id; const key = `${schedule.title}-${schedule.is_fixed}`;
this.title = schedule.title; if (!acc[key]) {
this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가 acc[key] = {
this.is_fixed = schedule.is_fixed; id: schedule.id,
this.createdAt = schedule.createdAt; user_id: schedule.user_id,
this.updatedAt = schedule.updatedAt; title: schedule.title,
is_fixed: schedule.is_fixed,
time_indices: [],
createdAt: schedule.createdAt,
updatedAt: schedule.updatedAt
};
}
acc[key].time_indices.push(schedule.time_idx);
return acc;
}, {});
return Object.values(grouped);
} }
} }
......
...@@ -118,20 +118,16 @@ class MeetingService { ...@@ -118,20 +118,16 @@ class MeetingService {
{ transaction } { transaction }
); );
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) const time_indices = Array.from(
const events = []; { length: time_idx_end - time_idx_start + 1 },
for (let idx = time_idx_start; idx <= time_idx_end; idx++) { (_, i) => time_idx_start + i
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: created_by,
title: `번개 모임: ${title}`,
is_fixed: false,
events: events,
},
transaction
); );
await ScheduleService.createSchedules({
userId: created_by,
title: `번개 모임: ${title}`,
is_fixed: false,
time_indices: time_indices,
}, transaction);
// 친구 초대 로직 호출 // 친구 초대 로직 호출
const invitedFriendIds = await this.sendInvites({ const invitedFriendIds = await this.sendInvites({
...@@ -264,24 +260,17 @@ class MeetingService { ...@@ -264,24 +260,17 @@ class MeetingService {
{ transaction } { transaction }
); );
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) const time_indices = Array.from(
const events = []; { length: meeting.time_idx_end - meeting.time_idx_start + 1 },
for ( (_, i) => meeting.time_idx_start + i
let idx = meeting.time_idx_start; );
idx <= meeting.time_idx_end;
idx++ await ScheduleService.createSchedules({
) { userId: userId,
events.push({ time_idx: idx }); title: `번개 모임: ${meeting.title}`,
} is_fixed: false,
await ScheduleService.createSchedules( time_indices: time_indices,
{ }, transaction);
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: false,
events: events,
},
transaction
);
// 채팅방 참가 (MongoDB) // 채팅방 참가 (MongoDB)
const user = await User.findOne({ const user = await User.findOne({
......
// test/scheduleService.test.js
const sequelize = require('../config/sequelize');
const { Schedule, User, Meeting, MeetingParticipant, FcmToken } = require('../models');
const ScheduleService = require('../services/scheduleService');
const MeetingService = require('../services/meetingService');
const ChatRooms = require('../schemas/chatRooms');
describe('Schedule Service and Meeting Integration Tests', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
beforeEach(async () => {
await MeetingParticipant.destroy({ where: {} });
await Meeting.destroy({ where: {} });
await Schedule.destroy({ where: {} });
await User.destroy({ where: {} });
await FcmToken.destroy({ where: {} });
// 더미 사용자 생성
await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
]);
// ChatRooms Mock 설정
jest.spyOn(ChatRooms.prototype, 'save').mockResolvedValue(undefined);
jest.spyOn(ChatRooms, 'findOne').mockResolvedValue({
participants: [],
isOnline: new Map(),
lastReadAt: new Map(),
lastReadLogId: new Map(),
save: jest.fn().mockResolvedValue(true)
});
});
afterAll(async () => {
await sequelize.close();
});
describe('Schedule Service Tests', () => {
test('should create schedule with time_indices', async () => {
const scheduleData = {
userId: 1,
title: 'Test Schedule',
is_fixed: true,
time_indices: [36, 37, 38]
};
const schedule = await ScheduleService.createSchedules(scheduleData);
expect(schedule).toBeDefined();
expect(schedule.title).toBe('Test Schedule');
expect(schedule.time_indices).toEqual([36, 37, 38]);
const dbSchedules = await Schedule.findAll({
where: { user_id: 1, title: 'Test Schedule' }
});
expect(dbSchedules.length).toBe(3);
});
test('should update schedule with new time_indices', async () => {
await ScheduleService.createSchedules({
userId: 1,
title: 'Original Schedule',
is_fixed: true,
time_indices: [36, 37, 38]
});
const updateData = {
originalTitle: 'Original Schedule',
title: 'Updated Schedule',
is_fixed: true,
time_indices: [36, 37, 38, 39]
};
const updatedSchedule = await ScheduleService.updateSchedules(1, updateData);
expect(updatedSchedule.title).toBe('Updated Schedule');
expect(updatedSchedule.time_indices).toEqual([36, 37, 38, 39]);
});
test('should delete schedule by title', async () => {
await ScheduleService.createSchedules({
userId: 1,
title: 'Schedule to Delete',
is_fixed: true,
time_indices: [40, 41, 42]
});
const result = await ScheduleService.deleteSchedules(1, 'Schedule to Delete');
expect(result.deletedCount).toBe(3);
const remainingSchedules = await Schedule.findAll({
where: { user_id: 1, title: 'Schedule to Delete' }
});
expect(remainingSchedules.length).toBe(0);
});
});
describe('Meeting Integration Tests', () => {
beforeEach(() => {
jest.spyOn(User, 'findOne').mockResolvedValue({
id: 1,
name: 'Alice',
email: 'alice@example.com',
fcmTokenList: []
});
});
test('should create meeting with correct schedules', async () => {
const meetingData = {
title: 'Test Meeting',
time_idx_start: 50,
time_idx_end: 52,
created_by: 1,
type: 'OPEN',
max_num: 5
};
const meeting = await MeetingService.createMeeting(meetingData);
const creatorSchedules = await Schedule.findAll({
where: {
user_id: 1,
title: `번개 모임: ${meetingData.title}`
}
});
expect(creatorSchedules.length).toBe(3);
expect(creatorSchedules.map(s => s.time_idx).sort()).toEqual([50, 51, 52]);
});
test('should create correct schedules when joining meeting', async () => {
const meetingData = {
title: 'Join Test Meeting',
time_idx_start: 60,
time_idx_end: 62,
created_by: 1,
type: 'OPEN',
max_num: 5,
time_idx_deadline: 59
};
const meeting = await MeetingService.createMeeting(meetingData);
jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(58);
await MeetingService.joinMeeting(meeting.meeting_id, 2);
const participantSchedules = await Schedule.findAll({
where: {
user_id: 2,
title: `번개 모임: ${meetingData.title}`
}
});
expect(participantSchedules.length).toBe(3);
expect(participantSchedules.map(s => s.time_idx).sort()).toEqual([60, 61, 62]);
});
test('should handle schedule conflicts correctly', async () => {
await ScheduleService.createSchedules({
userId: 2,
title: 'Existing Schedule',
is_fixed: true,
time_indices: [70, 71]
});
const meetingData = {
title: 'Conflict Test Meeting',
time_idx_start: 70,
time_idx_end: 72,
created_by: 1,
type: 'OPEN',
max_num: 5,
time_idx_deadline: 69
};
const meeting = await MeetingService.createMeeting(meetingData);
await expect(
MeetingService.joinMeeting(meeting.meeting_id, 2)
).rejects.toThrow('스케줄이 겹칩니다');
});
});
});
\ No newline at end of file
...@@ -9,158 +9,222 @@ class ScheduleService { ...@@ -9,158 +9,222 @@ class ScheduleService {
* 스케줄 생성 (벌크) * 스케줄 생성 (벌크)
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가 * @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/ */
async createSchedules({ userId, title, is_fixed, events }, transaction = null) { async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
const scheduleDTOs = []; // 중복 검사
for (const time_idx of time_indices) {
for (const event of events) {
const { time_idx } = event;
// 중복 스케줄 검사
const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction); const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction);
if (overlap) { if (overlap) {
throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`); throw new Error(`Schedule overlaps 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; const createdSchedules = await Promise.all(
} time_indices.map(time_idx =>
Schedule.create({
/** user_id: userId,
* 스케줄 수정 (벌크) title,
* @param {Array} updates - 수정할 스케줄 배열 time_idx,
*/ is_fixed
async updateSchedules(userId, updates, transaction = null) { }, { transaction })
const updatedSchedules = []; )
);
for (const update of updates) { return {
const { time_idx, title, is_fixed } = update; id: createdSchedules[0].id,
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt
};
}
const schedule = await Schedule.findOne({ async getAllSchedules(userId) {
where: { user_id: userId, time_idx }, try {
transaction, const schedules = await Schedule.findAll({
where: { user_id: userId },
order: [['time_idx', 'ASC']]
}); });
if (!schedule) { return ScheduleResponseDTO.groupSchedules(schedules);
throw new Error(`Schedule not found at time_idx ${time_idx}`); } catch (error) {
} throw new Error(`Failed to fetch schedules: ${error.message}`);
}
}
async updateSchedules(userId, updates, transaction = null) {
const { originalTitle, title, is_fixed, time_indices } = updates;
const updatedData = {}; // 기존 스케줄 조회
if (title !== undefined) updatedData.title = title; const existingSchedules = await Schedule.findAll({
if (is_fixed !== undefined) updatedData.is_fixed = is_fixed; where: {
user_id: userId,
title: originalTitle
},
transaction
});
const updatedSchedule = await schedule.update(updatedData, { transaction }); if (existingSchedules.length === 0) {
updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule)); throw new Error('Schedule not found');
} }
return updatedSchedules; const existingTimeIndices = existingSchedules.map(s => s.time_idx); // 기존 시간대
} const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); // 삭제할 시간대
const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); // 추가할 시간대
const t = transaction || await sequelize.transaction();
/** try {
* 스케줄 삭제 (벌크) // 삭제
* @param {number} userId - 사용자 ID if (toDelete.length > 0) {
* @param {Array<number>} time_idxs - 삭제할 스케줄의 time_idx 배열 await Schedule.destroy({
* @param {object} [transaction] - Sequelize 트랜잭션 객체 where: {
*/ user_id: userId,
async deleteSchedules(userId, time_idxs, transaction = null) { title: originalTitle,
const deleted_time_idxs = []; time_idx: {
[Op.in]: toDelete
}
},
transaction: t
});
}
for (const time_idx of time_idxs) { // 제목, 고정/유동 업데이트
const deletedCount = await Schedule.destroy({ await Schedule.update(
where: { user_id: userId, time_idx }, {
transaction, title,
}); is_fixed
},
{
where: {
user_id: userId,
title: originalTitle
},
transaction: t
}
);
// 새로운 time_indices 추가
if (toAdd.length > 0) {
await Promise.all(
toAdd.map(time_idx =>
Schedule.create({
user_id: userId,
title,
time_idx,
is_fixed
}, { transaction: t })
)
);
}
if (deletedCount === 0) { if (!transaction) {
throw new Error(`Schedule not found at time_idx ${time_idx}`); await t.commit();
} }
deleted_time_idxs.push(time_idx); return {
id: existingSchedules[0].id,
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: existingSchedules[0].createdAt,
updatedAt: new Date()
};
} catch (error) {
if (!transaction) {
await t.rollback();
}
throw error;
} }
}
async deleteSchedules(userId, title, transaction = null) {
const deletedSchedules = await Schedule.destroy({
where: {
user_id: userId,
title
},
transaction
});
return { deleted_time_idxs }; return { deletedCount: deletedSchedules };
} }
/** /**
* 특정 time_idx로 스케줄 조회 * 특정 time_idx로 스케줄 조회
*/ */
async getScheduleByTimeIdx(userId, time_idx) { async getScheduleByTimeIdx(userId, time_idx) {
// 해당 time_idx의 스케줄 찾기
const schedule = await Schedule.findOne({ const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx }, 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); // 같은 제목의 모든 스케줄 찾기
const relatedSchedules = await Schedule.findAll({
where: {
user_id: userId,
title: schedule.title,
is_fixed: schedule.is_fixed
},
order: [['time_idx', 'ASC']]
});
return ScheduleResponseDTO.groupSchedules(relatedSchedules)[0];
} }
/**
* 모든 스케줄 조회
*/
async getAllSchedules(userId) { async getAllSchedules(userId) {
try { try {
const schedules = await Schedule.findAll({ const schedules = await Schedule.findAll({
where: { user_id: userId }, where: { user_id: userId },
order: [['time_idx', 'ASC']], order: [['time_idx', 'ASC']]
}); });
return schedules.map((schedule) => new ScheduleResponseDTO(schedule)); return ScheduleResponseDTO.groupSchedules(schedules);
} catch (error) { } catch (error) {
throw new Error(`Failed to fetch schedules: ${error.message}`); throw new Error(`Failed to fetch schedules: ${error.message}`);
} }
} }
/**
* 중복 스케줄 검사
*/
async checkScheduleOverlap(userId, time_idx, transaction = null) { async checkScheduleOverlap(userId, time_idx, transaction = null) {
const overlappingSchedule = await Schedule.findOne({ const overlappingSchedule = await Schedule.findOne({
where: { user_id: userId, time_idx }, where: { user_id: userId, time_idx },
transaction, transaction
}); });
return !!overlappingSchedule; return !!overlappingSchedule;
} }
async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) { async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) {
console.log( const overlappingSchedules = await Schedule.findAll({
`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`
);
const overlappingSchedule = await Schedule.findOne({
where: { where: {
user_id: userId, user_id: userId,
time_idx: { time_idx: {
[Op.between]: [time_idx_start, time_idx_end] [Op.between]: [time_idx_start, time_idx_end]
} }
}, },
transaction, transaction
}); });
console.log(`중복 스케줄: ${JSON.stringify(overlappingSchedule)}`);
const result = !!overlappingSchedule; const groupedSchedules = ScheduleResponseDTO.groupSchedules(overlappingSchedules);
console.log(`스케줄 충돌 결과: ${result}`); const result = groupedSchedules.length > 0;
return result;
console.log(`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`);
console.log(`중복 스케줄: ${JSON.stringify(groupedSchedules)}`);
console.log(`스케줄 충돌 결과: ${result}`);
return result;
} }
/**
* 만료된 스케줄 삭제
*/
async cleanExpiredSchedules() { async cleanExpiredSchedules() {
try { try {
const deletedCount = await Schedule.destroy({ const deletedCount = await Schedule.destroy({
where: { is_fixed: false }, where: { is_fixed: false }
}); });
//console.log(`Deleted ${deletedCount} flexible schedules.`); return { deletedCount };
} catch (error) { } catch (error) {
console.error('Failed to clean expired schedules:', error); console.error('Failed to clean expired schedules:', error);
throw error; throw error;
......
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