2025-10-17 15:26:21 +09:00
|
|
|
|
"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;
|
2025-10-21 16:45:04 +09:00
|
|
|
|
yard_layout_id?: number;
|
2025-10-20 09:58:51 +09:00
|
|
|
|
material_code?: string | null;
|
|
|
|
|
|
material_name?: string | null;
|
|
|
|
|
|
quantity?: number | null;
|
|
|
|
|
|
unit?: string | null;
|
2025-10-17 15:26:21 +09:00
|
|
|
|
position_x: number;
|
|
|
|
|
|
position_y: number;
|
|
|
|
|
|
position_z: number;
|
|
|
|
|
|
size_x: number;
|
|
|
|
|
|
size_y: number;
|
|
|
|
|
|
size_z: number;
|
|
|
|
|
|
color: string;
|
2025-10-20 09:58:51 +09:00
|
|
|
|
data_source_type?: string | null;
|
2025-10-21 16:45:04 +09:00
|
|
|
|
data_source_config?: Record<string, unknown> | null;
|
|
|
|
|
|
data_binding?: Record<string, unknown> | null;
|
2025-10-17 15:26:21 +09:00
|
|
|
|
status?: string;
|
|
|
|
|
|
memo?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 16:45:04 +09:00
|
|
|
|
interface YardLayout {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
created_at?: string;
|
|
|
|
|
|
updated_at?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 15:26:21 +09:00
|
|
|
|
interface Yard3DViewerProps {
|
|
|
|
|
|
layoutId: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|
|
|
|
|
const [placements, setPlacements] = useState<YardPlacement[]>([]);
|
|
|
|
|
|
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
|
2025-10-17 16:36:51 +09:00
|
|
|
|
const [layoutName, setLayoutName] = useState<string>("");
|
2025-10-17 15:26:21 +09:00
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
2025-10-17 16:23:33 +09:00
|
|
|
|
// 선택 변경 로그
|
|
|
|
|
|
const handlePlacementClick = (placement: YardPlacement | null) => {
|
|
|
|
|
|
console.log("Yard3DViewer - Placement clicked:", placement?.material_name);
|
|
|
|
|
|
setSelectedPlacement(placement);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 선택 상태 변경 감지
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
console.log("selectedPlacement changed:", selectedPlacement?.material_name);
|
|
|
|
|
|
}, [selectedPlacement]);
|
|
|
|
|
|
|
2025-10-17 16:36:51 +09:00
|
|
|
|
// 야드 레이아웃 및 배치 데이터 로드
|
2025-10-17 15:26:21 +09:00
|
|
|
|
useEffect(() => {
|
2025-10-17 16:36:51 +09:00
|
|
|
|
const loadData = async () => {
|
2025-10-17 15:26:21 +09:00
|
|
|
|
try {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setError(null);
|
2025-10-17 16:36:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 야드 레이아웃 정보 조회
|
|
|
|
|
|
const layoutResponse = await yardLayoutApi.getLayoutById(layoutId);
|
|
|
|
|
|
if (layoutResponse.success) {
|
2025-10-21 16:45:04 +09:00
|
|
|
|
const layout = layoutResponse.data as YardLayout;
|
|
|
|
|
|
setLayoutName(layout.name);
|
2025-10-17 16:36:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 배치 데이터 조회
|
|
|
|
|
|
const placementsResponse = await yardLayoutApi.getPlacementsByLayoutId(layoutId);
|
|
|
|
|
|
if (placementsResponse.success) {
|
2025-10-21 16:45:04 +09:00
|
|
|
|
setPlacements(placementsResponse.data as YardPlacement[]);
|
2025-10-17 15:26:21 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
setError("배치 데이터를 불러올 수 없습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2025-10-17 16:36:51 +09:00
|
|
|
|
console.error("데이터 로드 실패:", err);
|
|
|
|
|
|
setError("데이터를 불러오는 중 오류가 발생했습니다.");
|
2025-10-17 15:26:21 +09:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 16:36:51 +09:00
|
|
|
|
loadData();
|
2025-10-17 15:26:21 +09:00
|
|
|
|
}, [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">
|
2025-10-17 15:26:21 +09:00
|
|
|
|
<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>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</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">
|
2025-10-17 15:26:21 +09:00
|
|
|
|
<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>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</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">
|
2025-10-17 15:26:21 +09:00
|
|
|
|
<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>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-17 16:23:33 +09:00
|
|
|
|
<div className="relative h-full w-full">
|
2025-10-17 15:26:21 +09:00
|
|
|
|
{/* 3D 캔버스 */}
|
2025-10-17 16:23:33 +09:00
|
|
|
|
<Yard3DCanvas
|
|
|
|
|
|
placements={placements}
|
|
|
|
|
|
selectedPlacementId={selectedPlacement?.id || null}
|
|
|
|
|
|
onPlacementClick={handlePlacementClick}
|
|
|
|
|
|
/>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
|
2025-10-17 16:36:51 +09:00
|
|
|
|
{/* 야드 이름 (좌측 상단) */}
|
|
|
|
|
|
{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>
|
2025-10-17 16:36:51 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 선택된 자재 정보 패널 (우측 상단) */}
|
2025-10-17 15:26:21 +09:00
|
|
|
|
{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">
|
2025-10-17 16:23:33 +09:00
|
|
|
|
<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">
|
2025-10-20 09:58:51 +09:00
|
|
|
|
{selectedPlacement.material_name ? "자재 정보" : "미설정 요소"}
|
|
|
|
|
|
</h3>
|
2025-10-17 16:23:33 +09:00
|
|
|
|
<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"
|
2025-10-17 16:23:33 +09:00
|
|
|
|
>
|
|
|
|
|
|
✕
|
|
|
|
|
|
</button>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-20 09:58:51 +09:00
|
|
|
|
{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>
|
2025-10-20 09:58:51 +09:00
|
|
|
|
</div>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
|
2025-10-20 09:58:51 +09:00
|
|
|
|
<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">
|
2025-10-20 09:58:51 +09:00
|
|
|
|
{selectedPlacement.quantity} {selectedPlacement.unit}
|
|
|
|
|
|
</div>
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-20 09:58:51 +09:00
|
|
|
|
) : (
|
2025-10-29 17:53:03 +09:00
|
|
|
|
<div className="rounded-lg bg-warning/10 p-3 text-center">
|
2025-10-20 09:58:51 +09:00
|
|
|
|
<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>
|
2025-10-20 09:58:51 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-17 15:26:21 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|