fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선

- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산
- 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader)
- 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql)
- synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
Hyungi Ahn
2025-12-02 13:08:44 +09:00
parent beaffcad49
commit a9bce9d20b
419 changed files with 275129 additions and 394 deletions

View File

@@ -0,0 +1,890 @@
<!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>

View File

@@ -0,0 +1,672 @@
<!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://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.project-card {
transition: all 0.3s ease;
border-left: 4px solid #3B82F6;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.work-type-row {
transition: background-color 0.2s ease;
}
.work-type-row:hover {
background-color: #f8fafc;
}
.error-high { border-left-color: #EF4444; }
.error-medium { border-left-color: #F59E0B; }
.error-low { border-left-color: #10B981; }
.progress-bar {
transition: width 0.8s ease-in-out;
}
.loading-spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-card.error {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.regular {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card.total {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 헤더 -->
<header class="bg-white shadow-lg border-b">
<div class="container mx-auto px-6 py-4">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">🏗️ 프로젝트별 작업 시간 분석</h1>
<p class="text-gray-600 mt-1">총시간 · 정규시간 · 에러시간 상세 분석</p>
<div class="mt-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
🔧 시스템 관리자 전용
</span>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-500">
<span id="last-updated">마지막 업데이트: -</span>
</div>
<button id="refresh-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">
🔄 새로고침
</button>
<button onclick="history.back()" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
← 뒤로가기
</button>
</div>
</div>
</div>
</header>
<!-- 날짜 선택 -->
<div class="container mx-auto px-6 py-6">
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center space-x-2">
<label for="start-date" class="text-sm font-medium text-gray-700">시작일:</label>
<input type="date" id="start-date" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<div class="flex items-center space-x-2">
<label for="end-date" class="text-sm font-medium text-gray-700">종료일:</label>
<input type="date" id="end-date" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<button id="analyze-btn" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-md text-sm font-medium transition-colors">
📊 분석 실행
</button>
<div class="flex items-center space-x-2">
<button id="preset-week" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">최근 1주일</button>
<button id="preset-month" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">최근 1개월</button>
<button id="preset-august" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">8월 전체</button>
</div>
</div>
</div>
</div>
<!-- 로딩 화면 -->
<div id="loading" class="hidden fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
<div class="text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-600">데이터를 분석하는 중...</p>
</div>
</div>
<!-- 메인 컨테이너 -->
<div class="container mx-auto px-6 pb-8" id="main-content">
<!-- 전체 요약 통계 -->
<div id="summary-stats" class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 hidden">
<div class="stat-card total rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="total-hours">-</div>
<div class="text-sm opacity-90">총 작업시간</div>
</div>
<div class="stat-card regular rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="regular-hours">-</div>
<div class="text-sm opacity-90">정규 시간</div>
</div>
<div class="stat-card error rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="error-hours">-</div>
<div class="text-sm opacity-90">에러 시간</div>
</div>
<div class="stat-card rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="error-rate">-</div>
<div class="text-sm opacity-90">전체 에러율</div>
</div>
</div>
<!-- 차트 섹션 -->
<div id="charts-section" class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 hidden">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-4">프로젝트별 시간 분포</h3>
<canvas id="project-chart"></canvas>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-4">에러율 분석</h3>
<canvas id="error-chart"></canvas>
</div>
</div>
<!-- 프로젝트별 상세 데이터 -->
<div id="projects-container" class="space-y-6">
<!-- 프로젝트 카드들이 여기에 동적으로 생성됩니다 -->
</div>
<!-- 데이터 없음 메시지 -->
<div id="no-data" class="hidden text-center py-12">
<div class="text-gray-400 text-6xl mb-4">📊</div>
<h3 class="text-xl font-semibold text-gray-600 mb-2">분석할 데이터가 없습니다</h3>
<p class="text-gray-500">날짜 범위를 선택하고 분석을 실행해주세요.</p>
</div>
</div>
<!-- 기존 인증 시스템 사용 -->
<script>
// 전역 변수
let analysisData = null;
let charts = {};
// API 호출 함수 (토큰 포함)
async function apiCall(endpoint, options = {}) {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('인증 토큰이 없습니다.');
}
const defaultHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const response = await fetch(`http://localhost:20005/api${endpoint}`, {
...options,
headers: {
...defaultHeaders,
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
}
// DOM 요소들
const elements = {
startDate: document.getElementById('start-date'),
endDate: document.getElementById('end-date'),
analyzeBtn: document.getElementById('analyze-btn'),
refreshBtn: document.getElementById('refresh-btn'),
loading: document.getElementById('loading'),
mainContent: document.getElementById('main-content'),
summaryStats: document.getElementById('summary-stats'),
chartsSection: document.getElementById('charts-section'),
projectsContainer: document.getElementById('projects-container'),
noData: document.getElementById('no-data'),
lastUpdated: document.getElementById('last-updated'),
presetWeek: document.getElementById('preset-week'),
presetMonth: document.getElementById('preset-month'),
presetAugust: document.getElementById('preset-august')
};
// 초기화
document.addEventListener('DOMContentLoaded', function() {
// 로그인 확인
const token = localStorage.getItem('token');
if (!token) {
alert('로그인이 필요합니다.');
window.location.href = '/index.html';
return;
}
// 사용자 정보 및 권한 확인
const userStr = localStorage.getItem('user');
if (!userStr) {
alert('사용자 정보를 찾을 수 없습니다.');
window.location.href = '/index.html';
return;
}
const user = JSON.parse(userStr);
// 시스템 권한 확인 (system 역할만 접근 가능)
if (user.role !== 'system') {
alert('시스템 관리자 권한이 필요합니다.');
window.location.href = '/pages/dashboard/user.html'; // 일반 사용자 대시보드로 리디렉션
return;
}
console.log('시스템 관리자 인증 완료:', user.name || user.username);
initializeDateInputs();
bindEventListeners();
// 8월 전체를 기본값으로 설정
setDatePreset('august');
});
// 날짜 입력 초기화
function initializeDateInputs() {
const today = new Date();
const oneMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
elements.endDate.value = today.toISOString().split('T')[0];
elements.startDate.value = oneMonthAgo.toISOString().split('T')[0];
}
// 이벤트 리스너 바인딩
function bindEventListeners() {
elements.analyzeBtn.addEventListener('click', performAnalysis);
elements.refreshBtn.addEventListener('click', performAnalysis);
// 날짜 프리셋 버튼들
elements.presetWeek.addEventListener('click', () => setDatePreset('week'));
elements.presetMonth.addEventListener('click', () => setDatePreset('month'));
elements.presetAugust.addEventListener('click', () => setDatePreset('august'));
// Enter 키로 분석 실행
elements.startDate.addEventListener('keypress', handleEnterKey);
elements.endDate.addEventListener('keypress', handleEnterKey);
}
// Enter 키 처리
function handleEnterKey(event) {
if (event.key === 'Enter') {
performAnalysis();
}
}
// 날짜 프리셋 설정
function setDatePreset(preset) {
const today = new Date();
let startDate, endDate;
switch (preset) {
case 'week':
startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
endDate = today;
break;
case 'month':
startDate = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
endDate = today;
break;
case 'august':
startDate = new Date(2025, 7, 1); // 2025년 8월 1일
endDate = new Date(2025, 7, 31); // 2025년 8월 31일
break;
}
elements.startDate.value = startDate.toISOString().split('T')[0];
elements.endDate.value = endDate.toISOString().split('T')[0];
}
// 분석 실행
async function performAnalysis() {
const startDate = elements.startDate.value;
const endDate = elements.endDate.value;
if (!startDate || !endDate) {
alert('시작일과 종료일을 모두 선택해주세요.');
return;
}
if (new Date(startDate) > new Date(endDate)) {
alert('시작일이 종료일보다 늦을 수 없습니다.');
return;
}
// 로그인 확인
const token = localStorage.getItem('token');
if (!token) {
alert('로그인이 필요합니다.');
window.location.href = '/index.html';
return;
}
showLoading(true);
try {
const response = await apiCall(`/work-analysis/project-worktype-analysis?start=${startDate}&end=${endDate}`);
const result = await response.json();
if (result.success) {
analysisData = result.data;
renderAnalysisResults();
updateLastUpdated();
} else {
throw new Error(result.error || '데이터 조회에 실패했습니다.');
}
} catch (error) {
console.error('분석 실패:', error);
showError(`분석 실패: ${error.message}`);
} finally {
showLoading(false);
}
}
// 로딩 표시/숨김
function showLoading(show) {
elements.loading.classList.toggle('hidden', !show);
}
// 에러 표시
function showError(message) {
alert(message);
}
// 분석 결과 렌더링
function renderAnalysisResults() {
if (!analysisData || !analysisData.projects || analysisData.projects.length === 0) {
showNoData();
return;
}
hideNoData();
renderSummaryStats();
renderCharts();
renderProjectCards();
}
// 데이터 없음 표시
function showNoData() {
elements.summaryStats.classList.add('hidden');
elements.chartsSection.classList.add('hidden');
elements.projectsContainer.innerHTML = '';
elements.noData.classList.remove('hidden');
}
// 데이터 없음 숨김
function hideNoData() {
elements.noData.classList.add('hidden');
elements.summaryStats.classList.remove('hidden');
elements.chartsSection.classList.remove('hidden');
}
// 요약 통계 렌더링
function renderSummaryStats() {
const summary = analysisData.summary;
document.getElementById('total-hours').textContent = `${(summary.grand_total_hours || 0).toFixed(1)}h`;
document.getElementById('regular-hours').textContent = `${(summary.grand_regular_hours || 0).toFixed(1)}h`;
document.getElementById('error-hours').textContent = `${(summary.grand_error_hours || 0).toFixed(1)}h`;
document.getElementById('error-rate').textContent = `${summary.grand_error_rate || 0}%`;
}
// 차트 렌더링
function renderCharts() {
renderProjectChart();
renderErrorChart();
}
// 프로젝트별 시간 분포 차트
function renderProjectChart() {
const ctx = document.getElementById('project-chart').getContext('2d');
// 기존 차트 삭제
if (charts.project) {
charts.project.destroy();
}
const projects = analysisData.projects;
const labels = projects.map(p => p.project_name || 'Unknown Project');
const totalHours = projects.map(p => p.total_project_hours || 0);
const regularHours = projects.map(p => p.total_regular_hours || 0);
const errorHours = projects.map(p => p.total_error_hours || 0);
charts.project = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '정규 시간',
data: regularHours,
backgroundColor: '#10B981',
borderColor: '#059669',
borderWidth: 1
},
{
label: '에러 시간',
data: errorHours,
backgroundColor: '#EF4444',
borderColor: '#DC2626',
borderWidth: 1
}
]
},
options: {
responsive: true,
scales: {
x: {
stacked: true,
ticks: {
maxRotation: 45
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: '시간 (h)'
}
}
},
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
footer: function(tooltipItems) {
const index = tooltipItems[0].dataIndex;
const total = totalHours[index];
return `총 시간: ${total.toFixed(1)}h`;
}
}
}
}
}
});
}
// 에러율 분석 차트
function renderErrorChart() {
const ctx = document.getElementById('error-chart').getContext('2d');
// 기존 차트 삭제
if (charts.error) {
charts.error.destroy();
}
const projects = analysisData.projects;
const labels = projects.map(p => p.project_name || 'Unknown Project');
const errorRates = projects.map(p => p.project_error_rate || 0);
// 에러율에 따른 색상 결정
const colors = errorRates.map(rate => {
if (rate >= 10) return '#EF4444'; // 높음 (빨강)
if (rate >= 5) return '#F59E0B'; // 중간 (주황)
return '#10B981'; // 낮음 (초록)
});
charts.error = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: errorRates,
backgroundColor: colors,
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed}%`;
}
}
}
}
}
});
}
// 프로젝트 카드 렌더링
function renderProjectCards() {
const container = elements.projectsContainer;
container.innerHTML = '';
analysisData.projects.forEach(project => {
const card = createProjectCard(project);
container.appendChild(card);
});
}
// 프로젝트 카드 생성
function createProjectCard(project) {
const card = document.createElement('div');
// 에러율에 따른 카드 스타일 결정
let errorClass = 'error-low';
const errorRate = project.project_error_rate || 0;
if (errorRate >= 10) errorClass = 'error-high';
else if (errorRate >= 5) errorClass = 'error-medium';
card.className = `project-card ${errorClass} bg-white rounded-lg shadow-md p-6`;
// 프로젝트 헤더
const header = `
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-gray-800">${project.project_name || 'Unknown Project'}</h3>
<p class="text-sm text-gray-600">Job No: ${project.job_no || 'N/A'}</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-blue-600">${(project.total_project_hours || 0).toFixed(1)}h</div>
<div class="text-sm text-gray-500">총 시간</div>
</div>
</div>
`;
// 프로젝트 요약 통계
const summary = `
<div class="grid grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 rounded-lg">
<div class="text-center">
<div class="text-lg font-semibold text-green-600">${(project.total_regular_hours || 0).toFixed(1)}h</div>
<div class="text-xs text-gray-600">정규 시간</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-red-600">${(project.total_error_hours || 0).toFixed(1)}h</div>
<div class="text-xs text-gray-600">에러 시간</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-purple-600">${errorRate}%</div>
<div class="text-xs text-gray-600">에러율</div>
</div>
</div>
`;
// 작업 유형별 테이블
let workTypesTable = `
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">작업 유형</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">총 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">정규 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">에러 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">에러율</th>
<th class="px-4 py-2 text-center text-xs font-medium text-gray-500 uppercase">진행률</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
`;
if (project.work_types && project.work_types.length > 0) {
project.work_types.forEach(workType => {
const totalHours = workType.total_hours || 0;
const regularHours = workType.regular_hours || 0;
const errorHours = workType.error_hours || 0;
const errorRatePercent = workType.error_rate_percent || 0;
const regularPercent = totalHours > 0 ? (regularHours / totalHours) * 100 : 0;
const errorPercent = totalHours > 0 ? (errorHours / totalHours) * 100 : 0;
workTypesTable += `
<tr class="work-type-row">
<td class="px-4 py-3 text-sm font-medium text-gray-900">${workType.work_type_name || 'Unknown'}</td>
<td class="px-4 py-3 text-sm text-right font-semibold">${totalHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right text-green-600">${regularHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right text-red-600">${errorHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right font-medium ${errorRatePercent >= 10 ? 'text-red-600' : errorRatePercent >= 5 ? 'text-yellow-600' : 'text-green-600'}">${errorRatePercent}%</td>
<td class="px-4 py-3">
<div class="flex items-center space-x-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full progress-bar" style="width: ${regularPercent}%"></div>
</div>
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div class="bg-red-500 h-2 rounded-full progress-bar" style="width: ${errorPercent}%"></div>
</div>
</div>
</td>
</tr>
`;
});
} else {
workTypesTable += `
<tr>
<td colspan="6" class="px-4 py-3 text-center text-gray-500">작업 유형 데이터가 없습니다.</td>
</tr>
`;
}
workTypesTable += `
</tbody>
</table>
</div>
`;
card.innerHTML = header + summary + workTypesTable;
return card;
}
// 마지막 업데이트 시간 갱신
function updateLastUpdated() {
const now = new Date();
const timeString = now.toLocaleString('ko-KR');
elements.lastUpdated.textContent = `마지막 업데이트: ${timeString}`;
}
// 유틸리티 함수들
function formatNumber(num) {
return new Intl.NumberFormat('ko-KR').format(num);
}
function formatHours(hours) {
return `${hours.toFixed(1)}시간`;
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
<!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="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/work-analysis.css?v=42">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js?v=1" defer></script>
<script src="/js/api-config.js?v=1" defer></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
</head>
<body>
<div class="analysis-container">
<!-- 페이지 헤더 -->
<header class="page-header fade-in">
<h1 class="page-title">
<span class="icon">📊</span>
작업 분석
</h1>
<p class="page-subtitle">기간별/프로젝트별 작업 현황을 분석하고 통계를 확인합니다</p>
</header>
<!-- 분석 모드 탭 -->
<nav class="analysis-tabs fade-in">
<button class="tab-button active" data-mode="period">
📅 기간별 분석
</button>
<button class="tab-button" data-mode="project">
🏗️ 프로젝트별 분석
</button>
</nav>
<!-- 분석 조건 설정 -->
<section class="analysis-controls fade-in">
<div class="controls-grid">
<!-- 기간 설정 -->
<div class="form-group">
<label class="form-label" for="startDate">
<span class="icon">📅</span>
시작일
</label>
<input type="date" id="startDate" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label" for="endDate">
<span class="icon">📅</span>
종료일
</label>
<input type="date" id="endDate" class="form-input" required>
</div>
<!-- 기간 확정 버튼 -->
<div class="form-group">
<button class="confirm-period-button" id="confirmPeriodBtn">
<span class="icon"></span>
기간 확정
</button>
</div>
<!-- 기간 상태 표시 -->
<div class="form-group" id="periodStatusGroup" style="display: none;">
<div class="period-status">
<span class="icon"></span>
<div>
<div style="font-size: 0.8rem; opacity: 0.8; margin-bottom: 2px;">분석 기간</div>
<div id="periodStatus">기간이 설정되지 않았습니다</div>
</div>
</div>
</div>
</div>
</section>
<!-- 분석 결과 영역 -->
<main id="analysisResults" class="fade-in">
<!-- 로딩 상태 -->
<div id="loadingState" class="loading-container" style="display: none;">
<div class="loading-spinner"></div>
<p class="loading-text">분석 중입니다...</p>
</div>
<!-- 분석 탭 네비게이션 -->
<div id="analysisTabNavigation" class="tab-navigation" style="display: none;">
<div class="tab-buttons">
<button class="tab-button active" data-tab="work-status">
<span class="icon">📈</span>
기간별 작업 현황
</button>
<button class="tab-button" data-tab="project-distribution">
<span class="icon">🥧</span>
프로젝트별 분포
</button>
<button class="tab-button" data-tab="worker-performance">
<span class="icon">👤</span>
작업자별 성과
</button>
<button class="tab-button" data-tab="error-analysis">
<span class="icon">⚠️</span>
오류 분석
</button>
</div>
</div>
<!-- 결과 카드 그리드 -->
<div id="resultsGrid" class="results-grid" style="display: none;">
<!-- 통계 카드들 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-label">총 작업시간</div>
<div class="stat-value" id="totalHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-label">정상 시간</div>
<div class="stat-value" id="normalHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚠️</div>
<div class="stat-content">
<div class="stat-label">오류 시간</div>
<div class="stat-value" id="errorHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-label">참여 작업자</div>
<div class="stat-value" id="workerCount">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-content">
<div class="stat-label">오류율</div>
<div class="stat-value" id="errorRate">0%</div>
</div>
</div>
</div>
<!-- 분석 탭 컨텐츠 -->
<div class="tab-contents">
<!-- 기간별 작업 현황 -->
<div id="work-status-tab" class="tab-content active">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">📈</span>
기간별 작업 현황
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkStatus()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<!-- 테이블이 동적으로 생성됩니다 -->
</div>
</div>
</div>
<!-- 프로젝트별 분포 -->
<div id="project-distribution-tab" class="tab-content">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">🥧</span>
프로젝트별 분포
</h3>
<button class="chart-analyze-btn" onclick="analyzeProjectDistribution()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="production-report-table">
<thead>
<tr>
<th class="job-no-header">Job No.</th>
<th class="work-content-header">작업내용</th>
<th class="man-days-header">공수</th>
<th class="load-rate-header">전체 부하율</th>
<th class="labor-cost-header">인건비</th>
</tr>
</thead>
<tbody id="projectDistributionTableBody">
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
분석을 실행해주세요
</td>
</tr>
</tbody>
<tfoot id="projectDistributionTableFooter" style="display: none;">
<tr class="total-row">
<td colspan="2"><strong>총계</strong></td>
<td><strong id="totalManDays">0</strong></td>
<td><strong>100%</strong></td>
<td><strong id="totalLaborCost">₩0</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- 작업자별 성과 -->
<div id="worker-performance-tab" class="tab-content">
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">👤</span>
작업자별 성과
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<canvas id="workerPerformanceChart"></canvas>
</div>
</div>
<!-- 오류 분석 -->
<div id="error-analysis-tab" class="tab-content">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">⚠️</span>
오류 분석
</h3>
<button class="chart-analyze-btn" onclick="analyzeErrorAnalysis()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="error-analysis-table">
<thead>
<tr>
<th>Job No.</th>
<th>작업내용</th>
<th>총 시간</th>
<th>세부시간</th>
<th>작업 타입</th>
<th>오류율</th>
</tr>
</thead>
<tbody id="errorAnalysisTableBody">
<tr>
<td colspan="6" style="text-align: center; padding: 2rem; color: #666;">
분석을 실행해주세요
</td>
</tr>
</tbody>
<tfoot id="errorAnalysisTableFooter" style="display: none;">
<tr class="total-row">
<td colspan="2"><strong>총계</strong></td>
<td><strong id="totalErrorHours">0h</strong></td>
<td><strong>-</strong></td>
<td><strong>-</strong></td>
<td><strong>0.0%</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 모듈화된 JavaScript 로딩 -->
<script src="/js/work-analysis/module-loader.js?v=1" defer></script>
<script>
// 서울 표준시(KST) 기준 날짜 함수들 (하위 호환성 유지)
function getKSTDate() {
const now = new Date();
// UTC 시간에 9시간 추가 (KST = UTC+9)
const kstOffset = 9 * 60; // 9시간을 분으로 변환
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
const kst = new Date(utc + (kstOffset * 60000));
return kst;
}
function formatDateToString(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 날짜 문자열을 간단한 형식으로 변환하는 함수 (하위 호환성 유지)
function formatSimpleDate(dateStr) {
if (!dateStr) return '날짜 없음';
if (typeof dateStr === 'string' && dateStr.includes('T')) {
return dateStr.split('T')[0]; // 2025-11-01T00:00:00.000Z → 2025-11-01
}
return dateStr;
}
// 현재 시간 업데이트 (하위 호환성 유지)
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('ko-KR', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// 시간 표시 요소가 있다면 업데이트
const timeElement = document.querySelector('.time-value');
if (timeElement) {
timeElement.textContent = timeString;
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('📦 작업 분석 모듈 로딩 시작...');
// 서울 표준시(KST) 기준 날짜 설정
const today = getKSTDate();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달 1일
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0); // 이번 달 마지막 날
document.getElementById('startDate').value = formatDateToString(monthStart);
document.getElementById('endDate').value = formatDateToString(monthEnd);
// 시간 업데이트 시작
updateTime();
setInterval(updateTime, 1000);
});
// 모듈 로딩 완료 후 초기화
window.addEventListener('workAnalysisModulesLoaded', function(event) {
console.log('🎉 작업 분석 모듈 로딩 완료:', event.detail.modules);
// 모듈 로딩 완료 후 추가 초기화 작업이 있다면 여기에 추가
});
// 초기 모드 설정 (하위 호환성 유지)
window.currentAnalysisMode = 'period';
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff