초기 배치 시 프리뷰 생성
This commit is contained in:
parent
b80d6cb85e
commit
711f2670de
|
|
@ -73,6 +73,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
const [draggedTool, setDraggedTool] = useState<ToolType | null>(null);
|
const [draggedTool, setDraggedTool] = useState<ToolType | null>(null);
|
||||||
const [draggedAreaData, setDraggedAreaData] = useState<Area | null>(null); // 드래그 중인 Area 정보
|
const [draggedAreaData, setDraggedAreaData] = useState<Area | null>(null); // 드래그 중인 Area 정보
|
||||||
const [draggedLocationData, setDraggedLocationData] = useState<Location | null>(null); // 드래그 중인 Location 정보
|
const [draggedLocationData, setDraggedLocationData] = useState<Location | null>(null); // 드래그 중인 Location 정보
|
||||||
|
const [previewPosition, setPreviewPosition] = useState<{ x: number; z: number } | null>(null); // 드래그 프리뷰 위치
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
|
@ -832,7 +833,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
|
|
||||||
// Location의 자재 목록 로드
|
// Location의 자재 목록 로드
|
||||||
const loadMaterialsForLocation = async (locaKey: string) => {
|
const loadMaterialsForLocation = async (locaKey: string) => {
|
||||||
|
console.log("🔍 자재 조회 시작:", { locaKey, selectedDbConnection, material: hierarchyConfig?.material });
|
||||||
|
|
||||||
if (!selectedDbConnection || !hierarchyConfig?.material) {
|
if (!selectedDbConnection || !hierarchyConfig?.material) {
|
||||||
|
console.error("❌ 설정 누락:", { selectedDbConnection, material: hierarchyConfig?.material });
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "자재 조회 실패",
|
title: "자재 조회 실패",
|
||||||
|
|
@ -844,10 +848,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
try {
|
try {
|
||||||
setLoadingMaterials(true);
|
setLoadingMaterials(true);
|
||||||
setShowMaterialPanel(true);
|
setShowMaterialPanel(true);
|
||||||
const response = await getMaterials(selectedDbConnection, {
|
|
||||||
|
const materialConfig = {
|
||||||
...hierarchyConfig.material,
|
...hierarchyConfig.material,
|
||||||
locaKey: locaKey,
|
locaKey: locaKey,
|
||||||
});
|
};
|
||||||
|
console.log("📡 API 호출:", { externalDbConnectionId: selectedDbConnection, materialConfig });
|
||||||
|
|
||||||
|
const response = await getMaterials(selectedDbConnection, materialConfig);
|
||||||
|
console.log("📦 API 응답:", response);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// layerColumn이 있으면 정렬
|
// layerColumn이 있으면 정렬
|
||||||
const sortedMaterials = hierarchyConfig.material.layerColumn
|
const sortedMaterials = hierarchyConfig.material.layerColumn
|
||||||
|
|
@ -1597,48 +1606,70 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 중앙: 3D 캔버스 */}
|
{/* 중앙: 3D 캔버스 */}
|
||||||
<div
|
<div className="relative h-full flex-1 bg-gray-100">
|
||||||
className="h-full flex-1 bg-gray-100"
|
{isLoading ? (
|
||||||
onDragOver={(e) => e.preventDefault()}
|
<div className="flex h-full items-center justify-center">
|
||||||
onDrop={(e) => {
|
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
e.preventDefault();
|
</div>
|
||||||
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;
|
<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 단위)
|
// 그리드 크기 (5 단위)
|
||||||
const gridSize = 5;
|
const gridSize = 5;
|
||||||
|
|
||||||
// 그리드에 스냅
|
// 그리드에 스냅
|
||||||
// Area(20x20)는 그리드 교차점에, 다른 객체(5x5)는 타일 중앙에
|
let snappedX = Math.round(rawX / gridSize) * gridSize;
|
||||||
let snappedX = Math.round(rawX / gridSize) * gridSize;
|
let snappedZ = Math.round(rawZ / gridSize) * gridSize;
|
||||||
let snappedZ = Math.round(rawZ / gridSize) * gridSize;
|
|
||||||
|
|
||||||
// 5x5 객체는 타일 중앙으로 오프셋 (Area는 제외)
|
// 5x5 객체는 타일 중앙으로 오프셋 (Area는 제외)
|
||||||
if (draggedTool !== "area") {
|
if (draggedTool !== "area") {
|
||||||
snappedX += gridSize / 2;
|
snappedX += gridSize / 2;
|
||||||
snappedZ += gridSize / 2;
|
snappedZ += gridSize / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCanvasDrop(snappedX, snappedZ);
|
setPreviewPosition({ x: snappedX, z: snappedZ });
|
||||||
}}
|
}}
|
||||||
>
|
onDragLeave={() => {
|
||||||
{isLoading ? (
|
setPreviewPosition(null);
|
||||||
<div className="flex h-full items-center justify-center">
|
}}
|
||||||
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
onDrop={(e) => {
|
||||||
</div>
|
e.preventDefault();
|
||||||
) : (
|
e.stopPropagation();
|
||||||
<Yard3DCanvas
|
if (previewPosition) {
|
||||||
placements={placements}
|
handleCanvasDrop(previewPosition.x, previewPosition.z);
|
||||||
selectedPlacementId={selectedObject?.id || null}
|
setPreviewPosition(null);
|
||||||
onPlacementClick={(placement) => handleObjectClick(placement?.id || null)}
|
}
|
||||||
onPlacementDrag={(id, position) => handleObjectMove(id, position.x, position.z, position.y)}
|
setDraggedTool(null);
|
||||||
focusOnPlacementId={null}
|
setDraggedAreaData(null);
|
||||||
onCollisionDetected={() => {}}
|
setDraggedLocationData(null);
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 우측: 객체 속성 편집 or 자재 목록 */}
|
{/* 우측: 객체 속성 편집 or 자재 목록 */}
|
||||||
<div className="h-full w-[25%] overflow-y-auto border-l">
|
<div className="h-full w-[25%] overflow-y-auto border-l">
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ interface Yard3DCanvasProps {
|
||||||
gridSize?: number; // 그리드 크기 (기본값: 5)
|
gridSize?: number; // 그리드 크기 (기본값: 5)
|
||||||
onCollisionDetected?: () => void; // 충돌 감지 시 콜백
|
onCollisionDetected?: () => void; // 충돌 감지 시 콜백
|
||||||
focusOnPlacementId?: number | null; // 카메라가 포커스할 요소 ID
|
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,
|
gridSize = 5,
|
||||||
onCollisionDetected,
|
onCollisionDetected,
|
||||||
focusOnPlacementId,
|
focusOnPlacementId,
|
||||||
|
previewTool,
|
||||||
|
previewPosition,
|
||||||
}: Yard3DCanvasProps) {
|
}: Yard3DCanvasProps) {
|
||||||
const [isDraggingAny, setIsDraggingAny] = useState(false);
|
const [isDraggingAny, setIsDraggingAny] = useState(false);
|
||||||
const orbitControlsRef = useRef<any>(null);
|
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 (
|
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
|
<OrbitControls
|
||||||
ref={orbitControlsRef}
|
ref={orbitControlsRef}
|
||||||
|
|
@ -1095,6 +1138,9 @@ export default function Yard3DCanvas({
|
||||||
gridSize = 5,
|
gridSize = 5,
|
||||||
onCollisionDetected,
|
onCollisionDetected,
|
||||||
focusOnPlacementId,
|
focusOnPlacementId,
|
||||||
|
previewTool,
|
||||||
|
previewPosition,
|
||||||
|
onPreviewPositionUpdate,
|
||||||
}: Yard3DCanvasProps) {
|
}: Yard3DCanvasProps) {
|
||||||
const handleCanvasClick = (e: any) => {
|
const handleCanvasClick = (e: any) => {
|
||||||
// Canvas의 빈 공간을 클릭했을 때만 선택 해제
|
// Canvas의 빈 공간을 클릭했을 때만 선택 해제
|
||||||
|
|
@ -1123,6 +1169,8 @@ export default function Yard3DCanvas({
|
||||||
gridSize={gridSize}
|
gridSize={gridSize}
|
||||||
onCollisionDetected={onCollisionDetected}
|
onCollisionDetected={onCollisionDetected}
|
||||||
focusOnPlacementId={focusOnPlacementId}
|
focusOnPlacementId={focusOnPlacementId}
|
||||||
|
previewTool={previewTool}
|
||||||
|
previewPosition={previewPosition}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue