From 3c611daa2939446fa5c62699530ca4830099c986 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 31 Mar 2026 07:49:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(tkfb):=20=EC=97=B0=EC=B0=A8=20=EC=B0=A8?= =?UTF-8?q?=EA=B0=90/=EB=B3=B5=EC=9B=90=EC=9D=84=20sp=5Fvacation=5Fbalance?= =?UTF-8?q?s=20=EC=A0=95=EB=B3=B8=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deductByPriority/restoreByPriority: vacation_balance_details → sp_vacation_balances - 트랜잭션 + SELECT FOR UPDATE 적용 (tksupport/tkuser 동시 쓰기 안전) - balance_type 우선순위: CARRY_OVER → AUTO → MANUAL → LONG_SERVICE → COMPANY_GRANT - deductDays/restoreDays/getAvailableVacationDays도 sp_vacation_balances로 전환 - DB: sp_vacation_balances DECIMAL(4,1) → DECIMAL(5,2) ALTER 완료 (반반차 0.25 지원) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/models/vacationBalanceModel.js | 188 +++++++++--------- 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/system1-factory/api/models/vacationBalanceModel.js b/system1-factory/api/models/vacationBalanceModel.js index 78c0f4f..7748e4f 100644 --- a/system1-factory/api/models/vacationBalanceModel.js +++ b/system1-factory/api/models/vacationBalanceModel.js @@ -105,7 +105,7 @@ const vacationBalanceModel = { async deductDays(userId, vacationTypeId, year, daysToDeduct) { const db = await getDb(); const [result] = await db.query(` - UPDATE vacation_balance_details + UPDATE sp_vacation_balances SET used_days = used_days + ?, updated_at = NOW() WHERE user_id = ? @@ -121,7 +121,7 @@ const vacationBalanceModel = { async restoreDays(userId, vacationTypeId, year, daysToRestore) { const db = await getDb(); const [result] = await db.query(` - UPDATE vacation_balance_details + UPDATE sp_vacation_balances SET used_days = GREATEST(0, used_days - ?), updated_at = NOW() WHERE user_id = ? @@ -138,20 +138,21 @@ const vacationBalanceModel = { const db = await getDb(); const [rows] = await db.query(` SELECT - vbd.id, - vbd.vacation_type_id, + svb.id, + svb.vacation_type_id, vt.type_name, vt.type_code, vt.priority, - vbd.total_days, - vbd.used_days, - vbd.remaining_days - FROM vacation_balance_details vbd - INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id - WHERE vbd.user_id = ? - AND vbd.year = ? - AND vbd.remaining_days > 0 - ORDER BY vt.priority ASC + svb.total_days, + svb.used_days, + (svb.total_days - svb.used_days) AS remaining_days, + svb.balance_type + FROM sp_vacation_balances svb + INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id + WHERE svb.user_id = ? + AND svb.year = ? + AND (svb.total_days - svb.used_days) > 0 + ORDER BY vt.priority ASC, FIELD(svb.balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT') `, [userId, year]); return rows; }, @@ -208,52 +209,56 @@ const vacationBalanceModel = { */ async deductByPriority(userId, year, daysToDeduct) { const db = await getDb(); + const conn = await db.getConnection(); + try { + await conn.beginTransaction(); - const [balances] = await db.query(` - SELECT vbd.id, vbd.vacation_type_id, vbd.total_days, vbd.used_days, - (vbd.total_days - vbd.used_days) as remaining_days, - vt.type_code, vt.type_name, vt.priority - FROM vacation_balance_details vbd - INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id - WHERE vbd.user_id = ? AND vbd.year = ? - AND (vbd.total_days - vbd.used_days) > 0 - ORDER BY vt.priority ASC - `, [userId, year]); + const [balances] = await conn.query(` + SELECT svb.id, svb.vacation_type_id, svb.total_days, svb.used_days, + (svb.total_days - svb.used_days) AS remaining_days, + svb.balance_type, + vt.type_code, vt.type_name, vt.priority + FROM sp_vacation_balances svb + INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id + WHERE svb.user_id = ? AND svb.year = ? + AND (svb.total_days - svb.used_days) > 0 + ORDER BY vt.priority ASC, FIELD(svb.balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT') + FOR UPDATE + `, [userId, year]); - if (balances.length === 0) { - console.warn(`[VacationBalance] 작업자 ${userId}의 ${year}년 휴가 잔액이 없습니다`); - return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 }; - } - - let remaining = daysToDeduct; - const deductions = []; - - for (const balance of balances) { - if (remaining <= 0) break; - - const available = parseFloat(balance.remaining_days); - const toDeduct = Math.min(remaining, available); - - if (toDeduct > 0) { - await db.query(` - UPDATE vacation_balance_details - SET used_days = used_days + ?, updated_at = NOW() - WHERE id = ? - `, [toDeduct, balance.id]); - - deductions.push({ - balance_id: balance.id, - type_code: balance.type_code, - type_name: balance.type_name, - deducted: toDeduct - }); - - remaining -= toDeduct; + if (balances.length === 0) { + await conn.rollback(); + console.warn(`[VacationBalance] 작업자 ${userId}의 ${year}년 휴가 잔액이 없습니다`); + return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 }; } - } - console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions); - return { success: true, deductions, totalDeducted: daysToDeduct - remaining }; + let remaining = daysToDeduct; + const deductions = []; + + for (const balance of balances) { + if (remaining <= 0) break; + const available = parseFloat(balance.remaining_days); + const toDeduct = Math.min(remaining, available); + if (toDeduct > 0) { + await conn.query(` + UPDATE sp_vacation_balances + SET used_days = used_days + ?, updated_at = NOW() + WHERE id = ? + `, [toDeduct, balance.id]); + deductions.push({ balance_id: balance.id, type_code: balance.type_code, type_name: balance.type_name, balance_type: balance.balance_type, deducted: toDeduct }); + remaining -= toDeduct; + } + } + + await conn.commit(); + console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions); + return { success: true, deductions, totalDeducted: daysToDeduct - remaining }; + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } }, /** @@ -261,46 +266,49 @@ const vacationBalanceModel = { */ async restoreByPriority(userId, year, daysToRestore) { const db = await getDb(); + const conn = await db.getConnection(); + try { + await conn.beginTransaction(); - const [balances] = await db.query(` - SELECT vbd.id, vbd.vacation_type_id, vbd.used_days, - vt.type_code, vt.type_name, vt.priority - FROM vacation_balance_details vbd - INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id - WHERE vbd.user_id = ? AND vbd.year = ? - AND vbd.used_days > 0 - ORDER BY vt.priority DESC - `, [userId, year]); + const [balances] = await conn.query(` + SELECT svb.id, svb.vacation_type_id, svb.used_days, + svb.balance_type, + vt.type_code, vt.type_name, vt.priority + FROM sp_vacation_balances svb + INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id + WHERE svb.user_id = ? AND svb.year = ? + AND svb.used_days > 0 + ORDER BY vt.priority DESC, FIELD(svb.balance_type, 'COMPANY_GRANT', 'LONG_SERVICE', 'MANUAL', 'AUTO', 'CARRY_OVER') + FOR UPDATE + `, [userId, year]); - let remaining = daysToRestore; - const restorations = []; + let remaining = daysToRestore; + const restorations = []; - for (const balance of balances) { - if (remaining <= 0) break; - - const usedDays = parseFloat(balance.used_days); - const toRestore = Math.min(remaining, usedDays); - - if (toRestore > 0) { - await db.query(` - UPDATE vacation_balance_details - SET used_days = used_days - ?, updated_at = NOW() - WHERE id = ? - `, [toRestore, balance.id]); - - restorations.push({ - balance_id: balance.id, - type_code: balance.type_code, - type_name: balance.type_name, - restored: toRestore - }); - - remaining -= toRestore; + for (const balance of balances) { + if (remaining <= 0) break; + const usedDays = parseFloat(balance.used_days); + const toRestore = Math.min(remaining, usedDays); + if (toRestore > 0) { + await conn.query(` + UPDATE sp_vacation_balances + SET used_days = used_days - ?, updated_at = NOW() + WHERE id = ? + `, [toRestore, balance.id]); + restorations.push({ balance_id: balance.id, type_code: balance.type_code, type_name: balance.type_name, balance_type: balance.balance_type, restored: toRestore }); + remaining -= toRestore; + } } - } - console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations); - return { success: true, restorations, totalRestored: daysToRestore - remaining }; + await conn.commit(); + console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations); + return { success: true, restorations, totalRestored: daysToRestore - remaining }; + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } }, /**