diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index f4f89d25..15e05473 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -193,10 +193,11 @@ export class EntityJoinController { async getEntityJoinConfigs(req: Request, res: Response): Promise { try { const { tableName } = req.params; + const companyCode = (req as any).user?.companyCode; - logger.info(`Entity 조인 설정 조회: ${tableName}`); + logger.info(`Entity 조인 설정 조회: ${tableName} (companyCode: ${companyCode})`); - const joinConfigs = await entityJoinService.detectEntityJoins(tableName); + const joinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode); res.status(200).json({ success: true, @@ -224,11 +225,12 @@ export class EntityJoinController { async getReferenceTableColumns(req: Request, res: Response): Promise { try { const { tableName } = req.params; + const companyCode = (req as any).user?.companyCode; - logger.info(`참조 테이블 컬럼 조회: ${tableName}`); + logger.info(`참조 테이블 컬럼 조회: ${tableName} (companyCode: ${companyCode})`); const columns = - await tableManagementService.getReferenceTableColumns(tableName); + await tableManagementService.getReferenceTableColumns(tableName, companyCode); res.status(200).json({ success: true, @@ -408,11 +410,12 @@ export class EntityJoinController { async getEntityJoinColumns(req: Request, res: Response): Promise { try { const { tableName } = req.params; + const companyCode = (req as any).user?.companyCode; - logger.info(`Entity 조인 컬럼 조회: ${tableName}`); + logger.info(`Entity 조인 컬럼 조회: ${tableName} (companyCode: ${companyCode})`); // 1. 현재 테이블의 Entity 조인 설정 조회 - const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName); + const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode); // 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외 // 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨 @@ -439,7 +442,7 @@ export class EntityJoinController { try { const columns = await tableManagementService.getReferenceTableColumns( - config.referenceTable + config.referenceTable, companyCode ); // 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 4441a636..13f757fd 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -16,16 +16,18 @@ export class EntityJoinService { * 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성 * @param tableName 테이블명 * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) + * @param companyCode 회사코드 (회사별 설정 우선, 없으면 전체 조회) */ async detectEntityJoins( tableName: string, - screenEntityConfigs?: Record + screenEntityConfigs?: Record, + companyCode?: string ): Promise { try { - logger.info(`Entity 컬럼 감지 시작: ${tableName}`); + logger.info(`Entity 컬럼 감지 시작: ${tableName} (companyCode: ${companyCode || 'all'})`); // table_type_columns에서 entity 및 category 타입인 컬럼들 조회 - // company_code = '*' (공통 설정) 우선 조회 + // 회사코드가 있으면 해당 회사 + '*' 만 조회, 회사별 우선 const entityColumns = await query<{ column_name: string; input_type: string; @@ -33,14 +35,17 @@ export class EntityJoinService { reference_column: string; display_column: string | null; }>( - `SELECT column_name, input_type, reference_table, reference_column, display_column + `SELECT DISTINCT ON (column_name) + column_name, input_type, reference_table, reference_column, display_column FROM table_type_columns WHERE table_name = $1 AND input_type IN ('entity', 'category') - AND company_code = '*' AND reference_table IS NOT NULL - AND reference_table != ''`, - [tableName] + AND reference_table != '' + ${companyCode ? `AND company_code IN ($2, '*')` : ''} + ORDER BY column_name, + CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, + companyCode ? [tableName, companyCode] : [tableName] ); logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); @@ -272,7 +277,8 @@ export class EntityJoinService { orderBy: string = "", limit?: number, offset?: number, - columnTypes?: Map // 컬럼명 → 데이터 타입 매핑 + columnTypes?: Map, // 컬럼명 → 데이터 타입 매핑 + referenceTableColumns?: Map // 🆕 참조 테이블별 전체 컬럼 목록 ): { query: string; aliasMap: Map } { try { // 기본 SELECT 컬럼들 (날짜는 YYYY-MM-DD 형식, 나머지는 TEXT 캐스팅) @@ -338,115 +344,100 @@ export class EntityJoinService { ); }); - // 🔧 _label 별칭 중복 방지를 위한 Set - // 같은 sourceColumn에서 여러 조인 설정이 있을 때 _label은 첫 번째만 생성 - const generatedLabelAliases = new Set(); + // 🔧 생성된 별칭 중복 방지를 위한 Set + const generatedAliases = new Set(); - const joinColumns = joinConfigs + const joinColumns = uniqueReferenceTableConfigs .map((config) => { const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; const alias = aliasMap.get(aliasKey); - const displayColumns = config.displayColumns || [ - config.displayColumn, - ]; - const separator = config.separator || " - "; - - // 결과 컬럼 배열 (aliasColumn + _label 필드) const resultColumns: string[] = []; - if (displayColumns.length === 0 || !displayColumns[0]) { - // displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우 - // 조인 테이블의 referenceColumn을 기본값으로 사용 - resultColumns.push( - `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}` - ); - } else if (displayColumns.length === 1) { - // 단일 컬럼인 경우 - const col = displayColumns[0]; + // 🆕 참조 테이블의 전체 컬럼 목록이 있으면 모든 컬럼을 SELECT + const refTableCols = referenceTableColumns?.get( + `${config.referenceTable}:${config.sourceColumn}` + ) || referenceTableColumns?.get(config.referenceTable); - // ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴 - // 이렇게 하면 item_info.size, item_info.material 등 모든 조인 테이블 컬럼 지원 - const isJoinTableColumn = - config.referenceTable && config.referenceTable !== tableName; + if (refTableCols && refTableCols.length > 0) { + // 메타 컬럼은 제외 (메인 테이블과 중복되거나 불필요) + const skipColumns = new Set(["company_code", "created_date", "updated_date", "writer"]); + + for (const col of refTableCols) { + if (skipColumns.has(col)) continue; + + const colAlias = `${config.sourceColumn}_${col}`; + if (generatedAliases.has(colAlias)) continue; - if (isJoinTableColumn) { resultColumns.push( - `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}` + `COALESCE(${alias}."${col}"::TEXT, '') AS "${colAlias}"` ); + generatedAliases.add(colAlias); + } - // _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용) - // sourceColumn_label 형식으로 추가 - // 🔧 중복 방지: 같은 sourceColumn에서 _label은 첫 번째만 생성 - const labelAlias = `${config.sourceColumn}_label`; - if (!generatedLabelAliases.has(labelAlias)) { - resultColumns.push( - `COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}` - ); - generatedLabelAliases.add(labelAlias); - } - - // 🆕 referenceColumn (PK)도 항상 SELECT (parentDataMapping용) - // 예: customer_code, item_number 등 - // col과 동일해도 별도의 alias로 추가 (customer_code as customer_code) - // 🔧 중복 방지: referenceColumn도 한 번만 추가 - const refColAlias = config.referenceColumn; - if (!generatedLabelAliases.has(refColAlias)) { - resultColumns.push( - `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}` - ); - generatedLabelAliases.add(refColAlias); - } - } else { + // _label 필드도 추가 (기존 호환성) + const labelAlias = `${config.sourceColumn}_label`; + if (!generatedAliases.has(labelAlias)) { + // 표시용 컬럼 자동 감지: *_name > name > label > referenceColumn + const nameCol = refTableCols.find((c) => c.endsWith("_name") && c !== "company_name"); + const displayCol = nameCol || refTableCols.find((c) => c === "name") || config.referenceColumn; resultColumns.push( - `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}` + `COALESCE(${alias}."${displayCol}"::TEXT, '') AS "${labelAlias}"` ); + generatedAliases.add(labelAlias); } } else { - // 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음) - // 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price) - displayColumns.forEach((col) => { + // 🔄 기존 로직 (참조 테이블 컬럼 목록이 없는 경우 - fallback) + const displayColumns = config.displayColumns || [config.displayColumn]; + + if (displayColumns.length === 0 || !displayColumns[0]) { + resultColumns.push( + `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}` + ); + } else if (displayColumns.length === 1) { + const col = displayColumns[0]; const isJoinTableColumn = config.referenceTable && config.referenceTable !== tableName; - const individualAlias = `${config.sourceColumn}_${col}`; - - // 🔧 중복 방지: 같은 alias가 이미 생성되었으면 스킵 - if (generatedLabelAliases.has(individualAlias)) { - return; - } - if (isJoinTableColumn) { - // 조인 테이블 컬럼은 조인 별칭 사용 resultColumns.push( - `COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}` + `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}` ); + const labelAlias = `${config.sourceColumn}_label`; + if (!generatedAliases.has(labelAlias)) { + resultColumns.push( + `COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}` + ); + generatedAliases.add(labelAlias); + } } else { - // 기본 테이블 컬럼은 main 별칭 사용 resultColumns.push( - `COALESCE(main.${col}::TEXT, '') AS ${individualAlias}` + `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}` ); } - generatedLabelAliases.add(individualAlias); - }); + } else { + displayColumns.forEach((col) => { + const isJoinTableColumn = + config.referenceTable && config.referenceTable !== tableName; + const individualAlias = `${config.sourceColumn}_${col}`; + if (generatedAliases.has(individualAlias)) return; - // 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용) - const isJoinTableColumn = - config.referenceTable && config.referenceTable !== tableName; - if ( - isJoinTableColumn && - !displayColumns.includes(config.referenceColumn) && - !generatedLabelAliases.has(config.referenceColumn) // 🔧 중복 방지 - ) { - resultColumns.push( - `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}` - ); - generatedLabelAliases.add(config.referenceColumn); + if (isJoinTableColumn) { + resultColumns.push( + `COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}` + ); + } else { + resultColumns.push( + `COALESCE(main.${col}::TEXT, '') AS ${individualAlias}` + ); + } + generatedAliases.add(individualAlias); + }); } } - // 모든 resultColumns를 반환 return resultColumns.join(", "); }) + .filter(Boolean) .join(", "); // SELECT 절 구성 @@ -725,7 +716,7 @@ export class EntityJoinService { /** * 참조 테이블의 컬럼 목록 조회 (UI용) */ - async getReferenceTableColumns(tableName: string): Promise< + async getReferenceTableColumns(tableName: string, companyCode?: string): Promise< Array<{ columnName: string; displayName: string; @@ -750,16 +741,19 @@ export class EntityJoinService { ); // 2. table_type_columns 테이블에서 라벨과 input_type 정보 조회 + // 회사코드가 있으면 해당 회사 + '*' 만, 회사별 우선 const columnLabels = await query<{ column_name: string; column_label: string | null; input_type: string | null; }>( - `SELECT column_name, column_label, input_type + `SELECT DISTINCT ON (column_name) column_name, column_label, input_type FROM table_type_columns WHERE table_name = $1 - AND company_code = '*'`, - [tableName] + ${companyCode ? `AND company_code IN ($2, '*')` : ''} + ORDER BY column_name, + CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, + companyCode ? [tableName, companyCode] : [tableName] ); // 3. 라벨 및 inputType 정보를 맵으로 변환 diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index db5f32ed..27f713fc 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2875,10 +2875,11 @@ export class TableManagementService { }; } - // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) + // Entity 조인 설정 감지 (화면별 엔티티 설정 + 회사코드 전달) let joinConfigs = await entityJoinService.detectEntityJoins( tableName, - options.screenEntityConfigs + options.screenEntityConfigs, + options.companyCode ); logger.info( @@ -3258,6 +3259,28 @@ export class TableManagementService { startTime: number ): Promise { try { + // 🆕 참조 테이블별 전체 컬럼 목록 미리 조회 + const referenceTableColumns = new Map(); + const uniqueRefTables = new Set( + joinConfigs + .filter((c) => c.referenceTable !== "table_column_category_values") // 카테고리는 제외 + .map((c) => `${c.referenceTable}:${c.sourceColumn}`) + ); + + for (const key of uniqueRefTables) { + const refTable = key.split(":")[0]; + if (!referenceTableColumns.has(key)) { + const cols = await query<{ column_name: string }>( + `SELECT column_name FROM information_schema.columns + WHERE table_name = $1 AND table_schema = 'public' + ORDER BY ordinal_position`, + [refTable] + ); + referenceTableColumns.set(key, cols.map((c) => c.column_name)); + logger.info(`🔍 참조 테이블 컬럼 조회: ${refTable} → ${cols.length}개`); + } + } + // 데이터 조회 쿼리 const dataQuery = entityJoinService.buildJoinQuery( tableName, @@ -3266,7 +3289,9 @@ export class TableManagementService { whereClause, orderBy, limit, - offset + offset, + undefined, + referenceTableColumns // 🆕 참조 테이블 전체 컬럼 전달 ).query; // 카운트 쿼리 @@ -3767,12 +3792,12 @@ export class TableManagementService { reference_table: string; reference_column: string; }>( - `SELECT column_name, reference_table, reference_column + `SELECT DISTINCT ON (column_name) column_name, reference_table, reference_column FROM table_type_columns WHERE table_name = $1 AND input_type = 'entity' AND reference_table = $2 - AND company_code = '*' + ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END LIMIT 1`, [tableName, refTable] ); @@ -3883,7 +3908,7 @@ export class TableManagementService { /** * 참조 테이블의 표시 컬럼 목록 조회 */ - async getReferenceTableColumns(tableName: string): Promise< + async getReferenceTableColumns(tableName: string, companyCode?: string): Promise< Array<{ columnName: string; displayName: string; @@ -3891,7 +3916,7 @@ export class TableManagementService { inputType?: string; }> > { - return await entityJoinService.getReferenceTableColumns(tableName); + return await entityJoinService.getReferenceTableColumns(tableName, companyCode); } /** @@ -5005,14 +5030,14 @@ export class TableManagementService { input_type: string; display_column: string | null; }>( - `SELECT column_name, reference_column, input_type, display_column + `SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column FROM table_type_columns WHERE table_name = $1 AND input_type IN ('entity', 'category') AND reference_table = $2 AND reference_column IS NOT NULL AND reference_column != '' - AND company_code = '*'`, + ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, [rightTable, leftTable] ); @@ -5034,14 +5059,14 @@ export class TableManagementService { input_type: string; display_column: string | null; }>( - `SELECT column_name, reference_column, input_type, display_column + `SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column FROM table_type_columns WHERE table_name = $1 AND input_type IN ('entity', 'category') AND reference_table = $2 AND reference_column IS NOT NULL AND reference_column != '' - AND company_code = '*'`, + ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, [leftTable, rightTable] ); diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 0a69843c..74b4add0 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -957,6 +957,67 @@ export const SplitPanelLayoutComponent: React.FC [formatDateValue, formatNumberValue], ); + // 🆕 패널 config의 columns에서 additionalJoinColumns 추출하는 헬퍼 + const extractAdditionalJoinColumns = useCallback((columns: any[] | undefined, tableName: string) => { + if (!columns || columns.length === 0) return undefined; + + const joinColumns: Array<{ + sourceTable: string; + sourceColumn: string; + referenceTable: string; + joinAlias: string; + }> = []; + + columns.forEach((col: any) => { + // 방법 1: isEntityJoin 플래그가 있는 경우 (설정 패널에서 Entity 조인 컬럼으로 추가한 경우) + if (col.isEntityJoin && col.joinInfo) { + const existing = joinColumns.find( + (j) => j.referenceTable === col.joinInfo.referenceTable && j.joinAlias === col.joinInfo.joinAlias + ); + if (!existing) { + joinColumns.push({ + sourceTable: col.joinInfo.sourceTable || tableName, + sourceColumn: col.joinInfo.sourceColumn, + referenceTable: col.joinInfo.referenceTable, + joinAlias: col.joinInfo.joinAlias, + }); + } + return; + } + + // 방법 2: "테이블명.컬럼명" 형식 (기존 좌측 패널 방식) + const colName = typeof col === "string" ? col : col.name || col.columnName; + if (colName && colName.includes(".")) { + const [refTable, refColumn] = colName.split("."); + const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); + const existing = joinColumns.find( + (j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn + ); + if (!existing) { + joinColumns.push({ + sourceTable: tableName, + sourceColumn: inferredSourceColumn, + referenceTable: refTable, + joinAlias: `${inferredSourceColumn}_${refColumn}`, + }); + } else { + // 이미 추가된 테이블이면 별칭만 추가 + const newAlias = `${inferredSourceColumn}_${refColumn}`; + if (!joinColumns.find((j) => j.joinAlias === newAlias)) { + joinColumns.push({ + sourceTable: tableName, + sourceColumn: inferredSourceColumn, + referenceTable: refTable, + joinAlias: newAlias, + }); + } + } + } + }); + + return joinColumns.length > 0 ? joinColumns : undefined; + }, []); + // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -967,74 +1028,22 @@ export const SplitPanelLayoutComponent: React.FC // 🎯 필터 조건을 API에 전달 (entityJoinApi 사용) const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; - // 🆕 "테이블명.컬럼명" 형식의 조인 컬럼들을 additionalJoinColumns로 변환 - const configuredColumns = componentConfig.leftPanel?.columns || []; - const additionalJoinColumns: Array<{ - sourceTable: string; - sourceColumn: string; - referenceTable: string; - joinAlias: string; - }> = []; + // 🆕 좌측 패널 config의 Entity 조인 컬럼 추출 (헬퍼 함수 사용) + const leftJoinColumns = extractAdditionalJoinColumns( + componentConfig.leftPanel?.columns, + leftTableName, + ); - // 소스 컬럼 매핑 (item_info → item_code, warehouse_info → warehouse_id 등) - const sourceColumnMap: Record = {}; - - configuredColumns.forEach((col: any) => { - const colName = typeof col === "string" ? col : col.name || col.columnName; - if (colName && colName.includes(".")) { - const [refTable, refColumn] = colName.split("."); - // 소스 컬럼 추론 (item_info → item_code 또는 warehouse_info → warehouse_id) - // 기본: _info → _code, 백업: _info → _id - const primarySourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); - const secondarySourceColumn = refTable.replace("_info", "_id").replace("_mng", "_id"); - // 실제 존재하는 소스 컬럼은 백엔드에서 결정 (프론트엔드는 두 패턴 모두 전달) - const inferredSourceColumn = primarySourceColumn; - - // 이미 추가된 조인인지 확인 (동일 테이블, 동일 소스컬럼) - const existingJoin = additionalJoinColumns.find( - (j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn, - ); - - if (!existingJoin) { - // 새로운 조인 추가 (첫 번째 컬럼) - additionalJoinColumns.push({ - sourceTable: leftTableName, - sourceColumn: inferredSourceColumn, - referenceTable: refTable, - joinAlias: `${inferredSourceColumn}_${refColumn}`, - }); - sourceColumnMap[refTable] = inferredSourceColumn; - } - - // 추가 컬럼도 별도로 요청 (item_code_standard, item_code_unit 등) - // 단, 첫 번째 컬럼과 다른 경우만 - const existingAliases = additionalJoinColumns - .filter((j) => j.referenceTable === refTable) - .map((j) => j.joinAlias); - const newAlias = `${sourceColumnMap[refTable] || inferredSourceColumn}_${refColumn}`; - - if (!existingAliases.includes(newAlias)) { - additionalJoinColumns.push({ - sourceTable: leftTableName, - sourceColumn: sourceColumnMap[refTable] || inferredSourceColumn, - referenceTable: refTable, - joinAlias: newAlias, - }); - } - } - }); - - console.log("🔗 [분할패널] additionalJoinColumns:", additionalJoinColumns); - console.log("🔗 [분할패널] configuredColumns:", configuredColumns); + console.log("🔗 [분할패널] 좌측 additionalJoinColumns:", leftJoinColumns); const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, - search: filters, // 필터 조건 전달 - enableEntityJoin: true, // 엔티티 조인 활성화 - dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달 - additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 🆕 추가 조인 컬럼 - companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 + search: filters, + enableEntityJoin: true, + dataFilter: componentConfig.leftPanel?.dataFilter, + additionalJoinColumns: leftJoinColumns, + companyCodeOverride: companyCode, }); // 🔍 디버깅: API 응답 데이터의 키 확인 @@ -1093,11 +1102,16 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 엔티티 조인 API 사용 const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const rightDetailJoinColumns = extractAdditionalJoinColumns( + componentConfig.rightPanel?.columns, + rightTableName, + ); const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: { id: primaryKey }, - enableEntityJoin: true, // 엔티티 조인 활성화 + enableEntityJoin: true, size: 1, - companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 + companyCodeOverride: companyCode, + additionalJoinColumns: rightDetailJoinColumns, // 🆕 Entity 조인 컬럼 전달 }); const detail = result.items && result.items.length > 0 ? result.items[0] : null; @@ -1141,6 +1155,12 @@ export const SplitPanelLayoutComponent: React.FC const { entityJoinApi } = await import("@/lib/api/entityJoin"); const allResults: any[] = []; + // 🆕 우측 패널 Entity 조인 컬럼 추출 (그룹 합산용) + const rightJoinColumnsForGroup = extractAdditionalJoinColumns( + componentConfig.rightPanel?.columns, + rightTableName, + ); + // 각 원본 항목에 대해 조회 for (const originalItem of leftItem._originalItems) { const searchConditions: Record = {}; @@ -1155,7 +1175,8 @@ export const SplitPanelLayoutComponent: React.FC search: searchConditions, enableEntityJoin: true, size: 1000, - companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 + companyCodeOverride: companyCode, + additionalJoinColumns: rightJoinColumnsForGroup, // 🆕 Entity 조인 컬럼 전달 }); if (result.data) { allResults.push(...result.data); @@ -1185,12 +1206,22 @@ export const SplitPanelLayoutComponent: React.FC console.log("🔗 [분할패널] 복합키 조건:", searchConditions); + // 🆕 우측 패널 config의 Entity 조인 컬럼 추출 + const rightJoinColumns = extractAdditionalJoinColumns( + componentConfig.rightPanel?.columns, + rightTableName, + ); + if (rightJoinColumns) { + console.log("🔗 [분할패널] 우측 패널 additionalJoinColumns:", rightJoinColumns); + } + // 엔티티 조인 API로 데이터 조회 const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: searchConditions, enableEntityJoin: true, size: 1000, - companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 + companyCodeOverride: companyCode, + additionalJoinColumns: rightJoinColumns, // 🆕 Entity 조인 컬럼 전달 }); console.log("🔗 [분할패널] 복합키 조회 결과:", result); @@ -1275,6 +1306,12 @@ export const SplitPanelLayoutComponent: React.FC const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn; const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn; + // 🆕 탭 config의 Entity 조인 컬럼 추출 + const tabJoinColumns = extractAdditionalJoinColumns(tabConfig.columns, tabTableName); + if (tabJoinColumns) { + console.log(`🔗 [분할패널] 탭 ${tabIndex} additionalJoinColumns:`, tabJoinColumns); + } + let resultData: any[] = []; if (leftColumn && rightColumn) { @@ -1303,12 +1340,14 @@ export const SplitPanelLayoutComponent: React.FC search: searchConditions, enableEntityJoin: true, size: 1000, + additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달 }); resultData = result.data || []; } else { const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { enableEntityJoin: true, size: 1000, + additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달 }); resultData = result.data || []; } diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx index dd7638bc..b2fff2cd 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -28,12 +28,13 @@ import { CSS } from "@dnd-kit/utilities"; // 드래그 가능한 컬럼 아이템 function SortableColumnRow({ - id, col, index, isNumeric, onLabelChange, onWidthChange, onFormatChange, onRemove, + id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, }: { id: string; col: { name: string; label: string; width?: number; format?: any }; index: number; isNumeric: boolean; + isEntityJoin?: boolean; onLabelChange: (value: string) => void; onWidthChange: (value: number) => void; onFormatChange: (checked: boolean) => void; @@ -49,12 +50,17 @@ function SortableColumnRow({ className={cn( "flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5", isDragging && "z-50 opacity-50 shadow-md", + isEntityJoin && "border-blue-200 bg-blue-50/30", )} >
- #{index + 1} + {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} onLabelChange(e.target.value)} @@ -1975,6 +1981,7 @@ export const SplitPanelLayoutConfigPanel: React.FC { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], label: value }; @@ -2021,6 +2028,78 @@ export const SplitPanelLayoutConfigPanel: React.FC ))} + + {/* 좌측 패널 - Entity 조인 컬럼 아코디언 */} + {(() => { + const leftTable = config.leftPanel?.tableName || screenTableName; + const joinData = leftTable ? entityJoinColumns[leftTable] : null; + if (!joinData || joinData.joinTables.length === 0) return null; + + return joinData.joinTables.map((joinTable, tableIndex) => { + const joinColumnsToShow = joinTable.availableColumns.filter((column) => { + const matchingJoinColumn = joinData.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + if (!matchingJoinColumn) return false; + return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); + }); + const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length; + + if (joinColumnsToShow.length === 0 && addedCount === 0) return null; + + return ( +
+ + + + {joinTable.tableName} + {addedCount > 0 && ( + {addedCount}개 선택 + )} + {joinColumnsToShow.length}개 남음 + +
+ {joinColumnsToShow.map((column, colIndex) => { + const matchingJoinColumn = joinData.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + if (!matchingJoinColumn) return null; + + return ( +
{ + updateLeftPanel({ + columns: [...selectedColumns, { + name: matchingJoinColumn.joinAlias, + label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, + width: 100, + isEntityJoin: true, + joinInfo: { + sourceTable: leftTable!, + sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", + referenceTable: matchingJoinColumn.tableName, + joinAlias: matchingJoinColumn.joinAlias, + }, + }], + }); + }} + > + + + {column.columnLabel || column.columnName} +
+ ); + })} + {joinColumnsToShow.length === 0 && ( +

모든 컬럼이 이미 추가되었습니다

+ )} +
+
+ ); + }); + })()} )} @@ -2029,76 +2108,6 @@ export const SplitPanelLayoutConfigPanel: React.FC - {/* 좌측 패널 Entity 조인 컬럼 */} - {(() => { - const leftTable = config.leftPanel?.tableName || screenTableName; - const joinData = leftTable ? entityJoinColumns[leftTable] : null; - if (!joinData || joinData.joinTables.length === 0) return null; - const selectedColumns = config.leftPanel?.columns || []; - - return ( -
-

Entity 조인 컬럼

-

연관 테이블의 컬럼을 표시 컬럼에 추가합니다

-
- {joinData.joinTables.map((joinTable, tableIndex) => ( -
-
- - {joinTable.tableName} - {joinTable.currentDisplayColumn} -
-
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = joinData.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); - if (!matchingJoinColumn) return null; - const isAdded = selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); - - return ( -
{ - if (isAdded) { - updateLeftPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) }); - } else { - updateLeftPanel({ - columns: [...selectedColumns, { - name: matchingJoinColumn.joinAlias, - label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, - width: 100, - isEntityJoin: true, - joinInfo: { - sourceTable: leftTable!, - sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", - referenceTable: matchingJoinColumn.tableName, - joinAlias: matchingJoinColumn.joinAlias, - }, - }], - }); - } - }} - > - - - {column.columnLabel} - {column.dataType} -
- ); - })} -
-
- ))} -
-
- ); - })()} - {/* 좌측 패널 데이터 필터링 */}

좌측 패널 데이터 필터링

@@ -2351,64 +2360,7 @@ export const SplitPanelLayoutConfigPanel: React.FC )} - {/* 엔티티 설정 선택 - 조건 필터 모드에서만 표시 */} - {relationshipType !== "detail" && ( -
- -

- 우측 테이블에서 좌측 테이블을 참조하는 컬럼을 선택하세요 -

- - {config.rightPanel?.relation?.foreignKey && ( -

선택된 컬럼의 엔티티 설정이 자동으로 적용됩니다.

- )} -
- )} + {/* 필터 연결 컬럼 제거됨 - Entity 조인이 자동으로 관계를 처리 */} {/* 우측 패널 표시 컬럼 설정 - 드래그앤드롭 */} {(() => { @@ -2455,6 +2407,7 @@ export const SplitPanelLayoutConfigPanel: React.FC { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], label: value }; @@ -2499,6 +2452,78 @@ export const SplitPanelLayoutConfigPanel: React.FC ))}
+ + {/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */} + {(() => { + const rightTable = config.rightPanel?.tableName; + const joinData = rightTable ? entityJoinColumns[rightTable] : null; + if (!joinData || joinData.joinTables.length === 0) return null; + + return joinData.joinTables.map((joinTable, tableIndex) => { + const joinColumnsToShow = joinTable.availableColumns.filter((column) => { + const matchingJoinColumn = joinData.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + if (!matchingJoinColumn) return false; + return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); + }); + const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length; + + if (joinColumnsToShow.length === 0 && addedCount === 0) return null; + + return ( +
+ + + + {joinTable.tableName} + {addedCount > 0 && ( + {addedCount}개 선택 + )} + {joinColumnsToShow.length}개 남음 + +
+ {joinColumnsToShow.map((column, colIndex) => { + const matchingJoinColumn = joinData.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + if (!matchingJoinColumn) return null; + + return ( +
{ + updateRightPanel({ + columns: [...selectedColumns, { + name: matchingJoinColumn.joinAlias, + label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, + width: 100, + isEntityJoin: true, + joinInfo: { + sourceTable: rightTable!, + sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", + referenceTable: matchingJoinColumn.tableName, + joinAlias: matchingJoinColumn.joinAlias, + }, + }], + }); + }} + > + + + {column.columnLabel || column.columnName} +
+ ); + })} + {joinColumnsToShow.length === 0 && ( +

모든 컬럼이 이미 추가되었습니다

+ )} +
+
+ ); + }); + })()} )} @@ -2507,75 +2532,7 @@ export const SplitPanelLayoutConfigPanel: React.FC - {/* 우측 패널 Entity 조인 컬럼 */} - {(() => { - const rightTable = config.rightPanel?.tableName; - const joinData = rightTable ? entityJoinColumns[rightTable] : null; - if (!joinData || joinData.joinTables.length === 0) return null; - const selectedColumns = config.rightPanel?.columns || []; - - return ( -
-

Entity 조인 컬럼

-

연관 테이블의 컬럼을 표시 컬럼에 추가합니다

-
- {joinData.joinTables.map((joinTable, tableIndex) => ( -
-
- - {joinTable.tableName} - {joinTable.currentDisplayColumn} -
-
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = joinData.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); - if (!matchingJoinColumn) return null; - const isAdded = selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); - - return ( -
{ - if (isAdded) { - updateRightPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) }); - } else { - updateRightPanel({ - columns: [...selectedColumns, { - name: matchingJoinColumn.joinAlias, - label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, - width: 100, - isEntityJoin: true, - joinInfo: { - sourceTable: rightTable!, - sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", - referenceTable: matchingJoinColumn.tableName, - joinAlias: matchingJoinColumn.joinAlias, - }, - }], - }); - } - }} - > - - - {column.columnLabel} - {column.dataType} -
- ); - })} -
-
- ))} -
-
- ); - })()} + {/* 우측 패널 Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */} {/* 우측 패널 데이터 필터링 */}