From 4aefb5be6a7a9e888245208fb542a86956eba665 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 15:58:54 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A4=EC=A4=91=20=ED=91=9C=EC=8B=9C=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: - EntityTypeConfig 인터페이스에 displayColumns 배열 추가 - EntityTypeConfigPanel에서 여러 표시 컬럼 선택 UI 구현 - 구분자 설정 기능 추가 - 하위 호환성을 위한 displayColumn 유지 Backend: - EntityJoinConfig에 displayColumns 배열 지원 - 화면별 엔티티 설정을 전달받는 API 확장 - CONCAT을 사용한 다중 컬럼 표시 SQL 생성 - 기존 단일 컬럼과의 호환성 유지 이제 화면마다 다른 표시 컬럼 조합을 설정할 수 있음 예: 한 화면에서는 '이름'만, 다른 화면에서는 '이름 - 부서명' 표시 --- .../src/controllers/entityJoinController.ts | 17 + .../src/services/entityJoinService.ts | 49 ++- .../src/services/tableManagementService.ts | 5 +- backend-node/src/types/tableManagement.ts | 4 +- .../webtype-configs/EntityTypeConfigPanel.tsx | 322 ++++++++---------- frontend/types/screen-management.ts | 5 +- 6 files changed, 209 insertions(+), 193 deletions(-) diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 53c5de22..12c04ba4 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -26,6 +26,7 @@ export class EntityJoinController { sortOrder = "asc", enableEntityJoin = true, additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열) + screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열) userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함 ...otherParams } = req.query; @@ -65,6 +66,21 @@ export class EntityJoinController { } } + // 화면별 엔티티 설정 처리 + let parsedScreenEntityConfigs: Record = {}; + if (screenEntityConfigs) { + try { + parsedScreenEntityConfigs = + typeof screenEntityConfigs === "string" + ? JSON.parse(screenEntityConfigs) + : screenEntityConfigs; + logger.info("화면별 엔티티 설정 파싱 완료:", parsedScreenEntityConfigs); + } catch (error) { + logger.warn("화면별 엔티티 설정 파싱 오류:", error); + parsedScreenEntityConfigs = {}; + } + } + const result = await tableManagementService.getTableDataWithEntityJoins( tableName, { @@ -79,6 +95,7 @@ export class EntityJoinController { enableEntityJoin: enableEntityJoin === "true" || enableEntityJoin === true, additionalJoinColumns: parsedAdditionalJoinColumns, + screenEntityConfigs: parsedScreenEntityConfigs, } ); diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index f84cf167..de3328fb 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -16,8 +16,13 @@ const prisma = new PrismaClient(); export class EntityJoinService { /** * 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성 + * @param tableName 테이블명 + * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) */ - async detectEntityJoins(tableName: string): Promise { + async detectEntityJoins( + tableName: string, + screenEntityConfigs?: Record + ): Promise { try { logger.info(`Entity 컬럼 감지 시작: ${tableName}`); @@ -48,8 +53,22 @@ export class EntityJoinService { continue; } - // display_column이 없으면 reference_column 사용 - const displayColumn = column.display_column || column.reference_column; + // 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용 + const screenConfig = screenEntityConfigs?.[column.column_name]; + let displayColumns: string[] = []; + let separator = " - "; + + if (screenConfig && screenConfig.displayColumns) { + // 화면에서 설정된 표시 컬럼들 사용 + displayColumns = screenConfig.displayColumns; + separator = screenConfig.separator || " - "; + } else if (column.display_column) { + // 기존 설정된 단일 표시 컬럼 사용 + displayColumns = [column.display_column]; + } else { + // 기본값: reference_column 사용 + displayColumns = [column.reference_column]; + } // 별칭 컬럼명 생성 (writer -> writer_name) const aliasColumn = `${column.column_name}_name`; @@ -59,8 +78,10 @@ export class EntityJoinService { sourceColumn: column.column_name, referenceTable: column.reference_table, referenceColumn: column.reference_column, - displayColumn: displayColumn, + displayColumns: displayColumns, + displayColumn: displayColumns[0], // 하위 호환성 aliasColumn: aliasColumn, + separator: separator, }; // 조인 설정 유효성 검증 @@ -130,10 +151,22 @@ export class EntityJoinService { }); const joinColumns = joinConfigs - .map( - (config) => - `COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}` - ) + .map((config) => { + const alias = aliasMap.get(config.referenceTable); + 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}, '')`) + .join(`, '${separator}', `); + return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; + } + }) .join(", "); // SELECT 절 구성 diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 94f8aa30..c5de403d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2023,6 +2023,7 @@ export class TableManagementService { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 화면별 엔티티 설정 } ): Promise { const startTime = Date.now(); @@ -2042,8 +2043,8 @@ export class TableManagementService { }; } - // Entity 조인 설정 감지 - let joinConfigs = await entityJoinService.detectEntityJoins(tableName); + // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) + let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs); // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( diff --git a/backend-node/src/types/tableManagement.ts b/backend-node/src/types/tableManagement.ts index 52dca092..ee5e97b1 100644 --- a/backend-node/src/types/tableManagement.ts +++ b/backend-node/src/types/tableManagement.ts @@ -77,8 +77,10 @@ export interface EntityJoinConfig { sourceColumn: string; // writer referenceTable: string; // user_info referenceColumn: string; // user_id (조인 키) - displayColumn: string; // user_name (표시할 값) + displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들) + displayColumn?: string; // user_name (하위 호환성용, deprecated) aliasColumn: string; // writer_name (결과 컬럼명) + separator?: string; // ' - ' (여러 컬럼 연결 시 구분자) } export interface EntityJoinResponse { diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index a3505430..866bd65a 100644 --- a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx @@ -18,40 +18,36 @@ interface EntityTypeConfigPanelProps { export const EntityTypeConfigPanel: React.FC = ({ config, onConfigChange }) => { // 기본값이 설정된 config 사용 const safeConfig = { - entityName: "", - displayField: "name", - valueField: "id", - searchable: true, - multiple: false, - allowClear: true, + referenceTable: "", + referenceColumn: "id", + displayColumns: config.displayColumns || (config.displayColumn ? [config.displayColumn] : ["name"]), // 호환성 처리 + searchColumns: [], + filters: {}, placeholder: "", - apiEndpoint: "", - filters: [], displayFormat: "simple", - maxSelections: undefined, + separator: " - ", ...config, }; // 로컬 상태로 실시간 입력 관리 const [localValues, setLocalValues] = useState({ - entityName: safeConfig.entityName, - displayField: safeConfig.displayField, - valueField: safeConfig.valueField, - searchable: safeConfig.searchable, - multiple: safeConfig.multiple, - allowClear: safeConfig.allowClear, + referenceTable: safeConfig.referenceTable, + referenceColumn: safeConfig.referenceColumn, + displayColumns: [...safeConfig.displayColumns], + searchColumns: [...(safeConfig.searchColumns || [])], placeholder: safeConfig.placeholder, - apiEndpoint: safeConfig.apiEndpoint, displayFormat: safeConfig.displayFormat, - maxSelections: safeConfig.maxSelections?.toString() || "", + separator: safeConfig.separator, }); const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" }); + const [newDisplayColumn, setNewDisplayColumn] = useState(""); + const [availableColumns, setAvailableColumns] = useState([]); // 표시 형식 옵션 const displayFormats = [ - { value: "simple", label: "단순 (이름만)" }, - { value: "detailed", label: "상세 (이름 + 설명)" }, + { value: "simple", label: "단순 (첫 번째 컬럼만)" }, + { value: "detailed", label: "상세 (모든 컬럼 표시)" }, { value: "custom", label: "사용자 정의" }, ]; @@ -71,37 +67,27 @@ export const EntityTypeConfigPanel: React.FC = ({ co // config가 변경될 때 로컬 상태 동기화 useEffect(() => { setLocalValues({ - entityName: safeConfig.entityName, - displayField: safeConfig.displayField, - valueField: safeConfig.valueField, - searchable: safeConfig.searchable, - multiple: safeConfig.multiple, - allowClear: safeConfig.allowClear, + referenceTable: safeConfig.referenceTable, + referenceColumn: safeConfig.referenceColumn, + displayColumns: [...safeConfig.displayColumns], + searchColumns: [...(safeConfig.searchColumns || [])], placeholder: safeConfig.placeholder, - apiEndpoint: safeConfig.apiEndpoint, displayFormat: safeConfig.displayFormat, - maxSelections: safeConfig.maxSelections?.toString() || "", + separator: safeConfig.separator, }); }, [ - safeConfig.entityName, - safeConfig.displayField, - safeConfig.valueField, - safeConfig.searchable, - safeConfig.multiple, - safeConfig.allowClear, + safeConfig.referenceTable, + safeConfig.referenceColumn, + safeConfig.displayColumns, + safeConfig.searchColumns, safeConfig.placeholder, - safeConfig.apiEndpoint, safeConfig.displayFormat, - safeConfig.maxSelections, + safeConfig.separator, ]); const updateConfig = (key: keyof EntityTypeConfig, value: any) => { // 로컬 상태 즉시 업데이트 - if (key === "maxSelections") { - setLocalValues((prev) => ({ ...prev, [key]: value?.toString() || "" })); - } else { - setLocalValues((prev) => ({ ...prev, [key]: value })); - } + setLocalValues((prev) => ({ ...prev, [key]: value })); // 실제 config 업데이트 const newConfig = { ...safeConfig, [key]: value }; @@ -114,82 +100,132 @@ export const EntityTypeConfigPanel: React.FC = ({ co onConfigChange(newConfig); }; + // 표시 컬럼 추가 + const addDisplayColumn = () => { + if (newDisplayColumn.trim() && !localValues.displayColumns.includes(newDisplayColumn.trim())) { + const updatedColumns = [...localValues.displayColumns, newDisplayColumn.trim()]; + updateConfig("displayColumns", updatedColumns); + setNewDisplayColumn(""); + } + }; + + // 표시 컬럼 제거 + const removeDisplayColumn = (index: number) => { + const updatedColumns = localValues.displayColumns.filter((_, i) => i !== index); + updateConfig("displayColumns", updatedColumns); + }; + const addFilter = () => { if (newFilter.field.trim() && newFilter.value.trim()) { - const updatedFilters = [...(safeConfig.filters || []), { ...newFilter }]; + const updatedFilters = { ...safeConfig.filters, [newFilter.field]: newFilter.value }; updateConfig("filters", updatedFilters); setNewFilter({ field: "", operator: "=", value: "" }); } }; - const removeFilter = (index: number) => { - const updatedFilters = (safeConfig.filters || []).filter((_, i) => i !== index); + const removeFilter = (field: string) => { + const updatedFilters = { ...safeConfig.filters }; + delete updatedFilters[field]; updateConfig("filters", updatedFilters); }; - const updateFilter = (index: number, field: keyof typeof newFilter, value: string) => { - const updatedFilters = [...(safeConfig.filters || [])]; - updatedFilters[index] = { ...updatedFilters[index], [field]: value }; + const updateFilter = (oldField: string, field: string, value: string) => { + const updatedFilters = { ...safeConfig.filters }; + if (oldField !== field) { + delete updatedFilters[oldField]; + } + updatedFilters[field] = value; updateConfig("filters", updatedFilters); }; return (
- {/* 엔터티 이름 */} + {/* 참조 테이블 */}
-
- {/* API 엔드포인트 */} + {/* 조인 컬럼 (값 필드) */}
-
- {/* 필드 설정 */} -
-
- - updateConfig("valueField", e.target.value)} - placeholder="id" - className="mt-1" - /> + {/* 표시 컬럼들 (다중 선택) */} +
+ + + {/* 현재 선택된 표시 컬럼들 */} +
+ {localValues.displayColumns.map((column, index) => ( +
+ + {column} + +
+ ))} + + {localValues.displayColumns.length === 0 && ( +
표시할 컬럼을 추가해주세요
+ )}
-
- + {/* 새 표시 컬럼 추가 */} +
updateConfig("displayField", e.target.value)} - placeholder="name" - className="mt-1" + value={newDisplayColumn} + onChange={(e) => setNewDisplayColumn(e.target.value)} + placeholder="컬럼명 입력 (예: user_name, dept_name)" + className="flex-1" /> +
+ +
+ • 여러 컬럼을 선택하면 "{localValues.separator || ' - '}"로 구분하여 표시됩니다 +
+ • 예: 이름{localValues.separator || ' - '}부서명 +
+
+ + {/* 구분자 설정 */} +
+ + updateConfig("separator", e.target.value)} + placeholder=" - " + className="mt-1" + />
{/* 표시 형식 */} @@ -225,59 +261,6 @@ export const EntityTypeConfigPanel: React.FC = ({ co />
- {/* 옵션들 */} -
-
- - updateConfig("searchable", !!checked)} - /> -
- -
- - updateConfig("multiple", !!checked)} - /> -
- -
- - updateConfig("allowClear", !!checked)} - /> -
-
- - {/* 최대 선택 개수 (다중 선택 시) */} - {localValues.multiple && ( -
- - updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)} - className="mt-1" - placeholder="제한 없음" - /> -
- )} {/* 필터 관리 */}
@@ -285,33 +268,22 @@ export const EntityTypeConfigPanel: React.FC = ({ co {/* 기존 필터 목록 */}
- {(safeConfig.filters || []).map((filter, index) => ( -
+ {Object.entries(safeConfig.filters || {}).map(([field, value]) => ( +
updateFilter(index, "field", e.target.value)} + value={field} + onChange={(e) => updateFilter(field, e.target.value, value as string)} placeholder="필드명" className="flex-1" /> - + = updateFilter(index, "value", e.target.value)} + value={value as string} + onChange={(e) => updateFilter(field, field, e.target.value)} placeholder="값" className="flex-1" /> -
@@ -326,21 +298,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="필드명" className="flex-1" /> - + = setNewFilter((prev) => ({ ...prev, value: e.target.value }))} @@ -352,7 +310,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
-
총 {(safeConfig.filters || []).length}개 필터
+
총 {Object.keys(safeConfig.filters || {}).length}개 필터
{/* 미리보기 */} @@ -360,31 +318,33 @@ export const EntityTypeConfigPanel: React.FC = ({ co
- {localValues.searchable && } +
- {localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`} + {localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
- 엔터티: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, 값필드:{" "} - {localValues.valueField}, 표시필드: {localValues.displayField} - {localValues.multiple && `, 다중선택`} - {localValues.searchable && `, 검색가능`} + 참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn} +
+ 표시컬럼: {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"}
{/* 안내 메시지 */}
-
엔터티 참조 설정
+
엔터티 타입 설정 가이드
- • 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다 + • 참조 테이블: 데이터를 가져올 다른 테이블 이름
- • API 엔드포인트를 통해 데이터를 동적으로 로드합니다 + • 조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID)
- • 필터를 사용하여 표시할 데이터를 제한할 수 있습니다 -
• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다 + • 표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능) +
+ • 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 +
+ • 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 0d8649a2..40dbd965 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -192,10 +192,13 @@ export interface FileTypeConfig { export interface EntityTypeConfig { referenceTable: string; referenceColumn: string; - displayColumn: string; + displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경 + displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated) searchColumns?: string[]; filters?: Record; placeholder?: string; + displayFormat?: 'simple' | 'detailed' | 'custom'; // 표시 형식 + separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ') } /** From de6c7a8008ddca886924722db0ab4018d059ae94 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 16:23:36 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=BB=AC=EB=9F=BC=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=84=20=ED=99=94=EB=A9=B4=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=EA=B8=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 타입 관리에서 엔티티 타입의 표시 컬럼 설정 완전 제거 - 컬럼 설정 패널에서 엔티티 타입일 때 표시 컬럼 조합 선택 기능 추가 - 기본 테이블과 조인 테이블의 컬럼을 자유롭게 조합 가능 - 구분자 설정 및 실시간 미리보기 기능 포함 - 별도 모달 방식 제거하고 기존 컬럼 설정 패널에 통합 --- .../src/controllers/entityJoinController.ts | 14 +- .../src/services/entityJoinService.ts | 21 +- .../src/services/tableManagementService.ts | 19 +- .../webtype-configs/EntityTypeConfigPanel.tsx | 34 ++- frontend/lib/api/entityJoin.ts | 2 + .../table-list/TableListComponent.tsx | 24 +- .../table-list/TableListConfigPanel.tsx | 220 +++++++++++++++++- .../registry/components/table-list/types.ts | 8 + frontend/types/screen-management.ts | 2 +- 9 files changed, 293 insertions(+), 51 deletions(-) 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} + )} + + ))} +
+
+ )} +
+ )} +
+ )} +
- handleDetailSettingsChange( - column.columnName, - "entity_display_column", - value, - ) - } - > - - - - - -- 선택 안함 -- - {referenceTableColumns[column.referenceTable]?.map((refCol, index) => ( - - {refCol.columnName} - - ))} - {(!referenceTableColumns[column.referenceTable] || - referenceTableColumns[column.referenceTable].length === 0) && ( - -
-
- 로딩중 -
-
- )} -
- -
- )} {/* 설정 완료 표시 - 간소화 */} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 8680c0cd..5e89166d 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -664,6 +664,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, webType: col.webType || col.web_type, + input_type: col.inputType || col.input_type, // 🎯 input_type 필드 추가 widgetType: col.widgetType || col.widget_type || col.webType || col.web_type, isNullable: col.isNullable || col.is_nullable, required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a10c6fa4..64a2eab6 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -194,10 +194,10 @@ export const TableListComponent: React.FC = ({ // Entity 타입인 경우 if (column.webType === "entity") { - // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 기본값 사용 - displayLabel = column.displayColumnLabel || column.displayColumn || `${column.columnName}_name`; + // 우선 기준 테이블의 컬럼 라벨을 사용 + displayLabel = column.displayName || column.columnName; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn || "기본값"})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, ); } diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 7eeb0d76..a4c1a5c6 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -62,12 +62,17 @@ export const TableListConfigPanel: React.FC = ({ const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); // 🎯 엔티티 컬럼 표시 설정을 위한 상태 - const [entityDisplayConfigs, setEntityDisplayConfigs] = useState; - joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; - selectedColumns: string[]; - separator: string; - }>>({}); + const [entityDisplayConfigs, setEntityDisplayConfigs] = useState< + Record< + string, + { + sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + selectedColumns: string[]; + separator: string; + } + > + >({}); // 화면 테이블명이 있으면 자동으로 설정 useEffect(() => { @@ -284,6 +289,72 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", updatedColumns); }; + // 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정 + useEffect(() => { + console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", { + hasColumns: !!config.columns, + columnsCount: config.columns?.length || 0, + hasTableColumns: !!tableColumns, + tableColumnsCount: tableColumns?.length || 0, + selectedTable: config.selectedTable + }); + + if (!config.columns || !tableColumns) { + console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵"); + return; + } + + const updatedColumns = config.columns.map((column) => { + // 이미 isEntityJoin이 설정된 경우 스킵 + if (column.isEntityJoin) { + console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName); + return column; + } + + // 테이블 컬럼 정보에서 해당 컬럼 찾기 + const tableColumn = tableColumns.find((tc) => tc.column_name === column.columnName); + console.log("🔍 컬럼 검색:", { + columnName: column.columnName, + found: !!tableColumn, + inputType: tableColumn?.input_type, + webType: tableColumn?.web_type + }); + + // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) + if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) { + console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", column.columnName); + + return { + ...column, + isEntityJoin: true, + entityJoinInfo: { + sourceTable: config.selectedTable || "", + sourceColumn: column.columnName, + joinAlias: column.columnName, + }, + entityDisplayConfig: { + displayColumns: [], // 빈 배열로 초기화 + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: tableColumn.reference_table || "", + }, + }; + } + + return column; + }); + + // 변경사항이 있는 경우에만 업데이트 + const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin); + + if (hasChanges) { + console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns); + handleChange("columns", updatedColumns); + } else { + console.log("ℹ️ 엔티티 컬럼 변경사항 없음"); + } + }, [config.columns, tableColumns, config.selectedTable]); + // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 const loadEntityDisplayConfig = async (column: ColumnConfig) => { if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; @@ -304,7 +375,7 @@ export const TableListConfigPanel: React.FC = ({ const sourceColumns = sourceResult.columns || []; const joinColumns = joinResult.columns || []; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { sourceColumns, @@ -325,10 +396,10 @@ export const TableListConfigPanel: React.FC = ({ if (!config) return; const newSelectedColumns = config.selectedColumns.includes(selectedColumn) - ? config.selectedColumns.filter(col => col !== selectedColumn) + ? config.selectedColumns.filter((col) => col !== selectedColumn) : [...config.selectedColumns, selectedColumn]; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { ...prev[configKey], @@ -351,7 +422,7 @@ export const TableListConfigPanel: React.FC = ({ const config = entityDisplayConfigs[configKey]; if (!config) return; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { ...prev[configKey], @@ -791,6 +862,135 @@ export const TableListConfigPanel: React.FC = ({ {/* 컬럼 설정 탭 */} + {/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */} + {config.columns?.some((col) => col.isEntityJoin) && ( + + + 🎯 엔티티 컬럼 표시 설정 + 엔티티 타입 컬럼의 표시할 컬럼들을 조합하여 설정하세요 + + + {config.columns + ?.filter((col) => col.isEntityJoin && col.entityDisplayConfig) + .map((column) => ( +
+
+
+ + {column.columnName} + + {column.displayName} +
+ +
+ + {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} + + )} + + ))} +
+
+ )} +
+ )} +
+ ))} +
+
+ )} + {!screenTableName ? ( @@ -921,105 +1121,17 @@ export const TableListConfigPanel: React.FC = ({ /> - {/* 🎯 엔티티 타입 컬럼일 때 표시 컬럼 선택 UI */} - {column.isEntityJoin && column.entityDisplayConfig && ( -
-
- - + {/* 엔티티 타입 컬럼 표시 */} + {column.isEntityJoin && ( +
+
+ + 엔티티 타입 + + + 표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요 +
- - {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} - )} - - ))} -
-
- )} -
- )}
)} @@ -1528,7 +1640,6 @@ export const TableListConfigPanel: React.FC = ({ -
); }; diff --git a/frontend/types/table-management.ts b/frontend/types/table-management.ts index d5bcf127..bd2cac09 100644 --- a/frontend/types/table-management.ts +++ b/frontend/types/table-management.ts @@ -46,6 +46,7 @@ export interface UnifiedColumnInfo { // 입력 설정 inputType: "direct" | "auto"; + input_type?: string; // 🎯 데이터베이스의 input_type 필드 (entity, text, number 등) detailSettings?: Record; // JSON 파싱된 객체 description?: string; From f01be49f6afd6f7e71051dfcd4fa345a728d0302 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 16:59:12 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20TableListConfigPanel=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=BB=AC=EB=9F=BC=20=EA=B2=80=EC=83=89=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tc.column_name → tc.columnName으로 수정 - tableColumns 구조에 맞게 컬럼 검색 로직 수정 - 디버깅 로그 개선 --- .../table-list/TableListConfigPanel.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index a4c1a5c6..bf150fe5 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -31,7 +31,12 @@ export const TableListConfigPanel: React.FC = ({ screenTableName, tableColumns, }) => { - console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns }); + console.log("🔍 TableListConfigPanel props:", { + config: config?.selectedTable, + screenTableName, + tableColumns: tableColumns?.length, + tableColumnsSample: tableColumns?.[0] + }); const [availableTables, setAvailableTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); @@ -296,7 +301,7 @@ export const TableListConfigPanel: React.FC = ({ columnsCount: config.columns?.length || 0, hasTableColumns: !!tableColumns, tableColumnsCount: tableColumns?.length || 0, - selectedTable: config.selectedTable + selectedTable: config.selectedTable, }); if (!config.columns || !tableColumns) { @@ -312,12 +317,12 @@ export const TableListConfigPanel: React.FC = ({ } // 테이블 컬럼 정보에서 해당 컬럼 찾기 - const tableColumn = tableColumns.find((tc) => tc.column_name === column.columnName); + const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName); console.log("🔍 컬럼 검색:", { columnName: column.columnName, found: !!tableColumn, inputType: tableColumn?.input_type, - webType: tableColumn?.web_type + webType: tableColumn?.web_type, }); // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) @@ -468,7 +473,7 @@ export const TableListConfigPanel: React.FC = ({ if (!column) return; // tableColumns에서 해당 컬럼의 메타정보 찾기 - const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName || tc.column_name === columnName); + const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName); // 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정 const inferWidgetType = (dataType: string, webType?: string): string => { From ad7f350f00f44553ba31c0b8d59037db0980384e Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:06:23 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20API=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=9B=90=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=88=20=ED=85=8C=EC=9D=B4=EB=B8=94=EB=AA=85=20?= =?UTF-8?q?API=20=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 클라이언트를 원래 포트 8080으로 되돌림 - loadEntityDisplayConfig에서 sourceTable/joinTable이 비어있을 때 API 호출 방지 - 불필요한 백엔드 서버 중지 --- frontend/components/screen/panels/DetailSettingsPanel.tsx | 2 ++ frontend/lib/api/client.ts | 4 ++-- .../registry/components/table-list/TableListConfigPanel.tsx | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 8543cc71..8423f164 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -1013,6 +1013,8 @@ export const DetailSettingsPanel: React.FC = ({ currentTable, columns: currentTable?.columns, columnsLength: currentTable?.columns?.length, + sampleColumn: currentTable?.columns?.[0], + deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"), }); return currentTable?.columns || []; })()} diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index 2660014f..a02871d0 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -6,12 +6,12 @@ const getApiBaseUrl = (): string => { const currentHost = window.location.hostname; const currentPort = window.location.port; - // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080 + // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:3001 if ( (currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000") ) { - return "http://localhost:8080/api"; + return "http://localhost:3001/api"; } // 서버 환경에서 localhost:5555 → 39.117.244.52:8080 diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index bf150fe5..a2b2997e 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -370,6 +370,12 @@ export const TableListConfigPanel: React.FC = ({ // 이미 로드된 경우 스킵 if (entityDisplayConfigs[configKey]) return; + // sourceTable과 joinTable이 모두 있어야 로드 + if (!sourceTable || !joinTable) { + console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable }); + return; + } + try { // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 const [sourceResult, joinResult] = await Promise.all([ From 4c5e0330ef2c56062e9dc52d7bd745fc062b4de5 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:08:52 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20TableListConfigPanel=20API=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=BB=AC=EB=9F=BC=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch('/api/tables') 직접 호출을 tableTypeApi.getTables()로 변경하여 올바른 포트 사용 - 엔티티 컬럼 감지 시 reference_table/referenceTable 필드 로깅 추가 - joinTable 설정 시 두 필드 모두 확인하도록 수정 --- frontend/lib/api/client.ts | 4 +- .../table-list/TableListConfigPanel.tsx | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index a02871d0..2660014f 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -6,12 +6,12 @@ const getApiBaseUrl = (): string => { const currentHost = window.location.hostname; const currentPort = window.location.port; - // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:3001 + // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080 if ( (currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000") ) { - return "http://localhost:3001/api"; + return "http://localhost:8080/api"; } // 서버 환경에서 localhost:5555 → 39.117.244.52:8080 diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index a2b2997e..e6e0ec02 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -12,6 +12,7 @@ import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; +import { tableTypeApi } from "@/lib/api/screen"; import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react"; export interface TableListConfigPanelProps { @@ -31,11 +32,11 @@ export const TableListConfigPanel: React.FC = ({ screenTableName, tableColumns, }) => { - console.log("🔍 TableListConfigPanel props:", { - config: config?.selectedTable, - screenTableName, + console.log("🔍 TableListConfigPanel props:", { + config: config?.selectedTable, + screenTableName, tableColumns: tableColumns?.length, - tableColumnsSample: tableColumns?.[0] + tableColumnsSample: tableColumns?.[0], }); const [availableTables, setAvailableTables] = useState>([]); @@ -92,18 +93,14 @@ export const TableListConfigPanel: React.FC = ({ const fetchTables = async () => { setLoadingTables(true); try { - const response = await fetch("/api/tables"); - if (response.ok) { - const result = await response.json(); - if (result.success && result.data) { - setAvailableTables( - result.data.map((table: any) => ({ - tableName: table.tableName, - displayName: table.displayName || table.tableName, - })), - ); - } - } + // API 클라이언트를 사용하여 올바른 포트로 호출 + const response = await tableTypeApi.getTables(); + setAvailableTables( + response.map((table: any) => ({ + tableName: table.tableName, + displayName: table.displayName || table.tableName, + })), + ); } catch (error) { console.error("테이블 목록 가져오기 실패:", error); } finally { @@ -327,7 +324,12 @@ export const TableListConfigPanel: React.FC = ({ // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) { - console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", column.columnName); + console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", { + columnName: column.columnName, + referenceTable: tableColumn.reference_table, + referenceTableAlt: tableColumn.referenceTable, + allTableColumnKeys: Object.keys(tableColumn), + }); return { ...column, @@ -341,7 +343,7 @@ export const TableListConfigPanel: React.FC = ({ displayColumns: [], // 빈 배열로 초기화 separator: " - ", sourceTable: config.selectedTable || "", - joinTable: tableColumn.reference_table || "", + joinTable: tableColumn.reference_table || tableColumn.referenceTable || "", }, }; } From 28109eb63b0a78a86ddf629bf1fd277fa33cfbc7 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:11:07 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=A1=B0=EC=9D=B8=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=A0=95=EB=B3=B4=20=EC=9E=90=EB=8F=99=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loadEntityDisplayConfig에서 joinTable이 비어있을 때 Entity 조인 API로 조인 테이블 정보 자동 조회 - 조인 테이블 정보를 찾으면 entityDisplayConfig에 자동 업데이트 - 상세한 로깅으로 조인 테이블 정보 조회 과정 추적 가능 --- .../table-list/TableListConfigPanel.tsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index e6e0ec02..96deb641 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -372,9 +372,47 @@ export const TableListConfigPanel: React.FC = ({ // 이미 로드된 경우 스킵 if (entityDisplayConfigs[configKey]) return; + // joinTable이 비어있으면 Entity 조인 API로 조인 테이블 정보를 가져와서 설정 + let actualJoinTable = joinTable; + if (!actualJoinTable && sourceTable) { + try { + console.log("🔍 조인 테이블 정보를 Entity 조인 API로 가져오기:", sourceTable); + const entityJoinResult = await entityJoinApi.getEntityJoinColumns(sourceTable); + + // 해당 컬럼에 대한 조인 설정 찾기 + const columnJoinConfig = entityJoinResult.availableColumns?.find( + (col) => col.columnName === column.columnName + ); + + if (columnJoinConfig?.joinTable) { + actualJoinTable = columnJoinConfig.joinTable; + console.log("✅ 조인 테이블 정보 찾음:", actualJoinTable); + + // entityDisplayConfig 업데이트 + const updatedConfig = { + ...column.entityDisplayConfig, + joinTable: actualJoinTable, + }; + + // 컬럼 설정 업데이트 + const updatedColumns = config.columns?.map((col) => + col.columnName === column.columnName + ? { ...col, entityDisplayConfig: updatedConfig } + : col + ); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + } + } + } catch (error) { + console.error("Entity 조인 정보 조회 실패:", error); + } + } + // sourceTable과 joinTable이 모두 있어야 로드 - if (!sourceTable || !joinTable) { - console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable }); + if (!sourceTable || !actualJoinTable) { + console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable }); return; } @@ -382,7 +420,7 @@ export const TableListConfigPanel: React.FC = ({ // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 const [sourceResult, joinResult] = await Promise.all([ entityJoinApi.getReferenceTableColumns(sourceTable), - entityJoinApi.getReferenceTableColumns(joinTable), + entityJoinApi.getReferenceTableColumns(actualJoinTable), ]); const sourceColumns = sourceResult.columns || []; From a757034d8615a64d8016a7f235cf3c1db2210a1f Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:43:24 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=ED=91=9C=EC=8B=9C=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 59 ++++- .../table-list/TableListConfigPanel.tsx | 216 ++++++++++++++---- 2 files changed, 232 insertions(+), 43 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 24886a3d..76848714 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -59,9 +59,14 @@ export class EntityJoinService { let separator = " - "; if (screenConfig && screenConfig.displayColumns) { - // 화면에서 설정된 표시 컬럼들 사용 + // 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원) displayColumns = screenConfig.displayColumns; separator = screenConfig.separator || " - "; + console.log(`🎯 화면별 엔티티 설정 적용: ${column.column_name}`, { + displayColumns, + separator, + screenConfig, + }); } else if (column.display_column) { // 기존 설정된 단일 표시 컬럼 사용 displayColumns = [column.display_column]; @@ -163,12 +168,51 @@ export class EntityJoinService { if (displayColumns.length === 1) { // 단일 컬럼인 경우 - return `COALESCE(${alias}.${displayColumns[0]}, '') AS ${config.aliasColumn}`; + const col = displayColumns[0]; + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + ].includes(col); + + if (isJoinTableColumn) { + return `COALESCE(${alias}.${col}, '') AS ${config.aliasColumn}`; + } else { + return `COALESCE(main.${col}, '') AS ${config.aliasColumn}`; + } } else { // 여러 컬럼인 경우 CONCAT으로 연결 + // 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 const concatParts = displayColumns - .map((col) => `COALESCE(${alias}.${col}, '')`) + .map((col) => { + // 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용) + // 현재는 dept_info 테이블의 컬럼들을 확인 + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + ].includes(col); + + if (isJoinTableColumn) { + // 조인 테이블 컬럼은 조인 별칭 사용 + return `COALESCE(${alias}.${col}, '')`; + } else { + // 기본 테이블 컬럼은 main 별칭 사용 + return `COALESCE(main.${col}, '')`; + } + }) .join(`, '${separator}', `); + return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; } }) @@ -237,6 +281,15 @@ export class EntityJoinService { try { const strategies = await Promise.all( joinConfigs.map(async (config) => { + // 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가 + if (config.displayColumns && config.displayColumns.length > 1) { + console.log( + `🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${config.sourceColumn}`, + config.displayColumns + ); + return "join"; + } + // 참조 테이블의 캐시 가능성 확인 const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 96deb641..6a9007d5 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -364,49 +364,145 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 const loadEntityDisplayConfig = async (column: ColumnConfig) => { - if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; + console.log("🔍 loadEntityDisplayConfig 시작:", { + columnName: column.columnName, + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + entityDisplayConfig: column.entityDisplayConfig, + configSelectedTable: config.selectedTable, + }); - const { sourceTable, joinTable } = column.entityDisplayConfig; - const configKey = `${column.columnName}`; + if (!column.isEntityJoin || !column.entityJoinInfo) { + console.log("⚠️ 엔티티 컬럼 조건 불만족:", { + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + }); + return; + } - // 이미 로드된 경우 스킵 - if (entityDisplayConfigs[configKey]) return; + // entityDisplayConfig가 없으면 초기화 + if (!column.entityDisplayConfig) { + console.log("🔧 entityDisplayConfig 초기화:", column.columnName); + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === column.columnName) { + return { + ...col, + entityDisplayConfig: { + displayColumns: [], + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: "", + }, + }; + } + return col; + }); - // joinTable이 비어있으면 Entity 조인 API로 조인 테이블 정보를 가져와서 설정 - let actualJoinTable = joinTable; - if (!actualJoinTable && sourceTable) { + if (updatedColumns) { + handleChange("columns", updatedColumns); + // 업데이트된 컬럼으로 다시 시도 + const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName); + if (updatedColumn) { + console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig); + return loadEntityDisplayConfig(updatedColumn); + } + } + return; + } + + console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig); + console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig)); + + // sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기 + let sourceTable = column.entityDisplayConfig.sourceTable; + let joinTable = column.entityDisplayConfig.joinTable; + + if (!sourceTable && column.entityJoinInfo) { + sourceTable = column.entityJoinInfo.sourceTable; + } + + if (!joinTable) { + // joinTable이 없으면 tableTypeApi로 조회해서 설정 try { - console.log("🔍 조인 테이블 정보를 Entity 조인 API로 가져오기:", sourceTable); - const entityJoinResult = await entityJoinApi.getEntityJoinColumns(sourceTable); - - // 해당 컬럼에 대한 조인 설정 찾기 - const columnJoinConfig = entityJoinResult.availableColumns?.find( - (col) => col.columnName === column.columnName - ); - - if (columnJoinConfig?.joinTable) { - actualJoinTable = columnJoinConfig.joinTable; - console.log("✅ 조인 테이블 정보 찾음:", actualJoinTable); - + console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable); + const columnList = await tableTypeApi.getColumns(sourceTable); + const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName); + + if (columnInfo?.reference_table || columnInfo?.referenceTable) { + joinTable = columnInfo.reference_table || columnInfo.referenceTable; + console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable); + // entityDisplayConfig 업데이트 const updatedConfig = { ...column.entityDisplayConfig, - joinTable: actualJoinTable, + sourceTable: sourceTable, + joinTable: joinTable, }; - + // 컬럼 설정 업데이트 const updatedColumns = config.columns?.map((col) => - col.columnName === column.columnName - ? { ...col, entityDisplayConfig: updatedConfig } - : col + col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col, ); - + if (updatedColumns) { handleChange("columns", updatedColumns); } } } catch (error) { - console.error("Entity 조인 정보 조회 실패:", error); + console.error("tableTypeApi 컬럼 정보 조회 실패:", error); + } + } + + console.log("🔍 최종 추출한 값:", { sourceTable, joinTable }); + const configKey = `${column.columnName}`; + + // 이미 로드된 경우 스킵 + if (entityDisplayConfigs[configKey]) return; + + // joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기 + let actualJoinTable = joinTable; + if (!actualJoinTable && sourceTable) { + try { + console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", { + tableName: sourceTable, + columnName: column.columnName, + }); + + const columnList = await tableTypeApi.getColumns(sourceTable); + const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName); + + console.log("🔍 컬럼 정보 조회 결과:", { + columnInfo: columnInfo, + referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable, + referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn, + }); + + if (columnInfo?.reference_table || columnInfo?.referenceTable) { + actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable; + console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable); + + // entityDisplayConfig 업데이트 + const updatedConfig = { + ...column.entityDisplayConfig, + joinTable: actualJoinTable, + }; + + // 컬럼 설정 업데이트 + const updatedColumns = config.columns?.map((col) => + col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col, + ); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + } + } else { + console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", { + columnName: column.columnName, + columnInfo: columnInfo, + }); + } + } catch (error) { + console.error("tableTypeApi 컬럼 정보 조회 실패:", error); } } @@ -443,13 +539,14 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 표시 컬럼 선택 토글 const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => { const configKey = `${columnName}`; - const config = entityDisplayConfigs[configKey]; - if (!config) return; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; - const newSelectedColumns = config.selectedColumns.includes(selectedColumn) - ? config.selectedColumns.filter((col) => col !== selectedColumn) - : [...config.selectedColumns, selectedColumn]; + const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn) + ? localConfig.selectedColumns.filter((col) => col !== selectedColumn) + : [...localConfig.selectedColumns, selectedColumn]; + // 로컬 상태 업데이트 setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { @@ -458,6 +555,29 @@ export const TableListConfigPanel: React.FC = ({ }, })); + // 실제 컬럼 설정도 업데이트 + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === columnName && col.entityDisplayConfig) { + return { + ...col, + entityDisplayConfig: { + ...col.entityDisplayConfig, + displayColumns: newSelectedColumns, + }, + }; + } + return col; + }); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", { + columnName, + selectedColumns: newSelectedColumns, + updatedColumn: updatedColumns.find((col) => col.columnName === columnName), + }); + } + // 컬럼 설정 업데이트 updateColumn(columnName, { entityDisplayConfig: { @@ -470,9 +590,10 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 표시 구분자 업데이트 const updateEntityDisplaySeparator = (columnName: string, separator: string) => { const configKey = `${columnName}`; - const config = entityDisplayConfigs[configKey]; - if (!config) return; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; + // 로컬 상태 업데이트 setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { @@ -481,13 +602,28 @@ export const TableListConfigPanel: React.FC = ({ }, })); - // 컬럼 설정 업데이트 - updateColumn(columnName, { - entityDisplayConfig: { - ...config.entityDisplayConfig, - separator, - }, + // 실제 컬럼 설정도 업데이트 + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === columnName && col.entityDisplayConfig) { + return { + ...col, + entityDisplayConfig: { + ...col.entityDisplayConfig, + separator, + }, + }; + } + return col; }); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + console.log("🎯 엔티티 표시 구분자 설정 업데이트:", { + columnName, + separator, + updatedColumn: updatedColumns.find((col) => col.columnName === columnName), + }); + } }; // 컬럼 순서 변경 From e75889a127160f3c3bf649db63becb5dea4b8149 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 10:33:54 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 50 ++-- .../src/services/tableManagementService.ts | 20 +- .../table-list/TableListComponent.tsx | 221 ++++++++++++++++-- .../table-list/TableListConfigPanel.tsx | 28 +-- 4 files changed, 248 insertions(+), 71 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 76848714..d0b01846 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -67,14 +67,24 @@ export class EntityJoinService { separator, screenConfig, }); - } else if (column.display_column) { - // 기존 설정된 단일 표시 컬럼 사용 + } else if (column.display_column && column.display_column !== "none") { + // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) displayColumns = [column.display_column]; } else { - // 화면에서 설정하도록 빈 배열로 초기화 (테이블 타입 관리에서 표시 컬럼 설정 제거) - displayColumns = []; + // 조인 탭에서 보여줄 기본 표시 컬럼 설정 + // dept_info 테이블의 경우 dept_name을 기본으로 사용 + let defaultDisplayColumn = column.reference_column; + if (column.reference_table === "dept_info") { + defaultDisplayColumn = "dept_name"; + } else if (column.reference_table === "company_info") { + defaultDisplayColumn = "company_name"; + } else if (column.reference_table === "user_info") { + defaultDisplayColumn = "user_name"; + } + + displayColumns = [defaultDisplayColumn]; console.log( - `🎯 표시 컬럼을 화면에서 설정하도록 초기화: ${column.column_name} (테이블 타입 관리에서 표시 컬럼 설정 제거됨)` + `🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` ); } @@ -119,8 +129,10 @@ export class EntityJoinService { offset?: number ): { query: string; aliasMap: Map } { try { - // 기본 SELECT 컬럼들 - const baseColumns = selectColumns.map((col) => `main.${col}`).join(", "); + // 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지) + const baseColumns = selectColumns + .map((col) => `main.${col}::TEXT AS ${col}`) + .join(", "); // Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리) // 별칭 매핑 생성 (JOIN 절과 동일한 로직) @@ -181,9 +193,9 @@ export class EntityJoinService { ].includes(col); if (isJoinTableColumn) { - return `COALESCE(${alias}.${col}, '') AS ${config.aliasColumn}`; + return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`; } else { - return `COALESCE(main.${col}, '') AS ${config.aliasColumn}`; + return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`; } } else { // 여러 컬럼인 경우 CONCAT으로 연결 @@ -205,15 +217,15 @@ export class EntityJoinService { if (isJoinTableColumn) { // 조인 테이블 컬럼은 조인 별칭 사용 - return `COALESCE(${alias}.${col}, '')`; + return `COALESCE(${alias}.${col}::TEXT, '')`; } else { // 기본 테이블 컬럼은 main 별칭 사용 - return `COALESCE(main.${col}, '')`; + return `COALESCE(main.${col}::TEXT, '')`; } }) - .join(`, '${separator}', `); + .join(` || '${separator}' || `); - return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; + return `(${concatParts}) AS ${config.aliasColumn}`; } }) .join(", "); @@ -336,17 +348,23 @@ export class EntityJoinService { return false; } - // 참조 컬럼 존재 확인 + // 참조 컬럼 존재 확인 (displayColumns[0] 사용) + const displayColumn = config.displayColumns?.[0] || config.displayColumn; + if (!displayColumn) { + logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`); + return false; + } + const columnExists = await prisma.$queryRaw` SELECT 1 FROM information_schema.columns WHERE table_name = ${config.referenceTable} - AND column_name = ${config.displayColumn} + AND column_name = ${displayColumn} LIMIT 1 `; if (!Array.isArray(columnExists) || columnExists.length === 0) { logger.warn( - `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}` + `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` ); return false; } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 69175941..5cb2853d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2065,21 +2065,27 @@ export class TableManagementService { ); if (baseJoinConfig) { + // joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name) + // sourceColumn을 제거한 나머지 부분이 실제 컬럼명 + const sourceColumn = baseJoinConfig.sourceColumn; // dept_code + const joinAlias = additionalColumn.joinAlias; // dept_code_location_name + const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // location_name + // 추가 조인 컬럼 설정 생성 const additionalJoinConfig: EntityJoinConfig = { sourceTable: tableName, - sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer) - referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info) - referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id) - displayColumns: [additionalColumn.sourceColumn], // 표시할 컬럼들 (email) - displayColumn: additionalColumn.sourceColumn, // 하위 호환성 - aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email) + sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code) + referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info) + referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code) + displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name) + displayColumn: actualColumnName, // 하위 호환성 + aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name) separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); logger.info( - `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn}` + `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` ); } } diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 64a2eab6..19bd3670 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -84,6 +84,12 @@ export const TableListComponent: React.FC = ({ ...componentConfig, } as TableListConfig; + // 🎯 디버깅: 초기 컬럼 설정 확인 + console.log( + "🔍 초기 tableConfig.columns:", + tableConfig.columns?.map((c) => c.columnName), + ); + // 상태 관리 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -98,6 +104,9 @@ export const TableListComponent: React.FC = ({ const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) + + // 🎯 조인 컬럼 매핑 상태 + const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) // 고급 필터 관련 state @@ -254,11 +263,60 @@ export const TableListComponent: React.FC = ({ // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; - const additionalJoinColumns = entityJoinColumns.map((col) => ({ - sourceTable: col.entityJoinInfo!.sourceTable, - sourceColumn: col.entityJoinInfo!.sourceColumn, - joinAlias: col.entityJoinInfo!.joinAlias, - })); + + // 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만) + const joinTabColumns = + tableConfig.columns?.filter( + (col) => + !col.isEntityJoin && + col.columnName.includes("_") && + (col.columnName.includes("dept_code_") || + col.columnName.includes("_dept_code") || + col.columnName.includes("_company_") || + col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들 + ) || []; + + console.log( + "🔍 조인 탭 컬럼들:", + joinTabColumns.map((c) => c.columnName), + ); + + const additionalJoinColumns = [ + ...entityJoinColumns.map((col) => ({ + sourceTable: col.entityJoinInfo!.sourceTable, + sourceColumn: col.entityJoinInfo!.sourceColumn, + joinAlias: col.entityJoinInfo!.joinAlias, + })), + // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) + ...joinTabColumns + .filter((col) => { + // 실제 API 응답에 존재하는 컬럼만 필터링 + const validJoinColumns = ["dept_code_name", "dept_name"]; + const isValid = validJoinColumns.includes(col.columnName); + if (!isValid) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); + } + return isValid; + }) + .map((col) => { + // 실제 존재하는 조인 컬럼만 처리 + let sourceTable = tableConfig.selectedTable; + let sourceColumn = col.columnName; + + if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { + sourceTable = "dept_info"; + sourceColumn = "dept_code"; + } + + console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); + + return { + sourceTable: sourceTable, + sourceColumn: sourceColumn, + joinAlias: col.columnName, + }; + }), + ]; // 🎯 화면별 엔티티 표시 설정 생성 const screenEntityConfigs: Record = {}; @@ -272,6 +330,8 @@ export const TableListComponent: React.FC = ({ } }); + console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); + console.log("🔗 조인 탭 컬럼:", joinTabColumns); console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); @@ -346,6 +406,10 @@ export const TableListComponent: React.FC = ({ }); if (result) { + console.log("🎯 API 응답 결과:", result); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 페이지:", result.totalPages); + console.log("🎯 총 아이템:", result.total); setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -383,12 +447,88 @@ export const TableListComponent: React.FC = ({ } } - // 🎯 Entity 조인된 컬럼 처리 + // 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용 let processedColumns = [...(tableConfig.columns || [])]; // 초기 컬럼이 있으면 먼저 설정 if (processedColumns.length > 0) { - setDisplayColumns(processedColumns); + console.log( + "🔍 사용자 설정 컬럼들:", + processedColumns.map((c) => c.columnName), + ); + + // 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링 + if (result.data.length > 0) { + const actualApiColumns = Object.keys(result.data[0]); + console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); + + // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) + // 실제 API 응답에 존재하는 컬럼만 매핑 + const newJoinColumnMapping: Record = { + dept_code_dept_code: "dept_code", // user_info.dept_code + dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) + dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) + dept_code_name: "dept_code_name", // dept_info.dept_name + dept_name: "dept_name", // dept_info.dept_name + status: "status", // user_info.status + }; + + // 🎯 조인 컬럼 매핑 상태 업데이트 + setJoinColumnMapping(newJoinColumnMapping); + + console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping); + console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns); + + // 🎯 컬럼명 매핑 및 유효성 검사 + const validColumns = processedColumns + .map((col) => { + // 체크박스는 그대로 유지 + if (col.columnName === "__checkbox__") return col; + + // 조인 컬럼 매핑 적용 + const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName; + + console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`); + + // API 응답에 존재하는지 확인 + const existsInApi = actualApiColumns.includes(mappedColumnName); + + if (!existsInApi) { + console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`); + return null; + } + + // 컬럼명이 변경된 경우 업데이트 + if (mappedColumnName !== col.columnName) { + console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`); + return { + ...col, + columnName: mappedColumnName, + }; + } + + console.log(`✅ 컬럼 유지: ${col.columnName}`); + return col; + }) + .filter((col) => col !== null) as ColumnConfig[]; + + if (validColumns.length !== processedColumns.length) { + console.log( + "🔍 필터링된 컬럼들:", + validColumns.map((c) => c.columnName), + ); + console.log( + "🔍 제거된 컬럼들:", + processedColumns + .filter((col) => { + const mappedName = newJoinColumnMapping[col.columnName] || col.columnName; + return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__"; + }) + .map((c) => c.columnName), + ); + processedColumns = validColumns; + } + } } if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { @@ -412,11 +552,11 @@ export const TableListComponent: React.FC = ({ }); } - // 컬럼 정보가 없으면 첫 번째 데이터 행에서 추출 + // 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성 if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) { const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ columnName: key, - displayName: columnLabels[key] || key, // 라벨명 우선 사용 + displayName: columnLabels[key] || key, visible: true, sortable: true, searchable: true, @@ -425,6 +565,11 @@ export const TableListComponent: React.FC = ({ order: index, })); + console.log( + "🎯 자동 생성된 컬럼들:", + autoColumns.map((c) => c.columnName), + ); + // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) if (onFormDataChange) { onFormDataChange({ @@ -440,6 +585,9 @@ export const TableListComponent: React.FC = ({ // 🎯 표시할 컬럼 상태 업데이트 setDisplayColumns(processedColumns); + console.log("🎯 displayColumns 업데이트됨:", processedColumns); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 데이터:", result.data); } } catch (err) { console.error("테이블 데이터 로딩 오류:", err); @@ -628,9 +776,22 @@ export const TableListComponent: React.FC = ({ let columns: ColumnConfig[] = []; - if (!displayColumns || displayColumns.length === 0) { - // displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용 - if (!tableConfig.columns) return []; + // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) + if (displayColumns && displayColumns.length > 0) { + console.log("🎯 displayColumns 사용:", displayColumns); + const filteredColumns = displayColumns.filter((col) => { + // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 + if (isDesignMode) { + return col.visible; // 디자인 모드에서는 visible만 체크 + } else { + return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 + } + }); + console.log("🎯 필터링된 컬럼:", filteredColumns); + columns = filteredColumns.sort((a, b) => a.order - b.order); + } else if (tableConfig.columns && tableConfig.columns.length > 0) { + // displayColumns가 없으면 기본 컬럼 사용 + console.log("🎯 tableConfig.columns 사용:", tableConfig.columns); columns = tableConfig.columns .filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -642,16 +803,8 @@ export const TableListComponent: React.FC = ({ }) .sort((a, b) => a.order - b.order); } else { - columns = displayColumns - .filter((col) => { - // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 - if (isDesignMode) { - return col.visible; // 디자인 모드에서는 visible만 체크 - } else { - return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 - } - }) - .sort((a, b) => a.order - b.order); + console.log("🎯 사용할 컬럼이 없음"); + return []; } // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 @@ -677,8 +830,14 @@ export const TableListComponent: React.FC = ({ } } + console.log("🎯 최종 visibleColumns:", columns); + console.log("🎯 visibleColumns 개수:", columns.length); + console.log( + "🎯 visibleColumns 컬럼명들:", + columns.map((c) => c.columnName), + ); return columns; - }, [displayColumns, tableConfig.columns, tableConfig.checkbox]); + }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 // 기존 테이블에서만 필요한 경우 다시 추가 가능 @@ -1050,7 +1209,21 @@ export const TableListComponent: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + : (() => { + // 🎯 매핑된 컬럼명으로 데이터 찾기 + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + const cellValue = row[mappedColumnName]; + if (index === 0) { + // 첫 번째 행만 로그 출력 + console.log( + `🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, + cellValue, + "전체 row:", + row, + ); + } + return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + })()} ))} diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 6a9007d5..a9b7f042 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -244,12 +244,12 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", [...(config.columns || []), newColumn]); }; - // 🎯 엔티티 컬럼 추가 (컬럼 설정 패널에서 표시 컬럼 선택) + // 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리) const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias); if (existingColumn) return; - // 기본 표시명으로 엔티티 컬럼 추가 (컬럼 설정 패널에서 나중에 표시 컬럼 조합 선택) + // 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false) const newColumn: ColumnConfig = { columnName: joinColumn.joinAlias, displayName: joinColumn.columnLabel, @@ -259,23 +259,11 @@ export const TableListConfigPanel: React.FC = ({ align: "left", format: "text", order: config.columns?.length || 0, - isEntityJoin: true, - entityJoinInfo: { - sourceTable: config.selectedTable || "", - sourceColumn: joinColumn.columnName, - joinAlias: joinColumn.joinAlias, - }, - // 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능) - entityDisplayConfig: { - displayColumns: [], // 빈 배열로 초기화 - separator: " - ", - sourceTable: config.selectedTable || "", - joinTable: joinColumn.tableName, - }, + isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님 }; handleChange("columns", [...(config.columns || []), newColumn]); - console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn); + console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn); }; // 컬럼 제거 @@ -577,14 +565,6 @@ export const TableListConfigPanel: React.FC = ({ updatedColumn: updatedColumns.find((col) => col.columnName === columnName), }); } - - // 컬럼 설정 업데이트 - updateColumn(columnName, { - entityDisplayConfig: { - ...config.entityDisplayConfig, - displayColumns: newSelectedColumns, - }, - }); }; // 🎯 엔티티 표시 구분자 업데이트