feature/screen-management #39
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue