/** * vacationBalanceModel.js * 휴가 잔액 관련 데이터베이스 쿼리 모델 */ const { getDb } = require('../dbPool'); const vacationBalanceModel = { /** * 특정 작업자의 모든 휴가 잔액 조회 (특정 연도) */ async getByWorkerAndYear(userId, year) { const db = await getDb(); const [rows] = await db.query(` SELECT svb.id, svb.user_id, svb.vacation_type_id, svb.year, svb.total_days, svb.used_days, (svb.total_days - svb.used_days) AS remaining_days, svb.balance_type, svb.expires_at, svb.notes, svb.created_by, svb.created_at, svb.updated_at, vt.type_name, vt.type_code, vt.priority, vt.is_special FROM sp_vacation_balances svb INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id WHERE svb.user_id = ? AND svb.year = ? ORDER BY vt.priority ASC, vt.type_name ASC `, [userId, year]); return rows; }, /** * 특정 작업자의 특정 휴가 유형 잔액 조회 */ async getByWorkerTypeYear(userId, vacationTypeId, year) { const db = await getDb(); const [rows] = await db.query(` SELECT svb.id, svb.user_id, svb.vacation_type_id, svb.year, svb.total_days, svb.used_days, (svb.total_days - svb.used_days) AS remaining_days, svb.balance_type, svb.expires_at, vt.type_name, vt.type_code FROM sp_vacation_balances svb INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id WHERE svb.user_id = ? AND svb.vacation_type_id = ? AND svb.year = ? `, [userId, vacationTypeId, year]); return rows; }, /** * 모든 작업자의 휴가 잔액 조회 (특정 연도) */ async getAllByYear(year) { const db = await getDb(); const [rows] = await db.query(` SELECT svb.id, svb.user_id, svb.vacation_type_id, svb.year, svb.total_days, svb.used_days, (svb.total_days - svb.used_days) AS remaining_days, svb.balance_type, svb.expires_at, svb.notes, svb.created_by, svb.created_at, svb.updated_at, w.worker_name, w.employment_status, vt.type_name, vt.type_code, vt.priority FROM sp_vacation_balances svb INNER JOIN workers w ON svb.user_id = w.user_id INNER JOIN vacation_types vt ON svb.vacation_type_id = vt.id WHERE svb.year = ? AND w.employment_status = 'employed' ORDER BY w.worker_name ASC, vt.priority ASC `, [year]); return rows; }, /** * 휴가 잔액 생성 */ async create(balanceData) { const db = await getDb(); const [result] = await db.query(`INSERT INTO sp_vacation_balances SET ?`, balanceData); return result; }, /** * 휴가 잔액 수정 */ async update(id, updateData) { const db = await getDb(); const [result] = await db.query(`UPDATE sp_vacation_balances SET ? WHERE id = ?`, [updateData, id]); return result; }, /** * 휴가 잔액 삭제 */ async delete(id) { const db = await getDb(); const [result] = await db.query(`DELETE FROM sp_vacation_balances WHERE id = ?`, [id]); return result; }, /** * 작업자의 휴가 사용 일수 업데이트 (차감) */ async deductDays(userId, vacationTypeId, year, daysToDeduct) { const db = await getDb(); 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; }, /** * 작업자의 휴가 사용 일수 복구 (취소) */ async restoreDays(userId, vacationTypeId, year, daysToRestore) { const db = await getDb(); 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; }, /** * 특정 작업자의 사용 가능한 휴가 일수 확인 */ async getAvailableVacationDays(userId, year) { const db = await getDb(); const [rows] = await db.query(` SELECT svb.id, svb.vacation_type_id, vt.type_name, vt.type_code, vt.priority, 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; }, /** * 작업자별 휴가 잔액 일괄 생성 (연도별) */ async bulkCreate(balances) { if (!balances || balances.length === 0) { throw new Error('생성할 휴가 잔액 데이터가 없습니다'); } const db = await getDb(); const query = `INSERT INTO sp_vacation_balances (user_id, vacation_type_id, year, total_days, used_days, notes, created_by, balance_type) VALUES ?`; const values = balances.map(b => [ b.user_id, b.vacation_type_id, b.year, b.total_days || 0, b.used_days || 0, b.notes || null, b.created_by, b.balance_type || 'AUTO' ]); const [result] = await db.query(query, [values]); return result; }, /** * 근속년수 기반 연차 일수 계산 (한국 근로기준법) */ calculateAnnualLeaveDays(hireDate, targetYear) { const hire = new Date(hireDate); const targetDate = new Date(targetYear, 0, 1); const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12 + (targetDate.getMonth() - hire.getMonth()); if (monthsDiff < 12) { return Math.floor(monthsDiff); } const yearsWorked = Math.floor(monthsDiff / 12); const additionalDays = Math.floor((yearsWorked - 1) / 2); return Math.min(15 + additionalDays, 25); }, /** * 휴가 사용 시 우선순위에 따라 잔액에서 차감 */ async deductByPriority(userId, year, daysToDeduct) { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); 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) { await conn.rollback(); 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 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(); } }, /** * 휴가 취소 시 우선순위 역순으로 복구 */ async restoreByPriority(userId, year, daysToRestore) { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); 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 = []; 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; } } 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(); } }, /** * 특정 ID로 휴가 잔액 조회 */ async getById(id) { const db = await getDb(); const [rows] = await db.query(` SELECT vbd.*, w.worker_name, vt.type_name, vt.type_code FROM vacation_balance_details vbd INNER JOIN workers w ON vbd.user_id = w.user_id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id WHERE vbd.id = ? `, [id]); return rows; } }; module.exports = vacationBalanceModel;