1044 lines
40 KiB
HTML
1044 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>📊 작업 분석 대시보드 | 개선된 버전</title>
|
|
<link rel="stylesheet" href="/css/main-layout.css">
|
|
<link rel="icon" type="image/png" href="/img/favicon.png">
|
|
<script src="/js/auth-check.js" defer></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
color: #333;
|
|
}
|
|
|
|
.main-layout-with-navbar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.content-wrapper {
|
|
flex: 1;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
color: #4a5568;
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.header p {
|
|
color: #718096;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.controls {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.control-group label {
|
|
font-weight: 600;
|
|
color: #4a5568;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.control-group input, .control-group select {
|
|
padding: 10px;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
font-size: 1em;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.control-group input:focus, .control-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 25px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.back-btn {
|
|
background: rgba(255,255,255,0.95);
|
|
color: #667eea;
|
|
border: 3px solid #667eea;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #718096;
|
|
}
|
|
|
|
.error {
|
|
background: #fed7d7;
|
|
color: #c53030;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin: 20px 0;
|
|
border: 1px solid #feb2b2;
|
|
}
|
|
|
|
.success {
|
|
background: #c6f6d5;
|
|
color: #2d7d32;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin: 20px 0;
|
|
border: 1px solid #9ae6b4;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.status-online {
|
|
background: #48bb78;
|
|
}
|
|
|
|
.status-offline {
|
|
background: #f56565;
|
|
}
|
|
|
|
/* 새로운 섹션 스타일 */
|
|
.section {
|
|
background: white;
|
|
padding: 25px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.section h2 {
|
|
color: #4a5568;
|
|
margin-bottom: 20px;
|
|
font-size: 1.5em;
|
|
border-bottom: 3px solid #667eea;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.summary-card {
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #e3f2fd 100%);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border-left: 5px solid #667eea;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.summary-card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.summary-card h3 {
|
|
color: #4a5568;
|
|
margin-bottom: 15px;
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.summary-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.8em;
|
|
font-weight: 700;
|
|
color: #667eea;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9em;
|
|
color: #718096;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
padding: 25px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.chart-title {
|
|
color: #4a5568;
|
|
font-size: 1.3em;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.chart-canvas {
|
|
width: 100%;
|
|
height: 300px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bar-chart {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.bar-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.bar-label {
|
|
min-width: 120px;
|
|
font-weight: 600;
|
|
color: #4a5568;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.bar-wrapper {
|
|
flex: 1;
|
|
height: 30px;
|
|
background: #f7fafc;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.bar-fill {
|
|
height: 100%;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 15px;
|
|
transition: width 0.8s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.bar-fill.error {
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
}
|
|
|
|
.bar-value {
|
|
position: absolute;
|
|
right: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: white;
|
|
font-weight: 600;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.project-details {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.project-card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
border-top: 4px solid #667eea;
|
|
}
|
|
|
|
.project-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.project-title {
|
|
font-size: 1.2em;
|
|
font-weight: 600;
|
|
color: #4a5568;
|
|
}
|
|
|
|
.error-badge {
|
|
background: #fed7d7;
|
|
color: #c53030;
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.8em;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.work-type-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.work-type-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px;
|
|
background: #f8f9ff;
|
|
border-radius: 8px;
|
|
border-left: 3px solid #667eea;
|
|
}
|
|
|
|
.work-type-item.has-error {
|
|
border-left-color: #ff6b6b;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.work-type-name {
|
|
font-weight: 600;
|
|
color: #4a5568;
|
|
}
|
|
|
|
.work-type-stats {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.work-type-hours {
|
|
color: #667eea;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.work-type-error {
|
|
color: #c53030;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.line-chart {
|
|
width: 100%;
|
|
height: 300px;
|
|
position: relative;
|
|
background: #f8f9ff;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.line-chart svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 600px;
|
|
}
|
|
|
|
.chart-point {
|
|
fill: #667eea;
|
|
stroke: white;
|
|
stroke-width: 2;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.chart-point:hover {
|
|
fill: #764ba2;
|
|
r: 6;
|
|
}
|
|
|
|
.chart-line {
|
|
fill: none;
|
|
stroke: #667eea;
|
|
stroke-width: 3;
|
|
stroke-linecap: round;
|
|
}
|
|
|
|
.chart-axis {
|
|
stroke: #a0aec0;
|
|
stroke-width: 1;
|
|
}
|
|
|
|
.chart-text {
|
|
fill: #4a5568;
|
|
font-size: 12px;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.project-details {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.summary-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="main-layout-with-navbar">
|
|
<!-- 네비게이션 바 -->
|
|
<div id="navbar-container"></div>
|
|
|
|
<div class="content-wrapper">
|
|
<div class="container">
|
|
<!-- 뒤로가기 버튼 -->
|
|
<a href="javascript:history.back()" class="back-btn">
|
|
← 뒤로가기
|
|
</a>
|
|
|
|
<div class="header">
|
|
<h1>📊 작업 분석 대시보드</h1>
|
|
<p>총 시간 분류 & 프로젝트별 상세 분석</p>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label for="startDate">시작일</label>
|
|
<input type="date" id="startDate">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="endDate">종료일</label>
|
|
<input type="date" id="endDate">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label> </label>
|
|
<button class="btn" onclick="loadDashboard()">🏠 대시보드</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label> </label>
|
|
<button class="btn" onclick="loadData()">📈 개별 API</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="connectionStatus"></div>
|
|
|
|
<!-- 총 시간 분류 섹션 -->
|
|
<div class="section">
|
|
<h2>⏱️ 총 시간 분류</h2>
|
|
<div class="summary-grid" id="timeSummary">
|
|
<div class="loading">데이터를 불러오는 중...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 프로젝트 분류 섹션 -->
|
|
<div class="section">
|
|
<h2>📋 프로젝트 분류</h2>
|
|
<div class="summary-grid" id="projectSummary">
|
|
<div class="loading">데이터를 불러오는 중...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 일별 작업시간 추이 -->
|
|
<div class="chart-container">
|
|
<h3 class="chart-title">📈 일별 작업시간 추이</h3>
|
|
<div class="line-chart" id="dailyTrendChart">
|
|
<div class="loading">데이터를 불러오는 중...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 프로젝트별 상세 분석 -->
|
|
<div class="section">
|
|
<h2>🔍 프로젝트별 작업 유형 & 에러 분석</h2>
|
|
<div class="project-details" id="projectDetails">
|
|
<div class="loading">데이터를 불러오는 중...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module" src="/js/load-navbar.js"></script>
|
|
<script type="module">
|
|
// 기존 시스템의 API 설정 사용
|
|
import { API, getAuthHeaders } from '/js/api-config.js';
|
|
|
|
// 초기 설정
|
|
const today = new Date();
|
|
const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
document.getElementById('startDate').value = lastWeek.toISOString().split('T')[0];
|
|
document.getElementById('endDate').value = today.toISOString().split('T')[0];
|
|
|
|
// API 호출 함수
|
|
async function apiCall(endpoint, options = {}) {
|
|
try {
|
|
const response = await fetch(`${API}${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API 호출 에러:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 대시보드 데이터 로드 (권장)
|
|
async function loadDashboard() {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
alert('시작일과 종료일을 모두 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="loading">대시보드 데이터를 불러오는 중...</div>
|
|
`;
|
|
|
|
console.log('🏠 대시보드 API 호출...');
|
|
const data = await apiCall(`/work-analysis/dashboard?start=${startDate}&end=${endDate}`);
|
|
|
|
console.log('📊 대시보드 데이터:', data);
|
|
|
|
// 데이터 구조 디버깅
|
|
console.log('프로젝트 통계 구조:', data.data.projectStats);
|
|
console.log('작업 유형 통계 구조:', data.data.workTypeStats);
|
|
|
|
if (data.success && data.data) {
|
|
const dashboardData = data.data;
|
|
|
|
// 개선된 UI로 데이터 표시
|
|
displayTimeSummary(dashboardData.stats);
|
|
displayProjectSummary(dashboardData.projectStats);
|
|
displayDailyTrendChart(dashboardData.dailyTrend);
|
|
await displayProjectDetails(dashboardData.projectStats, dashboardData.workTypeStats);
|
|
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="success">
|
|
<span class="status-indicator status-online"></span>
|
|
대시보드 데이터를 성공적으로 불러왔습니다.
|
|
<br><small>기간: ${dashboardData.metadata.period}</small>
|
|
</div>
|
|
`;
|
|
} else {
|
|
throw new Error('대시보드 데이터 형식이 올바르지 않습니다.');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('대시보드 로드 에러:', error);
|
|
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="error">
|
|
<span class="status-indicator status-offline"></span>
|
|
대시보드 로드 실패: ${error.message}
|
|
<br><small>개별 API로 재시도해보세요.</small>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 개별 API 호출 (백업용)
|
|
async function loadData() {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
alert('시작일과 종료일을 모두 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="loading">개별 데이터를 불러오는 중...</div>
|
|
`;
|
|
|
|
console.log('📊 개별 API 호출...');
|
|
|
|
// 개별 API 호출
|
|
const [stats, dailyTrend, workerStats, projectStats, workTypeStats, recentWork] = await Promise.all([
|
|
apiCall(`/work-analysis/stats?start=${startDate}&end=${endDate}`),
|
|
apiCall(`/work-analysis/daily-trend?start=${startDate}&end=${endDate}`),
|
|
apiCall(`/work-analysis/worker-stats?start=${startDate}&end=${endDate}`),
|
|
apiCall(`/work-analysis/project-stats?start=${startDate}&end=${endDate}`),
|
|
apiCall(`/work-analysis/work-type-stats?start=${startDate}&end=${endDate}`),
|
|
apiCall(`/work-analysis/recent-work?start=${startDate}&end=${endDate}&limit=20`)
|
|
]);
|
|
|
|
console.log('📊 개별 데이터 로드 완료');
|
|
|
|
// 데이터 구조 디버깅
|
|
console.log('통계 데이터:', stats.data);
|
|
console.log('프로젝트 통계:', projectStats.data);
|
|
console.log('작업 유형 통계:', workTypeStats.data);
|
|
|
|
// 개선된 UI로 데이터 표시
|
|
displayTimeSummary(stats.data);
|
|
displayProjectSummary(projectStats.data);
|
|
displayDailyTrendChart(dailyTrend.data);
|
|
await displayProjectDetails(projectStats.data, workTypeStats.data);
|
|
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="success">
|
|
<span class="status-indicator status-online"></span>
|
|
개별 API를 통해 데이터를 성공적으로 불러왔습니다.
|
|
<br><small>기간: ${startDate} ~ ${endDate}</small>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('개별 데이터 로드 에러:', error);
|
|
|
|
document.getElementById('connectionStatus').innerHTML = `
|
|
<div class="error">
|
|
<span class="status-indicator status-offline"></span>
|
|
API 연결 실패: ${error.message}
|
|
<br><small>API 서버 상태를 확인해주세요.</small>
|
|
</div>
|
|
`;
|
|
|
|
// 에러 시 로딩 상태 해제
|
|
document.getElementById('timeSummary').innerHTML = '<div class="loading">API 연결을 확인해주세요.</div>';
|
|
document.getElementById('projectSummary').innerHTML = '<div class="loading">API 연결을 확인해주세요.</div>';
|
|
document.getElementById('dailyTrendChart').innerHTML = '<div class="loading">API 연결을 확인해주세요.</div>';
|
|
document.getElementById('projectDetails').innerHTML = '<div class="loading">API 연결을 확인해주세요.</div>';
|
|
}
|
|
}
|
|
|
|
// 총 시간 분류 표시
|
|
function displayTimeSummary(stats) {
|
|
const container = document.getElementById('timeSummary');
|
|
|
|
container.innerHTML = `
|
|
<div class="summary-card">
|
|
<h3>⏱️ 전체 작업시간</h3>
|
|
<div class="summary-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-value">${(stats.totalHours || 0).toFixed(1)}</div>
|
|
<div class="stat-label">총 시간</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${stats.totalReports || 0}</div>
|
|
<div class="stat-label">총 보고서</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<h3>❌ 에러 현황</h3>
|
|
<div class="summary-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-value" style="color: #c53030;">${(stats.errorRate || 0).toFixed(1)}%</div>
|
|
<div class="stat-label">에러율</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" style="color: #c53030;">${Math.round((stats.totalReports || 0) * (stats.errorRate || 0) / 100)}</div>
|
|
<div class="stat-label">에러 건수</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<h3>👥 작업 현황</h3>
|
|
<div class="summary-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-value">${stats.activeWorkers || 0}</div>
|
|
<div class="stat-label">활성 작업자</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${stats.activeProjects || 0}</div>
|
|
<div class="stat-label">활성 프로젝트</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 프로젝트 분류 표시
|
|
function displayProjectSummary(projectStats) {
|
|
const container = document.getElementById('projectSummary');
|
|
|
|
if (!projectStats || projectStats.length === 0) {
|
|
container.innerHTML = '<div class="loading">프로젝트 데이터가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
const maxHours = Math.max(...projectStats.map(p => p.totalHours));
|
|
|
|
container.innerHTML = projectStats.map(project => {
|
|
// 에러율 계산 - 다양한 필드명 시도
|
|
let errorRate = 0;
|
|
let errorCount = 0;
|
|
let totalCount = 0;
|
|
|
|
// 가능한 필드명들 확인
|
|
if (project.errorRate !== undefined) {
|
|
errorRate = project.errorRate;
|
|
} else if (project.totalReports && project.errorReports) {
|
|
errorRate = (project.errorReports / project.totalReports) * 100;
|
|
errorCount = project.errorReports;
|
|
totalCount = project.totalReports;
|
|
} else if (project.totalReports && project.totalErrorReports) {
|
|
errorRate = (project.totalErrorReports / project.totalReports) * 100;
|
|
errorCount = project.totalErrorReports;
|
|
totalCount = project.totalReports;
|
|
} else if (project.reports && project.errorReports) {
|
|
errorRate = (project.errorReports / project.reports) * 100;
|
|
errorCount = project.errorReports;
|
|
totalCount = project.reports;
|
|
} else {
|
|
// 디버깅용 - 실제 데이터 구조 확인
|
|
console.log('프로젝트 데이터 구조:', project);
|
|
errorRate = 0;
|
|
}
|
|
|
|
return `
|
|
<div class="summary-card">
|
|
<h3>📋 ${project.project_name}</h3>
|
|
<div class="summary-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-value">${project.totalHours.toFixed(1)}</div>
|
|
<div class="stat-label">총 시간</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" style="color: ${errorRate > 10 ? '#c53030' : errorRate > 5 ? '#f56565' : '#667eea'};">
|
|
${errorRate.toFixed(1)}%
|
|
</div>
|
|
<div class="stat-label">에러율</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 10px;">
|
|
<div class="bar-wrapper">
|
|
<div class="bar-fill ${errorRate > 10 ? 'error' : ''}" style="width: ${(project.totalHours / maxHours) * 100}%">
|
|
<span class="bar-value">${project.totalHours.toFixed(1)}h</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
${errorCount > 0 ? `
|
|
<div style="margin-top: 10px; font-size: 0.9em; color: #c53030;">
|
|
에러 ${errorCount}건 / 총 ${totalCount}건
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 일별 추이 차트 표시 (개선된 버전)
|
|
function displayDailyTrendChart(dailyData) {
|
|
const container = document.getElementById('dailyTrendChart');
|
|
|
|
if (!dailyData || dailyData.length === 0) {
|
|
container.innerHTML = '<div class="loading">일별 데이터가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
const sortedData = dailyData.sort((a, b) => a.date.localeCompare(b.date));
|
|
const maxHours = Math.max(...sortedData.map(d => d.hours));
|
|
const width = Math.max(800, sortedData.length * 60);
|
|
const height = 250;
|
|
const padding = 50;
|
|
|
|
const xStep = (width - padding * 2) / (sortedData.length - 1);
|
|
const yScale = (height - padding * 2) / maxHours;
|
|
|
|
// 날짜 라벨 간격 조정
|
|
const dateInterval = Math.max(1, Math.floor(sortedData.length / 10));
|
|
|
|
let points = '';
|
|
let circles = '';
|
|
let labels = '';
|
|
|
|
sortedData.forEach((item, index) => {
|
|
const x = padding + index * xStep;
|
|
const y = height - padding - item.hours * yScale;
|
|
|
|
points += `${x},${y} `;
|
|
circles += `<circle cx="${x}" cy="${y}" r="4" class="chart-point">
|
|
<title>${item.date}: ${item.hours.toFixed(1)}시간</title>
|
|
</circle>`;
|
|
|
|
if (index % dateInterval === 0) {
|
|
labels += `
|
|
<text x="${x}" y="${height - 10}" class="chart-text">
|
|
${item.date.split('-').slice(1).join('/')}
|
|
</text>
|
|
`;
|
|
}
|
|
});
|
|
|
|
container.innerHTML = `
|
|
<svg width="100%" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
<!-- 격자선 -->
|
|
${Array.from({length: 6}, (_, i) => {
|
|
const y = padding + i * (height - padding * 2) / 5;
|
|
return `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" class="chart-axis" opacity="0.3"/>`;
|
|
}).join('')}
|
|
|
|
<!-- 선 그래프 -->
|
|
<polyline points="${points}" class="chart-line"/>
|
|
|
|
<!-- 데이터 포인트 -->
|
|
${circles}
|
|
|
|
<!-- 날짜 라벨 -->
|
|
${labels}
|
|
|
|
<!-- Y축 라벨 -->
|
|
${Array.from({length: 6}, (_, i) => {
|
|
const y = padding + i * (height - padding * 2) / 5;
|
|
const value = (maxHours * (5 - i) / 5).toFixed(1);
|
|
return `<text x="${padding - 10}" y="${y + 5}" class="chart-text" text-anchor="end">${value}</text>`;
|
|
}).join('')}
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
// 프로젝트별 상세 분석 표시
|
|
async function displayProjectDetails(projectStats, workTypeStats) {
|
|
const container = document.getElementById('projectDetails');
|
|
|
|
if (!projectStats || projectStats.length === 0) {
|
|
container.innerHTML = '<div class="loading">프로젝트 상세 데이터가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
// 각 프로젝트별로 에러 유형 상세 정보 가져오기
|
|
const projectCardsHtml = await Promise.all(projectStats.map(async (project) => {
|
|
// 에러율 계산 - 다양한 필드명 시도
|
|
let errorRate = 0;
|
|
let errorCount = 0;
|
|
let totalCount = 0;
|
|
|
|
if (project.errorRate !== undefined) {
|
|
errorRate = project.errorRate;
|
|
} else if (project.totalReports && project.errorReports) {
|
|
errorRate = (project.errorReports / project.totalReports) * 100;
|
|
errorCount = project.errorReports;
|
|
totalCount = project.totalReports;
|
|
} else if (project.totalReports && project.totalErrorReports) {
|
|
errorRate = (project.totalErrorReports / project.totalReports) * 100;
|
|
errorCount = project.totalErrorReports;
|
|
totalCount = project.totalReports;
|
|
} else if (project.reports && project.errorReports) {
|
|
errorRate = (project.errorReports / project.reports) * 100;
|
|
errorCount = project.errorReports;
|
|
totalCount = project.reports;
|
|
} else {
|
|
console.log('프로젝트 상세 데이터 구조:', project);
|
|
errorRate = 0;
|
|
}
|
|
|
|
// 프로젝트별 에러 유형 상세 정보 가져오기
|
|
let errorTypeDetails = '';
|
|
if (errorCount > 0) {
|
|
try {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
// 프로젝트별 에러 상세 정보 API 호출
|
|
const errorDetailsResponse = await apiCall(
|
|
`/work-analysis/project-errors?project=${encodeURIComponent(project.project_name)}&start=${startDate}&end=${endDate}`
|
|
);
|
|
|
|
if (errorDetailsResponse.success && errorDetailsResponse.data) {
|
|
const errorTypes = errorDetailsResponse.data;
|
|
console.log(`${project.project_name} 에러 유형:`, errorTypes);
|
|
|
|
// 에러 유형별 분류
|
|
const errorTypeMap = {};
|
|
errorTypes.forEach(error => {
|
|
const errorType = error.error_type || error.errorType || '알 수 없는 에러';
|
|
const workType = error.work_type_name || error.workType || '기타';
|
|
|
|
if (!errorTypeMap[errorType]) {
|
|
errorTypeMap[errorType] = {
|
|
count: 0,
|
|
workTypes: {},
|
|
totalHours: 0
|
|
};
|
|
}
|
|
|
|
errorTypeMap[errorType].count++;
|
|
errorTypeMap[errorType].totalHours += error.work_hours || 0;
|
|
|
|
if (!errorTypeMap[errorType].workTypes[workType]) {
|
|
errorTypeMap[errorType].workTypes[workType] = 0;
|
|
}
|
|
errorTypeMap[errorType].workTypes[workType]++;
|
|
});
|
|
|
|
// 에러 유형별 표시
|
|
errorTypeDetails = Object.entries(errorTypeMap)
|
|
.sort((a, b) => b[1].count - a[1].count)
|
|
.map(([errorType, details]) => `
|
|
<div class="work-type-item has-error">
|
|
<div class="work-type-name">
|
|
🚨 ${errorType}
|
|
</div>
|
|
<div class="work-type-stats">
|
|
<span class="work-type-error">
|
|
${details.count}건 (${details.totalHours.toFixed(1)}시간)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div style="margin-left: 20px; margin-bottom: 10px;">
|
|
<small style="color: #718096;">
|
|
작업 유형: ${Object.entries(details.workTypes)
|
|
.map(([wt, count]) => `${wt}(${count}건)`)
|
|
.join(', ')}
|
|
</small>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('에러 상세 정보 로드 실패:', error);
|
|
errorTypeDetails = `
|
|
<div class="work-type-item has-error">
|
|
<div class="work-type-name">에러 상세 정보 없음</div>
|
|
<div class="work-type-stats">
|
|
<span class="work-type-error">총 ${errorCount}건 에러</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
errorTypeDetails = `
|
|
<div class="work-type-item">
|
|
<div class="work-type-name">✅ 에러 없음</div>
|
|
<div class="work-type-stats">
|
|
<span style="color: #48bb78; font-weight: 600;">
|
|
정상 작업 완료
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="project-card">
|
|
<div class="project-header">
|
|
<h3 class="project-title">${project.project_name}</h3>
|
|
${errorRate > 15 ? `<span class="error-badge">고위험 ${errorRate.toFixed(1)}%</span>` :
|
|
errorRate > 5 ? `<span class="error-badge" style="background: #fff3cd; color: #856404;">주의 ${errorRate.toFixed(1)}%</span>` : ''}
|
|
</div>
|
|
|
|
<div class="work-type-list">
|
|
${errorTypeDetails}
|
|
</div>
|
|
|
|
${errorCount > 0 ? `
|
|
<div style="margin-top: 15px; padding: 10px; background: #fff5f5; border-radius: 8px; border-left: 4px solid #f56565;">
|
|
<strong style="color: #c53030;">⚠️ 프로젝트 에러 요약</strong><br>
|
|
<span style="color: #c53030; font-size: 0.9em;">
|
|
총 ${totalCount}건 중 ${errorCount}건 에러 발생 (${errorRate.toFixed(1)}%)
|
|
</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}));
|
|
|
|
container.innerHTML = projectCardsHtml.join('');
|
|
}
|
|
|
|
// 전역 함수로 노출
|
|
window.loadData = loadData;
|
|
window.loadDashboard = loadDashboard;
|
|
|
|
// 페이지 로드 시 자동 실행
|
|
window.addEventListener('load', () => {
|
|
loadDashboard(); // 대시보드 우선 시도
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |