/** * 신고 등록 페이지 JavaScript * URL 파라미터 ?type=nonconformity 또는 ?type=safety로 유형 사전 선택 지원 */ // API 설정 const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api'; // 상태 변수 let selectedFactoryId = null; let selectedWorkplaceId = null; let selectedWorkplaceName = null; let selectedType = null; // 'nonconformity' | 'safety' let selectedCategoryId = null; let selectedCategoryName = null; let selectedItemId = null; let selectedTbmSessionId = null; let selectedVisitRequestId = null; let photos = [null, null, null, null, null]; let customItemName = null; // 직접 입력한 항목명 // 지도 관련 변수 let canvas, ctx, canvasImage; let mapRegions = []; let todayWorkers = []; let todayVisitors = []; // DOM 요소 let factorySelect, issueMapCanvas; let photoInput, currentPhotoIndex; // 초기화 document.addEventListener('DOMContentLoaded', async () => { factorySelect = document.getElementById('factorySelect'); issueMapCanvas = document.getElementById('issueMapCanvas'); photoInput = document.getElementById('photoInput'); canvas = issueMapCanvas; ctx = canvas.getContext('2d'); // 이벤트 리스너 설정 setupEventListeners(); // 공장 목록 로드 await loadFactories(); // URL 파라미터에서 유형 확인 및 자동 선택 const urlParams = new URLSearchParams(window.location.search); const preselectedType = urlParams.get('type'); if (preselectedType === 'nonconformity' || preselectedType === 'safety') { onTypeSelect(preselectedType); } }); /** * 이벤트 리스너 설정 */ function setupEventListeners() { // 공장 선택 factorySelect.addEventListener('change', onFactoryChange); // 지도 클릭 canvas.addEventListener('click', onMapClick); // 기타 위치 토글 document.getElementById('useCustomLocation').addEventListener('change', (e) => { const customInput = document.getElementById('customLocationInput'); customInput.classList.toggle('visible', e.target.checked); if (e.target.checked) { // 지도 선택 초기화 selectedWorkplaceId = null; selectedWorkplaceName = null; selectedTbmSessionId = null; selectedVisitRequestId = null; updateLocationInfo(); } }); // 유형 버튼 클릭 document.querySelectorAll('.type-btn').forEach(btn => { btn.addEventListener('click', () => onTypeSelect(btn.dataset.type)); }); // 사진 슬롯 클릭 document.querySelectorAll('.photo-slot').forEach(slot => { slot.addEventListener('click', (e) => { if (e.target.classList.contains('remove-btn')) return; currentPhotoIndex = parseInt(slot.dataset.index); photoInput.click(); }); }); // 사진 삭제 버튼 document.querySelectorAll('.photo-slot .remove-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const slot = btn.closest('.photo-slot'); const index = parseInt(slot.dataset.index); removePhoto(index); }); }); // 사진 선택 photoInput.addEventListener('change', onPhotoSelect); } /** * 공장 목록 로드 */ async function loadFactories() { try { const response = await fetch(`${API_BASE}/workplaces/categories/active/list`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('공장 목록 조회 실패'); const data = await response.json(); if (data.success && data.data) { data.data.forEach(factory => { const option = document.createElement('option'); option.value = factory.category_id; option.textContent = factory.category_name; factorySelect.appendChild(option); }); // 첫 번째 공장 자동 선택 if (data.data.length > 0) { factorySelect.value = data.data[0].category_id; onFactoryChange(); } } } catch (error) { console.error('공장 목록 로드 실패:', error); } } /** * 공장 변경 시 */ async function onFactoryChange() { selectedFactoryId = factorySelect.value; if (!selectedFactoryId) return; // 위치 선택 초기화 selectedWorkplaceId = null; selectedWorkplaceName = null; selectedTbmSessionId = null; selectedVisitRequestId = null; updateLocationInfo(); // 지도 데이터 로드 await Promise.all([ loadMapImage(), loadMapRegions(), loadTodayData() ]); renderMap(); } /** * 배치도 이미지 로드 */ async function loadMapImage() { try { const response = await fetch(`${API_BASE}/workplaces/categories`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) return; const data = await response.json(); if (data.success && data.data) { const selectedCategory = data.data.find(c => c.category_id == selectedFactoryId); if (selectedCategory && selectedCategory.layout_image) { const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', ''); const fullImageUrl = selectedCategory.layout_image.startsWith('http') ? selectedCategory.layout_image : `${baseUrl}${selectedCategory.layout_image}`; canvasImage = new Image(); canvasImage.onload = () => renderMap(); canvasImage.src = fullImageUrl; } } } catch (error) { console.error('배치도 이미지 로드 실패:', error); } } /** * 지도 영역 로드 */ async function loadMapRegions() { try { const response = await fetch(`${API_BASE}/workplaces/categories/${selectedFactoryId}/map-regions`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) return; const data = await response.json(); if (data.success) { mapRegions = data.data || []; } } catch (error) { console.error('지도 영역 로드 실패:', error); } } /** * 오늘 TBM/출입신청 데이터 로드 */ async function loadTodayData() { // 로컬 시간대 기준으로 오늘 날짜 구하기 (UTC가 아닌 한국 시간 기준) const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const today = `${year}-${month}-${day}`; console.log('[신고페이지] 조회 날짜 (로컬):', today); try { // TBM 세션 로드 const tbmResponse = await fetch(`${API_BASE}/tbm/sessions/date/${today}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (tbmResponse.ok) { const tbmData = await tbmResponse.json(); const sessions = tbmData.data || []; // TBM 세션 데이터를 가공하여 member_count 계산 todayWorkers = sessions.map(session => { const memberCount = session.team_member_count || 0; const leaderCount = session.leader_id ? 1 : 0; return { ...session, member_count: memberCount + leaderCount }; }); console.log('[신고페이지] 로드된 TBM 작업:', todayWorkers.length, '건'); } // 출입 신청 로드 const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (visitResponse.ok) { const visitData = await visitResponse.json(); todayVisitors = (visitData.data || []).filter(v => { // 로컬 날짜로 비교 const visitDateObj = new Date(v.visit_date); const visitYear = visitDateObj.getFullYear(); const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0'); const visitDay = String(visitDateObj.getDate()).padStart(2, '0'); const visitDate = `${visitYear}-${visitMonth}-${visitDay}`; return visitDate === today && (v.status === 'approved' || v.status === 'training_completed'); }); console.log('[신고페이지] 로드된 방문자:', todayVisitors.length, '건'); } } catch (error) { console.error('오늘 데이터 로드 실패:', error); } } /** * 둥근 모서리 사각형 그리기 (Canvas roundRect 폴리필) */ function drawRoundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } /** * 지도 렌더링 */ function renderMap() { if (!canvas || !ctx) return; // 컨테이너 너비 가져오기 const container = canvas.parentElement; const containerWidth = container.clientWidth - 2; // border 고려 const maxWidth = Math.min(containerWidth, 800); // 이미지가 로드된 경우 이미지 비율에 맞춰 캔버스 크기 설정 if (canvasImage && canvasImage.complete && canvasImage.naturalWidth > 0) { const imgWidth = canvasImage.naturalWidth; const imgHeight = canvasImage.naturalHeight; // 스케일 계산 (maxWidth에 맞춤) const scale = imgWidth > maxWidth ? maxWidth / imgWidth : 1; canvas.width = imgWidth * scale; canvas.height = imgHeight * scale; // 이미지 그리기 ctx.drawImage(canvasImage, 0, 0, canvas.width, canvas.height); } else { // 이미지가 없는 경우 기본 크기 canvas.width = maxWidth; canvas.height = 400; // 배경 그리기 ctx.fillStyle = '#f3f4f6'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 이미지 없음 안내 ctx.fillStyle = '#9ca3af'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('배치도 이미지가 없습니다', canvas.width / 2, canvas.height / 2); } // 작업장 영역 그리기 (퍼센트 좌표 사용) mapRegions.forEach(region => { const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id); const workerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0); const visitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0); drawWorkplaceRegion(region, workerCount, visitorCount); }); } /** * 작업장 영역 그리기 */ function drawWorkplaceRegion(region, workerCount, visitorCount) { 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; const width = x2 - x1; const height = y2 - y1; // 선택된 작업장 하이라이트 const isSelected = region.workplace_id === selectedWorkplaceId; // 색상 결정 (더 진하게 조정) let fillColor, strokeColor, textColor; if (isSelected) { fillColor = 'rgba(34, 197, 94, 0.5)'; // 초록색 (선택됨) strokeColor = 'rgb(22, 163, 74)'; textColor = '#15803d'; } else if (workerCount > 0 && visitorCount > 0) { fillColor = 'rgba(34, 197, 94, 0.4)'; // 초록색 (작업+방문) strokeColor = 'rgb(22, 163, 74)'; textColor = '#166534'; } else if (workerCount > 0) { fillColor = 'rgba(59, 130, 246, 0.4)'; // 파란색 (작업만) strokeColor = 'rgb(37, 99, 235)'; textColor = '#1e40af'; } else if (visitorCount > 0) { fillColor = 'rgba(168, 85, 247, 0.4)'; // 보라색 (방문만) strokeColor = 'rgb(147, 51, 234)'; textColor = '#7c3aed'; } else { fillColor = 'rgba(107, 114, 128, 0.35)'; // 회색 (없음) - 더 진하게 strokeColor = 'rgb(75, 85, 99)'; textColor = '#374151'; } ctx.fillStyle = fillColor; ctx.strokeStyle = strokeColor; ctx.lineWidth = isSelected ? 4 : 2.5; ctx.beginPath(); ctx.rect(x1, y1, width, height); ctx.fill(); ctx.stroke(); // 작업장명 표시 (배경 추가로 가독성 향상) const centerX = x1 + width / 2; const centerY = y1 + height / 2; // 텍스트 배경 ctx.font = 'bold 13px sans-serif'; const textMetrics = ctx.measureText(region.workplace_name); const textWidth = textMetrics.width + 12; const textHeight = 20; ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; drawRoundRect(ctx, centerX - textWidth / 2, centerY - textHeight / 2, textWidth, textHeight, 4); ctx.fill(); // 텍스트 ctx.fillStyle = textColor; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(region.workplace_name, centerX, centerY); // 인원수 표시 const total = workerCount + visitorCount; if (total > 0) { // 인원수 배경 ctx.font = 'bold 12px sans-serif'; const countText = `${total}명`; const countMetrics = ctx.measureText(countText); const countWidth = countMetrics.width + 10; const countHeight = 18; ctx.fillStyle = strokeColor; drawRoundRect(ctx, centerX - countWidth / 2, centerY + 12, countWidth, countHeight, 4); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.fillText(countText, centerX, centerY + 21); } } /** * 지도 클릭 처리 */ function onMapClick(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.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; } } } /** * 작업장 선택 */ function selectWorkplace(region) { // 기타 위치 체크박스 해제 document.getElementById('useCustomLocation').checked = false; document.getElementById('customLocationInput').classList.remove('visible'); selectedWorkplaceId = region.workplace_id; selectedWorkplaceName = region.workplace_name; // 해당 작업장의 TBM/출입신청 확인 const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id); if (workers.length > 0 || visitors.length > 0) { // 작업 선택 모달 표시 showWorkSelectionModal(workers, visitors); } else { selectedTbmSessionId = null; selectedVisitRequestId = null; } updateLocationInfo(); renderMap(); updateStepStatus(); } /** * 작업 선택 모달 표시 */ function showWorkSelectionModal(workers, visitors) { const modal = document.getElementById('workSelectionModal'); const optionsList = document.getElementById('workOptionsList'); optionsList.innerHTML = ''; // TBM 작업 옵션 workers.forEach(w => { const option = document.createElement('div'); option.className = 'work-option'; const safeTaskName = escapeHtml(w.task_name || '작업'); const safeProjectName = escapeHtml(w.project_name || ''); const memberCount = parseInt(w.member_count) || 0; option.innerHTML = `