/** * 데일리 워크 레포트 분석 컨트롤러 * * 작업 보고서 종합 분석 API 엔드포인트 핸들러 * * @author TK-FB-Project * @since 2025-12-11 */ const { getDb } = require('../dbPool'); const { ValidationError, DatabaseError } = require('../utils/errors'); const { asyncHandler } = require('../middlewares/errorHandler'); const logger = require('../utils/logger'); /** * 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록) */ const getAnalysisFilters = asyncHandler(async (req, res) => { logger.info('분석 필터 데이터 조회 요청'); const db = await getDb(); try { // 프로젝트 목록 const [projects] = await db.query(` SELECT DISTINCT p.project_id, p.project_name FROM projects p INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id ORDER BY p.project_name `); // 작업자 목록 const [workers] = await db.query(` SELECT DISTINCT w.worker_id, w.worker_name FROM workers w INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id ORDER BY w.worker_name `); // 작업 유형 목록 const [workTypes] = await db.query(` SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name FROM work_types wt INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id ORDER BY wt.name `); // 날짜 범위 const [dateRange] = await db.query(` SELECT MIN(report_date) as min_date, MAX(report_date) as max_date FROM daily_work_reports `); logger.info('분석 필터 데이터 조회 성공', { projects: projects.length, workers: workers.length, workTypes: workTypes.length }); res.json({ success: true, data: { projects, workers, workTypes, dateRange: dateRange[0] }, message: '분석 필터 데이터 조회 성공' }); } catch (error) { logger.error('분석 필터 데이터 조회 실패', { error: error.message }); throw new DatabaseError('필터 데이터 조회 중 오류가 발생했습니다'); } }); /** * 기간별 작업 분석 데이터 조회 */ const getAnalyticsByPeriod = asyncHandler(async (req, res) => { const { start_date, end_date, project_id, worker_id } = req.query; if (!start_date || !end_date) { throw new ValidationError('start_date와 end_date가 필요합니다', { required: ['start_date', 'end_date'], received: { start_date, end_date }, example: 'start_date=2025-08-01&end_date=2025-08-31' }); } logger.info('기간별 분석 데이터 조회 요청', { start_date, end_date, project_id, worker_id }); const db = await getDb(); try { // 기본 조건 let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let queryParams = [start_date, end_date]; if (project_id) { whereConditions.push('dwr.project_id = ?'); queryParams.push(project_id); } if (worker_id) { whereConditions.push('dwr.worker_id = ?'); queryParams.push(worker_id); } const whereClause = whereConditions.join(' AND '); // 1. 전체 요약 통계 const overallSql = ` SELECT COUNT(*) as total_entries, SUM(dwr.work_hours) as total_hours, COUNT(DISTINCT dwr.worker_id) as unique_workers, COUNT(DISTINCT dwr.project_id) as unique_projects, COUNT(DISTINCT dwr.report_date) as working_days, AVG(dwr.work_hours) as avg_hours_per_entry, COUNT(DISTINCT dwr.created_by) as contributors, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_entries, ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate FROM daily_work_reports dwr WHERE ${whereClause} `; const [overallStats] = await db.query(overallSql, queryParams); // 2. 일별 통계 const dailyStatsSql = ` SELECT dwr.report_date, SUM(dwr.work_hours) as daily_hours, COUNT(*) as daily_entries, COUNT(DISTINCT dwr.worker_id) as daily_workers FROM daily_work_reports dwr WHERE ${whereClause} GROUP BY dwr.report_date ORDER BY dwr.report_date ASC `; const [dailyStats] = await db.query(dailyStatsSql, queryParams); // 3. 일별 에러 통계 const dailyErrorStatsSql = ` SELECT dwr.report_date, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors, COUNT(*) as daily_total, ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate FROM daily_work_reports dwr WHERE ${whereClause} GROUP BY dwr.report_date ORDER BY dwr.report_date ASC `; const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams); // 4. 에러 유형별 분석 const errorAnalysisSql = ` SELECT et.id as error_type_id, et.name as error_type_name, COUNT(*) as error_count, SUM(dwr.work_hours) as error_hours, ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage FROM daily_work_reports dwr LEFT JOIN error_types et ON dwr.error_type_id = et.id WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL GROUP BY et.id, et.name ORDER BY error_count DESC `; const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams); // 5. 작업 유형별 분석 const workTypeAnalysisSql = ` SELECT wt.id as work_type_id, wt.name as work_type_name, COUNT(*) as work_count, SUM(dwr.work_hours) as total_hours, AVG(dwr.work_hours) as avg_hours, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count, ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate FROM daily_work_reports dwr LEFT JOIN work_types wt ON dwr.work_type_id = wt.id WHERE ${whereClause} GROUP BY wt.id, wt.name ORDER BY total_hours DESC `; const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams); // 6. 작업자별 성과 분석 const workerAnalysisSql = ` SELECT w.worker_id, w.worker_name, COUNT(*) as total_entries, SUM(dwr.work_hours) as total_hours, AVG(dwr.work_hours) as avg_hours_per_entry, COUNT(DISTINCT dwr.project_id) as projects_worked, COUNT(DISTINCT dwr.report_date) as working_days, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count, ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE ${whereClause} GROUP BY w.worker_id, w.worker_name ORDER BY total_hours DESC `; const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams); // 7. 프로젝트별 분석 const projectAnalysisSql = ` SELECT p.project_id, p.project_name, COUNT(*) as total_entries, SUM(dwr.work_hours) as total_hours, COUNT(DISTINCT dwr.worker_id) as workers_count, COUNT(DISTINCT dwr.report_date) as working_days, AVG(dwr.work_hours) as avg_hours_per_entry, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count, ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate FROM daily_work_reports dwr LEFT JOIN projects p ON dwr.project_id = p.project_id WHERE ${whereClause} GROUP BY p.project_id, p.project_name ORDER BY total_hours DESC `; const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams); logger.info('기간별 분석 데이터 조회 성공', { start_date, end_date, total_entries: overallStats[0].total_entries, total_hours: overallStats[0].total_hours }); res.json({ success: true, data: { summary: overallStats[0], dailyStats, dailyErrorStats, errorAnalysis, workTypeAnalysis, workerAnalysis, projectAnalysis, period: { start_date, end_date }, filters: { project_id, worker_id } }, message: '기간별 분석 데이터 조회 성공' }); } catch (error) { logger.error('기간별 분석 데이터 조회 실패', { start_date, end_date, error: error.message }); throw new DatabaseError('기간별 분석 데이터 조회 중 오류가 발생했습니다'); } }); /** * 프로젝트별 상세 분석 */ const getProjectAnalysis = asyncHandler(async (req, res) => { const { start_date, end_date, project_id } = req.query; if (!start_date || !end_date) { throw new ValidationError('start_date와 end_date가 필요합니다', { required: ['start_date', 'end_date'], received: { start_date, end_date } }); } logger.info('프로젝트별 분석 조회 요청', { start_date, end_date, project_id }); const db = await getDb(); try { let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let queryParams = [start_date, end_date]; if (project_id) { whereConditions.push('dwr.project_id = ?'); queryParams.push(project_id); } const whereClause = whereConditions.join(' AND '); const projectStatsSql = ` SELECT dwr.project_id, p.project_name, SUM(dwr.work_hours) as total_hours, COUNT(*) as total_entries, COUNT(DISTINCT dwr.worker_id) as workers_count, COUNT(DISTINCT dwr.report_date) as working_days, AVG(dwr.work_hours) as avg_hours_per_entry FROM daily_work_reports dwr LEFT JOIN projects p ON dwr.project_id = p.project_id WHERE ${whereClause} GROUP BY dwr.project_id ORDER BY total_hours DESC `; const [projectStats] = await db.query(projectStatsSql, queryParams); logger.info('프로젝트별 분석 조회 성공', { start_date, end_date, projectCount: projectStats.length }); res.json({ success: true, data: { projectStats, period: { start_date, end_date } }, message: '프로젝트별 분석 조회 성공' }); } catch (error) { logger.error('프로젝트별 분석 조회 실패', { start_date, end_date, error: error.message }); throw new DatabaseError('프로젝트별 분석 데이터 조회 중 오류가 발생했습니다'); } }); /** * 작업자별 상세 분석 */ const getWorkerAnalysis = asyncHandler(async (req, res) => { const { start_date, end_date, worker_id } = req.query; if (!start_date || !end_date) { throw new ValidationError('start_date와 end_date가 필요합니다', { required: ['start_date', 'end_date'], received: { start_date, end_date } }); } logger.info('작업자별 분석 조회 요청', { start_date, end_date, worker_id }); const db = await getDb(); try { let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let queryParams = [start_date, end_date]; if (worker_id) { whereConditions.push('dwr.worker_id = ?'); queryParams.push(worker_id); } const whereClause = whereConditions.join(' AND '); const workerStatsSql = ` SELECT dwr.worker_id, w.worker_name, SUM(dwr.work_hours) as total_hours, COUNT(*) as total_entries, COUNT(DISTINCT dwr.project_id) as projects_worked, COUNT(DISTINCT dwr.report_date) as working_days, AVG(dwr.work_hours) as avg_hours_per_entry FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE ${whereClause} GROUP BY dwr.worker_id ORDER BY total_hours DESC `; const [workerStats] = await db.query(workerStatsSql, queryParams); logger.info('작업자별 분석 조회 성공', { start_date, end_date, workerCount: workerStats.length }); res.json({ success: true, data: { workerStats, period: { start_date, end_date } }, message: '작업자별 분석 조회 성공' }); } catch (error) { logger.error('작업자별 분석 조회 실패', { start_date, end_date, error: error.message }); throw new DatabaseError('작업자별 분석 데이터 조회 중 오류가 발생했습니다'); } }); module.exports = { getAnalysisFilters, getAnalyticsByPeriod, getProjectAnalysis, getWorkerAnalysis };