ERP-node/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx

283 lines
10 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 { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Plus, Check, Trash2 } from "lucide-react";
import YardLayoutCreateModal from "./yard-3d/YardLayoutCreateModal";
import DigitalTwinEditor from "./yard-3d/DigitalTwinEditor";
import DigitalTwinViewer from "./yard-3d/DigitalTwinViewer";
import { yardLayoutApi } from "@/lib/api/yardLayoutApi";
import type { YardManagementConfig } from "../types";
interface YardLayout {
id: number;
name: string;
description: string;
placement_count: number;
created_at: string;
updated_at: string;
}
interface YardManagement3DWidgetProps {
isEditMode?: boolean;
config?: YardManagementConfig;
onConfigChange?: (config: YardManagementConfig) => void;
}
export default function YardManagement3DWidget({
isEditMode = false,
config,
onConfigChange,
}: YardManagement3DWidgetProps) {
const [layouts, setLayouts] = useState<YardLayout[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingLayout, setEditingLayout] = useState<YardLayout | null>(null);
const [deleteLayoutId, setDeleteLayoutId] = useState<number | null>(null);
// 레이아웃 목록 로드
const loadLayouts = async () => {
try {
setIsLoading(true);
const response = await yardLayoutApi.getAllLayouts();
if (response.success) {
setLayouts(response.data as YardLayout[]);
}
} catch (error) {
console.error("야드 레이아웃 목록 조회 실패:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (isEditMode) {
loadLayouts();
}
}, [isEditMode]);
// 레이아웃 목록이 로드되었고, 설정이 없으면 첫 번째 레이아웃 자동 선택
useEffect(() => {
if (isEditMode && layouts.length > 0 && !config?.layoutId && onConfigChange) {
// console.log("🔧 첫 번째 야드 레이아웃 자동 선택:", layouts[0]);
onConfigChange({
layoutId: layouts[0].id,
layoutName: layouts[0].name,
});
}
}, [isEditMode, layouts, config?.layoutId, onConfigChange]);
// 레이아웃 선택 (편집 모드에서만)
const handleSelectLayout = (layout: YardLayout) => {
if (onConfigChange) {
onConfigChange({
layoutId: layout.id,
layoutName: layout.name,
});
}
};
// 새 레이아웃 생성
const handleCreateLayout = async (name: string) => {
try {
const response = await yardLayoutApi.createLayout({ name });
if (response.success) {
await loadLayouts();
setIsCreateModalOpen(false);
setEditingLayout(response.data as YardLayout);
}
} catch (error) {
console.error("야드 레이아웃 생성 실패:", error);
throw error;
}
};
// 편집 완료
const handleEditComplete = () => {
if (editingLayout && onConfigChange) {
onConfigChange({
layoutId: editingLayout.id,
layoutName: editingLayout.name,
});
}
setEditingLayout(null);
loadLayouts();
};
// 레이아웃 삭제
const handleDeleteLayout = async () => {
if (!deleteLayoutId) return;
try {
const response = await yardLayoutApi.deleteLayout(deleteLayoutId);
if (response.success) {
// 삭제된 레이아웃이 현재 선택된 레이아웃이면 설정 초기화
if (config?.layoutId === deleteLayoutId && onConfigChange) {
onConfigChange({ layoutId: 0, layoutName: "" });
}
await loadLayouts();
}
} catch (error) {
console.error("레이아웃 삭제 실패:", error);
} finally {
setDeleteLayoutId(null);
}
};
// 편집 모드: 편집 중인 경우 DigitalTwinEditor 표시
if (isEditMode && editingLayout) {
return (
<div className="h-full w-full">
<DigitalTwinEditor
layoutId={editingLayout.id}
layoutName={editingLayout.name}
onBack={handleEditComplete}
/>
</div>
);
}
// 편집 모드: 레이아웃 선택 UI
if (isEditMode) {
return (
<div className="widget-interactive-area flex h-full w-full flex-col bg-background">
<div className="flex items-center justify-between border-b p-4">
<div>
<h3 className="text-sm font-semibold text-foreground"> </h3>
<p className="mt-1 text-xs text-muted-foreground">
{config?.layoutName ? `선택됨: ${config.layoutName}` : "표시할 야드 레이아웃을 선택하세요"}
</p>
</div>
<Button onClick={() => setIsCreateModalOpen(true)} size="sm">
<Plus className="mr-1 h-4 w-4" />
</Button>
</div>
<div className="flex-1 overflow-auto p-4">
{isLoading ? (
<div className="flex h-full items-center justify-center">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
) : layouts.length === 0 ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="mb-2 text-4xl">🏗</div>
<div className="text-sm text-foreground"> </div>
<div className="mt-1 text-xs text-muted-foreground"> </div>
</div>
</div>
) : (
<div className="grid gap-3">
{layouts.map((layout) => (
<div
key={layout.id}
className={`rounded-lg border p-3 transition-all ${
config?.layoutId === layout.id ? "border-primary bg-primary/10" : "border-border bg-background"
}`}
>
<div className="flex items-start justify-between gap-3">
<button onClick={() => handleSelectLayout(layout)} className="flex-1 text-left hover:opacity-80">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{layout.name}</span>
{config?.layoutId === layout.id && <Check className="h-4 w-4 text-primary" />}
</div>
{layout.description && <p className="mt-1 text-xs text-muted-foreground">{layout.description}</p>}
<div className="mt-2 text-xs text-muted-foreground"> : {layout.placement_count}</div>
</button>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
setEditingLayout(layout);
}}
>
</Button>
<Button
variant="outline"
size="sm"
className="text-destructive hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
setDeleteLayoutId(layout.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 생성 모달 */}
<YardLayoutCreateModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onCreate={handleCreateLayout}
/>
{/* 삭제 확인 모달 */}
<Dialog
open={deleteLayoutId !== null}
onOpenChange={(open) => {
if (!open) setDeleteLayoutId(null);
}}
>
<DialogContent onPointerDown={(e) => e.stopPropagation()} className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-foreground">
?
<br />
.
<br />
<span className="font-semibold text-destructive"> .</span>
</p>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setDeleteLayoutId(null)}>
</Button>
<Button onClick={handleDeleteLayout} className="bg-destructive hover:bg-destructive/90">
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}
// 뷰 모드: 선택된 레이아웃의 3D 뷰어 표시
if (!config?.layoutId) {
console.warn("⚠️ 야드관리 위젯: layoutId가 설정되지 않음", { config, isEditMode });
return (
<div className="flex h-full w-full items-center justify-center bg-muted">
<div className="text-center">
<div className="mb-2 text-4xl">🏗</div>
<div className="text-sm font-medium text-foreground"> </div>
<div className="mt-1 text-xs text-muted-foreground"> </div>
<div className="mt-2 text-xs text-destructive">
디버그: config={JSON.stringify(config)}
</div>
</div>
</div>
);
}
// 선택된 레이아웃의 디지털 트윈 뷰어 표시
return (
<div className="h-full w-full">
<DigitalTwinViewer layoutId={config.layoutId} />
</div>
);
}