feat: Enhance V2Repeater and configuration panel with source detail auto-fetching

- Added support for automatic fetching of detail rows from the master data in the V2Repeater component, improving data management.
- Introduced a new configuration option in the V2RepeaterConfigPanel to enable source detail auto-fetching, allowing users to specify detail table and foreign key settings.
- Enhanced the V2Repeater component to handle entity joins for loading data, optimizing data retrieval processes.
- Updated the V2RepeaterProps and V2RepeaterConfig interfaces to include new properties for grouped data and source detail configuration, ensuring type safety and clarity in component usage.
- Improved logging for data loading processes to provide better insights during development and debugging.
This commit is contained in:
kjs
2026-02-28 14:33:18 +09:00
parent b1831ada04
commit e16d76936b
11 changed files with 858 additions and 200 deletions

View File

@@ -48,6 +48,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
onRowClick,
className,
formData: parentFormData,
groupedData,
...restProps
}) => {
// componentId 결정: 직접 전달 또는 component 객체에서 추출
@@ -419,65 +420,113 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
fkValue,
});
const response = await apiClient.post(
`/table-management/tables/${config.mainTableName}/data`,
{
let rows: any[] = [];
const useEntityJoinForLoad = config.sourceDetailConfig?.useEntityJoin;
if (useEntityJoinForLoad) {
// 엔티티 조인을 사용하여 데이터 로드 (part_code → item_info 자동 조인)
const searchParam = JSON.stringify({ [config.foreignKeyColumn!]: fkValue });
const params: Record<string, any> = {
page: 1,
size: 1000,
dataFilter: {
enabled: true,
filters: [{ columnName: config.foreignKeyColumn, operator: "equals", value: fkValue }],
},
autoFilter: true,
search: searchParam,
enableEntityJoin: true,
autoFilter: JSON.stringify({ enabled: true }),
};
const addJoinCols = config.sourceDetailConfig?.additionalJoinColumns;
if (addJoinCols && addJoinCols.length > 0) {
params.additionalJoinColumns = JSON.stringify(addJoinCols);
}
);
const response = await apiClient.get(
`/table-management/tables/${config.mainTableName}/data-with-joins`,
{ params }
);
const resultData = response.data?.data;
const rawRows = Array.isArray(resultData)
? resultData
: resultData?.data || resultData?.rows || [];
// 엔티티 조인 시 참조 테이블에 중복 레코드가 있으면 행이 늘어날 수 있으므로 id 기준 중복 제거
const seenIds = new Set<string>();
rows = rawRows.filter((row: any) => {
if (!row.id || seenIds.has(row.id)) return false;
seenIds.add(row.id);
return true;
});
} else {
const response = await apiClient.post(
`/table-management/tables/${config.mainTableName}/data`,
{
page: 1,
size: 1000,
dataFilter: {
enabled: true,
filters: [{ columnName: config.foreignKeyColumn, operator: "equals", value: fkValue }],
},
autoFilter: true,
}
);
rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
}
const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
if (Array.isArray(rows) && rows.length > 0) {
console.log(`✅ [V2Repeater] 기존 데이터 ${rows.length}건 로드 완료`);
console.log(`✅ [V2Repeater] 기존 데이터 ${rows.length}건 로드 완료`, useEntityJoinForLoad ? "(엔티티 조인)" : "");
// isSourceDisplay 컬럼이 있으면 소스 테이블에서 표시 데이터 보강
const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
const sourceTable = config.dataSource?.sourceTable;
const fkColumn = config.dataSource?.foreignKey;
const refKey = config.dataSource?.referenceKey || "id";
// 엔티티 조인 사용 시: columnMapping으로 _display_ 필드 보강
const columnMapping = config.sourceDetailConfig?.columnMapping;
if (useEntityJoinForLoad && columnMapping) {
const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
rows.forEach((row: any) => {
sourceDisplayColumns.forEach((col) => {
const mappedKey = columnMapping[col.key];
const value = mappedKey ? row[mappedKey] : row[col.key];
row[`_display_${col.key}`] = value ?? "";
});
});
console.log("✅ [V2Repeater] 엔티티 조인 표시 데이터 보강 완료");
}
if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) {
try {
const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean);
const uniqueValues = [...new Set(fkValues)];
// isSourceDisplay 컬럼이 있으면 소스 테이블에서 표시 데이터 보강 (엔티티 조인 미사용 시)
if (!useEntityJoinForLoad) {
const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
const sourceTable = config.dataSource?.sourceTable;
const fkColumn = config.dataSource?.foreignKey;
const refKey = config.dataSource?.referenceKey || "id";
if (uniqueValues.length > 0) {
// FK 값 기반으로 소스 테이블에서 해당 레코드만 조회
const sourcePromises = uniqueValues.map((val) =>
apiClient.post(`/table-management/tables/${sourceTable}/data`, {
page: 1, size: 1,
search: { [refKey]: val },
autoFilter: true,
}).then((r) => r.data?.data?.data || r.data?.data?.rows || [])
.catch(() => [])
);
const sourceResults = await Promise.all(sourcePromises);
const sourceMap = new Map<string, any>();
sourceResults.flat().forEach((sr: any) => {
if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr);
});
if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) {
try {
const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean);
const uniqueValues = [...new Set(fkValues)];
// 각 행에 소스 테이블의 표시 데이터 병합
rows.forEach((row: any) => {
const sourceRecord = sourceMap.get(String(row[fkColumn]));
if (sourceRecord) {
sourceDisplayColumns.forEach((col) => {
const displayValue = sourceRecord[col.key] ?? null;
row[col.key] = displayValue;
row[`_display_${col.key}`] = displayValue;
});
}
});
console.log("✅ [V2Repeater] 소스 테이블 표시 데이터 보강 완료");
if (uniqueValues.length > 0) {
const sourcePromises = uniqueValues.map((val) =>
apiClient.post(`/table-management/tables/${sourceTable}/data`, {
page: 1, size: 1,
search: { [refKey]: val },
autoFilter: true,
}).then((r) => r.data?.data?.data || r.data?.data?.rows || [])
.catch(() => [])
);
const sourceResults = await Promise.all(sourcePromises);
const sourceMap = new Map<string, any>();
sourceResults.flat().forEach((sr: any) => {
if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr);
});
rows.forEach((row: any) => {
const sourceRecord = sourceMap.get(String(row[fkColumn]));
if (sourceRecord) {
sourceDisplayColumns.forEach((col) => {
const displayValue = sourceRecord[col.key] ?? null;
row[col.key] = displayValue;
row[`_display_${col.key}`] = displayValue;
});
}
});
console.log("✅ [V2Repeater] 소스 테이블 표시 데이터 보강 완료");
}
} catch (sourceError) {
console.warn("⚠️ [V2Repeater] 소스 테이블 조회 실패 (표시만 영향):", sourceError);
}
} catch (sourceError) {
console.warn("⚠️ [V2Repeater] 소스 테이블 조회 실패 (표시만 영향):", sourceError);
}
}
@@ -964,8 +1013,113 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
[],
);
// V2Repeater는 자체 데이터 관리 (아이템 선택 모달, useCustomTable 로딩, DataReceiver)를 사용.
// EditModal의 groupedData는 메인 테이블 레코드이므로 V2Repeater에서는 사용하지 않음.
// sourceDetailConfig가 설정되고 groupedData(모달에서 전달된 마스터 데이터)가 있으면
// 마스터의 키를 추출하여 디테일 테이블에서 행을 조회 → 리피터에 자동 세팅
const sourceDetailLoadedRef = useRef(false);
useEffect(() => {
if (sourceDetailLoadedRef.current) return;
if (!groupedData || groupedData.length === 0) return;
if (!config.sourceDetailConfig) return;
const { tableName, foreignKey, parentKey } = config.sourceDetailConfig;
if (!tableName || !foreignKey || !parentKey) return;
const parentKeys = groupedData
.map((row) => row[parentKey])
.filter((v) => v !== undefined && v !== null && v !== "");
if (parentKeys.length === 0) return;
sourceDetailLoadedRef.current = true;
const loadSourceDetails = async () => {
try {
const uniqueKeys = [...new Set(parentKeys)] as string[];
const { useEntityJoin, columnMapping, additionalJoinColumns } = config.sourceDetailConfig!;
let detailRows: any[] = [];
if (useEntityJoin) {
// data-with-joins GET API 사용 (엔티티 조인 자동 적용)
const searchParam = JSON.stringify({ [foreignKey]: uniqueKeys.join("|") });
const params: Record<string, any> = {
page: 1,
size: 9999,
search: searchParam,
enableEntityJoin: true,
autoFilter: JSON.stringify({ enabled: true }),
};
if (additionalJoinColumns && additionalJoinColumns.length > 0) {
params.additionalJoinColumns = JSON.stringify(additionalJoinColumns);
}
const resp = await apiClient.get(`/table-management/tables/${tableName}/data-with-joins`, { params });
const resultData = resp.data?.data;
const rawRows = Array.isArray(resultData)
? resultData
: resultData?.data || resultData?.rows || [];
// 엔티티 조인 시 참조 테이블에 중복 레코드가 있으면 행이 늘어나므로 id 기준 중복 제거
const seenIds = new Set<string>();
detailRows = rawRows.filter((row: any) => {
if (!row.id || seenIds.has(row.id)) return false;
seenIds.add(row.id);
return true;
});
} else {
// 기존 POST API 사용
const resp = await apiClient.post(`/table-management/tables/${tableName}/data`, {
page: 1,
size: 9999,
search: { [foreignKey]: uniqueKeys },
});
const resultData = resp.data?.data;
detailRows = Array.isArray(resultData)
? resultData
: resultData?.data || resultData?.rows || [];
}
if (detailRows.length === 0) {
console.warn("[V2Repeater] sourceDetail 조회 결과 없음:", { tableName, uniqueKeys });
return;
}
console.log("[V2Repeater] sourceDetail 조회 완료:", detailRows.length, "건", useEntityJoin ? "(엔티티 조인)" : "");
// 디테일 행을 리피터 컬럼에 매핑
const newRows = detailRows.map((detail, index) => {
const row: any = { _id: `src_detail_${Date.now()}_${index}` };
for (const col of config.columns) {
if (col.isSourceDisplay) {
// columnMapping이 있으면 조인 alias에서 값 가져오기 (표시용)
const mappedKey = columnMapping?.[col.key];
const value = mappedKey ? detail[mappedKey] : detail[col.key];
row[`_display_${col.key}`] = value ?? "";
// 원본 값도 저장 (DB persist용 - _display_ 접두사 없이)
if (detail[col.key] !== undefined) {
row[col.key] = detail[col.key];
}
} else if (col.autoFill) {
const autoValue = generateAutoFillValueSync(col, index, parentFormData);
row[col.key] = autoValue ?? "";
} else if (col.sourceKey && detail[col.sourceKey] !== undefined) {
row[col.key] = detail[col.sourceKey];
} else if (detail[col.key] !== undefined) {
row[col.key] = detail[col.key];
} else {
row[col.key] = "";
}
}
return row;
});
setData(newRows);
onDataChange?.(newRows);
} catch (error) {
console.error("[V2Repeater] sourceDetail 조회 실패:", error);
}
};
loadSourceDetails();
}, [groupedData, config.sourceDetailConfig, config.columns, generateAutoFillValueSync, parentFormData, onDataChange]);
// parentSequence 컬럼의 부모 필드 값이 변경되면 행 데이터 갱신
useEffect(() => {