fix: 필터 select 옵션에서 카테고리/엔티티 라벨이 올바르게 표시되도록 수정

- 백엔드: entityJoinService에서 _label 필드를 SELECT에 추가
- 백엔드: tableManagementService에 멀티테넌시 필터링 추가 (company_code)
- 백엔드: categorizeJoins에서 table_column_category_values를 명시적으로 dbJoins로 분류
- 백엔드: executeCachedLookup와 getTableData에 companyCode 파라미터 추가
- 프론트엔드: getColumnUniqueValues가 백엔드 조인 결과의 _name 필드를 사용하도록 수정
- 프론트엔드: TableSearchWidget에서 select 옵션 로드 로직 개선

이제 필터 select 박스에서 코드 대신 실제 이름(라벨)이 표시됩니다.
예: CATEGORY_148700 → 정상, topseal_admin → 탑씰 관리자 계정
This commit is contained in:
kjs
2025-11-12 14:02:58 +09:00
parent 58870237b6
commit 71fd3f5ee7
4 changed files with 203 additions and 73 deletions

View File

@@ -24,20 +24,19 @@ export class EntityJoinService {
try {
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
// column_labels에서 entity 타입인 컬럼들 조회
// column_labels에서 entity 및 category 타입인 컬럼들 조회 (input_type 사용)
const entityColumns = await query<{
column_name: string;
input_type: string;
reference_table: string;
reference_column: string;
display_column: string | null;
}>(
`SELECT column_name, reference_table, reference_column, display_column
`SELECT column_name, input_type, reference_table, reference_column, display_column
FROM column_labels
WHERE table_name = $1
AND web_type = $2
AND reference_table IS NOT NULL
AND reference_column IS NOT NULL`,
[tableName, "entity"]
AND input_type IN ('entity', 'category')`,
[tableName]
);
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
@@ -77,18 +76,34 @@ export class EntityJoinService {
}
for (const column of entityColumns) {
// 카테고리 타입인 경우 자동으로 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,
});
}
logger.info(`🔍 Entity 컬럼 상세 정보:`, {
column_name: column.column_name,
reference_table: column.reference_table,
reference_column: column.reference_column,
display_column: column.display_column,
input_type: column.input_type,
reference_table: referenceTable,
reference_column: referenceColumn,
display_column: displayColumn,
});
if (
!column.column_name ||
!column.reference_table ||
!column.reference_column
) {
if (!column.column_name || !referenceTable || !referenceColumn) {
logger.warn(`⚠️ 필수 정보 누락으로 스킵: ${column.column_name}`);
continue;
}
@@ -112,27 +127,28 @@ export class EntityJoinService {
separator,
screenConfig,
});
} else if (column.display_column && column.display_column !== "none") {
} else if (displayColumn && displayColumn !== "none") {
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
displayColumns = [column.display_column];
displayColumns = [displayColumn];
logger.info(
`🔧 기존 display_column 사용: ${column.column_name}${column.display_column}`
`🔧 기존 display_column 사용: ${column.column_name}${displayColumn}`
);
} else {
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
// 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용
let defaultDisplayColumn = column.reference_column;
if (column.reference_table === "dept_info") {
let defaultDisplayColumn = referenceColumn;
if (referenceTable === "dept_info") {
defaultDisplayColumn = "dept_name";
} else if (column.reference_table === "company_info") {
} else if (referenceTable === "company_info") {
defaultDisplayColumn = "company_name";
} else if (column.reference_table === "user_info") {
} else if (referenceTable === "user_info") {
defaultDisplayColumn = "user_name";
} else if (referenceTable === "category_values") {
defaultDisplayColumn = "category_name";
}
displayColumns = [defaultDisplayColumn];
logger.info(
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name}${defaultDisplayColumn} (${column.reference_table})`
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name}${defaultDisplayColumn} (${referenceTable})`
);
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
}
@@ -143,8 +159,8 @@ export class EntityJoinService {
const joinConfig: EntityJoinConfig = {
sourceTable: tableName,
sourceColumn: column.column_name,
referenceTable: column.reference_table,
referenceColumn: column.reference_column,
referenceTable: referenceTable, // 카테고리의 경우 자동 설정된 값 사용
referenceColumn: referenceColumn, // 카테고리의 경우 자동 설정된 값 사용
displayColumns: displayColumns,
displayColumn: displayColumns[0], // 하위 호환성
aliasColumn: aliasColumn,
@@ -245,11 +261,14 @@ export class EntityJoinService {
config.displayColumn,
];
const separator = config.separator || " - ";
// 결과 컬럼 배열 (aliasColumn + _label 필드)
const resultColumns: string[] = [];
if (displayColumns.length === 0 || !displayColumns[0]) {
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
// 조인 테이블의 referenceColumn을 기본값으로 사용
return `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`;
resultColumns.push(`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`);
} else if (displayColumns.length === 1) {
// 단일 컬럼인 경우
const col = displayColumns[0];
@@ -265,12 +284,18 @@ export class EntityJoinService {
"company_name",
"sales_yn",
"status",
"value_label", // table_column_category_values
"user_name", // user_info
].includes(col);
if (isJoinTableColumn) {
return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`;
resultColumns.push(`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`);
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
// sourceColumn_label 형식으로 추가
resultColumns.push(`COALESCE(${alias}.${col}::TEXT, '') AS ${config.sourceColumn}_label`);
} else {
return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`;
resultColumns.push(`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`);
}
} else {
// 여러 컬럼인 경우 CONCAT으로 연결
@@ -291,6 +316,8 @@ export class EntityJoinService {
"company_name",
"sales_yn",
"status",
"value_label", // table_column_category_values
"user_name", // user_info
].includes(col);
if (isJoinTableColumn) {
@@ -303,8 +330,11 @@ export class EntityJoinService {
})
.join(` || '${separator}' || `);
return `(${concatParts}) AS ${config.aliasColumn}`;
resultColumns.push(`(${concatParts}) AS ${config.aliasColumn}`);
}
// 모든 resultColumns를 반환
return resultColumns.join(", ");
})
.join(", ");
@@ -320,6 +350,12 @@ export class EntityJoinService {
const joinClauses = uniqueReferenceTableConfigs
.map((config) => {
const alias = aliasMap.get(config.referenceTable);
// 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}'`;
}
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
})
.join("\n");
@@ -380,6 +416,14 @@ export class EntityJoinService {
return "join";
}
// table_column_category_values는 특수 조인 조건이 필요하므로 캐시 불가
if (config.referenceTable === 'table_column_category_values') {
logger.info(
`🎯 table_column_category_values는 캐시 전략 불가: ${config.sourceColumn}`
);
return "join";
}
// 참조 테이블의 캐시 가능성 확인
const displayCol =
config.displayColumn ||

