fix(vacation): 배정일수 음수 허용 + 특별휴가 우선 차감

- vbTotalDays min="0" 제거 (보정용 음수 입력 허용)
- deductDays 2단계 차감:
  1단계: vacation_type_id 정확 매칭 잔액 우선 (배우자출산 등)
  2단계: 나머지를 이월→기본→추가→장기→회사 순서로

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-31 09:32:51 +09:00
parent 8016237038
commit 9cbf4c98a5
2 changed files with 33 additions and 9 deletions

View File

@@ -61,7 +61,9 @@ const vacationBalanceModel = {
return result;
},
// 우선순위: 이월 → 기본연차 → 추가부여 → 장기근속 → 회사부여
// 차감 우선순위:
// 1. 특별휴가(배우자출산 등) — vacation_type_id가 정확히 일치하는 잔액 먼저
// 2. 이월 → 기본연차 → 추가부여 → 장기근속 → 회사부여 순서
async deductDays(userId, vacationTypeId, year, daysToDeduct, conn) {
const db = conn || getPool();
const needRelease = !conn;
@@ -69,16 +71,16 @@ const vacationBalanceModel = {
try {
if (needRelease) await c.beginTransaction();
const [balances] = await c.query(`
// 1단계: 해당 vacation_type_id와 정확히 매칭되는 잔액 우선 차감 (특별휴가)
const [exactMatch] = await c.query(`
SELECT id, total_days, used_days, (total_days - used_days) AS remaining_days, balance_type
FROM sp_vacation_balances
WHERE user_id = ? AND year = ? AND (total_days - used_days) > 0
ORDER BY FIELD(balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT')
WHERE user_id = ? AND year = ? AND vacation_type_id = ? AND (total_days - used_days) > 0
FOR UPDATE
`, [userId, year]);
`, [userId, year, vacationTypeId]);
let remaining = daysToDeduct;
for (const b of balances) {
for (const b of exactMatch) {
if (remaining <= 0) break;
const toDeduct = Math.min(remaining, parseFloat(b.remaining_days));
if (toDeduct > 0) {
@@ -87,8 +89,30 @@ const vacationBalanceModel = {
}
}
// 2단계: 남은 차감분을 우선순위 순서로 (이미 차감한 행 제외)
if (remaining > 0) {
const deductedIds = exactMatch.map(b => b.id);
const excludeClause = deductedIds.length > 0 ? `AND id NOT IN (${deductedIds.join(',')})` : '';
const [balances] = await c.query(`
SELECT id, total_days, used_days, (total_days - used_days) AS remaining_days, balance_type
FROM sp_vacation_balances
WHERE user_id = ? AND year = ? AND (total_days - used_days) > 0 ${excludeClause}
ORDER BY FIELD(balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT')
FOR UPDATE
`, [userId, year]);
for (const b of balances) {
if (remaining <= 0) break;
const toDeduct = Math.min(remaining, parseFloat(b.remaining_days));
if (toDeduct > 0) {
await c.query('UPDATE sp_vacation_balances SET used_days = used_days + ?, updated_at = NOW() WHERE id = ?', [toDeduct, b.id]);
remaining -= toDeduct;
}
}
}
if (needRelease) await c.commit();
return { affectedRows: balances.length };
return { affectedRows: exactMatch.length };
} catch (err) {
if (needRelease) await c.rollback();
throw err;