- Gateway 로그인/포탈 페이지 SSO 연동 - System1 web/fastapi-bridge API base URL 동적 설정 - SSO 토큰 기반 인증 흐름 통일 - deprecated JS 파일 삭제 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
444 lines
14 KiB
JavaScript
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는 브라우저 환경에서 제거됨
|