diff --git a/tksafety/api/controllers/visitRequestController.js b/tksafety/api/controllers/visitRequestController.js
index fc2fd3a..ebff1b4 100644
--- a/tksafety/api/controllers/visitRequestController.js
+++ b/tksafety/api/controllers/visitRequestController.js
@@ -280,6 +280,19 @@ exports.updateTrainingRecord = async (req, res) => {
}
};
+exports.deleteTrainingRecord = async (req, res) => {
+ try {
+ const result = await visitRequestModel.deleteTrainingRecord(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.completeTraining = async (req, res) => {
try {
const trainingId = req.params.id;
diff --git a/tksafety/api/models/visitRequestModel.js b/tksafety/api/models/visitRequestModel.js
index 6a1b649..66e249b 100644
--- a/tksafety/api/models/visitRequestModel.js
+++ b/tksafety/api/models/visitRequestModel.js
@@ -350,6 +350,42 @@ const completeTraining = async (trainingId, signatureData) => {
return result;
};
+const deleteTrainingRecord = async (trainingId) => {
+ const db = getPool();
+ const connection = await db.getConnection();
+ try {
+ await connection.beginTransaction();
+
+ // 1. training record 조회 → request_id 획득
+ const [rows] = await connection.query(
+ 'SELECT training_id, request_id FROM safety_training_records WHERE training_id = ?',
+ [trainingId]
+ );
+ if (!rows.length) {
+ await connection.rollback();
+ return { affectedRows: 0 };
+ }
+ const requestId = rows[0].request_id;
+
+ // 2. training record 삭제
+ await connection.query('DELETE FROM safety_training_records WHERE training_id = ?', [trainingId]);
+
+ // 3. 출입 신청 상태를 approved로 복원
+ await connection.query(
+ "UPDATE workplace_visit_requests SET status = 'approved', updated_at = NOW() WHERE request_id = ?",
+ [requestId]
+ );
+
+ await connection.commit();
+ return { affectedRows: 1 };
+ } catch (err) {
+ await connection.rollback();
+ throw err;
+ } finally {
+ connection.release();
+ }
+};
+
const getTrainingRecords = async (filters = {}) => {
const db = getPool();
let query = `
@@ -581,6 +617,7 @@ module.exports = {
createTrainingRecord,
getTrainingRecordByRequestId,
updateTrainingRecord,
+ deleteTrainingRecord,
completeTraining,
getTrainingRecords,
getAllCategories,
diff --git a/tksafety/api/routes/visitRequestRoutes.js b/tksafety/api/routes/visitRequestRoutes.js
index e02d6e2..f70fa6d 100644
--- a/tksafety/api/routes/visitRequestRoutes.js
+++ b/tksafety/api/routes/visitRequestRoutes.js
@@ -41,6 +41,7 @@ router.post('/training', requireAdmin, visitRequestController.createTrainingReco
router.get('/training', visitRequestController.getTrainingRecords);
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
router.put('/training/:id', requireAdmin, visitRequestController.updateTrainingRecord);
+router.delete('/training/:id', requireAdmin, visitRequestController.deleteTrainingRecord);
router.post('/training/:id/complete', requireAdmin, visitRequestController.completeTraining);
module.exports = router;
diff --git a/tksafety/web/static/js/tksafety-training.js b/tksafety/web/static/js/tksafety-training.js
index 7f49a7d..49b2156 100644
--- a/tksafety/web/static/js/tksafety-training.js
+++ b/tksafety/web/static/js/tksafety-training.js
@@ -36,6 +36,12 @@ function renderPendingTraining() {
+
+
`).join('');
}
@@ -70,6 +76,12 @@ function renderCompletedTraining() {
${escapeHtml(t.trainer_full_name || t.trainer_name || '-')} |
${t.completed_at ? '완료' : '진행중'}
+
+
|
`;
}).join('');
@@ -217,6 +229,112 @@ function clearSignature() {
hasSignature = false;
}
+/* ===== 대기 목록: 출입 신청 수정/삭제 ===== */
+function openEditRequest(requestId) {
+ const r = pendingRequests.find(x => x.request_id === requestId);
+ if (!r) return;
+ document.getElementById('editRequestId').value = requestId;
+ document.getElementById('editReqCompany').value = r.visitor_company || '';
+ document.getElementById('editReqCount').value = r.visitor_count || 1;
+ document.getElementById('editReqDate').value = r.visit_date ? r.visit_date.substring(0, 10) : '';
+ document.getElementById('editReqTime').value = r.visit_time ? String(r.visit_time).substring(0, 5) : '';
+ document.getElementById('editReqNotes').value = r.notes || '';
+ document.getElementById('editRequestModal').classList.remove('hidden');
+}
+
+function closeEditRequestModal() {
+ document.getElementById('editRequestModal').classList.add('hidden');
+}
+
+async function submitEditRequest(e) {
+ e.preventDefault();
+ const id = document.getElementById('editRequestId').value;
+ const r = pendingRequests.find(x => x.request_id === parseInt(id));
+ if (!r) return;
+
+ const data = {
+ visitor_company: document.getElementById('editReqCompany').value,
+ visitor_count: parseInt(document.getElementById('editReqCount').value),
+ category_id: r.category_id,
+ workplace_id: r.workplace_id,
+ visit_date: document.getElementById('editReqDate').value,
+ visit_time: document.getElementById('editReqTime').value,
+ purpose_id: r.purpose_id,
+ notes: document.getElementById('editReqNotes').value || null
+ };
+
+ try {
+ await api('/visit-requests/requests/' + id, {
+ method: 'PUT', body: JSON.stringify(data)
+ });
+ showToast('출입 신청이 수정되었습니다');
+ closeEditRequestModal();
+ await loadPendingTraining();
+ } catch (e) {
+ showToast(e.message, 'error');
+ }
+}
+
+async function doDeleteRequest(requestId) {
+ if (!confirm('이 출입 신청을 삭제하시겠습니까?')) return;
+ try {
+ await api('/visit-requests/requests/' + requestId, { method: 'DELETE' });
+ showToast('출입 신청이 삭제되었습니다');
+ await loadPendingTraining();
+ } catch (e) {
+ showToast(e.message, 'error');
+ }
+}
+
+/* ===== 완료 이력: 교육 기록 수정/삭제 ===== */
+function openEditTraining(trainingId) {
+ const t = completedTrainings.find(x => x.training_id === trainingId);
+ if (!t) return;
+ document.getElementById('editTrainingId').value = trainingId;
+ document.getElementById('editTrainDate').value = t.training_date ? t.training_date.substring(0, 10) : '';
+ document.getElementById('editTrainStartTime').value = t.training_start_time ? String(t.training_start_time).substring(0, 5) : '';
+ document.getElementById('editTrainEndTime').value = t.training_end_time ? String(t.training_end_time).substring(0, 5) : '';
+ document.getElementById('editTrainTopics').value = t.training_topics || '';
+ document.getElementById('editTrainingModal').classList.remove('hidden');
+}
+
+function closeEditTrainingModal() {
+ document.getElementById('editTrainingModal').classList.add('hidden');
+}
+
+async function submitEditTraining(e) {
+ e.preventDefault();
+ const id = document.getElementById('editTrainingId').value;
+ const data = {
+ training_date: document.getElementById('editTrainDate').value,
+ training_start_time: document.getElementById('editTrainStartTime').value,
+ training_end_time: document.getElementById('editTrainEndTime').value || null,
+ training_topics: document.getElementById('editTrainTopics').value || null
+ };
+
+ try {
+ await api('/visit-requests/training/' + id, {
+ method: 'PUT', body: JSON.stringify(data)
+ });
+ showToast('교육 기록이 수정되었습니다');
+ closeEditTrainingModal();
+ await loadCompletedTraining();
+ } catch (e) {
+ showToast(e.message, 'error');
+ }
+}
+
+async function doDeleteTraining(trainingId) {
+ if (!confirm('이 교육 기록을 삭제하시겠습니까?\n삭제 시 해당 출입 신청이 대기 목록으로 복원됩니다.')) return;
+ try {
+ await api('/visit-requests/training/' + trainingId, { method: 'DELETE' });
+ showToast('교육 기록이 삭제되었습니다');
+ await Promise.all([loadPendingTraining(), loadCompletedTraining()]);
+ } catch (e) {
+ showToast(e.message, 'error');
+ }
+}
+
/* ===== Init ===== */
function initTrainingPage() {
if (!initAuth()) return;
@@ -233,6 +351,8 @@ function initTrainingPage() {
}
document.getElementById('trainingForm').addEventListener('submit', submitTraining);
+ document.getElementById('editRequestForm').addEventListener('submit', submitEditRequest);
+ document.getElementById('editTrainingForm').addEventListener('submit', submitEditTraining);
initSignaturePad();
loadPendingTraining();
loadCompletedTraining();
diff --git a/tksafety/web/training.html b/tksafety/web/training.html
index 6eab0e1..a5619f2 100644
--- a/tksafety/web/training.html
+++ b/tksafety/web/training.html
@@ -133,6 +133,84 @@
+
+
+
+
+
+
+
+
교육 기록 수정
+
+
+
+
+
+