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
+72 -10
View File
@@ -23,7 +23,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { V2SelectProps, SelectOption } from "@/types/v2-components";
import { V2SelectProps, SelectOption, V2SelectFilter } from "@/types/v2-components";
import { Check, ChevronsUpDown, X, ArrowLeftRight } from "lucide-react";
import { apiClient } from "@/lib/api/client";
import V2FormContext from "./V2FormContext";
@@ -655,6 +655,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
const labelColumn = config.labelColumn;
const apiEndpoint = config.apiEndpoint;
const staticOptions = config.options;
const configFilters = config.filters;
// 계층 코드 연쇄 선택 관련
const hierarchical = config.hierarchical;
@@ -662,6 +663,54 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
// FormContext에서 부모 필드 값 가져오기 (Context가 없으면 null)
const formContext = useContext(V2FormContext);
/**
* 필터 조건을 API 전달용 JSON으로 변환
* field/user 타입은 런타임 값으로 치환
*/
const resolvedFiltersJson = useMemo(() => {
if (!configFilters || configFilters.length === 0) return undefined;
const resolved: Array<{ column: string; operator: string; value: unknown }> = [];
for (const f of configFilters) {
const vt = f.valueType || "static";
// isNull/isNotNull은 값 불필요
if (f.operator === "isNull" || f.operator === "isNotNull") {
resolved.push({ column: f.column, operator: f.operator, value: null });
continue;
}
let resolvedValue: unknown = f.value;
if (vt === "field" && f.fieldRef) {
// 다른 폼 필드 참조
if (formContext) {
resolvedValue = formContext.getValue(f.fieldRef);
} else {
const fd = (props as any).formData;
resolvedValue = fd?.[f.fieldRef];
}
// 참조 필드 값이 비어있으면 이 필터 건너뜀
if (resolvedValue === undefined || resolvedValue === null || resolvedValue === "") continue;
} else if (vt === "user" && f.userField) {
// 로그인 사용자 정보 참조 (props에서 가져옴)
const userMap: Record<string, string | undefined> = {
companyCode: (props as any).companyCode,
userId: (props as any).userId,
deptCode: (props as any).deptCode,
userName: (props as any).userName,
};
resolvedValue = userMap[f.userField];
if (!resolvedValue) continue;
}
resolved.push({ column: f.column, operator: f.operator, value: resolvedValue });
}
return resolved.length > 0 ? JSON.stringify(resolved) : undefined;
}, [configFilters, formContext, props]);
// 부모 필드의 값 계산
const parentValue = useMemo(() => {
@@ -684,6 +733,13 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
}
}, [parentValue, hierarchical, source]);
// 필터 조건이 변경되면 옵션 다시 로드
useEffect(() => {
if (resolvedFiltersJson !== undefined) {
setOptionsLoaded(false);
}
}, [resolvedFiltersJson]);
useEffect(() => {
// 이미 로드된 경우 스킵 (static 제외, 계층 구조 제외)
if (optionsLoaded && source !== "static") {
@@ -731,11 +787,13 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
}
} else if (source === "db" && table) {
// DB 테이블에서 로드
const dbParams: Record<string, any> = {
value: valueColumn || "id",
label: labelColumn || "name",
};
if (resolvedFiltersJson) dbParams.filters = resolvedFiltersJson;
const response = await apiClient.get(`/entity/${table}/options`, {
params: {
value: valueColumn || "id",
label: labelColumn || "name",
},
params: dbParams,
});
const data = response.data;
if (data.success && data.data) {
@@ -745,8 +803,10 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
// 엔티티(참조 테이블)에서 로드
const valueCol = entityValueColumn || "id";
const labelCol = entityLabelColumn || "name";
const entityParams: Record<string, any> = { value: valueCol, label: labelCol };
if (resolvedFiltersJson) entityParams.filters = resolvedFiltersJson;
const response = await apiClient.get(`/entity/${entityTable}/options`, {
params: { value: valueCol, label: labelCol },
params: entityParams,
});
const data = response.data;
if (data.success && data.data) {
@@ -790,11 +850,13 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
}
} else if (source === "select" || source === "distinct") {
// 해당 테이블의 해당 컬럼에서 DISTINCT 값 조회
// tableName, columnName은 props에서 가져옴
// 🆕 columnName이 컴포넌트 ID 형식(comp_xxx)이면 유효하지 않으므로 건너뜀
const isValidColumnName = columnName && !columnName.startsWith("comp_");
if (tableName && isValidColumnName) {
const response = await apiClient.get(`/entity/${tableName}/distinct/${columnName}`);
const distinctParams: Record<string, any> = {};
if (resolvedFiltersJson) distinctParams.filters = resolvedFiltersJson;
const response = await apiClient.get(`/entity/${tableName}/distinct/${columnName}`, {
params: distinctParams,
});
const data = response.data;
if (data.success && data.data) {
fetchedOptions = data.data.map((item: { value: string; label: string }) => ({
@@ -818,7 +880,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
};
loadOptions();
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue]);
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue, resolvedFiltersJson]);
// 같은 폼에서 참조 테이블(entityTable) 컬럼을 사용하는 다른 컴포넌트 자동 감지
const autoFillTargets = useMemo(() => {