From 08a629f662d2389f132801b43d5ffbed01b6bd2d Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 25 Mar 2026 12:51:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(tksupport):=20=EC=A0=84=EC=82=AC=20?= =?UTF-8?q?=EC=B0=A8=EA=B0=90=20=EC=9B=94=EB=B3=84=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?+=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20+=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=B0=A8=EA=B0=90=EC=9D=BC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vacationDashboardController.js | 7 ++-- .../api/models/vacationDashboardModel.js | 17 +++++++- tksupport/web/vacation-dashboard.html | 39 +++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/tksupport/api/controllers/vacationDashboardController.js b/tksupport/api/controllers/vacationDashboardController.js index de9eb22..6b231f7 100644 --- a/tksupport/api/controllers/vacationDashboardController.js +++ b/tksupport/api/controllers/vacationDashboardController.js @@ -51,11 +51,12 @@ const vacationDashboardController = { async getYearlyOverview(req, res) { try { const year = parseInt(req.query.year) || new Date().getFullYear(); - const [users, balances] = await Promise.all([ + const [users, balances, companyDeductions] = await Promise.all([ vacationDashboardModel.getYearlyOverview(year), - vacationDashboardModel.getBalances(year) + vacationDashboardModel.getBalances(year), + vacationDashboardModel.getCompanyDeductions(year) ]); - res.json({ success: true, data: { users, balances } }); + res.json({ success: true, data: { users, balances, companyDeductions } }); } catch (error) { console.error('연간 총괄 조회 오류:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' }); diff --git a/tksupport/api/models/vacationDashboardModel.js b/tksupport/api/models/vacationDashboardModel.js index 41559a9..059853c 100644 --- a/tksupport/api/models/vacationDashboardModel.js +++ b/tksupport/api/models/vacationDashboardModel.js @@ -56,7 +56,7 @@ const vacationDashboardModel = { const db = getPool(); const [rows] = await db.query(` SELECT - su.user_id, su.name, su.username, + su.user_id, su.name, su.username, su.hire_date, COALESCE(d.department_id, 0) as department_id, COALESCE(d.department_name, '미배정') as department_name, MONTH(vr.start_date) as month, @@ -104,11 +104,24 @@ const vacationDashboardModel = { return rows; }, + // View 1: 전사 휴가 차감 내역 + async getCompanyDeductions(year) { + const db = getPool(); + const [rows] = await db.query(` + SELECT holiday_date, holiday_name, MONTH(holiday_date) as month + FROM company_holidays + WHERE YEAR(holiday_date) = ? + AND holiday_type = 'ANNUAL_DEDUCT' + AND deduction_applied_at IS NOT NULL + `, [year]); + return rows; + }, + // View 2: 공휴일 표시용 async getHolidays(year, month) { const db = getPool(); const [rows] = await db.query(` - SELECT holiday_date, holiday_name + SELECT holiday_date, holiday_name, holiday_type, deduction_applied_at FROM company_holidays WHERE YEAR(holiday_date) = ? AND MONTH(holiday_date) = ? `, [year, month]); diff --git a/tksupport/web/vacation-dashboard.html b/tksupport/web/vacation-dashboard.html index 050e2ab..c10c97d 100644 --- a/tksupport/web/vacation-dashboard.html +++ b/tksupport/web/vacation-dashboard.html @@ -212,18 +212,36 @@ } function renderYearlyTable(data) { - const { users: rows, balances: balRows } = data; + const { users: rows, balances: balRows, companyDeductions: deductions } = data; const balMap = {}; balRows.forEach(b => { balMap[b.user_id] = { granted: parseFloat(b.granted || 0), used: parseFloat(b.used || 0) }; }); + // 전사 차감 월별 목록 (holiday_date 포함) + const deductionsByMonth = {}; + (deductions || []).forEach(d => { + if (!deductionsByMonth[d.month]) deductionsByMonth[d.month] = []; + deductionsByMonth[d.month].push(d.holiday_date); + }); + // 직원별 월 데이터 병합 const empMap = {}; rows.forEach(r => { if (!empMap[r.user_id]) { - empMap[r.user_id] = { user_id: r.user_id, name: r.name, username: r.username, department_id: r.department_id, department_name: r.department_name, months: {} }; + empMap[r.user_id] = { user_id: r.user_id, name: r.name, username: r.username, hire_date: r.hire_date, department_id: r.department_id, department_name: r.department_name, months: {} }; } if (r.month !== null) empMap[r.user_id].months[r.month] = parseFloat(r.total_days); }); + + // 전사 차감분 합산 (hire_date <= holiday_date 조건) + Object.values(empMap).forEach(emp => { + Object.entries(deductionsByMonth).forEach(([m, dates]) => { + dates.forEach(hDate => { + if (emp.hire_date && emp.hire_date.substring(0, 10) <= hDate.substring(0, 10)) { + emp.months[m] = (emp.months[m] || 0) + 1; + } + }); + }); + }); const employees = Object.values(empMap); // 부서 필터 @@ -245,12 +263,15 @@ } let html = ''; + let deptIdx = 0; Object.entries(deptGroups).forEach(([deptId, group]) => { group.employees.forEach((emp, idx) => { const bal = balMap[emp.user_id] || { granted: 0, used: 0 }; const remaining = bal.granted - bal.used; const remainClass = remaining <= 3 ? 'text-orange-600 font-bold' : 'text-gray-800'; - html += ''; + const zebraClass = idx % 2 === 1 ? ' bg-gray-50' : ''; + const deptBorder = idx === 0 && deptIdx > 0 ? ' border-t-2 border-gray-300' : ' border-b border-gray-100'; + html += ``; if (idx === 0) { html += `${escapeHtml(group.name)}`; } @@ -269,6 +290,7 @@ html += `${remaining % 1 === 0 ? remaining : remaining.toFixed(1)}`; html += ''; }); + deptIdx++; }); tbody.innerHTML = html; } @@ -308,9 +330,14 @@ const container = document.getElementById('calendarContainer'); const daysInMonth = new Date(year, month, 0).getDate(); const holidaySet = {}; + const deductionSet = {}; holidays.forEach(h => { const d = new Date(h.holiday_date).getDate(); - holidaySet[d] = h.holiday_name; + if (h.holiday_type === 'ANNUAL_DEDUCT' && h.deduction_applied_at) { + deductionSet[d] = h.holiday_name; + } else { + holidaySet[d] = h.holiday_name; + } }); // user_id별 그룹핑 @@ -358,9 +385,13 @@ const isHoliday = holidaySet[d]; const vacType = dayVacation[d]; + const isDeduction = deductionSet[d]; if (vacType) { const tc = TYPE_COLOR[vacType] || DEFAULT_TYPE; html += `
${tc.label}
`; + } else if (isDeduction) { + const tc = TYPE_COLOR.PAID; + html += `
${tc.label}
`; } else if (isWeekend || isHoliday) { html += `
${d}
`; } else {