배치 대략적인 완료
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user