// 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); }); } // 페이지 초기화 async function initializePage() { // 오늘 날짜 설정 const today = new Date().toISOString().slice(0, 10); document.getElementById('patrolDate').value = today; // 시간대 버튼 이벤트 document.querySelectorAll('.patrol-time-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.patrol-time-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); }); // 데이터 로드 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; const select = document.getElementById('categorySelect'); select.innerHTML = '' + categories.map(c => ``).join(''); } } catch (error) { console.error('공장 목록 로드 실패:', error); } } // 물품 유형 로드 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 ? `
${morning.inspector_name || ''}
` : ''}
오후
${afternoon ? (afternoon.status === 'completed' ? '완료' : '진행중') : '미점검'}
${afternoon ? `
${afternoon.inspector_name || ''}
` : ''}
`; } // 순회점검 시작 async function startPatrol() { const patrolDate = document.getElementById('patrolDate').value; const patrolTime = document.querySelector('.patrol-time-btn.active')?.dataset.time; const categoryId = document.getElementById('categorySelect').value; if (!patrolDate || !patrolTime || !categoryId) { alert('점검 일자, 시간대, 공장을 모두 선택해주세요.'); return; } 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('patrolArea').style.display = 'block'; renderSessionInfo(); renderWorkplaceMap(); // 시작 버튼 비활성화 document.getElementById('startPatrolBtn').textContent = '점검 진행중...'; document.getElementById('startPatrolBtn').disabled = true; } } catch (error) { console.error('순회점검 시작 실패:', error); alert('순회점검을 시작할 수 없습니다.'); } } // 작업장 목록 로드 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 = `
점검일자 ${formatDate(currentSession.patrol_date)}
시간대 ${currentSession.patrol_time === 'morning' ? '오전' : '오후'}
공장 ${category?.category_name || ''}
${progress}%
`; } // 작업장 지도/목록 렌더링 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 = `${category.category_name} 지도`; mapContainer.style.display = 'block'; listContainer.style.display = 'none'; // 작업장 마커 추가 workplaces.forEach(wp => { if (wp.x_percent && wp.y_percent) { const marker = document.createElement('div'); marker.className = 'workplace-marker'; marker.style.left = `${wp.x_percent}%`; marker.style.top = `${wp.y_percent}%`; marker.textContent = wp.workplace_name; 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); } }); } else { // 지도 없으면 카드 목록으로 표시 mapContainer.style.display = 'none'; listContainer.style.display = 'grid'; listContainer.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); return `
${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 = `

${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]) => `
${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; return `
${isChecked ? '✓' : ''}
${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 = `${workplace.workplace_name}`; // 물품 마커 추가 workplaceItems.forEach(item => { if (item.x_percent && item.y_percent) { const marker = document.createElement('div'); marker.className = `item-marker ${item.item_type}`; marker.style.left = `${item.x_percent}%`; marker.style.top = `${item.y_percent}%`; marker.style.width = `${item.width_percent || 5}%`; marker.style.height = `${item.height_percent || 5}%`; marker.innerHTML = item.icon || getItemTypeIcon(item.item_type); marker.title = `${item.item_name || item.type_name} (${item.quantity}개)`; 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; container.innerHTML = itemTypes.map(t => `
${t.icon}
${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(); } });