// zone-detail.js - 구역 상세 페이지 JavaScript // 전역 상태 let workplaceId = null; let workplaceData = null; let zoneItems = []; let selectedZoneItem = null; let isAddingItem = false; let selectionStart = null; let selectionBox = null; // XSS 방지를 위한 HTML 이스케이프 함수 function escapeHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // axios 설정 대기 function waitForAxiosConfig() { return new Promise((resolve) => { const check = setInterval(() => { if (axios.defaults.baseURL) { clearInterval(check); resolve(); } }, 50); setTimeout(() => { clearInterval(check); resolve(); }, 5000); }); } // 페이지 초기화 document.addEventListener('DOMContentLoaded', async () => { await waitForAxiosConfig(); // URL에서 workplace_id 파라미터 가져오기 const urlParams = new URLSearchParams(window.location.search); workplaceId = urlParams.get('id'); if (!workplaceId) { alert('잘못된 접근입니다.'); goBack(); return; } // 현재 날짜 표시 const now = new Date(); document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' }); // 데이터 로드 await loadWorkplaceDetail(); }); // 작업장 상세 정보 로드 async function loadWorkplaceDetail() { try { const today = new Date().toISOString().slice(0, 10); const response = await axios.get(`/patrol/workplaces/${workplaceId}/detail?date=${today}`); if (response.data.success) { workplaceData = response.data.data; renderPage(); } else { throw new Error(response.data.message || '데이터 로드 실패'); } } catch (error) { console.error('작업장 상세 정보 로드 실패:', error); alert('데이터를 불러오는데 실패했습니다.'); } } // 페이지 렌더링 function renderPage() { if (!workplaceData) return; const { workplace, summary } = workplaceData; // 헤더 정보 document.getElementById('zoneName').textContent = workplace.workplace_name; document.getElementById('zoneCategory').textContent = workplace.category_name || ''; // 요약 카드 renderSummaryCards(summary); // 각 탭 콘텐츠 렌더링 renderMapTab(); renderIssuesTab(); renderEquipmentTab(); renderVisitsTab(); renderTbmTab(); renderPatrolTab(); // 구역 현황 로드 loadZoneItems(); } // ==================== 구역 지도 탭 ==================== // 지도 탭 렌더링 function renderMapTab() { const container = document.getElementById('zoneMapContainer'); const { workplace } = workplaceData; // 작업장 자체의 지도 이미지 사용 const mapImage = workplace.layout_image; if (mapImage) { // 이미지 URL 생성 (API base URL에서 /api 제거) const staticUrl = window.API_BASE_URL ? window.API_BASE_URL.replace(/\/api$/, '') : ''; const imageUrl = mapImage.startsWith('http') ? mapImage : staticUrl + mapImage; container.innerHTML = ` ${escapeHtml(workplace.workplace_name)} 지도 `; } else { container.innerHTML = `
🗺️

작업장 지도 이미지가 등록되지 않았습니다.

지도 없이도 현황을 등록할 수 있습니다.

`; // 지도가 없어도 설비 목록은 표시 renderEquipmentList(); } // 마우스 이벤트 핸들러 설정 setupMapEventHandlers(container); } // 설비 목록 렌더링 (지도가 없을 때 텍스트로 표시) function renderEquipmentList() { if (!workplaceData || !workplaceData.equipments || workplaceData.equipments.length === 0) return; const container = document.getElementById('zoneMapContainer'); const { equipments } = workplaceData; const listHtml = `

등록된 설비 (${equipments.length}개)

${equipments.map(eq => { let statusIcon = '⚙️'; let statusText = '정상'; let statusColor = '#22c55e'; if (eq.status === 'repair_needed' || eq.status === 'repair_external') { statusIcon = '🔧'; statusText = eq.status === 'repair_external' ? '외부수리' : '수리필요'; statusColor = '#ef4444'; } else if (eq.status === 'maintenance') { statusIcon = '⚠️'; statusText = '점검중'; statusColor = '#f59e0b'; } else if (eq.is_temporarily_moved) { statusIcon = '📤'; statusText = '이동됨'; statusColor = '#9ca3af'; } return `
${statusIcon} ${escapeHtml(eq.equipment_name)} ${statusText}
`; }).join('')}
`; container.insertAdjacentHTML('beforeend', listHtml); } // 지도 이미지 로드 완료 function onMapImageLoaded() { renderEquipmentsOnMap(); renderZoneItemsOnMap(); } // 지도 이미지 로드 실패 function onMapImageError() { const container = document.getElementById('zoneMapContainer'); container.innerHTML = `
⚠️

