/** * 문제 신고 등록 페이지 JavaScript */ // 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 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(); }); /** * 이벤트 리스너 설정 */ 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() { const today = new Date().toISOString().split('T')[0]; 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(); todayWorkers = tbmData.data || []; } // 출입 신청 로드 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 => v.visit_date === today && (v.status === 'approved' || v.status === 'training_completed') ); } } catch (error) { console.error('오늘 데이터 로드 실패:', error); } } /** * 지도 렌더링 */ function renderMap() { if (!canvas || !ctx) return; // 캔버스 크기 설정 const container = canvas.parentElement; canvas.width = container.clientWidth; canvas.height = 400; // 배경 그리기 ctx.fillStyle = '#f3f4f6'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 배치도 이미지 if (canvasImage && canvasImage.complete) { const scale = Math.min(canvas.width / canvasImage.width, canvas.height / canvasImage.height); const x = (canvas.width - canvasImage.width * scale) / 2; const y = (canvas.height - canvasImage.height * scale) / 2; ctx.drawImage(canvasImage, x, y, canvasImage.width * scale, canvasImage.height * scale); } // 작업장 영역 그리기 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; if (isSelected) { fillColor = 'rgba(34, 197, 94, 0.3)'; // 초록색 strokeColor = 'rgb(34, 197, 94)'; } else if (workerCount > 0 && visitorCount > 0) { fillColor = 'rgba(34, 197, 94, 0.2)'; // 초록색 (작업+방문) strokeColor = 'rgb(34, 197, 94)'; } else if (workerCount > 0) { fillColor = 'rgba(59, 130, 246, 0.2)'; // 파란색 (작업만) strokeColor = 'rgb(59, 130, 246)'; } else if (visitorCount > 0) { fillColor = 'rgba(168, 85, 247, 0.2)'; // 보라색 (방문만) strokeColor = 'rgb(168, 85, 247)'; } else { fillColor = 'rgba(156, 163, 175, 0.2)'; // 회색 (없음) strokeColor = 'rgb(156, 163, 175)'; } ctx.fillStyle = fillColor; ctx.strokeStyle = strokeColor; ctx.lineWidth = isSelected ? 3 : 2; ctx.beginPath(); ctx.rect(x1, y1, width, height); ctx.fill(); ctx.stroke(); // 작업장명 표시 const centerX = x1 + width / 2; const centerY = y1 + height / 2; ctx.fillStyle = '#374151'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(region.workplace_name, centerX, centerY); // 인원수 표시 const total = workerCount + visitorCount; if (total > 0) { ctx.fillStyle = strokeColor; ctx.font = 'bold 14px sans-serif'; ctx.fillText(`(${total}명)`, centerX, centerY + 16); } } /** * 지도 클릭 처리 */ 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'; option.innerHTML = `
TBM: ${w.task_name || '작업'}
${w.project_name || ''} - ${w.member_count || 0}명
`; option.onclick = () => { selectedTbmSessionId = w.session_id; selectedVisitRequestId = null; closeWorkModal(); updateLocationInfo(); }; optionsList.appendChild(option); }); // 출입신청 옵션 visitors.forEach(v => { const option = document.createElement('div'); option.className = 'work-option'; option.innerHTML = `
출입: ${v.visitor_company}
${v.purpose_name || '방문'} - ${v.visitor_count || 0}명
`; option.onclick = () => { selectedVisitRequestId = v.request_id; selectedTbmSessionId = null; closeWorkModal(); updateLocationInfo(); }; optionsList.appendChild(option); }); modal.classList.add('visible'); } /** * 작업 선택 모달 닫기 */ function closeWorkModal() { document.getElementById('workSelectionModal').classList.remove('visible'); } /** * 선택된 위치 정보 업데이트 */ function updateLocationInfo() { const infoBox = document.getElementById('selectedLocationInfo'); const customLocation = document.getElementById('customLocation').value; const useCustom = document.getElementById('useCustomLocation').checked; if (useCustom && customLocation) { infoBox.classList.remove('empty'); infoBox.innerHTML = `선택된 위치: ${customLocation}`; } else if (selectedWorkplaceName) { infoBox.classList.remove('empty'); let html = `선택된 위치: ${selectedWorkplaceName}`; if (selectedTbmSessionId) { const worker = todayWorkers.find(w => w.session_id === selectedTbmSessionId); if (worker) { html += `
연결 작업: ${worker.task_name} (TBM)`; } } else if (selectedVisitRequestId) { const visitor = todayVisitors.find(v => v.request_id === selectedVisitRequestId); if (visitor) { html += `
연결 작업: ${visitor.visitor_company} (출입)`; } } infoBox.innerHTML = html; } else { infoBox.classList.add('empty'); infoBox.textContent = '지도에서 작업장을 클릭하여 위치를 선택하세요'; } } /** * 유형 선택 */ function onTypeSelect(type) { selectedType = type; selectedCategoryId = null; selectedCategoryName = null; selectedItemId = null; // 버튼 상태 업데이트 document.querySelectorAll('.type-btn').forEach(btn => { btn.classList.toggle('selected', btn.dataset.type === type); }); // 카테고리 로드 loadCategories(type); updateStepStatus(); } /** * 카테고리 로드 */ async function loadCategories(type) { try { const response = await fetch(`${API_BASE}/work-issues/categories/type/${type}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('카테고리 조회 실패'); const data = await response.json(); if (data.success && data.data) { renderCategories(data.data); } } catch (error) { console.error('카테고리 로드 실패:', error); } } /** * 카테고리 렌더링 */ function renderCategories(categories) { const container = document.getElementById('categoryContainer'); const grid = document.getElementById('categoryGrid'); grid.innerHTML = ''; categories.forEach(cat => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'category-btn'; btn.textContent = cat.category_name; btn.onclick = () => onCategorySelect(cat); grid.appendChild(btn); }); container.style.display = 'block'; } /** * 카테고리 선택 */ function onCategorySelect(category) { selectedCategoryId = category.category_id; selectedCategoryName = category.category_name; selectedItemId = null; // 버튼 상태 업데이트 document.querySelectorAll('.category-btn').forEach(btn => { btn.classList.toggle('selected', btn.textContent === category.category_name); }); // 항목 로드 loadItems(category.category_id); updateStepStatus(); } /** * 항목 로드 */ async function loadItems(categoryId) { try { const response = await fetch(`${API_BASE}/work-issues/items/category/${categoryId}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('항목 조회 실패'); const data = await response.json(); if (data.success && data.data) { renderItems(data.data); } } catch (error) { console.error('항목 로드 실패:', error); } } /** * 항목 렌더링 */ function renderItems(items) { const grid = document.getElementById('itemGrid'); grid.innerHTML = ''; if (items.length === 0) { grid.innerHTML = '

등록된 항목이 없습니다

'; return; } items.forEach(item => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'item-btn'; btn.textContent = item.item_name; btn.dataset.severity = item.severity; btn.onclick = () => onItemSelect(item, btn); grid.appendChild(btn); }); } /** * 항목 선택 */ function onItemSelect(item, btn) { // 단일 선택 (기존 선택 해제) document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); selectedItemId = item.item_id; updateStepStatus(); } /** * 사진 선택 */ function onPhotoSelect(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { photos[currentPhotoIndex] = event.target.result; updatePhotoSlot(currentPhotoIndex); }; reader.readAsDataURL(file); // 입력 초기화 e.target.value = ''; } /** * 사진 슬롯 업데이트 */ function updatePhotoSlot(index) { const slot = document.querySelector(`.photo-slot[data-index="${index}"]`); if (photos[index]) { slot.classList.add('has-photo'); let img = slot.querySelector('img'); if (!img) { img = document.createElement('img'); slot.insertBefore(img, slot.firstChild); } img.src = photos[index]; } else { slot.classList.remove('has-photo'); const img = slot.querySelector('img'); if (img) img.remove(); } } /** * 사진 삭제 */ function removePhoto(index) { photos[index] = null; updatePhotoSlot(index); } /** * 단계 상태 업데이트 */ function updateStepStatus() { const steps = document.querySelectorAll('.step'); const customLocation = document.getElementById('customLocation').value; const useCustom = document.getElementById('useCustomLocation').checked; // Step 1: 위치 const step1Complete = (useCustom && customLocation) || selectedWorkplaceId; steps[0].classList.toggle('completed', step1Complete); steps[1].classList.toggle('active', step1Complete); // Step 2: 유형 const step2Complete = selectedType && selectedCategoryId; steps[1].classList.toggle('completed', step2Complete); steps[2].classList.toggle('active', step2Complete); // Step 3: 항목 const step3Complete = selectedItemId; steps[2].classList.toggle('completed', step3Complete); steps[3].classList.toggle('active', step3Complete); // 제출 버튼 활성화 const submitBtn = document.getElementById('submitBtn'); const hasPhoto = photos.some(p => p !== null); submitBtn.disabled = !(step1Complete && step2Complete && hasPhoto); } /** * 신고 제출 */ async function submitReport() { const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = true; submitBtn.textContent = '제출 중...'; try { const useCustom = document.getElementById('useCustomLocation').checked; const customLocation = document.getElementById('customLocation').value; const additionalDescription = document.getElementById('additionalDescription').value; const requestBody = { factory_category_id: useCustom ? null : selectedFactoryId, workplace_id: useCustom ? null : selectedWorkplaceId, custom_location: useCustom ? customLocation : null, tbm_session_id: selectedTbmSessionId, visit_request_id: selectedVisitRequestId, issue_category_id: selectedCategoryId, issue_item_id: selectedItemId, additional_description: additionalDescription || null, photos: photos.filter(p => p !== null) }; const response = await fetch(`${API_BASE}/work-issues`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.success) { alert('문제 신고가 등록되었습니다.'); window.location.href = '/pages/safety/issue-list.html'; } else { throw new Error(data.error || '신고 등록 실패'); } } catch (error) { console.error('신고 제출 실패:', error); alert('신고 등록에 실패했습니다: ' + error.message); } finally { submitBtn.disabled = false; submitBtn.textContent = '신고 제출'; } } // 기타 위치 입력 시 위치 정보 업데이트 document.addEventListener('DOMContentLoaded', () => { const customLocationInput = document.getElementById('customLocation'); if (customLocationInput) { customLocationInput.addEventListener('input', () => { updateLocationInfo(); updateStepStatus(); }); } });