From 86dc9619682f5d859e3ab20d0503d4ebe60b4059 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 12:56:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/tableMng/page.tsx | 1 - .../table-list/SingleTableWithSticky.tsx | 6 +- .../table-list/TableListComponent.tsx | 157 +++++++++++++++--- 3 files changed, 138 insertions(+), 26 deletions(-) diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 9a83277a..3c95f4df 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -866,7 +866,6 @@ export default function TableManagementPage() { )} - {/* 설정 완료 표시 - 간소화 */} diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index 51cfdb6b..eeedfaaa 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -64,7 +64,7 @@ export const SingleTableWithSticky: React.FC = ({ return ( = ({ ) : ( data.map((row, index) => ( = ({ return ( = ({ const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); @@ -311,7 +311,7 @@ export const TableListComponent: React.FC = ({ console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); return { - sourceTable: sourceTable, + sourceTable: sourceTable || tableConfig.selectedTable || "unknown", sourceColumn: sourceColumn, joinAlias: col.columnName, }; @@ -427,7 +427,7 @@ export const TableListComponent: React.FC = ({ // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) const codeColumns = Object.entries(columnMeta).filter( - ([_, meta]) => meta.webType === "code" && meta.codeCategory, + ([, meta]) => meta.webType === "code" && meta.codeCategory, ); if (codeColumns.length > 0) { @@ -462,16 +462,81 @@ export const TableListComponent: React.FC = ({ 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 - }; + // 🎯 조인 컬럼 매핑 테이블 동적 생성 (사용자 설정 → API 응답) + const newJoinColumnMapping: Record = {}; + + // 사용자가 설정한 컬럼들과 실제 API 응답 컬럼들을 동적으로 매핑 + processedColumns.forEach((userColumn) => { + // 체크박스는 제외 + if (userColumn.columnName === "__checkbox__") return; + + console.log(`🔍 컬럼 매핑 분석: "${userColumn.columnName}"`, { + displayName: userColumn.displayName, + isEntityJoin: userColumn.isEntityJoin, + entityJoinInfo: userColumn.entityJoinInfo, + available: actualApiColumns, + }); + + // 사용자 설정 컬럼명이 API 응답에 정확히 있는지 확인 + if (actualApiColumns.includes(userColumn.columnName)) { + // 직접 매칭되는 경우 + newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; + console.log(`✅ 정확 매핑: ${userColumn.columnName} → ${userColumn.columnName}`); + } else { + // Entity 조인된 컬럼이거나 조인 탭에서 추가한 컬럼인 경우 + let foundMatch = false; + + // 1. Entity 조인 정보가 있는 경우 aliasColumn 우선 확인 + if (userColumn.entityJoinInfo?.joinAlias) { + const aliasColumn = userColumn.entityJoinInfo.joinAlias; + if (actualApiColumns.includes(aliasColumn)) { + newJoinColumnMapping[userColumn.columnName] = aliasColumn; + console.log(`🔗 Entity 별칭 매핑: ${userColumn.columnName} → ${aliasColumn}`); + foundMatch = true; + } + } + + // 2. 정확한 이름 매칭 (예: dept_code_company_name) + if (!foundMatch) { + const exactMatches = actualApiColumns.filter((apiCol) => apiCol === userColumn.columnName); + + if (exactMatches.length > 0) { + newJoinColumnMapping[userColumn.columnName] = exactMatches[0]; + console.log(`🎯 정확 이름 매핑: ${userColumn.columnName} → ${exactMatches[0]}`); + foundMatch = true; + } + } + + // 3. 부분 문자열 매칭 (컬럼명에 일부가 포함된 경우) + if (!foundMatch) { + const partialMatches = actualApiColumns.filter( + (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), + ); + + if (partialMatches.length > 0) { + // 가장 길이가 비슷한 것 선택 + const bestMatch = partialMatches.reduce((best, current) => + Math.abs(current.length - userColumn.columnName.length) < + Math.abs(best.length - userColumn.columnName.length) + ? current + : best, + ); + + newJoinColumnMapping[userColumn.columnName] = bestMatch; + console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); + foundMatch = true; + } + } + + // 4. 매칭 실패한 경우 원본 유지 (하지만 경고 표시) + if (!foundMatch) { + newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; + console.warn( + `⚠️ 매핑 실패: "${userColumn.columnName}" - 사용 가능한 컬럼: [${actualApiColumns.join(", ")}]`, + ); + } + } + }); // 🎯 조인 컬럼 매핑 상태 업데이트 setJoinColumnMapping(newJoinColumnMapping); @@ -533,11 +598,23 @@ export const TableListComponent: React.FC = ({ if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { // 원본 컬럼을 조인된 컬럼으로 교체 - const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); + let originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); if (originalColumnIndex !== -1) { console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); const originalColumn = processedColumns[originalColumnIndex]; + + // 🚨 중복 방지: 이미 같은 aliasColumn이 있는지 확인 + const existingAliasIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.aliasColumn); + if (existingAliasIndex !== -1 && existingAliasIndex !== originalColumnIndex) { + console.warn(`🚨 중복 컬럼 발견: ${joinConfig.aliasColumn}이 이미 존재합니다. 중복 제거합니다.`); + processedColumns.splice(existingAliasIndex, 1); + // 인덱스 재조정 + if (existingAliasIndex < originalColumnIndex) { + originalColumnIndex--; + } + } + processedColumns[originalColumnIndex] = { ...originalColumn, columnName: joinConfig.aliasColumn, // dept_code → dept_code_name @@ -583,9 +660,26 @@ export const TableListComponent: React.FC = ({ processedColumns = autoColumns; } + // 🚨 processedColumns에서 중복 제거 + const uniqueProcessedColumns = processedColumns.filter( + (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, + ); + + if (uniqueProcessedColumns.length !== processedColumns.length) { + console.error("🚨 processedColumns에서 중복 발견:"); + console.error( + "원본:", + processedColumns.map((c) => c.columnName), + ); + console.error( + "중복 제거 후:", + uniqueProcessedColumns.map((c) => c.columnName), + ); + } + // 🎯 표시할 컬럼 상태 업데이트 - setDisplayColumns(processedColumns); - console.log("🎯 displayColumns 업데이트됨:", processedColumns); + setDisplayColumns(uniqueProcessedColumns); + console.log("🎯 displayColumns 업데이트됨:", uniqueProcessedColumns); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 데이터:", result.data); } @@ -836,6 +930,25 @@ export const TableListComponent: React.FC = ({ "🎯 visibleColumns 컬럼명들:", columns.map((c) => c.columnName), ); + + // 🚨 중복 키 검사 + const columnNames = columns.map((c) => c.columnName); + const duplicates = columnNames.filter((name, index) => columnNames.indexOf(name) !== index); + if (duplicates.length > 0) { + console.error("🚨 중복된 컬럼명 발견:", duplicates); + console.error("🚨 전체 컬럼명 목록:", columnNames); + + // 중복 제거 + const uniqueColumns = columns.filter( + (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, + ); + console.log( + "🔧 중복 제거 후 컬럼들:", + uniqueColumns.map((c) => c.columnName), + ); + return uniqueColumns; + } + return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); @@ -1084,7 +1197,7 @@ export const TableListComponent: React.FC = ({ onClearFilters={handleClearAdvancedFilters} tableColumns={visibleColumns.map((col) => ({ columnName: col.columnName, - webType: columnMeta[col.columnName]?.webType || "text", + webType: (columnMeta[col.columnName]?.webType as any) || "text", displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, @@ -1138,9 +1251,9 @@ export const TableListComponent: React.FC = ({ - {visibleColumns.map((column) => ( + {visibleColumns.map((column, colIndex) => ( = ({ ) : ( data.map((row, index) => ( = ({ style={{ minHeight: "40px", height: "40px", lineHeight: "1" }} onClick={() => handleRowClick(row)} > - {visibleColumns.map((column) => ( + {visibleColumns.map((column, colIndex) => ( From 649ed5c6d71cfbce67c23364c52745a28df275af Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 14:31:46 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EC=88=98=EC=A0=95(=EC=A1=B0=EC=9D=B8=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=8B=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=ED=91=9C=EC=8B=9C=20=EC=98=A4=EB=A5=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 110 +++++++++++--- .../src/services/tableManagementService.ts | 25 +++- .../table-list/SingleTableWithSticky.tsx | 22 ++- .../table-list/TableListComponent.tsx | 137 +++++++++++++----- 4 files changed, 238 insertions(+), 56 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index d0b01846..56633952 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -42,9 +42,23 @@ export class EntityJoinService { }, }); + logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); + entityColumns.forEach((col, index) => { + logger.info( + ` ${index + 1}. ${col.column_name} -> ${col.reference_table}.${col.reference_column} (display: ${col.display_column})` + ); + }); + const joinConfigs: EntityJoinConfig[] = []; for (const column of entityColumns) { + logger.info(`🔍 Entity 컬럼 상세 정보:`, { + column_name: column.column_name, + reference_table: column.reference_table, + reference_column: column.reference_column, + display_column: column.display_column, + }); + if ( !column.column_name || !column.reference_table || @@ -58,6 +72,12 @@ export class EntityJoinService { let displayColumns: string[] = []; let separator = " - "; + logger.info(`🔍 조건 확인 - 컬럼: ${column.column_name}`, { + hasScreenConfig: !!screenConfig, + hasDisplayColumns: screenConfig?.displayColumns, + displayColumn: column.display_column, + }); + if (screenConfig && screenConfig.displayColumns) { // 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원) displayColumns = screenConfig.displayColumns; @@ -70,9 +90,12 @@ export class EntityJoinService { } else if (column.display_column && column.display_column !== "none") { // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) displayColumns = [column.display_column]; + logger.info( + `🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}` + ); } else { - // 조인 탭에서 보여줄 기본 표시 컬럼 설정 - // dept_info 테이블의 경우 dept_name을 기본으로 사용 + // display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정 + // 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용 let defaultDisplayColumn = column.reference_column; if (column.reference_table === "dept_info") { defaultDisplayColumn = "dept_name"; @@ -83,9 +106,10 @@ export class EntityJoinService { } displayColumns = [defaultDisplayColumn]; - console.log( - `🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` + logger.info( + `🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` ); + logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns); } // 별칭 컬럼명 생성 (writer -> writer_name) @@ -102,13 +126,32 @@ export class EntityJoinService { separator: separator, }; + logger.info(`🔧 기본 조인 설정 생성:`, { + sourceTable: joinConfig.sourceTable, + sourceColumn: joinConfig.sourceColumn, + referenceTable: joinConfig.referenceTable, + aliasColumn: joinConfig.aliasColumn, + displayColumns: joinConfig.displayColumns, + }); + // 조인 설정 유효성 검증 + logger.info( + `🔍 조인 설정 검증 중: ${joinConfig.sourceColumn} -> ${joinConfig.referenceTable}` + ); if (await this.validateJoinConfig(joinConfig)) { joinConfigs.push(joinConfig); + logger.info(`✅ 조인 설정 추가됨: ${joinConfig.aliasColumn}`); + } else { + logger.warn(`❌ 조인 설정 검증 실패: ${joinConfig.sourceColumn}`); } } - logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + logger.info(`🎯 Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable}.${config.referenceColumn} AS ${config.aliasColumn}` + ); + }); return joinConfigs; } catch (error) { logger.error(`Entity 조인 감지 실패: ${tableName}`, error); @@ -273,7 +316,7 @@ export class EntityJoinService { .filter(Boolean) .join("\n"); - logger.debug(`생성된 Entity 조인 쿼리:`, query); + logger.info(`🔍 생성된 Entity 조인 쿼리:`, query); return { query: query, aliasMap: aliasMap, @@ -303,10 +346,18 @@ export class EntityJoinService { } // 참조 테이블의 캐시 가능성 확인 + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 확인용 표시 컬럼: ${config.referenceTable} - ${displayCol}` + ); + const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn || config.displayColumns[0] + displayCol ); return cachedData ? "cache" : "join"; @@ -336,6 +387,14 @@ export class EntityJoinService { */ private async validateJoinConfig(config: EntityJoinConfig): Promise { try { + logger.info("🔍 조인 설정 검증 상세:", { + sourceColumn: config.sourceColumn, + referenceTable: config.referenceTable, + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + aliasColumn: config.aliasColumn, + }); + // 참조 테이블 존재 확인 const tableExists = await prisma.$queryRaw` SELECT 1 FROM information_schema.tables @@ -350,23 +409,32 @@ export class EntityJoinService { // 참조 컬럼 존재 확인 (displayColumns[0] 사용) const displayColumn = config.displayColumns?.[0] || config.displayColumn; - if (!displayColumn) { - logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`); - return false; - } + logger.info( + `🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})` + ); - const columnExists = await prisma.$queryRaw` - SELECT 1 FROM information_schema.columns - WHERE table_name = ${config.referenceTable} - AND column_name = ${displayColumn} - LIMIT 1 - `; + // 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용 + if (displayColumn && displayColumn !== "none") { + const columnExists = await prisma.$queryRaw` + SELECT 1 FROM information_schema.columns + WHERE table_name = ${config.referenceTable} + AND column_name = ${displayColumn} + LIMIT 1 + `; - if (!Array.isArray(columnExists) || columnExists.length === 0) { - logger.warn( - `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` + if (!Array.isArray(columnExists) || columnExists.length === 0) { + logger.warn( + `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` + ); + return false; + } + logger.info( + `✅ 표시 컬럼 확인 완료: ${config.referenceTable}.${displayColumn}` + ); + } else { + logger.info( + `🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음` ); - return false; } return true; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 5cb2853d..1278c1c3 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2049,6 +2049,17 @@ export class TableManagementService { options.screenEntityConfigs ); + logger.info( + `🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정` + ); + if (joinConfigs.length > 0) { + joinConfigs.forEach((config, index) => { + logger.info( + ` 조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}` + ); + }); + } + // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( options.additionalJoinColumns && @@ -2057,6 +2068,10 @@ export class TableManagementService { logger.info( `추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개` ); + logger.info( + "📋 전달받은 additionalJoinColumns:", + options.additionalJoinColumns + ); for (const additionalColumn of options.additionalJoinColumns) { // 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기 @@ -2251,10 +2266,18 @@ export class TableManagementService { try { // 캐시 데이터 미리 로드 for (const config of joinConfigs) { + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}` + ); + await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn || config.displayColumns[0] + displayCol ); } diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index eeedfaaa..5687bdba 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -22,6 +22,7 @@ interface SingleTableWithStickyProps { renderCheckboxCell: (row: any, index: number) => React.ReactNode; formatCellValue: (value: any, format?: string, columnName?: string) => string; getColumnWidth: (column: ColumnConfig) => number; + joinColumnMapping: Record; // 조인 컬럼 매핑 추가 } export const SingleTableWithSticky: React.FC = ({ @@ -39,6 +40,7 @@ export const SingleTableWithSticky: React.FC = ({ renderCheckboxCell, formatCellValue, getColumnWidth, + joinColumnMapping, }) => { const checkboxConfig = tableConfig.checkbox || {}; @@ -174,7 +176,25 @@ export const SingleTableWithSticky: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + : (() => { + // 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직) + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + + // 조인 컬럼 매핑 정보 로깅 + if (column.columnName !== mappedColumnName && index === 0) { + console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); + } + + const cellValue = row[mappedColumnName]; + if (index === 0) { + // 첫 번째 행만 로그 출력 (디버깅용) + console.log( + `🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, + cellValue, + ); + } + return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + })()} ); })} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 2c5b1dd1..10416855 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -290,28 +290,39 @@ export const TableListComponent: React.FC = ({ // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) ...joinTabColumns .filter((col) => { - // 실제 API 응답에 존재하는 컬럼만 필터링 - const validJoinColumns = ["dept_code_name", "dept_name"]; - const isValid = validJoinColumns.includes(col.columnName); - if (!isValid) { - console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); + // 조인 컬럼인지 확인 (언더스코어가 포함된 컬럼) + const isJoinColumn = col.columnName.includes("_") && col.columnName !== "__checkbox__"; + if (!isJoinColumn) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (조인 컬럼이 아님)`); } - return isValid; + return isJoinColumn; }) .map((col) => { - // 실제 존재하는 조인 컬럼만 처리 - let sourceTable = tableConfig.selectedTable; - let sourceColumn = col.columnName; + // 동적으로 조인 컬럼 정보 추출 + console.log(`🔍 조인 컬럼 분석: ${col.columnName}`); - if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { - sourceTable = "dept_info"; + // 컬럼명에서 기본 컬럼과 참조 테이블 추출 + // 예: dept_code_company_name -> dept_code (기본), company_name (참조) + const parts = col.columnName.split("_"); + let sourceColumn = ""; + let referenceTable = ""; + + // dept_code로 시작하는 경우 + if (col.columnName.startsWith("dept_code_")) { sourceColumn = "dept_code"; + referenceTable = "dept_info"; + } + // 다른 패턴들도 추가 가능 + else { + // 기본적으로 첫 번째 부분을 소스 컬럼으로 사용 + sourceColumn = parts[0]; + referenceTable = tableConfig.selectedTable || "unknown"; } - console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); + console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); return { - sourceTable: sourceTable || tableConfig.selectedTable || "unknown", + sourceTable: referenceTable, sourceColumn: sourceColumn, joinAlias: col.columnName, }; @@ -410,6 +421,14 @@ export const TableListComponent: React.FC = ({ console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 페이지:", result.totalPages); console.log("🎯 총 아이템:", result.total); + + // 🚨 데이터 샘플 확인 (첫 번째 행의 모든 컬럼과 값) + if (result.data && result.data.length > 0) { + console.log("🔍 첫 번째 행 데이터 샘플:", result.data[0]); + Object.entries(result.data[0]).forEach(([key, value]) => { + console.log(` 📊 ${key}: "${value}" (타입: ${typeof value})`); + }); + } setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -507,24 +526,62 @@ export const TableListComponent: React.FC = ({ } } - // 3. 부분 문자열 매칭 (컬럼명에 일부가 포함된 경우) + // 3. 조인 컬럼 검증 및 처리 if (!foundMatch) { - const partialMatches = actualApiColumns.filter( - (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), - ); + // 🚨 조인 컬럼인지 확인 (더 정확한 감지 로직) + const hasUnderscore = userColumn.columnName.includes("_"); + let isJoinColumn = false; + let baseColumnName = ""; - if (partialMatches.length > 0) { - // 가장 길이가 비슷한 것 선택 - const bestMatch = partialMatches.reduce((best, current) => - Math.abs(current.length - userColumn.columnName.length) < - Math.abs(best.length - userColumn.columnName.length) - ? current - : best, + if (hasUnderscore) { + // 가능한 모든 기본 컬럼명을 확인 (dept_code_company_name -> dept_code, dept 순으로) + const parts = userColumn.columnName.split("_"); + for (let i = parts.length - 1; i >= 1; i--) { + const possibleBase = parts.slice(0, i).join("_"); + if (actualApiColumns.includes(possibleBase)) { + baseColumnName = possibleBase; + isJoinColumn = true; + break; + } + } + } + + console.log(`🔍 조인 컬럼 검사: "${userColumn.columnName}"`, { + hasUnderscore, + baseColumnName, + isJoinColumn, + }); + + if (isJoinColumn) { + console.log(`🔍 조인 컬럼 기본 컬럼 확인: "${baseColumnName}"`, { + existsInApi: actualApiColumns.includes(baseColumnName), + actualApiColumns: actualApiColumns.slice(0, 10), // 처음 10개만 표시 + }); + + console.warn( + `⚠️ 조인 실패: "${userColumn.columnName}" - 백엔드에서 Entity 조인이 실행되지 않음. 기본 컬럼값 표시합니다.`, + ); + // 조인 실패 시 기본 컬럼값을 표시하도록 매핑 + newJoinColumnMapping[userColumn.columnName] = baseColumnName; + foundMatch = true; + } else { + // 일반 컬럼인 경우 부분 매칭 시도 + const partialMatches = actualApiColumns.filter( + (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), ); - newJoinColumnMapping[userColumn.columnName] = bestMatch; - console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); - foundMatch = true; + if (partialMatches.length > 0) { + const bestMatch = partialMatches.reduce((best, current) => + Math.abs(current.length - userColumn.columnName.length) < + Math.abs(best.length - userColumn.columnName.length) + ? current + : best, + ); + + newJoinColumnMapping[userColumn.columnName] = bestMatch; + console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); + foundMatch = true; + } } } @@ -1245,6 +1302,7 @@ export const TableListComponent: React.FC = ({ renderCheckboxCell={renderCheckboxCell} formatCellValue={formatCellValue} getColumnWidth={getColumnWidth} + joinColumnMapping={joinColumnMapping} /> ) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) @@ -1325,15 +1383,28 @@ export const TableListComponent: React.FC = ({ : (() => { // 🎯 매핑된 컬럼명으로 데이터 찾기 const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + + // 조인 컬럼 매핑 정보 로깅 + if (column.columnName !== mappedColumnName && index === 0) { + console.log(`🔗 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); + } + const cellValue = row[mappedColumnName]; if (index === 0) { // 첫 번째 행만 로그 출력 - console.log( - `🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, - cellValue, - "전체 row:", - row, - ); + console.log(`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, cellValue); + + // 🚨 조인된 컬럼인 경우 추가 디버깅 + if (column.columnName !== mappedColumnName) { + console.log(" 🔗 조인 컬럼 분석:"); + console.log(` 👤 사용자 설정 컬럼: "${column.columnName}"`); + console.log(` 📡 매핑된 API 컬럼: "${mappedColumnName}"`); + console.log(` 📋 컬럼 라벨: "${column.displayName}"`); + console.log(` 💾 실제 데이터: "${cellValue}"`); + console.log( + ` 🔄 원본 컬럼 데이터 (${column.columnName}): "${row[column.columnName]}"`, + ); + } } return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; })()} From 0d9ee4c40f9692381115a952b88a005b774c65bc Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 15:02:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=ED=91=9C=EC=8B=9C=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 6 ++ .../src/services/tableManagementService.ts | 73 ++++++++++++++++--- .../table-list/TableListComponent.tsx | 3 +- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 56633952..b88c0c8b 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -233,6 +233,9 @@ export class EntityJoinService { "master_sabun", "location", "data_type", + "company_name", + "sales_yn", + "status", ].includes(col); if (isJoinTableColumn) { @@ -256,6 +259,9 @@ export class EntityJoinService { "master_sabun", "location", "data_type", + "company_name", + "sales_yn", + "status", ].includes(col); if (isJoinTableColumn) { diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 1278c1c3..4ca5369d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2074,38 +2074,78 @@ export class TableManagementService { ); for (const additionalColumn of options.additionalJoinColumns) { - // 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기 + // 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기) const baseJoinConfig = joinConfigs.find( - (config) => config.referenceTable === additionalColumn.sourceTable + (config) => config.sourceColumn === additionalColumn.sourceColumn ); 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 joinAlias = additionalColumn.joinAlias; // dept_code_company_name + const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name + + logger.info(`🔍 조인 컬럼 상세 분석:`, { + sourceColumn, + joinAlias, + actualColumnName, + referenceTable: additionalColumn.sourceTable, + }); + + // 🚨 기본 Entity 조인과 중복되지 않도록 체크 + const isBasicEntityJoin = + additionalColumn.joinAlias === + `${baseJoinConfig.sourceColumn}_name`; + + if (isBasicEntityJoin) { + logger.info( + `⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀` + ); + continue; // 기본 Entity 조인과 중복되면 추가하지 않음 + } // 추가 조인 컬럼 설정 생성 const additionalJoinConfig: EntityJoinConfig = { sourceTable: tableName, sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code) - referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info) + referenceTable: + (additionalColumn as any).referenceTable || + baseJoinConfig.referenceTable, // 참조 테이블 (dept_info) referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code) - displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name) + displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name) displayColumn: actualColumnName, // 하위 호환성 - aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name) + aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name) separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); logger.info( - `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` + `✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` ); + logger.info(`🔍 추가된 조인 설정 상세:`, { + sourceTable: additionalJoinConfig.sourceTable, + sourceColumn: additionalJoinConfig.sourceColumn, + referenceTable: additionalJoinConfig.referenceTable, + displayColumns: additionalJoinConfig.displayColumns, + aliasColumn: additionalJoinConfig.aliasColumn, + }); } } } + // 최종 조인 설정 배열 로깅 + logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`, + { + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + } + ); + }); + if (joinConfigs.length === 0) { logger.info(`Entity 조인 설정이 없음: ${tableName}`); const basicResult = await this.getTableData(tableName, options); @@ -2119,8 +2159,21 @@ export class TableManagementService { } // 조인 전략 결정 (테이블 크기 기반) - const strategy = - await entityJoinService.determineJoinStrategy(joinConfigs); + // 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장) + let strategy: "full_join" | "cache_lookup" | "hybrid"; + + if ( + options.additionalJoinColumns && + options.additionalJoinColumns.length > 0 + ) { + strategy = "full_join"; + console.log( + `🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)` + ); + } else { + strategy = await entityJoinService.determineJoinStrategy(joinConfigs); + } + console.log( `🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)` ); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 10416855..6bd974ed 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -322,9 +322,10 @@ export const TableListComponent: React.FC = ({ console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); return { - sourceTable: referenceTable, + sourceTable: tableConfig.selectedTable || "unknown", // 기본 테이블 (user_info) sourceColumn: sourceColumn, joinAlias: col.columnName, + referenceTable: referenceTable, // 참조 테이블 정보도 추가 }; }), ];