From 4309d308bc0df67e509928c02a99fa693a856e30 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 1 Apr 2026 08:37:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(monthly-comparison):=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=83=81=EB=8B=A8=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?+=20=EC=9B=94=20=EC=9C=A0=EC=A7=80=20+=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=A1=B0=EA=B1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검토완료 버튼: 하단 제거 → 헤더 토글 ("검토하기" ↔ "✓ 검토완료") - 상세→목록 복귀: year/month URL 유지 (4월로 리셋 방지) - 확인요청: 전원 admin_checked 시만 활성화 (정책 변경) - Sprint 004 PLAN.md: 정책 변경 이력 추가 Co-Authored-By: Claude Opus 4.6 (1M context) --- .cowork/sprints/sprint-004/PLAN.md | 154 ++++++++++++++++++ system1-factory/web/js/monthly-comparison.js | 57 +++++-- .../pages/attendance/monthly-comparison.html | 13 +- 3 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 .cowork/sprints/sprint-004/PLAN.md diff --git a/.cowork/sprints/sprint-004/PLAN.md b/.cowork/sprints/sprint-004/PLAN.md new file mode 100644 index 0000000..2cbe058 --- /dev/null +++ b/.cowork/sprints/sprint-004/PLAN.md @@ -0,0 +1,154 @@ +# Sprint 004: 월간 근무 비교·확인·정산 + +> 작성: Cowork | 초안: 2026-03-30 | 갱신: 2026-03-31 (출근부 양식 반영) + +## 목표 +1. **작업보고서 vs 근태관리 비교 페이지**: 그룹장이 입력한 작업보고서와 근태관리 데이터를 일별로 비교하여 불일치를 시각화 +2. **월말 확인 프로세스**: 작업자가 자신의 월간 근무 내역을 확인(승인/반려)하는 워크플로우 +3. **출근부 엑셀 다운로드**: 전원 확인 완료 시, 업로드된 양식에 맞는 출근부 엑셀 내보내기 +4. **연차 잔액 연동**: sp_vacation_balances 기반 신규/사용/잔여 데이터 엑셀 포함 + +## 현재 구현 상태 (2026-03-31) + +| 항목 | 파일 | 상태 | 비고 | +|------|------|------|------| +| DB 마이그레이션 | `api/db/migrations/20260330_create_monthly_work_confirmations.sql` | ✅ 완료 | | +| 모델 | `api/models/monthlyComparisonModel.js` (232줄) | ✅ 완료 | getExportData 추가됨 | +| 컨트롤러 | `api/controllers/monthlyComparisonController.js` (560줄) | ✅ 완료 | exportExcel 출근부 양식 재작성 | +| 라우트 | `api/routes/monthlyComparisonRoutes.js` (33줄) | ✅ 완료 | | +| 라우트 등록 | `api/routes.js` 157줄 | ✅ 완료 | | +| 프론트엔드 HTML | `web/pages/attendance/monthly-comparison.html` | ✅ 완료 | | +| 프론트엔드 JS | `web/js/monthly-comparison.js` (558줄) | ✅ 완료 | | +| 프론트엔드 CSS | `web/css/monthly-comparison.css` | ✅ 완료 | | +| 대시보드 연동 | `web/js/production-dashboard.js` | ✅ 완료 | PAGE_ICONS 추가 | + +> 모든 파일이 존재하고 라우트가 등록되어 있으나, **실 서버 배포 및 통합 테스트 미완료**. + +## 배경 +- monthly.html에서 그룹장이 등록한 근태정보를 각 작업자가 개별 확인·승인 +- 전원 confirmed 완료 시 지원팀이 출근부 엑셀 다운로드 가능 +- 출근부 양식: 업로드된 `출근부_2026.02_TK 생산팀.xlsx` 기준 +- attendanceService.js에서 연차 자동 차감/복원이 이미 구현되어 있으므로 sp_vacation_balances가 소스 오브 트루스 + +## 아키텍처 + +``` +┌──────────────────────────────────────────────────────┐ +│ monthly.html (그룹장 → 근태 입력) │ +│ ↓ │ +│ daily_attendance_records 저장 │ +│ attendanceService.js → sp_vacation_balances 자동 반영 │ +└─────────────────┬────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────┐ +│ monthly-comparison.html │ +│ │ +│ [작업자 뷰] [관리자 뷰] │ +│ ├ 작업보고서 vs 근태 일별 비교 ├ 전체 확인 현황 │ +│ ├ 요약 카드 (근무일/시간/연장/휴가) ├ 개별 작업자 상세 │ +│ └ 확인(승인) / 반려(사유 입력) └ 엑셀 다운로드 │ +│ ↓ (반려 시) │ +│ notifications → 지원팀에 알림 │ +│ ↓ (전원 확인 완료 시) │ +│ 출근부 엑셀 다운로드 │ +│ (출근부_YYYY.MM_부서명.xlsx) │ +└──────────────────────────────────────────────────────┘ +``` + +## 출근부 엑셀 양식 (신규) + +기존 2-시트 구조(월간 근무 현황 + 일별 상세)를 **출근부 단일 시트**로 변경. + +``` +시트명: {YY}.{M}월 출근부 + +Row 2: 부서명 | [부서명 (병합)] +Row 3: 근로기간 | [YYYY년 M월 (병합)] +Row 4: [이름] [담당] 1 2 3 ... 31 [총시간] [M월(병합)] [비고(병합)] +Row 5: 일 월 화 ... [ ] 신규 사용 잔여 +Row 6+: 김두수 1 0 0 0 ... 휴무 =SUM() 15 2 =AF-AG + +셀 값 규칙: + 정상출근 → 근무시간(숫자, 0.00) + 주말(근무없음) → '휴무' + 연차(ANNUAL) → '연차' + 반차(HALF_ANNUAL) → '반차' + 반반차(ANNUAL_QUARTER) → '반반차' + 조퇴(EARLY_LEAVE) → '조퇴' + 병가(SICK) → '병가' + 경조사(SPECIAL) → '경조사' + +스타일: + 폰트: 맑은 고딕 12pt, 가운데 정렬 + 주말 헤더: 빨간 폰트 (FFFF0000) + 테두리: thin 전체 + 헤더 행 높이: 40, 데이터 행 높이: 60 + 수식: 총시간=SUM(날짜열), 잔여=신규-사용 +``` + +## API 엔드포인트 + +| Method | Path | 접근권한 | 설명 | +|--------|------|---------|------| +| GET | /api/monthly-comparison/my-records | 인증된 사용자 | 내 일별 비교 데이터 | +| GET | /api/monthly-comparison/records | support_team+ 또는 본인 | 특정 작업자 비교 조회 | +| POST | /api/monthly-comparison/confirm | 인증된 사용자 (본인) | 승인/반려 | +| GET | /api/monthly-comparison/all-status | support_team+ | 전체 확인 현황 | +| GET | /api/monthly-comparison/export | support_team+ | 출근부 엑셀 다운로드 | + +## 데이터 소스 + +| 데이터 | 테이블 | 비고 | +|--------|--------|------| +| 작업보고서 | daily_work_reports | report_date, work_hours, project_id | +| 근태 기록 | daily_attendance_records | record_date, total_work_hours, vacation_type_id | +| 확인 상태 | monthly_work_confirmations | 신규 테이블 (Sprint 004) | +| 연차 잔액 | sp_vacation_balances | year별 SUM(total_days, used_days) | +| 휴가 유형 | vacation_types | type_code → 출근부 텍스트 매핑 | +| 작업자 정보 | workers + departments | worker_id 순 정렬 | + +## 작업 분할 + +| 섹션 | 핵심 작업 | 규모 | +|------|----------|------| +| **A (Backend)** | 모델·컨트롤러 검증 + 출근부 엑셀 엣지케이스 + 배포 | 파일 4개, 검증 위주 | +| **B (Frontend)** | UI 최종 검증 + 모바일 반응형 + 캐시 버스팅 + 배포 | 파일 4개, 검증 위주 | + +> 기존 코드가 모두 구현되어 있으므로 **검증·배포 중심**으로 진행. + +## 섹션 간 의존성 + +``` +A ──(API 제공)──→ B + + GET /api/monthly-comparison/my-records + POST /api/monthly-comparison/confirm + GET /api/monthly-comparison/all-status + GET /api/monthly-comparison/records + GET /api/monthly-comparison/export +``` + +Section A 배포 완료 후 Section B 프론트엔드가 정상 동작 가능. + +## 완료 조건 +- [x] 마이그레이션 파일 생성 (20260330_create_monthly_work_confirmations.sql) +- [x] 모든 Backend 파일 생성 (model, controller, routes) +- [x] routes.js에 라우트 등록 +- [x] 프론트엔드 페이지 생성 (HTML, JS, CSS) +- [x] 대시보드 아이콘 매핑 추가 +- [x] 출근부 양식 엑셀 재작성 (getExportData + exportExcel) +- [ ] 실 서버 마이그레이션 실행 +- [ ] Docker 빌드·배포 +- [ ] 통합 테스트 (작업자 확인 → 지원팀 엑셀 다운로드 전체 플로우) +- [ ] 모바일 반응형 확인 +- [ ] 엣지케이스 확인 (데이터 없는 월, 중도 입사자 등) + +## 주요 변경 이력 + +| 날짜 | 변경 내용 | +|------|----------| +| 2026-03-30 | 초안 작성 + Section A/B 코드 구현 | +| 2026-03-31 | 출근부 양식 반영: getExcelData→getExportData, exportExcel 전면 재작성 | +| 2026-03-31 | 버그 수정: export 조건에 rejected 추가, admin detail 모드 버튼 숨김 | +| 2026-04-01 | 워크플로우 확장: pending→review_sent→confirmed/change_request/rejected 상태 추가 | +| 2026-04-01 | 정책 변경: 확인요청 발송 전 전원 admin_checked 필수 (선택적→필수) | diff --git a/system1-factory/web/js/monthly-comparison.js b/system1-factory/web/js/monthly-comparison.js index 5995de8..5e8a834 100644 --- a/system1-factory/web/js/monthly-comparison.js +++ b/system1-factory/web/js/monthly-comparison.js @@ -165,22 +165,21 @@ async function loadMyRecords() { comparisonData = res.data; - // detail 모드: 작업자 이름 표시 + // detail 모드: 작업자 이름 + 검토완료 버튼 (상단 헤더) if (currentMode === 'detail' && comparisonData.user) { - document.getElementById('pageTitle').textContent = - (comparisonData.user.worker_name || '') + ' 근무 비교'; + var isChecked = comparisonData.confirmation && comparisonData.confirmation.admin_checked; + var checkBtnHtml = ''; + document.getElementById('pageTitle').innerHTML = + (comparisonData.user.worker_name || '') + ' 근무 비교' + checkBtnHtml; } renderSummaryCards(comparisonData.summary); renderMismatchAlert(comparisonData.summary); renderDailyList(comparisonData.daily_records || []); renderConfirmationStatus(comparisonData.confirmation); - - // detail 모드: 검토완료 버튼 표시 - var adminCheckBtn = document.getElementById('adminCheckBtn'); - if (adminCheckBtn && currentMode === 'detail') { - adminCheckBtn.classList.remove('hidden'); - } } catch (e) { listEl.innerHTML = '

