2025-10-01 12:10:34 +09:00
import { query , queryOne } from "../database/db" ;
2025-09-16 15:13:00 +09:00
import { logger } from "../utils/logger" ;
import {
EntityJoinConfig ,
BatchLookupRequest ,
BatchLookupResponse ,
} from "../types/tableManagement" ;
2025-09-16 16:53:03 +09:00
import { referenceCacheService } from "./referenceCacheService" ;
2025-09-16 15:13:00 +09:00
/ * *
* Entity 조 인 기 능 을 제 공 하 는 서 비 스
* ID값을 의 미 있 는 데 이 터 로 자 동 변 환 하 는 스 마 트 테 이 블 시 스 템
* /
export class EntityJoinService {
/ * *
* 테 이 블 의 Entity 컬 럼 들 을 감 지 하 여 조 인 설 정 생 성
2025-09-23 15:58:54 +09:00
* @param tableName 테 이 블 명
* @param screenEntityConfigs 화 면 별 엔 티 티 설 정 ( 선 택 사 항 )
2025-09-16 15:13:00 +09:00
* /
2025-09-23 15:58:54 +09:00
async detectEntityJoins (
2025-09-23 16:23:36 +09:00
tableName : string ,
2025-09-23 15:58:54 +09:00
screenEntityConfigs? : Record < string , any >
) : Promise < EntityJoinConfig [ ] > {
2025-09-16 15:13:00 +09:00
try {
logger . info ( ` Entity 컬럼 감지 시작: ${ tableName } ` ) ;
2025-11-12 14:02:58 +09:00
// column_labels에서 entity 및 category 타입인 컬럼들 조회 (input_type 사용)
2025-10-01 12:10:34 +09:00
const entityColumns = await query < {
column_name : string ;
2025-11-12 14:02:58 +09:00
input_type : string ;
2025-10-01 12:10:34 +09:00
reference_table : string ;
reference_column : string ;
display_column : string | null ;
} > (
2025-11-12 14:02:58 +09:00
` SELECT column_name, input_type, reference_table, reference_column, display_column
2025-10-01 12:10:34 +09:00
FROM column_labels
WHERE table_name = $1
2025-11-12 14:02:58 +09:00
AND input_type IN ( 'entity' , 'category' ) ` ,
[ tableName ]
2025-10-01 12:10:34 +09:00
) ;
2025-09-16 15:13:00 +09:00
2025-09-24 14:31:46 +09:00
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 } ) `
) ;
} ) ;
2025-09-16 15:13:00 +09:00
const joinConfigs : EntityJoinConfig [ ] = [ ] ;
2025-11-10 16:32:00 +09:00
// 🎯 writer 컬럼 자동 감지 및 조인 설정 추가
const tableColumns = await query < { column_name : string } > (
` SELECT column_name
FROM information_schema . columns
WHERE table_name = $1
AND table_schema = 'public'
AND column_name = 'writer' ` ,
[ tableName ]
) ;
if ( tableColumns . length > 0 ) {
const writerJoinConfig : EntityJoinConfig = {
sourceTable : tableName ,
sourceColumn : "writer" ,
referenceTable : "user_info" ,
referenceColumn : "user_id" ,
displayColumns : [ "user_name" ] ,
displayColumn : "user_name" ,
aliasColumn : "writer_name" ,
separator : " - " ,
} ;
2025-11-10 16:38:16 +09:00
if ( await this . validateJoinConfig ( writerJoinConfig ) ) {
2025-11-10 16:32:00 +09:00
joinConfigs . push ( writerJoinConfig ) ;
}
}
2025-09-16 15:13:00 +09:00
for ( const column of entityColumns ) {
2025-11-12 14:02:58 +09:00
// 카테고리 타입인 경우 자동으로 category_values 테이블 참조 설정
let referenceTable = column . reference_table ;
let referenceColumn = column . reference_column ;
let displayColumn = column . display_column ;
if ( column . input_type === 'category' ) {
// 카테고리 타입: reference 정보가 비어있어도 자동 설정
referenceTable = referenceTable || 'table_column_category_values' ;
referenceColumn = referenceColumn || 'value_code' ;
displayColumn = displayColumn || 'value_label' ;
logger . info ( ` 🏷️ 카테고리 타입 자동 설정: ${ column . column_name } ` , {
referenceTable ,
referenceColumn ,
displayColumn ,
} ) ;
}
2025-09-24 14:31:46 +09:00
logger . info ( ` 🔍 Entity 컬럼 상세 정보: ` , {
column_name : column.column_name ,
2025-11-12 14:02:58 +09:00
input_type : column.input_type ,
reference_table : referenceTable ,
reference_column : referenceColumn ,
display_column : displayColumn ,
2025-09-24 14:31:46 +09:00
} ) ;
2025-11-12 14:02:58 +09:00
if ( ! column . column_name || ! referenceTable || ! referenceColumn ) {
logger . warn ( ` ⚠️ 필수 정보 누락으로 스킵: ${ column . column_name } ` ) ;
2025-09-16 15:13:00 +09:00
continue ;
}
2025-09-23 15:58:54 +09:00
// 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용
const screenConfig = screenEntityConfigs ? . [ column . column_name ] ;
let displayColumns : string [ ] = [ ] ;
let separator = " - " ;
2025-09-23 16:23:36 +09:00
2025-09-24 14:31:46 +09:00
logger . info ( ` 🔍 조건 확인 - 컬럼: ${ column . column_name } ` , {
hasScreenConfig : ! ! screenConfig ,
hasDisplayColumns : screenConfig?.displayColumns ,
displayColumn : column.display_column ,
} ) ;
2025-09-23 15:58:54 +09:00
if ( screenConfig && screenConfig . displayColumns ) {
2025-09-23 17:43:24 +09:00
// 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원)
2025-09-23 15:58:54 +09:00
displayColumns = screenConfig . displayColumns ;
separator = screenConfig . separator || " - " ;
2025-09-23 17:43:24 +09:00
console . log ( ` 🎯 화면별 엔티티 설정 적용: ${ column . column_name } ` , {
displayColumns ,
separator ,
screenConfig ,
} ) ;
2025-11-12 14:02:58 +09:00
} else if ( displayColumn && displayColumn !== "none" ) {
2025-09-24 10:33:54 +09:00
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
2025-11-12 14:02:58 +09:00
displayColumns = [ displayColumn ] ;
2025-09-24 14:31:46 +09:00
logger . info (
2025-11-12 14:02:58 +09:00
` 🔧 기존 display_column 사용: ${ column . column_name } → ${ displayColumn } `
2025-09-24 14:31:46 +09:00
) ;
2025-09-23 15:58:54 +09:00
} else {
2025-09-24 14:31:46 +09:00
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
2025-11-12 14:02:58 +09:00
let defaultDisplayColumn = referenceColumn ;
if ( referenceTable === "dept_info" ) {
2025-09-24 10:33:54 +09:00
defaultDisplayColumn = "dept_name" ;
2025-11-12 14:02:58 +09:00
} else if ( referenceTable === "company_info" ) {
2025-09-24 10:33:54 +09:00
defaultDisplayColumn = "company_name" ;
2025-11-12 14:02:58 +09:00
} else if ( referenceTable === "user_info" ) {
2025-09-24 10:33:54 +09:00
defaultDisplayColumn = "user_name" ;
2025-11-12 14:02:58 +09:00
} else if ( referenceTable === "category_values" ) {
defaultDisplayColumn = "category_name" ;
2025-09-24 10:33:54 +09:00
}
displayColumns = [ defaultDisplayColumn ] ;
2025-09-24 14:31:46 +09:00
logger . info (
2025-11-12 14:02:58 +09:00
` 🔧 Entity 조인 기본 표시 컬럼 설정: ${ column . column_name } → ${ defaultDisplayColumn } ( ${ referenceTable } ) `
2025-09-23 16:23:36 +09:00
) ;
2025-09-24 14:31:46 +09:00
logger . info ( ` 🔍 생성된 displayColumns 배열: ` , displayColumns ) ;
2025-09-23 15:58:54 +09:00
}
2025-09-16 15:13:00 +09:00
// 별칭 컬럼명 생성 (writer -> writer_name)
const aliasColumn = ` ${ column . column_name } _name ` ;
const joinConfig : EntityJoinConfig = {
sourceTable : tableName ,
sourceColumn : column.column_name ,
2025-11-12 14:02:58 +09:00
referenceTable : referenceTable , // 카테고리의 경우 자동 설정된 값 사용
referenceColumn : referenceColumn , // 카테고리의 경우 자동 설정된 값 사용
2025-09-23 15:58:54 +09:00
displayColumns : displayColumns ,
displayColumn : displayColumns [ 0 ] , // 하위 호환성
2025-09-16 15:13:00 +09:00
aliasColumn : aliasColumn ,
2025-09-23 15:58:54 +09:00
separator : separator ,
2025-09-16 15:13:00 +09:00
} ;
2025-09-24 14:31:46 +09:00
logger . info ( ` 🔧 기본 조인 설정 생성: ` , {
sourceTable : joinConfig.sourceTable ,
sourceColumn : joinConfig.sourceColumn ,
referenceTable : joinConfig.referenceTable ,
aliasColumn : joinConfig.aliasColumn ,
displayColumns : joinConfig.displayColumns ,
} ) ;
2025-09-16 15:13:00 +09:00
// 조인 설정 유효성 검증
2025-09-24 14:31:46 +09:00
logger . info (
` 🔍 조인 설정 검증 중: ${ joinConfig . sourceColumn } -> ${ joinConfig . referenceTable } `
) ;
2025-09-16 15:13:00 +09:00
if ( await this . validateJoinConfig ( joinConfig ) ) {
joinConfigs . push ( joinConfig ) ;
2025-09-24 14:31:46 +09:00
logger . info ( ` ✅ 조인 설정 추가됨: ${ joinConfig . aliasColumn } ` ) ;
} else {
logger . warn ( ` ❌ 조인 설정 검증 실패: ${ joinConfig . sourceColumn } ` ) ;
2025-09-16 15:13:00 +09:00
}
}
2025-09-24 14:31:46 +09:00
logger . info ( ` 🎯 Entity 조인 설정 생성 완료: ${ joinConfigs . length } 개 ` ) ;
joinConfigs . forEach ( ( config , index ) = > {
logger . info (
` ${ index + 1 } . ${ config . sourceColumn } -> ${ config . referenceTable } . ${ config . referenceColumn } AS ${ config . aliasColumn } `
) ;
} ) ;
2025-09-16 15:13:00 +09:00
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
2025-09-17 11:15:34 +09:00
) : { query : string ; aliasMap : Map < string , string > } {
2025-09-16 15:13:00 +09:00
try {
2025-09-24 10:33:54 +09:00
// 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지)
const baseColumns = selectColumns
. map ( ( col ) = > ` main. ${ col } ::TEXT AS ${ col } ` )
. join ( ", " ) ;
2025-09-16 15:13:00 +09:00
2025-09-16 18:02:19 +09:00
// Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리)
2025-09-17 11:15:34 +09:00
// 별칭 매핑 생성 (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 } ` ) ;
} ) ;
2025-09-16 15:13:00 +09:00
const joinColumns = joinConfigs
2025-09-23 15:58:54 +09:00
. map ( ( config ) = > {
const alias = aliasMap . get ( config . referenceTable ) ;
2025-09-23 16:23:36 +09:00
const displayColumns = config . displayColumns || [
config . displayColumn ,
] ;
2025-09-23 15:58:54 +09:00
const separator = config . separator || " - " ;
2025-11-12 14:02:58 +09:00
// 결과 컬럼 배열 (aliasColumn + _label 필드)
const resultColumns : string [ ] = [ ] ;
2025-09-23 16:23:36 +09:00
2025-10-17 15:31:23 +09:00
if ( displayColumns . length === 0 || ! displayColumns [ 0 ] ) {
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
// 조인 테이블의 referenceColumn을 기본값으로 사용
2025-11-12 14:02:58 +09:00
resultColumns . push ( ` COALESCE( ${ alias } . ${ config . referenceColumn } ::TEXT, '') AS ${ config . aliasColumn } ` ) ;
2025-10-17 15:31:23 +09:00
} else if ( displayColumns . length === 1 ) {
2025-09-23 15:58:54 +09:00
// 단일 컬럼인 경우
2025-09-23 17:43:24 +09:00
const col = displayColumns [ 0 ] ;
const isJoinTableColumn = [
"dept_name" ,
"dept_code" ,
"master_user_id" ,
"location_name" ,
"parent_dept_code" ,
"master_sabun" ,
"location" ,
"data_type" ,
2025-09-24 15:02:54 +09:00
"company_name" ,
"sales_yn" ,
"status" ,
2025-11-12 14:02:58 +09:00
"value_label" , // table_column_category_values
"user_name" , // user_info
2025-09-23 17:43:24 +09:00
] . includes ( col ) ;
if ( isJoinTableColumn ) {
2025-11-12 14:02:58 +09:00
resultColumns . push ( ` COALESCE( ${ alias } . ${ col } ::TEXT, '') AS ${ config . aliasColumn } ` ) ;
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
// sourceColumn_label 형식으로 추가
resultColumns . push ( ` COALESCE( ${ alias } . ${ col } ::TEXT, '') AS ${ config . sourceColumn } _label ` ) ;
2025-09-23 17:43:24 +09:00
} else {
2025-11-12 14:02:58 +09:00
resultColumns . push ( ` COALESCE(main. ${ col } ::TEXT, '') AS ${ config . aliasColumn } ` ) ;
2025-09-23 17:43:24 +09:00
}
2025-09-23 15:58:54 +09:00
} else {
// 여러 컬럼인 경우 CONCAT으로 연결
2025-09-23 17:43:24 +09:00
// 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리
2025-09-23 15:58:54 +09:00
const concatParts = displayColumns
2025-09-23 17:43:24 +09:00
. map ( ( col ) = > {
// 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용)
// 현재는 dept_info 테이블의 컬럼들을 확인
const isJoinTableColumn = [
"dept_name" ,
"dept_code" ,
"master_user_id" ,
"location_name" ,
"parent_dept_code" ,
"master_sabun" ,
"location" ,
"data_type" ,
2025-09-24 15:02:54 +09:00
"company_name" ,
"sales_yn" ,
"status" ,
2025-11-12 14:02:58 +09:00
"value_label" , // table_column_category_values
"user_name" , // user_info
2025-09-23 17:43:24 +09:00
] . includes ( col ) ;
if ( isJoinTableColumn ) {
// 조인 테이블 컬럼은 조인 별칭 사용
2025-09-24 10:33:54 +09:00
return ` COALESCE( ${ alias } . ${ col } ::TEXT, '') ` ;
2025-09-23 17:43:24 +09:00
} else {
// 기본 테이블 컬럼은 main 별칭 사용
2025-09-24 10:33:54 +09:00
return ` COALESCE(main. ${ col } ::TEXT, '') ` ;
2025-09-23 17:43:24 +09:00
}
} )
2025-09-24 10:33:54 +09:00
. join ( ` || ' ${ separator } ' || ` ) ;
2025-09-23 17:43:24 +09:00
2025-11-12 14:02:58 +09:00
resultColumns . push ( ` ( ${ concatParts } ) AS ${ config . aliasColumn } ` ) ;
2025-09-23 15:58:54 +09:00
}
2025-11-12 14:02:58 +09:00
// 모든 resultColumns를 반환
return resultColumns . join ( ", " ) ;
2025-09-23 15:58:54 +09:00
} )
2025-09-16 15:13:00 +09:00
. join ( ", " ) ;
// SELECT 절 구성
const selectClause = joinColumns
? ` ${ baseColumns } , ${ joinColumns } `
: baseColumns ;
// FROM 절 (메인 테이블)
const fromClause = ` FROM ${ tableName } main ` ;
2025-09-17 11:15:34 +09:00
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 중복 테이블 제거)
const joinClauses = uniqueReferenceTableConfigs
. map ( ( config ) = > {
const alias = aliasMap . get ( config . referenceTable ) ;
2025-11-12 14:02:58 +09:00
// table_column_category_values는 특별한 조인 조건 필요
if ( config . referenceTable === 'table_column_category_values' ) {
return ` LEFT JOIN ${ config . referenceTable } ${ alias } ON main. ${ config . sourceColumn } = ${ alias } . ${ config . referenceColumn } AND ${ alias } .table_name = ' ${ tableName } ' AND ${ alias } .column_name = ' ${ config . sourceColumn } ' ` ;
}
2025-09-16 15:13:00 +09:00
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" ) ;
2025-09-24 14:31:46 +09:00
logger . info ( ` 🔍 생성된 Entity 조인 쿼리: ` , query ) ;
2025-09-17 11:15:34 +09:00
return {
query : query ,
aliasMap : aliasMap ,
} ;
2025-09-16 15:13:00 +09:00
} catch ( error ) {
logger . error ( "Entity 조인 쿼리 생성 실패" , error ) ;
throw error ;
}
}
2025-09-16 16:53:03 +09:00
/ * *
* 조 인 전 략 결 정 ( 테 이 블 크 기 기 반 )
* /
async determineJoinStrategy (
joinConfigs : EntityJoinConfig [ ]
) : Promise < "full_join" | "cache_lookup" | "hybrid" > {
try {
const strategies = await Promise . all (
joinConfigs . map ( async ( config ) = > {
2025-09-23 17:43:24 +09:00
// 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가
if ( config . displayColumns && config . displayColumns . length > 1 ) {
console . log (
` 🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${ config . sourceColumn } ` ,
config . displayColumns
) ;
return "join" ;
}
2025-11-12 14:02:58 +09:00
// table_column_category_values는 특수 조인 조건이 필요하므로 캐시 불가
if ( config . referenceTable === 'table_column_category_values' ) {
logger . info (
` 🎯 table_column_category_values는 캐시 전략 불가: ${ config . sourceColumn } `
) ;
return "join" ;
}
2025-09-16 16:53:03 +09:00
// 참조 테이블의 캐시 가능성 확인
2025-09-24 14:31:46 +09:00
const displayCol =
config . displayColumn ||
config . displayColumns ? . [ 0 ] ||
config . referenceColumn ;
logger . info (
` 🔍 캐시 확인용 표시 컬럼: ${ config . referenceTable } - ${ displayCol } `
) ;
2025-09-16 16:53:03 +09:00
const cachedData = await referenceCacheService . getCachedReference (
config . referenceTable ,
config . referenceColumn ,
2025-09-24 14:31:46 +09:00
displayCol
2025-09-16 16:53:03 +09:00
) ;
return cachedData ? "cache" : "join" ;
} )
) ;
// 모두 캐시 가능한 경우
if ( strategies . every ( ( s ) = > s === "cache" ) ) {
return "cache_lookup" ;
}
// 혼합인 경우
if ( strategies . includes ( "cache" ) && strategies . includes ( "join" ) ) {
return "hybrid" ;
}
// 기본은 조인
return "full_join" ;
} catch ( error ) {
logger . error ( "조인 전략 결정 실패" , error ) ;
return "full_join" ; // 안전한 기본값
}
}
2025-09-16 15:13:00 +09:00
/ * *
* 조 인 설 정 유 효 성 검 증
* /
private async validateJoinConfig ( config : EntityJoinConfig ) : Promise < boolean > {
try {
2025-09-24 14:31:46 +09:00
logger . info ( "🔍 조인 설정 검증 상세:" , {
sourceColumn : config.sourceColumn ,
referenceTable : config.referenceTable ,
displayColumns : config.displayColumns ,
displayColumn : config.displayColumn ,
aliasColumn : config.aliasColumn ,
} ) ;
2025-09-16 15:13:00 +09:00
// 참조 테이블 존재 확인
2025-10-01 12:10:34 +09:00
const tableExists = await query < { exists : number } > (
` SELECT 1 as exists FROM information_schema.tables
WHERE table_name = $1
LIMIT 1 ` ,
[ config . referenceTable ]
) ;
2025-09-16 15:13:00 +09:00
2025-10-01 12:10:34 +09:00
if ( tableExists . length === 0 ) {
2025-09-16 15:13:00 +09:00
logger . warn ( ` 참조 테이블이 존재하지 않음: ${ config . referenceTable } ` ) ;
return false ;
}
2025-09-24 10:33:54 +09:00
// 참조 컬럼 존재 확인 (displayColumns[0] 사용)
const displayColumn = config . displayColumns ? . [ 0 ] || config . displayColumn ;
2025-09-24 14:31:46 +09:00
logger . info (
` 🔍 표시 컬럼 확인: ${ displayColumn } (from displayColumns: ${ config . displayColumns } , displayColumn: ${ config . displayColumn } ) `
) ;
2025-09-16 15:13:00 +09:00
2025-09-24 14:31:46 +09:00
// 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용
if ( displayColumn && displayColumn !== "none" ) {
2025-10-01 12:10:34 +09:00
const columnExists = await query < { exists : number } > (
` SELECT 1 as exists FROM information_schema.columns
WHERE table_name = $1
AND column_name = $2
LIMIT 1 ` ,
[ config . referenceTable , displayColumn ]
) ;
if ( columnExists . length === 0 ) {
2025-09-24 14:31:46 +09:00
logger . warn (
` 표시 컬럼이 존재하지 않음: ${ config . referenceTable } . ${ displayColumn } `
) ;
return false ;
}
logger . info (
` ✅ 표시 컬럼 확인 완료: ${ config . referenceTable } . ${ displayColumn } `
) ;
} else {
logger . info (
` 🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음 `
2025-09-16 15:13:00 +09:00
) ;
}
return true ;
} catch ( error ) {
logger . error ( "조인 설정 검증 실패" , error ) ;
return false ;
}
}
/ * *
* 카 운 트 쿼 리 생 성 ( 페 이 징 용 )
* /
buildCountQuery (
tableName : string ,
joinConfigs : EntityJoinConfig [ ] ,
whereClause : string = ""
) : string {
try {
2025-09-17 11:15:34 +09:00
// 별칭 매핑 생성 (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 ) ;
} ) ;
2025-09-16 15:13:00 +09:00
// JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요)
2025-09-17 11:15:34 +09:00
const joinClauses = uniqueReferenceTableConfigs
. map ( ( config ) = > {
const alias = aliasMap . get ( config . referenceTable ) ;
2025-09-16 15:13:00 +09:00
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 {
2025-09-17 10:35:36 +09:00
// 1. 테이블의 기본 컬럼 정보 조회
2025-10-01 12:10:34 +09:00
const columns = await query < {
column_name : string ;
data_type : string ;
} > (
` SELECT
2025-09-16 15:13:00 +09:00
column_name ,
data_type
FROM information_schema . columns
2025-10-01 12:10:34 +09:00
WHERE table_name = $1
2025-09-16 15:13:00 +09:00
AND data_type IN ( 'character varying' , 'varchar' , 'text' , 'char' )
2025-10-01 12:10:34 +09:00
ORDER BY ordinal_position ` ,
[ tableName ]
) ;
2025-09-16 15:13:00 +09:00
2025-09-17 10:35:36 +09:00
// 2. column_labels 테이블에서 라벨 정보 조회
2025-10-01 12:10:34 +09:00
const columnLabels = await query < {
column_name : string ;
column_label : string | null ;
} > (
` SELECT column_name, column_label
FROM column_labels
WHERE table_name = $1 ` ,
[ tableName ]
) ;
2025-09-17 10:35:36 +09:00
// 3. 라벨 정보를 맵으로 변환
const labelMap = new Map < string , string > ( ) ;
columnLabels . forEach ( ( label ) = > {
if ( label . column_name && label . column_label ) {
labelMap . set ( label . column_name , label . column_label ) ;
}
} ) ;
// 4. 컬럼 정보와 라벨 정보 결합
2025-09-16 15:13:00 +09:00
return columns . map ( ( col ) = > ( {
columnName : col.column_name ,
2025-09-17 10:35:36 +09:00
displayName : labelMap.get ( col . column_name ) || col . column_name , // 라벨이 있으면 사용, 없으면 컬럼명
2025-09-16 15:13:00 +09:00
dataType : col.data_type ,
} ) ) ;
} catch ( error ) {
logger . error ( ` 참조 테이블 컬럼 조회 실패: ${ tableName } ` , error ) ;
return [ ] ;
}
}
}
export const entityJoinService = new EntityJoinService ( ) ;