Files
TK-FB-Project/web-ui/pages/analysis/work-report-analytics.html
Hyungi Ahn 94ecc7333d feat: 데이터베이스 및 웹 UI 대규모 리팩토링
- 삭제된 DB 테이블들과 관련 코드 정리:
  * 12개 사용하지 않는 테이블 삭제 (activity_logs, CuttingPlan, DailyIssueReports 등)
  * 관련 모델, 컨트롤러, 라우트 파일들 삭제
  * index.js에서 삭제된 라우트들 제거

- 웹 UI 페이지 정리:
  * 21개 사용하지 않는 페이지 삭제
  * issue-reports 폴더 전체 삭제
  * 모든 사용자 권한을 그룹장 대시보드로 통일

- 데이터베이스 스키마 정리:
  * v1 스키마로 통일 (daily_work_reports 테이블)
  * JSON 데이터 임포트 스크립트 구현
  * 외래키 관계 정리 및 데이터 일관성 확보

- 통합 Docker Compose 설정:
  * 모든 서비스를 단일 docker-compose.yml로 통합
  * 20000번대 포트 유지
  * JWT 시크릿 및 환경변수 설정

- 문서화:
  * DATABASE_SCHEMA.md: 현재 DB 스키마 문서화
  * DELETED_TABLES.md: 삭제된 테이블 목록
  * DELETED_PAGES.md: 삭제된 페이지 목록
2025-11-03 09:26:50 +09:00

