초기 배치 시 프리뷰 생성

This commit is contained in:
dohyeons 2025-11-24 18:16:15 +09:00
parent b80d6cb85e
commit 711f2670de
2 changed files with 119 additions and 40 deletions

View File

@ -73,6 +73,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const [draggedTool, setDraggedTool] = useState<ToolType | null>(null);
const [draggedAreaData, setDraggedAreaData] = useState<Area | null>(null); // 드래그 중인 Area 정보
const [draggedLocationData, setDraggedLocationData] = useState<Location | null>(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
</div>
</div>
{/* 중앙: 3D 캔버스 */}
<div
className="h-full flex-1 bg-gray-100"
onDragOver={(e) => 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 캔버스 */}
<div className="relative h-full flex-1 bg-gray-100">
{isLoading ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
<Yard3DCanvas
placements={placements}
selectedPlacementId={selectedObject?.id || null}
onPlacementClick={(placement) => 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 && (
<div
className="pointer-events-auto absolute inset-0"
style={{ zIndex: 10 }}
onDragOver={(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;
// 그리드 크기 (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 ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<Yard3DCanvas
placements={placements}
selectedPlacementId={selectedObject?.id || null}
onPlacementClick={(placement) => handleObjectClick(placement?.id || null)}
onPlacementDrag={(id, position) => handleObjectMove(id, position.x, position.z, position.y)}
focusOnPlacementId={null}
onCollisionDetected={() => {}}
/>
)}
</div>
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);
}}
/>
)}
</>
)}
</div>
{/* 우측: 객체 속성 편집 or 자재 목록 */}
<div className="h-full w-[25%] overflow-y-auto border-l">

View File

@ -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<any>(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 && (
<Box
args={[
getPreviewSize(previewTool).x,
getPreviewSize(previewTool).y,
getPreviewSize(previewTool).z,
]}
position={[
previewPosition.x,
previewTool === "area" ? 0.05 : getPreviewSize(previewTool).y / 2,
previewPosition.z,
]}
>
<meshStandardMaterial
color={getPreviewColor(previewTool)}
transparent
opacity={0.4}
roughness={0.5}
metalness={0.1}
/>
</Box>
)}
{/* 카메라 컨트롤 */}
<OrbitControls
ref={orbitControlsRef}
@ -1095,6 +1138,9 @@ export default function Yard3DCanvas({
gridSize = 5,
onCollisionDetected,
focusOnPlacementId,
previewTool,
previewPosition,
onPreviewPositionUpdate,
}: Yard3DCanvasProps) {
const handleCanvasClick = (e: any) => {
// Canvas의 빈 공간을 클릭했을 때만 선택 해제
@ -1123,6 +1169,8 @@ export default function Yard3DCanvas({
gridSize={gridSize}
onCollisionDetected={onCollisionDetected}
focusOnPlacementId={focusOnPlacementId}
previewTool={previewTool}
previewPosition={previewPosition}
/>
</Suspense>
</Canvas>