ERP-node/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx

688 lines
28 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.

This file contains Unicode characters that might be confused with other characters. 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 { 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 TableInfo {
table_name: string;
description?: string;
}
interface ColumnInfo {
column_name: string;
data_type?: string;
description?: string;
}
interface HierarchyConfigPanelProps {
externalDbConnectionId: number | null;
hierarchyConfig: HierarchyConfig | null;
onHierarchyConfigChange: (config: HierarchyConfig) => void;
availableTables: TableInfo[];
onLoadTables: () => Promise<void>;
onLoadColumns: (tableName: string) => Promise<ColumnInfo[]>;
}
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]: ColumnInfo[] }>({});
// 외부에서 변경된 경우 동기화 및 컬럼 자동 로드
// 외부에서 변경된 경우 동기화 및 컬럼 자동 로드
useEffect(() => {
if (hierarchyConfig) {
setLocalConfig(hierarchyConfig);
// 저장된 설정의 테이블들에 대한 컬럼 자동 로드
const loadSavedColumns = async () => {
const tablesToLoad: string[] = [];
// 창고 테이블
if (hierarchyConfig.warehouse?.tableName) {
tablesToLoad.push(hierarchyConfig.warehouse.tableName);
}
// 계층 레벨 테이블들
hierarchyConfig.levels?.forEach((level) => {
if (level.tableName) {
tablesToLoad.push(level.tableName);
}
});
// 자재 테이블
if (hierarchyConfig.material?.tableName) {
tablesToLoad.push(hierarchyConfig.material.tableName);
}
// 중복 제거 후 로드
const uniqueTables = [...new Set(tablesToLoad)];
for (const tableName of uniqueTables) {
if (!columnsCache[tableName]) {
try {
const columns = await onLoadColumns(tableName);
setColumnsCache((prev) => ({ ...prev, [tableName]: columns }));
} catch (error) {
console.error(`컬럼 로드 실패 (${tableName}):`, error);
}
}
}
};
if (externalDbConnectionId) {
loadSavedColumns();
}
}
}, [hierarchyConfig, externalDbConnectionId]);
// 테이블 선택 시 컬럼 로드
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.table_name} value={table.table_name} className="text-[10px]">
<div className="flex flex-col">
<span>{table.table_name}</span>
{table.description && (
<span className="text-muted-foreground text-[9px]">{table.description}</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{!localConfig.warehouse?.tableName && (
<p className="text-muted-foreground mt-1 text-[9px]">
"설정 적용"
</p>
)}
</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.column_name} value={col.column_name} className="text-[10px]">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[8px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-[10px]">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[8px]">{col.description}</span>
)}
</div>
</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.table_name} value={table.table_name} className="text-xs">
<div className="flex flex-col">
<span>{table.table_name}</span>
{table.description && (
<span className="text-muted-foreground text-[10px]">{table.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.table_name} value={table.table_name} className="text-xs">
<div className="flex flex-col">
<span>{table.table_name}</span>
{table.description && (
<span className="text-muted-foreground text-[10px]">{table.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name} value={col.column_name} className="text-xs">
<div className="flex flex-col">
<span>{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[9px]">{col.description}</span>
)}
</div>
</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.column_name);
const isSelected = !!displayItem;
return (
<div key={col.column_name} 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.column_name, label: col.column_name }]
: currentDisplay.filter((d) => d.column !== col.column_name);
handleMaterialChange("displayColumns", newDisplay);
}}
className="h-3 w-3 shrink-0"
/>
<div className="flex w-24 shrink-0 flex-col">
<span className="text-[10px]">{col.column_name}</span>
{col.description && (
<span className="text-muted-foreground text-[8px]">{col.description}</span>
)}
</div>
{isSelected && (
<Input
value={displayItem?.label ?? ""}
onChange={(e) => {
const currentDisplay = localConfig.material?.displayColumns || [];
const newDisplay = currentDisplay.map((d) =>
d.column === col.column_name ? { ...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>
);
}