## 주요 변경사항
### 1. 미사용 페이지 아카이브 (24개)
- admin 폴더 전체 (8개) → .archived-admin/
- 분석 페이지 (5개) → .archived-*
- 공통 페이지 (5개) → .archived-*
- 대시보드 페이지 (2개) → .archived-*
- 기타 (4개) → .archived-*
### 2. 새로운 폴더 구조
```
pages/
├── dashboard.html (메인 대시보드)
├── work/ (작업 관련)
│ ├── report-create.html (작업보고서 작성)
│ ├── report-view.html (작업보고서 조회)
│ └── analysis.html (작업 분석)
├── admin/ (관리 기능)
│ ├── index.html (관리 메뉴 허브)
│ ├── projects.html (프로젝트 관리)
│ ├── workers.html (작업자 관리)
│ ├── codes.html (코드 관리)
│ └── accounts.html (계정 관리)
└── profile/ (프로필)
├── info.html (내 정보)
└── password.html (비밀번호 변경)
```
### 3. 파일명 개선
- group-leader.html → dashboard.html
- daily-work-report.html → work/report-create.html
- daily-work-report-viewer.html → work/report-view.html
- work-analysis.html → work/analysis.html
- work-management.html → admin/index.html
- project-management.html → admin/projects.html
- worker-management.html → admin/workers.html
- code-management.html → admin/codes.html
- my-profile.html → profile/info.html
- change-password.html → profile/password.html
- admin-settings.html → admin/accounts.html
### 4. 내부 링크 전면 수정
- navbar.html 프로필 메뉴 링크 업데이트
- dashboard.html 빠른 작업 링크 업데이트
- admin/* 페이지 간 링크 업데이트
- load-navbar.js 대시보드 경로 수정
영향받는 파일: 39개
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
733 lines
26 KiB
HTML
733 lines
26 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>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
// 날짜 범위별로 보고서 데이터 조회하는 헬퍼 함수
|
||
async function getReportsByDateRange(startDate, endDate, workerId, projectId) {
|
||
const allReports = [];
|
||
const start = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
|
||
// 날짜별로 개별 조회 (백엔드 API 구조상 날짜별 조회가 주된 방법)
|
||
while (start <= end) {
|
||
const dateStr = start.toISOString().split('T')[0];
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
date: dateStr,
|
||
view_all: 'true' // 전체 조회 권한 요청
|
||
});
|
||
|
||
if (workerId) params.append('worker_id', workerId);
|
||
|
||
const dayReports = await API.get(`/api/daily-work-reports?${params}`);
|
||
|
||
// 프로젝트 필터링 (클라이언트 사이드에서)
|
||
let filteredReports = dayReports;
|
||
if (projectId) {
|
||
filteredReports = dayReports.filter(report =>
|
||
report.project_id == projectId
|
||
);
|
||
}
|
||
|
||
allReports.push(...filteredReports);
|
||
} catch (error) {
|
||
console.warn(`${dateStr} 데이터 조회 실패:`, error);
|
||
}
|
||
|
||
start.setDate(start.getDate() + 1);
|
||
}
|
||
|
||
return allReports;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #f5f7fa;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
border-radius: 15px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header p {
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.filter-section {
|
||
background: white;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
margin-bottom: 25px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.filter-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
align-items: end;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.filter-group label {
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.filter-group input, .filter-group select {
|
||
padding: 12px;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.filter-group input:focus, .filter-group select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.validation-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 25px;
|
||
}
|
||
|
||
.validation-card {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.validation-card:hover {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.card-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 15px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.error-icon {
|
||
background: #fed7d7;
|
||
color: #e53e3e;
|
||
}
|
||
|
||
.warning-icon {
|
||
background: #feebc8;
|
||
color: #dd6b20;
|
||
}
|
||
|
||
.info-icon {
|
||
background: #bee3f8;
|
||
color: #3182ce;
|
||
}
|
||
|
||
.success-icon {
|
||
background: #c6f6d5;
|
||
color: #38a169;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 1.3em;
|
||
font-weight: 700;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2.5em;
|
||
font-weight: 900;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.error-stat { color: #e53e3e; }
|
||
.warning-stat { color: #dd6b20; }
|
||
.info-stat { color: #3182ce; }
|
||
.success-stat { color: #38a169; }
|
||
|
||
.issue-list {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.issue-item {
|
||
padding: 12px;
|
||
border-left: 4px solid #e2e8f0;
|
||
margin-bottom: 10px;
|
||
background: #f7fafc;
|
||
border-radius: 0 8px 8px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.issue-item.error {
|
||
border-left-color: #e53e3e;
|
||
background: #fef5f5;
|
||
}
|
||
|
||
.issue-item.warning {
|
||
border-left-color: #dd6b20;
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 50px;
|
||
color: #718096;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 5px solid #e2e8f0;
|
||
border-top: 5px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.summary-section {
|
||
background: white;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
margin-bottom: 25px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
.summary-item {
|
||
text-align: center;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 2em;
|
||
font-weight: 900;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.summary-label {
|
||
color: #718096;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>📊 작업 보고서 입력 검증</h1>
|
||
<p>일일 작업 보고서의 데이터 품질을 확인하고 누락된 정보를 찾아보세요</p>
|
||
</div>
|
||
|
||
<div class="filter-section">
|
||
<div class="filter-grid">
|
||
<div class="filter-group">
|
||
<label for="startDate">시작 날짜</label>
|
||
<input type="date" id="startDate" value="">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="endDate">종료 날짜</label>
|
||
<input type="date" id="endDate" value="">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="workerFilter">작업자</label>
|
||
<select id="workerFilter">
|
||
<option value="">전체</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="projectFilter">프로젝트</label>
|
||
<select id="projectFilter">
|
||
<option value="">전체</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<button class="btn" onclick="validateReports()">검증 실행</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="summarySection" class="summary-section" style="display: none;">
|
||
<h3 style="margin-bottom: 20px;">📋 검증 요약</h3>
|
||
<div class="summary-grid">
|
||
<div class="summary-item">
|
||
<div class="summary-value" id="totalReports">0</div>
|
||
<div class="summary-label">총 보고서 수</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-value error-stat" id="errorCount">0</div>
|
||
<div class="summary-label">오류 항목</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-value warning-stat" id="warningCount">0</div>
|
||
<div class="summary-label">경고 항목</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-value success-stat" id="validPercent">0%</div>
|
||
<div class="summary-label">정상 비율</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="loadingSection" class="loading" style="display: none;">
|
||
<div class="loading-spinner"></div>
|
||
<p>데이터를 검증하고 있습니다...</p>
|
||
</div>
|
||
|
||
<div id="validationResults" class="validation-grid">
|
||
<!-- 검증 결과가 여기에 표시됩니다 -->
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
// API 설정
|
||
import { API } from './js/api-config.js';
|
||
|
||
// 페이지 로드 시 초기화
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializePage();
|
||
});
|
||
|
||
async function initializePage() {
|
||
// 기본 날짜 설정 (최근 30일)
|
||
const endDate = new Date();
|
||
const startDate = new Date();
|
||
startDate.setDate(startDate.getDate() - 30);
|
||
|
||
document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
|
||
document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
|
||
|
||
// 필터 옵션 로드
|
||
await loadFilterOptions();
|
||
}
|
||
|
||
async function loadFilterOptions() {
|
||
try {
|
||
// 작업자 목록은 별도 API로 로드해야 함 (Workers 테이블)
|
||
// 임시로 하드코딩된 데이터 사용
|
||
const workerSelect = document.getElementById('workerFilter');
|
||
const workers = [
|
||
{ worker_id: 1, worker_name: '작업자1' },
|
||
{ worker_id: 2, worker_name: '작업자2' },
|
||
{ worker_id: 3, worker_name: '작업자3' }
|
||
];
|
||
|
||
workers.forEach(worker => {
|
||
const option = document.createElement('option');
|
||
option.value = worker.worker_id;
|
||
option.textContent = worker.worker_name;
|
||
workerSelect.appendChild(option);
|
||
});
|
||
|
||
// 프로젝트 목록도 별도 API로 로드해야 함 (Projects 테이블)
|
||
// 임시로 하드코딩된 데이터 사용
|
||
const projectSelect = document.getElementById('projectFilter');
|
||
const projects = [
|
||
{ project_id: 1, project_name: '프로젝트A' },
|
||
{ project_id: 2, project_name: '프로젝트B' },
|
||
{ project_id: 3, project_name: '프로젝트C' }
|
||
];
|
||
|
||
projects.forEach(project => {
|
||
const option = document.createElement('option');
|
||
option.value = project.project_id;
|
||
option.textContent = project.project_name;
|
||
projectSelect.appendChild(option);
|
||
});
|
||
} catch (error) {
|
||
console.error('필터 옵션 로드 실패:', error);
|
||
}
|
||
}
|
||
|
||
async function validateReports() {
|
||
const startDate = document.getElementById('startDate').value;
|
||
const endDate = document.getElementById('endDate').value;
|
||
const workerId = document.getElementById('workerFilter').value;
|
||
const projectId = document.getElementById('projectFilter').value;
|
||
|
||
if (!startDate || !endDate) {
|
||
alert('시작 날짜와 종료 날짜를 선택해주세요.');
|
||
return;
|
||
}
|
||
|
||
// 로딩 표시
|
||
document.getElementById('loadingSection').style.display = 'block';
|
||
document.getElementById('validationResults').innerHTML = '';
|
||
document.getElementById('summarySection').style.display = 'none';
|
||
|
||
try {
|
||
// 보고서 데이터 조회 - 백엔드 API 구조에 맞게 수정
|
||
const params = new URLSearchParams();
|
||
|
||
if (workerId && projectId) {
|
||
// 작업자와 프로젝트가 모두 선택된 경우
|
||
params.append('start_date', startDate);
|
||
params.append('end_date', endDate);
|
||
params.append('worker_id', workerId);
|
||
params.append('project_id', projectId);
|
||
params.append('view_all', 'true'); // 전체 조회 권한 요청
|
||
|
||
const reports = await API.get(`/api/daily-work-reports/search?${params}`);
|
||
const reportData = reports.reports || [];
|
||
|
||
// 날짜별로 개별 조회하여 통합
|
||
const allReports = await getReportsByDateRange(startDate, endDate, workerId, projectId);
|
||
|
||
// 검증 실행
|
||
const validationResults = await performValidation(allReports, startDate, endDate);
|
||
|
||
// 결과 표시
|
||
displayValidationResults(validationResults);
|
||
updateSummary(validationResults, allReports.length);
|
||
} else {
|
||
// 날짜 범위로 조회
|
||
const allReports = await getReportsByDateRange(startDate, endDate, workerId, projectId);
|
||
|
||
// 검증 실행
|
||
const validationResults = await performValidation(allReports, startDate, endDate);
|
||
|
||
// 결과 표시
|
||
displayValidationResults(validationResults);
|
||
updateSummary(validationResults, allReports.length);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('검증 실행 실패:', error);
|
||
alert('검증 실행 중 오류가 발생했습니다.');
|
||
} finally {
|
||
document.getElementById('loadingSection').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function performValidation(reports, startDate, endDate) {
|
||
const results = {
|
||
missingDates: [],
|
||
invalidWorkHours: [],
|
||
missingFields: [],
|
||
duplicateEntries: [],
|
||
unusualPatterns: [],
|
||
dataConsistency: []
|
||
};
|
||
|
||
// 1. 누락된 날짜 확인
|
||
const expectedDates = getDateRange(startDate, endDate);
|
||
const reportDates = [...new Set(reports.map(r => r.report_date))];
|
||
results.missingDates = expectedDates.filter(date =>
|
||
!reportDates.includes(date) && isWorkingDay(date)
|
||
);
|
||
|
||
// 2. 잘못된 작업시간 확인
|
||
results.invalidWorkHours = reports.filter(report => {
|
||
const hours = parseFloat(report.work_hours);
|
||
return isNaN(hours) || hours <= 0 || hours > 24;
|
||
});
|
||
|
||
// 3. 필수 필드 누락 확인
|
||
results.missingFields = reports.filter(report => {
|
||
return !report.worker_id || !report.project_id ||
|
||
!report.work_type_id || !report.work_status_id;
|
||
});
|
||
|
||
// 4. 중복 항목 확인
|
||
const reportKeys = new Map();
|
||
reports.forEach(report => {
|
||
const key = `${report.report_date}-${report.worker_id}-${report.project_id}`;
|
||
if (reportKeys.has(key)) {
|
||
results.duplicateEntries.push({
|
||
...report,
|
||
duplicateKey: key
|
||
});
|
||
} else {
|
||
reportKeys.set(key, report);
|
||
}
|
||
});
|
||
|
||
// 5. 비정상적인 패턴 확인
|
||
results.unusualPatterns = findUnusualPatterns(reports);
|
||
|
||
// 6. 데이터 일관성 확인
|
||
results.dataConsistency = checkDataConsistency(reports);
|
||
|
||
return results;
|
||
}
|
||
|
||
function getDateRange(startDate, endDate) {
|
||
const dates = [];
|
||
const current = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
|
||
while (current <= end) {
|
||
dates.push(current.toISOString().split('T')[0]);
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
|
||
return dates;
|
||
}
|
||
|
||
function isWorkingDay(dateString) {
|
||
const date = new Date(dateString);
|
||
const dayOfWeek = date.getDay();
|
||
return dayOfWeek >= 1 && dayOfWeek <= 5; // 월~금
|
||
}
|
||
|
||
function findUnusualPatterns(reports) {
|
||
const unusual = [];
|
||
|
||
// 작업자별 일일 총 작업시간이 8시간을 크게 초과하는 경우
|
||
const dailyHours = {};
|
||
reports.forEach(report => {
|
||
const key = `${report.report_date}-${report.worker_id}`;
|
||
dailyHours[key] = (dailyHours[key] || 0) + parseFloat(report.work_hours);
|
||
});
|
||
|
||
Object.entries(dailyHours).forEach(([key, hours]) => {
|
||
if (hours > 12) {
|
||
const [date, workerId] = key.split('-');
|
||
unusual.push({
|
||
type: 'excessive_hours',
|
||
date: date,
|
||
worker_id: workerId,
|
||
total_hours: hours,
|
||
message: `${date} 작업자 ${workerId}의 총 작업시간이 ${hours}시간입니다`
|
||
});
|
||
}
|
||
});
|
||
|
||
return unusual;
|
||
}
|
||
|
||
function checkDataConsistency(reports) {
|
||
const inconsistencies = [];
|
||
|
||
// 같은 프로젝트에서 완료 상태 이후 진행중 상태가 있는지 확인
|
||
const projectStatus = {};
|
||
reports.forEach(report => {
|
||
const key = `${report.project_id}-${report.worker_id}`;
|
||
if (!projectStatus[key]) {
|
||
projectStatus[key] = [];
|
||
}
|
||
projectStatus[key].push({
|
||
date: report.report_date,
|
||
status: report.work_status_id,
|
||
report: report
|
||
});
|
||
});
|
||
|
||
Object.entries(projectStatus).forEach(([key, statuses]) => {
|
||
statuses.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||
// 여기서 상태 변화의 논리적 일관성을 확인할 수 있습니다
|
||
});
|
||
|
||
return inconsistencies;
|
||
}
|
||
|
||
function displayValidationResults(results) {
|
||
const container = document.getElementById('validationResults');
|
||
|
||
// 누락된 날짜
|
||
if (results.missingDates.length > 0) {
|
||
container.appendChild(createValidationCard(
|
||
'📅 누락된 작업일',
|
||
'error',
|
||
results.missingDates.length,
|
||
results.missingDates.map(date => ({
|
||
message: `${date} (${getDayName(date)}) - 작업 보고서 없음`
|
||
}))
|
||
));
|
||
}
|
||
|
||
// 잘못된 작업시간
|
||
if (results.invalidWorkHours.length > 0) {
|
||
container.appendChild(createValidationCard(
|
||
'⏰ 잘못된 작업시간',
|
||
'error',
|
||
results.invalidWorkHours.length,
|
||
results.invalidWorkHours.map(report => ({
|
||
message: `${report.report_date} - 작업자 ${report.worker_id}: ${report.work_hours}시간`
|
||
}))
|
||
));
|
||
}
|
||
|
||
// 필수 필드 누락
|
||
if (results.missingFields.length > 0) {
|
||
container.appendChild(createValidationCard(
|
||
'❗ 필수 필드 누락',
|
||
'error',
|
||
results.missingFields.length,
|
||
results.missingFields.map(report => ({
|
||
message: `${report.report_date} - ID: ${report.id} - 필수 정보 누락`
|
||
}))
|
||
));
|
||
}
|
||
|
||
// 중복 항목
|
||
if (results.duplicateEntries.length > 0) {
|
||
container.appendChild(createValidationCard(
|
||
'🔄 중복 항목',
|
||
'warning',
|
||
results.duplicateEntries.length,
|
||
results.duplicateEntries.map(report => ({
|
||
message: `${report.report_date} - 작업자 ${report.worker_id}, 프로젝트 ${report.project_id}`
|
||
}))
|
||
));
|
||
}
|
||
|
||
// 비정상적인 패턴
|
||
if (results.unusualPatterns.length > 0) {
|
||
container.appendChild(createValidationCard(
|
||
'⚠️ 비정상적인 패턴',
|
||
'warning',
|
||
results.unusualPatterns.length,
|
||
results.unusualPatterns.map(pattern => ({
|
||
message: pattern.message
|
||
}))
|
||
));
|
||
}
|
||
|
||
// 검증 완료 메시지
|
||
if (container.children.length === 0) {
|
||
container.appendChild(createValidationCard(
|
||
'✅ 검증 완료',
|
||
'success',
|
||
0,
|
||
[{ message: '모든 데이터가 정상적으로 입력되었습니다!' }]
|
||
));
|
||
}
|
||
}
|
||
|
||
function createValidationCard(title, type, count, issues) {
|
||
const card = document.createElement('div');
|
||
card.className = 'validation-card';
|
||
|
||
const iconClass = type === 'error' ? 'error-icon' :
|
||
type === 'warning' ? 'warning-icon' :
|
||
type === 'success' ? 'success-icon' : 'info-icon';
|
||
|
||
const statClass = type === 'error' ? 'error-stat' :
|
||
type === 'warning' ? 'warning-stat' :
|
||
type === 'success' ? 'success-stat' : 'info-stat';
|
||
|
||
const icon = type === 'error' ? '❌' :
|
||
type === 'warning' ? '⚠️' :
|
||
type === 'success' ? '✅' : 'ℹ️';
|
||
|
||
card.innerHTML = `
|
||
<div class="card-header">
|
||
<div class="card-icon ${iconClass}">${icon}</div>
|
||
<div class="card-title">${title}</div>
|
||
</div>
|
||
<div class="stat-number ${statClass}">${count}</div>
|
||
<div class="issue-list">
|
||
${issues.map(issue => `
|
||
<div class="issue-item ${type}">
|
||
${issue.message}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
return card;
|
||
}
|
||
|
||
function updateSummary(results, totalReports) {
|
||
const errorCount = results.missingDates.length +
|
||
results.invalidWorkHours.length +
|
||
results.missingFields.length;
|
||
|
||
const warningCount = results.duplicateEntries.length +
|
||
results.unusualPatterns.length +
|
||
results.dataConsistency.length;
|
||
|
||
const totalIssues = errorCount + warningCount;
|
||
const validPercent = totalReports > 0 ?
|
||
Math.round(((totalReports - totalIssues) / totalReports) * 100) : 100;
|
||
|
||
document.getElementById('totalReports').textContent = totalReports;
|
||
document.getElementById('errorCount').textContent = errorCount;
|
||
document.getElementById('warningCount').textContent = warningCount;
|
||
document.getElementById('validPercent').textContent = validPercent + '%';
|
||
|
||
document.getElementById('summarySection').style.display = 'block';
|
||
}
|
||
|
||
function getDayName(dateString) {
|
||
const date = new Date(dateString);
|
||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||
return days[date.getDay()];
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |