feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환

Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
         미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 10:46:22 +09:00
parent 8373fe9e75
commit 9fda89a374
133 changed files with 5255 additions and 26181 deletions

View File

@@ -0,0 +1,85 @@
const checklistModel = require('../models/checklistModel');
exports.getAllChecks = async (req, res) => {
try {
const checks = await checklistModel.getAllChecks();
res.json({ success: true, data: checks });
} catch (err) {
console.error('체크리스트 조회 오류:', err);
res.status(500).json({ success: false, error: '체크리스트 조회 실패' });
}
};
exports.getCheckById = async (req, res) => {
try {
const check = await checklistModel.getCheckById(req.params.id);
if (!check) return res.status(404).json({ success: false, error: '항목을 찾을 수 없습니다' });
res.json({ success: true, data: check });
} catch (err) {
console.error('체크리스트 항목 조회 오류:', err);
res.status(500).json({ success: false, error: '조회 실패' });
}
};
exports.createCheck = async (req, res) => {
try {
if (!req.body.check_item) return res.status(400).json({ success: false, error: 'check_item은 필수입니다' });
const checkId = await checklistModel.createCheck(req.body);
res.status(201).json({ success: true, message: '항목이 추가되었습니다', data: { check_id: checkId } });
} catch (err) {
console.error('체크리스트 항목 추가 오류:', err);
res.status(500).json({ success: false, error: '추가 실패' });
}
};
exports.updateCheck = async (req, res) => {
try {
const result = await checklistModel.updateCheck(req.params.id, req.body);
if (result.affectedRows === 0) return res.status(404).json({ success: false, error: '항목을 찾을 수 없습니다' });
res.json({ success: true, message: '항목이 수정되었습니다' });
} catch (err) {
console.error('체크리스트 항목 수정 오류:', err);
res.status(500).json({ success: false, error: '수정 실패' });
}
};
exports.deleteCheck = async (req, res) => {
try {
const result = await checklistModel.deleteCheck(req.params.id);
if (result.affectedRows === 0) return res.status(404).json({ success: false, error: '항목을 찾을 수 없습니다' });
res.json({ success: true, message: '항목이 삭제되었습니다' });
} catch (err) {
console.error('체크리스트 항목 삭제 오류:', err);
res.status(500).json({ success: false, error: '삭제 실패' });
}
};
exports.getWeatherConditions = async (req, res) => {
try {
const conditions = await checklistModel.getWeatherConditions();
res.json({ success: true, data: conditions });
} catch (err) {
console.error('날씨 조건 조회 오류:', err);
res.status(500).json({ success: false, error: '조회 실패' });
}
};
exports.getWorkTypes = async (req, res) => {
try {
const types = await checklistModel.getWorkTypes();
res.json({ success: true, data: types });
} catch (err) {
console.error('공정 조회 오류:', err);
res.status(500).json({ success: false, error: '조회 실패' });
}
};
exports.getTasksByWorkType = async (req, res) => {
try {
const tasks = await checklistModel.getTasksByWorkType(req.params.workTypeId);
res.json({ success: true, data: tasks });
} catch (err) {
console.error('작업 목록 조회 오류:', err);
res.status(500).json({ success: false, error: '조회 실패' });
}
};

View File

