/** * Work Analysis Data Processor Module * 작업 분석 데이터 가공 및 변환을 담당하는 모듈 */ class WorkAnalysisDataProcessor { // ========== 유틸리티 함수 ========== /** * 주말 여부 확인 * @param {string} dateString - 날짜 문자열 * @returns {boolean} 주말 여부 */ isWeekendDate(dateString) { const date = new Date(dateString); const dayOfWeek = date.getDay(); return dayOfWeek === 0 || dayOfWeek === 6; // 일요일(0) 또는 토요일(6) } /** * 연차/휴무 프로젝트 여부 확인 * @param {string} projectName - 프로젝트명 * @returns {boolean} 연차/휴무 여부 */ isVacationProject(projectName) { if (!projectName) return false; const vacationKeywords = ['연차', '휴무', '휴가', '병가', '특별휴가']; return vacationKeywords.some(keyword => projectName.includes(keyword)); } /** * 날짜 포맷팅 (간단한 형식) * @param {string} dateString - 날짜 문자열 * @returns {string} 포맷된 날짜 */ formatSimpleDate(dateString) { if (!dateString) return ''; return dateString.split('T')[0]; // 시간 부분 제거 } // ========== 프로젝트 분포 데이터 처리 ========== /** * 프로젝트별 데이터 집계 * @param {Array} recentWorkData - 최근 작업 데이터 * @returns {Object} 집계된 프로젝트 데이터 */ aggregateProjectData(recentWorkData) { console.log('📊 프로젝트 데이터 집계 시작'); if (!recentWorkData || recentWorkData.length === 0) { return { projects: [], totalHours: 0 }; } const projectMap = new Map(); let vacationData = null; recentWorkData.forEach(work => { const isWeekend = this.isWeekendDate(work.report_date); const isVacation = this.isVacationProject(work.project_name); // 주말 연차는 제외 if (isWeekend && isVacation) { console.log('🏖️ 주말 연차/휴무 제외:', work.report_date, work.project_name); return; } if (isVacation) { // 연차/휴무 통합 처리 if (!vacationData) { vacationData = { project_id: 'vacation', project_name: '연차/휴무', job_no: null, totalHours: 0, workTypes: new Map() }; } this._addWorkToProject(vacationData, work, '연차/휴무'); } else { // 일반 프로젝트 처리 const projectKey = work.project_id || 'unknown'; if (!projectMap.has(projectKey)) { projectMap.set(projectKey, { project_id: projectKey, project_name: work.project_name || `프로젝트 ${projectKey}`, job_no: work.job_no, totalHours: 0, workTypes: new Map() }); } const project = projectMap.get(projectKey); this._addWorkToProject(project, work); } }); // 결과 배열 생성 const projects = Array.from(projectMap.values()); if (vacationData && vacationData.totalHours > 0) { projects.push(vacationData); } // 작업유형을 배열로 변환하고 정렬 projects.forEach(project => { project.workTypes = Array.from(project.workTypes.values()) .sort((a, b) => b.totalHours - a.totalHours); }); // 프로젝트를 총 시간 순으로 정렬 (연차/휴무는 맨 아래) projects.sort((a, b) => { if (a.project_id === 'vacation') return 1; if (b.project_id === 'vacation') return -1; return b.totalHours - a.totalHours; }); const totalHours = projects.reduce((sum, p) => sum + p.totalHours, 0); console.log('✅ 프로젝트 데이터 집계 완료:', projects.length, '개 프로젝트'); return { projects, totalHours }; } /** * 프로젝트에 작업 데이터 추가 (내부 헬퍼) */ _addWorkToProject(project, work, overrideWorkTypeName = null) { const hours = parseFloat(work.work_hours) || 0; project.totalHours += hours; const workTypeKey = work.work_type_id || 'unknown'; const workTypeName = overrideWorkTypeName || work.work_type_name || `작업유형 ${workTypeKey}`; if (!project.workTypes.has(workTypeKey)) { project.workTypes.set(workTypeKey, { work_type_id: workTypeKey, work_type_name: workTypeName, totalHours: 0 }); } project.workTypes.get(workTypeKey).totalHours += hours; } // ========== 오류 분석 데이터 처리 ========== /** * 작업 형태별 오류 데이터 집계 * @param {Array} recentWorkData - 최근 작업 데이터 * @returns {Array} 집계된 오류 데이터 */ aggregateErrorData(recentWorkData) { console.log('📊 오류 분석 데이터 집계 시작'); if (!recentWorkData || recentWorkData.length === 0) { return []; } const workTypeMap = new Map(); let vacationData = null; recentWorkData.forEach(work => { const isWeekend = this.isWeekendDate(work.report_date); const isVacation = this.isVacationProject(work.project_name); // 주말 연차는 완전히 제외 if (isWeekend && isVacation) { console.log('🏖️ 주말 연차/휴무 제외:', work.report_date, work.project_name); return; } if (isVacation) { // 모든 연차/휴무를 하나로 통합 if (!vacationData) { vacationData = { project_id: 'vacation', project_name: '연차/휴무', job_no: null, work_type_id: 'vacation', work_type_name: '연차/휴무', regularHours: 0, errorHours: 0, errorDetails: new Map(), isVacation: true }; } this._addWorkToErrorData(vacationData, work); } else { // 일반 프로젝트 처리 const workTypeKey = work.work_type_id || 'unknown'; const combinedKey = `${work.project_id || 'unknown'}_${workTypeKey}`; if (!workTypeMap.has(combinedKey)) { workTypeMap.set(combinedKey, { project_id: work.project_id, project_name: work.project_name || `프로젝트 ${work.project_id}`, job_no: work.job_no, work_type_id: workTypeKey, work_type_name: work.work_type_name || `작업유형 ${workTypeKey}`, regularHours: 0, errorHours: 0, errorDetails: new Map(), isVacation: false }); } const workTypeData = workTypeMap.get(combinedKey); this._addWorkToErrorData(workTypeData, work); } }); // 결과 배열 생성 const result = Array.from(workTypeMap.values()); // 연차/휴무 데이터가 있으면 추가 if (vacationData && (vacationData.regularHours > 0 || vacationData.errorHours > 0)) { result.push(vacationData); } // 최종 데이터 처리 const processedResult = result.map(wt => ({ ...wt, totalHours: wt.regularHours + wt.errorHours, errorRate: wt.regularHours + wt.errorHours > 0 ? ((wt.errorHours / (wt.regularHours + wt.errorHours)) * 100).toFixed(1) : '0.0', errorDetails: Array.from(wt.errorDetails.entries()).map(([type, hours]) => ({ type, hours })) })).filter(wt => wt.totalHours > 0) // 시간이 있는 것만 표시 .sort((a, b) => { // 연차/휴무를 맨 아래로 if (a.isVacation && !b.isVacation) return 1; if (!a.isVacation && b.isVacation) return -1; // 같은 프로젝트 내에서는 오류 시간 순으로 정렬 if (a.project_id === b.project_id) { return b.errorHours - a.errorHours; } // 다른 프로젝트는 프로젝트명 순으로 정렬 return (a.project_name || '').localeCompare(b.project_name || ''); }); console.log('✅ 오류 분석 데이터 집계 완료:', processedResult.length, '개 항목'); return processedResult; } /** * 작업 데이터를 오류 분석 데이터에 추가 (내부 헬퍼) */ _addWorkToErrorData(workTypeData, work) { const hours = parseFloat(work.work_hours) || 0; if (work.work_status === 'error' || work.error_type_id) { workTypeData.errorHours += hours; // 오류 유형별 세분화 const errorTypeName = work.error_type_name || work.error_description || '설계미스'; if (!workTypeData.errorDetails.has(errorTypeName)) { workTypeData.errorDetails.set(errorTypeName, 0); } workTypeData.errorDetails.set(errorTypeName, workTypeData.errorDetails.get(errorTypeName) + hours ); } else { workTypeData.regularHours += hours; } } // ========== 차트 데이터 처리 ========== /** * 시계열 차트 데이터 변환 * @param {Array} dailyData - 일별 데이터 * @returns {Object} 차트 데이터 */ processTimeSeriesData(dailyData) { if (!dailyData || dailyData.length === 0) { return { labels: [], datasets: [] }; } const labels = dailyData.map(item => this.formatSimpleDate(item.date)); const hours = dailyData.map(item => item.total_hours || 0); const workers = dailyData.map(item => item.worker_count || 0); return { labels, datasets: [ { label: '총 작업시간', data: hours, borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4 }, { label: '참여 작업자 수', data: workers, borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', tension: 0.4, yAxisID: 'y1' } ] }; } /** * 도넛 차트 데이터 변환 * @param {Array} projectData - 프로젝트 데이터 * @returns {Object} 차트 데이터 */ processDonutChartData(projectData) { if (!projectData || projectData.length === 0) { return { labels: [], datasets: [] }; } const labels = projectData.map(item => item.project_name || item.name); const data = projectData.map(item => item.total_hours || item.hours || 0); const colors = this._generateColors(data.length); return { labels, datasets: [{ data, backgroundColor: colors, borderWidth: 2, borderColor: '#ffffff' }] }; } /** * 색상 생성 헬퍼 */ _generateColors(count) { const baseColors = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1' ]; const colors = []; for (let i = 0; i < count; i++) { colors.push(baseColors[i % baseColors.length]); } return colors; } } // 전역 인스턴스 생성 window.WorkAnalysisDataProcessor = new WorkAnalysisDataProcessor(); // Export는 브라우저 환경에서 제거됨