검색기능 수정

This commit is contained in:
kjs 2025-09-17 11:15:34 +09:00
parent 2aa4d83f33
commit ebc3fa60dc
2 changed files with 252 additions and 23 deletions

View File

@ -88,16 +88,51 @@ export class EntityJoinService {
orderBy: string = "",
limit?: number,
offset?: number
): string {
): { query: string; aliasMap: Map<string, string> } {
try {
// 기본 SELECT 컬럼들
const baseColumns = selectColumns.map((col) => `main.${col}`).join(", ");
// Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리)
// 별칭 매핑 생성 (JOIN 절과 동일한 로직)
const aliasMap = new Map<string, string>();
const usedAliasesForColumns = new Set<string>();
// 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<string, string>();
const usedAliases = new Set<string>();
// 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");

View File

@ -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<string, any> = {};
// 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) => {