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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user