diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 12c04ba4..77fdb0dd 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -74,7 +74,10 @@ export class EntityJoinController { typeof screenEntityConfigs === "string" ? JSON.parse(screenEntityConfigs) : screenEntityConfigs; - logger.info("화면별 엔티티 설정 파싱 완료:", parsedScreenEntityConfigs); + logger.info( + "화면별 엔티티 설정 파싱 완료:", + parsedScreenEntityConfigs + ); } catch (error) { logger.warn("화면별 엔티티 설정 파싱 오류:", error); parsedScreenEntityConfigs = {}; @@ -365,14 +368,16 @@ export class EntityJoinController { ); // 현재 display_column으로 사용 중인 컬럼 제외 + const currentDisplayColumn = + config.displayColumn || config.displayColumns[0]; const availableColumns = columns.filter( - (col) => col.columnName !== config.displayColumn + (col) => col.columnName !== currentDisplayColumn ); return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: currentDisplayColumn, availableColumns: availableColumns.map((col) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnName, @@ -390,7 +395,8 @@ export class EntityJoinController { return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: + config.displayColumn || config.displayColumns[0], availableColumns: [], error: error instanceof Error ? error.message : "Unknown error", }; diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index de3328fb..24886a3d 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -20,7 +20,7 @@ export class EntityJoinService { * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) */ async detectEntityJoins( - tableName: string, + tableName: string, screenEntityConfigs?: Record ): Promise { try { @@ -57,7 +57,7 @@ export class EntityJoinService { const screenConfig = screenEntityConfigs?.[column.column_name]; let displayColumns: string[] = []; let separator = " - "; - + if (screenConfig && screenConfig.displayColumns) { // 화면에서 설정된 표시 컬럼들 사용 displayColumns = screenConfig.displayColumns; @@ -66,8 +66,11 @@ export class EntityJoinService { // 기존 설정된 단일 표시 컬럼 사용 displayColumns = [column.display_column]; } else { - // 기본값: reference_column 사용 - displayColumns = [column.reference_column]; + // 화면에서 설정하도록 빈 배열로 초기화 (테이블 타입 관리에서 표시 컬럼 설정 제거) + displayColumns = []; + console.log( + `🎯 표시 컬럼을 화면에서 설정하도록 초기화: ${column.column_name} (테이블 타입 관리에서 표시 컬럼 설정 제거됨)` + ); } // 별칭 컬럼명 생성 (writer -> writer_name) @@ -153,16 +156,18 @@ export class EntityJoinService { const joinColumns = joinConfigs .map((config) => { const alias = aliasMap.get(config.referenceTable); - const displayColumns = config.displayColumns || [config.displayColumn]; + const displayColumns = config.displayColumns || [ + config.displayColumn, + ]; const separator = config.separator || " - "; - + if (displayColumns.length === 1) { // 단일 컬럼인 경우 return `COALESCE(${alias}.${displayColumns[0]}, '') AS ${config.aliasColumn}`; } else { // 여러 컬럼인 경우 CONCAT으로 연결 const concatParts = displayColumns - .map(col => `COALESCE(${alias}.${col}, '')`) + .map((col) => `COALESCE(${alias}.${col}, '')`) .join(`, '${separator}', `); return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; } @@ -236,7 +241,7 @@ export class EntityJoinService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); return cachedData ? "cache" : "join"; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index c5de403d..69175941 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2044,7 +2044,10 @@ export class TableManagementService { } // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) - let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs); + let joinConfigs = await entityJoinService.detectEntityJoins( + tableName, + options.screenEntityConfigs + ); // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( @@ -2068,8 +2071,10 @@ export class TableManagementService { sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer) referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info) referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id) - displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email) + displayColumns: [additionalColumn.sourceColumn], // 표시할 컬럼들 (email) + displayColumn: additionalColumn.sourceColumn, // 하위 호환성 aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email) + separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); @@ -2243,7 +2248,7 @@ export class TableManagementService { await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); } @@ -2430,7 +2435,7 @@ export class TableManagementService { const lookupValue = referenceCacheService.getLookupValue( config.referenceTable, config.referenceColumn, - config.displayColumn, + config.displayColumn || config.displayColumns[0], String(sourceValue) ); @@ -2724,7 +2729,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData && cachedData.size > 0) { @@ -2808,7 +2813,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData) { @@ -2847,7 +2852,7 @@ export class TableManagementService { const hitRate = referenceCacheService.getCacheHitRate( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); totalHitRate += hitRate; } diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index 866bd65a..08fd5276 100644 --- a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx @@ -171,7 +171,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co {/* 표시 컬럼들 (다중 선택) */}
- + {/* 현재 선택된 표시 컬럼들 */}
{localValues.displayColumns.map((column, index) => ( @@ -183,7 +183,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
))} - + {localValues.displayColumns.length === 0 && (
표시할 컬럼을 추가해주세요
)} @@ -197,20 +197,19 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="컬럼명 입력 (예: user_name, dept_name)" className="flex-1" /> -
- +
- • 여러 컬럼을 선택하면 "{localValues.separator || ' - '}"로 구분하여 표시됩니다 -
- • 예: 이름{localValues.separator || ' - '}부서명 + • 여러 컬럼을 선택하면 "{localValues.separator || " - "}"로 구분하여 표시됩니다 +
• 예: 이름{localValues.separator || " - "}부서명
@@ -261,7 +260,6 @@ export const EntityTypeConfigPanel: React.FC = ({ co /> - {/* 필터 관리 */}
@@ -328,7 +326,10 @@ export const EntityTypeConfigPanel: React.FC = ({ co
참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn}
- 표시컬럼: {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"} + 표시컬럼:{" "} + {localValues.displayColumns.length > 0 + ? localValues.displayColumns.join(localValues.separator || " - ") + : "없음"}
@@ -337,14 +338,11 @@ export const EntityTypeConfigPanel: React.FC = ({ co
엔터티 타입 설정 가이드
참조 테이블: 데이터를 가져올 다른 테이블 이름 -
- • 조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) -
- • 표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능) +
조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) +
표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능)
• 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 -
- • 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시 +
• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
diff --git a/frontend/lib/api/entityJoin.ts b/frontend/lib/api/entityJoin.ts index ab531b29..dee758a5 100644 --- a/frontend/lib/api/entityJoin.ts +++ b/frontend/lib/api/entityJoin.ts @@ -67,6 +67,7 @@ export const entityJoinApi = { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 🎯 화면별 엔티티 설정 } = {}, ): Promise => { const searchParams = new URLSearchParams(); @@ -93,6 +94,7 @@ export const entityJoinApi = { ...params, search: params.search ? JSON.stringify(params.search) : undefined, additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined, + screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정 }, }); return response.data.data; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 813d36d2..a10c6fa4 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -192,12 +192,12 @@ export const TableListComponent: React.FC = ({ // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 let displayLabel = column.displayName || column.columnName; - // Entity 타입이고 display_column이 있는 경우 - if (column.webType === "entity" && column.displayColumn) { - // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 displayColumn 사용 - displayLabel = column.displayColumnLabel || column.displayColumn; + // Entity 타입인 경우 + if (column.webType === "entity") { + // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 기본값 사용 + displayLabel = column.displayColumnLabel || column.displayColumn || `${column.columnName}_name`; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn || "기본값"})`, ); } @@ -260,7 +260,20 @@ export const TableListComponent: React.FC = ({ joinAlias: col.entityJoinInfo!.joinAlias, })); + // 🎯 화면별 엔티티 표시 설정 생성 + const screenEntityConfigs: Record = {}; + entityJoinColumns.forEach((col) => { + if (col.entityDisplayConfig) { + const sourceColumn = col.entityJoinInfo!.sourceColumn; + screenEntityConfigs[sourceColumn] = { + displayColumns: col.entityDisplayConfig.displayColumns, + separator: col.entityDisplayConfig.separator || " - ", + }; + } + }); + console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); + console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, @@ -329,6 +342,7 @@ export const TableListComponent: React.FC = ({ sortOrder: sortDirection, enableEntityJoin: true, // 🎯 Entity 조인 활성화 additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 + screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 }); if (result) { diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 7ea374f2..7eeb0d76 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -58,8 +58,17 @@ export const TableListConfigPanel: React.FC = ({ }>; }>; }>({ availableColumns: [], joinTables: [] }); + const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); + // 🎯 엔티티 컬럼 표시 설정을 위한 상태 + const [entityDisplayConfigs, setEntityDisplayConfigs] = useState; + joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + selectedColumns: string[]; + separator: string; + }>>({}); + // 화면 테이블명이 있으면 자동으로 설정 useEffect(() => { if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) { @@ -228,30 +237,38 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", [...(config.columns || []), newColumn]); }; - // Entity 조인 컬럼 추가 - const addEntityJoinColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { + // 🎯 엔티티 컬럼 추가 (컬럼 설정 패널에서 표시 컬럼 선택) + const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias); if (existingColumn) return; + // 기본 표시명으로 엔티티 컬럼 추가 (컬럼 설정 패널에서 나중에 표시 컬럼 조합 선택) const newColumn: ColumnConfig = { columnName: joinColumn.joinAlias, - displayName: joinColumn.columnLabel, // 라벨명만 사용 + displayName: joinColumn.columnLabel, visible: true, sortable: true, searchable: true, align: "left", format: "text", order: config.columns?.length || 0, - isEntityJoin: true, // Entity 조인 컬럼임을 표시 + isEntityJoin: true, entityJoinInfo: { - sourceTable: joinColumn.tableName, + sourceTable: config.selectedTable || "", sourceColumn: joinColumn.columnName, joinAlias: joinColumn.joinAlias, }, + // 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능) + entityDisplayConfig: { + displayColumns: [], // 빈 배열로 초기화 + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: joinColumn.tableName, + }, }; handleChange("columns", [...(config.columns || []), newColumn]); - console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn); + console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn); }; // 컬럼 제거 @@ -267,6 +284,90 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", updatedColumns); }; + // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 + const loadEntityDisplayConfig = async (column: ColumnConfig) => { + if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; + + const { sourceTable, joinTable } = column.entityDisplayConfig; + const configKey = `${column.columnName}`; + + // 이미 로드된 경우 스킵 + if (entityDisplayConfigs[configKey]) return; + + try { + // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 + const [sourceResult, joinResult] = await Promise.all([ + entityJoinApi.getReferenceTableColumns(sourceTable), + entityJoinApi.getReferenceTableColumns(joinTable), + ]); + + const sourceColumns = sourceResult.columns || []; + const joinColumns = joinResult.columns || []; + + setEntityDisplayConfigs(prev => ({ + ...prev, + [configKey]: { + sourceColumns, + joinColumns, + selectedColumns: column.entityDisplayConfig?.displayColumns || [], + separator: column.entityDisplayConfig?.separator || " - ", + }, + })); + } catch (error) { + console.error("엔티티 표시 컬럼 정보 로드 실패:", error); + } + }; + + // 🎯 엔티티 표시 컬럼 선택 토글 + const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => { + const configKey = `${columnName}`; + const config = entityDisplayConfigs[configKey]; + if (!config) return; + + const newSelectedColumns = config.selectedColumns.includes(selectedColumn) + ? config.selectedColumns.filter(col => col !== selectedColumn) + : [...config.selectedColumns, selectedColumn]; + + setEntityDisplayConfigs(prev => ({ + ...prev, + [configKey]: { + ...prev[configKey], + selectedColumns: newSelectedColumns, + }, + })); + + // 컬럼 설정 업데이트 + updateColumn(columnName, { + entityDisplayConfig: { + ...config.entityDisplayConfig, + displayColumns: newSelectedColumns, + }, + }); + }; + + // 🎯 엔티티 표시 구분자 업데이트 + const updateEntityDisplaySeparator = (columnName: string, separator: string) => { + const configKey = `${columnName}`; + const config = entityDisplayConfigs[configKey]; + if (!config) return; + + setEntityDisplayConfigs(prev => ({ + ...prev, + [configKey]: { + ...prev[configKey], + separator, + }, + })); + + // 컬럼 설정 업데이트 + updateColumn(columnName, { + entityDisplayConfig: { + ...config.entityDisplayConfig, + separator, + }, + }); + }; + // 컬럼 순서 변경 const moveColumn = (columnName: string, direction: "up" | "down") => { const columns = [...(config.columns || [])]; @@ -820,6 +921,108 @@ export const TableListConfigPanel: React.FC = ({ /> + {/* 🎯 엔티티 타입 컬럼일 때 표시 컬럼 선택 UI */} + {column.isEntityJoin && column.entityDisplayConfig && ( +
+
+ + +
+ + {entityDisplayConfigs[column.columnName] && ( +
+ {/* 구분자 설정 */} +
+ + updateEntityDisplaySeparator(column.columnName, e.target.value)} + className="h-7 text-xs" + placeholder=" - " + /> +
+ + {/* 기본 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].sourceColumns.map((col) => ( +
+ toggleEntityDisplayColumn(column.columnName, col.columnName)} + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 조인 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].joinColumns.map((col) => ( +
+ toggleEntityDisplayColumn(column.columnName, col.columnName)} + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 선택된 컬럼 미리보기 */} + {entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && ( +
+ +
+ {entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => ( + + + {colName} + + {idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && ( + {entityDisplayConfigs[column.columnName].separator} + )} + + ))} +
+
+ )} +
+ )} +
+ )} +