- api-base.js에 4개 전역 유틸리티 추가 (showToast, formatDate, waitForApi, generateUUID) - 24개 파일에서 중복 정의 제거 (-932줄) - showToast: 18곳 중복 → 1곳 통합 (자동 컨테이너/스타일 생성) - waitForApi/waitForApiConfig/waitForApiCall: 5곳 → 1곳 통합 - generateUUID: tbm.js 중복 제거 - tbm/utils.js, workplace-management/utils.js: window 재정의 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
475 lines
13 KiB
JavaScript
475 lines
13 KiB
JavaScript
// 안전교육 진행 페이지 JavaScript
|
|
|
|
let requestId = null;
|
|
let requestData = null;
|
|
let canvas = null;
|
|
let ctx = null;
|
|
let isDrawing = false;
|
|
let hasSignature = false;
|
|
let savedSignatures = []; // 저장된 서명 목록
|
|
|
|
// showToast → api-base.js 전역 사용
|
|
|
|
// ==================== 초기화 ====================
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// URL 파라미터에서 request_id 가져오기
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
requestId = urlParams.get('request_id');
|
|
|
|
if (!requestId) {
|
|
showToast('출입 신청 ID가 없습니다.', 'error');
|
|
setTimeout(() => {
|
|
window.location.href = '/pages/safety/management.html';
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
// 서명 캔버스 초기화
|
|
initSignatureCanvas();
|
|
|
|
// 현재 날짜 표시
|
|
const today = new Date().toLocaleDateString('ko-KR');
|
|
document.getElementById('signatureDate').textContent = today;
|
|
|
|
// 출입 신청 정보 로드
|
|
await loadRequestInfo();
|
|
});
|
|
|
|
// ==================== 출입 신청 정보 로드 ====================
|
|
|
|
/**
|
|
* 출입 신청 정보 로드
|
|
*/
|
|
async function loadRequestInfo() {
|
|
try {
|
|
const response = await window.apiCall(`/workplace-visits/requests/${requestId}`, 'GET');
|
|
|
|
if (response && response.success) {
|
|
requestData = response.data;
|
|
|
|
// 상태 확인 - 승인됨 상태만 진행 가능
|
|
if (requestData.status !== 'approved') {
|
|
showToast('이미 처리되었거나 승인되지 않은 신청입니다.', 'error');
|
|
setTimeout(() => {
|
|
window.location.href = '/pages/safety/management.html';
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
renderRequestInfo();
|
|
} else {
|
|
throw new Error(response?.message || '정보를 불러올 수 없습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('출입 신청 정보 로드 오류:', error);
|
|
showToast('출입 신청 정보를 불러오는데 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 출입 신청 정보 렌더링
|
|
*/
|
|
function renderRequestInfo() {
|
|
const container = document.getElementById('requestInfo');
|
|
|
|
// 날짜 포맷 변환
|
|
const visitDate = new Date(requestData.visit_date);
|
|
const formattedDate = visitDate.toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
weekday: 'short'
|
|
});
|
|
|
|
const html = `
|
|
<div class="info-item">
|
|
<div class="info-label">신청 번호</div>
|
|
<div class="info-value">#${requestData.request_id}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">신청자</div>
|
|
<div class="info-value">${requestData.requester_full_name || requestData.requester_name}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">방문자 소속</div>
|
|
<div class="info-value">${requestData.visitor_company}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">방문 인원</div>
|
|
<div class="info-value">${requestData.visitor_count}명</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">방문 작업장</div>
|
|
<div class="info-value">${requestData.category_name} - ${requestData.workplace_name}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">방문 일시</div>
|
|
<div class="info-value">${formattedDate} ${requestData.visit_time}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">방문 목적</div>
|
|
<div class="info-value">${requestData.purpose_name}</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ==================== 서명 캔버스 ====================
|
|
|
|
/**
|
|
* 서명 캔버스 초기화
|
|
*/
|
|
function initSignatureCanvas() {
|
|
canvas = document.getElementById('signatureCanvas');
|
|
ctx = canvas.getContext('2d');
|
|
|
|
// 캔버스 크기 설정
|
|
const container = canvas.parentElement;
|
|
canvas.width = container.clientWidth - 4; // border 제외
|
|
canvas.height = 300;
|
|
|
|
// 그리기 설정
|
|
ctx.strokeStyle = '#000';
|
|
ctx.lineWidth = 2;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
// 마우스 이벤트
|
|
canvas.addEventListener('mousedown', startDrawing);
|
|
canvas.addEventListener('mousemove', draw);
|
|
canvas.addEventListener('mouseup', stopDrawing);
|
|
canvas.addEventListener('mouseout', stopDrawing);
|
|
|
|
// 터치 이벤트 (모바일, Apple Pencil)
|
|
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
canvas.addEventListener('touchend', stopDrawing);
|
|
canvas.addEventListener('touchcancel', stopDrawing);
|
|
|
|
// Pointer Events (Apple Pencil 최적화)
|
|
if (window.PointerEvent) {
|
|
canvas.addEventListener('pointerdown', handlePointerDown);
|
|
canvas.addEventListener('pointermove', handlePointerMove);
|
|
canvas.addEventListener('pointerup', stopDrawing);
|
|
canvas.addEventListener('pointercancel', stopDrawing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 그리기 시작 (마우스)
|
|
*/
|
|
function startDrawing(e) {
|
|
isDrawing = true;
|
|
hasSignature = true;
|
|
document.getElementById('signaturePlaceholder').style.display = 'none';
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.beginPath();
|
|
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
|
}
|
|
|
|
/**
|
|
* 그리기 (마우스)
|
|
*/
|
|
function draw(e) {
|
|
if (!isDrawing) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
|
ctx.stroke();
|
|
}
|
|
|
|
/**
|
|
* 그리기 중지
|
|
*/
|
|
function stopDrawing() {
|
|
isDrawing = false;
|
|
ctx.beginPath();
|
|
}
|
|
|
|
/**
|
|
* 터치 시작 처리
|
|
*/
|
|
function handleTouchStart(e) {
|
|
e.preventDefault();
|
|
isDrawing = true;
|
|
hasSignature = true;
|
|
document.getElementById('signaturePlaceholder').style.display = 'none';
|
|
|
|
const touch = e.touches[0];
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.beginPath();
|
|
ctx.moveTo(touch.clientX - rect.left, touch.clientY - rect.top);
|
|
}
|
|
|
|
/**
|
|
* 터치 이동 처리
|
|
*/
|
|
function handleTouchMove(e) {
|
|
if (!isDrawing) return;
|
|
e.preventDefault();
|
|
|
|
const touch = e.touches[0];
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.lineTo(touch.clientX - rect.left, touch.clientY - rect.top);
|
|
ctx.stroke();
|
|
}
|
|
|
|
/**
|
|
* Pointer 시작 처리 (Apple Pencil)
|
|
*/
|
|
function handlePointerDown(e) {
|
|
isDrawing = true;
|
|
hasSignature = true;
|
|
document.getElementById('signaturePlaceholder').style.display = 'none';
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.beginPath();
|
|
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
|
}
|
|
|
|
/**
|
|
* Pointer 이동 처리 (Apple Pencil)
|
|
*/
|
|
function handlePointerMove(e) {
|
|
if (!isDrawing) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
|
ctx.stroke();
|
|
}
|
|
|
|
/**
|
|
* 서명 지우기
|
|
*/
|
|
function clearSignature() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
hasSignature = false;
|
|
document.getElementById('signaturePlaceholder').style.display = 'block';
|
|
}
|
|
|
|
/**
|
|
* 서명을 Base64로 변환
|
|
*/
|
|
function getSignatureBase64() {
|
|
if (!hasSignature) {
|
|
return null;
|
|
}
|
|
return canvas.toDataURL('image/png');
|
|
}
|
|
|
|
/**
|
|
* 현재 서명 저장
|
|
*/
|
|
function saveSignature() {
|
|
if (!hasSignature) {
|
|
showToast('서명이 없습니다. 이름과 서명을 작성해주세요.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const signatureImage = getSignatureBase64();
|
|
const now = new Date();
|
|
|
|
savedSignatures.push({
|
|
id: Date.now(),
|
|
image: signatureImage,
|
|
timestamp: now.toLocaleString('ko-KR')
|
|
});
|
|
|
|
// 서명 카운트 업데이트
|
|
document.getElementById('signatureCount').textContent = savedSignatures.length;
|
|
|
|
// 캔버스 초기화
|
|
clearSignature();
|
|
|
|
// 저장된 서명 목록 렌더링
|
|
renderSavedSignatures();
|
|
|
|
// 교육 완료 버튼 활성화
|
|
updateCompleteButton();
|
|
|
|
showToast('서명이 저장되었습니다.', 'success');
|
|
}
|
|
|
|
/**
|
|
* 저장된 서명 목록 렌더링
|
|
*/
|
|
function renderSavedSignatures() {
|
|
const container = document.getElementById('savedSignatures');
|
|
|
|
if (savedSignatures.length === 0) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '<h3 style="font-size: var(--text-lg); font-weight: 600; margin-bottom: 16px; color: var(--gray-700);">저장된 서명 목록</h3>';
|
|
|
|
savedSignatures.forEach((sig, index) => {
|
|
html += `
|
|
<div class="saved-signature-card">
|
|
<img src="${sig.image}" alt="서명 ${index + 1}">
|
|
<div class="saved-signature-info">
|
|
<div class="saved-signature-number">방문자 ${index + 1}</div>
|
|
<div class="saved-signature-date">저장 시간: ${sig.timestamp}</div>
|
|
</div>
|
|
<div class="saved-signature-actions">
|
|
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSignature(${sig.id})">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* 서명 삭제
|
|
*/
|
|
function deleteSignature(signatureId) {
|
|
if (!confirm('이 서명을 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
savedSignatures = savedSignatures.filter(sig => sig.id !== signatureId);
|
|
|
|
// 서명 카운트 업데이트
|
|
document.getElementById('signatureCount').textContent = savedSignatures.length;
|
|
|
|
// 목록 다시 렌더링
|
|
renderSavedSignatures();
|
|
|
|
// 교육 완료 버튼 상태 업데이트
|
|
updateCompleteButton();
|
|
|
|
showToast('서명이 삭제되었습니다.', 'success');
|
|
}
|
|
|
|
/**
|
|
* 교육 완료 버튼 활성화/비활성화
|
|
*/
|
|
function updateCompleteButton() {
|
|
const completeBtn = document.getElementById('completeBtn');
|
|
|
|
// 체크리스트와 서명이 모두 있어야 활성화
|
|
const checkboxes = document.querySelectorAll('input[name="safety-check"]');
|
|
const checkedItems = Array.from(checkboxes).filter(cb => cb.checked);
|
|
const allChecked = checkedItems.length === checkboxes.length;
|
|
const hasSignatures = savedSignatures.length > 0;
|
|
|
|
completeBtn.disabled = !(allChecked && hasSignatures);
|
|
}
|
|
|
|
// ==================== 교육 완료 처리 ====================
|
|
|
|
/**
|
|
* 교육 완료 처리
|
|
*/
|
|
async function completeTraining() {
|
|
// 체크리스트 검증
|
|
const checkboxes = document.querySelectorAll('input[name="safety-check"]');
|
|
const checkedItems = Array.from(checkboxes).filter(cb => cb.checked);
|
|
|
|
if (checkedItems.length !== checkboxes.length) {
|
|
showToast('모든 안전교육 항목을 체크해주세요.', 'warning');
|
|
return;
|
|
}
|
|
|
|
// 서명 검증
|
|
if (savedSignatures.length === 0) {
|
|
showToast('최소 1명 이상의 서명이 필요합니다.', 'warning');
|
|
return;
|
|
}
|
|
|
|
// 확인
|
|
if (!confirm(`${savedSignatures.length}명의 방문자 안전교육을 완료하시겠습니까?\n완료 후에는 수정할 수 없습니다.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 교육 항목 수집
|
|
const trainingItems = checkedItems.map(cb => cb.value).join(', ');
|
|
|
|
// API 호출
|
|
const userData = localStorage.getItem('sso_user');
|
|
const currentUser = userData ? JSON.parse(userData) : null;
|
|
|
|
if (!currentUser) {
|
|
showToast('로그인 정보를 찾을 수 없습니다.', 'error');
|
|
return;
|
|
}
|
|
|
|
// 현재 시간
|
|
const now = new Date();
|
|
const currentTime = now.toTimeString().split(' ')[0]; // HH:MM:SS
|
|
const trainingDate = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
|
|
// 각 서명에 대해 개별적으로 API 호출
|
|
let successCount = 0;
|
|
for (let i = 0; i < savedSignatures.length; i++) {
|
|
const sig = savedSignatures[i];
|
|
|
|
const payload = {
|
|
request_id: requestId,
|
|
conducted_by: currentUser.user_id,
|
|
training_date: trainingDate,
|
|
training_start_time: currentTime,
|
|
training_end_time: currentTime,
|
|
training_items: trainingItems,
|
|
visitor_name: `방문자 ${i + 1}`, // 순번으로 구분
|
|
signature_image: sig.image,
|
|
notes: `교육 완료 - ${checkedItems.length}개 항목 (${i + 1}/${savedSignatures.length})`
|
|
};
|
|
|
|
const response = await window.apiCall(
|
|
'/workplace-visits/training',
|
|
'POST',
|
|
payload
|
|
);
|
|
|
|
if (response && response.success) {
|
|
successCount++;
|
|
} else {
|
|
console.error(`서명 ${i + 1} 저장 실패:`, response);
|
|
}
|
|
}
|
|
|
|
if (successCount === savedSignatures.length) {
|
|
showToast(`${successCount}명의 안전교육이 완료되었습니다.`, 'success');
|
|
setTimeout(() => {
|
|
window.location.href = '/pages/safety/management.html';
|
|
}, 1500);
|
|
} else if (successCount > 0) {
|
|
showToast(`${successCount}/${savedSignatures.length}명의 교육만 저장되었습니다.`, 'warning');
|
|
} else {
|
|
throw new Error('교육 완료 처리 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('교육 완료 처리 오류:', error);
|
|
showToast(error.message || '교육 완료 처리 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 뒤로 가기
|
|
*/
|
|
function goBack() {
|
|
if (hasSignature || document.querySelector('input[name="safety-check"]:checked')) {
|
|
if (!confirm('작성 중인 내용이 있습니다. 정말 나가시겠습니까?')) {
|
|
return;
|
|
}
|
|
}
|
|
window.location.href = '/pages/safety/management.html';
|
|
}
|
|
|
|
// 전역 함수로 노출
|
|
window.clearSignature = clearSignature;
|
|
window.saveSignature = saveSignature;
|
|
window.deleteSignature = deleteSignature;
|
|
window.updateCompleteButton = updateCompleteButton;
|
|
window.completeTraining = completeTraining;
|
|
window.goBack = goBack;
|