/* ===== Layout Map (구역지도) ===== */ let layoutMapImage = null; let mapRegions = []; let mapCanvas = null; let mapCtx = null; let isDrawing = false; let drawStartX = 0; let drawStartY = 0; let currentRect = null; let selectedMapCategoryId = null; // 구역지도 프리뷰 캔버스 클릭 -> 해당 영역의 작업장으로 드릴다운 document.getElementById('previewCanvas')?.addEventListener('click', function(e) { if (!previewMapRegions.length) return; const rect = this.getBoundingClientRect(); const xPct = ((e.clientX - rect.left) / rect.width) * 100; const yPct = ((e.clientY - rect.top) / rect.height) * 100; for (const region of previewMapRegions) { if (xPct >= region.x_start && xPct <= region.x_end && yPct >= region.y_start && yPct <= region.y_end) { const wp = workplaces.find(w => w.workplace_id === region.workplace_id); if (wp) { selectWorkplaceForEquipments(wp.workplace_id, wp.workplace_name); } return; } } }); async function loadLayoutPreview(categoryId) { const cat = workplaceCategories.find(c => c.category_id == categoryId); if (!cat || !cat.layout_image) { document.getElementById('layoutPreviewArea').classList.remove('hidden'); document.getElementById('layoutPreviewCanvas').classList.add('hidden'); document.getElementById('layoutPreviewArea').innerHTML = '

레이아웃 이미지가 없습니다. "지도 설정"에서 업로드하세요.

'; return; } document.getElementById('layoutPreviewArea').classList.add('hidden'); document.getElementById('layoutPreviewCanvas').classList.remove('hidden'); const pCanvas = document.getElementById('previewCanvas'); const pCtx = pCanvas.getContext('2d'); const imgUrl = cat.layout_image.startsWith('http') ? cat.layout_image : '/uploads/' + cat.layout_image.replace(/^\/uploads\//, ''); const img = new Image(); img.onload = async function() { const maxW = 800; const scale = img.width > maxW ? maxW / img.width : 1; pCanvas.width = img.width * scale; pCanvas.height = img.height * scale; pCtx.drawImage(img, 0, 0, pCanvas.width, pCanvas.height); try { const r = await api(`/workplaces/categories/${categoryId}/map-regions`); const regions = r.data || []; previewMapRegions = regions; regions.forEach(region => { const x1 = (region.x_start / 100) * pCanvas.width; const y1 = (region.y_start / 100) * pCanvas.height; const x2 = (region.x_end / 100) * pCanvas.width; const y2 = (region.y_end / 100) * pCanvas.height; pCtx.strokeStyle = '#10b981'; pCtx.lineWidth = 2; pCtx.strokeRect(x1, y1, x2 - x1, y2 - y1); pCtx.fillStyle = 'rgba(16, 185, 129, 0.15)'; pCtx.fillRect(x1, y1, x2 - x1, y2 - y1); pCtx.fillStyle = '#10b981'; pCtx.font = '14px sans-serif'; pCtx.fillText(region.workplace_name || '', x1 + 5, y1 + 20); }); } catch(e) { console.warn('영역 로드 실패:', e); } }; img.src = imgUrl; } // 구역지도 모달 function openLayoutMapModal() { if (!selectedMapCategoryId) { showToast('공장을 먼저 선택해주세요.', 'error'); return; } const modal = document.getElementById('layoutMapModal'); mapCanvas = document.getElementById('regionCanvas'); mapCtx = mapCanvas.getContext('2d'); modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; loadLayoutMapData(); updateRegionWorkplaceSelect(); } function closeLayoutMapModal() { const modal = document.getElementById('layoutMapModal'); modal.style.display = 'none'; document.body.style.overflow = ''; if (mapCanvas) { mapCanvas.removeEventListener('mousedown', onCanvasMouseDown); mapCanvas.removeEventListener('mousemove', onCanvasMouseMove); mapCanvas.removeEventListener('mouseup', onCanvasMouseUp); mapCanvas.removeEventListener('mouseleave', onCanvasMouseUp); } currentRect = null; if (selectedMapCategoryId) loadLayoutPreview(selectedMapCategoryId); } async function loadLayoutMapData() { try { const cat = workplaceCategories.find(c => c.category_id == selectedMapCategoryId); if (!cat) return; const imgDiv = document.getElementById('currentLayoutImage'); if (cat.layout_image) { const imgUrl = cat.layout_image.startsWith('http') ? cat.layout_image : '/uploads/' + cat.layout_image.replace(/^\/uploads\//, ''); imgDiv.innerHTML = `레이아웃`; loadImageToCanvas(imgUrl); } else { imgDiv.innerHTML = '업로드된 이미지가 없습니다'; } const r = await api(`/workplaces/categories/${selectedMapCategoryId}/map-regions`); mapRegions = r.data || []; renderRegionList(); } catch(e) { console.error('레이아웃 데이터 로딩 오류:', e); } } function loadImageToCanvas(imgUrl) { const img = new Image(); img.onload = function() { const maxW = 800; const scale = img.width > maxW ? maxW / img.width : 1; mapCanvas.width = img.width * scale; mapCanvas.height = img.height * scale; mapCtx.drawImage(img, 0, 0, mapCanvas.width, mapCanvas.height); layoutMapImage = img; drawExistingRegions(); setupCanvasEvents(); }; img.src = imgUrl; } function updateRegionWorkplaceSelect() { const sel = document.getElementById('regionWorkplaceSelect'); if (!sel) return; const catWps = workplaces.filter(w => w.category_id == selectedMapCategoryId); let html = ''; catWps.forEach(wp => { const hasRegion = mapRegions.some(r => r.workplace_id === wp.workplace_id); html += ``; }); sel.innerHTML = html; } function previewLayoutImage(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { document.getElementById('currentLayoutImage').innerHTML = ` 미리보기

미리보기 (저장하려면 "이미지 업로드" 버튼 클릭)

`; }; reader.readAsDataURL(file); } async function uploadLayoutImage() { const file = document.getElementById('layoutImageFile').files[0]; if (!file) { showToast('이미지를 선택해주세요.', 'error'); return; } if (!selectedMapCategoryId) { showToast('공장을 먼저 선택해주세요.', 'error'); return; } try { const fd = new FormData(); fd.append('image', file); const token = getToken(); const res = await fetch(`${API_BASE}/workplaces/categories/${selectedMapCategoryId}/layout-image`, { method: 'POST', headers: { 'Authorization': token ? `Bearer ${token}` : '' }, body: fd }); const result = await res.json(); if (!res.ok) throw new Error(result.error || '업로드 실패'); showToast('이미지가 업로드되었습니다.'); const imgUrl = '/uploads/' + result.data.image_path.replace(/^\/uploads\//, ''); document.getElementById('currentLayoutImage').innerHTML = `레이아웃`; loadImageToCanvas(imgUrl); // 카테고리 데이터 갱신 await loadWorkplaceCategories(); } catch(e) { showToast(e.message || '업로드 실패', 'error'); } } // 캔버스 드로잉 function setupCanvasEvents() { mapCanvas.removeEventListener('mousedown', onCanvasMouseDown); mapCanvas.removeEventListener('mousemove', onCanvasMouseMove); mapCanvas.removeEventListener('mouseup', onCanvasMouseUp); mapCanvas.removeEventListener('mouseleave', onCanvasMouseUp); mapCanvas.addEventListener('mousedown', onCanvasMouseDown); mapCanvas.addEventListener('mousemove', onCanvasMouseMove); mapCanvas.addEventListener('mouseup', onCanvasMouseUp); mapCanvas.addEventListener('mouseleave', onCanvasMouseUp); } function onCanvasMouseDown(e) { const r = mapCanvas.getBoundingClientRect(); const scaleX = mapCanvas.width / r.width; const scaleY = mapCanvas.height / r.height; drawStartX = (e.clientX - r.left) * scaleX; drawStartY = (e.clientY - r.top) * scaleY; isDrawing = true; } function onCanvasMouseMove(e) { if (!isDrawing) return; const r = mapCanvas.getBoundingClientRect(); const scaleX = mapCanvas.width / r.width; const scaleY = mapCanvas.height / r.height; const curX = (e.clientX - r.left) * scaleX; const curY = (e.clientY - r.top) * scaleY; mapCtx.clearRect(0, 0, mapCanvas.width, mapCanvas.height); if (layoutMapImage) mapCtx.drawImage(layoutMapImage, 0, 0, mapCanvas.width, mapCanvas.height); drawExistingRegions(); const w = curX - drawStartX; const h = curY - drawStartY; mapCtx.strokeStyle = '#3b82f6'; mapCtx.lineWidth = 3; mapCtx.strokeRect(drawStartX, drawStartY, w, h); mapCtx.fillStyle = 'rgba(59, 130, 246, 0.2)'; mapCtx.fillRect(drawStartX, drawStartY, w, h); currentRect = { startX: drawStartX, startY: drawStartY, endX: curX, endY: curY }; } function onCanvasMouseUp() { isDrawing = false; } function drawExistingRegions() { mapRegions.forEach(region => { const x1 = (region.x_start / 100) * mapCanvas.width; const y1 = (region.y_start / 100) * mapCanvas.height; const x2 = (region.x_end / 100) * mapCanvas.width; const y2 = (region.y_end / 100) * mapCanvas.height; mapCtx.strokeStyle = '#10b981'; mapCtx.lineWidth = 2; mapCtx.strokeRect(x1, y1, x2 - x1, y2 - y1); mapCtx.fillStyle = 'rgba(16, 185, 129, 0.15)'; mapCtx.fillRect(x1, y1, x2 - x1, y2 - y1); mapCtx.fillStyle = '#10b981'; mapCtx.font = '14px sans-serif'; mapCtx.fillText(region.workplace_name || '', x1 + 5, y1 + 20); }); } function clearCurrentRegion() { currentRect = null; mapCtx.clearRect(0, 0, mapCanvas.width, mapCanvas.height); if (layoutMapImage) mapCtx.drawImage(layoutMapImage, 0, 0, mapCanvas.width, mapCanvas.height); drawExistingRegions(); } async function saveRegion() { const wpId = document.getElementById('regionWorkplaceSelect').value; if (!wpId) { showToast('작업장을 선택해주세요.', 'error'); return; } if (!currentRect) { showToast('영역을 그려주세요.', 'error'); return; } try { const xStart = (Math.min(currentRect.startX, currentRect.endX) / mapCanvas.width * 100).toFixed(2); const yStart = (Math.min(currentRect.startY, currentRect.endY) / mapCanvas.height * 100).toFixed(2); const xEnd = (Math.max(currentRect.startX, currentRect.endX) / mapCanvas.width * 100).toFixed(2); const yEnd = (Math.max(currentRect.startY, currentRect.endY) / mapCanvas.height * 100).toFixed(2); const existing = mapRegions.find(r => r.workplace_id == wpId); const body = { workplace_id: parseInt(wpId), category_id: selectedMapCategoryId, x_start: xStart, y_start: yStart, x_end: xEnd, y_end: yEnd, shape: 'rect' }; if (existing) { await api(`/workplaces/map-regions/${existing.region_id}`, { method: 'PUT', body: JSON.stringify(body) }); } else { await api('/workplaces/map-regions', { method: 'POST', body: JSON.stringify(body) }); } showToast('영역이 저장되었습니다.'); await loadLayoutMapData(); updateRegionWorkplaceSelect(); clearCurrentRegion(); document.getElementById('regionWorkplaceSelect').value = ''; } catch(e) { showToast(e.message || '저장 실패', 'error'); } } function renderRegionList() { const div = document.getElementById('regionList'); if (!mapRegions.length) { div.innerHTML = '

정의된 영역이 없습니다

'; return; } div.innerHTML = '
' + mapRegions.map(r => `
${r.workplace_name || ''} (${Number(r.x_start).toFixed(1)}%, ${Number(r.y_start).toFixed(1)}%) ~ (${Number(r.x_end).toFixed(1)}%, ${Number(r.y_end).toFixed(1)}%)
`).join('') + '
'; } function editRegion(workplaceId) { const sel = document.getElementById('regionWorkplaceSelect'); if (sel) sel.value = workplaceId; const region = mapRegions.find(r => r.workplace_id == workplaceId); if (region && layoutMapImage && mapCanvas && mapCtx) { mapCtx.clearRect(0, 0, mapCanvas.width, mapCanvas.height); mapCtx.drawImage(layoutMapImage, 0, 0, mapCanvas.width, mapCanvas.height); drawExistingRegions(); const x1 = (region.x_start / 100) * mapCanvas.width; const y1 = (region.y_start / 100) * mapCanvas.height; const x2 = (region.x_end / 100) * mapCanvas.width; const y2 = (region.y_end / 100) * mapCanvas.height; mapCtx.strokeStyle = '#f59e0b'; mapCtx.lineWidth = 3; mapCtx.setLineDash([6, 4]); mapCtx.strokeRect(x1, y1, x2 - x1, y2 - y1); mapCtx.setLineDash([]); mapCtx.fillStyle = 'rgba(245, 158, 11, 0.25)'; mapCtx.fillRect(x1, y1, x2 - x1, y2 - y1); mapCtx.fillStyle = '#f59e0b'; mapCtx.font = 'bold 14px sans-serif'; mapCtx.fillText('✏️ ' + (region.workplace_name || ''), x1 + 5, y1 + 20); } showToast(`"${region?.workplace_name || '작업장'}" 위치를 수정합니다. 새 위치를 드래그한 후 저장하세요.`); const canvasEl = document.getElementById('regionCanvas'); if (canvasEl) canvasEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } async function deleteRegion(regionId) { if (!confirm('이 영역을 삭제하시겠습니까?')) return; try { await api(`/workplaces/map-regions/${regionId}`, { method: 'DELETE' }); showToast('영역이 삭제되었습니다.'); await loadLayoutMapData(); updateRegionWorkplaceSelect(); } catch(e) { showToast(e.message || '삭제 실패', 'error'); } }