네트워크 오류

'; } @@ -348,13 +347,21 @@ function renderAdminSummary(s) { `📝 ${s.change_request || 0} 수정요청` + `❌ ${s.rejected || 0} 반려`; - // 확인요청 일괄 발송 버튼 + // 확인요청 일괄 발송 버튼 — 전원 검토완료 시만 활성화 var reviewBtn = document.getElementById('reviewSendBtn'); if (reviewBtn) { var pendingCount = (s.pending || 0); - if (pendingCount > 0) { + var uncheckedCount = (adminData?.workers || []).filter(function(w) { return !w.admin_checked && w.status === 'pending'; }).length; + if (pendingCount > 0 && uncheckedCount === 0) { reviewBtn.classList.remove('hidden'); - reviewBtn.textContent = `미검토 ${pendingCount}명 확인요청 발송`; + reviewBtn.disabled = false; + reviewBtn.textContent = `${pendingCount}명 확인요청 발송`; + reviewBtn.style.background = '#2563eb'; + } else if (pendingCount > 0 && uncheckedCount > 0) { + reviewBtn.classList.remove('hidden'); + reviewBtn.disabled = true; + reviewBtn.textContent = `${uncheckedCount}명 미검토 — 전원 검토 후 발송 가능`; + reviewBtn.style.background = '#9ca3af'; } else { reviewBtn.classList.add('hidden'); } @@ -524,17 +531,28 @@ async function downloadExcel() { } } -// ===== Admin Check (검토완료 태깅) ===== -async function markAdminChecked() { +// ===== Admin Check (검토완료 토글) ===== +async function toggleAdminCheck() { if (!currentUserId || isProcessing) return; + var isCurrentlyChecked = comparisonData?.confirmation?.admin_checked; + var newChecked = !isCurrentlyChecked; isProcessing = true; try { var res = await window.apiCall('/monthly-comparison/admin-check', 'POST', { - user_id: currentUserId, year: currentYear, month: currentMonth, checked: true + user_id: currentUserId, year: currentYear, month: currentMonth, checked: newChecked }); if (res && res.success) { - showToast((comparisonData?.user?.worker_name || '') + ' 검토완료', 'success'); - history.back(); + // 상태 업데이트 + if (comparisonData.confirmation) { + comparisonData.confirmation.admin_checked = newChecked ? 1 : 0; + } + var btn = document.getElementById('headerCheckBtn'); + if (btn) { + btn.textContent = newChecked ? '✓ 검토완료' : '검토하기'; + btn.style.background = newChecked ? '#dcfce7' : '#f3f4f6'; + btn.style.color = newChecked ? '#166534' : '#6b7280'; + } + showToast(newChecked ? '검토완료' : '검토 해제', 'success'); } else { showToast(res?.message || '처리 실패', 'error'); } @@ -542,6 +560,11 @@ async function markAdminChecked() { finally { isProcessing = false; } } +// 목록으로 복귀 (월 유지) +function goBackToList() { + location.href = '/pages/attendance/monthly-comparison.html?mode=admin&year=' + currentYear + '&month=' + currentMonth; +} + // ===== Review Send (확인요청 일괄 발송) ===== async function sendReviewAll() { if (isProcessing) return; diff --git a/system1-factory/web/pages/attendance/monthly-comparison.html b/system1-factory/web/pages/attendance/monthly-comparison.html index 705226b..d812f0b 100644 --- a/system1-factory/web/pages/attendance/monthly-comparison.html +++ b/system1-factory/web/pages/attendance/monthly-comparison.html @@ -37,8 +37,8 @@
- -

월간 근무 비교

+ +

월간 근무 비교

@@ -87,12 +87,7 @@
- - +
@@ -169,7 +164,7 @@ - +