From 9cbf4c98a52de5ddfafa366ec8a3f5de3b96d56b Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 31 Mar 2026 09:32:51 +0900 Subject: [PATCH] =?UTF-8?q?fix(vacation):=20=EB=B0=B0=EC=A0=95=EC=9D=BC?= =?UTF-8?q?=EC=88=98=20=EC=9D=8C=EC=88=98=20=ED=97=88=EC=9A=A9=20+=20?= =?UTF-8?q?=ED=8A=B9=EB=B3=84=ED=9C=B4=EA=B0=80=20=EC=9A=B0=EC=84=A0=20?= =?UTF-8?q?=EC=B0=A8=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vbTotalDays min="0" 제거 (보정용 음수 입력 허용) - deductDays 2단계 차감: 1단계: vacation_type_id 정확 매칭 잔액 우선 (배우자출산 등) 2단계: 나머지를 이월→기본→추가→장기→회사 순서로 Co-Authored-By: Claude Opus 4.6 (1M context) --- tksupport/api/models/vacationBalanceModel.js | 38 ++++++++++++++++---- user-management/web/index.html | 4 +-- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/tksupport/api/models/vacationBalanceModel.js b/tksupport/api/models/vacationBalanceModel.js index f8063de..03f30e5 100644 --- a/tksupport/api/models/vacationBalanceModel.js +++ b/tksupport/api/models/vacationBalanceModel.js @@ -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; diff --git a/user-management/web/index.html b/user-management/web/index.html index d0be3c9..3c70664 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -918,7 +918,7 @@
- +
@@ -2424,7 +2424,7 @@ - +