지도 이미지를 불러올 수 없습니다.

지도 없이도 현황을 등록할 수 있습니다.

`; setupMapEventHandlers(container); } // 지도 이벤트 핸들러 설정 function setupMapEventHandlers(container) { container.addEventListener('mousedown', onMapMouseDown); container.addEventListener('mousemove', onMapMouseMove); container.addEventListener('mouseup', onMapMouseUp); container.addEventListener('mouseleave', onMapMouseLeave); // 터치 이벤트 (모바일) container.addEventListener('touchstart', onMapTouchStart, { passive: false }); container.addEventListener('touchmove', onMapTouchMove, { passive: false }); container.addEventListener('touchend', onMapTouchEnd); } // 설비를 지도에 렌더링 function renderEquipmentsOnMap() { const container = document.getElementById('zoneMapContainer'); if (!workplaceData || !workplaceData.equipments) return; const { equipments } = workplaceData; const currentWorkplaceId = parseInt(workplaceId); // 기존 설비 마커 제거 container.querySelectorAll('.equipment-map-marker').forEach(el => el.remove()); equipments.forEach(eq => { // 이 작업장에 표시할 좌표 결정 let x, y, width, height; let isHere = true; // 이 작업장에 현재 있는지 let isVisitor = false; // 다른 곳에서 임시로 온 설비인지 // 원래 이 작업장 설비인 경우 if (eq.workplace_id === currentWorkplaceId) { if (eq.is_temporarily_moved && eq.current_workplace_id !== currentWorkplaceId) { // 다른 곳으로 이동함 - 원래 위치에 "이동됨" 표시 x = eq.map_x_percent; y = eq.map_y_percent; width = eq.map_width_percent || 5; height = eq.map_height_percent || 5; isHere = false; } else { // 현재 여기 있음 x = eq.current_map_x_percent || eq.map_x_percent; y = eq.current_map_y_percent || eq.map_y_percent; width = eq.current_map_width_percent || eq.map_width_percent || 5; height = eq.current_map_height_percent || eq.map_height_percent || 5; } } else if (eq.current_workplace_id === currentWorkplaceId) { // 다른 곳에서 임시로 온 설비 x = eq.current_map_x_percent; y = eq.current_map_y_percent; width = eq.current_map_width_percent || 5; height = eq.current_map_height_percent || 5; isVisitor = true; } else { return; // 이 작업장과 관계없는 설비 } // 좌표가 없으면 건너뜀 if (x === null || x === undefined || y === null || y === undefined) return; // 상태에 따른 색상과 아이콘 let bgColor, borderColor, icon, statusText; if (!isHere) { // 다른 곳으로 이동됨 bgColor = 'rgba(156, 163, 175, 0.3)'; borderColor = '#9ca3af'; icon = '📤'; statusText = `→ ${eq.current_workplace_name || '외부'}`; } else if (eq.status === 'repair_needed' || eq.status === 'repair_external') { // 수리 필요/요청 bgColor = 'rgba(239, 68, 68, 0.4)'; borderColor = '#ef4444'; icon = '🔧'; statusText = eq.status === 'repair_external' ? '외부수리중' : '수리필요'; } else if (eq.status === 'maintenance') { // 점검중 bgColor = 'rgba(245, 158, 11, 0.4)'; borderColor = '#f59e0b'; icon = '⚠️'; statusText = '점검중'; } else if (isVisitor) { // 다른 곳에서 온 임시 설비 bgColor = 'rgba(139, 92, 246, 0.4)'; borderColor = '#8b5cf6'; icon = '📥'; statusText = '임시배치'; } else { // 정상 bgColor = 'rgba(34, 197, 94, 0.4)'; borderColor = '#22c55e'; icon = '⚙️'; statusText = '정상'; } // 상태별 마커 클래스 결정 let markerClass = 'equipment-marker'; if (!isHere) { markerClass += ' inactive'; } else if (eq.status === 'repair_needed' || eq.status === 'repair_external') { markerClass += ' repair'; } else if (eq.status === 'maintenance') { markerClass += ' maintenance'; } else if (isVisitor) { markerClass += ' moved'; } else { markerClass += ' active'; } const marker = document.createElement('div'); marker.className = markerClass; marker.style.cssText = ` left: ${x}%; top: ${y}%; width: ${width}%; height: ${height}%; `; // 이동/임시배치 표시 이모지 const badge = !isHere ? ' 📤' : isVisitor ? ' 🚚' : ''; // 마커 라벨 (설비 이름만 표시) marker.innerHTML = `${escapeHtml(eq.equipment_name)}${badge}`; marker.title = `${eq.equipment_name}\n상태: ${statusText}`; // 호버 시 정보 표시 marker.addEventListener('mouseenter', () => { marker.style.transform = 'scale(1.1)'; marker.style.zIndex = '50'; }); marker.addEventListener('mouseleave', () => { marker.style.transform = 'scale(1)'; marker.style.zIndex = '5'; }); // 클릭 시 상세 정보 표시 marker.addEventListener('click', (e) => { e.stopPropagation(); showEquipmentInfo(eq, isHere, isVisitor); }); container.appendChild(marker); }); } // 설비 정보 표시 function showEquipmentInfo(eq, isHere, isVisitor) { let statusText = '정상'; if (!isHere) { statusText = `다른 작업장으로 이동됨 (${eq.current_workplace_name || '외부'})`; } else if (eq.status === 'repair_needed') { statusText = '수리 필요'; } else if (eq.status === 'repair_external') { statusText = '외부 수리중'; } else if (eq.status === 'maintenance') { statusText = '점검중'; } else if (isVisitor) { statusText = '임시 배치 (원래 위치 아님)'; } alert(`📌 ${eq.equipment_name}\n\n` + `유형: ${eq.equipment_type || '-'}\n` + `관리번호: ${eq.equipment_code || '-'}\n` + `상태: ${statusText}\n` + (eq.notes ? `비고: ${eq.notes}` : '')); } // 구역 현황 목록 로드 async function loadZoneItems() { try { const response = await axios.get(`/patrol/workplaces/${workplaceId}/zone-items`); if (response.data.success) { zoneItems = response.data.data || []; renderZoneItemsList(); renderZoneItemsOnMap(); } } catch (error) { console.error('구역 현황 로드 실패:', error); zoneItems = []; renderZoneItemsList(); } } // 현황 목록 렌더링 function renderZoneItemsList() { const container = document.getElementById('zoneItemsList'); const typeLabels = { 'working': '작업중', 'temp_storage': '임시적치', 'moved_equipment': '이동설비', 'unreported': '미신고품', 'general': '일반', 'other': '기타' }; const warningLabels = { 'good': '양호', 'caution': '주의', 'needs_management': '관리필요' }; const projectTypeLabels = { 'project': '프로젝트', 'non_project': '비프로젝트', 'unknown': '미확인' }; if (!zoneItems.length) { container.innerHTML = `

등록된 현황

0개
등록된 현황이 없습니다.
위 [현황 등록] 버튼을 눌러 지도에서 범위를 선택하세요.
`; return; } container.innerHTML = `

등록된 현황

${zoneItems.length}개
${zoneItems.map(item => `
${escapeHtml(item.item_name)} ${item.photos && item.photos.length > 0 ? `📷${item.photos.length}` : ''}
${typeLabels[item.item_type] || item.item_type} ${item.project_type === 'project' && item.project_name ? `• ${escapeHtml(item.project_name)}` : ''} ${item.project_type === 'unknown' ? `• 미확인` : ''}
${warningLabels[item.warning_level] || '양호'}
`).join('')} `; } // 지도 위에 현황 마커 렌더링 function renderZoneItemsOnMap() { const container = document.getElementById('zoneMapContainer'); // 기존 마커 제거 container.querySelectorAll('.zone-item-marker').forEach(el => el.remove()); // 유형 라벨 const typeLabels = { 'working': '작업중', 'temp_storage': '임시적치', 'moved_equipment': '이동설비', 'unreported': '미신고품' }; // 마커 추가 zoneItems.forEach(item => { const marker = document.createElement('div'); const isSelected = selectedZoneItem === item.item_id; const warningClass = item.warning_level === 'needs_management' ? 'warning-high' : item.warning_level === 'caution' ? 'warning-mid' : ''; marker.className = `zone-item-marker ${isSelected ? 'selected' : ''} ${warningClass}`; marker.style.cssText = ` left: ${item.x_percent}%; top: ${item.y_percent}%; width: ${item.width_percent || 5}%; height: ${item.height_percent || 5}%; --marker-color: ${item.color || '#3b82f6'}; `; marker.dataset.itemId = item.item_id; // 클릭 시 바로 수정 모달 열기 marker.onclick = (e) => { e.stopPropagation(); editZoneItem(item.item_id); }; // 주의 수준 아이콘 const warningIcon = item.warning_level === 'needs_management' ? '⚠️ ' : item.warning_level === 'caution' ? '⚡ ' : ''; // 라벨 (이름 + 유형) const typeName = typeLabels[item.item_type] || item.item_type; marker.innerHTML = ` ${warningIcon}${escapeHtml(item.item_name)} ${typeName} `; marker.title = `${item.item_name}\n유형: ${typeName}\n클릭하여 수정`; container.appendChild(marker); }); } // 현황 선택 function selectZoneItem(itemId) { selectedZoneItem = selectedZoneItem === itemId ? null : itemId; renderZoneItemsList(); renderZoneItemsOnMap(); } // 현황 등록 시작/토글 function startAddItem() { const container = document.getElementById('zoneMapContainer'); const btn = document.getElementById('addItemBtn'); if (isAddingItem) { // 이미 등록 모드면 취소 cancelAddItem(); return; } isAddingItem = true; selectedZoneItem = null; container.classList.add('adding-item'); // 버튼 텍스트 변경 btn.innerHTML = '❌ 취소'; btn.classList.add('btn-danger'); btn.classList.remove('btn-primary'); // 하단 안내 바 (오버레이 대신) let guideBar = document.getElementById('addItemGuideBar'); if (!guideBar) { guideBar = document.createElement('div'); guideBar.id = 'addItemGuideBar'; guideBar.className = 'add-item-guide-bar'; container.appendChild(guideBar); } guideBar.innerHTML = ` 👆 클릭: 위치 지정 | 드래그: 영역 선택 `; guideBar.style.display = 'flex'; } // 현황 등록 취소 function cancelAddItem() { isAddingItem = false; const container = document.getElementById('zoneMapContainer'); container.classList.remove('adding-item'); // 안내 바 숨기기 const guideBar = document.getElementById('addItemGuideBar'); if (guideBar) guideBar.style.display = 'none'; // 선택 박스 정리 if (selectionBox) { selectionBox.remove(); selectionBox = null; } selectionStart = null; // 버튼 원복 const btn = document.getElementById('addItemBtn'); btn.innerHTML = '➕ 현황 등록'; btn.classList.remove('btn-danger'); btn.classList.add('btn-primary'); } // 마우스 이벤트 핸들러 function onMapMouseDown(e) { if (!isAddingItem) return; if (e.target.closest('.zone-item-marker, .equipment-marker')) return; // 기존 마커 클릭 무시 e.preventDefault(); const container = document.getElementById('zoneMapContainer'); const rect = container.getBoundingClientRect(); selectionStart = { x: e.clientX - rect.left, y: e.clientY - rect.top, xPercent: ((e.clientX - rect.left) / rect.width) * 100, yPercent: ((e.clientY - rect.top) / rect.height) * 100, time: Date.now() }; // 선택 박스 생성 selectionBox = document.createElement('div'); selectionBox.className = 'selection-box'; selectionBox.style.left = selectionStart.x + 'px'; selectionBox.style.top = selectionStart.y + 'px'; selectionBox.style.width = '0px'; selectionBox.style.height = '0px'; container.appendChild(selectionBox); // 크기 표시 라벨 const sizeLabel = document.createElement('div'); sizeLabel.className = 'selection-size-label'; sizeLabel.id = 'selectionSizeLabel'; selectionBox.appendChild(sizeLabel); } function onMapMouseMove(e) { if (!isAddingItem || !selectionStart || !selectionBox) return; const container = document.getElementById('zoneMapContainer'); const rect = container.getBoundingClientRect(); const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height)); const left = Math.min(selectionStart.x, currentX); const top = Math.min(selectionStart.y, currentY); const width = Math.abs(currentX - selectionStart.x); const height = Math.abs(currentY - selectionStart.y); selectionBox.style.left = left + 'px'; selectionBox.style.top = top + 'px'; selectionBox.style.width = width + 'px'; selectionBox.style.height = height + 'px'; // 크기 표시 업데이트 const widthPercent = ((width / rect.width) * 100).toFixed(1); const heightPercent = ((height / rect.height) * 100).toFixed(1); const sizeLabel = document.getElementById('selectionSizeLabel'); if (sizeLabel && width > 30) { sizeLabel.textContent = `${widthPercent}% × ${heightPercent}%`; sizeLabel.style.display = 'block'; } } function onMapMouseUp(e) { if (!isAddingItem || !selectionStart) return; const container = document.getElementById('zoneMapContainer'); const rect = container.getBoundingClientRect(); const endX = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const endY = Math.max(0, Math.min(e.clientY - rect.top, rect.height)); const width = Math.abs(endX - selectionStart.x); const height = Math.abs(endY - selectionStart.y); const elapsed = Date.now() - selectionStart.time; // 선택 박스 제거 if (selectionBox) { selectionBox.remove(); selectionBox = null; } // 빠른 클릭 (200ms 이하) 또는 작은 영역 (15px 이하) → 고정 크기 마커 if (elapsed < 200 || (width < 15 && height < 15)) { const xPercent = Math.max(0, selectionStart.xPercent - 2.5); // 중앙 정렬 const yPercent = Math.max(0, selectionStart.yPercent - 2.5); openZoneItemModal(xPercent, yPercent, 5, 5); } else { // 드래그로 선택한 영역 const xPercent = Math.min(selectionStart.xPercent, (endX / rect.width) * 100); const yPercent = Math.min(selectionStart.yPercent, (endY / rect.height) * 100); const widthPercent = (width / rect.width) * 100; const heightPercent = (height / rect.height) * 100; openZoneItemModal(xPercent, yPercent, widthPercent, heightPercent); } selectionStart = null; // 등록 모드 유지 (연속 등록 가능) } function onMapMouseLeave() { // 지도 영역 벗어나면 현재 선택만 취소 (모드는 유지) if (selectionBox && selectionStart) { selectionBox.remove(); selectionBox = null; selectionStart = null; } } // 터치 이벤트 (모바일) function onMapTouchStart(e) { if (!isAddingItem) return; if (e.target.closest('.zone-item-marker, .equipment-marker')) return; e.preventDefault(); const touch = e.touches[0]; onMapMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: e.target }); } function onMapTouchMove(e) { if (!isAddingItem || !selectionStart) return; e.preventDefault(); const touch = e.touches[0]; onMapMouseMove({ clientX: touch.clientX, clientY: touch.clientY }); } function onMapTouchEnd(e) { if (!isAddingItem || !selectionStart) return; const touch = e.changedTouches[0]; onMapMouseUp({ clientX: touch.clientX, clientY: touch.clientY }); } // 프로젝트 목록 캐시 let projectsCache = null; let selectedPhotos = []; // 프로젝트 목록 로드 async function loadProjects() { if (projectsCache) return projectsCache; try { const response = await axios.get('/projects?status=in_progress&limit=100'); if (response.data.success) { projectsCache = response.data.data || []; return projectsCache; } } catch (err) { console.error('프로젝트 로드 실패:', err); } return []; } // 프로젝트 타입 변경 function onProjectTypeChange(value) { const projectGroup = document.getElementById('projectSelectGroup'); if (value === 'project') { projectGroup.style.display = 'block'; populateProjectSelect(); } else { projectGroup.style.display = 'none'; } } // 프로젝트 셀렉트 채우기 async function populateProjectSelect() { const select = document.getElementById('zoneItemProject'); const projects = await loadProjects(); select.innerHTML = ''; projects.forEach(p => { select.innerHTML += ``; }); } // 커스텀 유형 추가 function addCustomType() { const customType = prompt('새로운 상태/유형을 입력하세요:'); if (!customType || !customType.trim()) return; const select = document.getElementById('zoneItemType'); const value = customType.trim().toLowerCase().replace(/\s+/g, '_'); // 중복 체크 if (Array.from(select.options).some(opt => opt.value === value)) { alert('이미 존재하는 유형입니다.'); return; } const option = document.createElement('option'); option.value = value; option.textContent = customType.trim(); select.appendChild(option); select.value = value; } // 사진 선택 function onPhotoSelected(event) { const files = Array.from(event.target.files); files.forEach(file => { if (!file.type.startsWith('image/')) return; if (selectedPhotos.length >= 5) { alert('사진은 최대 5장까지 등록할 수 있습니다.'); return; } const reader = new FileReader(); reader.onload = (e) => { selectedPhotos.push({ file: file, dataUrl: e.target.result }); renderPhotoPreview(); }; reader.readAsDataURL(file); }); event.target.value = ''; // 리셋 } // 사진 미리보기 렌더링 function renderPhotoPreview() { const container = document.getElementById('photoPreviewList'); container.innerHTML = selectedPhotos.map((photo, idx) => `
사진 ${idx + 1}
`).join(''); } // 사진 제거 function removePhoto(index) { selectedPhotos.splice(index, 1); renderPhotoPreview(); } // 현황 모달 열기 function openZoneItemModal(x, y, width, height, item = null) { const modal = document.getElementById('zoneItemModal'); const title = document.getElementById('zoneItemModalTitle'); const deleteBtn = document.getElementById('deleteZoneItemBtn'); // 사진 초기화 selectedPhotos = []; renderPhotoPreview(); if (item) { // 수정 모드 title.textContent = '현황 수정'; deleteBtn.style.display = 'inline-block'; document.getElementById('zoneItemId').value = item.item_id; document.getElementById('zoneItemX').value = item.x_percent; document.getElementById('zoneItemY').value = item.y_percent; document.getElementById('zoneItemWidth').value = item.width_percent || 5; document.getElementById('zoneItemHeight').value = item.height_percent || 5; document.getElementById('zoneItemName').value = item.item_name || ''; document.getElementById('zoneItemType').value = item.item_type || 'working'; document.getElementById('zoneItemWarning').value = item.warning_level || 'good'; document.getElementById('zoneItemDesc').value = item.description || ''; document.getElementById('zoneItemColor').value = item.color || '#3b82f6'; // 프로젝트 타입 const projectType = item.project_type || 'non_project'; document.querySelector(`input[name="zoneItemProjectType"][value="${projectType}"]`).checked = true; onProjectTypeChange(projectType); if (projectType === 'project' && item.project_id) { setTimeout(() => { document.getElementById('zoneItemProject').value = item.project_id; }, 100); } // 기존 사진 로드 (있으면) if (item.photos && item.photos.length > 0) { item.photos.forEach(photo => { selectedPhotos.push({ existing: true, photo_id: photo.photo_id, dataUrl: photo.photo_url }); }); renderPhotoPreview(); } } else { // 등록 모드 title.textContent = '현황 등록'; deleteBtn.style.display = 'none'; document.getElementById('zoneItemId').value = ''; document.getElementById('zoneItemX').value = x; document.getElementById('zoneItemY').value = y; document.getElementById('zoneItemWidth').value = width; document.getElementById('zoneItemHeight').value = height; document.getElementById('zoneItemForm').reset(); document.getElementById('zoneItemColor').value = '#3b82f6'; // 기본값 설정 document.querySelector('input[name="zoneItemProjectType"][value="non_project"]').checked = true; onProjectTypeChange('non_project'); } modal.style.display = 'flex'; } // 현황 수정 모달 열기 function editZoneItem(itemId) { const item = zoneItems.find(i => i.item_id === itemId); if (!item) return; openZoneItemModal(item.x_percent, item.y_percent, item.width_percent, item.height_percent, item); } // 모달 닫기 function closeZoneItemModal() { document.getElementById('zoneItemModal').style.display = 'none'; } // 색상 프리셋 선택 function setItemColor(color) { document.getElementById('zoneItemColor').value = color; } // 현황 저장 async function saveZoneItem() { const itemId = document.getElementById('zoneItemId').value; const projectType = document.querySelector('input[name="zoneItemProjectType"]:checked')?.value || 'non_project'; const projectId = projectType === 'project' ? document.getElementById('zoneItemProject').value : null; const itemData = { item_name: document.getElementById('zoneItemName').value.trim(), item_type: document.getElementById('zoneItemType').value, warning_level: document.getElementById('zoneItemWarning').value, project_type: projectType, project_id: projectId || null, description: document.getElementById('zoneItemDesc').value.trim(), color: document.getElementById('zoneItemColor').value, x_percent: parseFloat(document.getElementById('zoneItemX').value), y_percent: parseFloat(document.getElementById('zoneItemY').value), width_percent: parseFloat(document.getElementById('zoneItemWidth').value), height_percent: parseFloat(document.getElementById('zoneItemHeight').value) }; if (!itemData.item_name) { alert('명칭을 입력해주세요.'); return; } if (projectType === 'project' && !projectId) { alert('프로젝트를 선택해주세요.'); return; } try { let response; if (itemId) { // 수정 response = await axios.put(`/patrol/zone-items/${itemId}`, itemData); } else { // 등록 response = await axios.post(`/patrol/workplaces/${workplaceId}/zone-items`, itemData); } if (response.data.success) { const savedItemId = itemId || response.data.data?.item_id; // 사진 업로드 (새로 추가된 사진만) const newPhotos = selectedPhotos.filter(p => !p.existing && p.file); if (newPhotos.length > 0 && savedItemId) { await uploadZoneItemPhotos(savedItemId, newPhotos); } closeZoneItemModal(); await loadZoneItems(); alert(itemId ? '현황이 수정되었습니다.' : '현황이 등록되었습니다.'); } else { throw new Error(response.data.message || '저장 실패'); } } catch (error) { console.error('현황 저장 실패:', error); alert('저장에 실패했습니다: ' + (error.response?.data?.message || error.message)); } } // 사진 업로드 async function uploadZoneItemPhotos(itemId, photos) { for (const photo of photos) { try { const formData = new FormData(); formData.append('photo', photo.file); formData.append('item_id', itemId); await axios.post('/patrol/zone-items/photos', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); } catch (err) { console.error('사진 업로드 실패:', err); } } } // 현황 삭제 async function deleteZoneItem() { const itemId = document.getElementById('zoneItemId').value; if (!itemId) return; if (!confirm('이 현황을 삭제하시겠습니까?')) return; try { const response = await axios.delete(`/patrol/zone-items/${itemId}`); if (response.data.success) { closeZoneItemModal(); selectedZoneItem = null; await loadZoneItems(); alert('현황이 삭제되었습니다.'); } else { throw new Error(response.data.message || '삭제 실패'); } } catch (error) { console.error('현황 삭제 실패:', error); alert('삭제에 실패했습니다: ' + (error.response?.data?.message || error.message)); } } // 요약 카드 렌더링 function renderSummaryCards(summary) { const container = document.getElementById('summaryCards'); container.innerHTML = `
⚙️
${summary.equipmentCount}
등록 설비
🔧
${summary.pendingRepairs}
수리 요청
🚨
${summary.openIssues}
미해결 신고
🚶
${summary.todayVisitors}
금일 방문자
📋
${summary.todayTbmSessions}
금일 TBM
`; } // 안전신고/부적합 탭 렌더링 function renderIssuesTab() { const container = document.getElementById('issuesContent'); const { workIssues } = workplaceData; if (!workIssues.all.length) { container.innerHTML = `
최근 30일간 신고 내역이 없습니다.
`; return; } let html = ''; // 안전 신고 if (workIssues.safety.length > 0) { html += `

