// daily-patrol.js - 일일순회점검 페이지 JavaScript // 전역 상태 let currentSession = null; let categories = []; // 공장(대분류) 목록 let workplaces = []; // 작업장 목록 let checklistItems = []; // 체크리스트 항목 let checkRecords = {}; // 체크 기록 (workplace_id -> records) let selectedWorkplace = null; let itemTypes = []; // 물품 유형 let workplaceItems = []; // 현재 작업장 물품 let isItemEditMode = false; let workplaceDetail = null; // 작업장 상세 정보 // XSS 방지를 위한 HTML 이스케이프 함수 function escapeHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // 이미지 URL 헬퍼 함수 (정적 파일용 - /api 경로 제외) function getImageUrl(path) { if (!path) return ''; // 이미 http로 시작하면 그대로 반환 if (path.startsWith('http')) return path; // API_BASE_URL에서 /api 제거하여 정적 파일 서버 URL 생성 // /uploads 경로는 인증 없이 접근 가능한 정적 파일 경로 const staticUrl = window.API_BASE_URL.replace(/\/api$/, ''); return staticUrl + path; } // 페이지 초기화 document.addEventListener('DOMContentLoaded', async () => { await waitForAxiosConfig(); initializePage(); }); // axios 설정 대기 function waitForAxiosConfig() { return new Promise((resolve) => { const check = setInterval(() => { if (axios.defaults.baseURL) { clearInterval(check); resolve(); } }, 50); setTimeout(() => { clearInterval(check); resolve(); }, 5000); }); } // 자동 날짜/시간대 결정 function getAutoPatrolDateTime() { const now = new Date(); const patrolDate = now.toISOString().slice(0, 10); const hour = now.getHours(); // 오전(~12시), 오후(12시~) const patrolTime = hour < 12 ? 'morning' : 'afternoon'; return { patrolDate, patrolTime }; } // 페이지 초기화 async function initializePage() { // 데이터 로드 await Promise.all([ loadCategories(), loadItemTypes(), loadTodayStatus() ]); // 저장된 세션 상태 복원 (구역 상세에서 돌아온 경우) await restoreSessionState(); } // 세션 상태 저장 (페이지 이동 전) function saveSessionState() { if (currentSession) { const state = { session: currentSession, categoryId: currentSession.category_id, checkRecords: checkRecords, timestamp: Date.now() }; sessionStorage.setItem('patrolSessionState', JSON.stringify(state)); } } // 세션 상태 복원 async function restoreSessionState() { const savedState = sessionStorage.getItem('patrolSessionState'); if (!savedState) return; try { const state = JSON.parse(savedState); // 5분 이내의 상태만 복원 if (Date.now() - state.timestamp > 5 * 60 * 1000) { sessionStorage.removeItem('patrolSessionState'); return; } // categories가 비어있으면 복원 불가 if (!categories || categories.length === 0) { console.log('카테고리 목록이 없어 세션 복원 불가'); sessionStorage.removeItem('patrolSessionState'); return; } // 해당 카테고리가 존재하는지 확인 const category = categories.find(c => c.category_id == state.categoryId); if (!category) { console.log('저장된 카테고리를 찾을 수 없음:', state.categoryId); sessionStorage.removeItem('patrolSessionState'); return; } // 세션 복원 currentSession = state.session; checkRecords = state.checkRecords || {}; // 작업장 목록 로드 await loadWorkplaces(state.categoryId); // 체크리스트 항목 로드 await loadChecklistItems(state.categoryId); // UI 표시 document.getElementById('startPatrolBtn').style.display = 'none'; document.getElementById('factorySelectionArea').style.display = 'none'; document.getElementById('patrolArea').style.display = 'block'; renderSessionInfo(); renderWorkplaceMap(); console.log('세션 상태 복원 완료:', state.categoryId); // 복원 후 저장 상태 삭제 sessionStorage.removeItem('patrolSessionState'); } catch (error) { console.error('세션 상태 복원 실패:', error); sessionStorage.removeItem('patrolSessionState'); } } // 공장(대분류) 목록 로드 async function loadCategories() { try { const response = await axios.get('/workplaces/categories'); if (response.data.success) { categories = response.data.data; } } catch (error) { console.error('공장 목록 로드 실패:', error); } } // 공장 선택 화면 표시 function showFactorySelection() { const { patrolDate, patrolTime } = getAutoPatrolDateTime(); const timeLabel = patrolTime === 'morning' ? '오전' : '오후'; const dateObj = new Date(patrolDate); const dateLabel = dateObj.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', weekday: 'short' }); // 세션 정보 표시 document.getElementById('patrolSessionInfo').textContent = `${dateLabel} ${timeLabel} 순회점검`; // 공장 카드 렌더링 const container = document.getElementById('factoryCardsContainer'); container.innerHTML = categories.map(c => `
${c.layout_image ? `${escapeHtml(c.category_name)}` : '🏭'}
${escapeHtml(c.category_name)}
`).join(''); // 시작 버튼 숨기고 공장 선택 영역 표시 document.getElementById('startPatrolBtn').style.display = 'none'; document.getElementById('factorySelectionArea').style.display = 'block'; } // 공장 선택 후 점검 시작 async function selectFactory(categoryId) { const { patrolDate, patrolTime } = getAutoPatrolDateTime(); try { // 세션 생성 또는 조회 const response = await axios.post('/patrol/sessions', { patrol_date: patrolDate, patrol_time: patrolTime, category_id: categoryId }); if (response.data.success) { currentSession = response.data.data; currentSession.patrol_date = patrolDate; currentSession.patrol_time = patrolTime; currentSession.category_id = categoryId; // 작업장 목록 로드 await loadWorkplaces(categoryId); // 체크리스트 항목 로드 await loadChecklistItems(categoryId); // 공장 선택 영역 숨기고 점검 영역 표시 document.getElementById('factorySelectionArea').style.display = 'none'; document.getElementById('patrolArea').style.display = 'block'; renderSessionInfo(); renderWorkplaceMap(); } } catch (error) { console.error('순회점검 시작 실패:', error); alert('순회점검을 시작할 수 없습니다.'); } } // 물품 유형 로드 async function loadItemTypes() { try { const response = await axios.get('/patrol/item-types'); if (response.data.success) { itemTypes = response.data.data; renderItemTypesSelect(); renderItemsLegend(); } } catch (error) { console.error('물품 유형 로드 실패:', error); } } // 오늘 점검 현황 로드 async function loadTodayStatus() { try { const response = await axios.get('/patrol/today-status'); if (response.data.success) { renderTodayStatus(response.data.data); } } catch (error) { console.error('오늘 현황 로드 실패:', error); } } // 오늘 점검 현황 렌더링 function renderTodayStatus(statusList) { const container = document.getElementById('todayStatusSummary'); if (!statusList || statusList.length === 0) { container.innerHTML = `
오전
미점검
오후
미점검
`; return; } const morning = statusList.find(s => s.patrol_time === 'morning'); const afternoon = statusList.find(s => s.patrol_time === 'afternoon'); container.innerHTML = `
오전
${morning ? (morning.status === 'completed' ? '완료' : '진행중') : '미점검'}
${morning ? `
${escapeHtml(morning.inspector_name || '')}
` : ''}
오후
${afternoon ? (afternoon.status === 'completed' ? '완료' : '진행중') : '미점검'}
${afternoon ? `
${escapeHtml(afternoon.inspector_name || '')}
` : ''}
`; } // 작업장 목록 로드 async function loadWorkplaces(categoryId) { try { // 작업장 목록 로드 const response = await axios.get(`/workplaces?category_id=${categoryId}`); if (response.data.success) { workplaces = response.data.data; } // 지도 영역(좌표) 로드 try { const regionsResponse = await axios.get(`/workplaces/categories/${categoryId}/map-regions`); if (regionsResponse.data.success && regionsResponse.data.data) { // 작업장에 좌표 정보 병합 const regions = regionsResponse.data.data; workplaces = workplaces.map(wp => { const region = regions.find(r => r.workplace_id === wp.workplace_id); if (region) { // x_start, y_start를 x_percent, y_percent로 매핑 return { ...wp, x_percent: region.x_start, y_percent: region.y_start, x_end: region.x_end, y_end: region.y_end }; } return wp; }); } } catch (regError) { console.log('지도 영역 로드 스킵:', regError.message); } } catch (error) { console.error('작업장 목록 로드 실패:', error); } } // 체크리스트 항목 로드 async function loadChecklistItems(categoryId) { try { const response = await axios.get(`/patrol/checklist?category_id=${categoryId}`); if (response.data.success) { checklistItems = response.data.data.items; } } catch (error) { console.error('체크리스트 항목 로드 실패:', error); } } // 세션 정보 렌더링 function renderSessionInfo() { const container = document.getElementById('sessionInfo'); const category = categories.find(c => c.category_id == currentSession.category_id); const checkedCount = Object.values(checkRecords).flat().filter(r => r.is_checked).length; const totalCount = workplaces.length * checklistItems.length; const progress = totalCount > 0 ? Math.round(checkedCount / totalCount * 100) : 0; container.innerHTML = `
점검일자 ${escapeHtml(formatDate(currentSession.patrol_date))}
시간대 ${currentSession.patrol_time === 'morning' ? '오전' : '오후'}
공장 ${escapeHtml(category?.category_name || '')}
${parseInt(progress) || 0}%
`; } // 작업장 지도/목록 렌더링 function renderWorkplaceMap() { const mapContainer = document.getElementById('patrolMapContainer'); const listContainer = document.getElementById('workplaceListContainer'); const category = categories.find(c => c.category_id == currentSession.category_id); // 지도 이미지가 있으면 지도 표시 if (category?.layout_image) { mapContainer.innerHTML = `${escapeHtml(category.category_name)} 지도`; mapContainer.style.display = 'block'; // 좌표가 있는 작업장만 마커 추가 const hasMarkers = workplaces.some(wp => wp.x_percent !== undefined && wp.y_percent !== undefined); // 마커 위치 정보를 먼저 계산 const markerData = workplaces .filter(wp => wp.x_percent !== undefined && wp.y_percent !== undefined) .map(wp => { let centerX = parseFloat(wp.x_percent) || 0; let centerY = parseFloat(wp.y_percent) || 0; if (wp.x_end && wp.y_end) { centerX = (parseFloat(wp.x_percent) + parseFloat(wp.x_end)) / 2; centerY = (parseFloat(wp.y_percent) + parseFloat(wp.y_end)) / 2; } return { wp, centerX, centerY }; }); // y좌표 기준 정렬 (아래에 있을수록 나중에 추가 = 위에 표시) markerData.sort((a, b) => a.centerY - b.centerY); markerData.forEach((data, index) => { const { wp, centerX, centerY } = data; const marker = document.createElement('div'); marker.className = 'workplace-marker'; // 밀집도 체크 - 근처에 다른 마커가 있으면 compact 클래스 추가 const nearbyMarkers = markerData.filter(other => other !== data && Math.abs(other.centerX - centerX) < 12 && Math.abs(other.centerY - centerY) < 12 ); if (nearbyMarkers.length > 0) { marker.classList.add('compact'); } marker.style.left = `${centerX}%`; marker.style.top = `${centerY}%`; // y좌표가 클수록 (아래쪽일수록) z-index가 높아서 위에 표시 marker.style.zIndex = Math.floor(centerY) + 10; marker.textContent = wp.workplace_name; marker.dataset.workplaceId = wp.workplace_id; marker.onclick = () => goToZoneDetail(wp.workplace_id); // 점검 상태에 따른 스타일 const records = checkRecords[wp.workplace_id]; if (records && records.some(r => r.is_checked)) { marker.classList.add(records.every(r => r.is_checked) ? 'completed' : 'in-progress'); } mapContainer.appendChild(marker); }); // 좌표가 없는 작업장이 있으면 카드 목록도 표시 const hasWorkplacesWithoutCoords = workplaces.some(wp => wp.x_percent === undefined || wp.y_percent === undefined); if (!hasMarkers || hasWorkplacesWithoutCoords) { listContainer.style.display = 'grid'; renderWorkplaceCards(listContainer); } else { listContainer.style.display = 'none'; } } else { // 지도 없으면 카드 목록으로 표시 mapContainer.style.display = 'none'; listContainer.style.display = 'grid'; renderWorkplaceCards(listContainer); } } // 구역 상세 페이지로 이동 function goToZoneDetail(workplaceId) { // 현재 세션 상태 저장 saveSessionState(); window.location.href = `/pages/inspection/zone-detail.html?id=${workplaceId}`; } // 작업장 카드 렌더링 function renderWorkplaceCards(container) { container.innerHTML = workplaces.map(wp => { const records = checkRecords[wp.workplace_id]; const isCompleted = records && records.length > 0 && records.every(r => r.is_checked); const isInProgress = records && records.some(r => r.is_checked); const workplaceId = parseInt(wp.workplace_id) || 0; return `
${escapeHtml(wp.workplace_name)}
${isCompleted ? '점검완료' : (isInProgress ? '점검중' : '미점검')}
`; }).join(''); } // 작업장 선택 async function selectWorkplace(workplaceId) { selectedWorkplace = workplaces.find(w => w.workplace_id === workplaceId); // 마커/카드 선택 상태 업데이트 document.querySelectorAll('.workplace-marker, .workplace-card').forEach(el => { el.classList.remove('selected'); if (el.dataset.workplaceId == workplaceId) { el.classList.add('selected'); } }); // 기존 체크 기록 로드 if (!checkRecords[workplaceId]) { try { const response = await axios.get(`/patrol/sessions/${currentSession.session_id}/records?workplace_id=${workplaceId}`); if (response.data.success) { checkRecords[workplaceId] = response.data.data; } } catch (error) { console.error('체크 기록 로드 실패:', error); checkRecords[workplaceId] = []; } } // 체크리스트 렌더링 renderChecklist(workplaceId); // 물품 현황 로드 및 표시 await loadWorkplaceItems(workplaceId); // 작업장 상세 정보 로드 (신고, TBM, 출입 등) await loadWorkplaceDetail(workplaceId); // 액션 버튼 표시 document.getElementById('checklistActions').style.display = 'flex'; } // 체크리스트 렌더링 function renderChecklist(workplaceId) { const header = document.getElementById('checklistHeader'); const content = document.getElementById('checklistContent'); const workplace = workplaces.find(w => w.workplace_id === workplaceId); header.innerHTML = `

${escapeHtml(workplace?.workplace_name || '')} 체크리스트

각 항목을 점검하고 체크해주세요

`; // 카테고리별 그룹화 const grouped = {}; checklistItems.forEach(item => { if (!grouped[item.check_category]) { grouped[item.check_category] = []; } grouped[item.check_category].push(item); }); const records = checkRecords[workplaceId] || []; content.innerHTML = Object.entries(grouped).map(([category, items]) => `
${escapeHtml(getCategoryName(category))}
${items.map(item => { const record = records.find(r => r.check_item_id === item.item_id); const isChecked = record?.is_checked; const checkResult = record?.check_result; const itemId = parseInt(item.item_id) || 0; const wpId = parseInt(workplaceId) || 0; return `
${isChecked ? '✓' : ''}
${escapeHtml(item.check_item)} ${item.is_required ? '*' : ''}
${isChecked ? `
` : ''}
`; }).join('')}
`).join(''); } // 카테고리명 변환 function getCategoryName(code) { const names = { 'SAFETY': '안전', 'ORGANIZATION': '정리정돈', 'EQUIPMENT': '설비', 'ENVIRONMENT': '환경' }; return names[code] || code; } // 체크 항목 토글 function toggleCheckItem(workplaceId, itemId) { if (!checkRecords[workplaceId]) { checkRecords[workplaceId] = []; } const records = checkRecords[workplaceId]; const existingIndex = records.findIndex(r => r.check_item_id === itemId); if (existingIndex >= 0) { records[existingIndex].is_checked = !records[existingIndex].is_checked; if (!records[existingIndex].is_checked) { records[existingIndex].check_result = null; } } else { records.push({ check_item_id: itemId, is_checked: true, check_result: 'good', note: null }); } renderChecklist(workplaceId); renderWorkplaceMap(); renderSessionInfo(); } // 체크 결과 설정 function setCheckResult(workplaceId, itemId, result) { const records = checkRecords[workplaceId]; const record = records.find(r => r.check_item_id === itemId); if (record) { record.check_result = result; renderChecklist(workplaceId); } } // 임시 저장 async function saveChecklistDraft() { if (!selectedWorkplace) return; try { const records = checkRecords[selectedWorkplace.workplace_id] || []; await axios.post(`/patrol/sessions/${currentSession.session_id}/records/batch`, { workplace_id: selectedWorkplace.workplace_id, records: records }); alert('임시 저장되었습니다.'); } catch (error) { console.error('임시 저장 실패:', error); alert('저장에 실패했습니다.'); } } // 저장 후 다음 async function saveChecklist() { if (!selectedWorkplace) return; try { const records = checkRecords[selectedWorkplace.workplace_id] || []; await axios.post(`/patrol/sessions/${currentSession.session_id}/records/batch`, { workplace_id: selectedWorkplace.workplace_id, records: records }); // 다음 미점검 작업장으로 이동 const currentIndex = workplaces.findIndex(w => w.workplace_id === selectedWorkplace.workplace_id); const nextWorkplace = workplaces.slice(currentIndex + 1).find(w => { const records = checkRecords[w.workplace_id]; return !records || records.length === 0 || !records.every(r => r.is_checked); }); if (nextWorkplace) { selectWorkplace(nextWorkplace.workplace_id); } else { alert('모든 작업장 점검이 완료되었습니다!'); } } catch (error) { console.error('저장 실패:', error); alert('저장에 실패했습니다.'); } } // 순회점검 완료 async function completePatrol() { if (!currentSession) return; // 미점검 작업장 확인 const uncheckedCount = workplaces.filter(w => { const records = checkRecords[w.workplace_id]; return !records || records.length === 0; }).length; if (uncheckedCount > 0) { if (!confirm(`아직 ${uncheckedCount}개 작업장이 미점검 상태입니다. 그래도 완료하시겠습니까?`)) { return; } } try { const notes = document.getElementById('patrolNotes').value; if (notes) { await axios.patch(`/patrol/sessions/${currentSession.session_id}/notes`, { notes }); } await axios.patch(`/patrol/sessions/${currentSession.session_id}/complete`); alert('순회점검이 완료되었습니다.'); location.reload(); } catch (error) { console.error('순회점검 완료 실패:', error); alert('순회점검 완료에 실패했습니다.'); } } // ==================== 물품 현황 ==================== // 작업장 물품 로드 async function loadWorkplaceItems(workplaceId) { try { const response = await axios.get(`/patrol/workplaces/${workplaceId}/items`); if (response.data.success) { workplaceItems = response.data.data; renderItemsSection(workplaceId); } } catch (error) { console.error('물품 로드 실패:', error); workplaceItems = []; } } // 물품 섹션 렌더링 function renderItemsSection(workplaceId) { const section = document.getElementById('itemsSection'); const workplace = workplaces.find(w => w.workplace_id === workplaceId); const container = document.getElementById('itemsMapContainer'); document.getElementById('selectedWorkplaceName').textContent = workplace?.workplace_name || ''; // 작업장 레이아웃 이미지가 있으면 표시 if (workplace?.layout_image) { container.innerHTML = `${escapeHtml(workplace.workplace_name)}`; // 물품 마커 추가 workplaceItems.forEach(item => { if (item.x_percent && item.y_percent) { const marker = document.createElement('div'); // item_type은 화이트리스트로 검증 const safeItemType = ['container', 'plate', 'material', 'tool', 'other'].includes(item.item_type) ? item.item_type : 'other'; marker.className = `item-marker ${safeItemType}`; marker.style.left = `${parseFloat(item.x_percent) || 0}%`; marker.style.top = `${parseFloat(item.y_percent) || 0}%`; marker.style.width = `${parseFloat(item.width_percent) || 5}%`; marker.style.height = `${parseFloat(item.height_percent) || 5}%`; marker.textContent = item.icon || getItemTypeIcon(item.item_type); // textContent 사용 marker.title = `${item.item_name || item.type_name} (${parseInt(item.quantity) || 0}개)`; marker.dataset.itemId = item.item_id; marker.onclick = () => openItemModal(item); container.appendChild(marker); } }); } else { container.innerHTML = '

작업장 레이아웃 이미지가 없습니다.

'; } section.style.display = 'block'; } // 물품 유형 아이콘 function getItemTypeIcon(typeCode) { const icons = { 'container': '📦', 'plate': '🔲', 'material': '🧱', 'tool': '🔧', 'other': '📍' }; return icons[typeCode] || '📍'; } // 물품 유형 셀렉트 렌더링 function renderItemTypesSelect() { const select = document.getElementById('itemType'); if (!select) return; select.innerHTML = itemTypes.map(t => `` ).join(''); } // 물품 범례 렌더링 function renderItemsLegend() { const container = document.getElementById('itemsLegend'); if (!container) return; // 색상 값 검증 (hex color만 허용) const isValidColor = (color) => /^#[0-9A-Fa-f]{3,6}$/.test(color); container.innerHTML = itemTypes.map(t => { const safeColor = isValidColor(t.color) ? t.color : '#888888'; return `
${escapeHtml(t.icon)}
${escapeHtml(t.type_name)}
`; }).join(''); } // 편집 모드 토글 function toggleItemEditMode() { isItemEditMode = !isItemEditMode; document.getElementById('itemEditModeText').textContent = isItemEditMode ? '편집모드 종료' : '편집모드'; if (isItemEditMode) { // 지도 클릭으로 물품 추가 const container = document.getElementById('itemsMapContainer'); container.style.cursor = 'crosshair'; container.onclick = (e) => { if (e.target === container || e.target.tagName === 'IMG') { const rect = container.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width * 100).toFixed(2); const y = ((e.clientY - rect.top) / rect.height * 100).toFixed(2); openItemModal(null, x, y); } }; } else { const container = document.getElementById('itemsMapContainer'); container.style.cursor = 'default'; container.onclick = null; } } // 물품 모달 열기 function openItemModal(item = null, x = null, y = null) { const modal = document.getElementById('itemModal'); const title = document.getElementById('itemModalTitle'); const deleteBtn = document.getElementById('deleteItemBtn'); if (item) { title.textContent = '물품 수정'; document.getElementById('itemId').value = item.item_id; document.getElementById('itemType').value = item.item_type; document.getElementById('itemName').value = item.item_name || ''; document.getElementById('itemQuantity').value = item.quantity || 1; deleteBtn.style.display = 'inline-block'; } else { title.textContent = '물품 추가'; document.getElementById('itemForm').reset(); document.getElementById('itemId').value = ''; document.getElementById('itemId').dataset.x = x; document.getElementById('itemId').dataset.y = y; deleteBtn.style.display = 'none'; } modal.style.display = 'flex'; } // 물품 모달 닫기 function closeItemModal() { document.getElementById('itemModal').style.display = 'none'; } // 물품 저장 async function saveItem() { if (!selectedWorkplace) return; const itemId = document.getElementById('itemId').value; const data = { item_type: document.getElementById('itemType').value, item_name: document.getElementById('itemName').value, quantity: parseInt(document.getElementById('itemQuantity').value) || 1, patrol_session_id: currentSession?.session_id }; // 새 물품일 경우 위치 추가 if (!itemId) { data.x_percent = parseFloat(document.getElementById('itemId').dataset.x); data.y_percent = parseFloat(document.getElementById('itemId').dataset.y); data.width_percent = 5; data.height_percent = 5; } try { if (itemId) { await axios.put(`/patrol/items/${itemId}`, data); } else { await axios.post(`/patrol/workplaces/${selectedWorkplace.workplace_id}/items`, data); } closeItemModal(); await loadWorkplaceItems(selectedWorkplace.workplace_id); } catch (error) { console.error('물품 저장 실패:', error); alert('물품 저장에 실패했습니다.'); } } // 물품 삭제 async function deleteItem() { const itemId = document.getElementById('itemId').value; if (!itemId) return; if (!confirm('이 물품을 삭제하시겠습니까?')) return; try { await axios.delete(`/patrol/items/${itemId}`); closeItemModal(); await loadWorkplaceItems(selectedWorkplace.workplace_id); } catch (error) { console.error('물품 삭제 실패:', error); alert('물품 삭제에 실패했습니다.'); } } // 유틸리티 함수 function formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' }); } // ESC 키로 모달 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeItemModal(); closeWorkplaceDetailPanel(); } }); // ==================== 작업장 상세 정보 패널 ==================== // 작업장 상세 정보 로드 async function loadWorkplaceDetail(workplaceId) { try { const today = new Date().toISOString().slice(0, 10); const response = await axios.get(`/patrol/workplaces/${workplaceId}/detail?date=${today}`); if (response.data.success) { workplaceDetail = response.data.data; renderWorkplaceDetailPanel(); } } catch (error) { console.error('작업장 상세 정보 로드 실패:', error); // 에러 발생 시에도 기본 패널 표시 workplaceDetail = null; renderWorkplaceDetailPanel(); } } // 상세 정보 패널 렌더링 function renderWorkplaceDetailPanel() { let panel = document.getElementById('workplaceDetailPanel'); if (!panel) { panel = document.createElement('div'); panel.id = 'workplaceDetailPanel'; panel.className = 'workplace-detail-panel'; document.body.appendChild(panel); } if (!workplaceDetail || !selectedWorkplace) { panel.style.display = 'none'; return; } const { workplace, equipments, repairRequests, workIssues, visitRecords, tbmSessions, recentPatrol, summary } = workplaceDetail; panel.innerHTML = `

