819 lines
33 KiB
TypeScript
819 lines
33 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 TableInfo {
|
||
table_name: string;
|
||
description?: string;
|
||
}
|
||
|
||
interface ColumnInfo {
|
||
column_name: string;
|
||
data_type?: string;
|
||
description?: string;
|
||
// 백엔드에서 내려주는 Primary Key 플래그 ("YES"/"NO" 또는 boolean)
|
||
is_primary_key?: string | boolean;
|
||
}
|
||
|
||
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[] }>({});
|
||
|
||
// 동일한 column_name 이 여러 번 내려오는 경우(조인 중복 등) 제거
|
||
const normalizeColumns = (columns: ColumnInfo[]): ColumnInfo[] => {
|
||
const map = new Map<string, ColumnInfo>();
|
||
for (const col of columns) {
|
||
const key = col.column_name;
|
||
if (!map.has(key)) {
|
||
map.set(key, col);
|
||
}
|
||
}
|
||
return Array.from(map.values());
|
||
};
|
||
|
||
// 외부에서 변경된 경우 동기화 및 컬럼 자동 로드
|
||
// 외부에서 변경된 경우 동기화 및 컬럼 자동 로드
|
||
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)];
|
||
const tablesToFetch = uniqueTables.filter((tableName) => !columnsCache[tableName]);
|
||
|
||
if (tablesToFetch.length === 0) {
|
||
return;
|
||
}
|
||
|
||
setLoadingColumns(true);
|
||
try {
|
||
await Promise.all(
|
||
tablesToFetch.map(async (tableName) => {
|
||
try {
|
||
const columns = await onLoadColumns(tableName);
|
||
const normalized = normalizeColumns(columns);
|
||
setColumnsCache((prev) => ({ ...prev, [tableName]: normalized }));
|
||
} catch (error) {
|
||
console.error(`컬럼 로드 실패 (${tableName}):`, error);
|
||
}
|
||
}),
|
||
);
|
||
} finally {
|
||
setLoadingColumns(false);
|
||
}
|
||
};
|
||
|
||
if (externalDbConnectionId) {
|
||
loadSavedColumns();
|
||
}
|
||
}
|
||
}, [hierarchyConfig, externalDbConnectionId]);
|
||
|
||
// 지정된 컬럼이 Primary Key 인지 여부
|
||
const isPrimaryKey = (col: ColumnInfo): boolean => {
|
||
if (col.is_primary_key === true) return true;
|
||
if (typeof col.is_primary_key === "string") {
|
||
const v = col.is_primary_key.toUpperCase();
|
||
return v === "YES" || v === "Y" || v === "TRUE" || v === "PK";
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// 테이블 선택 시 컬럼 로드 + PK 기반 기본값 설정
|
||
const handleTableChange = async (tableName: string, type: "warehouse" | "material" | number) => {
|
||
let loadedColumns = columnsCache[tableName];
|
||
|
||
// 아직 캐시에 없으면 먼저 컬럼 조회
|
||
if (!loadedColumns) {
|
||
setLoadingColumns(true);
|
||
try {
|
||
const fetched = await onLoadColumns(tableName);
|
||
loadedColumns = normalizeColumns(fetched);
|
||
setColumnsCache((prev) => ({ ...prev, [tableName]: loadedColumns! }));
|
||
} catch (error) {
|
||
console.error("컬럼 로드 실패:", error);
|
||
loadedColumns = [];
|
||
} finally {
|
||
setLoadingColumns(false);
|
||
}
|
||
}
|
||
|
||
const columns = loadedColumns || [];
|
||
|
||
// PK 기반으로 keyColumn 기본값 자동 설정 (이미 값이 있으면 건드리지 않음)
|
||
// PK 정보가 없으면 첫 번째 컬럼을 기본값으로 사용
|
||
setLocalConfig((prev) => {
|
||
const next = { ...prev };
|
||
const primaryColumns = columns.filter((col) => isPrimaryKey(col));
|
||
const pkName = (primaryColumns[0] || columns[0])?.column_name;
|
||
|
||
if (!pkName) {
|
||
return next;
|
||
}
|
||
|
||
if (type === "warehouse") {
|
||
const wh = {
|
||
...(next.warehouse || { tableName }),
|
||
tableName: next.warehouse?.tableName || tableName,
|
||
};
|
||
if (!wh.keyColumn) {
|
||
wh.keyColumn = pkName;
|
||
}
|
||
next.warehouse = wh;
|
||
} else if (type === "material") {
|
||
const material = {
|
||
...(next.material || { tableName }),
|
||
tableName: next.material?.tableName || tableName,
|
||
};
|
||
if (!material.keyColumn) {
|
||
material.keyColumn = pkName;
|
||
}
|
||
next.material = material as NonNullable<HierarchyConfig["material"]>;
|
||
} else if (typeof type === "number") {
|
||
// 계층 레벨
|
||
next.levels = next.levels.map((lvl) => {
|
||
if (lvl.level !== type) return lvl;
|
||
const updated: HierarchyLevel = {
|
||
...lvl,
|
||
tableName: lvl.tableName || tableName,
|
||
};
|
||
if (!updated.keyColumn) {
|
||
updated.keyColumn = pkName;
|
||
}
|
||
return updated;
|
||
});
|
||
}
|
||
|
||
return next;
|
||
});
|
||
};
|
||
|
||
// 창고 키 변경 (제거됨 - 상위 컴포넌트에서 처리)
|
||
|
||
// 레벨 추가
|
||
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) => {
|
||
const pk = isPrimaryKey(col);
|
||
return (
|
||
<SelectItem key={col.column_name} value={col.column_name} className="text-[10px]">
|
||
<div className="flex flex-col">
|
||
<span>
|
||
{col.column_name}
|
||
{pk && <span className="text-amber-500 ml-1 text-[8px]">PK</span>}
|
||
</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>
|
||
)}
|
||
|
||
{localConfig.warehouse?.tableName &&
|
||
!columnsCache[localConfig.warehouse.tableName] &&
|
||
loadingColumns && (
|
||
<div className="flex items-center gap-2 pt-2 text-[10px] text-muted-foreground">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
<span>컬럼 정보를 불러오는 중입니다...</span>
|
||
</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) => {
|
||
const pk = isPrimaryKey(col);
|
||
return (
|
||
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||
<div className="flex flex-col">
|
||
<span>
|
||
{col.column_name}
|
||
{pk && <span className="text-amber-500 ml-1 text-[9px]">PK</span>}
|
||
</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>
|
||
</>
|
||
)}
|
||
|
||
{level.tableName && !columnsCache[level.tableName] && loadingColumns && (
|
||
<div className="flex items-center gap-2 pt-2 text-[10px] text-muted-foreground">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
<span>컬럼 정보를 불러오는 중입니다...</span>
|
||
</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) => {
|
||
const pk = isPrimaryKey(col);
|
||
return (
|
||
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||
<div className="flex flex-col">
|
||
<span>
|
||
{col.column_name}
|
||
{pk && <span className="text-amber-500 ml-1 text-[9px]">PK</span>}
|
||
</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>
|
||
</>
|
||
)}
|
||
|
||
{localConfig.material?.tableName &&
|
||
!columnsCache[localConfig.material.tableName] &&
|
||
loadingColumns && (
|
||
<div className="flex items-center gap-2 pt-2 text-[10px] text-muted-foreground">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
<span>컬럼 정보를 불러오는 중입니다...</span>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 적용 버튼 */}
|
||
<div className="flex justify-end">
|
||
<Button onClick={handleApplyConfig} className="h-10 gap-2 text-sm font-medium">
|
||
설정 적용
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|