feat(tksafety): 통합 출입신고 관리 시스템 구현

- DB 마이그레이션: request_type, visitor_name, department_id, check_in/out_time 컬럼 + status ENUM 확장
- 4소스 UNION 대시보드: 방문(외부/내부) + TBM + 협력업체 통합 조회
- 체크인/체크아웃 API + 내부 출입 신고(승인 불필요) 지원
- 통합 출입 현황판 페이지 신규 (entry-dashboard.html)
- 출입 신청/관리 페이지에 유형 필터 + 체크인/아웃 버튼 추가
- safety_entry_dashboard 권한 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 14:24:13 +09:00
parent 5a062759c5
commit 6a20056e05
16 changed files with 810 additions and 57 deletions

View File

@@ -6,18 +6,31 @@ exports.createVisitRequest = async (req, res) => {
try {
const requester_id = req.user.user_id;
const requestData = { requester_id, ...req.body };
const isInternal = requestData.request_type === 'internal';
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
for (const field of requiredFields) {
if (!requestData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
// 내부 출입: visitor_name 필수, 외부: visitor_company 필수
if (isInternal) {
const requiredFields = ['visitor_name', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
for (const field of requiredFields) {
if (!requestData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
}
}
requestData.visitor_count = 1;
requestData.visitor_company = requestData.visitor_company || '내부';
} else {
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
for (const field of requiredFields) {
if (!requestData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
}
}
}
const requestId = await visitRequestModel.createVisitRequest(requestData);
res.status(201).json({
success: true,
message: '출입 신청이 성공적으로 생성되었습니다.',
message: isInternal ? '내부 출입 신고가 완료되었습니다.' : '출입 신청이 성공적으로 생성되었습니다.',
data: { request_id: requestId }
});
} catch (err) {
@@ -34,7 +47,8 @@ exports.getAllVisitRequests = async (req, res) => {
start_date: req.query.start_date,
end_date: req.query.end_date,
requester_id: req.query.requester_id,
category_id: req.query.category_id
category_id: req.query.category_id,
request_type: req.query.request_type
};
const requests = await visitRequestModel.getAllVisitRequests(filters);
@@ -303,3 +317,67 @@ exports.getWorkplaces = async (req, res) => {
res.status(500).json({ success: false, message: '작업장 조회 중 오류가 발생했습니다.' });
}
};
// ==================== 체크인/체크아웃 ====================
exports.checkIn = async (req, res) => {
try {
const result = await visitRequestModel.checkIn(req.params.id, req.user.user_id);
if (result.error) {
return res.status(result.status).json({ success: false, message: result.error });
}
res.json({ success: true, message: '체크인되었습니다.' });
} catch (err) {
console.error('체크인 오류:', err);
res.status(500).json({ success: false, message: '체크인 중 오류가 발생했습니다.' });
}
};
exports.checkOut = async (req, res) => {
try {
const result = await visitRequestModel.checkOut(req.params.id, req.user.user_id);
if (result.error) {
return res.status(result.status).json({ success: false, message: result.error });
}
res.json({ success: true, message: '체크아웃되었습니다.' });
} catch (err) {
console.error('체크아웃 오류:', err);
res.status(500).json({ success: false, message: '체크아웃 중 오류가 발생했습니다.' });
}
};
// ==================== 통합 대시보드 ====================
exports.getEntryDashboard = async (req, res) => {
try {
const date = req.query.date || new Date().toISOString().substring(0, 10);
const data = await visitRequestModel.getEntryDashboard(date);
res.json({ success: true, data, date });
} catch (err) {
console.error('출입 대시보드 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 대시보드 조회 중 오류가 발생했습니다.' });
}
};
exports.getEntryStats = async (req, res) => {
try {
const date = req.query.date || new Date().toISOString().substring(0, 10);
const stats = await visitRequestModel.getEntryStats(date);
res.json({ success: true, data: stats, date });
} catch (err) {
console.error('출입 통계 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 통계 조회 중 오류가 발생했습니다.' });
}
};
// ==================== 부서 목록 ====================
exports.getDepartments = async (req, res) => {
try {
const departments = await visitRequestModel.getAllDepartments();
res.json({ success: true, data: departments });
} catch (err) {
console.error('부서 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '부서 목록 조회 중 오류가 발생했습니다.' });
}
};