feat: Add multi-table Excel upload functionality

- Implemented new API endpoints for multi-table Excel upload and auto-detection of table chains.
- The GET endpoint `/api/data/multi-table/auto-detect` allows automatic detection of foreign key relationships based on the provided root table.
- The POST endpoint `/api/data/multi-table/upload` handles the upload of multi-table data, including validation and logging of the upload process.
- Updated the frontend to include options for multi-table Excel upload in the button configuration panel and integrated the corresponding action handler.

This feature enhances the data management capabilities by allowing users to upload and manage data across multiple related tables efficiently.
This commit is contained in:
kjs
2026-03-05 19:17:35 +09:00
parent 7e2ae4335e
commit 0e8c68a9ff
11 changed files with 2384 additions and 69 deletions

View File

@@ -1235,6 +1235,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (!leftItem) return;
setIsLoadingRight(true);
setRightData([]);
try {
// detail / join 모두 동일한 필터링 로직 사용
// (차이점: 초기 로드 여부만 다름 - detail은 초기 로드 안 함)
@@ -1343,34 +1344,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode,
additionalJoinColumns: rightJoinColumns, // 🆕 Entity 조인 컬럼 전달
additionalJoinColumns: rightJoinColumns,
});
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
// 추가 dataFilter 적용
let filteredData = result.data || [];
const dataFilter = componentConfig.rightPanel?.dataFilter;
if (dataFilter?.enabled && dataFilter.conditions?.length > 0) {
filteredData = filteredData.filter((item: any) => {
return dataFilter.conditions.every((cond: any) => {
const value = item[cond.column];
const condValue = cond.value;
switch (cond.operator) {
case "equals":
return value === condValue;
case "notEquals":
return value !== condValue;
case "contains":
return String(value).includes(String(condValue));
default:
return true;
}
});
});
}
setRightData(filteredData);
setRightData(result.data || []);
} else {
// 단일키 (하위 호환성) → entityJoinApi 사용으로 전환 (entity 조인 컬럼 지원)
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
@@ -1380,9 +1359,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const leftValue = leftItem[leftColumn];
const { entityJoinApi } = await import("@/lib/api/entityJoin");
// 단일키를 복합키 형식으로 변환
console.log("🔗 [분할패널] 단일키 조건:", { leftColumn, rightColumn, leftValue, rightTableName });
// 단일키를 복합키 형식으로 변환 (entity 컬럼이므로 equals 연산자 필수)
const searchConditions: Record<string, any> = {};
searchConditions[rightColumn] = leftValue;
searchConditions[rightColumn] = { value: leftValue, operator: "equals" };
// Entity 조인 컬럼 추출
const rightJoinColumnsLegacy = extractAdditionalJoinColumns(
@@ -1401,35 +1382,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
additionalJoinColumns: rightJoinColumnsLegacy,
});
let filteredDataLegacy = result.data || [];
// 데이터 필터 적용
const dataFilterLegacy = componentConfig.rightPanel?.dataFilter;
if (dataFilterLegacy?.enabled && dataFilterLegacy.conditions?.length > 0) {
filteredDataLegacy = filteredDataLegacy.filter((item: any) => {
return dataFilterLegacy.conditions.every((cond: any) => {
const value = item[cond.column];
const condValue = cond.value;
switch (cond.operator) {
case "equals":
return value === condValue;
case "notEquals":
return value !== condValue;
case "contains":
return String(value).includes(String(condValue));
default:
return true;
}
});
});
}
setRightData(filteredDataLegacy || []);
setRightData(result.data || []);
}
}
}
} catch (error) {
console.error("우측 데이터 로드 실패:", error);
setRightData([]);
toast({
title: "데이터 로드 실패",
description: "우측 패널 데이터를 불러올 수 없습니다.",