feat(training): 안전교육 실시 페이지 수정/삭제 기능 추가

대기 목록·완료 이력 양쪽에 수정/삭제 버튼 추가.
교육 기록 삭제 시 트랜잭션으로 출입 신청 상태를 approved로 복원.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 20:31:29 +09:00
parent e236883c64
commit 3e50639914
5 changed files with 249 additions and 0 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -36,6 +36,12 @@ function renderPendingTraining() {
<button onclick="openTrainingModal(${r.request_id})" class="text-blue-600 hover:text-blue-800 text-xs px-2 py-1 border border-blue-200 rounded hover:bg-blue-50">
<i class="fas fa-chalkboard-teacher mr-1"></i>교육실시
</button>
<button onclick="openEditRequest(${r.request_id})" class="text-gray-600 hover:text-gray-800 text-xs px-1.5 py-1 border border-gray-200 rounded hover:bg-gray-50 ml-1" title="수정">
<i class="fas fa-pen"></i>
</button>
<button onclick="doDeleteRequest(${r.request_id})" class="text-red-500 hover:text-red-700 text-xs px-1.5 py-1 border border-red-200 rounded hover:bg-red-50 ml-1" title="삭제">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`).join('');
}
@@ -70,6 +76,12 @@ function renderCompletedTraining() {
<td>${escapeHtml(t.trainer_full_name || t.trainer_name || '-')}</td>
<td class="text-right">
${t.completed_at ? '<span class="badge badge-green">완료</span>' : '<span class="badge badge-amber">진행중</span>'}
<button onclick="openEditTraining(${t.training_id})" class="text-gray-600 hover:text-gray-800 text-xs px-1.5 py-1 border border-gray-200 rounded hover:bg-gray-50 ml-1" title="수정">
<i class="fas fa-pen"></i>
</button>
<button onclick="doDeleteTraining(${t.training_id})" class="text-red-500 hover:text-red-700 text-xs px-1.5 py-1 border border-red-200 rounded hover:bg-red-50 ml-1" title="삭제">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`;
}).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();

View File

@@ -133,6 +133,84 @@
</div>
</div>
<!-- 출입 신청 수정 모달 -->
<div id="editRequestModal" class="hidden modal-overlay" onclick="if(event.target===this)closeEditRequestModal()">
<div class="modal-content p-6" style="max-width: 32rem;">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">출입 신청 수정</h3>
<button onclick="closeEditRequestModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
</div>
<form id="editRequestForm">
<input type="hidden" id="editRequestId">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">업체 <span class="text-red-400">*</span></label>
<input type="text" id="editReqCompany" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">인원 <span class="text-red-400">*</span></label>
<input type="number" id="editReqCount" class="input-field w-full px-3 py-2 rounded-lg text-sm" min="1" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">방문일 <span class="text-red-400">*</span></label>
<input type="date" id="editReqDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">방문시간 <span class="text-red-400">*</span></label>
<input type="time" id="editReqTime" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
<textarea id="editReqNotes" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
</div>
</div>
<div class="flex justify-end mt-4 gap-2">
<button type="button" onclick="closeEditRequestModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
<i class="fas fa-save mr-1"></i>저장
</button>
</div>
</form>
</div>
</div>
<!-- 교육 기록 수정 모달 -->
<div id="editTrainingModal" class="hidden modal-overlay" onclick="if(event.target===this)closeEditTrainingModal()">
<div class="modal-content p-6" style="max-width: 32rem;">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">교육 기록 수정</h3>
<button onclick="closeEditTrainingModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
</div>
<form id="editTrainingForm">
<input type="hidden" id="editTrainingId">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">교육일 <span class="text-red-400">*</span></label>
<input type="date" id="editTrainDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">시작시간 <span class="text-red-400">*</span></label>
<input type="time" id="editTrainStartTime" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">종료시간</label>
<input type="time" id="editTrainEndTime" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">교육 내용</label>
<textarea id="editTrainTopics" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="3"></textarea>
</div>
</div>
<div class="flex justify-end mt-4 gap-2">
<button type="button" onclick="closeEditTrainingModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
<i class="fas fa-save mr-1"></i>저장
</button>
</div>
</form>
</div>
</div>
<script src="/static/js/tksafety-core.js?v=3"></script>
<script src="/static/js/tksafety-training.js"></script>
<script>initTrainingPage();</script>