/** * vacationBalanceModel.js * 휴가 잔액 관련 데이터베이스 쿼리 모델 */ const { getDb } = require('../dbPool'); const vacationBalanceModel = { /** * 특정 작업자의 모든 휴가 잔액 조회 (특정 연도) */ async getByWorkerAndYear(userId, year) { const db = await getDb(); const [rows] = await db.query(` SELECT vbd.*, vt.type_name, vt.type_code, vt.priority, vt.is_special FROM vacation_balance_details vbd INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id WHERE vbd.user_id = ? AND vbd.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 vbd.*, vt.type_name, vt.type_code FROM vacation_balance_details vbd INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id WHERE vbd.user_id = ? AND vbd.vacation_type_id = ? AND vbd.year = ? `, [userId, vacationTypeId, year]); return rows; }, /** * 모든 작업자의 휴가 잔액 조회 (특정 연도) */ async getAllByYear(year) { const db = await getDb(); const [rows] = await db.query(` SELECT vbd.*, w.worker_name, w.employment_status, vt.type_name, vt.type_code, vt.priority 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.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 vacation_balance_details SET ?`, balanceData); return result; }, /** * 휴가 잔액 수정 */ async update(id, updateData) { const db = await getDb(); const [result] = await db.query(`UPDATE vacation_balance_details SET ? WHERE id = ?`, [updateData, id]); return result; }, /** * 휴가 잔액 삭제 */ async delete(id) { const db = await getDb(); const [result] = await db.query(`DELETE FROM vacation_balance_details WHERE id = ?`, [id]); return result; }, /** * 작업자의 휴가 사용 일수 업데이트 (차감) */ async deductDays(userId, vacationTypeId, year, daysToDeduct) { const db = await getDb(); const [result] = await db.query(` UPDATE vacation_balance_details 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 vacation_balance_details 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 vbd.id, vbd.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 `, [userId, year]); return rows; }, /** * 작업자별 휴가 잔액 일괄 생성 (연도별) */ async bulkCreate(balances) { if (!balances || balances.length === 0) { throw new Error('생성할 휴가 잔액 데이터가 없습니다'); } const db = await getDb(); const query = `INSERT INTO vacation_balance_details (user_id, vacation_type_id, year, total_days, used_days, notes, created_by) 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 ]); 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 [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]); 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; } } console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions); return { success: true, deductions, totalDeducted: daysToDeduct - remaining }; }, /** * 휴가 취소 시 우선순위 역순으로 복구 */ async restoreByPriority(userId, year, daysToRestore) { const db = await getDb(); 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]); 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; } } console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations); return { success: true, restorations, totalRestored: daysToRestore - remaining }; }, /** * 특정 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;