ERP-node/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx

184 lines
6.1 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import Yard3DCanvas from "./Yard3DCanvas";
import { yardLayoutApi } from "@/lib/api/yardLayoutApi";
import { Loader2 } from "lucide-react";
interface YardPlacement {
id: number;
yard_layout_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?: Record<string, unknown> | null;
data_binding?: Record<string, unknown> | null;
status?: string;
memo?: string;
}
interface YardLayout {
id: number;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
}
interface Yard3DViewerProps {
layoutId: number;
}
export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
const [placements, setPlacements] = useState<YardPlacement[]>([]);
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
const [layoutName, setLayoutName] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 선택 변경 로그
const handlePlacementClick = (placement: YardPlacement | null) => {
console.log("Yard3DViewer - Placement clicked:", placement?.material_name);
setSelectedPlacement(placement);
};
// 선택 상태 변경 감지
useEffect(() => {
console.log("selectedPlacement changed:", selectedPlacement?.material_name);
}, [selectedPlacement]);
// 야드 레이아웃 및 배치 데이터 로드
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true);
setError(null);
// 야드 레이아웃 정보 조회
const layoutResponse = await yardLayoutApi.getLayoutById(layoutId);
if (layoutResponse.success) {
const layout = layoutResponse.data as YardLayout;
setLayoutName(layout.name);
}
// 배치 데이터 조회
const placementsResponse = await yardLayoutApi.getPlacementsByLayoutId(layoutId);
if (placementsResponse.success) {
setPlacements(placementsResponse.data as YardPlacement[]);
} else {
setError("배치 데이터를 불러올 수 없습니다.");
}
} catch (err) {
console.error("데이터 로드 실패:", err);
setError("데이터를 불러오는 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
loadData();
}, [layoutId]);
if (isLoading) {
return (
2025-10-29 17:53:03 +09:00
<div className="flex h-full w-full items-center justify-center bg-muted">
<div className="text-center">
2025-10-29 17:53:03 +09:00
<Loader2 className="mx-auto h-8 w-8 animate-spin text-primary" />
<div className="mt-2 text-sm text-foreground">3D ...</div>
</div>
</div>
);
}
if (error) {
return (
2025-10-29 17:53:03 +09:00
<div className="flex h-full w-full items-center justify-center bg-muted">
<div className="text-center">
<div className="mb-2 text-4xl"></div>
2025-10-29 17:53:03 +09:00
<div className="text-sm font-medium text-foreground">{error}</div>
</div>
</div>
);
}
if (placements.length === 0) {
return (
2025-10-29 17:53:03 +09:00
<div className="flex h-full w-full items-center justify-center bg-muted">
<div className="text-center">
<div className="mb-2 text-4xl">📦</div>
2025-10-29 17:53:03 +09:00
<div className="text-sm font-medium text-foreground"> </div>
</div>
</div>
);
}
return (
<div className="relative h-full w-full">
{/* 3D 캔버스 */}
<Yard3DCanvas
placements={placements}
selectedPlacementId={selectedPlacement?.id || null}
onPlacementClick={handlePlacementClick}
/>
{/* 야드 이름 (좌측 상단) */}
{layoutName && (
2025-10-29 17:53:03 +09:00
<div className="absolute top-4 left-4 z-49 rounded-lg border border-border bg-background px-4 py-2 shadow-lg">
<h2 className="text-base font-bold text-foreground">{layoutName}</h2>
</div>
)}
{/* 선택된 자재 정보 패널 (우측 상단) */}
{selectedPlacement && (
2025-10-29 17:53:03 +09:00
<div className="absolute top-4 right-4 z-50 w-64 rounded-lg border border-border bg-background p-4 shadow-xl">
<div className="mb-3 flex items-center justify-between">
2025-10-29 17:53:03 +09:00
<h3 className="text-sm font-semibold text-foreground">
{selectedPlacement.material_name ? "자재 정보" : "미설정 요소"}
</h3>
<button
onClick={() => {
setSelectedPlacement(null);
}}
2025-10-29 17:53:03 +09:00
className="rounded-full p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
</button>
</div>
{selectedPlacement.material_name && selectedPlacement.quantity && selectedPlacement.unit ? (
<div className="space-y-2">
<div>
2025-10-29 17:53:03 +09:00
<label className="text-xs font-medium text-muted-foreground"></label>
<div className="mt-1 text-sm font-semibold text-foreground">{selectedPlacement.material_name}</div>
</div>
<div>
2025-10-29 17:53:03 +09:00
<label className="text-xs font-medium text-muted-foreground"></label>
<div className="mt-1 text-sm font-semibold text-foreground">
{selectedPlacement.quantity} {selectedPlacement.unit}
</div>
</div>
</div>
) : (
2025-10-29 17:53:03 +09:00
<div className="rounded-lg bg-warning/10 p-3 text-center">
<div className="mb-2 text-2xl"></div>
2025-10-29 17:53:03 +09:00
<div className="text-sm font-medium text-warning"> </div>
<div className="text-sm font-medium text-warning"> </div>
<div className="mt-2 text-xs text-warning"> </div>
</div>
)}
</div>
)}
</div>
);
}