🛡️ 안전 신고 ${workIssues.safety.length}

${workIssues.safety.map(issue => renderIssueCard(issue, 'safety')).join('')}
`; } // 부적합 사항 if (workIssues.nonconformity.length > 0) { html += `

⚠️ 부적합 사항 ${workIssues.nonconformity.length}

${workIssues.nonconformity.map(issue => renderIssueCard(issue, 'nonconformity')).join('')}
`; } container.innerHTML = html; } // 신고 카드 렌더링 function renderIssueCard(issue, type) { const statusLabels = { 'pending': '대기', 'received': '접수', 'in_progress': '처리중', 'completed': '완료', 'closed': '종료' }; const severityLabels = { 'low': '경미', 'medium': '보통', 'high': '중요', 'critical': '긴급' }; return `
${escapeHtml(issue.title)} ${statusLabels[issue.status] || issue.status}
📁 ${escapeHtml(issue.category_name || '미분류')} ${issue.severity ? `${severityLabels[issue.severity]}` : ''} 📅 ${formatDateTime(issue.created_at)} 👤 ${escapeHtml(issue.reporter_name || '익명')}
${issue.description ? `
${escapeHtml(issue.description)}
` : ''}
`; } // 설비/수리 탭 렌더링 function renderEquipmentTab() { const container = document.getElementById('equipmentContent'); const { equipments, repairRequests } = workplaceData; let html = ''; // 수리 요청 if (repairRequests.length > 0) { html += `

