diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 26a5d7c4..42ca862f 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -938,8 +938,14 @@ export const ButtonPrimaryComponent: React.FC = ({ effectiveMappingRules = multiTableMappings[0]?.mappingRules || []; } + // 소스 DataProvider에서 엔티티 조인 메타데이터 가져오기 + const entityJoinColumns = sourceProvider?.getEntityJoinColumns?.() || []; + if (entityJoinColumns.length > 0) { + console.log(`🔗 [ButtonPrimary] 엔티티 조인 메타데이터 ${entityJoinColumns.length}개 감지`, entityJoinColumns); + } + const mappedData = sourceData.map((row) => { - const mappedRow = applyMappingRules(row, effectiveMappingRules); + const mappedRow = applyMappingRules(row, effectiveMappingRules, entityJoinColumns); return { ...mappedRow, diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index d1faf281..617d88fe 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -1164,6 +1164,17 @@ export const TableListComponent: React.FC = ({ setSelectedRows(new Set()); setIsAllSelected(false); }, + + getEntityJoinColumns: () => { + return (tableConfig.columns || []) + .filter((col) => col.additionalJoinInfo) + .map((col) => ({ + sourceTable: col.additionalJoinInfo!.sourceTable || tableConfig.selectedTable, + sourceColumn: col.additionalJoinInfo!.sourceColumn, + joinAlias: col.additionalJoinInfo!.joinAlias, + referenceTable: col.additionalJoinInfo!.referenceTable, + })); + }, }; // DataReceivable 인터페이스 구현 diff --git a/frontend/lib/utils/dataMapping.ts b/frontend/lib/utils/dataMapping.ts index 92aa2243..993cb89d 100644 --- a/frontend/lib/utils/dataMapping.ts +++ b/frontend/lib/utils/dataMapping.ts @@ -8,21 +8,27 @@ import type { Condition, TransformFunction, } from "@/types/screen-embedding"; +import type { EntityJoinColumnMeta } from "@/types/data-transfer"; import { logger } from "./logger"; /** * 매핑 규칙 적용 * @param data 배열 또는 단일 객체 * @param rules 매핑 규칙 배열 + * @param entityJoinColumns 엔티티 조인 메타데이터 (선택적) - sourceField 값이 비었을 때 조인 alias에서 해결 * @returns 매핑된 배열 */ -export function applyMappingRules(data: any[] | any, rules: MappingRule[]): any[] { +export function applyMappingRules( + data: any[] | any, + rules: MappingRule[], + entityJoinColumns?: EntityJoinColumnMeta[], +): any[] { // 빈 데이터 처리 if (!data) { return []; } - // 🆕 배열이 아닌 경우 배열로 변환 + // 배열이 아닌 경우 배열로 변환 const dataArray = Array.isArray(data) ? data : [data]; if (dataArray.length === 0) { @@ -42,22 +48,34 @@ export function applyMappingRules(data: any[] | any, rules: MappingRule[]): any[ return [applyTransformRules(dataArray, rules)]; } + // 엔티티 조인 alias 역방향 맵 구성: { referenceColumn → joinAlias } + // ex) joinAlias "part_code_item_name" → sourceColumn "part_code", referenceColumn "item_name" + const joinAliasMap = buildJoinAliasMap(entityJoinColumns); + // 일반 매핑 (각 행에 대해 매핑) - // 🆕 원본 데이터를 복사한 후 매핑 규칙 적용 (매핑되지 않은 필드도 유지) + // 원본 데이터를 복사한 후 매핑 규칙 적용 (매핑되지 않은 필드도 유지) return dataArray.map((row) => { - // 원본 데이터 복사 const mappedRow: any = { ...row }; for (const rule of rules) { - // sourceField와 targetField가 모두 있어야 매핑 적용 if (!rule.sourceField || !rule.targetField) { continue; } - const sourceValue = getNestedValue(row, rule.sourceField); + let sourceValue = getNestedValue(row, rule.sourceField); + + // sourceField 값이 비어있으면 엔티티 조인 alias에서 해결 시도 + if (isEmptyValue(sourceValue) && joinAliasMap.size > 0) { + sourceValue = resolveFromEntityJoin(row, rule.targetField, joinAliasMap); + if (sourceValue !== undefined) { + logger.info( + `[dataMapping] 엔티티 조인 해결: ${rule.sourceField}(비어있음) → targetField "${rule.targetField}" → alias에서 값 획득`, + ); + } + } + const targetValue = sourceValue ?? rule.defaultValue; - // 소스 필드와 타겟 필드가 다르면 소스 필드 제거 후 타겟 필드에 설정 if (rule.sourceField !== rule.targetField) { delete mappedRow[rule.sourceField]; } @@ -69,6 +87,50 @@ export function applyMappingRules(data: any[] | any, rules: MappingRule[]): any[ }); } +/** + * 엔티티 조인 alias에서 역방향 참조 맵 구성 + * joinAlias 네이밍 규칙: {sourceColumn}_{referenceColumn} + * 예: "part_code_item_name" → sourceColumn="part_code", referenceColumn="item_name" + * + * 반환 Map: referenceColumn → joinAlias + * 예: "item_name" → "part_code_item_name" + */ +function buildJoinAliasMap( + entityJoinColumns?: EntityJoinColumnMeta[], +): Map { + const map = new Map(); + if (!entityJoinColumns || entityJoinColumns.length === 0) return map; + + for (const meta of entityJoinColumns) { + const prefix = `${meta.sourceColumn}_`; + if (meta.joinAlias.startsWith(prefix)) { + const referenceColumn = meta.joinAlias.slice(prefix.length); + map.set(referenceColumn, meta.joinAlias); + } + } + return map; +} + +/** + * 엔티티 조인 alias에서 targetField에 해당하는 값 해결 + * targetField 이름으로 조인 alias를 찾아 row에서 값을 가져옴 + */ +function resolveFromEntityJoin( + row: any, + targetField: string, + joinAliasMap: Map, +): any { + const joinAlias = joinAliasMap.get(targetField); + if (!joinAlias) return undefined; + + const value = row[joinAlias]; + return isEmptyValue(value) ? undefined : value; +} + +function isEmptyValue(value: any): boolean { + return value === null || value === undefined || value === ""; +} + /** * 변환 함수 적용 */ diff --git a/frontend/types/data-transfer.ts b/frontend/types/data-transfer.ts index 61aad8db..0cf04ef6 100644 --- a/frontend/types/data-transfer.ts +++ b/frontend/types/data-transfer.ts @@ -157,6 +157,17 @@ export interface DataReceivable { getData(): any; } +/** + * 엔티티 조인 컬럼 메타데이터 + * 소스 테이블의 FK가 참조 테이블과 어떻게 조인되었는지 정보 + */ +export interface EntityJoinColumnMeta { + sourceColumn: string; + joinAlias: string; + referenceTable: string; + sourceTable?: string; +} + /** * 데이터 제공 가능한 컴포넌트 인터페이스 * 데이터를 제공할 수 있는 컴포넌트가 구현해야 하는 인터페이스 @@ -180,5 +191,11 @@ export interface DataProvidable { * 선택 초기화 메서드 */ clearSelection(): void; + + /** + * 엔티티 조인 컬럼 메타데이터 반환 (선택적) + * 전달 매핑 시 조인 alias 해결에 사용 + */ + getEntityJoinColumns?(): EntityJoinColumnMeta[]; }