fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선
- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산 - 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader) - 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql) - synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
2233
synology_deployment/web-ui/pages/analysis/work-analysis-legacy.html
Normal file
2233
synology_deployment/web-ui/pages/analysis/work-analysis-legacy.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
2899
synology_deployment/web-ui/pages/analysis/work-analysis.html
Normal file
2899
synology_deployment/web-ui/pages/analysis/work-analysis.html
Normal file
File diff suppressed because it is too large
Load Diff
2230
synology_deployment/web-ui/pages/analysis/work-analysis.html.backup
Normal file
2230
synology_deployment/web-ui/pages/analysis/work-analysis.html.backup
Normal file
File diff suppressed because it is too large
Load Diff
1083
synology_deployment/web-ui/pages/analysis/work-report-analytics.html
Normal file
1083
synology_deployment/web-ui/pages/analysis/work-report-analytics.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user