🔧 수리 요청 ${repairRequests.length}

${repairRequests.map(req => renderRepairCard(req)).join('')}
`; } // 설비 현황 if (equipments.length > 0) { html += `

⚙️ 설비 현황 ${equipments.length}

${equipments.map(eq => renderEquipmentCard(eq)).join('')}
`; } if (!html) { html = `
⚙️
등록된 설비가 없습니다.
`; } container.innerHTML = html; } // 수리 요청 카드 렌더링 function renderRepairCard(req) { const priorityLabels = { 'emergency': '긴급', 'high': '높음', 'normal': '보통', 'low': '낮음' }; return `
${escapeHtml(req.equipment_name)} ${priorityLabels[req.priority] || req.priority}
📋 ${escapeHtml(req.repair_category)}
${req.description ? `
${escapeHtml(req.description)}
` : ''}
📅 ${formatDate(req.request_date)}
`; } // 설비 카드 렌더링 function renderEquipmentCard(eq) { const statusLabels = { 'active': '정상', 'inactive': '비활성', 'repair_needed': '수리필요', 'under_repair': '수리중', 'disposed': '폐기' }; return `
⚙️
${escapeHtml(eq.equipment_name)}
${escapeHtml(eq.equipment_type || '-')}
${statusLabels[eq.status] || eq.status}
`; } // 출입현황 탭 렌더링 function renderVisitsTab() { const container = document.getElementById('visitsContent'); const { visitRecords } = workplaceData; if (!visitRecords.length) { container.innerHTML = `
🚶
금일 승인된 방문자가 없습니다.
`; return; } container.innerHTML = `

