Files
TK-FB-Project/web-ui/pages.backup.20260202/.archived-daily-work-analysis.html
Hyungi Ahn 74d3a78aa3 feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: 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>
2026-02-02 14:27:22 +09:00

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>