sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
323 lines
8.9 KiB
JavaScript
323 lines
8.9 KiB
JavaScript
/**
|
|
* 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;
|