1084 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업 보고서 종합 분석 - TK 생산팀 포털</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="../../css/main-layout.css">
<link rel="stylesheet" href="../../css/daily-work-report.css">
<style>
.analytics-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.filter-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.filter-row {
display: flex;
gap: 15px;
align-items: end;
flex-wrap: wrap;
margin-bottom: 15px;
}
.filter-group {
display: flex;
flex-direction: column;
min-width: 150px;
}
.filter-group label {
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.filter-group select,
.filter-group input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.btn-analyze {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
height: fit-content;
}
.btn-analyze:hover {
background: #0056b3;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stat-card h3 {
margin: 0 0 10px 0;
color: #666;
font-size: 14px;
font-weight: 500;
}
.stat-card .value {
font-size: 24px;
font-weight: 700;
color: #007bff;
margin-bottom: 5px;
}
.stat-card .unit {
font-size: 12px;
color: #888;
}
.chart-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-section h2 {
margin: 0 0 20px 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.chart-container {
height: 400px;
position: relative;
}
.table-container {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.analysis-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.analysis-table th,
.analysis-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.analysis-table th {
background: #f8f9fa;
font-weight: 600;
color: #333;
}
.analysis-table tr:hover {
background: #f8f9fa;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #007bff;
transition: width 0.3s ease;
}
.error-rate {
color: #dc3545;
font-weight: 600;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
cursor: pointer;
border: none;
background: none;
color: #666;
font-weight: 500;
}
.tab.active {
color: #007bff;
border-bottom: 2px solid #007bff;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="analytics-container">
<h1>📊 작업 보고서 종합 분석</h1>
<!-- 필터 섹션 -->
<div class="filter-section">
<h2>🔍 분석 조건 설정</h2>
<div class="filter-row">
<div class="filter-group">
<label for="startDate">시작 날짜</label>
<input type="date" id="startDate" required>
</div>
<div class="filter-group">
<label for="endDate">종료 날짜</label>
<input type="date" id="endDate" required>
</div>
<div class="filter-group">
<label for="projectFilter">프로젝트</label>
<select id="projectFilter">
<option value="">전체 프로젝트</option>
</select>
</div>
<div class="filter-group">
<label for="workerFilter">작업자</label>
<select id="workerFilter">
<option value="">전체 작업자</option>
</select>
</div>
<button class="btn-analyze" onclick="performAnalysis()">분석 실행</button>
</div>
</div>
<!-- 로딩 상태 -->
<div id="loadingState" class="loading" style="display: none;">
<p>📊 데이터를 분석하고 있습니다...</p>
</div>
<!-- 분석 결과 -->
<div id="analysisResults" style="display: none;">
<!-- 전체 통계 -->
<div class="stats-grid" id="overallStats">
<!-- 통계 카드들이 동적으로 생성됩니다 -->
</div>
<!-- 탭 메뉴 -->
<div class="tabs">
<button class="tab active" onclick="showTab('daily')">일별 분석</button>
<button class="tab" onclick="showTab('worker')">작업자별 분석</button>
<button class="tab" onclick="showTab('project')">프로젝트별 분석</button>
<button class="tab" onclick="showTab('worktype')">작업유형별 분석</button>
<button class="tab" onclick="showTab('error')">에러 분석</button>
</div>
<!-- 일별 분석 -->
<div id="dailyTab" class="tab-content active">
<div class="chart-section">
<h2>📅 일별 작업 현황</h2>
<div class="analysis-insights" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h4>💡 일별 분석 활용 가이드</h4>
<ul style="margin: 10px 0; padding-left: 20px; color: #666;">
<li><strong>작업량 패턴 파악:</strong> 주중/주말, 월초/월말 작업량 변화 확인</li>
<li><strong>생산성 트렌드:</strong> 평균선 대비 높은/낮은 날 분석으로 효율성 개선점 도출</li>
<li><strong>리소스 계획:</strong> 피크 시간대 파악으로 인력 배치 최적화</li>
<li><strong>품질 관리:</strong> 작업량이 급증한 날의 에러율 상관관계 분석</li>
</ul>
</div>
<div class="chart-container">
<canvas id="dailyChart"></canvas>
</div>
</div>
</div>
<!-- 작업자별 분석 -->
<div id="workerTab" class="tab-content">
<div class="table-container">
<h2>👥 작업자별 성과 분석</h2>
<table class="analysis-table" id="workerTable">
<thead>
<tr>
<th>작업자</th>
<th>총 작업시간</th>
<th>작업 항목</th>
<th>근무일수</th>
<th>평균 시간/항목</th>
<th>에러율</th>
<th>진행률</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 생성됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 프로젝트별 분석 -->
<div id="projectTab" class="tab-content">
<div class="table-container">
<h2>📋 프로젝트별 진행 분석</h2>
<table class="analysis-table" id="projectTable">
<thead>
<tr>
<th>프로젝트</th>
<th>총 작업시간</th>
<th>작업 항목</th>
<th>참여 작업자</th>
<th>활동일수</th>
<th>에러율</th>
<th>진행률</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 생성됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 작업유형별 분석 -->
<div id="worktypeTab" class="tab-content">
<div class="chart-section">
<h2>🔧 작업 유형별 분포</h2>
<div class="chart-container">
<canvas id="worktypeChart"></canvas>
</div>
</div>
</div>
<!-- 에러 분석 -->
<div id="errorTab" class="tab-content">
<div class="chart-section">
<h2>⚠️ 에러 유형별 분석</h2>
<div class="analysis-insights" style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #ffc107;">
<h4>🔍 에러 분석 활용 가이드</h4>
<ul style="margin: 10px 0; padding-left: 20px; color: #856404;">
<li><strong>품질 개선:</strong> 가장 빈번한 에러 유형 우선 해결</li>
<li><strong>교육 계획:</strong> 특정 에러가 많은 작업자 대상 맞춤 교육</li>
<li><strong>프로세스 개선:</strong> 에러 패턴 분석으로 작업 절차 최적화</li>
<li><strong>예방 조치:</strong> 에러 발생 시간대 분석으로 예방책 수립</li>
</ul>
</div>
<!-- 에러 분포 차트 -->
<div class="chart-container" style="margin-bottom: 30px;">
<canvas id="errorChart"></canvas>
</div>
<!-- 에러 상세 테이블 -->
<div class="table-container">
<h3>📋 에러 상세 분석</h3>
<table class="analysis-table" id="errorTable">
<thead>
<tr>
<th>에러 유형</th>
<th>발생 횟수</th>
<th>에러 시간</th>
<th>비율 (%)</th>
<th>영향받은 작업자</th>
<th>영향받은 프로젝트</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 생성됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 데이터 없음 상태 -->
<div id="noDataState" class="no-data" style="display: none;">
<p>📭 선택한 조건에 해당하는 데이터가 없습니다.</p>
</div>
</div>
<!-- Chart.js 라이브러리 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script>
// 전역 변수
let currentAnalysisData = null;
let dailyChart = null;
let worktypeChart = null;
let errorChart = null;
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', function() {
initializePage();
});
// 권한 체크 함수
function checkUserPermission() {
const user = JSON.parse(localStorage.getItem('user') || '{}');
const adminRoles = ['admin', 'system'];
if (!user.role || !adminRoles.includes(user.role)) {
// 권한 없음 메시지 표시
document.body.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: Arial, sans-serif;">
<div style="text-align: center; padding: 2rem; border: 2px solid #ff6b6b; border-radius: 10px; background: #fff5f5;">
<h2 style="color: #e74c3c; margin-bottom: 1rem;">🚫 접근 권한 없음</h2>
<p style="color: #666; margin-bottom: 1.5rem;">
작업보고서 분석 페이지는 <strong>관리자 권한</strong>이 필요합니다.<br>
현재 권한: <span style="color: #e74c3c;">${user.role || '없음'}</span><br>
필요 권한: <span style="color: #27ae60;">admin 또는 system</span>
</p>
<button onclick="window.history.back()" style="
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
">← 이전 페이지</button>
<button onclick="window.location.href='/pages/dashboard/main.html'" style="
background: #27ae60;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
">🏠 대시보드</button>
</div>
</div>
`;
return false;
}
return true;
}
// 페이지 초기화
async function initializePage() {
try {
// 권한 체크
if (!checkUserPermission()) {
return;
}
// 기본 날짜 설정 (데이터가 있는 범위로 설정)
document.getElementById('startDate').value = '2025-06-02';
document.getElementById('endDate').value = '2025-09-01';
// 필터 옵션 로드
await loadFilterOptions();
} catch (error) {
console.error('페이지 초기화 오류:', error);
alert('페이지 초기화 중 오류가 발생했습니다.');
}
}
// 필터 옵션 로드
async function loadFilterOptions() {
try {
const response = await fetch('http://localhost:20005/api/daily-work-reports-analysis/filters', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
throw new Error('필터 데이터 로드 실패');
}
const data = await response.json();
// 프로젝트 옵션 추가
const projectSelect = document.getElementById('projectFilter');
data.data.projects.forEach(project => {
const option = document.createElement('option');
option.value = project.project_id;
option.textContent = `${project.project_name}`;
projectSelect.appendChild(option);
});
// 작업자 옵션 추가
const workerSelect = document.getElementById('workerFilter');
data.data.workers.forEach(worker => {
const option = document.createElement('option');
option.value = worker.worker_id;
option.textContent = `${worker.worker_name}`;
workerSelect.appendChild(option);
});
} catch (error) {
console.error('필터 옵션 로드 오류:', error);
}
}
// 분석 실행
async function performAnalysis() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const projectId = document.getElementById('projectFilter').value;
const workerId = document.getElementById('workerFilter').value;
if (!startDate || !endDate) {
alert('시작 날짜와 종료 날짜를 선택해주세요.');
return;
}
// 로딩 상태 표시
showLoadingState();
try {
// API 호출
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate
});
if (projectId) params.append('project_id', projectId);
if (workerId) params.append('worker_id', workerId);
const response = await fetch(`http://localhost:20005/api/daily-work-reports-analysis/period?${params}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
throw new Error('분석 데이터 조회 실패');
}
const data = await response.json();
currentAnalysisData = data;
// 결과 표시
displayAnalysisResults(data);
} catch (error) {
console.error('분석 실행 오류:', error);
alert('분석 실행 중 오류가 발생했습니다.');
hideLoadingState();
}
}
// 로딩 상태 표시
function showLoadingState() {
document.getElementById('loadingState').style.display = 'block';
document.getElementById('analysisResults').style.display = 'none';
document.getElementById('noDataState').style.display = 'none';
}
// 로딩 상태 숨김
function hideLoadingState() {
document.getElementById('loadingState').style.display = 'none';
}
// 분석 결과 표시
function displayAnalysisResults(data) {
hideLoadingState();
if (!data.data || !data.data.summary || data.data.summary.total_entries === 0) {
document.getElementById('noDataState').style.display = 'block';
return;
}
document.getElementById('analysisResults').style.display = 'block';
// 전체 통계 표시
displayOverallStats(data.data.summary);
// 각 탭별 데이터 표시
displayDailyChart(data.data.dailyStats);
displayWorkerTable(data.data.workerAnalysis);
displayProjectTable(data.data.projectAnalysis);
// displayWorktypeChart(data.data.workTypeAnalysis); // TODO: 작업유형 차트 구현 필요
displayErrorChart(data.data.dailyErrorStats);
displayErrorTable(data.data.errorAnalysis);
}
// 전체 통계 표시
function displayOverallStats(stats) {
const container = document.getElementById('overallStats');
container.innerHTML = '';
const statCards = [
{ title: '총 작업 항목', value: stats.total_entries, unit: '개' },
{ title: '총 작업 시간', value: stats.total_hours, unit: '시간' },
{ title: '참여 작업자', value: stats.unique_workers, unit: '명' },
{ title: '진행 프로젝트', value: stats.unique_projects, unit: '개' },
{ title: '근무일수', value: stats.working_days, unit: '일' },
{ title: '평균 시간/항목', value: parseFloat(stats.avg_hours_per_entry || 0).toFixed(1), unit: '시간' },
{ title: '에러 항목', value: stats.error_entries, unit: '개' },
{ title: '기여자 수', value: stats.contributors, unit: '명' }
];
statCards.forEach(stat => {
const card = document.createElement('div');
card.className = 'stat-card';
card.innerHTML = `
<h3>${stat.title}</h3>
<div class="value">${stat.value}</div>
<div class="unit">${stat.unit}</div>
`;
container.appendChild(card);
});
}
// 일별 차트 표시
function displayDailyChart(dailyData) {
const ctx = document.getElementById('dailyChart').getContext('2d');
if (dailyChart) {
dailyChart.destroy();
}
const labels = dailyData.map(d => {
const date = new Date(d.report_date);
return `${date.getMonth() + 1}/${date.getDate()}`;
});
const hours = dailyData.map(d => d.daily_hours);
const entries = dailyData.map(d => d.daily_entries);
// 평균선 계산
const avgHours = hours.reduce((a, b) => a + b, 0) / hours.length;
dailyChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '일별 작업시간',
data: hours,
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4,
yAxisID: 'y',
fill: true,
pointRadius: 4,
pointHoverRadius: 6
}, {
label: '일별 작업항목',
data: entries,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.4,
yAxisID: 'y1',
fill: true,
pointRadius: 4,
pointHoverRadius: 6
}, {
label: `평균 작업시간 (${avgHours.toFixed(1)}h)`,
data: Array(labels.length).fill(avgHours),
borderColor: '#ff6b6b',
borderWidth: 2,
borderDash: [5, 5],
fill: false,
pointRadius: 0,
yAxisID: 'y'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '작업시간 (h)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '작업항목 (개)'
},
grid: {
drawOnChartArea: false,
},
}
}
}
});
}
// 작업자별 테이블 표시
function displayWorkerTable(workerData) {
const tbody = document.querySelector('#workerTable tbody');
tbody.innerHTML = '';
const maxHours = Math.max(...workerData.map(w => w.total_hours));
workerData.forEach(worker => {
const row = document.createElement('tr');
const progressPercent = (worker.total_hours / maxHours) * 100;
row.innerHTML = `
<td>${worker.worker_name}</td>
<td>${worker.total_hours}h</td>
<td>${worker.total_entries}개</td>
<td>${worker.working_days}일</td>
<td>${parseFloat(worker.avg_hours_per_entry).toFixed(1)}h</td>
<td class="error-rate">${worker.error_rate}%</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
</div>
</td>
`;
tbody.appendChild(row);
});
}
// 프로젝트별 테이블 표시
function displayProjectTable(projectData) {
const tbody = document.querySelector('#projectTable tbody');
tbody.innerHTML = '';
const maxHours = Math.max(...projectData.map(p => p.total_hours));
projectData.forEach(project => {
const row = document.createElement('tr');
const progressPercent = (project.total_hours / maxHours) * 100;
row.innerHTML = `
<td>${project.project_name}</td>
<td>${project.total_hours}h</td>
<td>${project.total_entries}개</td>
<td>${project.workers_involved}명</td>
<td>${project.active_days}일</td>
<td class="error-rate">${project.error_rate}%</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
</div>
</td>
`;
tbody.appendChild(row);
});
}
// 작업유형별 차트 표시
function displayWorktypeChart(worktypeData) {
const ctx = document.getElementById('worktypeChart').getContext('2d');
if (worktypeChart) {
worktypeChart.destroy();
}
const labels = worktypeData.map(w => w.work_type_name);
const hours = worktypeData.map(w => w.total_hours);
const colors = [
'#007bff', '#28a745', '#ffc107', '#dc3545', '#6f42c1',
'#fd7e14', '#20c997', '#e83e8c', '#6c757d', '#17a2b8'
];
worktypeChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: hours,
backgroundColor: colors.slice(0, labels.length),
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
// 에러 테이블 표시
function displayErrorTable(errorData) {
const tbody = document.querySelector('#errorTable tbody');
tbody.innerHTML = '';
if (errorData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="5" style="text-align: center; color: #28a745;">에러가 발생하지 않았습니다! 🎉</td>';
tbody.appendChild(row);
return;
}
errorData.forEach(error => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${error.error_type_name || '기타'}</td>
<td>${error.error_count}회</td>
<td>${error.error_hours}h</td>
<td>${error.affected_workers}명</td>
<td>${error.affected_projects}개</td>
`;
tbody.appendChild(row);
});
}
// 에러 분석 차트 표시 (일별 추이)
function displayErrorChart(dailyErrorData) {
const ctx = document.getElementById('errorChart').getContext('2d');
if (errorChart) {
errorChart.destroy();
}
if (!dailyErrorData || dailyErrorData.length === 0) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.font = '16px Arial';
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText('📭 에러 데이터가 없습니다', ctx.canvas.width / 2, ctx.canvas.height / 2);
return;
}
const labels = dailyErrorData.map(d => {
const date = new Date(d.report_date);
return `${date.getMonth() + 1}/${date.getDate()}`;
});
const errorCounts = dailyErrorData.map(d => d.daily_errors);
const errorRates = dailyErrorData.map(d => d.daily_error_rate);
// 평균 에러율 계산
const avgErrorRate = errorRates.reduce((a, b) => a + b, 0) / errorRates.length;
errorChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '일별 에러 발생 건수',
data: errorCounts,
borderColor: '#ff6b6b',
backgroundColor: 'rgba(255, 107, 107, 0.1)',
tension: 0.4,
yAxisID: 'y',
fill: true,
pointRadius: 4,
pointHoverRadius: 6,
borderWidth: 3
}, {
label: '일별 에러율 (%)',
data: errorRates,
borderColor: '#ffa726',
backgroundColor: 'rgba(255, 167, 38, 0.1)',
tension: 0.4,
yAxisID: 'y1',
fill: true,
pointRadius: 4,
pointHoverRadius: 6,
borderWidth: 3
}, {
label: `평균 에러율 (${avgErrorRate.toFixed(1)}%)`,
data: Array(labels.length).fill(avgErrorRate),
borderColor: '#e91e63',
borderWidth: 2,
borderDash: [5, 5],
fill: false,
pointRadius: 0,
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '에러 발생 건수'
},
beginAtZero: true
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '에러율 (%)'
},
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
}
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 20
}
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.dataset.yAxisID === 'y1') {
label += context.parsed.y + '%';
} else {
label += context.parsed.y + '건';
}
return label;
}
}
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
}
});
}
// 에러 분석 테이블 표시
function displayErrorTable(errorData) {
const container = document.getElementById('errorTable');
if (!container) return;
container.innerHTML = '';
if (!errorData || errorData.length === 0) {
container.innerHTML = '<p class="no-data">📭 에러 데이터가 없습니다.</p>';
return;
}
const table = document.createElement('table');
table.className = 'analysis-table';
table.innerHTML = `
<thead>
<tr>
<th>에러 유형</th>
<th>발생 횟수</th>
<th>에러 시간</th>
<th>비율</th>
<th>진행률</th>
</tr>
</thead>
<tbody>
${errorData.map(error => `
<tr>
<td><strong>${error.error_type_name}</strong></td>
<td>${error.error_count}회</td>
<td>${parseFloat(error.error_hours || 0).toFixed(1)}시간</td>
<td><span class="error-rate">${error.error_percentage}%</span></td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${error.error_percentage}%"></div>
</div>
</td>
</tr>
`).join('')}
</tbody>
`;
container.appendChild(table);
}
// 작업자별 분석 테이블 표시
function displayWorkerTable(workerData) {
const container = document.getElementById('workerTable');
if (!container) return;
container.innerHTML = '';
if (!workerData || workerData.length === 0) {
container.innerHTML = '<p class="no-data">📭 작업자 데이터가 없습니다.</p>';
return;
}
const table = document.createElement('table');
table.className = 'analysis-table';
table.innerHTML = `
<thead>
<tr>
<th>작업자</th>
<th>총 작업시간</th>
<th>작업 항목</th>
<th>근무일수</th>
<th>평균 시간/항목</th>
<th>에러율</th>
</tr>
</thead>
<tbody>
${workerData.map(worker => `
<tr>
<td><strong>${worker.worker_name}</strong></td>
<td>${parseFloat(worker.total_hours).toFixed(1)}시간</td>
<td>${worker.total_entries}개</td>
<td>${worker.working_days}일</td>
<td>${parseFloat(worker.avg_hours_per_entry).toFixed(1)}시간</td>
<td><span class="error-rate">${worker.error_rate || 0}%</span></td>
</tr>
`).join('')}
</tbody>
`;
container.appendChild(table);
}
// 프로젝트별 분석 테이블 표시
function displayProjectTable(projectData) {
const container = document.getElementById('projectTable');
if (!container) return;
container.innerHTML = '';
if (!projectData || projectData.length === 0) {
container.innerHTML = '<p class="no-data">📭 프로젝트 데이터가 없습니다.</p>';
return;
}
const table = document.createElement('table');
table.className = 'analysis-table';
table.innerHTML = `
<thead>
<tr>
<th>프로젝트</th>
<th>총 작업시간</th>
<th>작업 항목</th>
<th>활동일수</th>
<th>에러율</th>
</tr>
</thead>
<tbody>
${projectData.map(project => `
<tr>
<td><strong>${project.project_name}</strong></td>
<td>${parseFloat(project.total_hours).toFixed(1)}시간</td>
<td>${project.total_entries}개</td>
<td>${project.working_days}일</td>
<td><span class="error-rate">${project.error_rate || 0}%</span></td>
</tr>
`).join('')}
</tbody>
`;
container.appendChild(table);
}
// 탭 전환
function showTab(tabName) {
// 모든 탭 비활성화
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// 선택된 탭 활성화
event.target.classList.add('active');
document.getElementById(tabName + 'Tab').classList.add('active');
}
</script>
</body>
</html>