fix: update filter handling in data filtering logic
- Refactored the handling of "in" and "not_in" operators to ensure proper array handling and prevent errors when values are not provided. - Enhanced the InteractiveDataTable component to re-fetch data when filters are applied, improving user experience. - Updated DataFilterConfigPanel to correctly manage filter values based on selected operators. - Adjusted SplitPanelLayoutComponent to apply client-side data filtering based on defined conditions. These changes aim to improve the robustness and usability of the data filtering features across the application.
This commit is contained in:
parent
270687f405
commit
20c85569b0
|
|
@ -947,6 +947,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
|
|
@ -2184,6 +2185,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.12.0",
|
"pg-connection-string": "^2.12.0",
|
||||||
"pg-pool": "^3.13.0",
|
"pg-pool": "^3.13.0",
|
||||||
|
|
|
||||||
|
|
@ -3367,22 +3367,26 @@ export class TableManagementService {
|
||||||
`${safeColumn} != '${String(value).replace(/'/g, "''")}'`
|
`${safeColumn} != '${String(value).replace(/'/g, "''")}'`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "in":
|
case "in": {
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
const inArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
|
||||||
const values = value
|
if (inArr.length > 0) {
|
||||||
|
const values = inArr
|
||||||
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
filterConditions.push(`${safeColumn} IN (${values})`);
|
filterConditions.push(`${safeColumn} IN (${values})`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "not_in":
|
}
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
case "not_in": {
|
||||||
const values = value
|
const notInArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
|
||||||
|
if (notInArr.length > 0) {
|
||||||
|
const values = notInArr
|
||||||
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
filterConditions.push(`${safeColumn} NOT IN (${values})`);
|
filterConditions.push(`${safeColumn} NOT IN (${values})`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "contains":
|
case "contains":
|
||||||
filterConditions.push(
|
filterConditions.push(
|
||||||
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}%'`
|
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}%'`
|
||||||
|
|
|
||||||
|
|
@ -98,23 +98,27 @@ export function buildDataFilterWhereClause(
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "in":
|
case "in": {
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
const inArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
|
||||||
const placeholders = value.map((_, idx) => `$${paramIndex + idx}`).join(", ");
|
if (inArr.length > 0) {
|
||||||
|
const placeholders = inArr.map((_, idx) => `$${paramIndex + idx}`).join(", ");
|
||||||
conditions.push(`${columnRef} IN (${placeholders})`);
|
conditions.push(`${columnRef} IN (${placeholders})`);
|
||||||
params.push(...value);
|
params.push(...inArr);
|
||||||
paramIndex += value.length;
|
paramIndex += inArr.length;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "not_in":
|
case "not_in": {
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
const notInArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : [];
|
||||||
const placeholders = value.map((_, idx) => `$${paramIndex + idx}`).join(", ");
|
if (notInArr.length > 0) {
|
||||||
|
const placeholders = notInArr.map((_, idx) => `$${paramIndex + idx}`).join(", ");
|
||||||
conditions.push(`${columnRef} NOT IN (${placeholders})`);
|
conditions.push(`${columnRef} NOT IN (${placeholders})`);
|
||||||
params.push(...value);
|
params.push(...notInArr);
|
||||||
paramIndex += value.length;
|
paramIndex += notInArr.length;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "contains":
|
case "contains":
|
||||||
conditions.push(`${columnRef} LIKE $${paramIndex}`);
|
conditions.push(`${columnRef} LIKE $${paramIndex}`);
|
||||||
|
|
|
||||||
|
|
@ -605,6 +605,23 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
}
|
}
|
||||||
}, [relatedButtonFilter]);
|
}, [relatedButtonFilter]);
|
||||||
|
|
||||||
|
// TableOptionsContext 필터 변경 시 데이터 재조회 (TableSearchWidget 연동)
|
||||||
|
const filtersAppliedRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
// 초기 렌더 시 빈 배열은 무시 (불필요한 재조회 방지)
|
||||||
|
if (!filtersAppliedRef.current && filters.length === 0) return;
|
||||||
|
filtersAppliedRef.current = true;
|
||||||
|
|
||||||
|
const filterSearchParams: Record<string, any> = {};
|
||||||
|
filters.forEach((f) => {
|
||||||
|
if (f.value !== "" && f.value !== undefined && f.value !== null) {
|
||||||
|
filterSearchParams[f.columnName] = f.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadData(1, { ...searchValues, ...filterSearchParams });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
// 카테고리 타입 컬럼의 값 매핑 로드
|
// 카테고리 타입 컬럼의 값 매핑 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCategoryMappings = async () => {
|
const loadCategoryMappings = async () => {
|
||||||
|
|
|
||||||
|
|
@ -541,8 +541,31 @@ export function DataFilterConfigPanel({
|
||||||
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
|
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
|
||||||
{filter.valueType === "category" && categoryValues[filter.columnName] ? (
|
{filter.valueType === "category" && categoryValues[filter.columnName] ? (
|
||||||
<Select
|
<Select
|
||||||
value={Array.isArray(filter.value) ? filter.value[0] : filter.value}
|
value={
|
||||||
onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
|
filter.operator === "in" || filter.operator === "not_in"
|
||||||
|
? Array.isArray(filter.value) && filter.value.length > 0
|
||||||
|
? filter.value[0]
|
||||||
|
: ""
|
||||||
|
: Array.isArray(filter.value)
|
||||||
|
? filter.value[0]
|
||||||
|
: filter.value
|
||||||
|
}
|
||||||
|
onValueChange={(selectedValue) => {
|
||||||
|
if (filter.operator === "in" || filter.operator === "not_in") {
|
||||||
|
const currentValues = Array.isArray(filter.value) ? filter.value : [];
|
||||||
|
if (currentValues.includes(selectedValue)) {
|
||||||
|
handleFilterChange(
|
||||||
|
filter.id,
|
||||||
|
"value",
|
||||||
|
currentValues.filter((v) => v !== selectedValue),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handleFilterChange(filter.id, "value", [...currentValues, selectedValue]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleFilterChange(filter.id, "value", selectedValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,8 @@ export const TableOptionsToolbar: React.FC = () => {
|
||||||
onOpenChange={setColumnPanelOpen}
|
onOpenChange={setColumnPanelOpen}
|
||||||
/>
|
/>
|
||||||
<FilterPanel
|
<FilterPanel
|
||||||
tableId={selectedTableId}
|
isOpen={filterPanelOpen}
|
||||||
open={filterPanelOpen}
|
onClose={() => setFilterPanelOpen(false)}
|
||||||
onOpenChange={setFilterPanelOpen}
|
|
||||||
/>
|
/>
|
||||||
<GroupingPanel
|
<GroupingPanel
|
||||||
tableId={selectedTableId}
|
tableId={selectedTableId}
|
||||||
|
|
|
||||||
|
|
@ -1150,10 +1150,44 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]);
|
console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 좌측 패널 dataFilter 클라이언트 사이드 적용
|
||||||
|
let filteredLeftData = result.data || [];
|
||||||
|
const leftDataFilter = componentConfig.leftPanel?.dataFilter;
|
||||||
|
if (leftDataFilter?.enabled && leftDataFilter.filters?.length > 0) {
|
||||||
|
const matchFn = leftDataFilter.matchType === "any" ? "some" : "every";
|
||||||
|
filteredLeftData = filteredLeftData.filter((item: any) => {
|
||||||
|
return leftDataFilter.filters[matchFn]((cond: any) => {
|
||||||
|
const val = item[cond.columnName];
|
||||||
|
switch (cond.operator) {
|
||||||
|
case "equals":
|
||||||
|
return val === cond.value;
|
||||||
|
case "not_equals":
|
||||||
|
return val !== cond.value;
|
||||||
|
case "in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return arr.includes(val);
|
||||||
|
}
|
||||||
|
case "not_in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return !arr.includes(val);
|
||||||
|
}
|
||||||
|
case "contains":
|
||||||
|
return String(val || "").includes(String(cond.value));
|
||||||
|
case "is_null":
|
||||||
|
return val === null || val === undefined || val === "";
|
||||||
|
case "is_not_null":
|
||||||
|
return val !== null && val !== undefined && val !== "";
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
||||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||||
if (leftColumn && result.data.length > 0) {
|
if (leftColumn && filteredLeftData.length > 0) {
|
||||||
result.data.sort((a, b) => {
|
filteredLeftData.sort((a, b) => {
|
||||||
const aValue = String(a[leftColumn] || "");
|
const aValue = String(a[leftColumn] || "");
|
||||||
const bValue = String(b[leftColumn] || "");
|
const bValue = String(b[leftColumn] || "");
|
||||||
return aValue.localeCompare(bValue, "ko-KR");
|
return aValue.localeCompare(bValue, "ko-KR");
|
||||||
|
|
@ -1161,7 +1195,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계층 구조 빌드
|
// 계층 구조 빌드
|
||||||
const hierarchicalData = buildHierarchy(result.data);
|
const hierarchicalData = buildHierarchy(filteredLeftData);
|
||||||
setLeftData(hierarchicalData);
|
setLeftData(hierarchicalData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("좌측 데이터 로드 실패:", error);
|
console.error("좌측 데이터 로드 실패:", error);
|
||||||
|
|
@ -1220,7 +1254,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
case "equals":
|
case "equals":
|
||||||
return value === cond.value;
|
return value === cond.value;
|
||||||
case "notEquals":
|
case "notEquals":
|
||||||
|
case "not_equals":
|
||||||
return value !== cond.value;
|
return value !== cond.value;
|
||||||
|
case "in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return arr.includes(value);
|
||||||
|
}
|
||||||
|
case "not_in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return !arr.includes(value);
|
||||||
|
}
|
||||||
case "contains":
|
case "contains":
|
||||||
return String(value || "").includes(String(cond.value));
|
return String(value || "").includes(String(cond.value));
|
||||||
case "is_null":
|
case "is_null":
|
||||||
|
|
@ -1537,7 +1580,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
case "equals":
|
case "equals":
|
||||||
return value === cond.value;
|
return value === cond.value;
|
||||||
case "notEquals":
|
case "notEquals":
|
||||||
|
case "not_equals":
|
||||||
return value !== cond.value;
|
return value !== cond.value;
|
||||||
|
case "in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return arr.includes(value);
|
||||||
|
}
|
||||||
|
case "not_in": {
|
||||||
|
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||||
|
return !arr.includes(value);
|
||||||
|
}
|
||||||
case "contains":
|
case "contains":
|
||||||
return String(value || "").includes(String(cond.value));
|
return String(value || "").includes(String(cond.value));
|
||||||
case "is_null":
|
case "is_null":
|
||||||
|
|
|
||||||
|
|
@ -1932,7 +1932,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
|
|
||||||
{/* ===== 기본 설정 모달 ===== */}
|
{/* ===== 기본 설정 모달 ===== */}
|
||||||
<Dialog open={activeModal === "basic"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
<Dialog open={activeModal === "basic"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base">기본 설정</DialogTitle>
|
<DialogTitle className="text-base">기본 설정</DialogTitle>
|
||||||
<DialogDescription className="text-xs">패널 관계 타입 및 레이아웃을 설정합니다</DialogDescription>
|
<DialogDescription className="text-xs">패널 관계 타입 및 레이아웃을 설정합니다</DialogDescription>
|
||||||
|
|
@ -2010,7 +2010,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
|
|
||||||
{/* ===== 좌측 패널 모달 ===== */}
|
{/* ===== 좌측 패널 모달 ===== */}
|
||||||
<Dialog open={activeModal === "left"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
<Dialog open={activeModal === "left"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base">좌측 패널 설정</DialogTitle>
|
<DialogTitle className="text-base">좌측 패널 설정</DialogTitle>
|
||||||
<DialogDescription className="text-xs">마스터 데이터 표시 및 필터링을 설정합니다</DialogDescription>
|
<DialogDescription className="text-xs">마스터 데이터 표시 및 필터링을 설정합니다</DialogDescription>
|
||||||
|
|
@ -2680,7 +2680,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
|
|
||||||
{/* ===== 우측 패널 모달 ===== */}
|
{/* ===== 우측 패널 모달 ===== */}
|
||||||
<Dialog open={activeModal === "right"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
<Dialog open={activeModal === "right"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base">우측 패널 설정</DialogTitle>
|
<DialogTitle className="text-base">우측 패널 설정</DialogTitle>
|
||||||
<DialogDescription className="text-xs">
|
<DialogDescription className="text-xs">
|
||||||
|
|
@ -3604,7 +3604,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
|
|
||||||
{/* ===== 추가 탭 모달 ===== */}
|
{/* ===== 추가 탭 모달 ===== */}
|
||||||
<Dialog open={activeModal === "tabs"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
<Dialog open={activeModal === "tabs"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base">추가 탭 설정</DialogTitle>
|
<DialogTitle className="text-base">추가 탭 설정</DialogTitle>
|
||||||
<DialogDescription className="text-xs">
|
<DialogDescription className="text-xs">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue