From 6a721258b834f9e2da8ed13212ab8f77f4a7ba3b Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 31 Mar 2026 08:53:56 +0900 Subject: [PATCH] =?UTF-8?q?fix(tksupport):=20=ED=9C=B4=EA=B0=80=20?= =?UTF-8?q?=EC=B0=A8=EA=B0=90=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(=EC=9D=B4=EC=9B=94=E2=86=92=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=E2=86=92=EC=B6=94=EA=B0=80=E2=86=92=EC=9E=A5=EA=B8=B0?= =?UTF-8?q?=E2=86=92=ED=9A=8C=EC=82=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단순 UPDATE → 트랜잭션 + 우선순위 순차 차감으로 변경. 이월 연차부터 먼저 소진되도록 보장. 복원도 역순(회사→장기→추가→기본→이월) 적용. 참조: system1-factory/api/models/vacationBalanceModel.js deductByPriority Co-Authored-By: Claude Opus 4.6 (1M context) --- tksupport/api/models/vacationBalanceModel.js | 76 ++++++++++++++++---- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/tksupport/api/models/vacationBalanceModel.js b/tksupport/api/models/vacationBalanceModel.js index 444aa82..f8063de 100644 --- a/tksupport/api/models/vacationBalanceModel.js +++ b/tksupport/api/models/vacationBalanceModel.js @@ -61,24 +61,76 @@ const vacationBalanceModel = { return result; }, + // 우선순위: 이월 → 기본연차 → 추가부여 → 장기근속 → 회사부여 async deductDays(userId, vacationTypeId, year, daysToDeduct, conn) { const db = conn || getPool(); - const [result] = await db.query(` - UPDATE sp_vacation_balances - SET used_days = used_days + ?, updated_at = NOW() - WHERE user_id = ? AND vacation_type_id = ? AND year = ? - `, [daysToDeduct, userId, vacationTypeId, year]); - return result; + const needRelease = !conn; + const c = needRelease ? await db.getConnection() : db; + try { + if (needRelease) await c.beginTransaction(); + + 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 + ORDER BY FIELD(balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT') + FOR UPDATE + `, [userId, year]); + + let remaining = daysToDeduct; + 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 }; + } catch (err) { + if (needRelease) await c.rollback(); + throw err; + } finally { + if (needRelease) c.release(); + } }, + // 복원: 역순 (회사부여 → 장기근속 → 추가부여 → 기본연차 → 이월) async restoreDays(userId, vacationTypeId, year, daysToRestore, conn) { const db = conn || getPool(); - const [result] = await db.query(` - UPDATE sp_vacation_balances - SET used_days = GREATEST(0, used_days - ?), updated_at = NOW() - WHERE user_id = ? AND vacation_type_id = ? AND year = ? - `, [daysToRestore, userId, vacationTypeId, year]); - return result; + const needRelease = !conn; + const c = needRelease ? await db.getConnection() : db; + try { + if (needRelease) await c.beginTransaction(); + + const [balances] = await c.query(` + SELECT id, used_days, balance_type + FROM sp_vacation_balances + WHERE user_id = ? AND year = ? AND used_days > 0 + ORDER BY FIELD(balance_type, 'COMPANY_GRANT', 'LONG_SERVICE', 'MANUAL', 'AUTO', 'CARRY_OVER') + FOR UPDATE + `, [userId, year]); + + let remaining = daysToRestore; + for (const b of balances) { + if (remaining <= 0) break; + const toRestore = Math.min(remaining, parseFloat(b.used_days)); + if (toRestore > 0) { + await c.query('UPDATE sp_vacation_balances SET used_days = used_days - ?, updated_at = NOW() WHERE id = ?', [toRestore, b.id]); + remaining -= toRestore; + } + } + + if (needRelease) await c.commit(); + return { affectedRows: balances.length }; + } catch (err) { + if (needRelease) await c.rollback(); + throw err; + } finally { + if (needRelease) c.release(); + } }, calculateAnnualLeaveDays(hireDate, targetYear) {