From f728f84117677ec1db7ddd98f12ba6f45ebc6faf Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 31 Mar 2026 13:29:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(attendance):=20=EC=A3=BC=EB=A7=90+?= =?UTF-8?q?=ED=9A=8C=EC=82=AC=20=ED=9C=B4=EB=AC=B4=EC=9D=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - monthlyComparisonModel: getCompanyHolidays 추가 - monthlyComparisonController: isHoliday에 company_holidays 포함 + holiday_name - proxyInputModel: getDailyStatus에 is_holiday/holiday_name 추가 - proxy-input.js: 휴무일 배너 + both_missing 작업자 비활성화 (특근자는 활성 유지) - 마이그레이션: 2026년 공휴일 16건 일괄 INSERT Co-Authored-By: Claude Opus 4.6 (1M context) --- .../monthlyComparisonController.js | 8 ++-- .../20260331_fix_deduct_days_precision.sql | 17 +++++++ .../api/models/monthlyComparisonModel.js | 20 ++++++++ system1-factory/api/models/proxyInputModel.js | 12 +++++ system1-factory/web/css/proxy-input.css | 13 ++++++ system1-factory/web/js/proxy-input.js | 46 +++++++++++++++---- .../web/pages/work/proxy-input.html | 3 ++ 7 files changed, 107 insertions(+), 12 deletions(-) diff --git a/system1-factory/api/controllers/monthlyComparisonController.js b/system1-factory/api/controllers/monthlyComparisonController.js index b3ec2a2..aeb10f7 100644 --- a/system1-factory/api/controllers/monthlyComparisonController.js +++ b/system1-factory/api/controllers/monthlyComparisonController.js @@ -22,10 +22,11 @@ function determineStatus(report, attendance, isHoliday) { // 날짜별 비교 데이터 생성 async function buildComparisonData(userId, year, month) { - const [reports, attendances, confirmation] = await Promise.all([ + const [reports, attendances, confirmation, holidays] = await Promise.all([ Model.getWorkReports(userId, year, month), Model.getAttendanceRecords(userId, year, month), - Model.getConfirmation(userId, year, month) + Model.getConfirmation(userId, year, month), + Model.getCompanyHolidays(year, month) ]); // 날짜 맵 생성 @@ -57,7 +58,7 @@ async function buildComparisonData(userId, year, month) { const date = new Date(year, month - 1, day); const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dayOfWeek = date.getDay(); - const isHoliday = dayOfWeek === 0 || dayOfWeek === 6; + const isHoliday = dayOfWeek === 0 || dayOfWeek === 6 || holidays.dateSet.has(dateStr); const report = reportMap[dateStr] || null; const attend = attendMap[dateStr] || null; @@ -89,6 +90,7 @@ async function buildComparisonData(userId, year, month) { date: dateStr, day_of_week: DAYS_KR[dayOfWeek], is_holiday: isHoliday, + holiday_name: holidays.nameMap[dateStr] || (dayOfWeek === 0 || dayOfWeek === 6 ? '주말' : null), work_report: report ? { total_hours: parseFloat(report.total_hours), entries: [{ project_name: report.project_names || '', work_type: report.work_type_names || '', hours: parseFloat(report.total_hours) }] diff --git a/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql b/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql index a7070d7..5552b06 100644 --- a/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql +++ b/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql @@ -13,3 +13,20 @@ VALUES ('EARLY_LEAVE', '조퇴', 0.75, 1, 10); -- 작업자 월간 확인 페이지 등록 INSERT IGNORE INTO pages (page_key, page_name, page_path, category, display_order) VALUES ('attendance.my_monthly_confirm', '월간 근무 확인', '/pages/attendance/my-monthly-confirm.html', '근태 관리', 25); +-- 2026년 법정 공휴일 + 대체공휴일 일괄 등록 +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-01-01', '신정', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-02-16', '설날 연휴', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-02-17', '설날', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-02-18', '설날 연휴', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-03-01', '삼일절', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-03-02', '대체공휴일(삼일절)', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-05-05', '어린이날', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-05-24', '석가탄신일', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-06-06', '현충일', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-08-15', '광복절', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-09-24', '추석 연휴', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-09-25', '추석', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-09-26', '추석 연휴', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-10-03', '개천절', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-10-09', '한글날', 'PAID', 1); +INSERT IGNORE INTO company_holidays (holiday_date, holiday_name, holiday_type, created_by) VALUES ('2026-12-25', '크리스마스', 'PAID', 1); diff --git a/system1-factory/api/models/monthlyComparisonModel.js b/system1-factory/api/models/monthlyComparisonModel.js index 4903946..6799f0d 100644 --- a/system1-factory/api/models/monthlyComparisonModel.js +++ b/system1-factory/api/models/monthlyComparisonModel.js @@ -2,6 +2,26 @@ const { getDb } = require('../dbPool'); const MonthlyComparisonModel = { + // 0. 해당 월의 회사 휴무일 조회 + async getCompanyHolidays(year, month) { + const db = await getDb(); + const [rows] = await db.query( + `SELECT holiday_date, holiday_name FROM company_holidays + WHERE YEAR(holiday_date) = ? AND MONTH(holiday_date) = ?`, + [year, month] + ); + const dateSet = new Set(); + const nameMap = {}; + rows.forEach(r => { + const d = r.holiday_date instanceof Date + ? r.holiday_date.toISOString().split('T')[0] + : String(r.holiday_date).split('T')[0]; + dateSet.add(d); + nameMap[d] = r.holiday_name; + }); + return { dateSet, nameMap }; + }, + // 1. 작업보고서 일별 합산 async getWorkReports(userId, year, month) { const db = await getDb(); diff --git a/system1-factory/api/models/proxyInputModel.js b/system1-factory/api/models/proxyInputModel.js index 6ff7488..2d739ea 100644 --- a/system1-factory/api/models/proxyInputModel.js +++ b/system1-factory/api/models/proxyInputModel.js @@ -110,6 +110,16 @@ const ProxyInputModel = { WHERE dar.record_date = ? AND dar.vacation_type_id IS NOT NULL `, [date]); + // 5. 해당 날짜가 회사 휴무일인지 확인 + const [holidayRows] = await db.query( + `SELECT holiday_date, holiday_name FROM company_holidays WHERE holiday_date = ?`, + [date] + ); + const isCompanyHoliday = holidayRows.length > 0; + const holidayName = isCompanyHoliday ? holidayRows[0].holiday_name : null; + const dateObj = new Date(date); + const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6; + // 메모리에서 조합 const tbmMap = {}; tbmAssignments.forEach(ta => { @@ -158,6 +168,8 @@ const ProxyInputModel = { return { date, + is_holiday: isWeekend || isCompanyHoliday, + holiday_name: isCompanyHoliday ? holidayName : (isWeekend ? '주말' : null), summary: { total_active_workers: workers.length, tbm_completed: tbmCompleted, diff --git a/system1-factory/web/css/proxy-input.css b/system1-factory/web/css/proxy-input.css index a505968..a62e746 100644 --- a/system1-factory/web/css/proxy-input.css +++ b/system1-factory/web/css/proxy-input.css @@ -227,6 +227,19 @@ } @keyframes ds-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } +/* 휴무일 배너 */ +.pi-holiday-banner { + padding: 8px 12px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + font-size: 0.8rem; + font-weight: 600; + color: #166534; + text-align: center; + margin-bottom: 8px; +} + /* 연차 비활성화 */ .pi-card.vacation-disabled { opacity: 0.5; } .pi-card.vacation-disabled .pi-card-form { pointer-events: none; } diff --git a/system1-factory/web/js/proxy-input.js b/system1-factory/web/js/proxy-input.js index 69273db..6d58395 100644 --- a/system1-factory/web/js/proxy-input.js +++ b/system1-factory/web/js/proxy-input.js @@ -128,7 +128,32 @@ async function loadWorkers() { } if (!res || !res.success) { cardsEl.innerHTML = '

데이터를 불러올 수 없습니다

'; return; } + // 휴무일 정보 저장 + window._isHoliday = res.data.is_holiday || false; + window._holidayName = res.data.holiday_name || null; + + // 휴무일 뱃지 표시 + var holidayBanner = document.getElementById('holidayBanner'); + if (holidayBanner) { + if (window._isHoliday) { + holidayBanner.classList.remove('hidden'); + holidayBanner.textContent = '휴무일' + (window._holidayName ? ' (' + window._holidayName + ')' : ''); + } else { + holidayBanner.classList.add('hidden'); + } + } + missingWorkers = (res.data.workers || []).filter(w => w.status !== 'complete'); + + // 휴무일에 both_missing인 작업자에게 휴무 플래그 추가 + if (window._isHoliday) { + missingWorkers.forEach(function(w) { + if (w.status === 'both_missing' && !w.vacation_type_code) { + w._isHolidayOff = true; + } + }); + } + document.getElementById('missingNum').textContent = missingWorkers.length; if (missingWorkers.length === 0) { @@ -147,8 +172,10 @@ function renderCards() { const cardsEl = document.getElementById('workerCards'); cardsEl.innerHTML = missingWorkers.map(w => { const isFullVacation = w.vacation_type_code === 'ANNUAL_FULL'; + const isHolidayOff = !!w._isHolidayOff; + const isDisabled = isFullVacation || isHolidayOff; const hasVacation = !!w.vacation_type_code; - const statusLabel = isFullVacation ? '' + const statusLabel = isDisabled ? '' : ({ both_missing: 'TBM+보고서 미입력', tbm_only: '보고서만 미입력', report_only: 'TBM만 미입력' }[w.status] || ''); const fd = workerFormData[w.user_id] || getDefaultFormData(w); if (hasVacation && !isFullVacation && w.vacation_hours != null) { @@ -156,13 +183,14 @@ function renderCards() { } workerFormData[w.user_id] = fd; const sel = selectedIds.has(w.user_id); - const vacBadge = hasVacation ? '' + escHtml(w.vacation_type_name) + '' : ''; - const disabledClass = isFullVacation ? ' vacation-disabled' : ''; + var badgeText = isHolidayOff ? '휴무' : (hasVacation ? w.vacation_type_name : ''); + const vacBadge = badgeText ? '' + escHtml(badgeText) + '' : ''; + const disabledClass = isDisabled ? ' vacation-disabled' : ''; return `
-
${isFullVacation ? '' : (sel ? '' : '')}
+
${isDisabled ? '' : (sel ? '' : '')}
${escHtml(w.worker_name)} ${vacBadge}
${escHtml(w.job_type)} · ${escHtml(w.department_name)}
@@ -237,7 +265,7 @@ function escHtml(s) { return (s || '').replace(/&/g, '&').replace(/ x.user_id === uid); - if (ww && ww.vacation_type_code === 'ANNUAL_FULL') { + if (ww && (ww.vacation_type_code === 'ANNUAL_FULL' || ww._isHolidayOff)) { selectedIds.delete(uid); } } diff --git a/system1-factory/web/pages/work/proxy-input.html b/system1-factory/web/pages/work/proxy-input.html index 70a5058..bb92a6f 100644 --- a/system1-factory/web/pages/work/proxy-input.html +++ b/system1-factory/web/pages/work/proxy-input.html @@ -54,6 +54,9 @@
+ + +