From ebc3fa60dcbcf1df13d99ed1a5dc3ff44a145c08 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 17 Sep 2025 11:15:34 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=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 | 88 ++++++++- .../src/services/tableManagementService.ts | 187 ++++++++++++++++-- 2 files changed, 252 insertions(+), 23 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 5de783f1..f84cf167 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -88,16 +88,51 @@ export class EntityJoinService { orderBy: string = "", limit?: number, offset?: number - ): string { + ): { query: string; aliasMap: Map } { try { // 기본 SELECT 컬럼들 const baseColumns = selectColumns.map((col) => `main.${col}`).join(", "); // Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리) + // 별칭 매핑 생성 (JOIN 절과 동일한 로직) + const aliasMap = new Map(); + const usedAliasesForColumns = new Set(); + + // joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성 + const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => { + if ( + !acc.some( + (existingConfig) => + existingConfig.referenceTable === config.referenceTable + ) + ) { + acc.push(config); + } + return acc; + }, [] as EntityJoinConfig[]); + + logger.info( + `🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블` + ); + + uniqueReferenceTableConfigs.forEach((config) => { + let baseAlias = config.referenceTable.substring(0, 3); + let alias = baseAlias; + let counter = 1; + + while (usedAliasesForColumns.has(alias)) { + alias = `${baseAlias}${counter}`; + counter++; + } + usedAliasesForColumns.add(alias); + aliasMap.set(config.referenceTable, alias); + logger.info(`🔧 별칭 생성: ${config.referenceTable} → ${alias}`); + }); + const joinColumns = joinConfigs .map( (config) => - `COALESCE(${config.referenceTable.substring(0, 3)}.${config.displayColumn}, '') AS ${config.aliasColumn}` + `COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}` ) .join(", "); @@ -109,10 +144,10 @@ export class EntityJoinService { // FROM 절 (메인 테이블) const fromClause = `FROM ${tableName} main`; - // LEFT JOIN 절들 - const joinClauses = joinConfigs - .map((config, index) => { - const alias = config.referenceTable.substring(0, 3); // user_info -> use, companies -> com + // LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 중복 테이블 제거) + const joinClauses = uniqueReferenceTableConfigs + .map((config) => { + const alias = aliasMap.get(config.referenceTable); return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`; }) .join("\n"); @@ -145,7 +180,10 @@ export class EntityJoinService { .join("\n"); logger.debug(`생성된 Entity 조인 쿼리:`, query); - return query; + return { + query: query, + aliasMap: aliasMap, + }; } catch (error) { logger.error("Entity 조인 쿼리 생성 실패", error); throw error; @@ -238,10 +276,40 @@ export class EntityJoinService { whereClause: string = "" ): string { try { + // 별칭 매핑 생성 (buildJoinQuery와 동일한 로직) + const aliasMap = new Map(); + const usedAliases = new Set(); + + // joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성 + const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => { + if ( + !acc.some( + (existingConfig) => + existingConfig.referenceTable === config.referenceTable + ) + ) { + acc.push(config); + } + return acc; + }, [] as EntityJoinConfig[]); + + uniqueReferenceTableConfigs.forEach((config) => { + let baseAlias = config.referenceTable.substring(0, 3); + let alias = baseAlias; + let counter = 1; + + while (usedAliases.has(alias)) { + alias = `${baseAlias}${counter}`; + counter++; + } + usedAliases.add(alias); + aliasMap.set(config.referenceTable, alias); + }); + // JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요) - const joinClauses = joinConfigs - .map((config, index) => { - const alias = config.referenceTable.substring(0, 3); + const joinClauses = uniqueReferenceTableConfigs + .map((config) => { + const alias = aliasMap.get(config.referenceTable); return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`; }) .join("\n"); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 97d72c40..5fb06ba7 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1546,16 +1546,7 @@ export class TableManagementService { } } catch (error) { logger.error(`Entity 조인 데이터 조회 실패: ${tableName}`, error); - - // 에러 발생 시 기본 데이터 반환 - const basicResult = await this.getTableData(tableName, options); - return { - data: basicResult.data, - total: basicResult.total, - page: options.page, - size: options.size, - totalPages: Math.ceil(basicResult.total / options.size), - }; + throw error; } } @@ -1582,7 +1573,7 @@ export class TableManagementService { orderBy, limit, offset - ); + ).query; // 카운트 쿼리 const countQuery = entityJoinService.buildCountQuery( @@ -1650,8 +1641,178 @@ export class TableManagementService { ); } - // 기본 데이터 조회 - const basicResult = await this.getTableData(tableName, options); + // Entity 조인 컬럼 검색이 있는지 확인 (기본 조인 + 추가 조인 컬럼 모두 포함) + const allEntityColumns = [ + ...joinConfigs.map((config) => config.aliasColumn), + // 추가 조인 컬럼들도 포함 (writer_dept_code, company_code_status 등) + ...joinConfigs.flatMap((config) => { + const additionalColumns = []; + // writer -> writer_dept_code 패턴 + if (config.sourceColumn === "writer") { + additionalColumns.push("writer_dept_code"); + } + // company_code -> company_code_status 패턴 + if (config.sourceColumn === "company_code") { + additionalColumns.push("company_code_status"); + } + return additionalColumns; + }), + ]; + + const hasEntitySearch = + options.search && + Object.keys(options.search).some((key) => + allEntityColumns.includes(key) + ); + + if (hasEntitySearch) { + const entitySearchKeys = options.search + ? Object.keys(options.search).filter((key) => + allEntityColumns.includes(key) + ) + : []; + logger.info( + `🔍 Entity 조인 컬럼 검색 감지: ${entitySearchKeys.join(", ")}` + ); + } + + let basicResult; + + if (hasEntitySearch) { + // Entity 조인 컬럼으로 검색하는 경우 SQL JOIN 방식 사용 + logger.info("🔍 Entity 조인 컬럼 검색 감지, SQL JOIN 방식으로 전환"); + + try { + // 테이블 컬럼 정보 조회 + const columns = await this.getTableColumns(tableName); + const selectColumns = columns.data.map((col: any) => col.column_name); + + // Entity 조인 컬럼 검색을 위한 WHERE 절 구성 + const whereConditions: string[] = []; + const entitySearchColumns: string[] = []; + + // Entity 조인 쿼리 생성하여 별칭 매핑 얻기 + const joinQueryResult = entityJoinService.buildJoinQuery( + tableName, + joinConfigs, + selectColumns, + "", // WHERE 절은 나중에 추가 + options.sortBy + ? `main.${options.sortBy} ${options.sortOrder || "ASC"}` + : undefined, + options.size, + (options.page - 1) * options.size + ); + + const aliasMap = joinQueryResult.aliasMap; + logger.info( + `🔧 [검색] 별칭 매핑 사용: ${Array.from(aliasMap.entries()) + .map(([table, alias]) => `${table}→${alias}`) + .join(", ")}` + ); + + if (options.search) { + for (const [key, value] of Object.entries(options.search)) { + const joinConfig = joinConfigs.find( + (config) => config.aliasColumn === key + ); + + if (joinConfig) { + // 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색 + const alias = aliasMap.get(joinConfig.referenceTable); + whereConditions.push( + `${alias}.${joinConfig.displayColumn} ILIKE '%${value}%'` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${value}%' (별칭: ${alias})` + ); + } else if (key === "writer_dept_code") { + // writer_dept_code: user_info.dept_code에서 검색 + const userAlias = aliasMap.get("user_info"); + whereConditions.push( + `${userAlias}.dept_code ILIKE '%${value}%'` + ); + entitySearchColumns.push(`${key} (user_info.dept_code)`); + logger.info( + `🎯 추가 Entity 조인 검색: ${key} → user_info.dept_code LIKE '%${value}%' (별칭: ${userAlias})` + ); + } else if (key === "company_code_status") { + // company_code_status: company_info.status에서 검색 + const companyAlias = aliasMap.get("company_info"); + whereConditions.push( + `${companyAlias}.status ILIKE '%${value}%'` + ); + entitySearchColumns.push(`${key} (company_info.status)`); + logger.info( + `🎯 추가 Entity 조인 검색: ${key} → company_info.status LIKE '%${value}%' (별칭: ${companyAlias})` + ); + } else { + // 일반 컬럼인 경우: 메인 테이블에서 검색 + whereConditions.push(`main.${key} ILIKE '%${value}%'`); + logger.info( + `🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${value}%'` + ); + } + } + } + + const whereClause = whereConditions.join(" AND "); + const orderBy = options.sortBy + ? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}` + : ""; + + // 페이징 계산 + const offset = (options.page - 1) * options.size; + + // SQL JOIN 쿼리 실행 + const joinResult = await this.executeJoinQuery( + tableName, + joinConfigs, + selectColumns, + whereClause, + orderBy, + options.size, + offset, + startTime + ); + + return joinResult; + } catch (joinError) { + logger.error( + `Entity 조인 검색 실패, 캐시 방식으로 폴백: ${tableName}`, + joinError + ); + + // Entity 조인 검색 실패 시 Entity 조인 컬럼을 제외한 검색 조건으로 캐시 방식 사용 + const fallbackOptions = { ...options }; + if (options.search) { + const filteredSearch: Record = {}; + + // Entity 조인 컬럼을 제외한 검색 조건만 유지 + for (const [key, value] of Object.entries(options.search)) { + const isEntityColumn = joinConfigs.some( + (config) => config.aliasColumn === key + ); + if (!isEntityColumn) { + filteredSearch[key] = value; + } + } + + fallbackOptions.search = filteredSearch; + logger.info( + `🔄 Entity 조인 에러 시 검색 조건 필터링: ${Object.keys(filteredSearch).join(", ")}` + ); + } + + basicResult = await this.getTableData(tableName, fallbackOptions); + } + } else { + // Entity 조인 컬럼 검색이 없는 경우 기존 캐시 방식 사용 + basicResult = await this.getTableData(tableName, options); + } // Entity 값들을 캐시에서 룩업하여 변환 const enhancedData = basicResult.data.map((row: any) => {