Files
vexplor/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx
2025-11-21 02:25:25 +09:00

560 lines
22 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Loader2, Plus, Trash2, GripVertical } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
// 계층 레벨 설정 인터페이스
export interface HierarchyLevel {
level: number;
name: string;
tableName: string;
keyColumn: string;
nameColumn: string;
parentKeyColumn: string;
typeColumn?: string;
objectTypes: string[];
}
// 전체 계층 구조 설정
export interface HierarchyConfig {
warehouseKey: string; // 이 레이아웃이 속한 창고 키 (예: "DY99")
warehouse?: {
tableName: string; // 창고 테이블명 (예: "MWARMA")
keyColumn: string;
nameColumn: string;
};
levels: HierarchyLevel[];
material?: {
tableName: string;
keyColumn: string;
locationKeyColumn: string;
layerColumn?: string;
quantityColumn?: string;
displayColumns?: Array<{ column: string; label: string }>; // 우측 패널에 표시할 컬럼들 (컬럼명 + 표시명)
};
}
interface HierarchyConfigPanelProps {
externalDbConnectionId: number | null;
hierarchyConfig: HierarchyConfig | null;
onHierarchyConfigChange: (config: HierarchyConfig) => void;
availableTables: string[];
onLoadTables: () => Promise<void>;
onLoadColumns: (tableName: string) => Promise<string[]>;
}
export default function HierarchyConfigPanel({
externalDbConnectionId,
hierarchyConfig,
onHierarchyConfigChange,
availableTables,
onLoadTables,
onLoadColumns,
}: HierarchyConfigPanelProps) {
const [localConfig, setLocalConfig] = useState<HierarchyConfig>(
hierarchyConfig || {
warehouseKey: "",
levels: [],
},
);
const [loadingColumns, setLoadingColumns] = useState(false);
const [columnsCache, setColumnsCache] = useState<{ [tableName: string]: string[] }>({});
// 외부에서 변경된 경우 동기화
useEffect(() => {
if (hierarchyConfig) {
setLocalConfig(hierarchyConfig);
}
}, [hierarchyConfig]);
// 테이블 선택 시 컬럼 로드
const handleTableChange = async (tableName: string, type: "warehouse" | "material" | number) => {
if (columnsCache[tableName]) return; // 이미 로드된 경우 스킵
setLoadingColumns(true);
try {
const columns = await onLoadColumns(tableName);
setColumnsCache((prev) => ({ ...prev, [tableName]: columns }));
} catch (error) {
console.error("컬럼 로드 실패:", error);
} finally {
setLoadingColumns(false);
}
};
// 창고 키 변경 (제거됨 - 상위 컴포넌트에서 처리)
// 레벨 추가
const handleAddLevel = () => {
const maxLevel = localConfig.levels.length > 0 ? Math.max(...localConfig.levels.map((l) => l.level)) : 0;
const newLevel: HierarchyLevel = {
level: maxLevel + 1,
name: `레벨 ${maxLevel + 1}`,
tableName: "",
keyColumn: "",
nameColumn: "",
parentKeyColumn: "",
objectTypes: [],
};
const newConfig = {
...localConfig,
levels: [...localConfig.levels, newLevel],
};
setLocalConfig(newConfig);
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
};
// 레벨 삭제
const handleRemoveLevel = (level: number) => {
const newConfig = {
...localConfig,
levels: localConfig.levels.filter((l) => l.level !== level),
};
setLocalConfig(newConfig);
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
};
// 레벨 설정 변경
const handleLevelChange = (level: number, field: keyof HierarchyLevel, value: any) => {
const newConfig = {
...localConfig,
levels: localConfig.levels.map((l) => (l.level === level ? { ...l, [field]: value } : l)),
};
setLocalConfig(newConfig);
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
};
// 자재 설정 변경
const handleMaterialChange = (field: keyof NonNullable<HierarchyConfig["material"]>, value: string) => {
const newConfig = {
...localConfig,
material: {
...localConfig.material,
[field]: value,
} as NonNullable<HierarchyConfig["material"]>,
};
setLocalConfig(newConfig);
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
};
// 창고 설정 변경
const handleWarehouseChange = (field: keyof NonNullable<HierarchyConfig["warehouse"]>, value: string) => {
const newWarehouse = {
...localConfig.warehouse,
[field]: value,
} as NonNullable<HierarchyConfig["warehouse"]>;
setLocalConfig({ ...localConfig, warehouse: newWarehouse });
};
// 설정 적용
const handleApplyConfig = () => {
onHierarchyConfigChange(localConfig);
};
if (!externalDbConnectionId) {
return <div className="text-muted-foreground p-4 text-center text-sm"> DB를 </div>;
}
return (
<div className="space-y-4">
{/* 창고 설정 */}
<Card>
<CardHeader className="p-4 pb-3">
<CardTitle className="text-sm"> </CardTitle>
<CardDescription className="text-[10px]"> </CardDescription>
</CardHeader>
<CardContent className="space-y-2 p-4 pt-0">
{/* 창고 테이블 선택 */}
<div>
<Label className="text-[10px]"></Label>
<Select
value={localConfig.warehouse?.tableName || ""}
onValueChange={async (value) => {
handleWarehouseChange("tableName", value);
await handleTableChange(value, "warehouse");
}}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="테이블 선택..." />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table} value={table} className="text-[10px]">
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 창고 컬럼 매핑 */}
{localConfig.warehouse?.tableName && columnsCache[localConfig.warehouse.tableName] && (
<div className="space-y-2">
<div>
<Label className="text-[10px]">ID </Label>
<Select
value={localConfig.warehouse.keyColumn || ""}
onValueChange={(value) => handleWarehouseChange("keyColumn", value)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="선택..." />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.warehouse.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-[10px]">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localConfig.warehouse.nameColumn || ""}
onValueChange={(value) => handleWarehouseChange("nameColumn", value)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="선택..." />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.warehouse.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-[10px]">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
</CardContent>
</Card>
{/* 계층 레벨 목록 */}
<Card>
<CardHeader className="p-4 pb-3">
<CardTitle className="text-sm"> </CardTitle>
<CardDescription className="text-[10px]">, </CardDescription>
</CardHeader>
<CardContent className="space-y-3 p-4 pt-0">
{localConfig.levels.length === 0 && (
<div className="text-muted-foreground py-6 text-center text-xs"> </div>
)}
{localConfig.levels.map((level) => (
<Card key={level.level} className="border-muted">
<CardHeader className="flex flex-row items-center justify-between p-3">
<div className="flex items-center gap-2">
<GripVertical className="text-muted-foreground h-4 w-4" />
<Input
value={level.name}
onChange={(e) => handleLevelChange(level.level, "name", e.target.value)}
className="h-7 w-32 text-xs"
placeholder="레벨명"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveLevel(level.level)}
className="h-7 w-7 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent className="space-y-2 p-3 pt-0">
<div>
<Label className="text-[10px]"></Label>
<Select
value={level.tableName}
onValueChange={(val) => {
handleLevelChange(level.level, "tableName", val);
handleTableChange(val, level.level);
}}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table} value={table} className="text-xs">
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{level.tableName && columnsCache[level.tableName] && (
<>
<div>
<Label className="text-[10px]">ID </Label>
<Select
value={level.keyColumn}
onValueChange={(val) => handleLevelChange(level.level, "keyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={level.nameColumn}
onValueChange={(val) => handleLevelChange(level.level, "nameColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={level.parentKeyColumn}
onValueChange={(val) => handleLevelChange(level.level, "parentKeyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="부모 키 컬럼" />
</SelectTrigger>
<SelectContent>
{columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={level.typeColumn || "__none__"}
onValueChange={(val) =>
handleLevelChange(level.level, "typeColumn", val === "__none__" ? undefined : val)
}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="타입 컬럼 (선택)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
{columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)}
</CardContent>
</Card>
))}
<Button variant="outline" size="sm" onClick={handleAddLevel} className="h-8 w-full text-xs">
<Plus className="mr-1 h-3 w-3" />
</Button>
</CardContent>
</Card>
{/* 자재 설정 */}
<Card>
<CardHeader className="p-4 pb-3">
<CardTitle className="text-sm"> </CardTitle>
<CardDescription className="text-[10px]"> </CardDescription>
</CardHeader>
<CardContent className="space-y-3 p-4 pt-0">
<div>
<Label className="text-[10px]"></Label>
<Select
value={localConfig.material?.tableName || ""}
onValueChange={(val) => {
handleMaterialChange("tableName", val);
handleTableChange(val, "material");
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table} value={table} className="text-xs">
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{localConfig.material?.tableName && columnsCache[localConfig.material.tableName] && (
<>
<div>
<Label className="text-[10px]">ID </Label>
<Select
value={localConfig.material.keyColumn}
onValueChange={(val) => handleMaterialChange("keyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localConfig.material.locationKeyColumn}
onValueChange={(val) => handleMaterialChange("locationKeyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={localConfig.material.layerColumn || "__none__"}
onValueChange={(val) => handleMaterialChange("layerColumn", val === "__none__" ? undefined : val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="레이어 컬럼" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={localConfig.material.quantityColumn || "__none__"}
onValueChange={(val) => handleMaterialChange("quantityColumn", val === "__none__" ? undefined : val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="수량 컬럼" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator className="my-3" />
{/* 표시 컬럼 선택 */}
<div>
<Label className="text-[10px]"> </Label>
<p className="text-muted-foreground mb-2 text-[9px]">
</p>
<div className="max-h-60 space-y-2 overflow-y-auto rounded border p-2">
{columnsCache[localConfig.material.tableName].map((col) => {
const displayItem = localConfig.material?.displayColumns?.find((d) => d.column === col);
const isSelected = !!displayItem;
return (
<div key={col} className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
const currentDisplay = localConfig.material?.displayColumns || [];
const newDisplay = e.target.checked
? [...currentDisplay, { column: col, label: col }]
: currentDisplay.filter((d) => d.column !== col);
handleMaterialChange("displayColumns", newDisplay);
}}
className="h-3 w-3 shrink-0"
/>
<span className="w-20 shrink-0 text-[10px]">{col}</span>
{isSelected && (
<Input
value={displayItem?.label || col}
onChange={(e) => {
const currentDisplay = localConfig.material?.displayColumns || [];
const newDisplay = currentDisplay.map((d) =>
d.column === col ? { ...d, label: e.target.value } : d,
);
handleMaterialChange("displayColumns", newDisplay);
}}
placeholder="표시명 입력..."
className="h-6 flex-1 text-[10px]"
/>
)}
</div>
);
})}
</div>
</div>
</>
)}
</CardContent>
</Card>
{/* 적용 버튼 */}
<div className="flex justify-end">
<Button onClick={handleApplyConfig} className="h-10 gap-2 text-sm font-medium">
</Button>
</div>
</div>
);
}