From 10fd65ba9e14230c97a476dd265ed878c8f967f4 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 1 Apr 2026 08:20:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(monthly-comparison):=200=EC=9D=BC=200h=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20+=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=ED=83=9C=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getAllStatus: daily_attendance_records JOIN으로 실제 근무일/시간 집계 - vacation_days: vacation_types.deduct_days SUM (반차 0.5 정확 반영) - admin_checked 컬럼 + POST /admin-check API (upsert 패턴) - 상태 뱃지 라벨: 미검토/확인요청/수정요청/반려/확인완료 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../monthlyComparisonController.js | 14 ++++++++ .../20260401_monthly_confirm_workflow.sql | 1 + .../api/models/monthlyComparisonModel.js | 35 ++++++++++++++++--- .../api/routes/monthlyComparisonRoutes.js | 3 ++ system1-factory/web/js/monthly-comparison.js | 8 ++--- .../pages/attendance/monthly-comparison.html | 2 +- 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/system1-factory/api/controllers/monthlyComparisonController.js b/system1-factory/api/controllers/monthlyComparisonController.js index 8f75b95..9f0f3d9 100644 --- a/system1-factory/api/controllers/monthlyComparisonController.js +++ b/system1-factory/api/controllers/monthlyComparisonController.js @@ -262,6 +262,20 @@ const MonthlyComparisonController = { } }, + // POST /admin-check (관리자 개별 검토 태깅) + adminCheck: async (req, res) => { + try { + const { user_id, year, month, checked } = req.body; + if (!user_id || !year || !month) return res.status(400).json({ success: false, message: 'user_id, year, month 필수' }); + const checkedBy = req.user.user_id || req.user.id; + const result = await Model.adminCheck(parseInt(user_id), parseInt(year), parseInt(month), !!checked, checkedBy); + res.json({ success: true, data: result, message: checked ? '검토완료 표시' : '검토 해제' }); + } catch (err) { + logger.error('monthlyComparison adminCheck error:', err); + res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' }); + } + }, + // GET /all-status (support_team+) getAllStatus: async (req, res) => { try { diff --git a/system1-factory/api/db/migrations/20260401_monthly_confirm_workflow.sql b/system1-factory/api/db/migrations/20260401_monthly_confirm_workflow.sql index 9786cbf..83c9745 100644 --- a/system1-factory/api/db/migrations/20260401_monthly_confirm_workflow.sql +++ b/system1-factory/api/db/migrations/20260401_monthly_confirm_workflow.sql @@ -4,3 +4,4 @@ ALTER TABLE monthly_work_confirmations ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS reviewed_by INT NULL; ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMP NULL; ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS change_details TEXT NULL; +ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS admin_checked TINYINT(1) DEFAULT 0; diff --git a/system1-factory/api/models/monthlyComparisonModel.js b/system1-factory/api/models/monthlyComparisonModel.js index 7a6eae9..805eeec 100644 --- a/system1-factory/api/models/monthlyComparisonModel.js +++ b/system1-factory/api/models/monthlyComparisonModel.js @@ -256,7 +256,7 @@ const MonthlyComparisonModel = { } }, - // 5. 전체 작업자 확인 현황 + // 5. 전체 작업자 확인 현황 (실제 근태 데이터 집계 포함) async getAllStatus(year, month, departmentId) { const db = await getDb(); let sql = ` @@ -265,15 +265,29 @@ const MonthlyComparisonModel = { COALESCE(d.department_name, '미배정') AS department_name, COALESCE(mwc.status, 'pending') AS status, mwc.confirmed_at, mwc.rejected_at, mwc.reject_reason, - mwc.total_work_days, mwc.total_work_hours, - mwc.total_overtime_hours, mwc.vacation_days, mwc.mismatch_count + mwc.change_details, COALESCE(mwc.admin_checked, 0) AS admin_checked, + COALESCE(att.work_days, 0) AS total_work_days, + COALESCE(att.work_hours, 0) AS total_work_hours, + COALESCE(att.overtime_hours, 0) AS total_overtime_hours, + COALESCE(att.vac_days, 0) AS vacation_days FROM workers w LEFT JOIN departments d ON w.department_id = d.department_id LEFT JOIN monthly_work_confirmations mwc ON w.user_id = mwc.user_id AND mwc.year = ? AND mwc.month = ? - WHERE w.status = 'active' + LEFT JOIN ( + SELECT dar.user_id, + COUNT(CASE WHEN dar.total_work_hours > 0 THEN 1 END) AS work_days, + COALESCE(SUM(dar.total_work_hours), 0) AS work_hours, + COALESCE(SUM(CASE WHEN dar.total_work_hours > 8 THEN dar.total_work_hours - 8 ELSE 0 END), 0) AS overtime_hours, + COALESCE(SUM(CASE WHEN vt.deduct_days IS NOT NULL THEN vt.deduct_days ELSE 0 END), 0) AS vac_days + FROM daily_attendance_records dar + LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id + WHERE YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ? + GROUP BY dar.user_id + ) att ON w.user_id = att.user_id + WHERE w.status = 'active' AND w.user_id IS NOT NULL `; - const params = [year, month]; + const params = [year, month, year, month]; if (departmentId) { sql += ' AND w.department_id = ?'; params.push(departmentId); @@ -284,6 +298,17 @@ const MonthlyComparisonModel = { return rows; }, + // 5b. 관리자 개별 검토 태깅 + async adminCheck(userId, year, month, checked, checkedBy) { + const db = await getDb(); + await db.query(` + INSERT INTO monthly_work_confirmations (user_id, year, month, status, admin_checked) + VALUES (?, ?, ?, 'pending', ?) + ON DUPLICATE KEY UPDATE admin_checked = ? + `, [userId, year, month, checked ? 1 : 0, checked ? 1 : 0]); + return { admin_checked: checked }; + }, + // 6. 지원팀 사용자 목록 (알림 수신자) async getSupportTeamUsers() { const db = await getDb(); diff --git a/system1-factory/api/routes/monthlyComparisonRoutes.js b/system1-factory/api/routes/monthlyComparisonRoutes.js index c6f5d87..079082f 100644 --- a/system1-factory/api/routes/monthlyComparisonRoutes.js +++ b/system1-factory/api/routes/monthlyComparisonRoutes.js @@ -29,6 +29,9 @@ router.post('/review-send', requireSupportTeam, ctrl.reviewSend); // 관리자: 수정요청 응답 (change_request → review_sent 또는 rejected) router.post('/review-respond', requireSupportTeam, ctrl.reviewRespond); +// 관리자: 개별 검토 태깅 +router.post('/admin-check', requireSupportTeam, ctrl.adminCheck); + // 전체 현황 (support_team+) router.get('/all-status', requireSupportTeam, ctrl.getAllStatus); diff --git a/system1-factory/web/js/monthly-comparison.js b/system1-factory/web/js/monthly-comparison.js index b017970..b023b87 100644 --- a/system1-factory/web/js/monthly-comparison.js +++ b/system1-factory/web/js/monthly-comparison.js @@ -361,9 +361,9 @@ function renderWorkerList(workers) { } el.innerHTML = filtered.map(w => { - const statusBadge = `${ - { confirmed: '확인완료', pending: '미확인', rejected: '반려' }[w.status] || '' - }`; + const statusLabels = { confirmed: '확인완료', pending: '미검토', review_sent: '확인요청', change_request: '수정요청', rejected: '반려' }; + const statusBadge = `${statusLabels[w.status] || ''}`; + const checkedBadge = w.admin_checked ? ' ✓검토' : ''; const mismatchBadge = w.mismatch_count > 0 ? `⚠️ 불일치${w.mismatch_count}` : ''; const rejectReason = w.status === 'rejected' && w.reject_reason @@ -374,7 +374,7 @@ function renderWorkerList(workers) {
-
${escHtml(w.worker_name)} ${mismatchBadge}
+
${escHtml(w.worker_name)}${checkedBadge} ${mismatchBadge}
${escHtml(w.department_name)} · ${escHtml(w.job_type)}
diff --git a/system1-factory/web/pages/attendance/monthly-comparison.html b/system1-factory/web/pages/attendance/monthly-comparison.html index a5b4a97..6370618 100644 --- a/system1-factory/web/pages/attendance/monthly-comparison.html +++ b/system1-factory/web/pages/attendance/monthly-comparison.html @@ -162,7 +162,7 @@ - +