@@ -0,0 +1,305 @@
const visitRequestModel = require('../models/visitRequestModel');
// ==================== 출입 신청 관리 ====================
exports.createVisitRequest = async (req, res) => {
try {
const requester_id = req.user.user_id;
const requestData = { requester_id, ...req.body };
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: '출입 신청이 성공적으로 생성되었습니다.',
data: { request_id: requestId }
});
} catch (err) {
console.error('출입 신청 생성 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 생성 중 오류가 발생했습니다.' });
}
};
exports.getAllVisitRequests = async (req, res) => {
try {
const filters = {
status: req.query.status,
visit_date: req.query.visit_date,
start_date: req.query.start_date,
end_date: req.query.end_date,
requester_id: req.query.requester_id,
category_id: req.query.category_id
};
const requests = await visitRequestModel.getAllVisitRequests(filters);
res.json({ success: true, data: requests });
} catch (err) {
console.error('출입 신청 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 목록 조회 중 오류가 발생했습니다.' });
}
};
exports.getVisitRequestById = async (req, res) => {
try {
const request = await visitRequestModel.getVisitRequestById(req.params.id);
if (!request) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, data: request });
} catch (err) {
console.error('출입 신청 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 조회 중 오류가 발생했습니다.' });
}
};
exports.updateVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitRequest(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 수정되었습니다.' });
} catch (err) {
console.error('출입 신청 수정 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 수정 중 오류가 발생했습니다.' });
}
};
exports.deleteVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitRequest(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 삭제되었습니다.' });
} catch (err) {
console.error('출입 신청 삭제 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 삭제 중 오류가 발생했습니다.' });
}
};
exports.approveVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.approveVisitRequest(req.params.id, req.user.user_id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 승인되었습니다.' });
} catch (err) {
console.error('출입 신청 승인 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 승인 중 오류가 발생했습니다.' });
}
};
exports.rejectVisitRequest = async (req, res) => {
try {
const rejectionData = {
approved_by: req.user.user_id,
rejection_reason: req.body.rejection_reason || '사유 없음'
};
const result = await visitRequestModel.rejectVisitRequest(req.params.id, rejectionData);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 반려되었습니다.' });
} catch (err) {
console.error('출입 신청 반려 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 반려 중 오류가 발생했습니다.' });
}
};
// ==================== 방문 목적 관리 ====================
exports.getAllVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getAllVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
console.error('방문 목적 조회 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 조회 중 오류가 발생했습니다.' });
}
};
exports.getActiveVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getActiveVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
console.error('활성 방문 목적 조회 오류:', err);
res.status(500).json({ success: false, message: '활성 방문 목적 조회 중 오류가 발생했습니다.' });
}
};
exports.createVisitPurpose = async (req, res) => {
try {
if (!req.body.purpose_name) {
return res.status(400).json({ success: false, message: 'purpose_name은 필수 입력 항목입니다.' });
}
const purposeId = await visitRequestModel.createVisitPurpose(req.body);
res.status(201).json({
success: true,
message: '방문 목적이 추가되었습니다.',
data: { purpose_id: purposeId }
});
} catch (err) {
console.error('방문 목적 추가 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 추가 중 오류가 발생했습니다.' });
}
};
exports.updateVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitPurpose(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 수정되었습니다.' });
} catch (err) {
console.error('방문 목적 수정 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 수정 중 오류가 발생했습니다.' });
}
};
exports.deleteVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitPurpose(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 삭제되었습니다.' });
} catch (err) {
console.error('방문 목적 삭제 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 삭제 중 오류가 발생했습니다.' });
}
};
// ==================== 안전교육 기록 관리 ====================
exports.createTrainingRecord = async (req, res) => {
try {
const trainingData = { trainer_id: req.user.user_id, ...req.body };
const requiredFields = ['request_id', 'training_date', 'training_start_time'];
for (const field of requiredFields) {
if (!trainingData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
}
}
const trainingId = await visitRequestModel.createTrainingRecord(trainingData);
// 안전교육 기록 생성 후 출입 신청 상태를 training_completed로 변경
try {
await visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed');
} catch (statusErr) {
console.error('출입 신청 상태 업데이트 오류:', statusErr);
}
res.status(201).json({
success: true,
message: '안전교육 기록이 생성되었습니다.',
data: { training_id: trainingId }
});
} catch (err) {
console.error('안전교육 기록 생성 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 생성 중 오류가 발생했습니다.' });
}
};
exports.getTrainingRecordByRequestId = async (req, res) => {
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(req.params.requestId);
res.json({ success: true, data: record || null });
} catch (err) {
console.error('안전교육 기록 조회 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 조회 중 오류가 발생했습니다.' });
}
};
exports.updateTrainingRecord = async (req, res) => {
try {
const result = await visitRequestModel.updateTrainingRecord(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '안전교육 기록이 수정되었습니다.' });
} catch (err) {
console.error('안전교육 기록 수정 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 수정 중 오류가 발생했습니다.' });
}
};
exports.completeTraining = async (req, res) => {
try {
const trainingId = req.params.id;
const signatureData = req.body.signature_data;
if (!signatureData) {
return res.status(400).json({ success: false, message: '서명 데이터가 필요합니다.' });
}
const result = await visitRequestModel.completeTraining(trainingId, signatureData);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
// 교육 완료 후 출입 신청 상태 변경
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(trainingId);
if (record) {
await visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed');
}
} catch (statusErr) {
console.error('출입 신청 상태 업데이트 오류:', statusErr);
}
res.json({ success: true, message: '안전교육이 완료되었습니다.' });
} catch (err) {
console.error('안전교육 완료 처리 오류:', err);
res.status(500).json({ success: false, message: '안전교육 완료 처리 중 오류가 발생했습니다.' });
}
};
exports.getTrainingRecords = async (req, res) => {
try {
const filters = {
training_date: req.query.training_date,
start_date: req.query.start_date,
end_date: req.query.end_date,
trainer_id: req.query.trainer_id
};
const records = await visitRequestModel.getTrainingRecords(filters);
res.json({ success: true, data: records });
} catch (err) {
console.error('안전교육 기록 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
}
};
// ==================== 작업장 분류/작업장 조회 ====================
exports.getAllCategories = async (req, res) => {
try {
const categories = await visitRequestModel.getAllCategories();
res.json({ success: true, data: categories });
} catch (err) {
console.error('작업장 분류 조회 오류:', err);
res.status(500).json({ success: false, message: '작업장 분류 조회 중 오류가 발생했습니다.' });
}
};
exports.getWorkplaces = async (req, res) => {
try {
const workplaces = await visitRequestModel.getWorkplacesByCategory(req.query.category_id);
res.json({ success: true, data: workplaces });
} catch (err) {
console.error('작업장 조회 오류:', err);
res.status(500).json({ success: false, message: '작업장 조회 중 오류가 발생했습니다.' });
}
};

View File

@@ -3,6 +3,8 @@ const cors = require('cors');
const cron = require('node-cron');
const dailyVisitRoutes = require('./routes/dailyVisitRoutes');
const educationRoutes = require('./routes/educationRoutes');
const visitRequestRoutes = require('./routes/visitRequestRoutes');
const checklistRoutes = require('./routes/checklistRoutes');
const dailyVisitModel = require('./models/dailyVisitModel');
const { requireAuth } = require('./middleware/auth');
@@ -37,6 +39,8 @@ app.get('/health', (req, res) => {
// Routes
app.use('/api/daily-visits', dailyVisitRoutes);
app.use('/api/education', educationRoutes);
app.use('/api/visit-requests', visitRequestRoutes);
app.use('/api/checklist', checklistRoutes);
// Partner search (autocomplete)
app.get('/api/partners/search', requireAuth, async (req, res) => {

View File

@@ -0,0 +1,67 @@
const { getPool } = require('../middleware/auth');
// Get all safety checks
async function getAllChecks() {
const db = getPool();
const [rows] = await db.query('SELECT * FROM tbm_safety_checks ORDER BY check_type, check_category, display_order, check_id');
return rows;
}
// Get check by ID
async function getCheckById(checkId) {
const db = getPool();
const [rows] = await db.query('SELECT * FROM tbm_safety_checks WHERE check_id = ?', [checkId]);
return rows[0];
}
// Create check
async function createCheck(data) {
const db = getPool();
const { check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id } = data;
const [result] = await db.query(
'INSERT INTO tbm_safety_checks (check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[check_type, check_category || null, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition || null, task_id || null]
);
return result.insertId;
}
// Update check
async function updateCheck(checkId, data) {
const db = getPool();
const { check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id } = data;
const [result] = await db.query(
'UPDATE tbm_safety_checks SET check_type = ?, check_category = ?, check_item = ?, description = ?, is_required = ?, display_order = ?, weather_condition = ?, task_id = ? WHERE check_id = ?',
[check_type, check_category || null, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition || null, task_id || null, checkId]
);
return result;
}
// Delete check
async function deleteCheck(checkId) {
const db = getPool();
const [result] = await db.query('DELETE FROM tbm_safety_checks WHERE check_id = ?', [checkId]);
return result;
}
// Get weather conditions
async function getWeatherConditions() {
const db = getPool();
const [rows] = await db.query('SELECT * FROM tbm_weather_conditions ORDER BY display_order, condition_code');
return rows;
}
// Get work types
async function getWorkTypes() {
const db = getPool();
const [rows] = await db.query('SELECT * FROM work_types ORDER BY name');
return rows;
}
// Get tasks by work type
async function getTasksByWorkType(workTypeId) {
const db = getPool();
const [rows] = await db.query('SELECT * FROM tasks WHERE work_type_id = ? ORDER BY task_name', [workTypeId]);
return rows;
}
module.exports = { getAllChecks, getCheckById, createCheck, updateCheck, deleteCheck, getWeatherConditions, getWorkTypes, getTasksByWorkType };

View File

@@ -0,0 +1,366 @@
const { getPool } = require('../middleware/auth');
// ==================== 출입 신청 관리 ====================
const createVisitRequest = async (requestData) => {
const db = getPool();
const {
requester_id,
visitor_company,
visitor_count = 1,
category_id,
workplace_id,
visit_date,
visit_time,
purpose_id,
notes = null
} = requestData;
const [result] = await db.query(
`INSERT INTO workplace_visit_requests
(requester_id, visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[requester_id, visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes]
);
return result.insertId;
};
const getAllVisitRequests = async (filters = {}) => {
const db = getPool();
let query = `
SELECT
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
vr.purpose_id, vr.notes, vr.status,
vr.approved_by, vr.approved_at, vr.rejection_reason,
vr.created_at, vr.updated_at,
u.username as requester_name, u.name as requester_full_name,
wc.category_name, w.workplace_name,
vpt.purpose_name,
approver.username as approver_name
FROM workplace_visit_requests vr
INNER JOIN users u ON vr.requester_id = u.user_id
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
LEFT JOIN users approver ON vr.approved_by = approver.user_id
WHERE 1=1
`;
const params = [];
if (filters.status) {
query += ` AND vr.status = ?`;
params.push(filters.status);
}
if (filters.visit_date) {
query += ` AND vr.visit_date = ?`;
params.push(filters.visit_date);
}
if (filters.start_date && filters.end_date) {
query += ` AND vr.visit_date BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
if (filters.requester_id) {
query += ` AND vr.requester_id = ?`;
params.push(filters.requester_id);
}
if (filters.category_id) {
query += ` AND vr.category_id = ?`;
params.push(filters.category_id);
}
query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`;
const [rows] = await db.query(query, params);
return rows;
};
const getVisitRequestById = async (requestId) => {
const db = getPool();
const [rows] = await db.query(
`SELECT
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
vr.purpose_id, vr.notes, vr.status,
vr.approved_by, vr.approved_at, vr.rejection_reason,
vr.created_at, vr.updated_at,
u.username as requester_name, u.name as requester_full_name,
wc.category_name, w.workplace_name,
vpt.purpose_name,
approver.username as approver_name
FROM workplace_visit_requests vr
INNER JOIN users u ON vr.requester_id = u.user_id
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
LEFT JOIN users approver ON vr.approved_by = approver.user_id
WHERE vr.request_id = ?`,
[requestId]
);
return rows[0];
};
const updateVisitRequest = async (requestId, requestData) => {
const db = getPool();
const {
visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes
} = requestData;
const [result] = await db.query(
`UPDATE workplace_visit_requests
SET visitor_company = ?, visitor_count = ?, category_id = ?, workplace_id = ?,
visit_date = ?, visit_time = ?, purpose_id = ?, notes = ?, updated_at = NOW()
WHERE request_id = ?`,
[visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes, requestId]
);
return result;
};
const deleteVisitRequest = async (requestId) => {
const db = getPool();
const [result] = await db.query(
`DELETE FROM workplace_visit_requests WHERE request_id = ?`,
[requestId]
);
return result;
};
const approveVisitRequest = async (requestId, approvedBy) => {
const db = getPool();
const [result] = await db.query(
`UPDATE workplace_visit_requests
SET status = 'approved', approved_by = ?, approved_at = NOW(), updated_at = NOW()
WHERE request_id = ?`,
[approvedBy, requestId]
);
return result;
};
const rejectVisitRequest = async (requestId, rejectionData) => {
const db = getPool();
const { approved_by, rejection_reason } = rejectionData;
const [result] = await db.query(
`UPDATE workplace_visit_requests
SET status = 'rejected', approved_by = ?, approved_at = NOW(),
rejection_reason = ?, updated_at = NOW()
WHERE request_id = ?`,
[approved_by, rejection_reason, requestId]
);
return result;
};
const updateVisitRequestStatus = async (requestId, status) => {
const db = getPool();
const [result] = await db.query(
`UPDATE workplace_visit_requests
SET status = ?, updated_at = NOW()
WHERE request_id = ?`,
[status, requestId]
);
return result;
};
// ==================== 방문 목적 관리 ====================
const getAllVisitPurposes = async () => {
const db = getPool();
const [rows] = await db.query(
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
FROM visit_purpose_types
ORDER BY display_order ASC, purpose_id ASC`
);
return rows;
};
const getActiveVisitPurposes = async () => {
const db = getPool();
const [rows] = await db.query(
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
FROM visit_purpose_types
WHERE is_active = TRUE
ORDER BY display_order ASC, purpose_id ASC`
);
return rows;
};
const createVisitPurpose = async (purposeData) => {
const db = getPool();
const { purpose_name, display_order = 0, is_active = true } = purposeData;
const [result] = await db.query(
`INSERT INTO visit_purpose_types (purpose_name, display_order, is_active)
VALUES (?, ?, ?)`,
[purpose_name, display_order, is_active]
);
return result.insertId;
};
const updateVisitPurpose = async (purposeId, purposeData) => {
const db = getPool();
const { purpose_name, display_order, is_active } = purposeData;
const [result] = await db.query(
`UPDATE visit_purpose_types
SET purpose_name = ?, display_order = ?, is_active = ?
WHERE purpose_id = ?`,
[purpose_name, display_order, is_active, purposeId]
);
return result;
};
const deleteVisitPurpose = async (purposeId) => {
const db = getPool();
const [result] = await db.query(
`DELETE FROM visit_purpose_types WHERE purpose_id = ?`,
[purposeId]
);
return result;
};
// ==================== 안전교육 기록 관리 ====================
const createTrainingRecord = async (trainingData) => {
const db = getPool();
const {
request_id, trainer_id, training_date,
training_start_time, training_end_time = null, training_topics = null
} = trainingData;
const [result] = await db.query(
`INSERT INTO safety_training_records
(request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics)
VALUES (?, ?, ?, ?, ?, ?)`,
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
);
return result.insertId;
};
const getTrainingRecordByRequestId = async (requestId) => {
const db = getPool();
const [rows] = await db.query(
`SELECT
str.training_id, str.request_id, str.trainer_id, str.training_date,
str.training_start_time, str.training_end_time, str.training_topics,
str.signature_data, str.completed_at, str.created_at, str.updated_at,
u.username as trainer_name, u.name as trainer_full_name
FROM safety_training_records str
INNER JOIN users u ON str.trainer_id = u.user_id
WHERE str.request_id = ?`,
[requestId]
);
return rows[0];
};
const updateTrainingRecord = async (trainingId, trainingData) => {
const db = getPool();
const { training_date, training_start_time, training_end_time, training_topics } = trainingData;
const [result] = await db.query(
`UPDATE safety_training_records
SET training_date = ?, training_start_time = ?, training_end_time = ?,
training_topics = ?, updated_at = NOW()
WHERE training_id = ?`,
[training_date, training_start_time, training_end_time, training_topics, trainingId]
);
return result;
};
const completeTraining = async (trainingId, signatureData) => {
const db = getPool();
const [result] = await db.query(
`UPDATE safety_training_records
SET signature_data = ?, completed_at = NOW(), updated_at = NOW()
WHERE training_id = ?`,
[signatureData, trainingId]
);
return result;
};
const getTrainingRecords = async (filters = {}) => {
const db = getPool();
let query = `
SELECT
str.training_id, str.request_id, str.trainer_id, str.training_date,
str.training_start_time, str.training_end_time, str.training_topics,
str.completed_at, str.created_at, str.updated_at,
u.username as trainer_name, u.name as trainer_full_name,
vr.visitor_company, vr.visitor_count, vr.visit_date
FROM safety_training_records str
INNER JOIN users u ON str.trainer_id = u.user_id
INNER JOIN workplace_visit_requests vr ON str.request_id = vr.request_id
WHERE 1=1
`;
const params = [];
if (filters.training_date) {
query += ` AND str.training_date = ?`;
params.push(filters.training_date);
}
if (filters.start_date && filters.end_date) {
query += ` AND str.training_date BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
if (filters.trainer_id) {
query += ` AND str.trainer_id = ?`;
params.push(filters.trainer_id);
}
query += ` ORDER BY str.training_date DESC, str.training_start_time DESC`;
const [rows] = await db.query(query, params);
return rows;
};
// ==================== 작업장 분류/작업장 조회 ====================
const getAllCategories = async () => {
const db = getPool();
const [rows] = await db.query(
'SELECT category_id, category_name FROM workplace_categories ORDER BY category_name'
);
return rows;
};
const getWorkplacesByCategory = async (categoryId) => {
const db = getPool();
let query = 'SELECT workplace_id, workplace_name, category_id FROM workplaces';
const params = [];
if (categoryId) {
query += ' WHERE category_id = ?';
params.push(categoryId);
}
query += ' ORDER BY workplace_name';
const [rows] = await db.query(query, params);
return rows;
};
module.exports = {
createVisitRequest,
getAllVisitRequests,
getVisitRequestById,
updateVisitRequest,
deleteVisitRequest,
approveVisitRequest,
rejectVisitRequest,
updateVisitRequestStatus,
getAllVisitPurposes,
getActiveVisitPurposes,
createVisitPurpose,
updateVisitPurpose,
deleteVisitPurpose,
createTrainingRecord,
getTrainingRecordByRequestId,
updateTrainingRecord,
completeTraining,
getTrainingRecords,
getAllCategories,
getWorkplacesByCategory
};

View File

@@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const checklistController = require('../controllers/checklistController');
const { requireAuth, requireAdmin } = require('../middleware/auth');
router.use(requireAuth);
// Safety checks CRUD
router.get('/', checklistController.getAllChecks);
router.get('/weather-conditions', checklistController.getWeatherConditions);
router.get('/work-types', checklistController.getWorkTypes);
router.get('/tasks/:workTypeId', checklistController.getTasksByWorkType);
router.get('/:id', checklistController.getCheckById);
router.post('/', requireAdmin, checklistController.createCheck);
router.put('/:id', requireAdmin, checklistController.updateCheck);
router.delete('/:id', requireAdmin, checklistController.deleteCheck);
module.exports = router;

View File

@@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const visitRequestController = require('../controllers/visitRequestController');
const { requireAuth, requireAdmin, requirePage } = require('../middleware/auth');
router.use(requireAuth);
// Visit requests CRUD
router.post('/requests', requirePage('safety_visit_request'), visitRequestController.createVisitRequest);
router.get('/requests', visitRequestController.getAllVisitRequests);
router.get('/requests/:id', visitRequestController.getVisitRequestById);
router.put('/requests/:id', requirePage('safety_visit_request'), visitRequestController.updateVisitRequest);
router.delete('/requests/:id', requirePage('safety_visit_request'), visitRequestController.deleteVisitRequest);
router.put('/requests/:id/approve', requireAdmin, visitRequestController.approveVisitRequest);
router.put('/requests/:id/reject', requireAdmin, visitRequestController.rejectVisitRequest);
// Categories & Workplaces
router.get('/categories', visitRequestController.getAllCategories);
router.get('/workplaces', visitRequestController.getWorkplaces);
// Visit purposes
router.get('/purposes', visitRequestController.getAllVisitPurposes);
router.get('/purposes/active', visitRequestController.getActiveVisitPurposes);
router.post('/purposes', requireAdmin, visitRequestController.createVisitPurpose);
router.put('/purposes/:id', requireAdmin, visitRequestController.updateVisitPurpose);
router.delete('/purposes/:id', requireAdmin, visitRequestController.deleteVisitPurpose);
// Training records
router.post('/training', requireAdmin, visitRequestController.createTrainingRecord);
router.get('/training', visitRequestController.getTrainingRecords);
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
router.put('/training/:id', requireAdmin, visitRequestController.updateTrainingRecord);
router.post('/training/:id/complete', requireAdmin, visitRequestController.completeTraining);
module.exports = router;