// models/WorkAnalysis.js - 향상된 버전 class WorkAnalysis { constructor(db) { this.db = db; } // 기본 통계 조회 async getBasicStats(startDate, endDate) { const query = ` SELECT COALESCE(SUM(work_hours), 0) as total_hours, COUNT(*) as total_reports, COUNT(DISTINCT project_id) as active_projects, COUNT(DISTINCT worker_id) as active_workers, SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports, ROUND(AVG(work_hours), 2) as avg_hours_per_report FROM daily_work_reports WHERE report_date BETWEEN ? AND ? `; try { const [results] = await this.db.execute(query, [startDate, endDate]); const stats = results[0]; const errorRate = stats.total_reports > 0 ? (stats.error_reports / stats.total_reports) * 100 : 0; return { totalHours: parseFloat(stats.total_hours) || 0, totalReports: parseInt(stats.total_reports) || 0, activeProjects: parseInt(stats.active_projects) || 0, activeworkers: parseInt(stats.active_workers) || 0, errorRate: parseFloat(errorRate.toFixed(2)) || 0, avgHoursPerReport: parseFloat(stats.avg_hours_per_report) || 0 }; } catch (error) { throw new Error(`기본 통계 조회 실패: ${error.message}`); } } // 일별 작업시간 추이 async getDailyTrend(startDate, endDate) { const query = ` SELECT report_date as date, SUM(work_hours) as hours, COUNT(*) as reports, COUNT(DISTINCT worker_id) as workers, SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors FROM daily_work_reports WHERE report_date BETWEEN ? AND ? GROUP BY report_date ORDER BY report_date `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ date: row.date, hours: parseFloat(row.hours) || 0, reports: parseInt(row.reports) || 0, workers: parseInt(row.workers) || 0, errors: parseInt(row.errors) || 0 })); } catch (error) { throw new Error(`일별 추이 조회 실패: ${error.message}`); } } // 작업자별 통계 async getWorkerStats(startDate, endDate) { const query = ` SELECT dwr.worker_id, w.worker_name, SUM(dwr.work_hours) as totalHours, COUNT(*) as totalReports, ROUND(AVG(dwr.work_hours), 2) as avgHours, COUNT(DISTINCT dwr.project_id) as projectCount, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, COUNT(DISTINCT dwr.report_date) as workingDays FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE dwr.report_date BETWEEN ? AND ? GROUP BY dwr.worker_id, w.worker_name ORDER BY totalHours DESC `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ worker_id: row.worker_id, worker_name: row.worker_name || `작업자 ${row.worker_id}`, totalHours: parseFloat(row.totalHours) || 0, totalReports: parseInt(row.totalReports) || 0, avgHours: parseFloat(row.avgHours) || 0, projectCount: parseInt(row.projectCount) || 0, errorCount: parseInt(row.errorCount) || 0, workingDays: parseInt(row.workingDays) || 0, errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0 })); } catch (error) { throw new Error(`작업자별 통계 조회 실패: ${error.message}`); } } // 프로젝트별 통계 async getProjectStats(startDate, endDate) { const query = ` SELECT dwr.project_id, p.project_name, SUM(dwr.work_hours) as totalHours, COUNT(*) as totalReports, COUNT(DISTINCT dwr.worker_id) as workerCount, ROUND(AVG(dwr.work_hours), 2) as avgHours, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, COUNT(DISTINCT dwr.report_date) as activeDays FROM daily_work_reports dwr LEFT JOIN Projects p ON dwr.project_id = p.project_id WHERE dwr.report_date BETWEEN ? AND ? GROUP BY dwr.project_id, p.project_name ORDER BY totalHours DESC `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ project_id: row.project_id, project_name: row.project_name || `프로젝트 ${row.project_id}`, totalHours: parseFloat(row.totalHours) || 0, totalReports: parseInt(row.totalReports) || 0, workerCount: parseInt(row.workerCount) || 0, avgHours: parseFloat(row.avgHours) || 0, errorCount: parseInt(row.errorCount) || 0, activeDays: parseInt(row.activeDays) || 0, errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0 })); } catch (error) { throw new Error(`프로젝트별 통계 조회 실패: ${error.message}`); } } // 작업유형별 통계 async getWorkTypeStats(startDate, endDate) { const query = ` SELECT dwr.work_type_id, wt.name as work_type_name, SUM(dwr.work_hours) as totalHours, COUNT(*) as totalReports, ROUND(AVG(dwr.work_hours), 2) as avgHours, COUNT(DISTINCT dwr.worker_id) as workerCount, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, COUNT(DISTINCT dwr.project_id) as projectCount FROM daily_work_reports dwr LEFT JOIN work_types wt ON dwr.work_type_id = wt.id WHERE dwr.report_date BETWEEN ? AND ? GROUP BY dwr.work_type_id, wt.name ORDER BY totalHours DESC `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ work_type_id: row.work_type_id, work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`, totalHours: parseFloat(row.totalHours) || 0, totalReports: parseInt(row.totalReports) || 0, avgHours: parseFloat(row.avgHours) || 0, workerCount: parseInt(row.workerCount) || 0, errorCount: parseInt(row.errorCount) || 0, projectCount: parseInt(row.projectCount) || 0, errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0 })); } catch (error) { throw new Error(`작업유형별 통계 조회 실패: ${error.message}`); } } // 최근 작업 현황 async getRecentWork(startDate, endDate, limit = 50) { const query = ` SELECT dwr.id, dwr.report_date, dwr.worker_id, w.worker_name, dwr.project_id, p.project_name, dwr.work_type_id, wt.name as work_type_name, dwr.work_status_id, wst.name as work_status_name, dwr.error_type_id, et.name as error_type_name, dwr.work_hours, dwr.created_by, u.name as created_by_name, dwr.created_at FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN Projects p ON dwr.project_id = p.project_id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN error_types et ON dwr.error_type_id = et.id LEFT JOIN Users u ON dwr.created_by = u.user_id WHERE dwr.report_date BETWEEN ? AND ? ORDER BY dwr.created_at DESC LIMIT ? `; try { const [results] = await this.db.execute(query, [startDate, endDate, parseInt(limit)]); return results.map(row => ({ id: row.id, report_date: row.report_date, worker_id: row.worker_id, worker_name: row.worker_name || `작업자 ${row.worker_id}`, project_id: row.project_id, project_name: row.project_name || `프로젝트 ${row.project_id}`, work_type_id: row.work_type_id, work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`, work_status_id: row.work_status_id, work_status_name: row.work_status_name || '정상', error_type_id: row.error_type_id, error_type_name: row.error_type_name || null, work_hours: parseFloat(row.work_hours) || 0, created_by: row.created_by, created_by_name: row.created_by_name || '미지정', created_at: row.created_at })); } catch (error) { throw new Error(`최근 작업 현황 조회 실패: ${error.message}`); } } // 요일별 패턴 분석 async getWeekdayPattern(startDate, endDate) { const query = ` SELECT DAYOFWEEK(report_date) as day_of_week, CASE DAYOFWEEK(report_date) WHEN 1 THEN '일요일' WHEN 2 THEN '월요일' WHEN 3 THEN '화요일' WHEN 4 THEN '수요일' WHEN 5 THEN '목요일' WHEN 6 THEN '금요일' WHEN 7 THEN '토요일' END as day_name, SUM(work_hours) as total_hours, COUNT(*) as total_reports, ROUND(AVG(work_hours), 2) as avg_hours, COUNT(DISTINCT worker_id) as active_workers FROM daily_work_reports WHERE report_date BETWEEN ? AND ? GROUP BY DAYOFWEEK(report_date) ORDER BY day_of_week `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ dayOfWeek: row.day_of_week, dayName: row.day_name, totalHours: parseFloat(row.total_hours) || 0, totalReports: parseInt(row.total_reports) || 0, avgHours: parseFloat(row.avg_hours) || 0, activeworkers: parseInt(row.active_workers) || 0 })); } catch (error) { throw new Error(`요일별 패턴 분석 실패: ${error.message}`); } } // 에러 분석 async getErrorAnalysis(startDate, endDate) { const query = ` SELECT dwr.error_type_id, et.name as error_type_name, COUNT(*) as error_count, SUM(dwr.work_hours) as total_hours, COUNT(DISTINCT dwr.worker_id) as affected_workers, COUNT(DISTINCT dwr.project_id) as affected_projects FROM daily_work_reports dwr LEFT JOIN error_types et ON dwr.error_type_id = et.id WHERE dwr.report_date BETWEEN ? AND ? AND dwr.work_status_id = 2 GROUP BY dwr.error_type_id, et.name ORDER BY error_count DESC `; try { const [results] = await this.db.execute(query, [startDate, endDate]); return results.map(row => ({ error_type_id: row.error_type_id, error_type_name: row.error_type_name || `에러유형 ${row.error_type_id}`, errorCount: parseInt(row.error_count) || 0, totalHours: parseFloat(row.total_hours) || 0, affectedworkers: parseInt(row.affected_workers) || 0, affectedProjects: parseInt(row.affected_projects) || 0 })); } catch (error) { throw new Error(`에러 분석 실패: ${error.message}`); } } // 월별 비교 분석 async getMonthlyComparison(year) { const query = ` SELECT MONTH(report_date) as month, MONTHNAME(report_date) as month_name, SUM(work_hours) as total_hours, COUNT(*) as total_reports, COUNT(DISTINCT worker_id) as active_workers, COUNT(DISTINCT project_id) as active_projects, SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count FROM daily_work_reports WHERE YEAR(report_date) = ? GROUP BY MONTH(report_date), MONTHNAME(report_date) ORDER BY month `; try { const [results] = await this.db.execute(query, [year]); return results.map(row => ({ month: row.month, monthName: row.month_name, totalHours: parseFloat(row.total_hours) || 0, totalReports: parseInt(row.total_reports) || 0, activeworkers: parseInt(row.active_workers) || 0, activeProjects: parseInt(row.active_projects) || 0, errorCount: parseInt(row.error_count) || 0, errorRate: row.total_reports > 0 ? parseFloat(((row.error_count / row.total_reports) * 100).toFixed(2)) : 0 })); } catch (error) { throw new Error(`월별 비교 분석 실패: ${error.message}`); } } // 작업자별 전문분야 분석 async getWorkerSpecialization(startDate, endDate) { const query = ` SELECT dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name as work_type_name, dwr.project_id, p.project_name, SUM(dwr.work_hours) as totalHours, COUNT(*) as totalReports, ROUND((SUM(dwr.work_hours) / ( SELECT SUM(work_hours) FROM daily_work_reports WHERE worker_id = dwr.worker_id AND report_date BETWEEN ? AND ? )) * 100, 2) as percentage FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN Projects p ON dwr.project_id = p.project_id WHERE dwr.report_date BETWEEN ? AND ? GROUP BY dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name HAVING totalHours > 0 ORDER BY dwr.worker_id, totalHours DESC `; try { const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]); return results.map(row => ({ worker_id: row.worker_id, worker_name: row.worker_name || `작업자 ${row.worker_id}`, work_type_id: row.work_type_id, work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`, project_id: row.project_id, project_name: row.project_name || `프로젝트 ${row.project_id}`, totalHours: parseFloat(row.totalHours) || 0, totalReports: parseInt(row.totalReports) || 0, percentage: parseFloat(row.percentage) || 0 })); } catch (error) { throw new Error(`작업자별 전문분야 분석 실패: ${error.message}`); } } // 대시보드용 종합 데이터 async getDashboardData(startDate, endDate) { try { // 병렬로 모든 데이터 조회 const [ stats, dailyTrend, workerStats, projectStats, workTypeStats, recentWork ] = await Promise.all([ this.getBasicStats(startDate, endDate), this.getDailyTrend(startDate, endDate), this.getWorkerStats(startDate, endDate), this.getProjectStats(startDate, endDate), this.getWorkTypeStats(startDate, endDate), this.getRecentWork(startDate, endDate, 20) ]); return { stats, dailyTrend, workerStats, projectStats, workTypeStats, recentWork, metadata: { period: `${startDate} ~ ${endDate}`, timestamp: new Date().toISOString() } }; } catch (error) { throw new Error(`대시보드 데이터 조회 실패: ${error.message}`); } } } module.exports = WorkAnalysis;