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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user