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

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

parents 383434ba 1abb4eea
No related branches found
No related tags found
2 merge requests!31Develop,!22[#13] 서비스로직에 인덱스 도입하여 로직 최적화 안료
......@@ -10,7 +10,7 @@ class MeetingController {
*/
async createMeeting(req, res) {
try {
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
const userId = req.user.id;
const meetingData = { ...req.body, created_by: userId };
// CreateMeetingRequestDTO를 사용하여 요청 데이터 검증
......@@ -31,7 +31,7 @@ class MeetingController {
*/
async getMeetings(req, res) {
try {
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
const meetings = await MeetingService.getMeetings(userId);
res.status(200).json(meetings);
......@@ -64,7 +64,7 @@ class MeetingController {
async joinMeeting(req, res) {
try {
const { meetingId } = req.params;
const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
await MeetingService.joinMeeting(meetingId, userId);
res.status(200).json({ message: '모임 및 채팅방 참가 완료' });
......
......@@ -6,28 +6,46 @@ class scheduleController {
/**
* 스케줄 생성
* 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) {
try {
const userId = req.user.id;
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,
...validatedData
title,
is_fixed,
events
});
return res.status(201).json({
success: true,
data: scheduleDTO
data: {
schedules
}
});
} catch (error) {
return res.status(400).json({
success: false,
error: {
message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR',
code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR'
message: error.message,
code: 'SCHEDULE_CREATE_ERROR'
}
});
}
......@@ -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 {
const { id } = req.params;
const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('update');
const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증
const { updates } = validatedData;
const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData);
const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
return res.status(200).json({
success: true,
data: scheduleDTO
data: {
schedules: updatedSchedules
}
});
} catch (error) {
if (error.message === 'Schedule not found') {
if (error.code === 'SCHEDULE_NOT_FOUND') {
return res.status(404).json({
success: false,
error: {
......@@ -59,14 +89,6 @@ class scheduleController {
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({
success: false,
......@@ -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 {
const { id } = req.params;
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({
success: true,
data: deleteResult
data: {
message: 'Schedules successfully deleted',
deleted_time_idxs: result.deleted_time_idxs
}
});
} catch (error) {
if (error.message === 'Schedule not found') {
return res.status(404).json({
success: false,
error: {
message: error.message,
code: 'SCHEDULE_NOT_FOUND'
}
});
}
return res.status(500).json({
return res.status(404).json({
success: false,
error: {
message: 'Failed to delete schedule',
code: 'DELETE_ERROR'
message: error.message,
code: 'SCHEDULE_DELETE_ERROR'
}
});
}
......@@ -120,11 +144,11 @@ class scheduleController {
async getAllSchedules(req, res) {
try {
const userId = req.user.id;
const schedulesDTO = await ScheduleService.getAllSchedules(userId);
const schedules = await ScheduleService.getAllSchedules(userId);
return res.status(200).json({
success: true,
data: schedulesDTO
data: schedules
});
} catch (error) {
return res.status(500).json({
......@@ -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 {
const { id } = req.params;
const { time_idx } = req.params;
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({
success: true,
data: scheduleDTO
data: schedule
});
} catch (error) {
if (error.message === 'Schedule not found') {
......
// dto/FriendListDTO.js
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) {
this.id = friend.id;
this.status = friend.status;
......
// dto/FriendRequestDTO.js
// dto/FriendResponseDTO.js
class FriendRequestDTO {
/**
* @param {object} friendRequest - Friend request object retrieved from the database.
*/
constructor(friendRequest) {
this.id = friendRequest.id;
class FriendResponseDTO {
constructor(friendResponse) {
this.id = friendResponse.id;
this.requester = {
id: friendRequest.requester.id,
name: friendRequest.requester.name,
email: friendRequest.requester.email
id: friendResponse.requester.id,
name: friendResponse.requester.name,
email: friendResponse.requester.email
};
this.receiver = {
id: friendRequest.receiver.id,
name: friendRequest.receiver.name,
email: friendRequest.receiver.email
id: friendResponse.receiver.id,
name: friendResponse.receiver.name,
email: friendResponse.receiver.email
};
this.status = friendRequest.status;
this.createdAt = friendRequest.createdAt;
this.updatedAt = friendRequest.updatedAt;
this.status = friendResponse.status;
this.createdAt = friendResponse.createdAt;
this.updatedAt = friendResponse.updatedAt;
}
}
module.exports = FriendRequestDTO;
module.exports = FriendResponseDTO;
// dtos/ScheduleRequestDTO.js
const Joi = require('joi');
class ScheduleRequestDTO {
constructor(data) {
this.data = data;
}
validate(type = 'create') {
// 기본 스키마 정의
let schema = Joi.object({
title: Joi.string().min(1).max(255).required(),
start_time: Joi.date().iso().required(),
end_time: Joi.date().iso().required(),
is_fixed: Joi.boolean().required()
});
let schema;
// 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음
if (type === 'update') {
if (type === 'create') {
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()
});
} 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()
});
} else if (type === 'bulk_delete') {
schema = Joi.object({
title: Joi.string().min(1).max(255).optional(),
start_time: Joi.date().iso().optional(),
end_time: Joi.date().iso().optional(),
is_fixed: Joi.boolean().optional()
}).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요
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 });
if (error) {
// 모든 에러 메시지를 하나의 문자열로 결합
const errorMessages = error.details.map(detail => detail.message).join(', ');
throw new Error(`Validation error: ${errorMessages}`);
}
// 검증된 데이터를 반환
return value;
}
}
......
......@@ -3,12 +3,10 @@
class ScheduleResponseDTO {
constructor(schedule) {
this.id = schedule.id;
this.user_id = schedule.user_id;
this.user_id = schedule.user_id;
this.title = schedule.title;
this.start_time = schedule.start_time;
this.end_time = schedule.end_time;
this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가
this.is_fixed = schedule.is_fixed;
this.expiry_date = schedule.expiry_date;
this.createdAt = schedule.createdAt;
this.updatedAt = schedule.updatedAt;
}
......
......@@ -8,29 +8,33 @@ const Schedule = sequelize.define('Schedule', {
type: DataTypes.STRING,
allowNull: false,
},
start_time: {
type: DataTypes.DATE,
allowNull: false,
},
end_time: {
type: DataTypes.DATE,
time_idx: { // 일주일을 15분 단위로 나눈 시간 인덱스
type: DataTypes.INTEGER,
allowNull: false,
validate: {
min: 0,
max: 671, // 7일 * 24시간 * 4 (15분 단위) - 1
},
},
is_fixed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
expiry_date: {
type: DataTypes.DATE,
allowNull: true
}
}, {
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;
\ No newline at end of file
module.exports = Schedule;
......@@ -4,7 +4,6 @@ const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임
// const User = require('../models/User');
// const Friend = require('../models/Friend');
const { Friend,User} = require('../models');
const friendService = require('./friendService'); // FriendService 임포트
// Sequelize의 Op를 가져오기 위해 추가
......
This diff is collapsed.
// services/scheduleService.js
const sequelize = require('../config/sequelize');
const { Op } = require('sequelize');
const {Schedule} = require('../models');
const Schedule = require('../models/Schedule');
const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
class scheduleService {
/**
* 트랜잭션 래퍼 함수
*/
async withTransaction(callback) {
const transaction = await sequelize.transaction(); // 직접 sequelize 사용
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;
class ScheduleService {
/**
* 스케줄 생성 (벌크)
*/
async createSchedules({ userId, title, is_fixed, events }) {
return await sequelize.transaction(async (transaction) => {
const scheduleDTOs = [];
for (const event of events) {
const { time_idx } = event;
// 중복 스케줄 검사
const overlap = await this.checkScheduleOverlap(
userId,
time_idx,
transaction
);
if (overlap) {
throw new Error(`Schedule overlaps with existing schedule 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;
});
}
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.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(
userId,
start_time,
end_time
);
if (overlap) {
throw new Error("Schedule overlaps with existing schedule");
}
/**
* 스케줄 수정 (벌크)
*/
async updateSchedules(userId, updates) {
return await sequelize.transaction(async (transaction) => {
const updatedSchedules = [];
const scheduleData = {
user_id: userId,
title,
start_time,
end_time,
is_fixed,
expiry_date: is_fixed ? null : this.getNextMonday(start_time),
};
for (const update of updates) {
const { time_idx, title, is_fixed } = update;
return Schedule.create(scheduleData, { transaction });
});
const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx },
transaction,
});
return new ScheduleResponseDTO(schedule);
}
if (!schedule) {
throw { code: 'SCHEDULE_NOT_FOUND', message: `Schedule not found at time_idx ${time_idx}` };
}
/**
* 사용자 스케줄 수정
*/
async updateSchedule(id, userId, updateData) {
const updatedSchedule = await this.withTransaction(async (transaction) => {
const schedule = await Schedule.findOne({
where: { id, user_id: userId },
transaction,
});
const updatedData = {};
if (title !== undefined) updatedData.title = title;
if (is_fixed !== undefined) updatedData.is_fixed = is_fixed;
if (!schedule) {
throw new Error("Schedule not found");
}
const updatedSchedule = await schedule.update(updatedData, { transaction });
updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule));
}
// 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행
if (updateData.start_time && updateData.end_time) {
this.validateScheduleTime(updateData.start_time, updateData.end_time);
}
const overlap = await this.checkScheduleOverlap(
userId,
updateData.start_time || schedule.start_time,
updateData.end_time || schedule.end_time,
id
);
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 || schedule.start_time),
updatedAt: new Date(),
};
delete updatedDataWithExpiry.is_fixed;
return schedule.update(updatedDataWithExpiry, { transaction });
});
return updatedSchedules;
});
}
return new ScheduleResponseDTO(updatedSchedule);
}
/**
* 스케줄 삭제 (벌크)
*/
async deleteSchedules(userId, time_idxs) {
return await sequelize.transaction(async (transaction) => {
const deleted_time_idxs = [];
/**
* 사용자 스케줄 삭제
*/
async deleteSchedule(id, userId) {
return this.withTransaction(async (transaction) => {
const result = await Schedule.destroy({
where: { id, user_id: userId },
transaction,
});
for (const time_idx of time_idxs) {
const deletedCount = await Schedule.destroy({
where: { user_id: userId, time_idx },
transaction,
});
if (!result) {
throw new Error("Schedule not found");
}
if (deletedCount === 0) {
throw new Error(`Schedule not found at time_idx ${time_idx}`);
}
// 삭제 성공 메시지 반환
return { message: "Schedule successfully deleted" };
});
}
deleted_time_idxs.push(time_idx);
}
/**
* 해당 사용자의 스케줄 정보 조회
*/
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}`);
return { deleted_time_idxs };
});
}
}
/**
* 해당 사용자의 특정 스케줄 조회
*/
async getScheduleById(id, userId) {
try {
const schedule = await Schedule.findOne({
where: this.getScheduleWhereClause(userId, id),
});
/**
* 특정 time_idx로 스케줄 조회
*/
async getScheduleByTimeIdx(userId, time_idx) {
const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx },
});
if (!schedule) {
throw new Error("Schedule not found");
}
if (!schedule) {
throw new Error('Schedule not found');
}
return new ScheduleResponseDTO(schedule);
} catch (error) {
throw new Error(`Failed to fetch schedule: ${error.message}`);
return new ScheduleResponseDTO(schedule);
}
}
/**
* 만료된 유동 스케줄 정리
*/
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 getAllSchedules(userId) {
try {
const schedules = await Schedule.findAll({
where: { user_id: userId },
order: [['time_idx', 'ASC']],
});
return schedules.map((schedule) => new ScheduleResponseDTO(schedule));
} catch (error) {
throw new Error(`Failed to fetch 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 } },
],
},
],
};
/**
* 중복 스케줄 검사
*/
async checkScheduleOverlap(userId, time_idx, transaction) {
const overlappingSchedule = await Schedule.findOne({
where: { user_id: userId, time_idx },
transaction,
});
if (excludeId) {
where.id = { [Op.ne]: excludeId };
}
return !!overlappingSchedule;
}
const overlappingSchedule = await Schedule.findOne({ where });
return overlappingSchedule;
} catch (error) {
throw new Error(`Failed to check schedule overlap: ${error.message}`);
async cleanExpiredSchedules() {
try {
const deletedCount = await Schedule.destroy({
where: { is_fixed: false },
});
//console.log(`Deleted ${deletedCount} flexible schedules.`);
} catch (error) {
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.
Finish editing this message first!
Please register or to comment