/** * Work Analysis Chart Renderer Module * 작업 분석 차트 렌더링을 담당하는 모듈 */ class WorkAnalysisChartRenderer { constructor() { this.charts = new Map(); // 차트 인스턴스 관리 this.dataProcessor = window.WorkAnalysisDataProcessor; this.defaultColors = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1' ]; } // ========== 차트 관리 ========== /** * 기존 차트 제거 * @param {string} chartId - 차트 ID */ destroyChart(chartId) { if (this.charts.has(chartId)) { this.charts.get(chartId).destroy(); this.charts.delete(chartId); console.log('🗑️ 차트 제거:', chartId); } } /** * 모든 차트 제거 */ destroyAllCharts() { this.charts.forEach((chart, id) => { chart.destroy(); console.log('🗑️ 차트 제거:', id); }); this.charts.clear(); } /** * 차트 생성 및 등록 * @param {string} chartId - 차트 ID * @param {HTMLCanvasElement} canvas - 캔버스 요소 * @param {Object} config - 차트 설정 * @returns {Chart} 생성된 차트 인스턴스 */ createChart(chartId, canvas, config) { // 기존 차트가 있으면 제거 this.destroyChart(chartId); const chart = new Chart(canvas, config); this.charts.set(chartId, chart); console.log('📊 차트 생성:', chartId); return chart; } // ========== 시계열 차트 ========== /** * 시계열 차트 렌더링 (기간별 작업 현황) * @param {string} startDate - 시작일 * @param {string} endDate - 종료일 * @param {string} projectId - 프로젝트 ID (선택사항) */ async renderTimeSeriesChart(startDate, endDate, projectId = '') { console.log('📈 시계열 차트 렌더링 시작'); try { const api = window.WorkAnalysisAPI; const dailyTrendResponse = await api.getDailyTrend(startDate, endDate, projectId); if (!dailyTrendResponse.success || !dailyTrendResponse.data) { throw new Error('일별 추이 데이터를 가져올 수 없습니다'); } const canvas = document.getElementById('workStatusChart'); if (!canvas) { console.error('❌ workStatusChart 캔버스를 찾을 수 없습니다'); return; } const chartData = this.dataProcessor.processTimeSeriesData(dailyTrendResponse.data); const config = { type: 'line', data: chartData, options: { responsive: true, maintainAspectRatio: false, aspectRatio: 2, scales: { y: { beginAtZero: true, title: { display: true, text: '작업시간 (h)' } }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: '작업자 수 (명)' }, grid: { drawOnChartArea: false, }, } }, plugins: { title: { display: true, text: '일별 작업 현황' }, legend: { display: true, position: 'top' } } } }; this.createChart('workStatus', canvas, config); console.log('✅ 시계열 차트 렌더링 완료'); } catch (error) { console.error('❌ 시계열 차트 렌더링 실패:', error); this._showChartError('workStatusChart', '시계열 차트를 불러올 수 없습니다'); } } // ========== 스택 바 차트 ========== /** * 스택 바 차트 렌더링 (프로젝트별 → 작업유형별) * @param {Array} projectData - 프로젝트 데이터 */ renderStackedBarChart(projectData) { console.log('📊 스택 바 차트 렌더링 시작'); const canvas = document.getElementById('projectDistributionChart'); if (!canvas) { console.error('❌ projectDistributionChart 캔버스를 찾을 수 없습니다'); return; } if (!projectData || !projectData.projects || projectData.projects.length === 0) { this._showChartError('projectDistributionChart', '프로젝트 데이터가 없습니다'); return; } // 데이터 변환 const { labels, datasets } = this._processStackedBarData(projectData.projects); const config = { type: 'bar', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, aspectRatio: 2, scales: { x: { stacked: true, title: { display: true, text: '프로젝트' } }, y: { stacked: true, beginAtZero: true, title: { display: true, text: '작업시간 (h)' } } }, plugins: { title: { display: true, text: '프로젝트별 작업유형 분포' }, legend: { display: true, position: 'top' }, tooltip: { mode: 'index', intersect: false, callbacks: { title: function(context) { return `${context[0].label}`; }, label: function(context) { const workType = context.dataset.label; const hours = context.parsed.y; const percentage = ((hours / projectData.totalHours) * 100).toFixed(1); return `${workType}: ${hours}h (${percentage}%)`; } } } } } }; this.createChart('projectDistribution', canvas, config); console.log('✅ 스택 바 차트 렌더링 완료'); } /** * 스택 바 차트 데이터 처리 */ _processStackedBarData(projects) { // 모든 작업유형 수집 const allWorkTypes = new Set(); projects.forEach(project => { project.workTypes.forEach(wt => { allWorkTypes.add(wt.work_type_name); }); }); const workTypeArray = Array.from(allWorkTypes); const labels = projects.map(p => p.project_name); // 작업유형별 데이터셋 생성 const datasets = workTypeArray.map((workTypeName, index) => { const data = projects.map(project => { const workType = project.workTypes.find(wt => wt.work_type_name === workTypeName); return workType ? workType.totalHours : 0; }); return { label: workTypeName, data, backgroundColor: this.defaultColors[index % this.defaultColors.length], borderColor: this.defaultColors[index % this.defaultColors.length], borderWidth: 1 }; }); return { labels, datasets }; } // ========== 도넛 차트 ========== /** * 도넛 차트 렌더링 (작업자별 성과) * @param {Array} workerData - 작업자 데이터 */ renderWorkerPerformanceChart(workerData) { console.log('👤 작업자별 성과 차트 렌더링 시작'); const canvas = document.getElementById('workerPerformanceChart'); if (!canvas) { console.error('❌ workerPerformanceChart 캔버스를 찾을 수 없습니다'); return; } if (!workerData || workerData.length === 0) { this._showChartError('workerPerformanceChart', '작업자 데이터가 없습니다'); return; } const chartData = this.dataProcessor.processDonutChartData( workerData.map(worker => ({ name: worker.worker_name, hours: worker.totalHours })) ); const config = { type: 'doughnut', data: chartData, options: { responsive: true, maintainAspectRatio: false, aspectRatio: 1, plugins: { title: { display: true, text: '작업자별 작업시간 분포' }, legend: { display: true, position: 'right' }, tooltip: { callbacks: { label: function(context) { const label = context.label; const value = context.parsed; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return `${label}: ${value}h (${percentage}%)`; } } } } } }; this.createChart('workerPerformance', canvas, config); console.log('✅ 작업자별 성과 차트 렌더링 완료'); } // ========== 오류 분석 차트 ========== /** * 오류 분석 차트 렌더링 * @param {Array} errorData - 오류 데이터 */ renderErrorAnalysisChart(errorData) { console.log('⚠️ 오류 분석 차트 렌더링 시작'); const canvas = document.getElementById('errorAnalysisChart'); if (!canvas) { console.error('❌ errorAnalysisChart 캔버스를 찾을 수 없습니다'); return; } if (!errorData || errorData.length === 0) { this._showChartError('errorAnalysisChart', '오류 데이터가 없습니다'); return; } // 오류가 있는 데이터만 필터링 const errorItems = errorData.filter(item => item.error_count > 0 || (item.errorDetails && item.errorDetails.length > 0) ); if (errorItems.length === 0) { this._showChartError('errorAnalysisChart', '오류가 발생한 항목이 없습니다'); return; } const chartData = this.dataProcessor.processDonutChartData( errorItems.map(item => ({ name: item.project_name || item.name, hours: item.errorHours || item.error_count })) ); const config = { type: 'doughnut', data: chartData, options: { responsive: true, maintainAspectRatio: false, aspectRatio: 1, plugins: { title: { display: true, text: '프로젝트별 오류 분포' }, legend: { display: true, position: 'bottom' }, tooltip: { callbacks: { label: function(context) { const label = context.label; const value = context.parsed; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return `${label}: ${value}h (${percentage}%)`; } } } } } }; this.createChart('errorAnalysis', canvas, config); console.log('✅ 오류 분석 차트 렌더링 완료'); } // ========== 유틸리티 ========== /** * 차트 오류 표시 * @param {string} canvasId - 캔버스 ID * @param {string} message - 오류 메시지 */ _showChartError(canvasId, message) { const canvas = document.getElementById(canvasId); if (!canvas) return; const container = canvas.parentElement; if (container) { container.innerHTML = `
📊
차트를 표시할 수 없습니다
${message}
`; } } /** * 차트 리사이즈 */ resizeCharts() { this.charts.forEach((chart, id) => { try { chart.resize(); console.log('📏 차트 리사이즈:', id); } catch (error) { console.warn('⚠️ 차트 리사이즈 실패:', id, error); } }); } /** * 차트 상태 확인 */ getChartStatus() { const status = {}; this.charts.forEach((chart, id) => { status[id] = { type: chart.config.type, datasetCount: chart.data.datasets.length, dataPointCount: chart.data.labels ? chart.data.labels.length : 0 }; }); return status; } } // 전역 인스턴스 생성 window.WorkAnalysisChartRenderer = new WorkAnalysisChartRenderer(); // 윈도우 리사이즈 이벤트 리스너 window.addEventListener('resize', () => { window.WorkAnalysisChartRenderer.resizeCharts(); }); // Export는 브라우저 환경에서 제거됨