View File

@@ -1494,6 +1494,7 @@ export class TableManagementService {
search?: Record<string, any>;
sortBy?: string;
sortOrder?: string;
companyCode?: string;
}
): Promise<{
data: any[];
@@ -1503,7 +1504,7 @@ export class TableManagementService {
totalPages: number;
}> {
try {
const { page, size, search = {}, sortBy, sortOrder = "asc" } = options;
const { page, size, search = {}, sortBy, sortOrder = "asc", companyCode } = options;
const offset = (page - 1) * size;
logger.info(`테이블 데이터 조회: ${tableName}`, options);
@@ -1517,6 +1518,14 @@ export class TableManagementService {
let searchValues: any[] = [];
let paramIndex = 1;
// 멀티테넌시 필터 추가 (company_code)
if (companyCode) {
whereConditions.push(`company_code = $${paramIndex}`);
searchValues.push(companyCode);
paramIndex++;
logger.info(`🔒 멀티테넌시 필터 추가 (기본 조회): company_code = ${companyCode}`);
}
if (search && Object.keys(search).length > 0) {
for (const [column, value] of Object.entries(search)) {
if (value !== null && value !== undefined && value !== "") {
@@ -2213,11 +2222,20 @@ export class TableManagementService {
const selectColumns = columns.data.map((col: any) => col.column_name);
// WHERE 절 구성
const whereClause = await this.buildWhereClause(
let whereClause = await this.buildWhereClause(
tableName,
options.search
);
// 멀티테넌시 필터 추가 (company_code)
if (options.companyCode) {
const companyFilter = `main.company_code = '${options.companyCode.replace(/'/g, "''")}'`;
whereClause = whereClause
? `${whereClause} AND ${companyFilter}`
: companyFilter;
logger.info(`🔒 멀티테넌시 필터 추가 (Entity 조인): company_code = ${options.companyCode}`);
}
// ORDER BY 절 구성
const orderBy = options.sortBy
? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
@@ -2343,6 +2361,7 @@ export class TableManagementService {
search?: Record<string, any>;
sortBy?: string;
sortOrder?: string;
companyCode?: string;
},
startTime: number
): Promise<EntityJoinResponse> {
@@ -2530,11 +2549,11 @@ export class TableManagementService {
);
}
basicResult = await this.getTableData(tableName, fallbackOptions);
basicResult = await this.getTableData(tableName, { ...fallbackOptions, companyCode: options.companyCode });
}
} else {
// Entity 조인 컬럼 검색이 없는 경우 기존 캐시 방식 사용
basicResult = await this.getTableData(tableName, options);
basicResult = await this.getTableData(tableName, { ...options, companyCode: options.companyCode });
}
// Entity 값들을 캐시에서 룩업하여 변환
@@ -2807,10 +2826,14 @@ export class TableManagementService {
}
// 모든 조인이 캐시 가능한 경우: 기본 쿼리 + 캐시 룩업
else {
// whereClause에서 company_code 추출 (멀티테넌시 필터)
const companyCodeMatch = whereClause.match(/main\.company_code\s*=\s*'([^']+)'/);
const companyCode = companyCodeMatch ? companyCodeMatch[1] : undefined;
return await this.executeCachedLookup(
tableName,
cacheableJoins,
{ page: Math.floor(offset / limit) + 1, size: limit, search: {} },
{ page: Math.floor(offset / limit) + 1, size: limit, search: {}, companyCode },
startTime
);
}
@@ -2831,6 +2854,13 @@ export class TableManagementService {
const dbJoins: EntityJoinConfig[] = [];
for (const config of joinConfigs) {
// table_column_category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
if (config.referenceTable === 'table_column_category_values') {
dbJoins.push(config);
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
continue;
}
// 캐시 가능성 확인
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,