- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
890 lines
32 KiB
HTML
890 lines
32 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-color: #f5f7fa;
|
|
color: #333;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
margin-bottom: 30px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 28px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.date-selector {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.date-selector input {
|
|
padding: 8px 12px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.btn {
|
|
background: rgba(255,255,255,0.2);
|
|
color: white;
|
|
border: 1px solid rgba(255,255,255,0.3);
|
|
padding: 8px 16px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: rgba(255,255,255,0.3);
|
|
}
|
|
|
|
.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 2px 10px rgba(0,0,0,0.1);
|
|
text-align: center;
|
|
transition: transform 0.3s, box-shadow 0.3s;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.stat-change {
|
|
font-size: 12px;
|
|
margin-top: 5px;
|
|
padding: 3px 8px;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.stat-change.positive {
|
|
background: #e8f5e8;
|
|
color: #2e7d2e;
|
|
}
|
|
|
|
.stat-change.negative {
|
|
background: #ffeaea;
|
|
color: #c53030;
|
|
}
|
|
|
|
.charts-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 15px;
|
|
color: #333;
|
|
}
|
|
|
|
.chart-canvas {
|
|
position: relative;
|
|
height: 300px;
|
|
}
|
|
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.table-header {
|
|
background: #667eea;
|
|
color: white;
|
|
padding: 15px 20px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.table th,
|
|
.table td {
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.table th {
|
|
background: #f8f9fa;
|
|
font-weight: 600;
|
|
color: #555;
|
|
}
|
|
|
|
.table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-completed {
|
|
background: #e8f5e8;
|
|
color: #2e7d2e;
|
|
}
|
|
|
|
.status-progress {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.status-pending {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #666;
|
|
}
|
|
|
|
.error-message {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.debug-info {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.charts-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.date-selector {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>📊 일일 작업 현황 분석</h1>
|
|
<p>실시간 작업 현황과 주요 지표를 확인하세요</p>
|
|
<div class="date-selector">
|
|
<label>조회 기간:</label>
|
|
<input type="date" id="startDate">
|
|
<span>~</span>
|
|
<input type="date" id="endDate">
|
|
<button class="btn" onclick="loadData()">조회</button>
|
|
<button class="btn" onclick="setToday()">오늘</button>
|
|
<button class="btn" onclick="setThisWeek()">이번주</button>
|
|
<button class="btn" onclick="setThisMonth()">이번달</button>
|
|
<button class="btn" onclick="toggleDebug()">디버그</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="errorContainer"></div>
|
|
<div id="debugInfo" class="debug-info"></div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalHours">0</div>
|
|
<div class="stat-label">총 작업시간</div>
|
|
<div class="stat-change positive" id="hoursChange">+0%</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalReports">0</div>
|
|
<div class="stat-label">보고서 건수</div>
|
|
<div class="stat-change positive" id="reportsChange">+0%</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="activeProjects">0</div>
|
|
<div class="stat-label">진행 프로젝트</div>
|
|
<div class="stat-change" id="projectsChange">+0%</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="errorRate">0%</div>
|
|
<div class="stat-label">에러율</div>
|
|
<div class="stat-change negative" id="errorChange">+0%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charts-grid">
|
|
<div class="chart-container">
|
|
<div class="chart-title">📈 일별 작업시간 추이</div>
|
|
<div class="chart-canvas">
|
|
<canvas id="dailyHoursChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<div class="chart-title">👥 작업자별 작업량</div>
|
|
<div class="chart-canvas">
|
|
<canvas id="workerChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charts-grid">
|
|
<div class="chart-container">
|
|
<div class="chart-title">🏗️ 프로젝트별 투입시간</div>
|
|
<div class="chart-canvas">
|
|
<canvas id="projectChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<div class="chart-title">⚙️ 작업 유형별 분포</div>
|
|
<div class="chart-canvas">
|
|
<canvas id="workTypeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<div class="table-header">🔍 최근 작업 현황</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>날짜</th>
|
|
<th>작업자</th>
|
|
<th>프로젝트</th>
|
|
<th>작업유형</th>
|
|
<th>작업시간</th>
|
|
<th>상태</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="recentWorkTable">
|
|
<tr>
|
|
<td colspan="6" class="loading">데이터를 불러오는 중...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 전역 변수
|
|
let API_URL = 'http://192.168.0.3:3005';
|
|
let dailyChart, workerChart, projectChart, workTypeChart;
|
|
let debugMode = false;
|
|
|
|
// 디버그 함수
|
|
function log(message, data = null) {
|
|
console.log(message, data);
|
|
if (debugMode) {
|
|
const debugDiv = document.getElementById('debugInfo');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
debugDiv.innerHTML += `[${timestamp}] ${message}${data ? ': ' + JSON.stringify(data, null, 2) : ''}<br>`;
|
|
debugDiv.scrollTop = debugDiv.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function toggleDebug() {
|
|
debugMode = !debugMode;
|
|
const debugDiv = document.getElementById('debugInfo');
|
|
debugDiv.style.display = debugMode ? 'block' : 'none';
|
|
if (debugMode) {
|
|
debugDiv.innerHTML = '=== 디버그 모드 활성화 ===<br>';
|
|
}
|
|
}
|
|
|
|
function showError(message, details = null) {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
errorContainer.innerHTML = `
|
|
<div class="error-message">
|
|
<strong>오류:</strong> ${message}
|
|
${details ? `<br><small>${details}</small>` : ''}
|
|
</div>
|
|
`;
|
|
log('ERROR: ' + message, details);
|
|
}
|
|
|
|
function clearError() {
|
|
document.getElementById('errorContainer').innerHTML = '';
|
|
}
|
|
|
|
// API 설정 초기화
|
|
async function initializeAPI() {
|
|
log('API 초기화 시작');
|
|
|
|
// 기존 API 설정 파일 시도
|
|
try {
|
|
const module = await import('/js/api-config.js');
|
|
if (module.API && module.API.BASE_URL) {
|
|
API_URL = module.API.BASE_URL;
|
|
log('API config 파일에서 설정 로드', API_URL);
|
|
} else {
|
|
log('API config 파일에서 BASE_URL을 찾을 수 없음');
|
|
}
|
|
} catch (error) {
|
|
log('API config 파일 로드 실패, 기본값 사용', error.message);
|
|
}
|
|
|
|
// API 연결 테스트
|
|
await testAPIConnection();
|
|
}
|
|
|
|
// API 연결 테스트 함수
|
|
async function testAPIConnection() {
|
|
const testUrls = [
|
|
{ url: 'http://192.168.0.3:3005/api', hasApi: false }, // 직접 연결 우선
|
|
{ url: 'http://192.168.0.3:3001', hasApi: true } // nginx 프록시
|
|
];
|
|
|
|
for (const testConfig of testUrls) {
|
|
try {
|
|
const healthUrl = testConfig.hasApi
|
|
? `${testConfig.url}/health`
|
|
: `${testConfig.url}/health`;
|
|
|
|
log(`API 연결 테스트: ${healthUrl}`);
|
|
const response = await fetch(healthUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.text();
|
|
// HTML 응답이면 실패로 간주
|
|
if (data.includes('<!DOCTYPE html>')) {
|
|
log(`❌ HTML 응답 (로그인 페이지): ${healthUrl}`);
|
|
continue;
|
|
}
|
|
|
|
API_URL = testConfig.hasApi ? testConfig.url : testConfig.url;
|
|
log(`✅ API 연결 성공: ${API_URL}`);
|
|
return;
|
|
} else {
|
|
log(`❌ API 연결 실패 (${response.status}): ${healthUrl}`);
|
|
}
|
|
} catch (error) {
|
|
log(`❌ API 연결 오류: ${testConfig.url}`, error.message);
|
|
}
|
|
}
|
|
|
|
// 모든 URL 실패 시 직접 연결 사용
|
|
API_URL = 'http://192.168.0.3:3005/api';
|
|
log(`⚠️ 모든 연결 실패, 직접 연결 사용: ${API_URL}`);
|
|
}
|
|
|
|
// 차트 초기화
|
|
function initializeCharts() {
|
|
log('차트 초기화 시작');
|
|
|
|
try {
|
|
// 일별 작업시간 차트
|
|
const dailyCtx = document.getElementById('dailyHoursChart').getContext('2d');
|
|
dailyChart = new Chart(dailyCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: '작업시간',
|
|
data: [],
|
|
borderColor: '#667eea',
|
|
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '시간'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 작업자별 차트
|
|
const workerCtx = document.getElementById('workerChart').getContext('2d');
|
|
workerChart = new Chart(workerCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: [
|
|
'#667eea',
|
|
'#764ba2',
|
|
'#f093fb',
|
|
'#f5576c',
|
|
'#4facfe',
|
|
'#43e97b'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 프로젝트별 차트
|
|
const projectCtx = document.getElementById('projectChart').getContext('2d');
|
|
projectChart = new Chart(projectCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: '투입시간',
|
|
data: [],
|
|
backgroundColor: '#667eea'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '시간'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 작업유형별 차트
|
|
const workTypeCtx = document.getElementById('workTypeChart').getContext('2d');
|
|
workTypeChart = new Chart(workTypeCtx, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: [
|
|
'#667eea',
|
|
'#764ba2',
|
|
'#f093fb',
|
|
'#f5576c',
|
|
'#4facfe',
|
|
'#43e97b',
|
|
'#f6d55c',
|
|
'#ed4a7b'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
log('차트 초기화 완료');
|
|
} catch (error) {
|
|
log('차트 초기화 실패', error);
|
|
showError('차트 초기화에 실패했습니다.', error.message);
|
|
}
|
|
}
|
|
|
|
// 날짜 설정 함수들
|
|
function setToday() {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.getElementById('startDate').value = today;
|
|
document.getElementById('endDate').value = today;
|
|
log('오늘 날짜 설정', today);
|
|
loadData();
|
|
}
|
|
|
|
function setThisWeek() {
|
|
const today = new Date();
|
|
const monday = new Date(today.setDate(today.getDate() - today.getDay() + 1));
|
|
const sunday = new Date(today.setDate(today.getDate() - today.getDay() + 7));
|
|
|
|
const mondayStr = monday.toISOString().split('T')[0];
|
|
const sundayStr = sunday.toISOString().split('T')[0];
|
|
|
|
document.getElementById('startDate').value = mondayStr;
|
|
document.getElementById('endDate').value = sundayStr;
|
|
log('이번주 날짜 설정', { start: mondayStr, end: sundayStr });
|
|
loadData();
|
|
}
|
|
|
|
function setThisMonth() {
|
|
const today = new Date();
|
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
|
|
const firstDayStr = firstDay.toISOString().split('T')[0];
|
|
const lastDayStr = lastDay.toISOString().split('T')[0];
|
|
|
|
document.getElementById('startDate').value = firstDayStr;
|
|
document.getElementById('endDate').value = lastDayStr;
|
|
log('이번달 날짜 설정', { start: firstDayStr, end: lastDayStr });
|
|
loadData();
|
|
}
|
|
|
|
// 토큰 확인 함수
|
|
function checkToken() {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
showError('로그인이 필요합니다.');
|
|
setTimeout(() => {
|
|
location.href = '/login';
|
|
}, 2000);
|
|
return null;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
// API 호출 헬퍼 함수
|
|
async function makeAPICall(url, options = {}) {
|
|
log('API 호출', url);
|
|
|
|
const token = checkToken();
|
|
if (!token) return null;
|
|
|
|
const defaultOptions = {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
};
|
|
|
|
const finalOptions = { ...defaultOptions, ...options };
|
|
log('요청 옵션', finalOptions);
|
|
|
|
try {
|
|
const response = await fetch(url, finalOptions);
|
|
log('응답 상태', { status: response.status, statusText: response.statusText });
|
|
|
|
const responseText = await response.text();
|
|
log('응답 내용 (텍스트)', responseText);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${responseText}`);
|
|
}
|
|
|
|
const data = JSON.parse(responseText);
|
|
log('파싱된 데이터', data);
|
|
|
|
return data;
|
|
} catch (error) {
|
|
log('API 호출 실패', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 데이터 로딩
|
|
async function loadData() {
|
|
clearError();
|
|
log('데이터 로딩 시작');
|
|
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
showError('조회 기간을 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
log('조회 기간', { startDate, endDate });
|
|
|
|
try {
|
|
// 기본 통계 조회
|
|
log('기본 통계 조회 시작');
|
|
const statsUrl = `${API_URL}/work-analysis/stats?start=${startDate}&end=${endDate}`;
|
|
const statsData = await makeAPICall(statsUrl);
|
|
|
|
if (statsData) {
|
|
updateStats(statsData.data || statsData);
|
|
}
|
|
|
|
// 일별 추이 데이터
|
|
log('일별 추이 조회 시작');
|
|
const dailyUrl = `${API_URL}/work-analysis/daily-trend?start=${startDate}&end=${endDate}`;
|
|
const dailyData = await makeAPICall(dailyUrl);
|
|
|
|
if (dailyData) {
|
|
updateDailyChart(dailyData.data || dailyData);
|
|
}
|
|
|
|
// 작업자별 데이터
|
|
log('작업자별 통계 조회 시작');
|
|
const workerUrl = `${API_URL}/work-analysis/worker-stats?start=${startDate}&end=${endDate}`;
|
|
const workerData = await makeAPICall(workerUrl);
|
|
|
|
if (workerData) {
|
|
updateWorkerChart(workerData.data || workerData);
|
|
}
|
|
|
|
// 프로젝트별 데이터
|
|
log('프로젝트별 통계 조회 시작');
|
|
const projectUrl = `${API_URL}/work-analysis/project-stats?start=${startDate}&end=${endDate}`;
|
|
const projectData = await makeAPICall(projectUrl);
|
|
|
|
if (projectData) {
|
|
updateProjectChart(projectData.data || projectData);
|
|
}
|
|
|
|
// 작업유형별 데이터
|
|
log('작업유형별 통계 조회 시작');
|
|
const workTypeUrl = `${API_URL}/work-analysis/worktype-stats?start=${startDate}&end=${endDate}`;
|
|
const workTypeData = await makeAPICall(workTypeUrl);
|
|
|
|
if (workTypeData) {
|
|
updateWorkTypeChart(workTypeData.data || workTypeData);
|
|
}
|
|
|
|
// 최근 작업 현황
|
|
log('최근 작업 현황 조회 시작');
|
|
const recentUrl = `${API_URL}/work-analysis/recent-work?start=${startDate}&end=${endDate}&limit=10`;
|
|
const recentData = await makeAPICall(recentUrl);
|
|
|
|
if (recentData) {
|
|
updateRecentWorkTable(recentData.data || recentData);
|
|
}
|
|
|
|
log('모든 데이터 로딩 완료');
|
|
|
|
} catch (error) {
|
|
log('데이터 로딩 오류', error);
|
|
|
|
if (error.message.includes('403')) {
|
|
showError('접근 권한이 없습니다. 관리자에게 문의하세요.');
|
|
} else if (error.message.includes('401')) {
|
|
showError('로그인이 만료되었습니다. 다시 로그인해주세요.');
|
|
setTimeout(() => {
|
|
location.href = '/login';
|
|
}, 2000);
|
|
} else {
|
|
showError('데이터를 불러오는 중 오류가 발생했습니다.', error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 통계 업데이트
|
|
function updateStats(stats) {
|
|
log('통계 업데이트', stats);
|
|
|
|
document.getElementById('totalHours').textContent = (stats.totalHours || 0).toFixed(1);
|
|
document.getElementById('totalReports').textContent = stats.totalReports || 0;
|
|
document.getElementById('activeProjects').textContent = stats.activeProjects || 0;
|
|
document.getElementById('errorRate').textContent = (stats.errorRate || 0).toFixed(1) + '%';
|
|
|
|
// 변화율 표시 (임시 데이터)
|
|
document.getElementById('hoursChange').textContent = '+12.5%';
|
|
document.getElementById('reportsChange').textContent = '+8.3%';
|
|
document.getElementById('projectsChange').textContent = '+2';
|
|
document.getElementById('errorChange').textContent = '-0.5%';
|
|
}
|
|
|
|
// 차트 업데이트 함수들
|
|
function updateDailyChart(data) {
|
|
log('일별 차트 업데이트', data);
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
log('일별 차트 데이터 없음');
|
|
return;
|
|
}
|
|
|
|
dailyChart.data.labels = data.map(item => item.date);
|
|
dailyChart.data.datasets[0].data = data.map(item => parseFloat(item.hours) || 0);
|
|
dailyChart.update();
|
|
}
|
|
|
|
function updateWorkerChart(data) {
|
|
log('작업자 차트 업데이트', data);
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
log('작업자 차트 데이터 없음');
|
|
return;
|
|
}
|
|
|
|
workerChart.data.labels = data.map(item => item.workerName || `작업자 ${item.worker_id}`);
|
|
workerChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
|
|
workerChart.update();
|
|
}
|
|
|
|
function updateProjectChart(data) {
|
|
log('프로젝트 차트 업데이트', data);
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
log('프로젝트 차트 데이터 없음');
|
|
return;
|
|
}
|
|
|
|
projectChart.data.labels = data.map(item => item.projectName || `프로젝트 ${item.project_id}`);
|
|
projectChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
|
|
projectChart.update();
|
|
}
|
|
|
|
function updateWorkTypeChart(data) {
|
|
log('작업유형 차트 업데이트', data);
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
log('작업유형 차트 데이터 없음');
|
|
return;
|
|
}
|
|
|
|
workTypeChart.data.labels = data.map(item => item.workTypeName || `작업유형 ${item.work_type_id}`);
|
|
workTypeChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
|
|
workTypeChart.update();
|
|
}
|
|
|
|
// 테이블 업데이트
|
|
function updateRecentWorkTable(data) {
|
|
log('테이블 업데이트', data);
|
|
|
|
const tbody = document.getElementById('recentWorkTable');
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #666;">조회된 데이터가 없습니다.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = 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><span class="status-badge ${getStatusClass(item.work_status_id)}">${getStatusText(item.work_status_id)}</span></td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function getStatusClass(statusId) {
|
|
switch(statusId) {
|
|
case 1: return 'status-completed';
|
|
case 2: return 'status-progress';
|
|
default: return 'status-pending';
|
|
}
|
|
}
|
|
|
|
function getStatusText(statusId) {
|
|
switch(statusId) {
|
|
case 1: return '완료';
|
|
case 2: return '진행중';
|
|
default: return '대기';
|
|
}
|
|
}
|
|
|
|
// 페이지 초기화
|
|
async function initializePage() {
|
|
log('페이지 초기화 시작');
|
|
|
|
try {
|
|
await initializeAPI();
|
|
initializeCharts();
|
|
setToday(); // 오늘 날짜로 초기 설정 및 데이터 로드
|
|
|
|
log('페이지 초기화 완료');
|
|
} catch (error) {
|
|
log('페이지 초기화 실패', error);
|
|
showError('페이지 초기화에 실패했습니다.', error.message);
|
|
}
|
|
}
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
</script>
|
|
</body>
|
|
</html> |