From 83034cff02712195f4c623bd1fac8ad006971b3f Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 28 Oct 2025 18:55:30 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9A=94=EC=86=8C=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=ED=95=98=EB=A9=B4=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EC=8B=9C?= =?UTF-8?q?=EC=95=BC=20=EB=B3=80=EA=B2=BD=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/yard-3d/Yard3DCanvas.tsx | 80 +++++++++++++++++++ .../dashboard/widgets/yard-3d/YardEditor.tsx | 27 ++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx index b5af2630..7e9cddec 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx @@ -31,6 +31,7 @@ interface Yard3DCanvasProps { onPlacementDrag?: (id: number, position: { x: number; y: number; z: number }) => void; gridSize?: number; // 그리드 크기 (기본값: 5) onCollisionDetected?: () => void; // 충돌 감지 시 콜백 + focusOnPlacementId?: number | null; // 카메라가 포커스할 요소 ID } // 좌표를 그리드 칸의 중심에 스냅 (마인크래프트 스타일) @@ -467,6 +468,75 @@ function MaterialBox({ } // 3D 씬 컴포넌트 +// 카메라 포커스 컨트롤러 +function CameraFocusController({ + focusOnPlacementId, + placements, + orbitControlsRef, +}: { + focusOnPlacementId?: number | null; + placements: YardPlacement[]; + orbitControlsRef: React.RefObject; +}) { + const { camera } = useThree(); + + useEffect(() => { + console.log("🎥 CameraFocusController triggered"); + console.log(" - focusOnPlacementId:", focusOnPlacementId); + console.log(" - orbitControlsRef.current:", orbitControlsRef.current); + console.log(" - placements count:", placements.length); + + if (focusOnPlacementId && orbitControlsRef.current) { + const targetPlacement = placements.find((p) => p.id === focusOnPlacementId); + console.log(" - targetPlacement:", targetPlacement); + + if (targetPlacement) { + console.log("✅ Starting camera animation to:", targetPlacement.material_name || targetPlacement.id); + + const controls = orbitControlsRef.current; + const targetPosition = new THREE.Vector3( + targetPlacement.position_x, + targetPlacement.position_y, + targetPlacement.position_z, + ); + + // 카메라 위치 계산 (요소 위에서 약간 비스듬히) + const cameraOffset = new THREE.Vector3(15, 15, 15); + const newCameraPosition = targetPosition.clone().add(cameraOffset); + + // 부드러운 애니메이션으로 카메라 이동 + const startPos = camera.position.clone(); + const startTarget = controls.target.clone(); + const duration = 1000; // 1초 + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // easeInOutCubic 이징 함수 + const eased = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2; + + // 카메라 위치 보간 + camera.position.lerpVectors(startPos, newCameraPosition, eased); + + // 카메라 타겟 보간 + controls.target.lerpVectors(startTarget, targetPosition, eased); + controls.update(); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + animate(); + } + } + }, [focusOnPlacementId, placements, camera, orbitControlsRef]); + + return null; +} + function Scene({ placements, selectedPlacementId, @@ -474,12 +544,20 @@ function Scene({ onPlacementDrag, gridSize = 5, onCollisionDetected, + focusOnPlacementId, }: Yard3DCanvasProps) { const [isDraggingAny, setIsDraggingAny] = useState(false); const orbitControlsRef = useRef(null); return ( <> + {/* 카메라 포커스 컨트롤러 */} + + {/* 조명 */} @@ -551,6 +629,7 @@ export default function Yard3DCanvas({ onPlacementDrag, gridSize = 5, onCollisionDetected, + focusOnPlacementId, }: Yard3DCanvasProps) { const handleCanvasClick = (e: any) => { // Canvas의 빈 공간을 클릭했을 때만 선택 해제 @@ -577,6 +656,7 @@ export default function Yard3DCanvas({ onPlacementDrag={onPlacementDrag} gridSize={gridSize} onCollisionDetected={onCollisionDetected} + focusOnPlacementId={focusOnPlacementId} /> diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx index a9fea2f3..726239f0 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/YardEditor.tsx @@ -38,6 +38,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { const [placements, setPlacements] = useState([]); const [originalPlacements, setOriginalPlacements] = useState([]); // 원본 데이터 보관 const [selectedPlacement, setSelectedPlacement] = useState(null); + const [focusPlacementId, setFocusPlacementId] = useState(null); // 카메라 포커스할 요소 ID const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [showConfigPanel, setShowConfigPanel] = useState(false); @@ -203,9 +204,30 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { }; // 요소 선택 (3D 캔버스 또는 목록에서) - const handleSelectPlacement = (placement: YardPlacement) => { + const handleSelectPlacement = (placement: YardPlacement | null) => { + console.log("📍 handleSelectPlacement called with:", placement); + + if (!placement) { + // 빈 공간 클릭 시 선택 해제 + console.log(" → Deselecting (null placement)"); + setSelectedPlacement(null); + setShowConfigPanel(false); + setFocusPlacementId(null); + return; + } + + console.log(" → Selecting placement:", placement.id, placement.material_name); setSelectedPlacement(placement); setShowConfigPanel(false); // 선택 시에는 설정 패널 닫기 + + console.log(" → Setting focusPlacementId to:", placement.id); + setFocusPlacementId(placement.id); // 카메라 포커스 + + // 카메라 애니메이션 완료 후 focusPlacementId 초기화 (재클릭 시 다시 포커스 가능) + setTimeout(() => { + console.log(" → Clearing focusPlacementId"); + setFocusPlacementId(null); + }, 1100); // 애니메이션 시간(1000ms)보다 약간 길게 }; // 설정 버튼 클릭 @@ -500,8 +522,9 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) { handleSelectPlacement(placement as YardPlacement)} + onPlacementClick={(placement) => handleSelectPlacement(placement as YardPlacement | null)} onPlacementDrag={handlePlacementDrag} + focusOnPlacementId={focusPlacementId} onCollisionDetected={() => { toast({ title: "배치 불가",