175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
"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?: any;
|
||
data_binding?: any;
|
||
status?: string;
|
||
memo?: 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) {
|
||
setLayoutName(layoutResponse.data.name);
|
||
}
|
||
|
||
// 배치 데이터 조회
|
||
const placementsResponse = await yardLayoutApi.getPlacementsByLayoutId(layoutId);
|
||
if (placementsResponse.success) {
|
||
setPlacements(placementsResponse.data);
|
||
} else {
|
||
setError("배치 데이터를 불러올 수 없습니다.");
|
||
}
|
||
} catch (err) {
|
||
console.error("데이터 로드 실패:", err);
|
||
setError("데이터를 불러오는 중 오류가 발생했습니다.");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
loadData();
|
||
}, [layoutId]);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
|
||
<div className="mt-2 text-sm text-gray-600">3D 장면 로딩 중...</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<div className="mb-2 text-4xl">⚠️</div>
|
||
<div className="text-sm font-medium text-gray-600">{error}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (placements.length === 0) {
|
||
return (
|
||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<div className="mb-2 text-4xl">📦</div>
|
||
<div className="text-sm font-medium text-gray-600">배치된 자재가 없습니다</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="relative h-full w-full">
|
||
{/* 3D 캔버스 */}
|
||
<Yard3DCanvas
|
||
placements={placements}
|
||
selectedPlacementId={selectedPlacement?.id || null}
|
||
onPlacementClick={handlePlacementClick}
|
||
/>
|
||
|
||
{/* 야드 이름 (좌측 상단) */}
|
||
{layoutName && (
|
||
<div className="absolute top-4 left-4 z-50 rounded-lg border border-gray-300 bg-white px-4 py-2 shadow-lg">
|
||
<h2 className="text-base font-bold text-gray-900">{layoutName}</h2>
|
||
</div>
|
||
)}
|
||
|
||
{/* 선택된 자재 정보 패널 (우측 상단) */}
|
||
{selectedPlacement && (
|
||
<div className="absolute top-4 right-4 z-50 w-64 rounded-lg border border-gray-300 bg-white p-4 shadow-xl">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<h3 className="text-sm font-semibold text-gray-800">
|
||
{selectedPlacement.material_name ? "자재 정보" : "미설정 요소"}
|
||
</h3>
|
||
<button
|
||
onClick={() => {
|
||
setSelectedPlacement(null);
|
||
}}
|
||
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{selectedPlacement.material_name && selectedPlacement.quantity && selectedPlacement.unit ? (
|
||
<div className="space-y-2">
|
||
<div>
|
||
<label className="text-xs font-medium text-gray-500">자재명</label>
|
||
<div className="mt-1 text-sm font-semibold text-gray-900">{selectedPlacement.material_name}</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-xs font-medium text-gray-500">수량</label>
|
||
<div className="mt-1 text-sm font-semibold text-gray-900">
|
||
{selectedPlacement.quantity} {selectedPlacement.unit}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="rounded-lg bg-orange-50 p-3 text-center">
|
||
<div className="mb-2 text-2xl">⚠️</div>
|
||
<div className="text-sm font-medium text-orange-700">데이터 바인딩이</div>
|
||
<div className="text-sm font-medium text-orange-700">설정되지 않았습니다</div>
|
||
<div className="mt-2 text-xs text-orange-600">편집 모드에서 설정해주세요</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|