🚶 금일 방문자 ${visitRecords.length}

${visitRecords.map(visit => renderVisitCard(visit)).join('')}
`; } // 방문자 카드 렌더링 function renderVisitCard(visit) { return `
${escapeHtml(visit.visitor_name)} ${visit.visitor_company ? `(${escapeHtml(visit.visitor_company)})` : ''}
${escapeHtml(visit.purpose_name || visit.visit_purpose || '')}
🕐 ${escapeHtml(visit.visit_time_from || '')} ~ ${escapeHtml(visit.visit_time_to || '')} ${visit.companion_count > 0 ? `👥 동행 ${visit.companion_count}명` : ''} ${visit.vehicle_number ? `🚗 ${escapeHtml(visit.vehicle_number)}` : ''}
`; } // TBM 탭 렌더링 function renderTbmTab() { const container = document.getElementById('tbmContent'); const { tbmSessions } = workplaceData; if (!tbmSessions.length) { container.innerHTML = `
📋
금일 TBM 세션이 없습니다.
`; return; } container.innerHTML = `

📋 금일 TBM ${tbmSessions.length}

${tbmSessions.map(tbm => renderTbmCard(tbm)).join('')}
`; } // TBM 카드 렌더링 function renderTbmCard(tbm) { const statusLabels = { 'draft': '작성중', 'in_progress': '진행중', 'completed': '완료' }; return `
${escapeHtml(tbm.task_name || tbm.work_type_name || '작업')} ${statusLabels[tbm.status] || tbm.status}
📍 ${escapeHtml(tbm.work_location || '-')} 👷 ${escapeHtml(tbm.leader_name || tbm.leader_worker_name || '-')} 👥 ${tbm.team_size || (tbm.team ? tbm.team.length : 0)}명
${tbm.work_content ? `
작업내용
${escapeHtml(tbm.work_content)}
` : ''} ${tbm.safety_measures ? `
안전조치
${escapeHtml(tbm.safety_measures)}
` : ''} ${tbm.team && tbm.team.length > 0 ? `
참석자 (${tbm.team.length}명)
${tbm.team.map(m => ` ${escapeHtml(m.worker_name)} `).join('')}
` : ''}
`; } // 순회점검 탭 렌더링 function renderPatrolTab() { const container = document.getElementById('patrolContent'); const { recentPatrol } = workplaceData; if (!recentPatrol || !recentPatrol.length) { container.innerHTML = `
🔍
최근 7일간 순회점검 기록이 없습니다.
`; return; } container.innerHTML = `

