feat(tksupport): Sprint 001 Section C — 전사 휴가관리 구현
- 전사 휴가 부여/관리 (company-holidays) CRUD + 연차차감 트랜잭션 - 전체 휴가관리 대시보드 (vacation-dashboard) 부서별/직원별 현황 - 내 휴가 현황 개선 (/my-status) balance_type별 카드, 전사 휴가일 - requireSupportTeam 미들웨어, 부서명 JOIN, 마이그레이션 002 추가 - 사이드바 roles 기반 메뉴 필터링 (하위호환 유지) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
tksupport/api/models/companyHolidayModel.js
Normal file
93
tksupport/api/models/companyHolidayModel.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { getPool } = require('../middleware/auth');
|
||||
|
||||
const companyHolidayModel = {
|
||||
async getByYear(year) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM company_holidays WHERE YEAR(holiday_date) = ? ORDER BY holiday_date',
|
||||
[year]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM company_holidays WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
},
|
||||
|
||||
async create(data) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
'INSERT INTO company_holidays (holiday_date, holiday_name, holiday_type, description, created_by) VALUES (?, ?, ?, ?, ?)',
|
||||
[data.holiday_date, data.holiday_name, data.holiday_type, data.description || null, data.created_by]
|
||||
);
|
||||
return result;
|
||||
},
|
||||
|
||||
async delete(id) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query('DELETE FROM company_holidays WHERE id = ?', [id]);
|
||||
return result;
|
||||
},
|
||||
|
||||
async applyAnnualDeduction(holidayId) {
|
||||
const db = getPool();
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 1. SELECT FOR UPDATE — 동시 실행 방지
|
||||
const [holidays] = await conn.query(
|
||||
'SELECT * FROM company_holidays WHERE id = ? FOR UPDATE', [holidayId]
|
||||
);
|
||||
if (holidays.length === 0) {
|
||||
throw new Error('해당 전사 휴가를 찾을 수 없습니다');
|
||||
}
|
||||
const holiday = holidays[0];
|
||||
if (holiday.holiday_type !== 'ANNUAL_DEDUCT') {
|
||||
throw new Error('연차차감 유형의 휴가만 차감할 수 있습니다');
|
||||
}
|
||||
if (holiday.deduction_applied_at) {
|
||||
throw new Error('이미 차감이 실행된 휴가입니다');
|
||||
}
|
||||
|
||||
// 2. vacation_types에서 ANNUAL_FULL id 조회
|
||||
const [types] = await conn.query(
|
||||
"SELECT id FROM vacation_types WHERE type_code = 'ANNUAL_FULL'"
|
||||
);
|
||||
if (types.length === 0) {
|
||||
throw new Error('연차(ANNUAL_FULL) 유형이 존재하지 않습니다');
|
||||
}
|
||||
const typeId = types[0].id;
|
||||
|
||||
// 3. 전 활성 사원 연차 차감 (잔여일 무관 일괄 적용)
|
||||
const [result] = await conn.query(`
|
||||
UPDATE sp_vacation_balances SET used_days = used_days + 1, updated_at = NOW()
|
||||
WHERE vacation_type_id = ? AND year = YEAR(?) AND balance_type = 'AUTO'
|
||||
AND user_id IN (SELECT user_id FROM sso_users WHERE is_active = 1)
|
||||
`, [typeId, holiday.holiday_date]);
|
||||
|
||||
// 4. 차감 완료 표시
|
||||
await conn.query(
|
||||
'UPDATE company_holidays SET deduction_applied_at = NOW() WHERE id = ?', [holidayId]
|
||||
);
|
||||
|
||||
// 5. balance 없는 사원 수 체크 (경고용)
|
||||
const [activeUsers] = await conn.query(
|
||||
'SELECT COUNT(*) as cnt FROM sso_users WHERE is_active = 1'
|
||||
);
|
||||
const missing = activeUsers[0].cnt - result.affectedRows;
|
||||
|
||||
await conn.commit();
|
||||
return { affected_count: result.affectedRows, missing_balance_count: missing };
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = companyHolidayModel;
|
||||
Reference in New Issue
Block a user