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 = '
데이터를 불러올 수 없습니다