156 lines
4.8 KiB
TypeScript
156 lines
4.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;
|
||
external_material_id: string;
|
||
material_code: string;
|
||
material_name: string;
|
||
quantity: number;
|
||
unit: string;
|
||
position_x: number;
|
||
position_y: number;
|
||
position_z: number;
|
||
size_x: number;
|
||
size_y: number;
|
||
size_z: number;
|
||
color: string;
|
||
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 [isLoading, setIsLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// 배치 데이터 로드
|
||
useEffect(() => {
|
||
const loadPlacements = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
const response = await yardLayoutApi.getPlacementsByLayoutId(layoutId);
|
||
if (response.success) {
|
||
setPlacements(response.data);
|
||
} else {
|
||
setError("배치 데이터를 불러올 수 없습니다.");
|
||
}
|
||
} catch (err) {
|
||
console.error("배치 데이터 로드 실패:", err);
|
||
setError("배치 데이터를 불러오는 중 오류가 발생했습니다.");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
loadPlacements();
|
||
}, [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="flex h-full w-full">
|
||
{/* 3D 캔버스 */}
|
||
<div className="flex-1">
|
||
<Yard3DCanvas
|
||
placements={placements}
|
||
selectedPlacementId={selectedPlacement?.id || null}
|
||
onPlacementClick={setSelectedPlacement}
|
||
/>
|
||
</div>
|
||
|
||
{/* 선택된 자재 정보 패널 (우측) */}
|
||
{selectedPlacement && (
|
||
<div className="w-80 border-l bg-white p-4">
|
||
<div className="mb-4">
|
||
<h3 className="text-sm font-semibold text-gray-700">자재 정보</h3>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div>
|
||
<label className="text-xs text-gray-500">자재 코드</label>
|
||
<div className="mt-1 text-sm font-medium">{selectedPlacement.material_code}</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-xs text-gray-500">자재명</label>
|
||
<div className="mt-1 text-sm font-medium">{selectedPlacement.material_name}</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-xs text-gray-500">수량</label>
|
||
<div className="mt-1 text-sm">
|
||
{selectedPlacement.quantity} {selectedPlacement.unit}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-xs text-gray-500">위치 (X, Y, Z)</label>
|
||
<div className="mt-1 text-sm">
|
||
({selectedPlacement.position_x.toFixed(1)}, {selectedPlacement.position_y.toFixed(1)},{" "}
|
||
{selectedPlacement.position_z.toFixed(1)})
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-xs text-gray-500">크기 (W × H × D)</label>
|
||
<div className="mt-1 text-sm">
|
||
{selectedPlacement.size_x} × {selectedPlacement.size_y} × {selectedPlacement.size_z}
|
||
</div>
|
||
</div>
|
||
|
||
{selectedPlacement.memo && (
|
||
<div>
|
||
<label className="text-xs text-gray-500">메모</label>
|
||
<div className="mt-1 text-sm text-gray-700">{selectedPlacement.memo}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|