feat: Implement advanced filtering capabilities in entity search

- Added a new helper function `applyFilters` to handle dynamic filter conditions for entity search queries.
- Enhanced the `getDistinctColumnValues` and `getEntityOptions` endpoints to support JSON array filters, allowing for more flexible data retrieval based on specified conditions.
- Updated the frontend components to integrate filter conditions, improving user interaction and data management in selection components.
- Introduced new filter options in the V2Select component, enabling users to define and apply various filter criteria dynamically.
This commit is contained in:
kjs
2026-02-26 16:07:15 +09:00
parent 63d8e17392
commit 20167ad359
10 changed files with 689 additions and 64 deletions

View File

@@ -3,16 +3,115 @@ import { AuthenticatedRequest } from "../types/auth";
import { getPool } from "../database/db";
import { logger } from "../utils/logger";
/**
* 필터 조건을 WHERE절에 적용하는 공통 헬퍼
* filters JSON 배열: [{ column, operator, value }]
*/
function applyFilters(
filtersJson: string | undefined,
existingColumns: Set<string>,
whereConditions: string[],
params: any[],
startParamIndex: number,
tableName: string,
): number {
let paramIndex = startParamIndex;
if (!filtersJson) return paramIndex;
let filters: Array<{ column: string; operator: string; value: unknown }>;
try {
filters = JSON.parse(filtersJson as string);
} catch {
logger.warn("filters JSON 파싱 실패", { tableName, filtersJson });
return paramIndex;
}
if (!Array.isArray(filters)) return paramIndex;
for (const filter of filters) {
const { column, operator = "=", value } = filter;
if (!column || !existingColumns.has(column)) {
logger.warn("필터 컬럼 미존재 제외", { tableName, column });
continue;
}
switch (operator) {
case "=":
whereConditions.push(`"${column}" = $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "!=":
whereConditions.push(`"${column}" != $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case ">":
case "<":
case ">=":
case "<=":
whereConditions.push(`"${column}" ${operator} $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "in": {
const inVals = Array.isArray(value) ? value : String(value).split(",").map(v => v.trim());
if (inVals.length > 0) {
const ph = inVals.map((_, i) => `$${paramIndex + i}`).join(", ");
whereConditions.push(`"${column}" IN (${ph})`);
params.push(...inVals);
paramIndex += inVals.length;
}
break;
}
case "notIn": {
const notInVals = Array.isArray(value) ? value : String(value).split(",").map(v => v.trim());
if (notInVals.length > 0) {
const ph = notInVals.map((_, i) => `$${paramIndex + i}`).join(", ");
whereConditions.push(`"${column}" NOT IN (${ph})`);
params.push(...notInVals);
paramIndex += notInVals.length;
}
break;
}
case "like":
whereConditions.push(`"${column}"::text ILIKE $${paramIndex}`);
params.push(`%${value}%`);
paramIndex++;
break;
case "isNull":
whereConditions.push(`"${column}" IS NULL`);
break;
case "isNotNull":
whereConditions.push(`"${column}" IS NOT NULL`);
break;
default:
whereConditions.push(`"${column}" = $${paramIndex}`);
params.push(value);
paramIndex++;
break;
}
}
return paramIndex;
}
/**
* 테이블 컬럼의 DISTINCT 값 조회 API (inputType: select 용)
* GET /api/entity/:tableName/distinct/:columnName
*
* 해당 테이블의 해당 컬럼에서 DISTINCT 값을 조회하여 선택박스 옵션으로 반환
*
* Query Params:
* - labelColumn: 별도의 라벨 컬럼 (선택)
* - filters: JSON 배열 형태의 필터 조건 (선택)
* 예: [{"column":"status","operator":"=","value":"active"}]
*/
export async function getDistinctColumnValues(req: AuthenticatedRequest, res: Response) {
try {
const { tableName, columnName } = req.params;
const { labelColumn } = req.query; // 선택적: 별도의 라벨 컬럼
const { labelColumn, filters: filtersParam } = req.query;
// 유효성 검증
if (!tableName || tableName === "undefined" || tableName === "null") {
@@ -68,6 +167,16 @@ export async function getDistinctColumnValues(req: AuthenticatedRequest, res: Re
whereConditions.push(`"${columnName}" IS NOT NULL`);
whereConditions.push(`"${columnName}" != ''`);
// 필터 조건 적용
paramIndex = applyFilters(
filtersParam as string | undefined,
existingColumns,
whereConditions,
params,
paramIndex,
tableName,
);
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
@@ -88,6 +197,7 @@ export async function getDistinctColumnValues(req: AuthenticatedRequest, res: Re
columnName,
labelColumn: effectiveLabelColumn,
companyCode,
hasFilters: !!filtersParam,
rowCount: result.rowCount,
});
@@ -111,11 +221,14 @@ export async function getDistinctColumnValues(req: AuthenticatedRequest, res: Re
* Query Params:
* - value: 값 컬럼 (기본: id)
* - label: 표시 컬럼 (기본: name)
* - fields: 추가 반환 컬럼 (콤마 구분)
* - filters: JSON 배열 형태의 필터 조건 (선택)
* 예: [{"column":"status","operator":"=","value":"active"}]
*/
export async function getEntityOptions(req: AuthenticatedRequest, res: Response) {
try {
const { tableName } = req.params;
const { value = "id", label = "name", fields } = req.query;
const { value = "id", label = "name", fields, filters: filtersParam } = req.query;
// tableName 유효성 검증
if (!tableName || tableName === "undefined" || tableName === "null") {
@@ -163,6 +276,16 @@ export async function getEntityOptions(req: AuthenticatedRequest, res: Response)
paramIndex++;
}
// 필터 조건 적용
paramIndex = applyFilters(
filtersParam as string | undefined,
existingColumns,
whereConditions,
params,
paramIndex,
tableName,
);
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
@@ -195,6 +318,7 @@ export async function getEntityOptions(req: AuthenticatedRequest, res: Response)
valueColumn,
labelColumn: effectiveLabelColumn,
companyCode,
hasFilters: !!filtersParam,
rowCount: result.rowCount,
extraFields: extraColumns ? true : false,
});