검색 필터기능 수정사항

This commit is contained in:
kjs
2025-09-23 14:26:18 +09:00
parent e653effac0
commit da9985cd24
11 changed files with 1537 additions and 318 deletions

View File

@@ -1019,6 +1019,434 @@ export class TableManagementService {
}
}
/**
* 고급 검색 조건 구성
*/
private async buildAdvancedSearchCondition(
tableName: string,
columnName: string,
value: any,
paramIndex: number
): Promise<{
whereClause: string;
values: any[];
paramCount: number;
} | null> {
try {
// "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
if (
value === "__ALL__" ||
value === "" ||
value === null ||
value === undefined
) {
return null;
}
// 컬럼 타입 정보 조회
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
if (!columnInfo) {
// 컬럼 정보가 없으면 기본 문자열 검색
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
const webType = columnInfo.webType;
// 웹타입별 검색 조건 구성
switch (webType) {
case "date":
case "datetime":
return this.buildDateRangeCondition(columnName, value, paramIndex);
case "number":
case "decimal":
return this.buildNumberRangeCondition(columnName, value, paramIndex);
case "code":
return await this.buildCodeSearchCondition(
tableName,
columnName,
value,
paramIndex
);
case "entity":
return await this.buildEntitySearchCondition(
tableName,
columnName,
value,
paramIndex
);
default:
// 기본 문자열 검색
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
} catch (error) {
logger.error(
`고급 검색 조건 구성 실패: ${tableName}.${columnName}`,
error
);
// 오류 시 기본 검색으로 폴백
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
}
/**
* 날짜 범위 검색 조건 구성
*/
private buildDateRangeCondition(
columnName: string,
value: any,
paramIndex: number
): {
whereClause: string;
values: any[];
paramCount: number;
} {
const conditions: string[] = [];
const values: any[] = [];
let paramCount = 0;
if (typeof value === "object" && value !== null) {
if (value.from) {
conditions.push(`${columnName} >= $${paramIndex + paramCount}`);
values.push(value.from);
paramCount++;
}
if (value.to) {
conditions.push(`${columnName} <= $${paramIndex + paramCount}`);
values.push(value.to);
paramCount++;
}
} else if (typeof value === "string" && value.trim() !== "") {
// 단일 날짜 검색 (해당 날짜의 데이터)
conditions.push(`DATE(${columnName}) = DATE($${paramIndex})`);
values.push(value);
paramCount = 1;
}
if (conditions.length === 0) {
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
return {
whereClause: `(${conditions.join(" AND ")})`,
values,
paramCount,
};
}
/**
* 숫자 범위 검색 조건 구성
*/
private buildNumberRangeCondition(
columnName: string,
value: any,
paramIndex: number
): {
whereClause: string;
values: any[];
paramCount: number;
} {
const conditions: string[] = [];
const values: any[] = [];
let paramCount = 0;
if (typeof value === "object" && value !== null) {
if (value.min !== undefined && value.min !== null && value.min !== "") {
conditions.push(
`${columnName}::numeric >= $${paramIndex + paramCount}`
);
values.push(parseFloat(value.min));
paramCount++;
}
if (value.max !== undefined && value.max !== null && value.max !== "") {
conditions.push(
`${columnName}::numeric <= $${paramIndex + paramCount}`
);
values.push(parseFloat(value.max));
paramCount++;
}
} else if (typeof value === "string" || typeof value === "number") {
// 정확한 값 검색
conditions.push(`${columnName}::numeric = $${paramIndex}`);
values.push(parseFloat(value.toString()));
paramCount = 1;
}
if (conditions.length === 0) {
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
return {
whereClause: `(${conditions.join(" AND ")})`,
values,
paramCount,
};
}
/**
* 코드 검색 조건 구성
*/
private async buildCodeSearchCondition(
tableName: string,
columnName: string,
value: any,
paramIndex: number
): Promise<{
whereClause: string;
values: any[];
paramCount: number;
}> {
try {
const codeTypeInfo = await this.getCodeTypeInfo(tableName, columnName);
if (!codeTypeInfo.isCodeType || !codeTypeInfo.codeCategory) {
// 코드 타입이 아니면 기본 검색
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
if (typeof value === "string" && value.trim() !== "") {
// 코드값 또는 코드명으로 검색
return {
whereClause: `(
${columnName}::text = $${paramIndex} OR
EXISTS (
SELECT 1 FROM code_info ci
WHERE ci.code_category = $${paramIndex + 1}
AND ci.code_value = ${columnName}
AND ci.code_name ILIKE $${paramIndex + 2}
)
)`,
values: [value, codeTypeInfo.codeCategory, `%${value}%`],
paramCount: 3,
};
} else {
// 정확한 코드값 매칭
return {
whereClause: `${columnName} = $${paramIndex}`,
values: [value],
paramCount: 1,
};
}
} catch (error) {
logger.error(
`코드 검색 조건 구성 실패: ${tableName}.${columnName}`,
error
);
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
}
/**
* 엔티티 검색 조건 구성
*/
private async buildEntitySearchCondition(
tableName: string,
columnName: string,
value: any,
paramIndex: number
): Promise<{
whereClause: string;
values: any[];
paramCount: number;
}> {
try {
const entityTypeInfo = await this.getEntityTypeInfo(
tableName,
columnName
);
if (!entityTypeInfo.isEntityType || !entityTypeInfo.referenceTable) {
// 엔티티 타입이 아니면 기본 검색
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
if (typeof value === "string" && value.trim() !== "") {
const displayColumn = entityTypeInfo.displayColumn || "name";
const referenceColumn = entityTypeInfo.referenceColumn || "id";
// 참조 테이블의 표시 컬럼으로 검색
return {
whereClause: `EXISTS (
SELECT 1 FROM ${entityTypeInfo.referenceTable} ref
WHERE ref.${referenceColumn} = ${columnName}
AND ref.${displayColumn} ILIKE $${paramIndex}
)`,
values: [`%${value}%`],
paramCount: 1,
};
} else {
// 정확한 참조값 매칭
return {
whereClause: `${columnName} = $${paramIndex}`,
values: [value],
paramCount: 1,
};
}
} catch (error) {
logger.error(
`엔티티 검색 조건 구성 실패: ${tableName}.${columnName}`,
error
);
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
}
/**
* 불린 검색 조건 구성
*/
private buildBooleanCondition(
columnName: string,
value: any,
paramIndex: number
): {
whereClause: string;
values: any[];
paramCount: number;
} {
if (value === "true" || value === true) {
return {
whereClause: `${columnName} = true`,
values: [],
paramCount: 0,
};
} else if (value === "false" || value === false) {
return {
whereClause: `${columnName} = false`,
values: [],
paramCount: 0,
};
} else {
// 기본 검색
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${value}%`],
paramCount: 1,
};
}
}
/**
* 컬럼 웹타입 정보 조회
*/
private async getColumnWebTypeInfo(
tableName: string,
columnName: string
): Promise<{
webType: string;
codeCategory?: string;
referenceTable?: string;
referenceColumn?: string;
displayColumn?: string;
} | null> {
try {
const result = await prisma.column_labels.findFirst({
where: {
table_name: tableName,
column_name: columnName,
},
select: {
web_type: true,
code_category: true,
reference_table: true,
reference_column: true,
display_column: true,
},
});
if (!result) {
return null;
}
return {
webType: result.web_type || "",
codeCategory: result.code_category || undefined,
referenceTable: result.reference_table || undefined,
referenceColumn: result.reference_column || undefined,
displayColumn: result.display_column || undefined,
};
} catch (error) {
logger.error(
`컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,
error
);
return null;
}
}
/**
* 엔티티 타입 정보 조회
*/
private async getEntityTypeInfo(
tableName: string,
columnName: string
): Promise<{
isEntityType: boolean;
referenceTable?: string;
referenceColumn?: string;
displayColumn?: string;
}> {
try {
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
if (!columnInfo || columnInfo.webType !== "entity") {
return { isEntityType: false };
}
return {
isEntityType: true,
referenceTable: columnInfo.referenceTable,
referenceColumn: columnInfo.referenceColumn,
displayColumn: columnInfo.displayColumn,
};
} catch (error) {
logger.error(
`엔티티 타입 정보 조회 실패: ${tableName}.${columnName}`,
error
);
return { isEntityType: false };
}
}
/**
* 테이블 데이터 조회 (페이징 + 검색)
*/
@@ -1071,42 +1499,19 @@ export class TableManagementService {
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
if (typeof value === "string") {
// 🎯 코드 타입 컬럼의 경우 코드값과 코드명 모두로 검색
const codeTypeInfo = await this.getCodeTypeInfo(
tableName,
safeColumn
);
// 🎯 고급 필터 처리
const condition = await this.buildAdvancedSearchCondition(
tableName,
safeColumn,
value,
paramIndex
);
if (codeTypeInfo.isCodeType && codeTypeInfo.codeCategory) {
// 코드 타입 컬럼: 코드값 또는 코드명으로 검색
// 1) 컬럼 값이 직접 검색어와 일치하는 경우
// 2) 컬럼 값이 코드값이고, 해당 코드의 코드명이 검색어와 일치하는 경우
whereConditions.push(`(
${safeColumn}::text ILIKE $${paramIndex} OR
EXISTS (
SELECT 1 FROM code_info ci
WHERE ci.code_category = $${paramIndex + 1}
AND ci.code_value = ${safeColumn}
AND ci.code_name ILIKE $${paramIndex + 2}
)
)`);
searchValues.push(`%${value}%`); // 직접 값 검색용
searchValues.push(codeTypeInfo.codeCategory); // 코드 카테고리
searchValues.push(`%${value}%`); // 코드명 검색용
paramIndex += 2; // 추가 파라미터로 인해 인덱스 증가
} else {
// 일반 컬럼: 기존 방식
whereConditions.push(
`${safeColumn}::text ILIKE $${paramIndex}`
);
searchValues.push(`%${value}%`);
}
} else {
whereConditions.push(`${safeColumn} = $${paramIndex}`);
searchValues.push(value);
if (condition) {
whereConditions.push(condition.whereClause);
searchValues.push(...condition.values);
paramIndex += condition.paramCount;
}
paramIndex++;
}
}
}
@@ -1698,7 +2103,10 @@ export class TableManagementService {
const selectColumns = columns.data.map((col: any) => col.column_name);
// WHERE 절 구성
const whereClause = this.buildWhereClause(options.search);
const whereClause = await this.buildWhereClause(
tableName,
options.search
);
// ORDER BY 절 구성
const orderBy = options.sortBy
@@ -2061,21 +2469,77 @@ export class TableManagementService {
}
/**
* WHERE 절 구성
* WHERE 절 구성 (고급 검색 지원)
*/
private buildWhereClause(search?: Record<string, any>): string {
private async buildWhereClause(
tableName: string,
search?: Record<string, any>
): Promise<string> {
if (!search || Object.keys(search).length === 0) {
return "";
}
const conditions: string[] = [];
for (const [key, value] of Object.entries(search)) {
if (value !== undefined && value !== null && value !== "") {
for (const [columnName, value] of Object.entries(search)) {
if (
value === undefined ||
value === null ||
value === "" ||
value === "__ALL__"
) {
continue;
}
try {
// 고급 검색 조건 구성
const searchCondition = await this.buildAdvancedSearchCondition(
tableName,
columnName,
value,
1 // paramIndex는 실제로는 사용되지 않음 (직접 값 삽입)
);
if (searchCondition) {
// SQL 인젝션 방지를 위해 값을 직접 삽입하는 대신 안전한 방식 사용
let condition = searchCondition.whereClause;
// 파라미터를 실제 값으로 치환 (안전한 방식)
searchCondition.values.forEach((val, index) => {
const paramPlaceholder = `$${index + 1}`;
if (typeof val === "string") {
condition = condition.replace(
paramPlaceholder,
`'${val.replace(/'/g, "''")}'`
);
} else if (typeof val === "number") {
condition = condition.replace(paramPlaceholder, val.toString());
} else {
condition = condition.replace(
paramPlaceholder,
`'${String(val).replace(/'/g, "''")}'`
);
}
});
// main. 접두사 추가 (조인 쿼리용)
condition = condition.replace(
new RegExp(`\\b${columnName}\\b`, "g"),
`main.${columnName}`
);
conditions.push(condition);
}
} catch (error) {
logger.warn(`검색 조건 구성 실패: ${columnName}`, error);
// 폴백: 기본 문자열 검색
if (typeof value === "string") {
conditions.push(`main.${key} ILIKE '%${value}%'`);
conditions.push(
`main.${columnName}::text ILIKE '%${value.replace(/'/g, "''")}%'`
);
} else {
conditions.push(`main.${key} = '${value}'`);
conditions.push(
`main.${columnName} = '${String(value).replace(/'/g, "''")}'`
);
}
}
}