${escapeHtml(workplace.workplace_name)}

${escapeHtml(workplace.category_name || '')}
${summary.equipmentCount} 설비
${summary.pendingRepairs} 수리요청
${summary.openIssues} 미해결 신고
${summary.todayVisitors} 금일 방문자
${summary.todayTbmSessions} 금일 TBM
${renderIssuesTab(workIssues)}
${renderEquipmentTab(equipments, repairRequests)}
${renderVisitsTab(visitRecords)}
${renderTbmTab(tbmSessions)}
`; panel.style.display = 'flex'; } // 신고/부적합 탭 렌더링 function renderIssuesTab(workIssues) { if (!workIssues.all.length) { return `
최근 30일간 신고 내역이 없습니다.
`; } const safetyIssues = workIssues.safety; const nonconformityIssues = workIssues.nonconformity; let html = ''; if (safetyIssues.length > 0) { html += `

🛡️ 안전 신고 (${safetyIssues.length})

${safetyIssues.map(issue => renderIssueItem(issue)).join('')}
`; } if (nonconformityIssues.length > 0) { html += `

⚠️ 부적합 사항 (${nonconformityIssues.length})

${nonconformityIssues.map(issue => renderIssueItem(issue)).join('')}
`; } return html || `
신고 내역이 없습니다.
`; } // 신고 항목 렌더링 function renderIssueItem(issue) { const statusColors = { 'pending': 'pending', 'received': 'info', 'in_progress': 'warning', 'completed': 'success', 'closed': 'muted' }; 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 || '')} ${severityLabels[issue.severity] || ''} ${formatDateTime(issue.created_at)}
${issue.description ? `
${escapeHtml(issue.description).slice(0, 100)}${issue.description.length > 100 ? '...' : ''}
` : ''}
신고자: ${escapeHtml(issue.reporter_name || '익명')}
`; } // 설비 탭 렌더링 function renderEquipmentTab(equipments, repairRequests) { let html = ''; // 수리 요청 먼저 표시 if (repairRequests.length > 0) { html += `

