/** * 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();