feat: Implement entity join functionality in V2Repeater and configuration panel

- Added support for entity joins in the V2Repeater component, allowing for automatic resolution of foreign key references to display data from related tables.
- Introduced a new `resolveEntityJoins` function to handle the fetching and mapping of reference data based on configured entity joins.
- Enhanced the V2RepeaterConfigPanel to manage entity join configurations, including loading available columns and toggling join settings.
- Updated the data handling logic to incorporate mapping rules for incoming data, ensuring that only necessary fields are retained during processing.
- Improved user experience by providing clear logging and feedback during entity join resolution and data mapping operations.
This commit is contained in:
kjs
2026-03-04 21:08:45 +09:00
parent f97edad1ea
commit ac2da7a1d7
10 changed files with 813 additions and 31 deletions

View File

@@ -2132,6 +2132,101 @@ export function TableSectionRenderer({
return Object.values(conditionalTableData).reduce((sum, data) => sum + data.length, 0);
}, [conditionalTableData]);
// 조건부 테이블: 모달 중복 체크용 alreadySelected 구성
// DB에서 로드된 항목은 _sourceData가 없으므로 참조 ID 필드를 기반으로 _sourceData를 생성
const conditionalAlreadySelected = useMemo(() => {
const allItems = Object.values(conditionalTableData).flat();
if (allItems.length === 0) return allItems;
// 참조 ID 필드 탐색 (소스 테이블의 id를 저장하는 디테일 테이블 컬럼)
const referenceIdField = (tableConfig.columns || [])
.map((col) => col.saveConfig?.referenceDisplay?.referenceIdField)
.find(Boolean)
|| (tableConfig.columns || [])
.map((col) => (col as any).dynamicSelectOptions?.rowSelectionMode?.targetIdField)
.find(Boolean);
// sourceField 매핑 수집 (소스 테이블 필드 → 디테일 테이블 필드)
const sourceFieldMap: Record<string, string> = {};
for (const col of tableConfig.columns || []) {
if (col.sourceField && col.sourceField !== col.field) {
sourceFieldMap[col.sourceField] = col.field;
}
}
return allItems.map((item) => {
if (item._sourceData) return item;
// DB에서 로드된 항목: _sourceData 재구성
const sourceData: any = {};
// 참조 ID 필드가 있으면 소스 테이블의 id로 매핑
if (referenceIdField && item[referenceIdField] !== undefined) {
sourceData.id = item[referenceIdField];
}
// sourceField 매핑을 역으로 적용 (디테일 필드 → 소스 필드)
for (const [srcField, detailField] of Object.entries(sourceFieldMap)) {
if (item[detailField] !== undefined) {
sourceData[srcField] = item[detailField];
}
}
// 디테일 테이블의 필드도 소스 데이터에 포함 (동일 필드명인 경우)
for (const col of tableConfig.columns || []) {
if (!col.sourceField && item[col.field] !== undefined) {
sourceData[col.field] = item[col.field];
}
}
return Object.keys(sourceData).length > 0
? { ...item, _sourceData: sourceData }
: item;
});
}, [conditionalTableData, tableConfig.columns]);
// 일반 테이블: 모달 중복 체크용 alreadySelected 구성 (DB 로드 항목 대응)
const normalAlreadySelected = useMemo(() => {
if (tableData.length === 0) return tableData;
const referenceIdField = (tableConfig.columns || [])
.map((col) => col.saveConfig?.referenceDisplay?.referenceIdField)
.find(Boolean)
|| (tableConfig.columns || [])
.map((col) => (col as any).dynamicSelectOptions?.rowSelectionMode?.targetIdField)
.find(Boolean);
const sourceFieldMap: Record<string, string> = {};
for (const col of tableConfig.columns || []) {
if (col.sourceField && col.sourceField !== col.field) {
sourceFieldMap[col.sourceField] = col.field;
}
}
return tableData.map((item) => {
if (item._sourceData) return item;
const sourceData: any = {};
if (referenceIdField && item[referenceIdField] !== undefined) {
sourceData.id = item[referenceIdField];
}
for (const [srcField, detailField] of Object.entries(sourceFieldMap)) {
if (item[detailField] !== undefined) {
sourceData[srcField] = item[detailField];
}
}
for (const col of tableConfig.columns || []) {
if (!col.sourceField && item[col.field] !== undefined) {
sourceData[col.field] = item[col.field];
}
}
return Object.keys(sourceData).length > 0
? { ...item, _sourceData: sourceData }
: item;
});
}, [tableData, tableConfig.columns]);
// ============================================
// 조건부 테이블 렌더링
// ============================================
@@ -2449,7 +2544,7 @@ export function TableSectionRenderer({
multiSelect={multiSelect}
filterCondition={conditionalFilterCondition}
modalTitle={`${effectiveOptions.find((o) => o.value === modalCondition)?.label || modalCondition} - ${modalTitle}`}
alreadySelected={Object.values(conditionalTableData).flat()}
alreadySelected={conditionalAlreadySelected}
uniqueField={tableConfig.saveConfig?.uniqueField}
onSelect={handleConditionalAddItems}
columnLabels={columnLabels}
@@ -2560,7 +2655,7 @@ export function TableSectionRenderer({
multiSelect={multiSelect}
filterCondition={baseFilterCondition}
modalTitle={modalTitle}
alreadySelected={tableData}
alreadySelected={normalAlreadySelected}
uniqueField={tableConfig.saveConfig?.uniqueField}
onSelect={handleAddItems}
columnLabels={columnLabels}