// 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; // 이미지 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() ]); } // 공장(대분류) 목록 로드 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; } } 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 && wp.y_percent); workplaces.forEach(wp => { if (wp.x_percent && wp.y_percent) { const marker = document.createElement('div'); marker.className = 'workplace-marker'; marker.style.left = `${parseFloat(wp.x_percent) || 0}%`; marker.style.top = `${parseFloat(wp.y_percent) || 0}%`; marker.textContent = wp.workplace_name; // textContent는 자동 이스케이프 marker.dataset.workplaceId = wp.workplace_id; marker.onclick = () => selectWorkplace(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); } }); // 좌표가 없는 작업장이 있으면 카드 목록도 표시 if (!hasMarkers || workplaces.some(wp => !wp.x_percent || !wp.y_percent)) { listContainer.style.display = 'grid'; renderWorkplaceCards(listContainer); } else { listContainer.style.display = 'none'; } } else { // 지도 없으면 카드 목록으로 표시 mapContainer.style.display = 'none'; listContainer.style.display = 'grid'; renderWorkplaceCards(listContainer); } } // 작업장 카드 렌더링 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); // 액션 버튼 표시 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(); } });