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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user