Files
tk-factory-services/system1-factory/web/js/work-analysis/chart-renderer.js
Hyungi Ahn 61c810bd47 refactor: 프론트엔드 SSO 인증 통합 및 API 경로 정리
- Gateway 로그인/포탈 페이지 SSO 연동
- System1 web/fastapi-bridge API base URL 동적 설정
- SSO 토큰 기반 인증 흐름 통일
- deprecated JS 파일 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:09 +09:00

444 lines
14 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);
}
}
/**
* 모든 차트 제거
*/
destroyAllCharts() {
this.charts.forEach((chart, id) => {
chart.destroy();
});
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);
return chart;
}
// ========== 시계열 차트 ==========
/**
* 시계열 차트 렌더링 (기간별 작업 현황)
* @param {string} startDate - 시작일
* @param {string} endDate - 종료일
* @param {string} projectId - 프로젝트 ID (선택사항)
*/
async renderTimeSeriesChart(startDate, endDate, projectId = '') {
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);
} catch (error) {
console.error(' 시계열 차트 렌더링 실패:', error);
this._showChartError('workStatusChart', '시계열 차트를 불러올 수 없습니다');
}
}
// ========== 스택 바 차트 ==========
/**
* 스택 바 차트 렌더링 (프로젝트별 → 작업유형별)
* @param {Array} projectData - 프로젝트 데이터
*/
renderStackedBarChart(projectData) {
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);
}
/**
* 스택 바 차트 데이터 처리
*/
_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) {
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);
}
// ========== 오류 분석 차트 ==========
/**
* 오류 분석 차트 렌더링
* @param {Array} errorData - 오류 데이터
*/
renderErrorAnalysisChart(errorData) {
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);
}
// ========== 유틸리티 ==========
/**
* 차트 오류 표시
* @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();
} 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는 브라우저 환경에서 제거됨