N-Level 계층 구조 및 공간 종속성 시스템 구현
This commit is contained in:
@@ -49,6 +49,8 @@ interface ColumnInfo {
|
||||
column_name: string;
|
||||
data_type?: string;
|
||||
description?: string;
|
||||
// 백엔드에서 내려주는 Primary Key 플래그 ("YES"/"NO" 또는 boolean)
|
||||
is_primary_key?: string | boolean;
|
||||
}
|
||||
|
||||
interface HierarchyConfigPanelProps {
|
||||
@@ -78,6 +80,18 @@ export default function HierarchyConfigPanel({
|
||||
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(() => {
|
||||
@@ -111,7 +125,8 @@ export default function HierarchyConfigPanel({
|
||||
if (!columnsCache[tableName]) {
|
||||
try {
|
||||
const columns = await onLoadColumns(tableName);
|
||||
setColumnsCache((prev) => ({ ...prev, [tableName]: columns }));
|
||||
const normalized = normalizeColumns(columns);
|
||||
setColumnsCache((prev) => ({ ...prev, [tableName]: normalized }));
|
||||
} catch (error) {
|
||||
console.error(`컬럼 로드 실패 (${tableName}):`, error);
|
||||
}
|
||||
@@ -125,21 +140,83 @@ export default function HierarchyConfigPanel({
|
||||
}
|
||||
}, [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) => {
|
||||
if (columnsCache[tableName]) {
|
||||
return; // 이미 로드된 경우 스킵
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingColumns(true);
|
||||
try {
|
||||
const columns = await onLoadColumns(tableName);
|
||||
setColumnsCache((prev) => ({ ...prev, [tableName]: columns }));
|
||||
} catch (error) {
|
||||
console.error("컬럼 로드 실패:", error);
|
||||
} 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;
|
||||
});
|
||||
};
|
||||
|
||||
// 창고 키 변경 (제거됨 - 상위 컴포넌트에서 처리)
|
||||
@@ -271,16 +348,22 @@ export default function HierarchyConfigPanel({
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
@@ -310,6 +393,15 @@ export default function HierarchyConfigPanel({
|
||||
</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>
|
||||
|
||||
@@ -385,16 +477,22 @@ export default function HierarchyConfigPanel({
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
@@ -475,6 +573,13 @@ export default function HierarchyConfigPanel({
|
||||
</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>
|
||||
))}
|
||||
@@ -528,21 +633,27 @@ export default function HierarchyConfigPanel({
|
||||
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>
|
||||
<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>
|
||||
|
||||
@@ -673,6 +784,15 @@ export default function HierarchyConfigPanel({
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user