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, - }, - }); }; // 🎯 엔티티 표시 구분자 업데이트