298 lines
8.2 KiB
TypeScript
298 lines
8.2 KiB
TypeScript
|
|
import { PrismaClient } from "@prisma/client";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
import {
|
||
|
|
EntityJoinConfig,
|
||
|
|
BatchLookupRequest,
|
||
|
|
BatchLookupResponse,
|
||
|
|
} from "../types/tableManagement";
|
||
|
|
|
||
|
|
const prisma = new PrismaClient();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Entity 조인 기능을 제공하는 서비스
|
||
|
|
* ID값을 의미있는 데이터로 자동 변환하는 스마트 테이블 시스템
|
||
|
|
*/
|
||
|
|
export class EntityJoinService {
|
||
|
|
/**
|
||
|
|
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
||
|
|
*/
|
||
|
|
async detectEntityJoins(tableName: string): Promise<EntityJoinConfig[]> {
|
||
|
|
try {
|
||
|
|
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
||
|
|
|
||
|
|
// column_labels에서 entity 타입인 컬럼들 조회
|
||
|
|
const entityColumns = await prisma.column_labels.findMany({
|
||
|
|
where: {
|
||
|
|
table_name: tableName,
|
||
|
|
web_type: "entity",
|
||
|
|
reference_table: { not: null },
|
||
|
|
reference_column: { not: null },
|
||
|
|
},
|
||
|
|
select: {
|
||
|
|
column_name: true,
|
||
|
|
reference_table: true,
|
||
|
|
reference_column: true,
|
||
|
|
display_column: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const joinConfigs: EntityJoinConfig[] = [];
|
||
|
|
|
||
|
|
for (const column of entityColumns) {
|
||
|
|
if (
|
||
|
|
!column.column_name ||
|
||
|
|
!column.reference_table ||
|
||
|
|
!column.reference_column
|
||
|
|
) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// display_column이 없으면 reference_column 사용
|
||
|
|
const displayColumn = column.display_column || column.reference_column;
|
||
|
|
|
||
|
|
// 별칭 컬럼명 생성 (writer -> writer_name)
|
||
|
|
const aliasColumn = `${column.column_name}_name`;
|
||
|
|
|
||
|
|
const joinConfig: EntityJoinConfig = {
|
||
|
|
sourceTable: tableName,
|
||
|
|
sourceColumn: column.column_name,
|
||
|
|
referenceTable: column.reference_table,
|
||
|
|
referenceColumn: column.reference_column,
|
||
|
|
displayColumn: displayColumn,
|
||
|
|
aliasColumn: aliasColumn,
|
||
|
|
};
|
||
|
|
|
||
|
|
// 조인 설정 유효성 검증
|
||
|
|
if (await this.validateJoinConfig(joinConfig)) {
|
||
|
|
joinConfigs.push(joinConfig);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`);
|
||
|
|
return joinConfigs;
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`Entity 조인 감지 실패: ${tableName}`, error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Entity 조인이 포함된 SQL 쿼리 생성
|
||
|
|
*/
|
||
|
|
buildJoinQuery(
|
||
|
|
tableName: string,
|
||
|
|
joinConfigs: EntityJoinConfig[],
|
||
|
|
selectColumns: string[],
|
||
|
|
whereClause: string = "",
|
||
|
|
orderBy: string = "",
|
||
|
|
limit?: number,
|
||
|
|
offset?: number
|
||
|
|
): string {
|
||
|
|
try {
|
||
|
|
// 기본 SELECT 컬럼들
|
||
|
|
const baseColumns = selectColumns.map((col) => `main.${col}`).join(", ");
|
||
|
|
|
||
|
|
// Entity 조인 컬럼들
|
||
|
|
const joinColumns = joinConfigs
|
||
|
|
.map(
|
||
|
|
(config) =>
|
||
|
|
`${config.referenceTable.substring(0, 3)}.${config.displayColumn} AS ${config.aliasColumn}`
|
||
|
|
)
|
||
|
|
.join(", ");
|
||
|
|
|
||
|
|
// SELECT 절 구성
|
||
|
|
const selectClause = joinColumns
|
||
|
|
? `${baseColumns}, ${joinColumns}`
|
||
|
|
: baseColumns;
|
||
|
|
|
||
|
|
// 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
|
||
|
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||
|
|
})
|
||
|
|
.join("\n");
|
||
|
|
|
||
|
|
// WHERE 절
|
||
|
|
const whereSQL = whereClause ? `WHERE ${whereClause}` : "";
|
||
|
|
|
||
|
|
// ORDER BY 절
|
||
|
|
const orderSQL = orderBy ? `ORDER BY ${orderBy}` : "";
|
||
|
|
|
||
|
|
// LIMIT 및 OFFSET
|
||
|
|
let limitSQL = "";
|
||
|
|
if (limit !== undefined) {
|
||
|
|
limitSQL = `LIMIT ${limit}`;
|
||
|
|
if (offset !== undefined) {
|
||
|
|
limitSQL += ` OFFSET ${offset}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 최종 쿼리 조합
|
||
|
|
const query = [
|
||
|
|
`SELECT ${selectClause}`,
|
||
|
|
fromClause,
|
||
|
|
joinClauses,
|
||
|
|
whereSQL,
|
||
|
|
orderSQL,
|
||
|
|
limitSQL,
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join("\n");
|
||
|
|
|
||
|
|
logger.debug(`생성된 Entity 조인 쿼리:`, query);
|
||
|
|
return query;
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("Entity 조인 쿼리 생성 실패", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 조인 설정 유효성 검증
|
||
|
|
*/
|
||
|
|
private async validateJoinConfig(config: EntityJoinConfig): Promise<boolean> {
|
||
|
|
try {
|
||
|
|
// 참조 테이블 존재 확인
|
||
|
|
const tableExists = await prisma.$queryRaw`
|
||
|
|
SELECT 1 FROM information_schema.tables
|
||
|
|
WHERE table_name = ${config.referenceTable}
|
||
|
|
LIMIT 1
|
||
|
|
`;
|
||
|
|
|
||
|
|
if (!Array.isArray(tableExists) || tableExists.length === 0) {
|
||
|
|
logger.warn(`참조 테이블이 존재하지 않음: ${config.referenceTable}`);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 참조 컬럼 존재 확인
|
||
|
|
const columnExists = await prisma.$queryRaw`
|
||
|
|
SELECT 1 FROM information_schema.columns
|
||
|
|
WHERE table_name = ${config.referenceTable}
|
||
|
|
AND column_name = ${config.displayColumn}
|
||
|
|
LIMIT 1
|
||
|
|
`;
|
||
|
|
|
||
|
|
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||
|
|
logger.warn(
|
||
|
|
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}`
|
||
|
|
);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("조인 설정 검증 실패", error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 카운트 쿼리 생성 (페이징용)
|
||
|
|
*/
|
||
|
|
buildCountQuery(
|
||
|
|
tableName: string,
|
||
|
|
joinConfigs: EntityJoinConfig[],
|
||
|
|
whereClause: string = ""
|
||
|
|
): string {
|
||
|
|
try {
|
||
|
|
// JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요)
|
||
|
|
const joinClauses = joinConfigs
|
||
|
|
.map((config, index) => {
|
||
|
|
const alias = config.referenceTable.substring(0, 3);
|
||
|
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||
|
|
})
|
||
|
|
.join("\n");
|
||
|
|
|
||
|
|
// WHERE 절
|
||
|
|
const whereSQL = whereClause ? `WHERE ${whereClause}` : "";
|
||
|
|
|
||
|
|
// COUNT 쿼리 조합
|
||
|
|
const query = [
|
||
|
|
`SELECT COUNT(*) as total`,
|
||
|
|
`FROM ${tableName} main`,
|
||
|
|
joinClauses,
|
||
|
|
whereSQL,
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join("\n");
|
||
|
|
|
||
|
|
return query;
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("COUNT 쿼리 생성 실패", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 참조 테이블의 컬럼 목록 조회 (UI용)
|
||
|
|
*/
|
||
|
|
async getReferenceTableColumns(tableName: string): Promise<
|
||
|
|
Array<{
|
||
|
|
columnName: string;
|
||
|
|
displayName: string;
|
||
|
|
dataType: string;
|
||
|
|
}>
|
||
|
|
> {
|
||
|
|
try {
|
||
|
|
const columns = (await prisma.$queryRaw`
|
||
|
|
SELECT
|
||
|
|
column_name,
|
||
|
|
column_name as display_name,
|
||
|
|
data_type
|
||
|
|
FROM information_schema.columns
|
||
|
|
WHERE table_name = ${tableName}
|
||
|
|
AND data_type IN ('character varying', 'varchar', 'text', 'char')
|
||
|
|
ORDER BY ordinal_position
|
||
|
|
`) as Array<{
|
||
|
|
column_name: string;
|
||
|
|
display_name: string;
|
||
|
|
data_type: string;
|
||
|
|
}>;
|
||
|
|
|
||
|
|
return columns.map((col) => ({
|
||
|
|
columnName: col.column_name,
|
||
|
|
displayName: col.display_name,
|
||
|
|
dataType: col.data_type,
|
||
|
|
}));
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`참조 테이블 컬럼 조회 실패: ${tableName}`, error);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Entity 조인 전략 결정 (full_join vs cache_lookup)
|
||
|
|
*/
|
||
|
|
async determineJoinStrategy(
|
||
|
|
joinConfigs: EntityJoinConfig[]
|
||
|
|
): Promise<"full_join" | "cache_lookup"> {
|
||
|
|
try {
|
||
|
|
// 참조 테이블 크기 확인
|
||
|
|
for (const config of joinConfigs) {
|
||
|
|
const result = (await prisma.$queryRawUnsafe(`
|
||
|
|
SELECT COUNT(*) as count
|
||
|
|
FROM ${config.referenceTable}
|
||
|
|
`)) as Array<{ count: bigint }>;
|
||
|
|
|
||
|
|
const count = Number(result[0]?.count || 0);
|
||
|
|
|
||
|
|
// 1000건 이상이면 조인 방식 사용
|
||
|
|
if (count > 1000) {
|
||
|
|
return "full_join";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return "cache_lookup";
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("조인 전략 결정 실패", error);
|
||
|
|
return "full_join"; // 기본값
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const entityJoinService = new EntityJoinService();
|