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:
Hyungi Ahn
2026-01-06 15:50:40 +09:00
parent 3549710325
commit 48fff7df64
6 changed files with 226 additions and 187 deletions

View File

@@ -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}`);
}