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

184 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 (
<div className="bg-muted flex h-full w-full items-center justify-center">
<div className="text-center">
<Loader2 className="text-primary mx-auto h-8 w-8 animate-spin" />
<div className="text-foreground mt-2 text-sm">3D ...</div>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-muted flex h-full w-full items-center justify-center">
<div className="text-center">
<div className="mb-2 text-4xl"></div>
<div className="text-foreground text-sm font-medium">{error}</div>
</div>
</div>
);
}
if (placements.length === 0) {
return (
<div className="bg-muted flex h-full w-full items-center justify-center">
<div className="text-center">
<div className="mb-2 text-4xl">📦</div>
<div className="text-foreground text-sm font-medium"> </div>
</div>
</div>
);
}
return (
<div className="relative h-full w-full">
{/* 3D 캔버스 */}
<Yard3DCanvas
placements={placements}
selectedPlacementId={selectedPlacement?.id || null}
onPlacementClick={handlePlacementClick}
/>
{/* 야드 이름 (좌측 상단) */}
{layoutName && (
<div className="border-border bg-background absolute top-4 left-4 z-49 rounded-lg border px-4 py-2 shadow-lg">
<h2 className="text-foreground text-base font-bold">{layoutName}</h2>
</div>
)}
{/* 선택된 자재 정보 패널 (우측 상단) */}
{selectedPlacement && (
<div className="border-border bg-background absolute top-4 right-4 z-50 w-64 rounded-lg border p-4 shadow-xl">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-foreground text-sm font-semibold">
{selectedPlacement.material_name ? "자재 정보" : "미설정 요소"}
</h3>
<button
onClick={() => {
setSelectedPlacement(null);
}}
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded-full p-1"
>
</button>
</div>
{selectedPlacement.material_name && selectedPlacement.quantity && selectedPlacement.unit ? (
<div className="space-y-2">
<div>
<label className="text-muted-foreground text-xs font-medium"></label>
<div className="text-foreground mt-1 text-sm font-semibold">{selectedPlacement.material_name}</div>
</div>
<div>
<label className="text-muted-foreground text-xs font-medium"></label>
<div className="text-foreground mt-1 text-sm font-semibold">
{selectedPlacement.quantity} {selectedPlacement.unit}
</div>
</div>
</div>
) : (
<div className="bg-warning/10 rounded-lg p-3 text-center">
<div className="mb-2 text-2xl"></div>
<div className="text-warning text-sm font-medium"> </div>
<div className="text-warning text-sm font-medium"> </div>
<div className="text-warning mt-2 text-xs"> </div>
</div>
)}
</div>
)}
</div>
);
}