Fix: Worker/Project status update and filtering issues
- Added cache invalidation for Workers and Projects - Implemented server-side status filtering for Workers - Fixed worker update query bug (removed non-existent join_date column) - Updated daily-work-report UI to fetch only active workers
This commit is contained in:
@@ -11,6 +11,7 @@ const projectModel = require('../models/projectModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
const cache = require('../utils/cache');
|
||||
|
||||
/**
|
||||
* 프로젝트 생성
|
||||
@@ -27,6 +28,9 @@ exports.createProject = asyncHandler(async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 생성 성공', { project_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
@@ -123,6 +127,9 @@ exports.updateProject = asyncHandler(async (req, res) => {
|
||||
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 수정 성공', { project_id: id });
|
||||
|
||||
res.json({
|
||||
@@ -153,6 +160,9 @@ exports.removeProject = asyncHandler(async (req, res) => {
|
||||
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 삭제 성공', { project_id: id });
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -45,9 +45,9 @@ exports.createWorker = asyncHandler(async (req, res) => {
|
||||
* 전체 작업자 조회 (캐싱 및 페이지네이션 적용)
|
||||
*/
|
||||
exports.getAllWorkers = asyncHandler(async (req, res) => {
|
||||
const { page = 1, limit = 10, search = '' } = req.query;
|
||||
const { page = 1, limit = 10, search = '', status = '' } = req.query;
|
||||
|
||||
const cacheKey = cache.createKey('workers', 'list', page, limit, search);
|
||||
const cacheKey = cache.createKey('workers', 'list', page, limit, search, status);
|
||||
|
||||
// 캐시에서 조회
|
||||
const cachedData = await cache.get(cacheKey);
|
||||
@@ -62,7 +62,7 @@ exports.getAllWorkers = asyncHandler(async (req, res) => {
|
||||
}
|
||||
|
||||
// 최적화된 쿼리 사용
|
||||
const result = await optimizedQueries.getWorkersPaged(page, limit, search);
|
||||
const result = await optimizedQueries.getWorkersPaged(page, limit, search, status);
|
||||
|
||||
// 캐시에 저장 (5분)
|
||||
await cache.set(cacheKey, result, cache.TTL.MEDIUM);
|
||||
@@ -127,6 +127,10 @@ exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
throw new NotFoundError('작업자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
logger.info('작업자 수정 후 캐시 무효화', { worker_id: id });
|
||||
await cache.invalidateCache.worker();
|
||||
|
||||
logger.info('작업자 수정 성공', { worker_id: id });
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -79,7 +79,6 @@ const update = async (worker, callback) => {
|
||||
worker_id,
|
||||
worker_name,
|
||||
job_type,
|
||||
join_date,
|
||||
status,
|
||||
phone_number,
|
||||
email,
|
||||
@@ -92,7 +91,6 @@ const update = async (worker, callback) => {
|
||||
`UPDATE workers
|
||||
SET worker_name = ?,
|
||||
job_type = ?,
|
||||
join_date = ?,
|
||||
status = ?,
|
||||
phone_number = ?,
|
||||
email = ?,
|
||||
@@ -103,7 +101,6 @@ const update = async (worker, callback) => {
|
||||
[
|
||||
worker_name,
|
||||
job_type,
|
||||
formatDate(join_date),
|
||||
status,
|
||||
phone_number,
|
||||
email,
|
||||
@@ -116,7 +113,7 @@ const update = async (worker, callback) => {
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,12 +121,12 @@ const update = async (worker, callback) => {
|
||||
const remove = async (worker_id, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
|
||||
console.log(`🗑️ 작업자 삭제 시작: worker_id=${worker_id}`);
|
||||
|
||||
|
||||
// 안전한 삭제: 각 테이블을 개별적으로 처리하고 오류가 발생해도 계속 진행
|
||||
const tables = [
|
||||
{ name: 'users', query: 'UPDATE users SET worker_id = NULL WHERE worker_id = ?', action: '업데이트' },
|
||||
@@ -142,7 +139,7 @@ const remove = async (worker_id, callback) => {
|
||||
{ name: 'monthly_worker_status', query: 'DELETE FROM monthly_worker_status WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'worker_groups', query: 'DELETE FROM worker_groups WHERE worker_id = ?', action: '삭제' }
|
||||
];
|
||||
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const [result] = await conn.query(table.query, [worker_id]);
|
||||
@@ -153,17 +150,17 @@ const remove = async (worker_id, callback) => {
|
||||
console.log(`⚠️ ${table.name} 테이블 ${table.action} 실패 (무시): ${tableError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 마지막으로 작업자 삭제
|
||||
const [result] = await conn.query(
|
||||
`DELETE FROM workers WHERE worker_id = ?`,
|
||||
[worker_id]
|
||||
);
|
||||
console.log(`✅ 작업자 삭제 완료: ${result.affectedRows}건`);
|
||||
|
||||
|
||||
await conn.commit();
|
||||
callback(null, result.affectedRows);
|
||||
|
||||
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error(`❌ 작업자 삭제 오류 (worker_id: ${worker_id}):`, err);
|
||||
|
||||
@@ -9,7 +9,7 @@ const paginate = (page = 1, limit = 10) => {
|
||||
const pageNum = Math.max(1, parseInt(page));
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(limit))); // 최대 100개 제한
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
|
||||
return {
|
||||
limit: limitNum,
|
||||
offset,
|
||||
@@ -23,21 +23,21 @@ const paginate = (page = 1, limit = 10) => {
|
||||
const executePagedQuery = async (baseQuery, countQuery, params = [], options = {}) => {
|
||||
const { page = 1, limit = 10, orderBy = 'id', orderDirection = 'DESC' } = options;
|
||||
const { limit: limitNum, offset, page: pageNum } = paginate(page, limit);
|
||||
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
|
||||
// 전체 개수 조회
|
||||
const [countResult] = await db.execute(countQuery, params);
|
||||
const totalCount = countResult[0]?.total || 0;
|
||||
|
||||
|
||||
// 데이터 조회 (ORDER BY와 LIMIT 추가)
|
||||
const pagedQuery = `${baseQuery} ORDER BY ${orderBy} ${orderDirection} LIMIT ${limitNum} OFFSET ${offset}`;
|
||||
const [rows] = await db.execute(pagedQuery, params);
|
||||
|
||||
|
||||
// 페이지네이션 메타데이터 계산
|
||||
const totalPages = Math.ceil(totalCount / limitNum);
|
||||
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
@@ -49,7 +49,7 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = {
|
||||
hasPrevPage: pageNum > 1
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`페이지네이션 쿼리 실행 오류: ${error.message}`);
|
||||
}
|
||||
@@ -61,20 +61,20 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = {
|
||||
const suggestIndexes = async (tableName) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
|
||||
// 현재 인덱스 조회
|
||||
const [indexes] = await db.execute(`SHOW INDEX FROM ${tableName}`);
|
||||
|
||||
|
||||
// 테이블 구조 조회
|
||||
const [columns] = await db.execute(`DESCRIBE ${tableName}`);
|
||||
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
|
||||
// 외래키 컬럼에 인덱스 제안
|
||||
const foreignKeyColumns = columns.filter(col =>
|
||||
const foreignKeyColumns = columns.filter(col =>
|
||||
col.Field.endsWith('_id') && !indexes.some(idx => idx.Column_name === col.Field)
|
||||
);
|
||||
|
||||
|
||||
foreignKeyColumns.forEach(col => {
|
||||
suggestions.push({
|
||||
type: 'INDEX',
|
||||
@@ -83,13 +83,13 @@ const suggestIndexes = async (tableName) => {
|
||||
sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 날짜 컬럼에 인덱스 제안
|
||||
const dateColumns = columns.filter(col =>
|
||||
(col.Type.includes('date') || col.Type.includes('timestamp')) &&
|
||||
const dateColumns = columns.filter(col =>
|
||||
(col.Type.includes('date') || col.Type.includes('timestamp')) &&
|
||||
!indexes.some(idx => idx.Column_name === col.Field)
|
||||
);
|
||||
|
||||
|
||||
dateColumns.forEach(col => {
|
||||
suggestions.push({
|
||||
type: 'INDEX',
|
||||
@@ -98,7 +98,7 @@ const suggestIndexes = async (tableName) => {
|
||||
sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
tableName,
|
||||
currentIndexes: indexes.map(idx => ({
|
||||
@@ -108,7 +108,7 @@ const suggestIndexes = async (tableName) => {
|
||||
})),
|
||||
suggestions
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`인덱스 분석 오류: ${error.message}`);
|
||||
}
|
||||
@@ -120,23 +120,23 @@ const suggestIndexes = async (tableName) => {
|
||||
const analyzeQuery = async (query, params = []) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
|
||||
// EXPLAIN 실행
|
||||
const explainQuery = `EXPLAIN ${query}`;
|
||||
const [explainResult] = await db.execute(explainQuery, params);
|
||||
|
||||
|
||||
// 쿼리 실행 시간 측정
|
||||
const startTime = Date.now();
|
||||
await db.execute(query, params);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
|
||||
// 성능 분석
|
||||
const analysis = {
|
||||
executionTime,
|
||||
explainResult,
|
||||
recommendations: []
|
||||
};
|
||||
|
||||
|
||||
// 성능 권장사항 생성
|
||||
explainResult.forEach(row => {
|
||||
if (row.type === 'ALL') {
|
||||
@@ -146,7 +146,7 @@ const analyzeQuery = async (query, params = []) => {
|
||||
suggestion: '적절한 인덱스 추가 권장'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (row.rows > 1000) {
|
||||
analysis.recommendations.push({
|
||||
type: 'WARNING',
|
||||
@@ -154,7 +154,7 @@ const analyzeQuery = async (query, params = []) => {
|
||||
suggestion: 'WHERE 조건 최적화 또는 인덱스 추가 권장'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (row.Extra && row.Extra.includes('Using filesort')) {
|
||||
analysis.recommendations.push({
|
||||
type: 'INFO',
|
||||
@@ -163,9 +163,9 @@ const analyzeQuery = async (query, params = []) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return analysis;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`쿼리 분석 오류: ${error.message}`);
|
||||
}
|
||||
@@ -178,39 +178,39 @@ const batchInsert = async (tableName, data, batchSize = 100) => {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
throw new Error('삽입할 데이터가 없습니다.');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const connection = await db.getConnection();
|
||||
|
||||
|
||||
await connection.beginTransaction();
|
||||
|
||||
|
||||
const columns = Object.keys(data[0]);
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
const insertQuery = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
|
||||
|
||||
let insertedCount = 0;
|
||||
|
||||
|
||||
// 배치 단위로 처리
|
||||
for (let i = 0; i < data.length; i += batchSize) {
|
||||
const batch = data.slice(i, i + batchSize);
|
||||
|
||||
|
||||
for (const row of batch) {
|
||||
const values = columns.map(col => row[col]);
|
||||
await connection.execute(insertQuery, values);
|
||||
insertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
|
||||
|
||||
return {
|
||||
insertedCount,
|
||||
batchSize,
|
||||
totalBatches: Math.ceil(data.length / batchSize)
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`배치 삽입 오류: ${error.message}`);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ const generateCacheKey = (query, params = [], prefix = 'query') => {
|
||||
.createHash('md5')
|
||||
.update(query + paramString)
|
||||
.digest('hex');
|
||||
|
||||
|
||||
return `${prefix}:${queryHash}`;
|
||||
};
|
||||
|
||||
@@ -234,30 +234,43 @@ const generateCacheKey = (query, params = [], prefix = 'query') => {
|
||||
*/
|
||||
const optimizedQueries = {
|
||||
// 작업자 목록 (페이지네이션)
|
||||
getWorkersPaged: async (page = 1, limit = 10, search = '') => {
|
||||
getWorkersPaged: async (page = 1, limit = 10, search = '', status = '') => {
|
||||
let baseQuery = `
|
||||
SELECT w.*, COUNT(dwr.id) as report_count
|
||||
FROM workers w
|
||||
LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id
|
||||
`;
|
||||
|
||||
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM workers w';
|
||||
let params = [];
|
||||
|
||||
let conditions = [];
|
||||
|
||||
// 검색 조건
|
||||
if (search) {
|
||||
const searchCondition = ' WHERE w.worker_name LIKE ? OR w.position LIKE ?';
|
||||
baseQuery += searchCondition + ' GROUP BY w.worker_id';
|
||||
countQuery += searchCondition;
|
||||
params = [`%${search}%`, `%${search}%`];
|
||||
} else {
|
||||
baseQuery += ' GROUP BY w.worker_id';
|
||||
conditions.push('(w.worker_name LIKE ? OR w.position LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
|
||||
// 상태 조건
|
||||
if (status) {
|
||||
conditions.push('w.status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
// 조건 조합
|
||||
if (conditions.length > 0) {
|
||||
const whereClause = ' WHERE ' + conditions.join(' AND ');
|
||||
baseQuery += whereClause;
|
||||
countQuery += whereClause;
|
||||
}
|
||||
|
||||
baseQuery += ' GROUP BY w.worker_id';
|
||||
|
||||
return executePagedQuery(baseQuery, countQuery, params, {
|
||||
page, limit, orderBy: 'w.worker_id', orderDirection: 'DESC'
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// 프로젝트 목록 (페이지네이션)
|
||||
getProjectsPaged: async (page = 1, limit = 10, status = '') => {
|
||||
let baseQuery = `
|
||||
@@ -266,10 +279,10 @@ const optimizedQueries = {
|
||||
FROM projects p
|
||||
LEFT JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
|
||||
`;
|
||||
|
||||
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM projects p';
|
||||
let params = [];
|
||||
|
||||
|
||||
if (status) {
|
||||
const statusCondition = ' WHERE p.status = ?';
|
||||
baseQuery += statusCondition + ' GROUP BY p.project_id';
|
||||
@@ -278,12 +291,12 @@ const optimizedQueries = {
|
||||
} else {
|
||||
baseQuery += ' GROUP BY p.project_id';
|
||||
}
|
||||
|
||||
|
||||
return executePagedQuery(baseQuery, countQuery, params, {
|
||||
page, limit, orderBy: 'p.project_id', orderDirection: 'DESC'
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// 일일 작업 보고서 (날짜 범위, 페이지네이션)
|
||||
getDailyWorkReportsPaged: async (startDate, endDate, page = 1, limit = 10) => {
|
||||
const baseQuery = `
|
||||
@@ -299,13 +312,13 @@ const optimizedQueries = {
|
||||
LEFT JOIN users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM daily_work_reports dwr
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
|
||||
return executePagedQuery(baseQuery, countQuery, [startDate, endDate], {
|
||||
page, limit, orderBy: 'dwr.report_date', orderDirection: 'DESC'
|
||||
});
|
||||
@@ -318,11 +331,11 @@ const optimizedQueries = {
|
||||
const getPerformanceStats = async () => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
|
||||
// 연결 상태 조회
|
||||
const [connections] = await db.execute('SHOW STATUS LIKE "Threads_connected"');
|
||||
const [maxConnections] = await db.execute('SHOW VARIABLES LIKE "max_connections"');
|
||||
|
||||
|
||||
// 쿼리 캐시 상태 (MySQL 8.0 이전 버전)
|
||||
let queryCacheStats = null;
|
||||
try {
|
||||
@@ -331,10 +344,10 @@ const getPerformanceStats = async () => {
|
||||
} catch (error) {
|
||||
// MySQL 8.0+에서는 쿼리 캐시가 제거됨
|
||||
}
|
||||
|
||||
|
||||
// 슬로우 쿼리 로그 상태
|
||||
const [slowQueries] = await db.execute('SHOW STATUS LIKE "Slow_queries"');
|
||||
|
||||
|
||||
return {
|
||||
connections: {
|
||||
current: parseInt(connections[0]?.Value || 0),
|
||||
@@ -344,7 +357,7 @@ const getPerformanceStats = async () => {
|
||||
slowQueries: parseInt(slowQueries[0]?.Value || 0),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`성능 통계 조회 오류: ${error.message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user