feat: 테이블 검색 필터 UI 개선 및 실시간 검색 구현
- 모든 필터 입력창 높이 통일 (h-9, 36px) - 실시간 검색: 입력 시 즉시 필터 적용 (검색 버튼 제거) - 초기화 버튼 추가: 모든 필터값을 한번에 리셋 - filters → searchValues 자동 변환 로직 추가 - select 필터: 선택된 값의 라벨 저장하여 데이터 없을 때도 표시 유지 - select 옵션 초기 로드 후 계속 유지 (dataCount 변경 시에도 유지) 주요 개선사항: 1. Input, Select, Date 등 모든 필터의 높이가 동일하게 표시 2. 사용자가 값을 입력하면 바로 테이블이 필터링됨 3. 초기화 버튼으로 간편하게 모든 필터 제거 가능 4. 필터링 결과가 0건이어도 select 박스의 선택값이 유지됨 알려진 제한사항: - 카테고리/엔티티 필터는 현재 테이블 데이터 기반으로만 옵션 표시 (전체 정의된 카테고리 값이 아닌, 실제 데이터에 있는 값만 표시)
This commit is contained in:
parent
71fd3f5ee7
commit
5c205753e2
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue