- 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>
444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
// 출입 신청 페이지 JavaScript
|
|
|
|
let categories = [];
|
|
let workplaces = [];
|
|
let mapRegions = [];
|
|
let visitPurposes = [];
|
|
let selectedWorkplace = null;
|
|
let selectedCategory = null;
|
|
let canvas = null;
|
|
let ctx = null;
|
|
let layoutImage = null;
|
|
|
|
// showToast, createToastContainer → api-base.js 전역 사용
|
|
|
|
// ==================== 초기화 ====================
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// 오늘 날짜 기본값 설정
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.getElementById('visitDate').value = today;
|
|
document.getElementById('visitDate').min = today;
|
|
|
|
// 현재 시간 + 1시간 기본값 설정
|
|
const now = new Date();
|
|
now.setHours(now.getHours() + 1);
|
|
const timeString = now.toTimeString().slice(0, 5);
|
|
document.getElementById('visitTime').value = timeString;
|
|
|
|
// 데이터 로드
|
|
await loadCategories();
|
|
await loadVisitPurposes();
|
|
await loadMyRequests();
|
|
|
|
// 폼 제출 이벤트
|
|
document.getElementById('visitRequestForm').addEventListener('submit', handleSubmit);
|
|
|
|
// 캔버스 초기화
|
|
canvas = document.getElementById('workplaceMapCanvas');
|
|
ctx = canvas.getContext('2d');
|
|
});
|
|
|
|
// ==================== 데이터 로드 ====================
|
|
|
|
/**
|
|
* 카테고리(공장) 목록 로드
|
|
*/
|
|
async function loadCategories() {
|
|
try {
|
|
const response = await window.apiCall('/workplaces/categories', 'GET');
|
|
if (response && response.success) {
|
|
categories = response.data || [];
|
|
|
|
const categorySelect = document.getElementById('categorySelect');
|
|
categorySelect.innerHTML = '<option value="">구역을 선택하세요</option>';
|
|
|
|
categories.forEach(cat => {
|
|
if (cat.is_active) {
|
|
const option = document.createElement('option');
|
|
option.value = cat.category_id;
|
|
option.textContent = cat.category_name;
|
|
categorySelect.appendChild(option);
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('카테고리 로드 오류:', error);
|
|
window.showToast('카테고리 로드 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 방문 목적 목록 로드
|
|
*/
|
|
async function loadVisitPurposes() {
|
|
try {
|
|
const response = await window.apiCall('/workplace-visits/purposes/active', 'GET');
|
|
if (response && response.success) {
|
|
visitPurposes = response.data || [];
|
|
|
|
const purposeSelect = document.getElementById('visitPurpose');
|
|
purposeSelect.innerHTML = '<option value="">선택하세요</option>';
|
|
|
|
visitPurposes.forEach(purpose => {
|
|
const option = document.createElement('option');
|
|
option.value = purpose.purpose_id;
|
|
option.textContent = purpose.purpose_name;
|
|
purposeSelect.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('방문 목적 로드 오류:', error);
|
|
window.showToast('방문 목적 로드 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 내 출입 신청 목록 로드
|
|
*/
|
|
async function loadMyRequests() {
|
|
try {
|
|
// localStorage에서 사용자 정보 가져오기
|
|
const userData = localStorage.getItem('sso_user');
|
|
const currentUser = userData ? JSON.parse(userData) : null;
|
|
|
|
if (!currentUser || !currentUser.user_id) {
|
|
console.log('사용자 정보 없음');
|
|
return;
|
|
}
|
|
|
|
const response = await window.apiCall(`/workplace-visits/requests?requester_id=${currentUser.user_id}`, 'GET');
|
|
|
|
if (response && response.success) {
|
|
const requests = response.data || [];
|
|
renderMyRequests(requests);
|
|
}
|
|
} catch (error) {
|
|
console.error('내 신청 목록 로드 오류:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 내 신청 목록 렌더링
|
|
*/
|
|
function renderMyRequests(requests) {
|
|
const listDiv = document.getElementById('myRequestsList');
|
|
|
|
if (requests.length === 0) {
|
|
listDiv.innerHTML = '<p style="text-align: center; color: var(--gray-500); padding: 32px;">신청 내역이 없습니다</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
requests.forEach(req => {
|
|
const statusText = {
|
|
'pending': '승인 대기',
|
|
'approved': '승인됨',
|
|
'rejected': '반려됨',
|
|
'training_completed': '교육 완료'
|
|
}[req.status] || req.status;
|
|
|
|
html += `
|
|
<div class="request-card">
|
|
<div class="request-card-header">
|
|
<h3 style="margin: 0; font-size: var(--text-lg);">${req.visitor_company} (${req.visitor_count}명)</h3>
|
|
<span class="request-status ${req.status}">${statusText}</span>
|
|
</div>
|
|
<div class="request-info">
|
|
<div class="info-item">
|
|
<span class="info-label">방문 작업장</span>
|
|
<span class="info-value">${req.category_name} - ${req.workplace_name}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">방문 일시</span>
|
|
<span class="info-value">${req.visit_date} ${req.visit_time}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">방문 목적</span>
|
|
<span class="info-value">${req.purpose_name}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">신청일</span>
|
|
<span class="info-value">${new Date(req.created_at).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
${req.rejection_reason ? `<p style="margin-top: 12px; padding: 12px; background: var(--red-50); color: var(--red-700); border-radius: var(--radius-md); font-size: var(--text-sm);"><strong>반려 사유:</strong> ${req.rejection_reason}</p>` : ''}
|
|
${req.notes ? `<p style="margin-top: 12px; color: var(--gray-600); font-size: var(--text-sm);"><strong>비고:</strong> ${req.notes}</p>` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
listDiv.innerHTML = html;
|
|
}
|
|
|
|
// ==================== 작업장 지도 모달 ====================
|
|
|
|
/**
|
|
* 지도 모달 열기
|
|
*/
|
|
function openMapModal() {
|
|
document.getElementById('mapModal').style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
/**
|
|
* 지도 모달 닫기
|
|
*/
|
|
function closeMapModal() {
|
|
document.getElementById('mapModal').style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
/**
|
|
* 작업장 지도 로드
|
|
*/
|
|
async function loadWorkplaceMap() {
|
|
const categoryId = document.getElementById('categorySelect').value;
|
|
if (!categoryId) {
|
|
document.getElementById('mapCanvasContainer').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
selectedCategory = categories.find(c => c.category_id == categoryId);
|
|
|
|
try {
|
|
// 작업장 목록 로드
|
|
const workplacesResponse = await window.apiCall(`/workplaces/categories/${categoryId}`, 'GET');
|
|
if (workplacesResponse && workplacesResponse.success) {
|
|
workplaces = workplacesResponse.data || [];
|
|
}
|
|
|
|
// 지도 영역 로드
|
|
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`, 'GET');
|
|
if (regionsResponse && regionsResponse.success) {
|
|
mapRegions = regionsResponse.data || [];
|
|
}
|
|
|
|
// 레이아웃 이미지가 있으면 표시
|
|
if (selectedCategory && selectedCategory.layout_image) {
|
|
// API_BASE_URL에서 /api 제거하고 이미지 경로 생성
|
|
const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', '');
|
|
const fullImageUrl = selectedCategory.layout_image.startsWith('http')
|
|
? selectedCategory.layout_image
|
|
: `${baseUrl}${selectedCategory.layout_image}`;
|
|
|
|
console.log('이미지 URL:', fullImageUrl);
|
|
loadImageToCanvas(fullImageUrl);
|
|
document.getElementById('mapCanvasContainer').style.display = 'block';
|
|
} else {
|
|
window.showToast('선택한 구역에 레이아웃 지도가 없습니다.', 'warning');
|
|
document.getElementById('mapCanvasContainer').style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error('작업장 지도 로드 오류:', error);
|
|
window.showToast('작업장 지도 로드 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지를 캔버스에 로드
|
|
*/
|
|
function loadImageToCanvas(imagePath) {
|
|
const img = new Image();
|
|
// crossOrigin 제거 - 같은 도메인이므로 불필요
|
|
img.onload = function() {
|
|
// 캔버스 크기 설정
|
|
const maxWidth = 800;
|
|
const scale = img.width > maxWidth ? maxWidth / img.width : 1;
|
|
|
|
canvas.width = img.width * scale;
|
|
canvas.height = img.height * scale;
|
|
|
|
// 이미지 그리기
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
|
|
layoutImage = img;
|
|
|
|
// 영역 표시
|
|
drawRegions();
|
|
|
|
// 클릭 이벤트 등록
|
|
canvas.onclick = handleCanvasClick;
|
|
};
|
|
img.onerror = function() {
|
|
window.showToast('지도 이미지를 불러올 수 없습니다.', 'error');
|
|
};
|
|
img.src = imagePath;
|
|
}
|
|
|
|
/**
|
|
* 지도 영역 그리기
|
|
*/
|
|
function drawRegions() {
|
|
mapRegions.forEach(region => {
|
|
const x1 = (region.x_start / 100) * canvas.width;
|
|
const y1 = (region.y_start / 100) * canvas.height;
|
|
const x2 = (region.x_end / 100) * canvas.width;
|
|
const y2 = (region.y_end / 100) * canvas.height;
|
|
|
|
// 영역 박스
|
|
ctx.strokeStyle = '#10b981';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
|
|
|
ctx.fillStyle = 'rgba(16, 185, 129, 0.1)';
|
|
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
|
|
|
|
// 작업장 이름
|
|
ctx.fillStyle = '#10b981';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.fillText(region.workplace_name || '', x1 + 5, y1 + 20);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 캔버스 클릭 핸들러
|
|
*/
|
|
function handleCanvasClick(event) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
// 클릭한 위치의 영역 찾기
|
|
for (const region of mapRegions) {
|
|
const x1 = (region.x_start / 100) * canvas.width;
|
|
const y1 = (region.y_start / 100) * canvas.height;
|
|
const x2 = (region.x_end / 100) * canvas.width;
|
|
const y2 = (region.y_end / 100) * canvas.height;
|
|
|
|
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {
|
|
// 작업장 선택
|
|
selectWorkplace(region);
|
|
return;
|
|
}
|
|
}
|
|
|
|
window.showToast('작업장 영역을 클릭해주세요.', 'warning');
|
|
}
|
|
|
|
/**
|
|
* 작업장 선택
|
|
*/
|
|
function selectWorkplace(region) {
|
|
selectedWorkplace = {
|
|
workplace_id: region.workplace_id,
|
|
workplace_name: region.workplace_name,
|
|
category_id: selectedCategory.category_id,
|
|
category_name: selectedCategory.category_name
|
|
};
|
|
|
|
// 선택 표시
|
|
const selectionDiv = document.getElementById('workplaceSelection');
|
|
selectionDiv.classList.add('selected');
|
|
selectionDiv.innerHTML = `
|
|
<div class="icon">✅</div>
|
|
<div class="text">${selectedCategory.category_name} - ${region.workplace_name}</div>
|
|
`;
|
|
|
|
// 상세 정보 카드 표시
|
|
const infoDiv = document.getElementById('selectedWorkplaceInfo');
|
|
infoDiv.style.display = 'block';
|
|
infoDiv.innerHTML = `
|
|
<div class="workplace-info-card">
|
|
<div class="icon">📍</div>
|
|
<div class="details">
|
|
<div class="name">${region.workplace_name}</div>
|
|
<div class="category">${selectedCategory.category_name}</div>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-secondary" onclick="clearWorkplaceSelection()">변경</button>
|
|
</div>
|
|
`;
|
|
|
|
// 모달 닫기
|
|
closeMapModal();
|
|
|
|
window.showToast(`${region.workplace_name} 작업장이 선택되었습니다.`, 'success');
|
|
}
|
|
|
|
/**
|
|
* 작업장 선택 초기화
|
|
*/
|
|
function clearWorkplaceSelection() {
|
|
selectedWorkplace = null;
|
|
|
|
const selectionDiv = document.getElementById('workplaceSelection');
|
|
selectionDiv.classList.remove('selected');
|
|
selectionDiv.innerHTML = `
|
|
<div class="icon">📍</div>
|
|
<div class="text">지도에서 작업장을 선택하세요</div>
|
|
`;
|
|
|
|
document.getElementById('selectedWorkplaceInfo').style.display = 'none';
|
|
}
|
|
|
|
// ==================== 폼 제출 ====================
|
|
|
|
/**
|
|
* 출입 신청 제출
|
|
*/
|
|
async function handleSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
if (!selectedWorkplace) {
|
|
window.showToast('작업장을 선택해주세요.', 'warning');
|
|
openMapModal();
|
|
return;
|
|
}
|
|
|
|
const formData = {
|
|
visitor_company: document.getElementById('visitorCompany').value.trim(),
|
|
visitor_count: parseInt(document.getElementById('visitorCount').value),
|
|
category_id: selectedWorkplace.category_id,
|
|
workplace_id: selectedWorkplace.workplace_id,
|
|
visit_date: document.getElementById('visitDate').value,
|
|
visit_time: document.getElementById('visitTime').value,
|
|
purpose_id: parseInt(document.getElementById('visitPurpose').value),
|
|
notes: document.getElementById('notes').value.trim() || null
|
|
};
|
|
|
|
try {
|
|
const response = await window.apiCall('/workplace-visits/requests', 'POST', formData);
|
|
|
|
if (response && response.success) {
|
|
window.showToast('출입 신청 및 안전교육 신청이 완료되었습니다. 안전관리자의 승인을 기다려주세요.', 'success');
|
|
|
|
// 폼 초기화
|
|
resetForm();
|
|
|
|
// 내 신청 목록 새로고침
|
|
await loadMyRequests();
|
|
} else {
|
|
throw new Error(response?.message || '신청 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('출입 신청 오류:', error);
|
|
window.showToast(error.message || '출입 신청 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 폼 초기화
|
|
*/
|
|
function resetForm() {
|
|
document.getElementById('visitRequestForm').reset();
|
|
clearWorkplaceSelection();
|
|
|
|
// 오늘 날짜와 시간 다시 설정
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.getElementById('visitDate').value = today;
|
|
|
|
const now = new Date();
|
|
now.setHours(now.getHours() + 1);
|
|
const timeString = now.toTimeString().slice(0, 5);
|
|
document.getElementById('visitTime').value = timeString;
|
|
|
|
document.getElementById('visitorCount').value = 1;
|
|
}
|
|
|
|
// 전역 함수로 노출
|
|
window.openMapModal = openMapModal;
|
|
window.closeMapModal = closeMapModal;
|
|
window.loadWorkplaceMap = loadWorkplaceMap;
|
|
window.clearWorkplaceSelection = clearWorkplaceSelection;
|
|
window.resetForm = resetForm;
|