🔧 수리 요청 (${repairRequests.length})

${repairRequests.map(req => `
${escapeHtml(req.equipment_name)} (${escapeHtml(req.equipment_code)}) ${getPriorityLabel(req.priority)}
${escapeHtml(req.repair_category)}
${escapeHtml(req.description || '')}
${formatDate(req.request_date)}
`).join('')}
`; } // 설비 목록 if (equipments.length > 0) { html += `

📦 설비 현황 (${equipments.length})

${equipments.map(eq => `
${escapeHtml(eq.equipment_name)} ${escapeHtml(eq.equipment_code)} ${getEquipmentStatusLabel(eq.status)}
`).join('')}
`; } else { html += `
등록된 설비가 없습니다.
`; } return html; } // 출입 탭 렌더링 function renderVisitsTab(visitRecords) { if (!visitRecords.length) { return `
금일 승인된 방문자가 없습니다.
`; } return `
${visitRecords.map(visit => `
${escapeHtml(visit.visitor_name)} ${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)}
` : ''}
`).join('')}
`; } // TBM 탭 렌더링 function renderTbmTab(tbmSessions) { if (!tbmSessions.length) { return `
금일 TBM 세션이 없습니다.
`; } return `
${tbmSessions.map(tbm => `
${escapeHtml(tbm.task_name || tbm.work_type_name || '작업')} ${getTbmStatusLabel(tbm.status)}
📍 ${escapeHtml(tbm.work_location || '')}
👷 ${escapeHtml(tbm.leader_name || tbm.leader_worker_name || '')}
${tbm.work_content ? `
작업내용: ${escapeHtml(tbm.work_content).slice(0, 80)}...
` : ''} ${tbm.team && tbm.team.length > 0 ? `
팀원 (${tbm.team.length}명): ${tbm.team.map(m => escapeHtml(m.worker_name)).join(', ')}
` : ''} ${tbm.safety_measures ? `
⚠️ ${escapeHtml(tbm.safety_measures).slice(0, 60)}...
` : ''}
`).join('')}
`; } // 탭 전환 function switchDetailTab(tabName) { // 탭 버튼 활성화 document.querySelectorAll('.detail-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); // 탭 콘텐츠 표시 document.querySelectorAll('.detail-tab-content').forEach(content => { content.classList.toggle('active', content.id === `tab-${tabName}`); }); } // 상세 패널 닫기 function closeWorkplaceDetailPanel() { const panel = document.getElementById('workplaceDetailPanel'); if (panel) { panel.style.display = 'none'; } workplaceDetail = null; } // 헬퍼 함수들 function getPriorityLabel(priority) { const labels = { 'emergency': '긴급', 'high': '높음', 'normal': '보통', 'low': '낮음' }; return labels[priority] || priority; } function getEquipmentStatusLabel(status) { const labels = { 'active': '정상', 'inactive': '비활성', 'repair_needed': '수리필요', 'under_repair': '수리중', 'disposed': '폐기' }; return labels[status] || status; } function getTbmStatusLabel(status) { const labels = { 'draft': '작성중', 'in_progress': '진행중', 'completed': '완료' }; return labels[status] || status; } 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' }); }