검색 필터기능 수정사항
This commit is contained in:
@@ -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, "''")}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user