- SEC-42: JWT algorithm HS256 명시 (sign 5곳, verify 3곳) - SEC-44: MariaDB/PhpMyAdmin 포트 127.0.0.1 바인딩 - SEC-29: escHtml = escapeHtml alias 추가 (XSS 방지) - SEC-39: Python Dockerfile 4개 non-root user + chown - SEC-43: deploy-remote.sh 삭제 (평문 비밀번호 포함) - SEC-11,12: SQL SET ? → 명시적 컬럼 whitelist + IN절 parameterized - QA-34: vacation approveRequest/cancelRequest 트랜잭션 래핑 - SEC-32,34: material_comparison.py 5개 엔드포인트 인증 + confirmed_by - SEC-33: files.py 17개 미인증 엔드포인트 인증 추가 - SEC-37: chatbot 프롬프트 인젝션 방어 (sanitize + XML 구분자) - SEC-38: fastapi-bridge 프록시 JWT 검증 + 캐시 키 user_id 포함 - SEC-58/QA-98: monthly-comparison API_BASE_URL 수정 + 401 처리 - SEC-61: monthlyComparisonModel SELECT FOR UPDATE 추가 - SEC-63: proxyInputController 에러 메시지 노출 제거 - QA-103: pageAccessRoutes error→message 통일 - SEC-62: tbm-create onclick 인젝션 → data-attribute event delegation - QA-99: tbm-mobile/create 캐시 버스팅 갱신 - QA-100,101: ESC 키 리스너 cleanup 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
439 lines
17 KiB
JavaScript
439 lines
17 KiB
JavaScript
const vacationRequestModel = require('../models/vacationRequestModel');
|
|
const vacationBalanceModel = require('../models/vacationBalanceModel');
|
|
const companyHolidayModel = require('../models/companyHolidayModel');
|
|
const { getPool } = require('../middleware/auth');
|
|
|
|
const vacationController = {
|
|
// ─── 휴가 신청 ───
|
|
|
|
async createRequest(req, res) {
|
|
try {
|
|
const { vacation_type_id, start_date, end_date, days_used, reason } = req.body;
|
|
const user_id = req.user.user_id || req.user.id;
|
|
|
|
if (!vacation_type_id || !start_date || !end_date || !days_used) {
|
|
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
|
|
}
|
|
if (new Date(end_date) < new Date(start_date)) {
|
|
return res.status(400).json({ success: false, error: '종료일은 시작일보다 이후여야 합니다' });
|
|
}
|
|
|
|
const overlapRows = await vacationRequestModel.checkOverlap(user_id, start_date, end_date);
|
|
if (overlapRows[0].count > 0) {
|
|
return res.status(400).json({ success: false, error: '해당 기간에 이미 신청된 휴가가 있습니다' });
|
|
}
|
|
|
|
const result = await vacationRequestModel.create({
|
|
user_id, vacation_type_id, start_date, end_date,
|
|
days_used, reason: reason || null, status: 'pending'
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '휴가 신청이 완료되었습니다',
|
|
data: { request_id: result.insertId }
|
|
});
|
|
} catch (error) {
|
|
console.error('휴가 신청 생성 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async getRequests(req, res) {
|
|
try {
|
|
const role = (req.user.role || '').toLowerCase();
|
|
const userId = req.user.user_id || req.user.id;
|
|
const filters = {
|
|
status: req.query.status,
|
|
start_date: req.query.start_date,
|
|
end_date: req.query.end_date,
|
|
vacation_type_id: req.query.vacation_type_id
|
|
};
|
|
|
|
if (!['admin', 'system'].includes(role)) {
|
|
filters.user_id = userId;
|
|
} else if (req.query.user_id) {
|
|
filters.user_id = req.query.user_id;
|
|
}
|
|
|
|
const results = await vacationRequestModel.getAll(filters);
|
|
res.json({ success: true, data: results });
|
|
} catch (error) {
|
|
console.error('휴가 신청 목록 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async getRequestById(req, res) {
|
|
try {
|
|
const results = await vacationRequestModel.getById(req.params.id);
|
|
if (results.length === 0) {
|
|
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
|
|
}
|
|
const request = results[0];
|
|
const role = (req.user.role || '').toLowerCase();
|
|
const userId = req.user.user_id || req.user.id;
|
|
if (!['admin', 'system'].includes(role) && userId !== request.user_id) {
|
|
return res.status(403).json({ success: false, error: '권한이 없습니다' });
|
|
}
|
|
res.json({ success: true, data: request });
|
|
} catch (error) {
|
|
console.error('휴가 신청 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async updateRequest(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { start_date, end_date, days_used, reason, vacation_type_id } = req.body;
|
|
|
|
const results = await vacationRequestModel.getById(id);
|
|
if (results.length === 0) {
|
|
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
|
|
}
|
|
|
|
const existing = results[0];
|
|
const role = (req.user.role || '').toLowerCase();
|
|
const userId = req.user.user_id || req.user.id;
|
|
if (!['admin', 'system'].includes(role) && userId !== existing.user_id) {
|
|
return res.status(403).json({ success: false, error: '권한이 없습니다' });
|
|
}
|
|
if (existing.status !== 'pending') {
|
|
return res.status(400).json({ success: false, error: '대기 중인 신청만 수정할 수 있습니다' });
|
|
}
|
|
|
|
const updateData = {};
|
|
if (vacation_type_id) updateData.vacation_type_id = vacation_type_id;
|
|
if (start_date) updateData.start_date = start_date;
|
|
if (end_date) updateData.end_date = end_date;
|
|
if (days_used) updateData.days_used = days_used;
|
|
if (reason !== undefined) updateData.reason = reason;
|
|
|
|
if (start_date || end_date) {
|
|
const newStart = start_date || existing.start_date;
|
|
const newEnd = end_date || existing.end_date;
|
|
const overlapRows = await vacationRequestModel.checkOverlap(existing.user_id, newStart, newEnd, id);
|
|
if (overlapRows[0].count > 0) {
|
|
return res.status(400).json({ success: false, error: '해당 기간에 이미 신청된 휴가가 있습니다' });
|
|
}
|
|
}
|
|
|
|
await vacationRequestModel.update(id, updateData);
|
|
res.json({ success: true, message: '휴가 신청이 수정되었습니다' });
|
|
} catch (error) {
|
|
console.error('휴가 신청 수정 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async cancelRequest(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];
|
|
const role = (req.user.role || '').toLowerCase();
|
|
const userId = req.user.user_id || req.user.id;
|
|
if (!['admin', 'system'].includes(role) && userId !== existing.user_id) {
|
|
return res.status(403).json({ success: false, error: '권한이 없습니다' });
|
|
}
|
|
if (existing.status === 'cancelled') {
|
|
return res.status(400).json({ success: false, error: '이미 취소된 신청입니다' });
|
|
}
|
|
|
|
await conn.beginTransaction();
|
|
|
|
// 승인된 건 취소 시 잔여일 복구
|
|
if (existing.status === 'approved') {
|
|
const year = new Date(existing.start_date).getFullYear();
|
|
await vacationBalanceModel.restoreDays(
|
|
existing.user_id, existing.vacation_type_id, year, parseFloat(existing.days_used), conn
|
|
);
|
|
}
|
|
|
|
await vacationRequestModel.updateStatus(id, {
|
|
status: 'cancelled',
|
|
reviewed_by: userId,
|
|
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();
|
|
}
|
|
},
|
|
|
|
// ─── 승인/반려 (관리자) ───
|
|
|
|
async getPending(req, res) {
|
|
try {
|
|
const results = await vacationRequestModel.getAllPending();
|
|
res.json({ success: true, data: results });
|
|
} catch (error) {
|
|
console.error('대기 목록 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async approveRequest(req, res) {
|
|
const db = getPool();
|
|
const conn = await db.getConnection();
|
|
try {
|
|
const { id } = req.params;
|
|
const { review_note } = req.body;
|
|
const reviewed_by = req.user.user_id || req.user.id;
|
|
|
|
const results = await vacationRequestModel.getById(id);
|
|
if (results.length === 0) {
|
|
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
|
|
}
|
|
if (results[0].status !== 'pending') {
|
|
return res.status(400).json({ success: false, error: '이미 처리된 신청입니다' });
|
|
}
|
|
|
|
const request = results[0];
|
|
const year = new Date(request.start_date).getFullYear();
|
|
|
|
await conn.beginTransaction();
|
|
await vacationBalanceModel.deductDays(
|
|
request.user_id, request.vacation_type_id, year, parseFloat(request.days_used), conn
|
|
);
|
|
await vacationRequestModel.updateStatus(id, { status: 'approved', reviewed_by, 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();
|
|
}
|
|
},
|
|
|
|
async rejectRequest(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { review_note } = req.body;
|
|
const reviewed_by = req.user.user_id || req.user.id;
|
|
|
|
const results = await vacationRequestModel.getById(id);
|
|
if (results.length === 0) {
|
|
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
|
|
}
|
|
if (results[0].status !== 'pending') {
|
|
return res.status(400).json({ success: false, error: '이미 처리된 신청입니다' });
|
|
}
|
|
|
|
await vacationRequestModel.updateStatus(id, { status: 'rejected', reviewed_by, review_note });
|
|
res.json({ success: true, message: '휴가 신청이 반려되었습니다' });
|
|
} catch (error) {
|
|
console.error('휴가 반려 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
// ─── 내 휴가 현황 ───
|
|
|
|
async getMyStatus(req, res) {
|
|
try {
|
|
const userId = req.user.user_id || req.user.id;
|
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
|
const startOfYear = `${year}-01-01`;
|
|
const endOfYear = `${year}-12-31`;
|
|
const [balances, requests, holidays] = await Promise.all([
|
|
vacationBalanceModel.getByUserAndYear(userId, year),
|
|
vacationRequestModel.getAll({ user_id: userId, start_date: startOfYear, end_date: endOfYear }),
|
|
companyHolidayModel.getByYear(year)
|
|
]);
|
|
res.json({
|
|
success: true,
|
|
data: { balances, requests, company_holidays: holidays, overtime_hours: null }
|
|
});
|
|
} catch (error) {
|
|
console.error('내 휴가 현황 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
// ─── 잔여일 ───
|
|
|
|
async getMyBalance(req, res) {
|
|
try {
|
|
const userId = req.user.user_id || req.user.id;
|
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
|
const balances = await vacationBalanceModel.getByUserAndYear(userId, year);
|
|
const hireDate = await vacationBalanceModel.getUserHireDate(userId);
|
|
res.json({ success: true, data: { balances, hire_date: hireDate } });
|
|
} catch (error) {
|
|
console.error('잔여일 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async getUserBalance(req, res) {
|
|
try {
|
|
const { userId } = req.params;
|
|
const requestedId = parseInt(userId);
|
|
const currentUserId = req.user.user_id || req.user.id;
|
|
const role = (req.user.role || '').toLowerCase();
|
|
if (requestedId !== currentUserId && !['admin', 'system'].includes(role)) {
|
|
return res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
|
}
|
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
|
const balances = await vacationBalanceModel.getByUserAndYear(userId, year);
|
|
const hireDate = await vacationBalanceModel.getUserHireDate(userId);
|
|
res.json({ success: true, data: { balances, hire_date: hireDate } });
|
|
} catch (error) {
|
|
console.error('사용자 잔여일 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async allocateBalance(req, res) {
|
|
try {
|
|
const { user_id, vacation_type_id, year, total_days, notes } = req.body;
|
|
const created_by = req.user.user_id || req.user.id;
|
|
|
|
if (!user_id || !vacation_type_id || !year || total_days === undefined) {
|
|
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
|
|
}
|
|
|
|
await vacationBalanceModel.allocate({ user_id, vacation_type_id, year, total_days, notes, created_by });
|
|
res.json({ success: true, message: '휴가 잔여일이 배정되었습니다' });
|
|
} catch (error) {
|
|
console.error('잔여일 배정 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
async getAllBalances(req, res) {
|
|
try {
|
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
|
const balances = await vacationBalanceModel.getAllByYear(year);
|
|
res.json({ success: true, data: balances });
|
|
} catch (error) {
|
|
console.error('전체 잔여일 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
// ─── 참조 데이터 ───
|
|
|
|
async getVacationTypes(req, res) {
|
|
try {
|
|
const db = getPool();
|
|
const [rows] = await db.query('SELECT * FROM vacation_types ORDER BY priority ASC, type_name ASC');
|
|
res.json({ success: true, data: rows });
|
|
} catch (error) {
|
|
console.error('휴가 유형 조회 오류:', error);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
|
}
|
|
},
|
|
|
|
// ─── 사용자 목록 (관리자 - 배정용) ───
|
|
|
|
async getUsers(req, res) {
|
|
try {
|
|
const db = getPool();
|
|
const [rows] = await db.query(`
|
|
SELECT user_id, username, name, hire_date
|
|
FROM sso_users
|
|
WHERE is_active = 1
|
|
ORDER BY name ASC
|
|
`);
|
|
res.json({ success: true, data: rows });
|
|
} catch (error) {
|
|
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();
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = vacationController;
|