Files
TK-FB-Project/api.hyungi.net/public/pages/work_analysis_dashboard.html

689 lines
22 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></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;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.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);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card h3 {
color: #718096;
font-size: 0.9em;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: 700;
color: #667eea;
margin-bottom: 5px;
}
.stat-card .unit {
color: #a0aec0;
font-size: 0.9em;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 30px;
margin-bottom: 30px;
}
.chart-card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.chart-card h3 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.3em;
text-align: center;
}
.chart-container {
position: relative;
height: 300px;
}
.data-table {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
overflow-x: auto;
}
.data-table h3 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.3em;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background: #f7fafc;
color: #4a5568;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85em;
letter-spacing: 0.5px;
}
tr:hover {
background: #f7fafc;
}
.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;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.charts-grid {
grid-template-columns: 1fr;
}
.chart-container {
height: 250px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 작업 분석 대시보드</h1>
<p>실시간 작업 현황 및 통계 분석</p>
</div>
<div class="controls">
<div class="control-group">
<label for="apiUrl">API 서버 URL</label>
<input type="text" id="apiUrl" value="http://192.168.0.3:3005" placeholder="http://localhost:3005">
</div>
<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>&nbsp;</label>
<button class="btn" onclick="testConnection()">🔍 연결 테스트</button>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button class="btn" onclick="loadData()">📈 데이터 로드</button>
</div>
</div>
<div id="connectionStatus"></div>
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<h3>총 작업시간</h3>
<div class="value" id="totalHours">-</div>
<div class="unit">시간</div>
</div>
<div class="stat-card">
<h3>총 보고서</h3>
<div class="value" id="totalReports">-</div>
<div class="unit"></div>
</div>
<div class="stat-card">
<h3>활성 프로젝트</h3>
<div class="value" id="activeProjects">-</div>
<div class="unit"></div>
</div>
<div class="stat-card">
<h3>에러율</h3>
<div class="value" id="errorRate">-</div>
<div class="unit">%</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-card">
<h3>📈 일별 작업시간 추이</h3>
<div class="chart-container">
<canvas id="dailyTrendChart"></canvas>
</div>
</div>
<div class="chart-card">
<h3>👥 작업자별 통계</h3>
<div class="chart-container">
<canvas id="workerStatsChart"></canvas>
</div>
</div>
<div class="chart-card">
<h3>📋 프로젝트별 통계</h3>
<div class="chart-container">
<canvas id="projectStatsChart"></canvas>
</div>
</div>
<div class="chart-card">
<h3>🔧 작업유형별 통계</h3>
<div class="chart-container">
<canvas id="workTypeStatsChart"></canvas>
</div>
</div>
</div>
<div class="data-table">
<h3>🕐 최근 작업 현황</h3>
<div id="recentWorkTable">
<div class="loading">데이터를 불러오는 중...</div>
</div>
</div>
</div>
<script>
// 초기 설정
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];
let charts = {};
// API 호출 함수
async function apiCall(endpoint, params = {}) {
const baseUrl = document.getElementById('apiUrl').value;
const queryString = new URLSearchParams(params).toString();
const url = `${baseUrl}/api/work-analysis/${endpoint}${queryString ? '?' + queryString : ''}`;
try {
console.log('API 호출:', url);
const response = await fetch(url);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
return data;
} catch (error) {
console.error('API 호출 에러:', error);
throw error;
}
}
// 연결 테스트
async function testConnection() {
const statusDiv = document.getElementById('connectionStatus');
statusDiv.innerHTML = '<div class="loading">연결 테스트 중...</div>';
try {
const response = await apiCall('health');
statusDiv.innerHTML = `
<div class="success">
<span class="status-indicator status-online"></span>
연결 성공! 서버가 정상 작동 중입니다.
<br><small>응답: ${response.message}</small>
</div>
`;
} catch (error) {
statusDiv.innerHTML = `
<div class="error">
<span class="status-indicator status-offline"></span>
연결 실패: ${error.message}
<br><small>API URL과 서버 상태를 확인해주세요.</small>
</div>
`;
}
}
// 데이터 로드
async function loadData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
alert('시작일과 종료일을 모두 선택해주세요.');
return;
}
const params = { start: startDate, end: endDate };
try {
// 기본 통계 로드
await loadStats(params);
// 차트 데이터 로드
await loadCharts(params);
// 최근 작업 로드
await loadRecentWork(params);
} catch (error) {
console.error('데이터 로드 에러:', error);
document.getElementById('connectionStatus').innerHTML = `
<div class="error">
데이터 로드 실패: ${error.message}
</div>
`;
}
}
// 기본 통계 로드
async function loadStats(params) {
const response = await apiCall('stats', params);
const stats = response.data;
document.getElementById('totalHours').textContent = stats.totalHours || 0;
document.getElementById('totalReports').textContent = stats.totalReports || 0;
document.getElementById('activeProjects').textContent = stats.activeProjects || 0;
document.getElementById('errorRate').textContent = (stats.errorRate || 0).toFixed(1);
}
// 차트 로드
async function loadCharts(params) {
// 일별 추이 차트
const dailyTrendResponse = await apiCall('daily-trend', params);
createDailyTrendChart(dailyTrendResponse.data);
// 작업자별 통계 차트
const workerStatsResponse = await apiCall('worker-stats', params);
createWorkerStatsChart(workerStatsResponse.data);
// 프로젝트별 통계 차트
const projectStatsResponse = await apiCall('project-stats', params);
createProjectStatsChart(projectStatsResponse.data);
// 작업유형별 통계 차트
const workTypeStatsResponse = await apiCall('worktype-stats', params);
createWorkTypeStatsChart(workTypeStatsResponse.data);
}
// 일별 추이 차트 생성
function createDailyTrendChart(data) {
const ctx = document.getElementById('dailyTrendChart').getContext('2d');
if (charts.dailyTrend) {
charts.dailyTrend.destroy();
}
charts.dailyTrend = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(item => item.date),
datasets: [{
label: '작업시간',
data: data.map(item => item.hours),
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '시간'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
// 작업자별 통계 차트 생성
function createWorkerStatsChart(data) {
const ctx = document.getElementById('workerStatsChart').getContext('2d');
if (charts.workerStats) {
charts.workerStats.destroy();
}
charts.workerStats = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(item => `작업자 ${item.worker_id}`),
datasets: [{
label: '총 작업시간',
data: data.map(item => item.totalHours),
backgroundColor: 'rgba(118, 75, 162, 0.8)',
borderColor: '#764ba2',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '시간'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
// 프로젝트별 통계 차트 생성
function createProjectStatsChart(data) {
const ctx = document.getElementById('projectStatsChart').getContext('2d');
if (charts.projectStats) {
charts.projectStats.destroy();
}
charts.projectStats = new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.map(item => `프로젝트 ${item.project_id}`),
datasets: [{
data: data.map(item => item.totalHours),
backgroundColor: [
'#667eea',
'#764ba2',
'#f093fb',
'#f5576c',
'#4ecdc4',
'#45b7d1',
'#96ceb4',
'#ffeaa7'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// 작업유형별 통계 차트 생성
function createWorkTypeStatsChart(data) {
const ctx = document.getElementById('workTypeStatsChart').getContext('2d');
if (charts.workTypeStats) {
charts.workTypeStats.destroy();
}
charts.workTypeStats = new Chart(ctx, {
type: 'pie',
data: {
labels: data.map(item => `작업유형 ${item.work_type_id}`),
datasets: [{
data: data.map(item => item.totalHours),
backgroundColor: [
'#ff6b6b',
'#4ecdc4',
'#45b7d1',
'#96ceb4',
'#ffeaa7',
'#dda0dd',
'#98d8c8',
'#f7dc6f'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// 최근 작업 로드
async function loadRecentWork(params) {
const response = await apiCall('recent-work', { ...params, limit: 10 });
const data = response.data;
const tableHtml = `
<table>
<thead>
<tr>
<th>날짜</th>
<th>작업자</th>
<th>프로젝트</th>
<th>작업유형</th>
<th>작업시간</th>
<th>상태</th>
</tr>
</thead>
<tbody>
${data.map(item => `
<tr>
<td>${item.report_date}</td>
<td>작업자 ${item.worker_id}</td>
<td>프로젝트 ${item.project_id}</td>
<td>유형 ${item.work_type_id}</td>
<td>${item.work_hours}시간</td>
<td>${item.error_type_id > 0 ? '❌ 에러' : '✅ 정상'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('recentWorkTable').innerHTML = tableHtml;
}
// 페이지 로드 시 연결 테스트
window.onload = function() {
testConnection();
};
</script>
</body>
</html>