feat(tkfb): 연차 차감/복원을 sp_vacation_balances 정본으로 전환
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,7 @@ const vacationBalanceModel = {
|
|||||||
async deductDays(userId, vacationTypeId, year, daysToDeduct) {
|
async deductDays(userId, vacationTypeId, year, daysToDeduct) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [result] = await db.query(`
|
const [result] = await db.query(`
|
||||||
UPDATE vacation_balance_details
|
UPDATE sp_vacation_balances
|
||||||
SET used_days = used_days + ?,
|
SET used_days = used_days + ?,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
@@ -121,7 +121,7 @@ const vacationBalanceModel = {
|
|||||||
async restoreDays(userId, vacationTypeId, year, daysToRestore) {
|
async restoreDays(userId, vacationTypeId, year, daysToRestore) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [result] = await db.query(`
|
const [result] = await db.query(`
|
||||||
UPDATE vacation_balance_details
|
UPDATE sp_vacation_balances
|
||||||
SET used_days = GREATEST(0, used_days - ?),
|
SET used_days = GREATEST(0, used_days - ?),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
@@ -138,20 +138,21 @@ const vacationBalanceModel = {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(`
|
const [rows] = await db.query(`
|
||||||
SELECT
|
SELECT
|
||||||
vbd.id,
|
svb.id,
|
||||||
vbd.vacation_type_id,
|
svb.vacation_type_id,
|
||||||
vt.type_name,
|
vt.type_name,
|
||||||
vt.type_code,
|
vt.type_code,
|
||||||
vt.priority,
|
vt.priority,
|
||||||
vbd.total_days,
|
svb.total_days,
|
||||||
vbd.used_days,
|
svb.used_days,
|
||||||
vbd.remaining_days
|
(svb.total_days - svb.used_days) AS remaining_days,
|
||||||
FROM vacation_balance_details vbd
|
svb.balance_type
|
||||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
FROM sp_vacation_balances svb
|
||||||
WHERE vbd.user_id = ?
|
INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id
|
||||||
AND vbd.year = ?
|
WHERE svb.user_id = ?
|
||||||
AND vbd.remaining_days > 0
|
AND svb.year = ?
|
||||||
ORDER BY vt.priority ASC
|
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]);
|
`, [userId, year]);
|
||||||
return rows;
|
return rows;
|
||||||
},
|
},
|
||||||
@@ -208,19 +209,25 @@ const vacationBalanceModel = {
|
|||||||
*/
|
*/
|
||||||
async deductByPriority(userId, year, daysToDeduct) {
|
async deductByPriority(userId, year, daysToDeduct) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
const conn = await db.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
const [balances] = await db.query(`
|
const [balances] = await conn.query(`
|
||||||
SELECT vbd.id, vbd.vacation_type_id, vbd.total_days, vbd.used_days,
|
SELECT svb.id, svb.vacation_type_id, svb.total_days, svb.used_days,
|
||||||
(vbd.total_days - vbd.used_days) as remaining_days,
|
(svb.total_days - svb.used_days) AS remaining_days,
|
||||||
|
svb.balance_type,
|
||||||
vt.type_code, vt.type_name, vt.priority
|
vt.type_code, vt.type_name, vt.priority
|
||||||
FROM vacation_balance_details vbd
|
FROM sp_vacation_balances svb
|
||||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id
|
||||||
WHERE vbd.user_id = ? AND vbd.year = ?
|
WHERE svb.user_id = ? AND svb.year = ?
|
||||||
AND (vbd.total_days - vbd.used_days) > 0
|
AND (svb.total_days - svb.used_days) > 0
|
||||||
ORDER BY vt.priority ASC
|
ORDER BY vt.priority ASC, FIELD(svb.balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT')
|
||||||
|
FOR UPDATE
|
||||||
`, [userId, year]);
|
`, [userId, year]);
|
||||||
|
|
||||||
if (balances.length === 0) {
|
if (balances.length === 0) {
|
||||||
|
await conn.rollback();
|
||||||
console.warn(`[VacationBalance] 작업자 ${userId}의 ${year}년 휴가 잔액이 없습니다`);
|
console.warn(`[VacationBalance] 작업자 ${userId}의 ${year}년 휴가 잔액이 없습니다`);
|
||||||
return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 };
|
return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 };
|
||||||
}
|
}
|
||||||
@@ -230,30 +237,28 @@ const vacationBalanceModel = {
|
|||||||
|
|
||||||
for (const balance of balances) {
|
for (const balance of balances) {
|
||||||
if (remaining <= 0) break;
|
if (remaining <= 0) break;
|
||||||
|
|
||||||
const available = parseFloat(balance.remaining_days);
|
const available = parseFloat(balance.remaining_days);
|
||||||
const toDeduct = Math.min(remaining, available);
|
const toDeduct = Math.min(remaining, available);
|
||||||
|
|
||||||
if (toDeduct > 0) {
|
if (toDeduct > 0) {
|
||||||
await db.query(`
|
await conn.query(`
|
||||||
UPDATE vacation_balance_details
|
UPDATE sp_vacation_balances
|
||||||
SET used_days = used_days + ?, updated_at = NOW()
|
SET used_days = used_days + ?, updated_at = NOW()
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [toDeduct, balance.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 });
|
||||||
deductions.push({
|
|
||||||
balance_id: balance.id,
|
|
||||||
type_code: balance.type_code,
|
|
||||||
type_name: balance.type_name,
|
|
||||||
deducted: toDeduct
|
|
||||||
});
|
|
||||||
|
|
||||||
remaining -= toDeduct;
|
remaining -= toDeduct;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await conn.commit();
|
||||||
console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions);
|
console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions);
|
||||||
return { success: true, deductions, totalDeducted: daysToDeduct - remaining };
|
return { success: true, deductions, totalDeducted: daysToDeduct - remaining };
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,15 +266,20 @@ const vacationBalanceModel = {
|
|||||||
*/
|
*/
|
||||||
async restoreByPriority(userId, year, daysToRestore) {
|
async restoreByPriority(userId, year, daysToRestore) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
const conn = await db.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
const [balances] = await db.query(`
|
const [balances] = await conn.query(`
|
||||||
SELECT vbd.id, vbd.vacation_type_id, vbd.used_days,
|
SELECT svb.id, svb.vacation_type_id, svb.used_days,
|
||||||
|
svb.balance_type,
|
||||||
vt.type_code, vt.type_name, vt.priority
|
vt.type_code, vt.type_name, vt.priority
|
||||||
FROM vacation_balance_details vbd
|
FROM sp_vacation_balances svb
|
||||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id
|
||||||
WHERE vbd.user_id = ? AND vbd.year = ?
|
WHERE svb.user_id = ? AND svb.year = ?
|
||||||
AND vbd.used_days > 0
|
AND svb.used_days > 0
|
||||||
ORDER BY vt.priority DESC
|
ORDER BY vt.priority DESC, FIELD(svb.balance_type, 'COMPANY_GRANT', 'LONG_SERVICE', 'MANUAL', 'AUTO', 'CARRY_OVER')
|
||||||
|
FOR UPDATE
|
||||||
`, [userId, year]);
|
`, [userId, year]);
|
||||||
|
|
||||||
let remaining = daysToRestore;
|
let remaining = daysToRestore;
|
||||||
@@ -277,30 +287,28 @@ const vacationBalanceModel = {
|
|||||||
|
|
||||||
for (const balance of balances) {
|
for (const balance of balances) {
|
||||||
if (remaining <= 0) break;
|
if (remaining <= 0) break;
|
||||||
|
|
||||||
const usedDays = parseFloat(balance.used_days);
|
const usedDays = parseFloat(balance.used_days);
|
||||||
const toRestore = Math.min(remaining, usedDays);
|
const toRestore = Math.min(remaining, usedDays);
|
||||||
|
|
||||||
if (toRestore > 0) {
|
if (toRestore > 0) {
|
||||||
await db.query(`
|
await conn.query(`
|
||||||
UPDATE vacation_balance_details
|
UPDATE sp_vacation_balances
|
||||||
SET used_days = used_days - ?, updated_at = NOW()
|
SET used_days = used_days - ?, updated_at = NOW()
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [toRestore, balance.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 });
|
||||||
restorations.push({
|
|
||||||
balance_id: balance.id,
|
|
||||||
type_code: balance.type_code,
|
|
||||||
type_name: balance.type_name,
|
|
||||||
restored: toRestore
|
|
||||||
});
|
|
||||||
|
|
||||||
remaining -= toRestore;
|
remaining -= toRestore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await conn.commit();
|
||||||
console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations);
|
console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations);
|
||||||
return { success: true, restorations, totalRestored: daysToRestore - remaining };
|
return { success: true, restorations, totalRestored: daysToRestore - remaining };
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user