검색필터 업그레이드

This commit is contained in:
leeheejin 2026-01-19 17:25:12 +09:00
parent faf4100056
commit d09a6977f7
1 changed files with 55 additions and 7 deletions

View File

@ -41,7 +41,7 @@ import {
Lock, Lock,
} from "lucide-react"; } from "lucide-react";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { FileText, ChevronRightIcon } from "lucide-react"; import { FileText, ChevronRightIcon, Search } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toast } from "sonner"; import { toast } from "sonner";
@ -455,6 +455,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🆕 컬럼 헤더 필터 상태 (상단에서 선언) // 🆕 컬럼 헤더 필터 상태 (상단에서 선언)
const [headerFilters, setHeaderFilters] = useState<Record<string, Set<string>>>({}); const [headerFilters, setHeaderFilters] = useState<Record<string, Set<string>>>({});
const [headerLikeFilters, setHeaderLikeFilters] = useState<Record<string, string>>({}); // LIKE 검색용
const [openFilterColumn, setOpenFilterColumn] = useState<string | null>(null); const [openFilterColumn, setOpenFilterColumn] = useState<string | null>(null);
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함 // 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
@ -488,6 +489,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}); });
} }
// 2-1. 🆕 LIKE 검색 필터 적용
if (Object.keys(headerLikeFilters).length > 0) {
result = result.filter((row) => {
return Object.entries(headerLikeFilters).every(([columnName, searchText]) => {
if (!searchText || searchText.trim() === "") return true;
// 여러 가능한 컬럼명 시도
const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue).toLowerCase() : "";
// LIKE 검색 (대소문자 무시)
return cellStr.includes(searchText.toLowerCase());
});
});
}
// 3. 🆕 Filter Builder 적용 // 3. 🆕 Filter Builder 적용
if (filterGroups.length > 0) { if (filterGroups.length > 0) {
result = result.filter((row) => { result = result.filter((row) => {
@ -541,7 +558,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} }
return result; return result;
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, headerLikeFilters, filterGroups]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@ -2935,6 +2952,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
headerFilters: Object.fromEntries( headerFilters: Object.fromEntries(
Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set<string>)]), Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set<string>)]),
), ),
headerLikeFilters, // LIKE 검색 필터 저장
pageSize: localPageSize, pageSize: localPageSize,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@ -2955,6 +2973,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
frozenColumnCount, frozenColumnCount,
showGridLines, showGridLines,
headerFilters, headerFilters,
headerLikeFilters,
localPageSize, localPageSize,
]); ]);
@ -2991,6 +3010,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}); });
setHeaderFilters(filters); setHeaderFilters(filters);
} }
if (state.headerLikeFilters) {
setHeaderLikeFilters(state.headerLikeFilters);
}
} catch (error) { } catch (error) {
console.error("❌ 테이블 상태 복원 실패:", error); console.error("❌ 테이블 상태 복원 실패:", error);
} }
@ -5737,7 +5759,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}} }}
className={cn( className={cn(
"hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors", "hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors",
headerFilters[column.columnName]?.size > 0 && "text-primary bg-primary/10", (headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && "text-primary bg-primary/10",
)} )}
title="필터" title="필터"
> >
@ -5745,7 +5767,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-48 p-2" className="w-56 p-2"
align="start" align="start"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
@ -5754,16 +5776,42 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<span className="text-xs font-medium"> <span className="text-xs font-medium">
: {columnLabels[column.columnName] || column.displayName} : {columnLabels[column.columnName] || column.displayName}
</span> </span>
{headerFilters[column.columnName]?.size > 0 && ( {(headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && (
<button <button
onClick={() => clearHeaderFilter(column.columnName)} onClick={() => {
clearHeaderFilter(column.columnName);
setHeaderLikeFilters((prev) => {
const newFilters = { ...prev };
delete newFilters[column.columnName];
return newFilters;
});
}}
className="text-destructive text-xs hover:underline" className="text-destructive text-xs hover:underline"
> >
</button> </button>
)} )}
</div> </div>
<div className="max-h-48 space-y-1 overflow-y-auto"> {/* LIKE 검색 입력 필드 */}
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2" />
<input
type="text"
placeholder="검색어 입력 (포함)"
value={headerLikeFilters[column.columnName] || ""}
onChange={(e) => {
setHeaderLikeFilters((prev) => ({
...prev,
[column.columnName]: e.target.value,
}));
}}
className="border-input bg-background placeholder:text-muted-foreground h-7 w-full rounded-md border pl-7 pr-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary"
onClick={(e) => e.stopPropagation()}
/>
</div>
{/* 구분선 */}
<div className="text-muted-foreground border-t pt-2 text-[10px]"> :</div>
<div className="max-h-40 space-y-1 overflow-y-auto">
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { {columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => {
const isSelected = headerFilters[column.columnName]?.has(val); const isSelected = headerFilters[column.columnName]?.has(val);
return ( return (