feat(tksupport): 휴가 보정 관리 페이지 추가 — 캘린더 기반 추가/삭제

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-25 14:12:14 +09:00
parent 05c9f22bdf
commit d663b9bfa6
13 changed files with 396 additions and 15 deletions

View File

@@ -343,6 +343,80 @@ const vacationController = {
console.error('사용자 목록 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
// ─── 관리자 보정 ───
async adminCreateRequest(req, res) {
const db = getPool();
const conn = await db.getConnection();
try {
const { user_id, vacation_type_id, start_date, end_date, days_used, reason } = req.body;
if (!user_id || !vacation_type_id || !start_date || !end_date || !days_used) {
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
}
const daysVal = parseFloat(days_used);
if (daysVal <= 0 || daysVal > 30) {
return res.status(400).json({ success: false, error: '일수는 0 초과 30 이하여야 합니다' });
}
if (new Date(start_date).getFullYear() !== new Date(end_date).getFullYear()) {
return res.status(400).json({ success: false, error: '연도를 걸친 휴가는 연도별로 분리하여 입력해주세요' });
}
const adminId = req.user.user_id || req.user.id;
const year = new Date(start_date).getFullYear();
await conn.beginTransaction();
const result = await vacationRequestModel.create({
user_id, vacation_type_id, start_date, end_date,
days_used: daysVal, reason: reason || null,
status: 'approved', reviewed_by: adminId, review_note: '관리자 보정 추가'
}, conn);
await vacationBalanceModel.deductDays(user_id, vacation_type_id, year, daysVal, conn);
await conn.commit();
res.status(201).json({ success: true, message: '휴가가 등록되었습니다', data: { request_id: result.insertId } });
} catch (error) {
await conn.rollback();
console.error('관리자 보정 추가 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
} finally {
conn.release();
}
},
async adminDeleteRequest(req, res) {
const db = getPool();
const conn = await db.getConnection();
try {
const { id } = req.params;
const results = await vacationRequestModel.getById(id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 기록을 찾을 수 없습니다' });
}
const existing = results[0];
if (existing.status !== 'approved') {
return res.status(400).json({ success: false, error: '승인된 기록만 삭제할 수 있습니다' });
}
const adminId = req.user.user_id || req.user.id;
const year = new Date(existing.start_date).getFullYear();
await conn.beginTransaction();
await vacationBalanceModel.restoreDays(existing.user_id, existing.vacation_type_id, year, parseFloat(existing.days_used), conn);
await vacationRequestModel.updateStatus(id, {
status: 'cancelled', reviewed_by: adminId, review_note: '관리자 보정 삭제'
}, conn);
await conn.commit();
res.json({ success: true, message: '휴가가 삭제되었습니다' });
} catch (error) {
await conn.rollback();
console.error('관리자 보정 삭제 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
} finally {
conn.release();
}
}
};

View File

@@ -61,8 +61,8 @@ const vacationBalanceModel = {
return result;
},
async deductDays(userId, vacationTypeId, year, daysToDeduct) {
const db = getPool();
async deductDays(userId, vacationTypeId, year, daysToDeduct, conn) {
const db = conn || getPool();
const [result] = await db.query(`
UPDATE sp_vacation_balances
SET used_days = used_days + ?, updated_at = NOW()
@@ -71,8 +71,8 @@ const vacationBalanceModel = {
return result;
},
async restoreDays(userId, vacationTypeId, year, daysToRestore) {
const db = getPool();
async restoreDays(userId, vacationTypeId, year, daysToRestore, conn) {
const db = conn || getPool();
const [result] = await db.query(`
UPDATE sp_vacation_balances
SET used_days = GREATEST(0, used_days - ?), updated_at = NOW()

View File

@@ -90,7 +90,7 @@ const vacationDashboardModel = {
const [rows] = await db.query(`
SELECT
su.user_id, su.name, su.username,
vr.start_date, vr.end_date, vr.days_used,
vr.request_id, vr.start_date, vr.end_date, vr.days_used,
vt.type_code, vt.type_name
FROM sso_users su
LEFT JOIN sp_vacation_requests vr

View File

@@ -1,8 +1,8 @@
const { getPool } = require('../middleware/auth');
const vacationRequestModel = {
async create(data) {
const db = getPool();
async create(data, conn) {
const db = conn || getPool();
const [result] = await db.query('INSERT INTO sp_vacation_requests SET ?', data);
return result;
},
@@ -85,8 +85,8 @@ const vacationRequestModel = {
return result;
},
async updateStatus(requestId, statusData) {
const db = getPool();
async updateStatus(requestId, statusData, conn) {
const db = conn || getPool();
const [result] = await db.query(`
UPDATE sp_vacation_requests
SET status = ?, reviewed_by = ?, reviewed_at = NOW(), review_note = ?

View File

@@ -29,6 +29,10 @@ router.get('/balance/all', requireAdmin, ctrl.getAllBalances);
router.get('/balance/:userId', requireAdmin, ctrl.getUserBalance);
router.post('/balance/allocate', requireAdmin, ctrl.allocateBalance);
// 관리자 보정
router.post('/admin/correct', requireAdmin, ctrl.adminCreateRequest);
router.delete('/admin/requests/:id', requireAdmin, ctrl.adminDeleteRequest);
// 사용자 목록 (관리자)
router.get('/users', requireAdmin, ctrl.getUsers);