// 작업장 현황 JavaScript let selectedCategory = null; let workplaceData = []; let mapRegions = []; // 작업장 영역 데이터 let canvas = null; let ctx = null; let canvasImage = null; // 금일 TBM 작업자 데이터 let todayWorkers = []; // 금일 출입 신청 데이터 let todayVisitors = []; // ==================== 초기화 ==================== document.addEventListener('DOMContentLoaded', async () => { await loadCategories(); // 이벤트 리스너 document.getElementById('categorySelect').addEventListener('change', onCategoryChange); document.getElementById('refreshMapBtn').addEventListener('click', refreshMapData); // 기본값으로 제1공장 선택 await selectFirstCategory(); // 임시 이동된 설비 로드 await loadMovedEquipments(); }); // ==================== 카테고리 (공장) 로드 ==================== async function loadCategories() { try { const response = await window.apiCall('/workplaces/categories', 'GET'); if (response && response.success) { const categories = response.data || []; const select = document.getElementById('categorySelect'); categories.forEach(cat => { const option = document.createElement('option'); option.value = cat.category_id; option.textContent = cat.category_name; option.dataset.layoutImage = cat.layout_image; select.appendChild(option); }); } } catch (error) { console.error('카테고리 로드 오류:', error); } } /** * 첫 번째 카테고리 자동 선택 */ async function selectFirstCategory() { const select = document.getElementById('categorySelect'); if (select.options.length > 1) { // 첫 번째 옵션 선택 (인덱스 0은 "공장을 선택하세요") select.selectedIndex = 1; // 변경 이벤트 트리거 await onCategoryChange({ target: select }); } } // ==================== 공장 선택 ==================== async function onCategoryChange(e) { const categoryId = e.target.value; if (!categoryId) { document.getElementById('workplaceMapContainer').style.display = 'none'; document.getElementById('mapPlaceholder').style.display = 'flex'; return; } const selectedOption = e.target.options[e.target.selectedIndex]; const layoutImage = selectedOption.dataset.layoutImage; selectedCategory = { category_id: categoryId, category_name: selectedOption.textContent, layout_image: layoutImage }; // 지도 로드 await loadWorkplaceMap(); // 금일 작업 데이터 로드 await loadTodayData(); // 지도 렌더링 renderMap(); } // ==================== 작업장 지도 로드 ==================== async function loadWorkplaceMap() { try { // 작업장 데이터 로드 const response = await window.apiCall(`/workplaces?category_id=${selectedCategory.category_id}`, 'GET'); if (response && response.success) { workplaceData = response.data || []; } // 작업장 영역 데이터 로드 (map-regions API) const regionsResponse = await window.apiCall(`/workplaces/categories/${selectedCategory.category_id}/map-regions`, 'GET'); if (regionsResponse && regionsResponse.success) { mapRegions = regionsResponse.data || []; console.log('[지도] 로드된 영역:', mapRegions); } // 이미지 로드 await loadMapImage(); // 지도 컨테이너 표시 document.getElementById('mapPlaceholder').style.display = 'none'; document.getElementById('workplaceMapContainer').style.display = 'block'; } catch (error) { console.error('작업장 데이터 로드 오류:', error); } } async function loadMapImage() { return new Promise((resolve, reject) => { const img = new Image(); const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', ''); const fullImageUrl = selectedCategory.layout_image.startsWith('http') ? selectedCategory.layout_image : `${baseUrl}${selectedCategory.layout_image}`; img.onload = () => { canvasImage = img; // 캔버스 초기화 canvas = document.getElementById('workplaceMapCanvas'); canvas.width = img.width; canvas.height = img.height; ctx = canvas.getContext('2d'); // 클릭 이벤트 canvas.addEventListener('click', onMapClick); resolve(); }; img.onerror = () => { console.error('이미지 로드 실패:', fullImageUrl); reject(); }; img.src = fullImageUrl; }); } // ==================== 금일 데이터 로드 ==================== async function loadTodayData() { // 로컬 시간대 기준으로 오늘 날짜 구하기 (UTC가 아닌 한국 시간 기준) const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const today = `${year}-${month}-${day}`; console.log('[대시보드] 조회 날짜 (로컬):', today); // TBM 작업자 데이터 로드 await loadTodayWorkers(today); // 출입 신청 데이터 로드 await loadTodayVisitors(today); } async function loadTodayWorkers(date) { try { const response = await window.apiCall(`/tbm/sessions/date/${date}`, 'GET'); if (response && response.success) { const sessions = response.data || []; todayWorkers = []; // 각 세션의 작업 정보 추가 sessions.forEach(session => { if (session.workplace_id) { const memberCount = session.team_member_count || 0; const leaderCount = session.leader_id ? 1 : 0; const totalCount = memberCount + leaderCount; todayWorkers.push({ workplace_id: session.workplace_id, task_name: session.task_name || '작업', work_location: session.work_location || '', member_count: totalCount, project_name: session.project_name || '' }); console.log(`[TBM] 작업 추가: ${session.work_location || session.workplace_id} - ${session.task_name} (${totalCount}명)`); } }); console.log('로드된 작업자:', todayWorkers); } } catch (error) { console.error('TBM 작업자 데이터 로드 오류:', error); } } async function loadTodayVisitors(date) { try { // 날짜 형식 확인 (YYYY-MM-DD) const formattedDate = date.split('T')[0]; const response = await window.apiCall(`/workplace-visits/requests`, 'GET'); if (response && response.success) { const requests = response.data || []; // 금일 날짜와 승인된 요청 필터링 todayVisitors = requests.filter(req => { // UTC 변환 없이 로컬 날짜로 비교 const visitDateObj = new Date(req.visit_date); const visitYear = visitDateObj.getFullYear(); const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0'); const visitDay = String(visitDateObj.getDate()).padStart(2, '0'); const visitDate = `${visitYear}-${visitMonth}-${visitDay}`; return visitDate === formattedDate && (req.status === 'approved' || req.status === 'training_completed'); }).map(req => ({ workplace_id: req.workplace_id, visitor_company: req.visitor_company, visitor_count: req.visitor_count, visit_time: req.visit_time, purpose_name: req.purpose_name, status: req.status })); console.log('로드된 방문자:', todayVisitors); } } catch (error) { console.error('출입 신청 데이터 로드 오류:', error); } } // ==================== 지도 렌더링 ==================== function renderMap() { if (!canvas || !ctx || !canvasImage) return; // 이미지 그리기 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(canvasImage, 0, 0); // 모든 작업장 영역 표시 mapRegions.forEach(region => { // 해당 작업장의 작업자/방문자 인원 계산 const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id); const totalWorkerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0); const totalVisitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0); // 영역 그리기 drawWorkplaceRegion(region, totalWorkerCount, totalVisitorCount); }); } function drawWorkplaceRegion(region, workerCount, visitorCount) { // 사각형 좌표 변환 const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; const x2 = (region.x_end / 100) * canvas.width; const y2 = (region.y_end / 100) * canvas.height; const width = x2 - x1; const height = y2 - y1; const centerX = x1 + width / 2; const centerY = y1 + height / 2; // 색상 결정 let fillColor, strokeColor; const hasActivity = workerCount > 0 || visitorCount > 0; if (workerCount > 0 && visitorCount > 0) { // 둘 다 있음 - 초록색 fillColor = 'rgba(34, 197, 94, 0.3)'; strokeColor = 'rgb(34, 197, 94)'; } else if (workerCount > 0) { // 내부 작업자만 - 파란색 fillColor = 'rgba(59, 130, 246, 0.3)'; strokeColor = 'rgb(59, 130, 246)'; } else if (visitorCount > 0) { // 외부 방문자만 - 보라색 fillColor = 'rgba(168, 85, 247, 0.3)'; strokeColor = 'rgb(168, 85, 247)'; } else { // 인원 없음 - 회색 테두리만 fillColor = 'rgba(0, 0, 0, 0)'; // 투명 strokeColor = 'rgb(156, 163, 175)'; // 회색 } // 사각형 그리기 ctx.save(); ctx.fillStyle = fillColor; ctx.fillRect(x1, y1, width, height); ctx.strokeStyle = strokeColor; ctx.lineWidth = hasActivity ? 3 : 2; ctx.strokeRect(x1, y1, width, height); ctx.restore(); // 인원수 표시 (인원이 있을 때만) if (hasActivity) { ctx.save(); ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // 배경 원 ctx.beginPath(); ctx.arc(centerX, centerY, 20, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = strokeColor; ctx.lineWidth = 2; ctx.stroke(); // 텍스트 const totalCount = workerCount + visitorCount; ctx.fillStyle = strokeColor; ctx.fillText(totalCount.toString(), centerX, centerY); ctx.restore(); } else { // 인원이 없을 때는 작업장 이름만 표시 ctx.save(); ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgb(107, 114, 128)'; ctx.fillText(region.workplace_name, centerX, centerY); ctx.restore(); } } // ==================== 지도 클릭 ==================== function onMapClick(e) { const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width * canvas.width; const y = (e.clientY - rect.top) / rect.height * canvas.height; // 클릭한 위치의 작업장 영역 찾기 for (const region of mapRegions) { if (isPointInRegion(x, y, region)) { // 작업장 정보를 찾아서 모달 표시 const workplace = workplaceData.find(w => w.workplace_id === region.workplace_id); if (workplace) { showWorkplaceDetail({ ...workplace, ...region }); } else { // 작업장 정보가 없으면 region 데이터만 사용 showWorkplaceDetail(region); } break; } } } function isPointInRegion(x, y, region) { // 사각형 영역 내부 체크 const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; const x2 = (region.x_end / 100) * canvas.width; const y2 = (region.y_end / 100) * canvas.height; return x >= x1 && x <= x2 && y >= y1 && y <= y2; } // ==================== 작업장 상세 정보 모달 ==================== // 현재 선택된 작업장 정보 (모달용) let currentModalWorkplace = null; function showWorkplaceDetail(workplace) { currentModalWorkplace = workplace; const workers = todayWorkers.filter(w => w.workplace_id === workplace.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === workplace.workplace_id); // 모달 제목 document.getElementById('modalWorkplaceName').textContent = workplace.workplace_name; document.getElementById('modalWorkplaceDesc').textContent = `${selectedCategory.category_name} • ${workplace.description || ''}`; // 요약 카드 업데이트 const totalWorkers = workers.reduce((sum, w) => sum + (w.member_count || 0), 0); const totalVisitors = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0); document.getElementById('summaryWorkerCount').textContent = totalWorkers; document.getElementById('summaryVisitorCount').textContent = totalVisitors; document.getElementById('summaryTaskCount').textContent = workers.length; // 배지 업데이트 document.getElementById('workerCountBadge').textContent = totalWorkers; document.getElementById('visitorCountBadge').textContent = totalVisitors; // 현황 개요 탭 - 현재 작업 목록 renderCurrentTasks(workers); // 현황 개요 탭 - 설비 현황 loadEquipmentStatus(workplace.workplace_id); // 작업자 탭 renderWorkersTab(workers); // 방문자 탭 renderVisitorsTab(visitors); // 상세 지도 초기화 initDetailMap(workplace); // 첫 번째 탭 활성화 switchWorkplaceTab('overview'); // 모달 표시 document.getElementById('workplaceDetailModal').style.display = 'flex'; } // 현재 작업 목록 렌더링 function renderCurrentTasks(workers) { const container = document.getElementById('currentTasksList'); if (workers.length === 0) { container.innerHTML = '

현재 진행 중인 작업이 없습니다.

'; return; } let html = ''; workers.forEach(worker => { html += `

${worker.task_name}

${worker.work_location ? `📍 ${worker.work_location}` : ''} ${worker.project_name ? ` • 📁 ${worker.project_name}` : ''}

👷 ${worker.member_count}명
`; }); container.innerHTML = html; } // 설비 현황 로드 async function loadEquipmentStatus(workplaceId) { const container = document.getElementById('equipmentSummary'); try { const response = await window.apiCall(`/equipments?workplace_id=${workplaceId}`, 'GET'); if (response && response.success && response.data && response.data.length > 0) { const equipments = response.data; let html = ''; // 최대 4개만 표시 equipments.slice(0, 4).forEach(eq => { const statusClass = eq.status === 'active' ? 'normal' : eq.status === 'maintenance' ? 'warning' : 'error'; const statusText = eq.status === 'active' ? '정상' : eq.status === 'maintenance' ? '점검중' : '수리필요'; html += `
⚙️

${eq.equipment_name}

${statusText}

`; }); if (equipments.length > 4) { html += `
+${equipments.length - 4}개 더...
`; } container.innerHTML = html; } else { container.innerHTML = '

등록된 설비가 없습니다.

'; } } catch (error) { console.error('설비 현황 로드 오류:', error); container.innerHTML = '

설비 정보를 불러올 수 없습니다.

'; } } // 작업자 탭 렌더링 function renderWorkersTab(workers) { const container = document.getElementById('internalWorkersList'); if (workers.length === 0) { container.innerHTML = '

금일 작업 예정 인원이 없습니다.

'; return; } let html = ''; workers.forEach(worker => { html += `

${worker.task_name}

${worker.member_count}명
${worker.work_location ? `

📍 ${worker.work_location}

` : ''} ${worker.project_name ? `

📁 ${worker.project_name}

` : ''}
`; }); container.innerHTML = html; } // 방문자 탭 렌더링 function renderVisitorsTab(visitors) { const container = document.getElementById('externalVisitorsList'); if (visitors.length === 0) { container.innerHTML = '

금일 방문 예정 인원이 없습니다.

'; return; } let html = ''; visitors.forEach(visitor => { const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨'; html += `

${visitor.visitor_company}

${visitor.visitor_count}명 • ${statusText}

⏰ ${visitor.visit_time}

📋 ${visitor.purpose_name}

`; }); container.innerHTML = html; } // 상세 지도 초기화 async function initDetailMap(workplace) { const container = document.getElementById('detailMapContainer'); const legendContainer = document.getElementById('detailMapLegend'); // 작업장에 레이아웃 이미지가 있는지 확인 if (workplace.layout_image) { const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', ''); const imageUrl = workplace.layout_image.startsWith('http') ? workplace.layout_image : `${baseUrl}${workplace.layout_image}`; // 이미지를 먼저 로드하여 비율 계산 const img = new Image(); img.onload = async () => { // 이미지 래퍼를 생성하여 이미지와 마커가 같은 영역을 공유하도록 함 container.innerHTML = `
${workplace.workplace_name} 상세 지도
`; // 설비 마커 로드 await loadEquipmentMarkers(workplace.workplace_id); }; img.onerror = () => { container.innerHTML = `
🗺️

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

`; }; img.src = imageUrl; // 범례 표시 legendContainer.innerHTML = `
정상 가동
점검 중
수리 필요
비활성
`; } else { container.innerHTML = `
🗺️

상세 지도가 등록되지 않았습니다.

작업장 관리에서 레이아웃 이미지를 등록해주세요.

`; legendContainer.innerHTML = ''; } } // 설비 마커 로드 및 표시 async function loadEquipmentMarkers(workplaceId) { const markersLayer = document.getElementById('equipmentMarkersLayer'); if (!markersLayer) return; try { const response = await window.apiCall(`/equipments?workplace_id=${workplaceId}`, 'GET'); if (response && response.success && response.data && response.data.length > 0) { const equipments = response.data; let markersHtml = ''; equipments.forEach(eq => { // 위치 정보가 있는 설비만 마커 표시 if (eq.map_x_percent != null && eq.map_y_percent != null) { // 임시 이동된 설비는 별도 클래스 적용 let statusClass = ''; if (eq.is_temporarily_moved) { statusClass = 'moved'; } else { statusClass = eq.status === 'active' ? 'active' : eq.status === 'maintenance' ? 'maintenance' : eq.status === 'repair_needed' ? 'repair' : 'inactive'; } // 마커 크기 (기본값 또는 설정된 값) const width = eq.map_width_percent || 8; const height = eq.map_height_percent || 6; // 표시 이름: [코드] 이름 const displayName = `[${eq.equipment_code}] ${eq.equipment_name}`; const movedBadge = eq.is_temporarily_moved ? ' 🚚' : ''; markersHtml += `
${displayName}${movedBadge}
`; } }); if (markersHtml) { markersLayer.innerHTML = markersHtml; } else { markersLayer.innerHTML = '
위치가 등록된 설비가 없습니다.
'; } } else { markersLayer.innerHTML = '
등록된 설비가 없습니다.
'; } } catch (error) { console.error('설비 마커 로드 오류:', error); markersLayer.innerHTML = '
설비 정보를 불러올 수 없습니다.
'; } } // 설비 툴팁 표시 function showEquipmentTooltip(event, equipment) { event.stopPropagation(); // 기존 툴팁 제거 const existingTooltip = document.querySelector('.equipment-tooltip'); if (existingTooltip) { existingTooltip.remove(); } const statusText = equipment.status === 'active' ? '정상 가동' : equipment.status === 'maintenance' ? '점검 중' : equipment.status === 'repair_needed' ? '수리 필요' : '비활성'; const statusClass = equipment.status === 'active' ? 'active' : equipment.status === 'maintenance' ? 'maintenance' : equipment.status === 'repair_needed' ? 'repair' : 'inactive'; const tooltip = document.createElement('div'); tooltip.className = 'equipment-tooltip'; tooltip.innerHTML = `
${equipment.equipment_name} ${statusText}

코드: ${equipment.equipment_code}

${equipment.equipment_type ? `

유형: ${equipment.equipment_type}

` : ''} ${equipment.model_name ? `

모델: ${equipment.model_name}

` : ''} ${equipment.manufacturer ? `

제조사: ${equipment.manufacturer}

` : ''}
`; // 툴팁 위치 설정 const container = document.getElementById('detailMapContainer'); const rect = container.getBoundingClientRect(); tooltip.style.left = `${event.clientX - rect.left + 10}px`; tooltip.style.top = `${event.clientY - rect.top + 10}px`; container.appendChild(tooltip); // 외부 클릭 시 툴팁 닫기 setTimeout(() => { document.addEventListener('click', function closeTooltip(e) { if (!tooltip.contains(e.target)) { tooltip.remove(); document.removeEventListener('click', closeTooltip); } }); }, 100); } // 탭 전환 function switchWorkplaceTab(tabName) { // 모든 탭 비활성화 document.querySelectorAll('.workplace-tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelectorAll('.workplace-tab-content').forEach(content => { content.classList.remove('active'); }); // 선택한 탭 활성화 document.querySelector(`.workplace-tab[data-tab="${tabName}"]`).classList.add('active'); document.getElementById(`tab-${tabName}`).classList.add('active'); // 이동 설비 탭 선택 시 데이터 로드 if (tabName === 'moved-eq' && currentModalWorkplace) { loadWorkplaceMovedEquipments(currentModalWorkplace.workplace_id); } } // 순회점검 페이지로 이동 function openPatrolPage() { closeWorkplaceModal(); window.location.href = `/pages/inspection/daily-patrol.html?category=${selectedCategory.category_id}`; } function closeWorkplaceModal() { document.getElementById('workplaceDetailModal').style.display = 'none'; currentModalWorkplace = null; } // ==================== 새로고침 ==================== async function refreshMapData() { if (!selectedCategory) return; await loadTodayData(); renderMap(); } // 전역 함수로 노출 window.closeWorkplaceModal = closeWorkplaceModal; window.switchWorkplaceTab = switchWorkplaceTab; window.openPatrolPage = openPatrolPage; window.showEquipmentTooltip = showEquipmentTooltip; // ========================================== // 설비 상세 슬라이드 패널 // ========================================== let currentPanelEquipment = null; let panelFactories = []; let panelWorkplaces = []; let panelMovePosition = null; let panelRepairCategories = []; let panelRepairPhotoBases = []; const STATUS_LABELS = { active: '정상 가동', maintenance: '점검 중', repair_needed: '수리 필요', inactive: '비활성', external: '외부 반출', repair_external: '수리 외주' }; // 패널 열기 async function openEquipmentPanel(equipment) { currentPanelEquipment = equipment; // 패널 헤더 설정 document.getElementById('panelEquipmentTitle').textContent = `[${equipment.equipment_code}] ${equipment.equipment_name}`; const statusEl = document.getElementById('panelEquipmentStatus'); statusEl.textContent = STATUS_LABELS[equipment.status] || equipment.status; statusEl.className = `slide-panel-status ${equipment.status}`; // 기본 정보 렌더링 renderPanelInfo(equipment); // 패널 열기 document.getElementById('equipmentSlidePanel').classList.add('open'); // 데이터 로드 await Promise.all([ loadPanelPhotos(), loadPanelRepairHistory(), loadPanelExternalLogs(), loadPanelFactories(), loadPanelRepairCategories() ]); } // 패널 닫기 function closeEquipmentPanel() { document.getElementById('equipmentSlidePanel').classList.remove('open'); currentPanelEquipment = null; } // 기본 정보 렌더링 function renderPanelInfo(eq) { document.getElementById('panelEquipmentInfo').innerHTML = `
모델명 ${eq.model_name || '-'}
규격 ${eq.specifications || '-'}
제조사 ${eq.manufacturer || '-'}
구입처 ${eq.supplier || '-'}
설비유형 ${eq.equipment_type || '-'}
구입일 ${eq.installation_date ? formatPanelDate(eq.installation_date) : '-'}
`; } // 사진 로드 async function loadPanelPhotos() { if (!currentPanelEquipment) return; try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/photos`, 'GET'); if (response && response.success) { renderPanelPhotos(response.data); } } catch (error) { console.error('사진 로드 실패:', error); document.getElementById('panelPhotoGrid').innerHTML = '
사진을 불러올 수 없습니다
'; } } function renderPanelPhotos(photos) { const grid = document.getElementById('panelPhotoGrid'); if (!photos || photos.length === 0) { grid.innerHTML = '
등록된 사진이 없습니다
'; return; } grid.innerHTML = photos.map(photo => `
`).join(''); } // 사진 확대 보기 function viewPanelPhoto(url) { // 간단한 이미지 뷰어 const viewer = document.createElement('div'); viewer.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);display:flex;align-items:center;justify-content:center;z-index:3000;cursor:pointer;'; viewer.innerHTML = ``; viewer.onclick = () => viewer.remove(); document.body.appendChild(viewer); } // 사진 업로드 모달 function openPanelPhotoUpload() { document.getElementById('panelPhotoInput').value = ''; document.getElementById('panelPhotoDesc').value = ''; document.getElementById('panelPhotoPreview').innerHTML = ''; document.getElementById('panelPhotoModal').style.display = 'flex'; } function closePanelPhotoModal() { document.getElementById('panelPhotoModal').style.display = 'none'; } function previewPanelPhoto(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = e => { document.getElementById('panelPhotoPreview').innerHTML = ``; }; reader.readAsDataURL(file); } } async function uploadPanelPhoto() { const fileInput = document.getElementById('panelPhotoInput'); const description = document.getElementById('panelPhotoDesc').value; if (!fileInput.files[0]) { alert('사진을 선택하세요.'); return; } const reader = new FileReader(); reader.onload = async e => { try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/photos`, 'POST', { photo_base64: e.target.result, description: description }); if (response && response.success) { closePanelPhotoModal(); loadPanelPhotos(); } } catch (error) { console.error('업로드 실패:', error); alert('사진 업로드에 실패했습니다.'); } }; reader.readAsDataURL(fileInput.files[0]); } async function deletePanelPhoto(photoId) { if (!confirm('이 사진을 삭제하시겠습니까?')) return; try { await window.apiCall(`/equipments/photos/${photoId}`, 'DELETE'); loadPanelPhotos(); } catch (error) { console.error('삭제 실패:', error); alert('삭제에 실패했습니다.'); } } // 수리 이력 로드 async function loadPanelRepairHistory() { if (!currentPanelEquipment) return; try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/repair-history`, 'GET'); if (response && response.success) { renderPanelRepairHistory(response.data); } } catch (error) { console.error('수리 이력 로드 실패:', error); } } function renderPanelRepairHistory(history) { const container = document.getElementById('panelRepairHistory'); if (!history || history.length === 0) { container.innerHTML = '
수리 이력이 없습니다
'; return; } container.innerHTML = history.slice(0, 5).map(h => { const statusLabels = { reported: '신고됨', received: '접수', in_progress: '처리중', completed: '완료', closed: '종료' }; const statusLabel = statusLabels[h.status] || h.status; const statusClass = (h.status === 'completed' || h.status === 'closed') ? 'completed' : 'pending'; return `
${formatPanelDate(h.created_at)}
${h.item_name || '수리 요청'}
${truncateText(h.description, 30)}
${statusLabel}
`; }).join(''); } // 외부반출 이력 로드 async function loadPanelExternalLogs() { if (!currentPanelEquipment) return; try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/external-logs`, 'GET'); if (response && response.success) { renderPanelExternalLogs(response.data); } } catch (error) { console.error('외부반출 이력 로드 실패:', error); } } function renderPanelExternalLogs(logs) { const container = document.getElementById('panelExternalHistory'); if (!logs || logs.length === 0) { container.innerHTML = '
외부반출 이력이 없습니다
'; return; } container.innerHTML = logs.slice(0, 5).map(log => { const isReturned = !!log.actual_return_date; const statusClass = isReturned ? 'returned' : 'exported'; const statusLabel = isReturned ? '반입완료' : '반출중'; return `
${formatPanelDate(log.export_date)}
${log.destination || '외부'}
${truncateText(log.reason, 30)}
${!isReturned ? `` : `${statusLabel}` }
`; }).join(''); } // 공장/작업장 로드 (이동용) async function loadPanelFactories() { try { const response = await window.apiCall('/workplaces/categories', 'GET'); if (response && response.success) { panelFactories = response.data; } } catch (error) { console.error('공장 목록 로드 실패:', error); } } // 수리 카테고리 로드 async function loadPanelRepairCategories() { try { const response = await window.apiCall('/equipments/repair-categories', 'GET'); if (response && response.success) { panelRepairCategories = response.data; } } catch (error) { console.error('수리 항목 로드 실패:', error); } } // ==================== 임시이동 ==================== let selectedMoveFactory = null; let selectedMoveWorkplace = null; function openPanelMoveModal() { // 초기화 selectedMoveFactory = null; selectedMoveWorkplace = null; panelMovePosition = null; document.getElementById('panelMoveReason').value = ''; document.getElementById('panelMoveConfirmBtn').disabled = true; // Step 1 표시 document.getElementById('moveStep1').style.display = 'block'; document.getElementById('moveStep2').style.display = 'none'; document.getElementById('moveStep3').style.display = 'none'; // 공장 카드 렌더링 renderMoveFactoryGrid(); document.getElementById('panelMoveModal').style.display = 'flex'; } function closePanelMoveModal() { document.getElementById('panelMoveModal').style.display = 'none'; } function renderMoveFactoryGrid() { const grid = document.getElementById('moveFactoryGrid'); const icons = ['🏭', '🏢', '🏗️', '🏛️', '⚙️']; grid.innerHTML = panelFactories.map((f, i) => `
${icons[i % icons.length]}
${f.category_name}
`).join(''); // 이벤트 리스너 추가 grid.querySelectorAll('.move-factory-card').forEach(card => { card.onclick = () => { const catId = card.dataset.categoryId; const factory = panelFactories.find(f => f.category_id == catId); if (factory) { selectMoveFactory(factory); } }; }); } let panelMapRegions = []; // 이동 모달용 지도 영역 async function selectMoveFactory(factory) { selectedMoveFactory = { category_id: factory.category_id, category_name: factory.category_name, layout_image: factory.layout_image }; // 작업장 목록 + 지도 영역 로드 try { const [wpResponse, regionsResponse] = await Promise.all([ window.apiCall(`/workplaces?category_id=${factory.category_id}`, 'GET'), window.apiCall(`/workplaces/categories/${factory.category_id}/map-regions`, 'GET') ]); if (wpResponse && wpResponse.success) { panelWorkplaces = wpResponse.data; } if (regionsResponse && regionsResponse.success) { panelMapRegions = regionsResponse.data; console.log('[이동모달] 로드된 영역:', panelMapRegions); } else { console.log('[이동모달] 영역 로드 실패:', regionsResponse); } } catch (error) { console.error('작업장 로드 실패:', error); return; } // Step 2로 이동 document.getElementById('moveStep1').style.display = 'none'; document.getElementById('moveStep2').style.display = 'block'; document.getElementById('moveStep2Title').textContent = `${factory.category_name} - 작업장 선택`; // 레이아웃 지도 렌더링 renderMoveLayoutMap(factory.layout_image); } function renderMoveLayoutMap(layoutImage) { const container = document.getElementById('moveLayoutMapContainer'); const baseUrl = (window.API_BASE_URL || '').replace('/api', ''); console.log('[이동모달] 렌더링 - layoutImage:', layoutImage); console.log('[이동모달] 렌더링 - panelMapRegions:', panelMapRegions); if (!layoutImage) { container.innerHTML = `
레이아웃 지도가 없습니다.
`; return; } // 지도 영역 데이터로 클릭 가능한 영역 생성 (x_start, y_start, x_end, y_end 사용) let regionsHtml = panelMapRegions.map(region => { const left = region.x_start; const top = region.y_start; const width = region.x_end - region.x_start; const height = region.y_end - region.y_start; return `
${region.workplace_name}
`; }).join(''); container.innerHTML = ` 공장 레이아웃 ${regionsHtml} `; // 영역 클릭 이벤트 container.querySelectorAll('.move-wp-region').forEach(region => { region.onclick = () => { const wpId = parseInt(region.dataset.wpId); const wp = panelWorkplaces.find(w => w.workplace_id === wpId); if (wp) selectMoveWorkplace(wp); }; }); } function selectMoveWorkplace(workplace) { if (!workplace) return; console.log('[이동모달] 선택된 작업장:', workplace); console.log('[이동모달] layout_image:', workplace.layout_image); selectedMoveWorkplace = workplace; // 상세 지도가 없으면 위치 없이 바로 확인 가능하게 if (!workplace.layout_image) { panelMovePosition = null; // 위치 좌표 없음 document.getElementById('moveStep2').style.display = 'none'; document.getElementById('moveStep3').style.display = 'block'; document.getElementById('moveStep3Title').textContent = `${workplace.workplace_name}`; document.getElementById('moveDetailMapContainer').innerHTML = `
📍
${workplace.workplace_name}
이 작업장에는 상세 지도가 없습니다.
위치 좌표 없이 작업장만 기록됩니다.
`; document.getElementById('panelMoveConfirmBtn').disabled = false; return; } // Step 3으로 이동 document.getElementById('moveStep2').style.display = 'none'; document.getElementById('moveStep3').style.display = 'block'; document.getElementById('moveStep3Title').textContent = `${workplace.workplace_name} - 위치 선택`; // 상세 지도 렌더링 renderMoveDetailMap(); } async function renderMoveDetailMap() { const container = document.getElementById('moveDetailMapContainer'); const baseUrl = (window.API_BASE_URL || '').replace('/api', ''); const imageUrl = `${baseUrl}${selectedMoveWorkplace.layout_image}`; // 이동 대상 작업장의 기존 설비 로드 let existingEquipments = []; try { const response = await window.apiCall(`/equipments?workplace_id=${selectedMoveWorkplace.workplace_id}`, 'GET'); if (response && response.success) { existingEquipments = response.data || []; } } catch (error) { console.error('설비 로드 실패:', error); } // 이미지 로드 후 마커 배치 const img = new Image(); img.onload = () => { // 기존 설비 마커 HTML 생성 const markersHtml = existingEquipments.map(eq => { const isCurrentEquipment = eq.equipment_id === currentPanelEquipment.equipment_id; const markerClass = isCurrentEquipment ? 'move-eq-marker current-moving' : 'move-eq-marker existing'; const width = eq.map_width_percent || 6; const height = eq.map_height_percent || 4; return `
${eq.equipment_code || eq.equipment_name}
`; }).join(''); container.innerHTML = `
${markersHtml}
`; // 지도 클릭 이벤트 document.getElementById('moveMapWrapper').addEventListener('click', onMoveDetailMapClick); }; img.onerror = () => { container.innerHTML = `
지도를 불러올 수 없습니다.
`; }; img.src = imageUrl; } function onMoveDetailMapClick(event) { const wrapper = document.getElementById('moveMapWrapper'); const rect = wrapper.getBoundingClientRect(); const x = ((event.clientX - rect.left) / rect.width) * 100; const y = ((event.clientY - rect.top) / rect.height) * 100; // 현재 설비의 크기 사용 (없으면 기본값) const width = currentPanelEquipment.map_width_percent || 6; const height = currentPanelEquipment.map_height_percent || 4; panelMovePosition = { x, y, width, height }; // 타겟 마커 표시 const targetMarker = document.getElementById('moveTargetMarker'); targetMarker.style.left = x + '%'; targetMarker.style.top = y + '%'; targetMarker.style.width = width + '%'; targetMarker.style.height = height + '%'; targetMarker.style.display = 'flex'; targetMarker.innerHTML = `${currentPanelEquipment.equipment_code || currentPanelEquipment.equipment_name}`; document.getElementById('panelMoveConfirmBtn').disabled = false; } function moveBackToStep1() { document.getElementById('moveStep2').style.display = 'none'; document.getElementById('moveStep1').style.display = 'block'; selectedMoveFactory = null; } function moveBackToStep2() { document.getElementById('moveStep3').style.display = 'none'; document.getElementById('moveStep2').style.display = 'block'; selectedMoveWorkplace = null; panelMovePosition = null; document.getElementById('panelMoveConfirmBtn').disabled = true; } async function confirmPanelMove() { if (!selectedMoveWorkplace) { alert('작업장을 선택하세요.'); return; } // 상세 지도가 있는데 위치를 선택 안 한 경우 if (selectedMoveWorkplace.layout_image && !panelMovePosition) { alert('지도에서 위치를 클릭하세요.'); return; } const reason = document.getElementById('panelMoveReason').value; try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/move`, 'POST', { target_workplace_id: selectedMoveWorkplace.workplace_id, target_x_percent: panelMovePosition ? panelMovePosition.x.toFixed(2) : null, target_y_percent: panelMovePosition ? panelMovePosition.y.toFixed(2) : null, from_workplace_id: currentPanelEquipment.workplace_id, from_x_percent: currentPanelEquipment.map_x_percent, from_y_percent: currentPanelEquipment.map_y_percent, reason: reason }); if (response && response.success) { closePanelMoveModal(); closeEquipmentPanel(); alert('설비가 임시 이동되었습니다.'); // 설비 마커 새로고침 if (currentModalWorkplace && currentModalWorkplace.workplace_id) { loadEquipmentMarkers(currentModalWorkplace.workplace_id); } } } catch (error) { console.error('이동 실패:', error); alert('이동에 실패했습니다.'); } } // ==================== 수리신청 ==================== function openPanelRepairModal() { const select = document.getElementById('panelRepairItem'); select.innerHTML = ''; panelRepairCategories.forEach(item => { select.innerHTML += ``; }); document.getElementById('panelRepairDesc').value = ''; document.getElementById('panelRepairPhotoInput').value = ''; panelRepairPhotoBases = []; document.getElementById('panelRepairModal').style.display = 'flex'; } function closePanelRepairModal() { document.getElementById('panelRepairModal').style.display = 'none'; } async function submitPanelRepair() { const itemId = document.getElementById('panelRepairItem').value; const description = document.getElementById('panelRepairDesc').value; if (!description) { alert('수리 내용을 입력하세요.'); return; } // 사진 처리 const fileInput = document.getElementById('panelRepairPhotoInput'); const photos = []; if (fileInput.files.length > 0) { for (const file of fileInput.files) { const base64 = await readFileAsBase64(file); photos.push(base64); } } try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/repair-request`, 'POST', { item_id: itemId || null, description: description, photo_base64_list: photos, workplace_id: currentPanelEquipment.workplace_id }); if (response && response.success) { closePanelRepairModal(); loadPanelRepairHistory(); alert('수리 신청이 접수되었습니다.'); } } catch (error) { console.error('수리 신청 실패:', error); alert('수리 신청에 실패했습니다.'); } } function readFileAsBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } // ==================== 외부반출 ==================== function openPanelExportModal() { document.getElementById('panelIsRepairExport').checked = false; document.getElementById('panelExportDate').value = new Date().toISOString().slice(0, 10); document.getElementById('panelExpectedReturn').value = ''; document.getElementById('panelExportDest').value = ''; document.getElementById('panelExportReason').value = ''; document.getElementById('panelExportModal').style.display = 'flex'; } function closePanelExportModal() { document.getElementById('panelExportModal').style.display = 'none'; } async function submitPanelExport() { const exportDate = document.getElementById('panelExportDate').value; if (!exportDate) { alert('반출일을 입력하세요.'); return; } try { const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/export`, 'POST', { export_date: exportDate, expected_return_date: document.getElementById('panelExpectedReturn').value || null, destination: document.getElementById('panelExportDest').value, reason: document.getElementById('panelExportReason').value, is_repair: document.getElementById('panelIsRepairExport').checked }); if (response && response.success) { closePanelExportModal(); loadPanelExternalLogs(); alert('외부 반출이 등록되었습니다.'); } } catch (error) { console.error('반출 실패:', error); alert('반출 등록에 실패했습니다.'); } } // ==================== 반입 ==================== function openPanelReturnModal(logId) { document.getElementById('panelReturnLogId').value = logId; document.getElementById('panelReturnDate').value = new Date().toISOString().slice(0, 10); document.getElementById('panelReturnStatus').value = 'active'; document.getElementById('panelReturnModal').style.display = 'flex'; } function closePanelReturnModal() { document.getElementById('panelReturnModal').style.display = 'none'; } async function submitPanelReturn() { const logId = document.getElementById('panelReturnLogId').value; const returnDate = document.getElementById('panelReturnDate').value; if (!returnDate) { alert('반입일을 입력하세요.'); return; } try { const response = await window.apiCall(`/equipments/external-logs/${logId}/return`, 'POST', { return_date: returnDate, new_status: document.getElementById('panelReturnStatus').value }); if (response && response.success) { closePanelReturnModal(); loadPanelExternalLogs(); alert('반입 처리가 완료되었습니다.'); } } catch (error) { console.error('반입 실패:', error); alert('반입 처리에 실패했습니다.'); } } // ==================== 임시 이동 설비 목록 ==================== async function loadMovedEquipments() { const listEl = document.getElementById('movedEquipmentList'); const emptyEl = document.getElementById('noMovedEquipment'); if (!listEl) return; try { const response = await window.apiCall('/equipments/moved/list', 'GET'); if (response && response.success && response.data && response.data.length > 0) { const equipments = response.data; emptyEl.style.display = 'none'; listEl.style.display = 'grid'; listEl.innerHTML = equipments.map(eq => `
${eq.equipment_code} 임시이동
${eq.equipment_name}
원래 위치 ${eq.original_workplace_name || '-'}
현재 위치 ${eq.current_workplace_name || '-'}
이동일: ${formatPanelDate(eq.moved_at)}
`).join(''); } else { listEl.style.display = 'none'; emptyEl.style.display = 'block'; } } catch (error) { console.error('임시 이동 설비 로드 실패:', error); listEl.innerHTML = '
로드 실패
'; } } // 작업장별 이동 설비 로드 async function loadWorkplaceMovedEquipments(workplaceId) { const movedInList = document.getElementById('movedInEquipmentList'); const movedOutList = document.getElementById('movedOutEquipmentList'); const badge = document.getElementById('movedEqCountBadge'); try { const response = await window.apiCall('/equipments/moved/list', 'GET'); if (response && response.success && response.data) { const allMoved = response.data; // 이 작업장으로 이동해 온 설비 (current_workplace_id = workplaceId) const movedIn = allMoved.filter(eq => eq.current_workplace_id === workplaceId); // 이 작업장에서 다른 곳으로 이동한 설비 (workplace_id = workplaceId) const movedOut = allMoved.filter(eq => eq.workplace_id === workplaceId); // 배지 업데이트 const totalCount = movedIn.length + movedOut.length; if (totalCount > 0) { badge.textContent = totalCount; badge.style.display = 'inline-flex'; } else { badge.style.display = 'none'; } // 이동해 온 설비 렌더링 if (movedIn.length > 0) { movedInList.innerHTML = movedIn.map(eq => `
${eq.equipment_code} ${eq.equipment_name}
📤 ${eq.original_workplace_name || '알 수 없음'} 📥 여기
이동일: ${formatPanelDate(eq.moved_at)}
`).join(''); } else { movedInList.innerHTML = '

없음

'; } // 이동해 간 설비 렌더링 if (movedOut.length > 0) { movedOutList.innerHTML = movedOut.map(eq => `
${eq.equipment_code} ${eq.equipment_name}
📤 여기 📥 ${eq.current_workplace_name || '알 수 없음'}
이동일: ${formatPanelDate(eq.moved_at)}
`).join(''); } else { movedOutList.innerHTML = '

없음

'; } } } catch (error) { console.error('작업장 이동 설비 로드 실패:', error); movedInList.innerHTML = '

로드 실패

'; movedOutList.innerHTML = '

로드 실패

'; } } // 원위치 복귀 async function returnEquipmentToOriginal(equipmentId) { if (!confirm('이 설비를 원래 위치로 복귀시키겠습니까?')) return; try { const response = await window.apiCall(`/equipments/${equipmentId}/return`, 'POST'); if (response && response.success) { alert('설비가 원래 위치로 복귀되었습니다.'); loadMovedEquipments(); // 지도 새로고침 if (selectedCategory) { renderMap(); } } } catch (error) { console.error('복귀 실패:', error); alert('복귀에 실패했습니다.'); } } // 이동된 설비 상세 보기 function showMovedEquipmentDetail(equipmentId) { // TODO: 설비 상세 패널 열기 console.log('설비 상세:', equipmentId); } // ==================== 유틸리티 ==================== function formatPanelDate(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return `${date.getMonth() + 1}/${date.getDate()}`; } function truncateText(text, maxLen) { if (!text) return '-'; return text.length > maxLen ? text.slice(0, maxLen) + '...' : text; } // 전역 함수 노출 window.openEquipmentPanel = openEquipmentPanel; window.closeEquipmentPanel = closeEquipmentPanel; window.openPanelPhotoUpload = openPanelPhotoUpload; window.closePanelPhotoModal = closePanelPhotoModal; window.previewPanelPhoto = previewPanelPhoto; window.uploadPanelPhoto = uploadPanelPhoto; window.deletePanelPhoto = deletePanelPhoto; window.viewPanelPhoto = viewPanelPhoto; window.openPanelMoveModal = openPanelMoveModal; window.closePanelMoveModal = closePanelMoveModal; window.selectMoveFactory = selectMoveFactory; window.selectMoveWorkplace = selectMoveWorkplace; window.onMoveDetailMapClick = onMoveDetailMapClick; window.moveBackToStep1 = moveBackToStep1; window.moveBackToStep2 = moveBackToStep2; window.confirmPanelMove = confirmPanelMove; window.openPanelRepairModal = openPanelRepairModal; window.closePanelRepairModal = closePanelRepairModal; window.submitPanelRepair = submitPanelRepair; window.openPanelExportModal = openPanelExportModal; window.closePanelExportModal = closePanelExportModal; window.submitPanelExport = submitPanelExport; window.openPanelReturnModal = openPanelReturnModal; window.closePanelReturnModal = closePanelReturnModal; window.submitPanelReturn = submitPanelReturn; window.loadMovedEquipments = loadMovedEquipments; window.returnEquipmentToOriginal = returnEquipmentToOriginal; window.showMovedEquipmentDetail = showMovedEquipmentDetail; window.loadWorkplaceMovedEquipments = loadWorkplaceMovedEquipments;