🔍 최근 순회점검 ${recentPatrol.length}

${recentPatrol.map(patrol => renderPatrolCard(patrol)).join('')}
`; } // 순회점검 카드 렌더링 function renderPatrolCard(patrol) { const timeLabels = { 'morning': '오전', 'afternoon': '오후' }; return `
${formatDate(patrol.patrol_date)} ${timeLabels[patrol.patrol_time] || patrol.patrol_time}
👤 ${escapeHtml(patrol.inspector_name || '-')} 📊 ${patrol.status === 'completed' ? '완료' : '진행중'}
${patrol.checked_count || 0}
점검 항목
${patrol.issue_count || 0}
주의/불량
${patrol.notes ? `
📝 ${escapeHtml(patrol.notes)}
` : ''}
`; } // 탭 전환 function switchTab(tabName) { // 탭 버튼 활성화 document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tabName); }); // 탭 콘텐츠 표시 document.querySelectorAll('.tab-content').forEach(content => { content.classList.toggle('active', content.id === `tab-${tabName}`); }); } // 뒤로 가기 function goBack() { // 이전 페이지로 이동, 없으면 일일순회점검 페이지로 if (document.referrer && document.referrer.includes(window.location.host)) { window.history.back(); } else { window.location.href = '/pages/inspection/daily-patrol.html'; } } // 유틸리티 함수 function formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', weekday: 'short' }); } function formatDateTime(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }