diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx index 8b2d88f8..47902b42 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx @@ -73,6 +73,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi const [draggedTool, setDraggedTool] = useState(null); const [draggedAreaData, setDraggedAreaData] = useState(null); // 드래그 중인 Area 정보 const [draggedLocationData, setDraggedLocationData] = useState(null); // 드래그 중인 Location 정보 + const [previewPosition, setPreviewPosition] = useState<{ x: number; z: number } | null>(null); // 드래그 프리뷰 위치 const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); @@ -832,7 +833,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi // Location의 자재 목록 로드 const loadMaterialsForLocation = async (locaKey: string) => { + console.log("🔍 자재 조회 시작:", { locaKey, selectedDbConnection, material: hierarchyConfig?.material }); + if (!selectedDbConnection || !hierarchyConfig?.material) { + console.error("❌ 설정 누락:", { selectedDbConnection, material: hierarchyConfig?.material }); toast({ variant: "destructive", title: "자재 조회 실패", @@ -844,10 +848,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi try { setLoadingMaterials(true); setShowMaterialPanel(true); - const response = await getMaterials(selectedDbConnection, { + + const materialConfig = { ...hierarchyConfig.material, locaKey: locaKey, - }); + }; + console.log("📡 API 호출:", { externalDbConnectionId: selectedDbConnection, materialConfig }); + + const response = await getMaterials(selectedDbConnection, materialConfig); + console.log("📦 API 응답:", response); if (response.success && response.data) { // layerColumn이 있으면 정렬 const sortedMaterials = hierarchyConfig.material.layerColumn @@ -1597,48 +1606,70 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi - {/* 중앙: 3D 캔버스 */} -
e.preventDefault()} - onDrop={(e) => { - e.preventDefault(); - const rect = e.currentTarget.getBoundingClientRect(); - const rawX = ((e.clientX - rect.left) / rect.width - 0.5) * 100; - const rawZ = ((e.clientY - rect.top) / rect.height - 0.5) * 100; + {/* 중앙: 3D 캔버스 */} +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + handleObjectClick(placement?.id || null)} + onPlacementDrag={(id, position) => handleObjectMove(id, position.x, position.z, position.y)} + focusOnPlacementId={null} + onCollisionDetected={() => {}} + previewTool={draggedTool} + previewPosition={previewPosition} + onPreviewPositionUpdate={setPreviewPosition} + /> + {/* 드래그 중일 때 Canvas 위에 투명한 오버레이 (프리뷰 및 드롭 이벤트 캐치용) */} + {draggedTool && ( +
{ + e.preventDefault(); + const rect = e.currentTarget.getBoundingClientRect(); + const rawX = ((e.clientX - rect.left) / rect.width - 0.5) * 100; + const rawZ = ((e.clientY - rect.top) / rect.height - 0.5) * 100; - // 그리드 크기 (5 단위) - const gridSize = 5; + // 그리드 크기 (5 단위) + const gridSize = 5; - // 그리드에 스냅 - // Area(20x20)는 그리드 교차점에, 다른 객체(5x5)는 타일 중앙에 - let snappedX = Math.round(rawX / gridSize) * gridSize; - let snappedZ = Math.round(rawZ / gridSize) * gridSize; + // 그리드에 스냅 + let snappedX = Math.round(rawX / gridSize) * gridSize; + let snappedZ = Math.round(rawZ / gridSize) * gridSize; - // 5x5 객체는 타일 중앙으로 오프셋 (Area는 제외) - if (draggedTool !== "area") { - snappedX += gridSize / 2; - snappedZ += gridSize / 2; - } + // 5x5 객체는 타일 중앙으로 오프셋 (Area는 제외) + if (draggedTool !== "area") { + snappedX += gridSize / 2; + snappedZ += gridSize / 2; + } - handleCanvasDrop(snappedX, snappedZ); - }} - > - {isLoading ? ( -
- -
- ) : ( - handleObjectClick(placement?.id || null)} - onPlacementDrag={(id, position) => handleObjectMove(id, position.x, position.z, position.y)} - focusOnPlacementId={null} - onCollisionDetected={() => {}} - /> - )} -
+ setPreviewPosition({ x: snappedX, z: snappedZ }); + }} + onDragLeave={() => { + setPreviewPosition(null); + }} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + if (previewPosition) { + handleCanvasDrop(previewPosition.x, previewPosition.z); + setPreviewPosition(null); + } + setDraggedTool(null); + setDraggedAreaData(null); + setDraggedLocationData(null); + }} + /> + )} + + )} +
{/* 우측: 객체 속성 편집 or 자재 목록 */}
diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx index 3de44b02..cb70b75a 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx @@ -35,6 +35,9 @@ interface Yard3DCanvasProps { gridSize?: number; // 그리드 크기 (기본값: 5) onCollisionDetected?: () => void; // 충돌 감지 시 콜백 focusOnPlacementId?: number | null; // 카메라가 포커스할 요소 ID + previewTool?: string | null; // 드래그 중인 도구 타입 + previewPosition?: { x: number; z: number } | null; // 프리뷰 위치 + onPreviewPositionUpdate?: (position: { x: number; z: number } | null) => void; } // 좌표를 그리드 칸의 중심에 스냅 (마인크래프트 스타일) @@ -1007,10 +1010,26 @@ function Scene({ gridSize = 5, onCollisionDetected, focusOnPlacementId, + previewTool, + previewPosition, }: Yard3DCanvasProps) { const [isDraggingAny, setIsDraggingAny] = useState(false); const orbitControlsRef = useRef(null); + // 프리뷰 박스 크기 계산 + const getPreviewSize = (tool: string) => { + if (tool === "area") return { x: 20, y: 0.1, z: 20 }; + return { x: 5, y: 5, z: 5 }; + }; + + // 프리뷰 박스 색상 + const getPreviewColor = (tool: string) => { + if (tool === "area") return "#3b82f6"; + if (tool === "location-bed") return "#10b981"; + if (tool === "location-stp") return "#f59e0b"; + return "#9ca3af"; + }; + return ( <> {/* 카메라 포커스 컨트롤러 */} @@ -1069,6 +1088,30 @@ function Scene({ /> ))} + {/* 드래그 프리뷰 박스 */} + {previewTool && previewPosition && ( + + + + )} + {/* 카메라 컨트롤 */} { // Canvas의 빈 공간을 클릭했을 때만 선택 해제 @@ -1123,6 +1169,8 @@ export default function Yard3DCanvas({ gridSize={gridSize} onCollisionDetected={onCollisionDetected} focusOnPlacementId={focusOnPlacementId} + previewTool={previewTool} + previewPosition={previewPosition} />