refactor(db,frontend): Improve queries and modularize frontend
- Replaced SELECT* queries in 8 models with explicit columns. - Began modularizing work-report-calendar.js by creating CalendarAPI.js, CalendarState.js, and CalendarView.js. - Refactored manage-project.js to use global API helpers. - Fixed API container crash by adding missing volume mounts to docker-compose.yml. - Added new migration for missing columns in the projects table. - Documented current DB schema and deployment notes.
This commit is contained in:
@@ -433,6 +433,8 @@ const getDashboardData = asyncHandler(async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const workAnalysisService = require('../services/workAnalysisService');
|
||||
|
||||
/**
|
||||
* 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
|
||||
*/
|
||||
@@ -443,156 +445,19 @@ const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
|
||||
logger.info('프로젝트별-작업별 시간 분석 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 데이터 존재 여부 확인
|
||||
const testQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date,
|
||||
SUM(work_hours) as total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const testResults = await db.query(testQuery, [start, end]);
|
||||
logger.debug('데이터 확인', {
|
||||
start,
|
||||
end,
|
||||
count: testResults[0][0]?.total_count
|
||||
});
|
||||
|
||||
// 먼저 간단한 테스트 쿼리로 데이터 확인
|
||||
const simpleQuery = `
|
||||
SELECT COUNT(*) as count, MIN(report_date) as min_date, MAX(report_date) as max_date
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const simpleResult = await db.query(simpleQuery, [start, end]);
|
||||
logger.debug('기간 내 데이터 확인', {
|
||||
start,
|
||||
end,
|
||||
result: simpleResult[0][0]
|
||||
});
|
||||
|
||||
// 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인)
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(p.project_id, dwr.project_id) as project_id,
|
||||
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
|
||||
COALESCE(p.job_no, 'N/A') as job_no,
|
||||
dwr.work_type_id,
|
||||
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
|
||||
|
||||
-- 총 시간
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
|
||||
-- 정규 시간 (work_status_id = 1)
|
||||
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
|
||||
|
||||
-- 에러 시간 (work_status_id = 2)
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
|
||||
|
||||
-- 작업 건수
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
|
||||
|
||||
-- 에러율 계산
|
||||
ROUND(
|
||||
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
|
||||
SUM(dwr.work_hours)) * 100, 2
|
||||
) as error_rate_percent
|
||||
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
|
||||
ORDER BY p.project_name, wt.name
|
||||
`;
|
||||
|
||||
const results = await db.query(query, [start, end]);
|
||||
logger.debug('쿼리 결과', {
|
||||
start,
|
||||
end,
|
||||
resultCount: results[0].length
|
||||
});
|
||||
|
||||
// 데이터를 프로젝트별로 그룹화
|
||||
const groupedData = {};
|
||||
|
||||
results[0].forEach(row => {
|
||||
const projectKey = `${row.project_id}_${row.project_name}`;
|
||||
|
||||
if (!groupedData[projectKey]) {
|
||||
groupedData[projectKey] = {
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name,
|
||||
job_no: row.job_no,
|
||||
total_project_hours: 0,
|
||||
total_regular_hours: 0,
|
||||
total_error_hours: 0,
|
||||
work_types: []
|
||||
};
|
||||
}
|
||||
|
||||
// 프로젝트 총계 누적
|
||||
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
|
||||
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
|
||||
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
|
||||
|
||||
// 작업 유형별 데이터 추가
|
||||
groupedData[projectKey].work_types.push({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name,
|
||||
total_hours: parseFloat(row.total_hours),
|
||||
regular_hours: parseFloat(row.regular_hours),
|
||||
error_hours: parseFloat(row.error_hours),
|
||||
total_reports: row.total_reports,
|
||||
regular_reports: row.regular_reports,
|
||||
error_reports: row.error_reports,
|
||||
error_rate_percent: parseFloat(row.error_rate_percent) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// 프로젝트별 에러율 계산
|
||||
Object.values(groupedData).forEach(project => {
|
||||
project.project_error_rate = project.total_project_hours > 0
|
||||
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
// 전체 요약 통계
|
||||
const totalStats = {
|
||||
total_projects: Object.keys(groupedData).length,
|
||||
total_work_types: new Set(results[0].map(r => r.work_type_id)).size,
|
||||
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
|
||||
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
|
||||
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
|
||||
};
|
||||
|
||||
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
|
||||
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
const result = await workAnalysisService.getProjectWorkTypeAnalysis(start, end);
|
||||
|
||||
logger.info('프로젝트별-작업별 시간 분석 성공', {
|
||||
start,
|
||||
end,
|
||||
projectCount: totalStats.total_projects,
|
||||
workTypeCount: totalStats.total_work_types,
|
||||
totalHours: totalStats.grand_total_hours
|
||||
projectCount: result.summary.total_projects,
|
||||
workTypeCount: result.summary.total_work_types,
|
||||
totalHours: result.summary.grand_total_hours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: totalStats,
|
||||
projects: Object.values(groupedData),
|
||||
period: { start, end }
|
||||
},
|
||||
data: result,
|
||||
message: '프로젝트별-작업별 시간 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -601,6 +466,10 @@ const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
|
||||
end,
|
||||
error: error.message
|
||||
});
|
||||
// Service throws DatabaseError wrapper or Error
|
||||
if (error.name === 'DatabaseError') {
|
||||
throw error;
|
||||
}
|
||||
throw new DatabaseError('프로젝트별-작업별 시간 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -427,6 +427,51 @@ class WorkAnalysis {
|
||||
throw new Error(`대시보드 데이터 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// 프로젝트별-작업별 시간 분석용 데이터 조회
|
||||
async getProjectWorkTypeRawData(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(p.project_id, dwr.project_id) as project_id,
|
||||
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
|
||||
COALESCE(p.job_no, 'N/A') as job_no,
|
||||
dwr.work_type_id,
|
||||
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
|
||||
|
||||
-- 총 시간
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
|
||||
-- 정규 시간 (work_status_id = 1)
|
||||
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
|
||||
|
||||
-- 에러 시간 (work_status_id = 2)
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
|
||||
|
||||
-- 작업 건수
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
|
||||
|
||||
-- 에러율 계산
|
||||
ROUND(
|
||||
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
|
||||
SUM(dwr.work_hours)) * 100, 2
|
||||
) as error_rate_percent
|
||||
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
|
||||
ORDER BY p.project_name, wt.name
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw new Error(`프로젝트-작업유형별 원본 데이터 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkAnalysis;
|
||||
109
api.hyungi.net/services/workAnalysisService.js
Normal file
109
api.hyungi.net/services/workAnalysisService.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Work Analysis Service
|
||||
*
|
||||
* Handles complex business logic and data aggregation for detailed work analysis reports
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const WorkAnalysis = require('../models/WorkAnalysis');
|
||||
const logger = require('../utils/logger');
|
||||
const { DatabaseError } = require('../utils/errors');
|
||||
|
||||
class WorkAnalysisService {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.db) {
|
||||
this.db = await getDb();
|
||||
this.model = new WorkAnalysis(this.db);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트별-작업별 시간 분석 및 집계 (Service Layer logic)
|
||||
*/
|
||||
async getProjectWorkTypeAnalysis(startDate, endDate) {
|
||||
await this.init();
|
||||
|
||||
try {
|
||||
// 1. Get Raw Data from Model
|
||||
const rawData = await this.model.getProjectWorkTypeRawData(startDate, endDate);
|
||||
|
||||
// 2. Process and Group Data
|
||||
const groupedData = {};
|
||||
|
||||
rawData.forEach(row => {
|
||||
const projectKey = `${row.project_id}_${row.project_name}`;
|
||||
|
||||
if (!groupedData[projectKey]) {
|
||||
groupedData[projectKey] = {
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name,
|
||||
job_no: row.job_no,
|
||||
total_project_hours: 0,
|
||||
total_regular_hours: 0,
|
||||
total_error_hours: 0,
|
||||
work_types: []
|
||||
};
|
||||
}
|
||||
|
||||
// Project Totals Accumulation
|
||||
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
|
||||
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
|
||||
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
|
||||
|
||||
// Add WorkType Entry
|
||||
groupedData[projectKey].work_types.push({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name,
|
||||
total_hours: parseFloat(row.total_hours),
|
||||
regular_hours: parseFloat(row.regular_hours),
|
||||
error_hours: parseFloat(row.error_hours),
|
||||
total_reports: row.total_reports,
|
||||
regular_reports: row.regular_reports,
|
||||
error_reports: row.error_reports,
|
||||
error_rate_percent: parseFloat(row.error_rate_percent) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Calculate Project Error Rates
|
||||
Object.values(groupedData).forEach(project => {
|
||||
project.project_error_rate = project.total_project_hours > 0
|
||||
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
// 4. Calculate Grand Totals
|
||||
const totalStats = {
|
||||
total_projects: Object.keys(groupedData).length,
|
||||
total_work_types: new Set(rawData.map(r => r.work_type_id)).size,
|
||||
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
|
||||
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
|
||||
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
|
||||
};
|
||||
|
||||
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
|
||||
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
summary: totalStats,
|
||||
projects: Object.values(groupedData),
|
||||
period: { start: startDate, end: endDate }
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Service: 프로젝트별-작업별 시간 분석 처리 실패', { error: error.message });
|
||||
// Re-throw as DatabaseError to maintain consistency with controller expectation, or custom ServiceError
|
||||
throw new DatabaseError(`분석 데이터 처리 중 오류 발생: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WorkAnalysisService();
|
||||
Reference in New Issue
Block a user