"use client"; import { Canvas, useThree } from "@react-three/fiber"; import { OrbitControls, Grid, Box } from "@react-three/drei"; import { Suspense, useRef, useState, useEffect } from "react"; import * as THREE from "three"; interface YardPlacement { id: number; material_code?: string | null; material_name?: string | null; quantity?: number | null; unit?: string | null; position_x: number; position_y: number; position_z: number; size_x: number; size_y: number; size_z: number; color: string; data_source_type?: string | null; data_source_config?: any; data_binding?: any; } interface Yard3DCanvasProps { placements: YardPlacement[]; selectedPlacementId: number | null; onPlacementClick: (placement: YardPlacement) => void; onPlacementDrag?: (id: number, position: { x: number; y: number; z: number }) => void; } // 자재 박스 컴포넌트 (드래그 가능) function MaterialBox({ placement, isSelected, onClick, onDrag, onDragStart, onDragEnd, }: { placement: YardPlacement; isSelected: boolean; onClick: () => void; onDrag?: (position: { x: number; y: number; z: number }) => void; onDragStart?: () => void; onDragEnd?: () => void; }) { const meshRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const dragStartPos = useRef<{ x: number; y: number; z: number }>({ x: 0, y: 0, z: 0 }); const mouseStartPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const { camera, gl } = useThree(); // 드래그 중이 아닐 때 위치 업데이트 useEffect(() => { if (!isDragging && meshRef.current) { meshRef.current.position.set(placement.position_x, placement.position_y, placement.position_z); } }, [placement.position_x, placement.position_y, placement.position_z, isDragging]); // 전역 이벤트 리스너 등록 useEffect(() => { const handleGlobalMouseMove = (e: MouseEvent) => { if (isDragging && onDrag && meshRef.current) { e.preventDefault(); e.stopPropagation(); // 마우스 이동 거리 계산 (픽셀) const deltaX = e.clientX - mouseStartPos.current.x; const deltaY = e.clientY - mouseStartPos.current.y; // 카메라 거리를 고려한 스케일 팩터 const distance = camera.position.distanceTo(meshRef.current.position); const scaleFactor = distance / 500; // 조정 가능한 값 // 카메라 방향 벡터 const cameraDirection = new THREE.Vector3(); camera.getWorldDirection(cameraDirection); // 카메라의 우측 벡터 (X축 이동용) const right = new THREE.Vector3(); right.crossVectors(camera.up, cameraDirection).normalize(); // 실제 3D 공간에서의 이동량 계산 const moveRight = right.multiplyScalar(-deltaX * scaleFactor); const moveForward = new THREE.Vector3(-cameraDirection.x, 0, -cameraDirection.z) .normalize() .multiplyScalar(deltaY * scaleFactor); // 최종 위치 계산 const finalX = dragStartPos.current.x + moveRight.x + moveForward.x; const finalZ = dragStartPos.current.z + moveRight.z + moveForward.z; // NaN 검증 if (isNaN(finalX) || isNaN(finalZ)) { return; } // 즉시 mesh 위치 업데이트 (부드러운 드래그) meshRef.current.position.set(finalX, dragStartPos.current.y, finalZ); // 상태 업데이트 (저장용) onDrag({ x: finalX, y: dragStartPos.current.y, z: finalZ, }); } }; const handleGlobalMouseUp = () => { if (isDragging) { setIsDragging(false); gl.domElement.style.cursor = isSelected ? "grab" : "pointer"; if (onDragEnd) { onDragEnd(); } } }; if (isDragging) { window.addEventListener("mousemove", handleGlobalMouseMove); window.addEventListener("mouseup", handleGlobalMouseUp); return () => { window.removeEventListener("mousemove", handleGlobalMouseMove); window.removeEventListener("mouseup", handleGlobalMouseUp); }; } }, [isDragging, onDrag, onDragEnd, camera, isSelected, gl.domElement]); const handlePointerDown = (e: any) => { e.stopPropagation(); // 뷰어 모드(onDrag 없음)에서는 클릭만 처리 if (!onDrag) { return; } // 편집 모드에서 선택되었고 드래그 가능한 경우 if (isSelected && meshRef.current) { // 드래그 시작 시점의 자재 위치 저장 (숫자로 변환) dragStartPos.current = { x: Number(placement.position_x), y: Number(placement.position_y), z: Number(placement.position_z), }; // 마우스 시작 위치 저장 mouseStartPos.current = { x: e.clientX, y: e.clientY, }; setIsDragging(true); gl.domElement.style.cursor = "grabbing"; if (onDragStart) { onDragStart(); } } }; // 요소가 설정되었는지 확인 const isConfigured = !!(placement.material_name && placement.quantity && placement.unit); return ( { e.stopPropagation(); e.nativeEvent?.stopPropagation(); e.nativeEvent?.stopImmediatePropagation(); onClick(); }} onPointerDown={handlePointerDown} onPointerOver={() => { // 뷰어 모드(onDrag 없음)에서는 기본 커서, 편집 모드에서는 grab 커서 if (onDrag) { gl.domElement.style.cursor = isSelected ? "grab" : "pointer"; } else { gl.domElement.style.cursor = "pointer"; } }} onPointerOut={() => { if (!isDragging) { gl.domElement.style.cursor = "default"; } }} > ); } // 3D 씬 컴포넌트 function Scene({ placements, selectedPlacementId, onPlacementClick, onPlacementDrag }: Yard3DCanvasProps) { const [isDraggingAny, setIsDraggingAny] = useState(false); const orbitControlsRef = useRef(null); return ( <> {/* 조명 */} {/* 바닥 그리드 */} {/* 자재 박스들 */} {placements.map((placement) => ( onPlacementClick(placement)} onDrag={onPlacementDrag ? (position) => onPlacementDrag(placement.id, position) : undefined} onDragStart={() => { setIsDraggingAny(true); if (orbitControlsRef.current) { orbitControlsRef.current.enabled = false; } }} onDragEnd={() => { setIsDraggingAny(false); if (orbitControlsRef.current) { orbitControlsRef.current.enabled = true; } }} /> ))} {/* 카메라 컨트롤 */} ); } export default function Yard3DCanvas({ placements, selectedPlacementId, onPlacementClick, onPlacementDrag, }: Yard3DCanvasProps) { const handleCanvasClick = (e: any) => { // Canvas의 빈 공간을 클릭했을 때만 선택 해제 // e.target이 canvas 엘리먼트인 경우 if (e.target.tagName === "CANVAS") { onPlacementClick(null as any); } }; return (
); }