fix(tksupport): 전사 차감 월별 반영 + 테이블 가독성 개선 + 캘린더 차감일 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-25 12:51:00 +09:00
parent 71289be375
commit 08a629f662
3 changed files with 54 additions and 9 deletions

View File

@@ -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: '서버 오류가 발생했습니다' });

View File

@@ -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]);

View File

@@ -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 += '<tr class="border-b border-gray-100">';
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 += `<tr class="${deptBorder}${zebraClass}">`;
if (idx === 0) {
html += `<td class="font-medium text-gray-700 align-top" rowspan="${group.employees.length}">${escapeHtml(group.name)}</td>`;
}
@@ -269,6 +290,7 @@
html += `<td class="text-center ${remainClass}">${remaining % 1 === 0 ? remaining : remaining.toFixed(1)}</td>`;
html += '</tr>';
});
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 += `<div class="cal-cell ${tc.bg} ${tc.text} font-medium" title="${d}일">${tc.label}</div>`;
} else if (isDeduction) {
const tc = TYPE_COLOR.PAID;
html += `<div class="cal-cell ${tc.bg} ${tc.text} font-medium" title="${isDeduction}">${tc.label}</div>`;
} else if (isWeekend || isHoliday) {
html += `<div class="cal-cell weekend" title="${isHoliday || (dow === 0 ? '일' : '토')}">${d}</div>`;
} else {