feat: 테이블 검색 필터 UI 개선 및 실시간 검색 구현

- 모든 필터 입력창 높이 통일 (h-9, 36px)
- 실시간 검색: 입력 시 즉시 필터 적용 (검색 버튼 제거)
- 초기화 버튼 추가: 모든 필터값을 한번에 리셋
- filters → searchValues 자동 변환 로직 추가
- select 필터: 선택된 값의 라벨 저장하여 데이터 없을 때도 표시 유지
- select 옵션 초기 로드 후 계속 유지 (dataCount 변경 시에도 유지)

주요 개선사항:
1. Input, Select, Date 등 모든 필터의 높이가 동일하게 표시
2. 사용자가 값을 입력하면 바로 테이블이 필터링됨
3. 초기화 버튼으로 간편하게 모든 필터 제거 가능
4. 필터링 결과가 0건이어도 select 박스의 선택값이 유지됨

알려진 제한사항:
- 카테고리/엔티티 필터는 현재 테이블 데이터 기반으로만 옵션 표시
  (전체 정의된 카테고리 값이 아닌, 실제 데이터에 있는 값만 표시)
This commit is contained in:
kjs 2025-11-12 14:16:16 +09:00
parent 71fd3f5ee7
commit 5c205753e2
2 changed files with 93 additions and 23 deletions

View File

@ -256,6 +256,24 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [grouping, setGrouping] = useState<string[]>([]);
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility[]>([]);
// filters가 변경되면 searchValues 업데이트 (실시간 검색)
useEffect(() => {
const newSearchValues: Record<string, any> = {};
filters.forEach((filter) => {
if (filter.value) {
newSearchValues[filter.columnName] = filter.value;
}
});
console.log("🔍 [TableListComponent] filters → searchValues:", {
filters: filters.length,
searchValues: newSearchValues,
});
setSearchValues(newSearchValues);
setCurrentPage(1); // 필터 변경 시 첫 페이지로
}, [filters]);
// 초기 로드 시 localStorage에서 저장된 설정 불러오기
useEffect(() => {
if (tableConfig.selectedTable && currentUserId) {

View File

@ -3,7 +3,7 @@
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Settings, Filter, Layers, Search } from "lucide-react";
import { Settings, Filter, Layers, X } from "lucide-react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
@ -45,6 +45,8 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
// select 타입 필터의 옵션들
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
const [selectedLabels, setSelectedLabels] = useState<Record<string, string>>({});
const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true;
const showTableSelector = component.componentConfig?.showTableSelector ?? true;
@ -96,7 +98,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
}
}, [currentTable?.tableName]);
// select 옵션 로드 (activeFilters 또는 dataCount 변경 시)
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
useEffect(() => {
if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) {
return;
@ -109,15 +111,21 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
return;
}
console.log("🔄 [TableSearchWidget] select 옵션 로드 시작:", {
console.log("🔄 [TableSearchWidget] select 옵션 초기 로드:", {
activeFiltersCount: activeFilters.length,
selectFiltersCount: selectFilters.length,
dataCount: currentTable.dataCount,
});
const newOptions: Record<string, Array<{ label: string; value: string }>> = {};
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...selectOptions };
for (const filter of selectFilters) {
// 이미 로드된 옵션이 있으면 스킵 (초기값 유지)
if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) {
console.log("⏭️ [TableSearchWidget] 이미 로드된 옵션 스킵:", filter.columnName);
continue;
}
try {
const options = await currentTable.getColumnUniqueValues(filter.columnName);
newOptions[filter.columnName] = options;
@ -136,7 +144,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
};
loadSelectOptions();
}, [activeFilters, currentTable?.dataCount, currentTable?.getColumnUniqueValues]);
}, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경
// 디버깅: 현재 테이블 정보 로깅
useEffect(() => {
@ -158,23 +166,48 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
// 필터 값 변경 핸들러
const handleFilterChange = (columnName: string, value: string) => {
setFilterValues((prev) => ({
...prev,
console.log("🔍 [TableSearchWidget] 필터 값 변경:", {
columnName,
value,
currentTable: currentTable?.tableId,
});
const newValues = {
...filterValues,
[columnName]: value,
}));
};
setFilterValues(newValues);
// 실시간 검색: 값 변경 시 즉시 필터 적용
applyFilters(newValues);
};
// 검색 실행
const handleSearch = () => {
// 필터 적용 함수
const applyFilters = (values: Record<string, string> = filterValues) => {
// 빈 값이 아닌 필터만 적용
const filtersWithValues = activeFilters.map((filter) => ({
...filter,
value: filterValues[filter.columnName] || "",
value: values[filter.columnName] || "",
})).filter((f) => f.value !== "");
console.log("🔍 [TableSearchWidget] 필터 적용:", {
activeFilters: activeFilters.length,
filtersWithValues: filtersWithValues.length,
filters: filtersWithValues,
hasOnFilterChange: !!currentTable?.onFilterChange,
});
currentTable?.onFilterChange(filtersWithValues);
};
// 필터 초기화
const handleResetFilters = () => {
setFilterValues({});
setSelectedLabels({});
currentTable?.onFilterChange([]);
};
// 필터 입력 필드 렌더링
const renderFilterInput = (filter: TableFilter) => {
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
@ -187,7 +220,8 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
type="date"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
className="h-9 text-xs sm:text-sm"
style={{ height: '36px', minHeight: '36px' }}
placeholder={column?.columnLabel}
/>
);
@ -198,19 +232,37 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
type="number"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
className="h-9 text-xs sm:text-sm"
style={{ height: '36px', minHeight: '36px' }}
placeholder={column?.columnLabel}
/>
);
case "select": {
const options = selectOptions[filter.columnName] || [];
let options = selectOptions[filter.columnName] || [];
// 현재 선택된 값이 옵션 목록에 없으면 추가 (데이터 없을 때도 선택값 유지)
if (value && !options.find(opt => opt.value === value)) {
const savedLabel = selectedLabels[filter.columnName] || value;
options = [{ value, label: savedLabel }, ...options];
}
return (
<Select
value={value}
onValueChange={(val) => handleFilterChange(filter.columnName, val)}
onValueChange={(val) => {
// 선택한 값의 라벨 저장
const selectedOption = options.find(opt => opt.value === val);
if (selectedOption) {
setSelectedLabels(prev => ({
...prev,
[filter.columnName]: selectedOption.label,
}));
}
handleFilterChange(filter.columnName, val);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:text-sm">
<SelectTrigger className="h-9 min-h-9 text-xs sm:text-sm" style={{ height: '36px', minHeight: '36px' }}>
<SelectValue placeholder={column?.columnLabel || "선택"} />
</SelectTrigger>
<SelectContent>
@ -236,7 +288,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
type="text"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
className="h-9 text-xs sm:text-sm"
placeholder={column?.columnLabel}
/>
);
@ -260,15 +312,15 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
</div>
))}
{/* 검색 버튼 */}
{/* 초기화 버튼 */}
<Button
variant="default"
variant="outline"
size="sm"
onClick={handleSearch}
className="h-8 shrink-0 text-xs sm:h-9 sm:text-sm"
onClick={handleResetFilters}
className="h-9 shrink-0 text-xs sm:text-sm"
>
<Search className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
<X className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
)}