- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
364 lines
17 KiB
HTML
364 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>작업 분석 | (주)테크니컬코리아</title>
|
|
<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 type="module" 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>
|