refactor: 페이지 구조 대대적 개편 - 명확한 폴더 구조 및 파일명 개선
## 주요 변경사항
### 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>
This commit is contained in:
@@ -1,345 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업 현황 확인 - TK 건설</title>
|
||||
<link rel="stylesheet" href="/css/common.css?v=13">
|
||||
<link rel="stylesheet" href="/css/modern-dashboard.css?v=14">
|
||||
<link rel="stylesheet" href="/css/work-report-calendar.css?v=29">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 대시보드 헤더 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<img src="/img/logo.png" alt="테크니컬코리아" class="brand-logo">
|
||||
<div class="brand-text">
|
||||
<h1 class="brand-title">테크니컬코리아</h1>
|
||||
<p class="brand-subtitle">작업 현황 확인</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<div class="current-time" id="currentTime">
|
||||
<span class="time-label">현재 시각</span>
|
||||
<span class="time-value" id="timeValue">--:--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-secondary dashboard-btn" onclick="window.location.href='/pages/dashboard/group-leader.html'">
|
||||
<span class="btn-icon">🏠</span>
|
||||
대시보드
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="user-profile" id="userProfile">
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text" id="userInitial">사</span>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name" id="userName">사용자</span>
|
||||
<span class="user-role" id="userRole">작업자</span>
|
||||
</div>
|
||||
<div class="profile-menu" id="profileMenu">
|
||||
<a href="/pages/profile/my-profile.html" class="menu-item">
|
||||
<span class="menu-icon">👤</span>
|
||||
내 프로필
|
||||
</a>
|
||||
<a href="/pages/profile/change-password.html" class="menu-item">
|
||||
<span class="menu-icon">🔐</span>
|
||||
비밀번호 변경
|
||||
</a>
|
||||
<button class="menu-item logout-btn" id="logoutBtn">
|
||||
<span class="menu-icon">🚪</span>
|
||||
로그아웃
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="dashboard-main">
|
||||
<div class="calendar-page-container">
|
||||
<!-- 페이지 제목 -->
|
||||
<div class="page-title-section">
|
||||
<h2 class="page-title">📅 작업 현황 확인</h2>
|
||||
<p class="page-subtitle">월별 작업자 현황을 한눈에 확인하세요</p>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 카드 -->
|
||||
<div class="calendar-card">
|
||||
<!-- 월 네비게이션 -->
|
||||
<div class="calendar-nav">
|
||||
<button id="prevMonthBtn" class="nav-btn prev-btn">
|
||||
<span class="nav-icon">‹</span>
|
||||
<span class="nav-text">이전</span>
|
||||
</button>
|
||||
|
||||
<div class="calendar-title">
|
||||
<h3 id="monthYearTitle">2025년 11월</h3>
|
||||
<button id="todayBtn" class="today-btn">오늘</button>
|
||||
</div>
|
||||
|
||||
<button id="nextMonthBtn" class="nav-btn next-btn">
|
||||
<span class="nav-text">다음</span>
|
||||
<span class="nav-icon">›</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 범례 -->
|
||||
<div class="calendar-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-overtime-warning"></div>
|
||||
<span>확인필요</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-errors"></div>
|
||||
<span>미입력</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-issues"></div>
|
||||
<span>부분입력</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-normal"></div>
|
||||
<span>이상 없음</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 -->
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-header">
|
||||
<div class="day-header sunday">일</div>
|
||||
<div class="day-header">월</div>
|
||||
<div class="day-header">화</div>
|
||||
<div class="day-header">수</div>
|
||||
<div class="day-header">목</div>
|
||||
<div class="day-header">금</div>
|
||||
<div class="day-header saturday">토</div>
|
||||
</div>
|
||||
<div class="calendar-days" id="calendarDays">
|
||||
<!-- 캘린더 날짜들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 로딩 스피너 -->
|
||||
<div id="loadingSpinner" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-content">
|
||||
<div class="spinner"></div>
|
||||
<p>데이터를 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 일일 작업 현황 모달 -->
|
||||
<div id="dailyWorkModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container large-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">2025년 11월 3일 작업 현황</h2>
|
||||
<button class="modal-close-btn" onclick="closeDailyWorkModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- 요약 정보 -->
|
||||
<div class="daily-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon success">👥</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">총 작업자</div>
|
||||
<div class="summary-value" id="modalTotalWorkers">0명</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon primary">⏰</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">총 작업시간</div>
|
||||
<div class="summary-value" id="modalTotalHours">0.0h</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon warning">📝</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">작업 건수</div>
|
||||
<div class="summary-value" id="modalTotalTasks">0건</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon error">⚠️</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">오류 건수</div>
|
||||
<div class="summary-value" id="modalErrorCount">0건</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 현황 리스트 -->
|
||||
<div class="modal-work-status">
|
||||
<div class="work-status-header">
|
||||
<h3>작업자별 현황</h3>
|
||||
<div class="status-filter">
|
||||
<select id="statusFilter">
|
||||
<option value="all">전체</option>
|
||||
<option value="incomplete">미입력</option>
|
||||
<option value="partial">부분입력</option>
|
||||
<option value="complete">완료</option>
|
||||
<option value="overtime">연장근로</option>
|
||||
<option value="error">오류</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalWorkersList" class="worker-status-list">
|
||||
<!-- 작업자 리스트가 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="modalNoData" class="empty-state" style="display: none;">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>해당 날짜의 작업 보고서가 없습니다</h3>
|
||||
<p>다른 날짜를 선택해 주세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업 입력/수정 모달 -->
|
||||
<div id="workEntryModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container large-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="workEntryModalTitle">작업 관리</h2>
|
||||
<button class="modal-close-btn" onclick="closeWorkEntryModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="modal-tabs">
|
||||
<button class="tab-btn active" data-tab="existing" onclick="switchTab('existing')">
|
||||
📋 기존 작업 (0건)
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="new" onclick="switchTab('new')">
|
||||
➕ 새 작업 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 기존 작업 목록 탭 -->
|
||||
<div id="existingWorkTab" class="tab-content active">
|
||||
<div class="existing-work-header">
|
||||
<h3>등록된 작업 목록</h3>
|
||||
<div class="work-summary" id="workSummary">
|
||||
총 <span id="totalWorkCount">0</span>건 | 총 <span id="totalWorkHours">0</span>시간
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="existingWorkList" class="existing-work-list">
|
||||
<!-- 기존 작업들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="noExistingWork" class="empty-state" style="display: none;">
|
||||
<div class="empty-icon">📝</div>
|
||||
<h3>등록된 작업이 없습니다</h3>
|
||||
<p>"새 작업 추가" 탭에서 작업을 등록해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 작업 추가 탭 -->
|
||||
<div id="newWorkTab" class="tab-content">
|
||||
<form id="workEntryForm">
|
||||
<!-- 작업자 정보 -->
|
||||
<div class="form-section">
|
||||
<h3>작업자 정보</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업자</label>
|
||||
<input type="text" id="workerNameDisplay" class="form-control" readonly>
|
||||
<input type="hidden" id="workerId">
|
||||
<input type="hidden" id="editingWorkId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 날짜</label>
|
||||
<input type="date" id="workDate" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업 내용 -->
|
||||
<div class="form-section">
|
||||
<h3 id="workContentTitle">작업 내용</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">프로젝트 *</label>
|
||||
<select id="projectSelect" class="form-control" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 유형 *</label>
|
||||
<select id="workTypeSelect" class="form-control" required>
|
||||
<option value="">작업 유형을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 시간 (시간) *</label>
|
||||
<input type="number" id="workHours" class="form-control" min="0" max="24" step="0.5" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 상태 *</label>
|
||||
<select id="workStatusSelect" class="form-control" required>
|
||||
<option value="">상태를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">오류 유형</label>
|
||||
<select id="errorTypeSelect" class="form-control">
|
||||
<option value="">오류 유형 (선택사항)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 설명</label>
|
||||
<textarea id="workDescription" class="form-control" rows="3" placeholder="작업 내용을 상세히 입력하세요"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 휴가 처리 -->
|
||||
<div class="form-section" id="vacationSection">
|
||||
<h3>휴가 처리</h3>
|
||||
<div class="vacation-buttons">
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('full')">연차 (8시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('half')">반차 (4시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('quarter')">반반차 (2시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('early')">조퇴 (6시간)</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeWorkEntryModal()">취소</button>
|
||||
<div class="footer-actions">
|
||||
<button type="button" class="btn btn-danger" id="deleteWorkBtn" onclick="deleteWork()" style="display: none;">
|
||||
🗑️ 삭제
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="saveWorkBtn" onclick="saveWorkEntry()">
|
||||
💾 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script type="module" src="/js/api-config.js?v=13"></script>
|
||||
<script src="/js/auth-check.js?v=13"></script>
|
||||
<!-- load-navbar.js 제거: 이 페이지는 자체 헤더 사용 -->
|
||||
<script src="/js/modules/calendar/CalendarState.js?v=1"></script>
|
||||
<script src="/js/modules/calendar/CalendarAPI.js?v=1"></script>
|
||||
<script src="/js/modules/calendar/CalendarView.js?v=1"></script>
|
||||
<script src="/js/work-report-calendar.js?v=41"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,172 +0,0 @@
|
||||
<!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="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/daily-work-report.css?v=2">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<!-- 뒤로가기 버튼 -->
|
||||
<a href="javascript:history.back()" class="back-button">
|
||||
← 뒤로가기
|
||||
</a>
|
||||
|
||||
<!-- 진행 단계 표시 -->
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step active" id="progressStep1">
|
||||
<div class="step-circle">1</div>
|
||||
<div class="step-label">날짜 선택</div>
|
||||
</div>
|
||||
<div class="progress-step" id="progressStep2">
|
||||
<div class="step-circle">2</div>
|
||||
<div class="step-label">작업자 선택</div>
|
||||
</div>
|
||||
<div class="progress-step" id="progressStep3">
|
||||
<div class="step-circle">3</div>
|
||||
<div class="step-label">작업 입력</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메시지 영역 -->
|
||||
<div id="message-container"></div>
|
||||
|
||||
<!-- 1단계: 날짜 선택 -->
|
||||
<div id="step1" class="step-section active">
|
||||
<div class="step-header">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-title">작업 날짜 선택</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reportDate" class="form-label">작업 날짜를 선택하세요</label>
|
||||
<input type="date" id="reportDate" class="form-input" required>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="nextStep1">다음 단계 →</button>
|
||||
</div>
|
||||
|
||||
<!-- 2단계: 작업자 선택 -->
|
||||
<div id="step2" class="step-section">
|
||||
<div class="step-header">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-title">작업자 선택</div>
|
||||
</div>
|
||||
<div id="workerGrid" class="worker-grid">
|
||||
<!-- 작업자 카드들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="nextStep2" disabled>다음 단계 →</button>
|
||||
</div>
|
||||
|
||||
<!-- 3단계: 작업 내역 입력 -->
|
||||
<div id="step3" class="step-section">
|
||||
<div class="step-header">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-title">작업 내역 입력</div>
|
||||
</div>
|
||||
|
||||
<!-- 총 작업시간 표시 -->
|
||||
<div class="total-hours-display" id="totalHoursDisplay">
|
||||
총 작업시간: 0시간
|
||||
</div>
|
||||
|
||||
<!-- 작업 항목들 -->
|
||||
<div id="workEntriesList">
|
||||
<!-- 작업 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- 작업 추가 버튼 -->
|
||||
<button type="button" class="btn btn-secondary btn-block" id="addWorkBtn">
|
||||
➕ 작업 추가
|
||||
</button>
|
||||
|
||||
<!-- 저장 버튼 -->
|
||||
<button type="button" class="btn btn-success btn-block" id="submitBtn">
|
||||
💾 작업보고서 저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 📊 내가 입력한 당일 작업 현황 (수정/삭제 가능) -->
|
||||
<div class="step-section" id="dailyWorkersSection" style="display: none;">
|
||||
<div class="step-header">
|
||||
<div class="step-number">📊</div>
|
||||
<div class="step-title">내가 입력한 작업 현황</div>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary); margin-bottom: var(--space-5);">
|
||||
✏️ 내가 입력한 작업만 표시되며, 각 작업을 <strong>수정</strong>하거나 <strong>삭제</strong>할 수 있습니다.
|
||||
</p>
|
||||
<div id="dailyWorkersContent">
|
||||
<!-- 작업자 현황이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용법 안내 -->
|
||||
<div class="step-section">
|
||||
<div class="step-header">
|
||||
<div class="step-number">📖</div>
|
||||
<div class="step-title">사용 가이드</div>
|
||||
</div>
|
||||
<div class="guide-grid">
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">📅</div>
|
||||
<strong>1단계</strong><br>
|
||||
작업 날짜 선택
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">👤</div>
|
||||
<strong>2단계</strong><br>
|
||||
작업자 선택 (터치)
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">🔧</div>
|
||||
<strong>3단계</strong><br>
|
||||
작업 내역 입력
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">💾</div>
|
||||
<strong>완료</strong><br>
|
||||
저장하여 마무리
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">✏️</div>
|
||||
<strong>관리</strong><br>
|
||||
입력한 작업 수정/삭제
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 저장 결과 모달 -->
|
||||
<div id="saveResultModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container result-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="resultModalTitle">저장 결과</h2>
|
||||
<button class="modal-close-btn" onclick="closeSaveResultModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="resultModalContent" class="result-content">
|
||||
<!-- 결과 내용이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script type="module" src="/js/api-config.js?v=3"></script>
|
||||
<script type="module" src="/js/load-navbar.js?v=5"></script>
|
||||
<script type="module" src="/js/daily-work-report.js?v=11"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,215 +0,0 @@
|
||||
<!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="stylesheet" href="/css/main-layout.css">
|
||||
<link rel="stylesheet" href="/css/management-dashboard.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script type="module" src="/js/auth-check.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout-with-navbar">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="dashboard-container">
|
||||
<!-- 뒤로가기 버튼 -->
|
||||
<a href="javascript:history.back()" class="back-btn">
|
||||
← 뒤로가기
|
||||
</a>
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="page-header">
|
||||
<h1>📊 관리자 대시보드</h1>
|
||||
<p class="subtitle">팀 전체의 일일 작업 입력 현황을 한눈에 확인하세요</p>
|
||||
</div>
|
||||
|
||||
<!-- 권한 체크 메시지 -->
|
||||
<div id="permission-check-message" class="message warning" style="display: none;">
|
||||
⚠️ 권한을 확인하는 중입니다...
|
||||
</div>
|
||||
|
||||
<!-- 메시지 영역 -->
|
||||
<div id="message-container"></div>
|
||||
|
||||
<!-- 날짜 선택 섹션 -->
|
||||
<div class="date-selection-card">
|
||||
<div class="date-selection-header">
|
||||
<h3>📅 조회 날짜 선택</h3>
|
||||
<button class="refresh-btn" id="refreshBtn">
|
||||
🔄 새로고침
|
||||
</button>
|
||||
</div>
|
||||
<div class="date-selection-body">
|
||||
<input type="date" id="selectedDate" class="date-input">
|
||||
<button class="btn btn-primary" id="loadDataBtn">📊 현황 조회</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 요약 대시보드 -->
|
||||
<div id="summarySection" class="summary-section" style="display: none;">
|
||||
<h3>📈 전체 현황 요약</h3>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card total-workers">
|
||||
<div class="summary-icon">👥</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="totalWorkers">0</div>
|
||||
<div class="summary-label">전체 작업자</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card completed-workers">
|
||||
<div class="summary-icon">✅</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="completedWorkers">0</div>
|
||||
<div class="summary-label">입력 완료</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card missing-workers">
|
||||
<div class="summary-icon">❌</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="missingWorkers">0</div>
|
||||
<div class="summary-label">입력 미완료</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card total-hours">
|
||||
<div class="summary-icon">⏰</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="totalHours">0</div>
|
||||
<div class="summary-label">총 작업시간</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card total-entries">
|
||||
<div class="summary-icon">📝</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="totalEntries">0</div>
|
||||
<div class="summary-label">총 작업항목</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card error-count">
|
||||
<div class="summary-icon">⚠️</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-number" id="errorCount">0</div>
|
||||
<div class="summary-label">에러 발생</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 액션 바 -->
|
||||
<div id="actionBar" class="action-bar" style="display: none;">
|
||||
<div class="filter-section">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="showOnlyMissing">
|
||||
<span class="checkmark"></span>
|
||||
미입력자만 보기
|
||||
</label>
|
||||
</div>
|
||||
<div class="action-section">
|
||||
<button class="btn btn-secondary" id="exportBtn">
|
||||
📥 엑셀 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 현황 테이블 -->
|
||||
<div id="workersSection" class="workers-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h3>👥 작업자별 입력 현황</h3>
|
||||
<div class="legend">
|
||||
<span class="legend-item completed">✅ 입력완료</span>
|
||||
<span class="legend-item missing">❌ 미입력</span>
|
||||
<span class="legend-item partial">⚠️ 부분입력</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="workers-table" id="workersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>작업자</th>
|
||||
<th>상태</th>
|
||||
<th>총시간</th>
|
||||
<th>항목수</th>
|
||||
<th>작업유형</th>
|
||||
<th>프로젝트</th>
|
||||
<th>기여자</th>
|
||||
<th>최근업데이트</th>
|
||||
<th>상세</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="workersTableBody">
|
||||
<!-- 작업자 데이터가 여기에 동적으로 추가됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 스피너 -->
|
||||
<div id="loadingSpinner" class="loading-spinner" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>데이터를 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- 데이터 없음 메시지 -->
|
||||
<div id="noDataMessage" class="no-data-message" style="display: none;">
|
||||
<div class="no-data-icon">📭</div>
|
||||
<h3>표시할 데이터가 없습니다</h3>
|
||||
<p>선택한 날짜에 입력된 작업 데이터가 없거나<br>조회 권한이 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 사용법 안내 -->
|
||||
<div class="guide-section">
|
||||
<h3>📖 사용 가이드</h3>
|
||||
<div class="guide-grid">
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">📅</div>
|
||||
<strong>날짜 선택</strong><br>
|
||||
확인하고 싶은 날짜를 선택하세요
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">📊</div>
|
||||
<strong>현황 확인</strong><br>
|
||||
팀 전체의 입력 현황을 확인하세요
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">🔍</div>
|
||||
<strong>필터링</strong><br>
|
||||
미입력자만 따로 확인할 수 있습니다
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<div class="guide-icon">📥</div>
|
||||
<strong>내보내기</strong><br>
|
||||
엑셀로 데이터를 다운로드하세요
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 상세 모달 -->
|
||||
<div id="workerDetailModal" class="worker-detail-modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalWorkerName">작업자 상세</h3>
|
||||
<button class="close-modal-btn" onclick="closeWorkerDetailModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalWorkerDetails">
|
||||
<!-- 작업자 상세 정보가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script type="module" src="/js/load-navbar.js"></script>
|
||||
<script type="module" src="/js/management-dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,151 +0,0 @@
|
||||
<!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="stylesheet" href="/css/main-layout.css">
|
||||
<link rel="stylesheet" href="/css/attendance.css">
|
||||
<link rel="stylesheet" href="/css/my-attendance.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div id="content-container">
|
||||
<!-- 페이지 헤더 -->
|
||||
<header class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">
|
||||
<span class="title-icon">📊</span>
|
||||
나의 출근 현황
|
||||
</h1>
|
||||
<p class="page-description">나의 출근 기록과 근태 현황을 확인할 수 있습니다</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 필터 섹션 -->
|
||||
<div class="controls">
|
||||
<label for="yearSelect">연도:</label>
|
||||
<select id="yearSelect"></select>
|
||||
|
||||
<label for="monthSelect">월:</label>
|
||||
<select id="monthSelect"></select>
|
||||
|
||||
<button id="loadAttendance" class="btn-primary">조회</button>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 섹션 -->
|
||||
<section class="stats-section">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⏱️</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value" id="totalHours">-</div>
|
||||
<div class="stat-label">총 근무시간</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📅</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value" id="totalDays">-</div>
|
||||
<div class="stat-label">근무일수</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🌴</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value" id="remainingLeave">-</div>
|
||||
<div class="stat-label">잔여 연차</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 탭 섹션 -->
|
||||
<div class="tab-container">
|
||||
<button class="tab-btn active" data-tab="list">
|
||||
<span class="tab-icon">📋</span> 리스트 보기
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="calendar">
|
||||
<span class="tab-icon">📅</span> 달력 보기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 리스트 뷰 -->
|
||||
<div id="listView" class="tab-content active">
|
||||
<div id="attendanceTableContainer">
|
||||
<table id="attendanceTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>요일</th>
|
||||
<th>출근시간</th>
|
||||
<th>퇴근시간</th>
|
||||
<th>근무시간</th>
|
||||
<th>상태</th>
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendanceTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="loading-cell">데이터를 불러오는 중...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 달력 뷰 -->
|
||||
<div id="calendarView" class="tab-content">
|
||||
<div id="calendarContainer">
|
||||
<div class="calendar-header">
|
||||
<button id="prevMonth" class="calendar-nav-btn">◀</button>
|
||||
<h3 id="calendarTitle">2026년 1월</h3>
|
||||
<button id="nextMonth" class="calendar-nav-btn">▶</button>
|
||||
</div>
|
||||
<div id="calendarGrid" class="calendar-grid">
|
||||
<!-- 달력이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="legend-dot normal"></span> 정상</span>
|
||||
<span class="legend-item"><span class="legend-dot late"></span> 지각</span>
|
||||
<span class="legend-item"><span class="legend-dot early"></span> 조퇴</span>
|
||||
<span class="legend-item"><span class="legend-dot absent"></span> 결근</span>
|
||||
<span class="legend-item"><span class="legend-dot vacation"></span> 휴가</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 일별 상세 모달 -->
|
||||
<div id="detailModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">출근 상세 정보</h2>
|
||||
<button class="modal-close-btn" onclick="closeDetailModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- 상세 정보가 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 로딩 -->
|
||||
<script type="module" src="/js/api-config.js"></script>
|
||||
<script type="module" src="/js/load-navbar.js"></script>
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
<script src="/js/my-attendance.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,304 +0,0 @@
|
||||
<!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="stylesheet" href="/css/main-layout.css">
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
<link rel="stylesheet" href="/css/work-report.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
<style>
|
||||
.period-selector {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.period-selector label {
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.period-selector input[type="date"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.analysis-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
border-radius: 4px 4px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.analysis-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.analysis-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.summary-card h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-card .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-table .project-col {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table .worker-col {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.data-table .task-col {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table .hours-col {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-row select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div id="content-container">
|
||||
<div class="page-header">
|
||||
<h1>📊 프로젝트 투입 분석</h1>
|
||||
<p class="subtitle">기간별 프로젝트/작업자/작업 투입 현황을 분석합니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📅 분석 기간 설정</h3>
|
||||
<div class="period-selector">
|
||||
<label for="startDate">시작일:</label>
|
||||
<input type="date" id="startDate">
|
||||
|
||||
<label for="endDate">종료일:</label>
|
||||
<input type="date" id="endDate">
|
||||
|
||||
<button id="analyzeBtn" class="btn btn-primary">분석 실행</button>
|
||||
<button id="quickMonth" class="btn btn-secondary">이번 달</button>
|
||||
<button id="quickLastMonth" class="btn btn-secondary">지난 달</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="analysisCard" style="display: none;">
|
||||
<div class="summary-cards" id="summaryCards">
|
||||
<!-- 요약 정보가 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<h4>🔍 필터 옵션</h4>
|
||||
<div class="filter-row">
|
||||
<label>프로젝트:</label>
|
||||
<select id="projectFilter">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
|
||||
<label>작업자:</label>
|
||||
<select id="workerFilter">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
|
||||
<label>작업 분류:</label>
|
||||
<select id="taskFilter">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
|
||||
<button id="applyFilter" class="btn btn-primary">필터 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analysis-tabs">
|
||||
<button class="tab-button active" data-tab="project">프로젝트별</button>
|
||||
<button class="tab-button" data-tab="worker">작업자별</button>
|
||||
<button class="tab-button" data-tab="task">작업별</button>
|
||||
<button class="tab-button" data-tab="detail">상세내역</button>
|
||||
</div>
|
||||
|
||||
<div id="projectTab" class="analysis-content active">
|
||||
<h4>📋 프로젝트별 투입 현황</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">순번</th>
|
||||
<th>프로젝트명</th>
|
||||
<th width="100">투입 시간</th>
|
||||
<th width="80">비율</th>
|
||||
<th width="100">참여 인원</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projectTableBody">
|
||||
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workerTab" class="analysis-content">
|
||||
<h4>👥 작업자별 투입 현황</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">순번</th>
|
||||
<th>작업자명</th>
|
||||
<th width="100">투입 시간</th>
|
||||
<th width="80">비율</th>
|
||||
<th width="100">참여 프로젝트</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="workerTableBody">
|
||||
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="taskTab" class="analysis-content">
|
||||
<h4>⚙️ 작업별 투입 현황</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">순번</th>
|
||||
<th>작업 분류</th>
|
||||
<th width="100">투입 시간</th>
|
||||
<th width="80">비율</th>
|
||||
<th width="100">참여 인원</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="taskTableBody">
|
||||
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="detailTab" class="analysis-content">
|
||||
<h4>📄 상세 내역</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">순번</th>
|
||||
<th width="100">날짜</th>
|
||||
<th>프로젝트</th>
|
||||
<th>작업자</th>
|
||||
<th>작업 분류</th>
|
||||
<th width="80">시간</th>
|
||||
<th>메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detailTableBody">
|
||||
<tr><td colspan="7" class="no-data">분석을 실행해주세요</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/js/load-navbar.js"></script>
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
<script type="module" src="/js/project-analysis.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,723 +0,0 @@
|
||||
<!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="stylesheet" href="/css/main-layout.css">
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
<link rel="stylesheet" href="/css/work-report.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
<style>
|
||||
/* 검토 페이지 전용 스타일 */
|
||||
.review-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: 24px;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 상단 대시보드 */
|
||||
.dashboard-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dashboard-number {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dashboard-card.total .dashboard-number { color: #007bff; }
|
||||
.dashboard-card.error .dashboard-number { color: #dc3545; }
|
||||
.dashboard-card.warning .dashboard-number { color: #ffc107; }
|
||||
.dashboard-card.missing .dashboard-number { color: #6c757d; }
|
||||
|
||||
/* 필터 섹션 */
|
||||
.filter-section {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 12px 24px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
/* 알림 영역 */
|
||||
.alerts-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alerts-header {
|
||||
background: #f8f9fa;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.alert-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.alert-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.alert-type {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.alert-type.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert-type.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-type.missing {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-type.pending {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 메인 테이블 */
|
||||
.table-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #f8f9fa;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.data-table tr {
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.data-table tr.selected {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
/* 상태 표시 */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.normal {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.row-normal { background: #fff; }
|
||||
.row-warning { background: #fffbf0; border-left: 4px solid #ffc107; }
|
||||
.row-error { background: #fef5f5; border-left: 4px solid #dc3545; }
|
||||
.row-missing { background: #f0f8ff; border-left: 4px solid #6c757d; }
|
||||
.row-reviewed { background: #f0f9ff; border-left: 4px solid #28a745; }
|
||||
|
||||
/* 새로운 배지 스타일 */
|
||||
.attendance-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.attendance-badge.NORMAL {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.attendance-badge.HALF_DAY {
|
||||
background: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.attendance-badge.HALF_HALF_DAY {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.attendance-badge.EARLY_LEAVE {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.hours-status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hours-status-badge.NORMAL {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.hours-status-badge.UNDER {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.hours-status-badge.OVER {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.review-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.review-badge.reviewed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.review-badge.pending {
|
||||
background: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.review-complete-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.review-complete-btn:hover {
|
||||
background: #1e7e34;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 우측 수정 패널 */
|
||||
.edit-panel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.panel-subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.panel-empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
background: #f8f9fa;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.panel-btn.save {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-btn.save:hover {
|
||||
background: #1e7e34;
|
||||
}
|
||||
|
||||
.panel-btn.delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-btn.delete:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.panel-btn.cancel {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-btn.cancel:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
/* 로딩 및 메시지 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 1200px) {
|
||||
.review-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.edit-panel {
|
||||
position: relative;
|
||||
top: 0;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-section {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div id="content-container">
|
||||
<div class="page-header">
|
||||
<h1>🔍 작업보고서 검토</h1>
|
||||
<p class="subtitle">전체 현황을 파악하고 이상 사항을 빠르게 처리하세요.</p>
|
||||
</div>
|
||||
|
||||
<!-- 메시지 영역 -->
|
||||
<div id="message-container"></div>
|
||||
|
||||
<div class="review-container">
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<div class="main-content">
|
||||
<!-- 상단 대시보드 -->
|
||||
<div class="dashboard-section">
|
||||
<div class="dashboard-card total">
|
||||
<div class="dashboard-number" id="totalReports">-</div>
|
||||
<div class="dashboard-label">총 보고서</div>
|
||||
</div>
|
||||
<div class="dashboard-card error">
|
||||
<div class="dashboard-number" id="errorReports">-</div>
|
||||
<div class="dashboard-label">에러 발생</div>
|
||||
</div>
|
||||
<div class="dashboard-card warning">
|
||||
<div class="dashboard-number" id="warningReports">-</div>
|
||||
<div class="dashboard-label">주의 필요</div>
|
||||
</div>
|
||||
<div class="dashboard-card missing">
|
||||
<div class="dashboard-number" id="missingReports">-</div>
|
||||
<div class="dashboard-label">미검토</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 섹션 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label>시작 날짜</label>
|
||||
<input type="date" id="startDate" class="filter-input">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>종료 날짜</label>
|
||||
<input type="date" id="endDate" class="filter-input">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>작업자</label>
|
||||
<select id="workerFilter" class="filter-input">
|
||||
<option value="">전체 작업자</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>프로젝트</label>
|
||||
<select id="projectFilter" class="filter-input">
|
||||
<option value="">전체 프로젝트</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<button type="button" id="applyFilter" class="filter-btn">필터 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 알림 영역 -->
|
||||
<div class="alerts-section">
|
||||
<div class="alerts-header">
|
||||
🚨 주의 필요 항목
|
||||
</div>
|
||||
<div id="alertsList">
|
||||
<!-- 알림 항목들이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 테이블 -->
|
||||
<div class="table-section">
|
||||
<div class="table-header">
|
||||
<div class="table-title">작업보고서 목록</div>
|
||||
<div class="table-actions">
|
||||
<button class="action-btn" id="refreshBtn">🔄 새로고침</button>
|
||||
<button class="action-btn" id="exportBtn">📊 내보내기</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>작업자</th>
|
||||
<th>출근형태</th>
|
||||
<th>기대시간</th>
|
||||
<th>실제시간</th>
|
||||
<th>시간상태</th>
|
||||
<th>프로젝트</th>
|
||||
<th>작업유형</th>
|
||||
<th>상태</th>
|
||||
<th>검토상태</th>
|
||||
<th>액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reportsTableBody">
|
||||
<!-- 데이터가 여기에 표시됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측 수정 패널 -->
|
||||
<div class="edit-panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">빠른 수정</div>
|
||||
<div class="panel-subtitle">항목을 선택하여 수정하세요</div>
|
||||
</div>
|
||||
<div class="panel-content" id="editPanelContent">
|
||||
<div class="panel-empty">
|
||||
<div class="panel-empty-icon">📝</div>
|
||||
<div>수정할 항목을 선택해주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 오버레이 -->
|
||||
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner">
|
||||
<div style="font-size: 24px; margin-bottom: 16px;">⏳</div>
|
||||
<div>데이터를 처리하는 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/js/load-navbar.js"></script>
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
<script type="module" src="/js/work-report-review.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,733 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,164 +0,0 @@
|
||||
<!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="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/daily-work-report.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
|
||||
<script type="module" src="/js/api-config.js?v=3"></script>
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<!-- 뒤로가기 버튼 -->
|
||||
<a href="javascript:history.back()" class="back-button">
|
||||
← 뒤로가기
|
||||
</a>
|
||||
|
||||
<!-- 작업자 정보 카드 -->
|
||||
<div class="worker-info-card" id="workerInfoCard">
|
||||
<div class="worker-avatar-large">
|
||||
<span id="workerInitial">작</span>
|
||||
</div>
|
||||
<div class="worker-info-details">
|
||||
<h2 id="workerName">작업자명</h2>
|
||||
<p id="workerJob">직종</p>
|
||||
<p id="selectedDate">날짜</p>
|
||||
</div>
|
||||
<div class="worker-status-summary" id="workerStatusSummary">
|
||||
<div class="status-item">
|
||||
<span class="status-label">총 작업시간</span>
|
||||
<span class="status-value" id="totalHours">0h</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">작업 건수</span>
|
||||
<span class="status-value" id="workCount">0건</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메시지 영역 -->
|
||||
<div id="message-container"></div>
|
||||
|
||||
<!-- 기존 작업 목록 -->
|
||||
<div class="existing-work-section" id="existingWorkSection">
|
||||
<div class="section-header">
|
||||
<h3>📋 기존 작업 목록</h3>
|
||||
<button class="btn btn-primary" id="addNewWorkBtn">
|
||||
➕ 새 작업 추가
|
||||
</button>
|
||||
</div>
|
||||
<div id="existingWorkList">
|
||||
<!-- 기존 작업들이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 작업 추가 폼 -->
|
||||
<div class="new-work-section" id="newWorkSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h3>➕ 새 작업 추가</h3>
|
||||
<button class="btn btn-secondary" id="cancelNewWorkBtn">
|
||||
✖️ 취소
|
||||
</button>
|
||||
</div>
|
||||
<div class="work-entry" id="newWorkEntry">
|
||||
<div class="work-entry-grid">
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">🏗️</span>
|
||||
프로젝트
|
||||
</label>
|
||||
<select id="newProjectSelect" class="form-select" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⚙️</span>
|
||||
작업 유형
|
||||
</label>
|
||||
<select id="newWorkTypeSelect" class="form-select" required>
|
||||
<option value="">작업 유형을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">📊</span>
|
||||
업무 상태
|
||||
</label>
|
||||
<select id="newWorkStatusSelect" class="form-select" required>
|
||||
<option value="">업무 상태를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="error-type-section" id="newErrorTypeSection">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⚠️</span>
|
||||
에러 유형
|
||||
</label>
|
||||
<select id="newErrorTypeSelect" class="form-select">
|
||||
<option value="">에러 유형을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="time-input-section">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⏰</span>
|
||||
작업 시간 (시간)
|
||||
</label>
|
||||
<input type="number" id="newWorkHours" class="time-input" step="0.25" min="0.25" max="24" value="1.00" required>
|
||||
<div class="quick-time-buttons">
|
||||
<button type="button" class="quick-time-btn" data-hours="0.5">0.5h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="1">1h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="2">2h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="4">4h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="8">8h</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-success" id="saveNewWorkBtn">
|
||||
💾 작업 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 휴가 처리 섹션 -->
|
||||
<div class="vacation-section" id="vacationSection">
|
||||
<div class="section-header">
|
||||
<h3>🏖️ 휴가 처리</h3>
|
||||
</div>
|
||||
<div class="vacation-buttons">
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="full">
|
||||
🏖️ 연차 (8시간)
|
||||
</button>
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="half-half">
|
||||
🌤️ 반반차 (6시간)
|
||||
</button>
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="half">
|
||||
🌅 반차 (4시간)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script type="module" src="/js/load-navbar.js?v=5"></script>
|
||||
<script type="module" src="/js/worker-individual-report.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user