Skip to content
Snippets Groups Projects
Commit 322150b1 authored by tpgus2603's avatar tpgus2603
Browse files

Merge branch 'develop' into deploy

parents 27122330 93d36c2b
No related branches found
No related tags found
1 merge request!42[#25] 배포코드 master브랜치로 이동
// controllers/scheduleController.js // controllers/scheduleController.js
const ScheduleService = require('../services/scheduleService'); const ScheduleService = require('../services/scheduleService');
const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO'); const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO');
const performanceMonitor = require('../utils/performanceMonitor');
class scheduleController { class scheduleController {
/** /**
...@@ -21,6 +22,7 @@ class scheduleController { ...@@ -21,6 +22,7 @@ class scheduleController {
*/ */
async createSchedule(req, res) { async createSchedule(req, res) {
try { try {
return await performanceMonitor.measureAsync('createSchedule', async () => {
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');
...@@ -32,9 +34,8 @@ class scheduleController { ...@@ -32,9 +34,8 @@ class scheduleController {
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: { data: { schedule }
schedule });
}
}); });
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
...@@ -61,6 +62,7 @@ class scheduleController { ...@@ -61,6 +62,7 @@ class scheduleController {
*/ */
async updateSchedules(req, res) { async updateSchedules(req, res) {
try { try {
return await performanceMonitor.measureAsync('updateSchedules', async () => {
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'); const validatedData = scheduleRequestDTO.validate('bulk_update');
...@@ -69,9 +71,8 @@ class scheduleController { ...@@ -69,9 +71,8 @@ class scheduleController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: { schedule: updatedSchedule }
schedule: updatedSchedule });
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
...@@ -104,9 +105,11 @@ class scheduleController { ...@@ -104,9 +105,11 @@ class scheduleController {
*/ */
async deleteSchedules(req, res) { async deleteSchedules(req, res) {
try { try {
return await performanceMonitor.measureAsync('deleteSchedules', async () => {
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'); const validatedData = scheduleRequestDTO.validate('bulk_delete');
const result = await ScheduleService.deleteSchedules(userId, validatedData.title); const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
return res.status(200).json({ return res.status(200).json({
...@@ -116,6 +119,7 @@ class scheduleController { ...@@ -116,6 +119,7 @@ class scheduleController {
deletedCount: result.deletedCount deletedCount: result.deletedCount
} }
}); });
});
} catch (error) { } catch (error) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
...@@ -132,14 +136,14 @@ class scheduleController { ...@@ -132,14 +136,14 @@ class scheduleController {
*/ */
async getAllSchedules(req, res) { async getAllSchedules(req, res) {
try { try {
return await performanceMonitor.measureAsync('getAllSchedules', async () => {
const userId = req.user.id; const userId = req.user.id;
const schedules = 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: { data: { schedules }
schedules });
}
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -159,18 +163,18 @@ class scheduleController { ...@@ -159,18 +163,18 @@ class scheduleController {
*/ */
async getScheduleByTimeIdx(req, res) { async getScheduleByTimeIdx(req, res) {
try { try {
return await performanceMonitor.measureAsync('getScheduleByTimeIdx', async () => {
const { time_idx } = req.params; const { time_idx } = req.params;
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) }); const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) });
const validatedData = scheduleRequestDTO.validate('get_by_time_idx'); const validatedData = scheduleRequestDTO.validate('get_by_time_idx');
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.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: { data: { schedule }
schedule });
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
......
// routes/performanceRoute.js
const express = require('express');
const router = express.Router();
const performanceMonitor = require('../utils/performanceMonitor');
router.get('/stats', (req, res) => {
const stats = performanceMonitor.getAllStats();
res.json({
success: true,
data: { stats }
});
});
module.exports = router;
// services/performance.test.js
require('dotenv').config();
const { Op } = require('sequelize');
const ScheduleService = require('./scheduleService');
const sequelize = require('../config/sequelize');
const Schedule = require('../models/schedule');
class PerformanceTester {
constructor() {
this.testUserIds = [1, 2, 3, 4, 5]; // 5명의 테스트 유저만 사용
this.results = {
operations: {
createSchedules: [],
getAllSchedules: [],
updateSchedules: [],
deleteSchedules: []
},
summary: {}
};
}
async setup() {
try {
await sequelize.authenticate();
console.log('Database connection established successfully.');
await Schedule.destroy({ where: {}, force: true });
console.log('Test data cleaned successfully.');
console.log('Using existing user IDs:', this.testUserIds);
} catch (error) {
console.error('Setup failed:', error);
throw error;
}
}
async runLoadTest() {
console.log('Starting simplified test...');
const testSchedules = this.testUserIds.map((userId, i) => ({
userId,
title: `Test Schedule ${i}`,
is_fixed: true,
time_indices: [i * 2, i * 2 + 1]
}));
console.log('Test schedules:', testSchedules);
const transaction = await sequelize.transaction();
try {
// Create 테스트
console.log('\nTesting createSchedules...');
const createdSchedules = [];
for (const schedule of testSchedules) {
const result = await this.measureOperation('createSchedules', async () => {
const created = await ScheduleService.createSchedules(schedule, transaction);
console.log(`Created schedule for user ${schedule.userId}`);
return created;
});
if (result) createdSchedules.push(result);
}
await transaction.commit();
// 생성된 스케줄 확인
const verifySchedules = await Schedule.findAll({
where: {
user_id: { [Op.in]: this.testUserIds }
},
raw: true
});
console.log('\nVerified schedules:', verifySchedules);
// GetAll 테스트
console.log('\nTesting getAllSchedules...');
for (const userId of this.testUserIds) {
await this.measureOperation('getAllSchedules', async () => {
return await ScheduleService.getAllSchedules(userId);
});
}
// Update 테스트
console.log('\nTesting updateSchedules...');
for (const schedule of createdSchedules) {
await this.measureOperation('updateSchedules', async () => {
return await ScheduleService.updateSchedules(schedule.user_id, {
originalTitle: schedule.title,
title: `Updated ${schedule.title}`,
is_fixed: schedule.is_fixed,
time_indices: schedule.time_indices
});
});
}
// Delete 테스트
console.log('\nTesting deleteSchedules...');
const deleteTransaction = await sequelize.transaction();
try {
for (const schedule of createdSchedules) {
await this.measureOperation('deleteSchedules', async () => {
return await ScheduleService.deleteSchedules(
schedule.user_id,
`Updated ${schedule.title}`,
deleteTransaction
);
});
}
await deleteTransaction.commit();
} catch (error) {
await deleteTransaction.rollback();
throw error;
}
} catch (error) {
await transaction.rollback();
throw error;
}
this.analyzePerfResults();
}
async measureOperation(name, operation) {
const start = process.hrtime.bigint();
try {
const result = await operation();
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000;
this.results.operations[name].push({ success: true, duration });
return result;
} catch (error) {
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000;
this.results.operations[name].push({
success: false,
duration,
error: error.message
});
console.error(`Error in ${name}:`, error.message);
return null;
}
}
analyzePerfResults() {
Object.entries(this.results.operations).forEach(([operation, results]) => {
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
if (successful.length > 0) {
const durations = successful.map(r => r.duration);
this.results.summary[operation] = {
totalRequests: results.length,
successCount: successful.length,
failCount: failed.length,
avgDuration: durations.reduce((a, b) => a + b, 0) / successful.length,
minDuration: Math.min(...durations),
maxDuration: Math.max(...durations),
p95: this.calculatePercentile(durations, 95),
p99: this.calculatePercentile(durations, 99)
};
}
});
this.printResults();
}
calculatePercentile(array, percentile) {
const sorted = array.sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
printResults() {
console.log('\n=== Performance Test Results ===');
Object.entries(this.results.summary).forEach(([operation, stats]) => {
console.log(`\n${operation}:`);
console.log(`Total Requests: ${stats.totalRequests}`);
console.log(`Success Rate: ${((stats.successCount / stats.totalRequests) * 100).toFixed(2)}%`);
console.log(`Average Duration: ${stats.avgDuration.toFixed(2)}ms`);
console.log(`Min Duration: ${stats.minDuration.toFixed(2)}ms`);
console.log(`Max Duration: ${stats.maxDuration.toFixed(2)}ms`);
console.log(`95th Percentile: ${stats.p95.toFixed(2)}ms`);
console.log(`99th Percentile: ${stats.p99.toFixed(2)}ms`);
});
}
async cleanup() {
try {
await Schedule.destroy({ where: {}, force: true });
console.log('Cleanup completed successfully.');
} catch (error) {
console.error('Cleanup failed:', error);
}
}
}
async function runTests() {
const tester = new PerformanceTester();
try {
await tester.setup();
console.log('Starting performance tests...');
await tester.runLoadTest();
} catch (error) {
console.error('Test failed:', error);
} finally {
await sequelize.close();
}
}
runTests();
\ No newline at end of file
...@@ -10,24 +10,33 @@ class ScheduleService { ...@@ -10,24 +10,33 @@ class ScheduleService {
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가 * @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/ */
async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) { async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
// 중복 검사 const overlaps = await Schedule.findAll({
for (const time_idx of time_indices) { where: {
const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction); user_id: userId,
if (overlap) { time_idx: {
throw new Error(`Schedule overlaps at time_idx ${time_idx}`); [Op.in]: time_indices
} }
},
transaction
});
if (overlaps.length > 0) {
throw new Error(`Schedule overlaps at time_idx ${overlaps[0].time_idx}`);
} }
const createdSchedules = await Promise.all( const scheduleData = time_indices.map(time_idx => ({
time_indices.map(time_idx =>
Schedule.create({
user_id: userId, user_id: userId,
title, title,
time_idx, time_idx,
is_fixed is_fixed
}, { transaction }) }));
)
); try {
const createdSchedules = await Schedule.bulkCreate(scheduleData, {
transaction,
returning: true,
validate: true
});
return { return {
id: createdSchedules[0].id, id: createdSchedules[0].id,
...@@ -38,6 +47,9 @@ class ScheduleService { ...@@ -38,6 +47,9 @@ class ScheduleService {
createdAt: createdSchedules[0].createdAt, createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt updatedAt: createdSchedules[0].updatedAt
}; };
} catch (error) {
throw new Error(`Failed to bulk create schedules: ${error.message}`);
}
} }
async getAllSchedules(userId) { async getAllSchedules(userId) {
...@@ -55,29 +67,43 @@ class ScheduleService { ...@@ -55,29 +67,43 @@ class ScheduleService {
async updateSchedules(userId, updates, transaction = null) { async updateSchedules(userId, updates, transaction = null) {
const { originalTitle, title, is_fixed, time_indices } = updates; const { originalTitle, title, is_fixed, time_indices } = updates;
const t = transaction || await sequelize.transaction();
try {
// 기존 스케줄 조회 // 기존 스케줄 조회
const existingSchedules = await Schedule.findAll({ const [existingSchedule, existingSchedules] = await Promise.all([
Schedule.findOne({
where: { where: {
user_id: userId, user_id: userId,
title: originalTitle title: originalTitle
}, },
transaction transaction: t
}); }),
Schedule.findAll({
attributes: ['time_idx'],
where: {
user_id: userId,
title: originalTitle
},
transaction: t
})
]);
if (existingSchedules.length === 0) { if (!existingSchedule) {
throw new Error('Schedule not found'); throw new Error('Schedule not found');
} }
const existingTimeIndices = existingSchedules.map(s => s.time_idx); // 기존 시간대 const existingTimeIndices = existingSchedules.map(s => s.time_idx);
const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); // 삭제할 시간대 const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx));
const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); // 추가할 시간대 const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx));
const t = transaction || await sequelize.transaction();
try { // 벌크 연산
// 삭제 const operations = [];
// 삭제 연산
if (toDelete.length > 0) { if (toDelete.length > 0) {
await Schedule.destroy({ operations.push(
Schedule.destroy({
where: { where: {
user_id: userId, user_id: userId,
title: originalTitle, title: originalTitle,
...@@ -86,15 +112,14 @@ class ScheduleService { ...@@ -86,15 +112,14 @@ class ScheduleService {
} }
}, },
transaction: t transaction: t
}); })
);
} }
// 제목, 고정/유동 업데이트 // 업데이트 연산
await Schedule.update( operations.push(
{ Schedule.update(
title, { title, is_fixed },
is_fixed
},
{ {
where: { where: {
user_id: userId, user_id: userId,
...@@ -102,33 +127,40 @@ class ScheduleService { ...@@ -102,33 +127,40 @@ class ScheduleService {
}, },
transaction: t transaction: t
} }
)
); );
// 새로운 time_indices 추가 // 생성 연산
if (toAdd.length > 0) { if (toAdd.length > 0) {
await Promise.all( operations.push(
toAdd.map(time_idx => Schedule.bulkCreate(
Schedule.create({ toAdd.map(time_idx => ({
user_id: userId, user_id: userId,
title, title,
time_idx, time_idx,
is_fixed is_fixed
}, { transaction: t }) })),
{
transaction: t,
validate: true
}
) )
); );
} }
await Promise.all(operations); // 병렬 처리
if (!transaction) { if (!transaction) {
await t.commit(); await t.commit();
} }
return { return {
id: existingSchedules[0].id, id: existingSchedule.id,
user_id: userId, user_id: userId,
title, title,
is_fixed, is_fixed,
time_indices, time_indices,
createdAt: existingSchedules[0].createdAt, createdAt: existingSchedule.createdAt,
updatedAt: new Date() updatedAt: new Date()
}; };
...@@ -157,25 +189,19 @@ class ScheduleService { ...@@ -157,25 +189,19 @@ class ScheduleService {
*/ */
async getScheduleByTimeIdx(userId, time_idx) { async getScheduleByTimeIdx(userId, time_idx) {
// 해당 time_idx의 스케줄 찾기 // 해당 time_idx의 스케줄 찾기
const schedule = await Schedule.findOne({ const schedules = await Schedule.findAll({
where: { user_id: userId, time_idx }
});
if (!schedule) {
throw new Error('Schedule not found');
}
// 같은 제목의 모든 스케줄 찾기
const relatedSchedules = await Schedule.findAll({
where: { where: {
user_id: userId, user_id: userId,
title: schedule.title, title: {
is_fixed: schedule.is_fixed [Op.in]: sequelize.literal(
`(SELECT title FROM Schedules WHERE user_id = ${userId} AND time_idx = ${time_idx})`
)
}
}, },
order: [['time_idx', 'ASC']] order: [['time_idx', 'ASC']]
}); });
return ScheduleResponseDTO.groupSchedules(relatedSchedules)[0]; return ScheduleResponseDTO.groupSchedules(schedules)[0];
} }
async getAllSchedules(userId) { async getAllSchedules(userId) {
......
// utils/performanceMonitor.js
const { performance, PerformanceObserver } = require('perf_hooks');
class PerformanceMonitor {
constructor() {
this.measurements = new Map();
// 성능 관찰자 설정
this.observer = new PerformanceObserver((items) => {
items.getEntries().forEach((entry) => {
const measurements = this.measurements.get(entry.name) || [];
measurements.push(entry.duration);
this.measurements.set(entry.name, measurements);
console.log(`Performance Measurement - ${entry.name}: ${entry.duration}ms`);
});
});
this.observer.observe({ entryTypes: ['measure'] });
}
async measureAsync(name, fn) {
const start = performance.now();
try {
return await fn();
} finally {
const duration = performance.now() - start;
performance.measure(name, {
start,
duration,
detail: { timestamp: new Date().toISOString() }
});
}
}
getStats(name) {
const measurements = this.measurements.get(name) || [];
if (measurements.length === 0) return null;
const sum = measurements.reduce((a, b) => a + b, 0);
const avg = sum / measurements.length;
const min = Math.min(...measurements);
const max = Math.max(...measurements);
return {
count: measurements.length,
average: avg,
min: min,
max: max,
total: sum
};
}
getAllStats() {
const stats = {};
for (const [name, measurements] of this.measurements.entries()) {
stats[name] = this.getStats(name);
}
return stats;
}
}
module.exports = new PerformanceMonitor();
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment