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 {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create'); // 'create' 타입 검증
const validatedData = scheduleRequestDTO.validate('create');
const { title, is_fixed, events } = validatedData;
const schedules = await ScheduleService.createSchedules({
const schedule = await ScheduleService.createSchedules({
userId,
title,
is_fixed,
events
...validatedData
});
return res.status(201).json({
success: true,
data: {
schedules
schedule
}
});
} catch (error) {
......@@ -49,7 +45,7 @@ class scheduleController {
}
});
}
}
}
/**
* 스케줄 수정
......@@ -57,31 +53,28 @@ class scheduleController {
* Bulk update 지원
* 요청 본문 예시:
* {
* updates: [
* { time_idx: 36, title: 'New Title', is_fixed: true },
* { time_idx: 44, title: 'Another Title' },
* // ...
* ]
* "originalTitle": "알고리즘 스터디", // 기존 스케줄의 제목
* "title": "알고리즘 스터디 2.0", // 변경할 제목 (제목 변경 안할거면 기존 제목을 넣어야함 * -> title로 동일 스케줄을 찾아서)
* "is_fixed": true,
* "time_indices": [36, 37, 38, 40] // 변경할 time_indices 배열
* }
*/
async updateSchedules(req, res) {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증
const { updates } = validatedData;
const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
const validatedData = scheduleRequestDTO.validate('bulk_update');
const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData);
return res.status(200).json({
success: true,
data: {
schedules: updatedSchedules
schedule: updatedSchedule
}
});
} catch (error) {
if (error.code === 'SCHEDULE_NOT_FOUND') {
if (error.message === 'Schedule not found') {
return res.status(404).json({
success: false,
error: {
......@@ -106,24 +99,21 @@ class scheduleController {
* Bulk delete 지원
* 요청 본문 예시:
* {
* time_idxs: [36, 44, ...]
* "title": "알고리즘 스터디"
* }
*/
async deleteSchedules(req, res) {
try {
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_delete'); // 'bulk_delete' 타입 검증
const { time_idxs } = validatedData;
const result = await ScheduleService.deleteSchedules(userId, time_idxs);
const validatedData = scheduleRequestDTO.validate('bulk_delete');
const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
return res.status(200).json({
success: true,
data: {
message: 'Schedules successfully deleted',
deleted_time_idxs: result.deleted_time_idxs
message: 'Schedule successfully deleted',
deletedCount: result.deletedCount
}
});
} catch (error) {
......@@ -136,7 +126,6 @@ class scheduleController {
});
}
}
/**
* 해당 사용자 전체 스케줄 조회
* GET /api/schedule/all
......@@ -148,7 +137,9 @@ class scheduleController {
return res.status(200).json({
success: true,
data: schedules
data: {
schedules
}
});
} catch (error) {
return res.status(500).json({
......@@ -171,11 +162,15 @@ class scheduleController {
const { time_idx } = req.params;
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({
success: true,
data: schedule
data: {
schedule
}
});
} catch (error) {
if (error.message === 'Schedule not found') {
......
......@@ -13,27 +13,28 @@ class ScheduleRequestDTO {
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()
time_indices: Joi.array()
.items(Joi.number().integer().min(0).max(671))
.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()
originalTitle: Joi.string().min(1).max(255).required(),
title: Joi.string().min(1).max(255).required(),
is_fixed: Joi.boolean().required(),
time_indices: Joi.array()
.items(Joi.number().integer().min(0).max(671))
.min(1)
.required()
});
} else if (type === 'bulk_delete') {
schema = Joi.object({
time_idxs: Joi.array().items(
Joi.number().integer().min(0).max(671).required()
).min(1).required()
title: Joi.string().min(1).max(255).required()
});
} else if (type === 'get_by_time_idx') {
schema = Joi.object({
time_idx: Joi.number().integer().min(0).max(671).required()
});
}
......
// dtos/ScheduleResponseDTO.js
class ScheduleResponseDTO {
constructor(schedule) {
this.id = schedule.id;
this.user_id = schedule.user_id;
this.title = schedule.title;
this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가
this.is_fixed = schedule.is_fixed;
this.createdAt = schedule.createdAt;
this.updatedAt = schedule.updatedAt;
static groupSchedules(schedules) {
const grouped = schedules.reduce((acc, schedule) => {
const key = `${schedule.title}-${schedule.is_fixed}`;
if (!acc[key]) {
acc[key] = {
id: schedule.id,
user_id: schedule.user_id,
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 {
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (let idx = time_idx_start; idx <= time_idx_end; idx++) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: created_by,
title: `번개 모임: ${title}`,
is_fixed: false,
events: events,
},
transaction
const time_indices = Array.from(
{ length: time_idx_end - time_idx_start + 1 },
(_, i) => time_idx_start + i
);
await ScheduleService.createSchedules({
userId: created_by,
title: `번개 모임: ${title}`,
is_fixed: false,
time_indices: time_indices,
}, transaction);
// 친구 초대 로직 호출
const invitedFriendIds = await this.sendInvites({
......@@ -264,24 +260,17 @@ class MeetingService {
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (
let idx = meeting.time_idx_start;
idx <= meeting.time_idx_end;
idx++
) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: false,
events: events,
},
transaction
);
const time_indices = Array.from(
{ length: meeting.time_idx_end - meeting.time_idx_start + 1 },
(_, i) => meeting.time_idx_start + i
);
await ScheduleService.createSchedules({
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: false,
time_indices: time_indices,
}, transaction);
// 채팅방 참가 (MongoDB)
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 {
* 스케줄 생성 (벌크)
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/
async createSchedules({ userId, title, is_fixed, events }, transaction = null) {
const scheduleDTOs = [];
for (const event of events) {
const { time_idx } = event;
// 중복 스케줄 검사
async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
// 중복 검사
for (const time_idx of time_indices) {
const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction);
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;
}
/**
* 스케줄 수정 (벌크)
* @param {Array} updates - 수정할 스케줄 배열
*/
async updateSchedules(userId, updates, transaction = null) {
const updatedSchedules = [];
const createdSchedules = await Promise.all(
time_indices.map(time_idx =>
Schedule.create({
user_id: userId,
title,
time_idx,
is_fixed
}, { transaction })
)
);
for (const update of updates) {
const { time_idx, title, is_fixed } = update;
return {
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({
where: { user_id: userId, time_idx },
transaction,
async getAllSchedules(userId) {
try {
const schedules = await Schedule.findAll({
where: { user_id: userId },
order: [['time_idx', 'ASC']]
});
if (!schedule) {
throw new Error(`Schedule not found at time_idx ${time_idx}`);
}
return ScheduleResponseDTO.groupSchedules(schedules);
} 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;
if (is_fixed !== undefined) updatedData.is_fixed = is_fixed;
// 기존 스케줄 조회
const existingSchedules = await Schedule.findAll({
where: {
user_id: userId,
title: originalTitle
},
transaction
});
const updatedSchedule = await schedule.update(updatedData, { transaction });
updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule));
if (existingSchedules.length === 0) {
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();
/**
* 스케줄 삭제 (벌크)
* @param {number} userId - 사용자 ID
* @param {Array<number>} time_idxs - 삭제할 스케줄의 time_idx 배열
* @param {object} [transaction] - Sequelize 트랜잭션 객체
*/
async deleteSchedules(userId, time_idxs, transaction = null) {
const deleted_time_idxs = [];
try {
// 삭제
if (toDelete.length > 0) {
await Schedule.destroy({
where: {
user_id: userId,
title: originalTitle,
time_idx: {
[Op.in]: toDelete
}
},
transaction: t
});
}
for (const time_idx of time_idxs) {
const deletedCount = await Schedule.destroy({
where: { user_id: userId, time_idx },
transaction,
});
// 제목, 고정/유동 업데이트
await Schedule.update(
{
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) {
throw new Error(`Schedule not found at time_idx ${time_idx}`);
if (!transaction) {
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로 스케줄 조회
*/
async getScheduleByTimeIdx(userId, time_idx) {
// 해당 time_idx의 스케줄 찾기
const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx },
where: { user_id: userId, time_idx }
});
if (!schedule) {
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) {
try {
const schedules = await Schedule.findAll({
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) {
throw new Error(`Failed to fetch schedules: ${error.message}`);
}
}
/**
* 중복 스케줄 검사
*/
async checkScheduleOverlap(userId, time_idx, transaction = null) {
const overlappingSchedule = await Schedule.findOne({
where: { user_id: userId, time_idx },
transaction,
transaction
});
return !!overlappingSchedule;
}
async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) {
console.log(
`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`
);
const overlappingSchedule = await Schedule.findOne({
const overlappingSchedules = await Schedule.findAll({
where: {
user_id: userId,
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;
console.log(`스케줄 충돌 결과: ${result}`);
return result;
const groupedSchedules = ScheduleResponseDTO.groupSchedules(overlappingSchedules);
const result = groupedSchedules.length > 0;
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() {
try {
const deletedCount = await Schedule.destroy({
where: { is_fixed: false },
where: { is_fixed: false }
});
//console.log(`Deleted ${deletedCount} flexible schedules.`);
return { deletedCount };
} catch (error) {
console.error('Failed to clean expired schedules:', 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