Files
TK-FB-Project/web-ui/js/work-analysis/chart-renderer.js
Hyungi Ahn ed40eec261 fix: 그룹 리더 대시보드 작업 저장/삭제 오류 해결 및 작업 분석 시스템 성능 최적화
🔧 그룹 리더 대시보드 수정사항:
- API 호출 방식 수정 (modern-dashboard.js)
- 서버 API 요구사항에 맞는 데이터 구조 변경
- work_entries 배열 구조로 변경
- work_type_id → task_id 필드명 매핑
- 400 Bad Request 오류 해결

 작업 분석 시스템 성능 최적화:
- 중복 함수 제거 (isWeekend, isVacationProject 통합)
- WorkAnalysisAPI 캐싱 시스템 구현 (5분 만료)
- 네임스페이스 조직화 (utils, ui, analysis, render)
- ErrorHandler 통합 에러 처리 시스템
- 성능 모니터링 및 메모리 누수 방지
- GPU 가속 CSS 애니메이션 추가
- 디바운스/스로틀 함수 적용
- 의미 없는 통계 카드 제거

📊 작업 분석 페이지 개선:
- 프로그레스 바 애니메이션
- 토스트 알림 시스템
- 부드러운 전환 효과
- 반응형 최적화
- 메모리 사용량 모니터링
2025-11-05 10:12:52 +09:00

456 lines
15 KiB
JavaScript

/**
* 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 = `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: #666;
text-align: center;
">
<div style="font-size: 3rem; margin-bottom: 1rem;">📊</div>
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;">차트를 표시할 수 없습니다</div>
<div style="font-size: 0.9rem;">${message}</div>
</div>
`;
}
}
/**
* 차트 리사이즈
*/
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는 브라우저 환경에서 제거됨