diff --git a/frontend/components/screen/table-options/FilterPanel.tsx b/frontend/components/screen/table-options/FilterPanel.tsx index c20a0115..f7e5108a 100644 --- a/frontend/components/screen/table-options/FilterPanel.tsx +++ b/frontend/components/screen/table-options/FilterPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { Dialog, @@ -18,197 +18,302 @@ import { SelectValue, } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Checkbox } from "@/components/ui/checkbox"; import { Plus, X } from "lucide-react"; import { TableFilter } from "@/types/table-options"; interface Props { isOpen: boolean; onClose: () => void; + onFiltersApplied?: (filters: TableFilter[]) => void; // 필터 적용 시 콜백 } -export const FilterPanel: React.FC = ({ - isOpen, - onClose, -}) => { - const { getTable, selectedTableId } = useTableOptions(); - const table = selectedTableId ? getTable(selectedTableId) : undefined; - - const [activeFilters, setActiveFilters] = useState([]); - - const addFilter = () => { - setActiveFilters([ - ...activeFilters, - { columnName: "", operator: "contains", value: "" }, - ]); - }; - - const removeFilter = (index: number) => { - setActiveFilters(activeFilters.filter((_, i) => i !== index)); - }; - - const updateFilter = ( - index: number, - field: keyof TableFilter, - value: any - ) => { - setActiveFilters( - activeFilters.map((filter, i) => - i === index ? { ...filter, [field]: value } : filter - ) - ); - }; - - const applyFilters = () => { - // 빈 필터 제거 - const validFilters = activeFilters.filter( - (f) => f.columnName && f.value !== "" - ); - table?.onFilterChange(validFilters); - onOpenChange(false); - }; - - const clearFilters = () => { - setActiveFilters([]); - table?.onFilterChange([]); - }; - - const operatorLabels: Record = { - equals: "같음", +// 필터 타입별 연산자 +const operatorsByType: Record> = { + text: { contains: "포함", + equals: "같음", startsWith: "시작", endsWith: "끝", + notEquals: "같지 않음", + }, + number: { + equals: "같음", gt: "보다 큼", lt: "보다 작음", gte: "이상", lte: "이하", notEquals: "같지 않음", + }, + date: { + equals: "같음", + gt: "이후", + lt: "이전", + gte: "이후 포함", + lte: "이전 포함", + }, + select: { + equals: "같음", + notEquals: "같지 않음", + }, +}; + +// 컬럼 필터 설정 인터페이스 +interface ColumnFilterConfig { + columnName: string; + columnLabel: string; + inputType: string; + enabled: boolean; + filterType: "text" | "number" | "date" | "select"; + selectOptions?: Array<{ label: string; value: string }>; +} + +export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied }) => { + const { getTable, selectedTableId } = useTableOptions(); + const table = selectedTableId ? getTable(selectedTableId) : undefined; + + const [columnFilters, setColumnFilters] = useState([]); + const [selectAll, setSelectAll] = useState(false); + + // localStorage에서 저장된 필터 설정 불러오기 + useEffect(() => { + if (table?.columns && table?.tableName) { + const storageKey = `table_filters_${table.tableName}`; + const savedFilters = localStorage.getItem(storageKey); + + let filters: ColumnFilterConfig[]; + + if (savedFilters) { + try { + const parsed = JSON.parse(savedFilters) as ColumnFilterConfig[]; + // 저장된 설정과 현재 컬럼 병합 + filters = table.columns + .filter((col) => col.filterable !== false) + .map((col) => { + const saved = parsed.find((f) => f.columnName === col.columnName); + return saved || { + columnName: col.columnName, + columnLabel: col.columnLabel, + inputType: col.inputType || "text", + enabled: false, + filterType: mapInputTypeToFilterType(col.inputType || "text"), + }; + }); + } catch (error) { + console.error("저장된 필터 설정 불러오기 실패:", error); + filters = table.columns + .filter((col) => col.filterable !== false) + .map((col) => ({ + columnName: col.columnName, + columnLabel: col.columnLabel, + inputType: col.inputType || "text", + enabled: false, + filterType: mapInputTypeToFilterType(col.inputType || "text"), + })); + } + } else { + filters = table.columns + .filter((col) => col.filterable !== false) + .map((col) => ({ + columnName: col.columnName, + columnLabel: col.columnLabel, + inputType: col.inputType || "text", + enabled: false, + filterType: mapInputTypeToFilterType(col.inputType || "text"), + })); + } + + setColumnFilters(filters); + } + }, [table?.columns, table?.tableName]); + + // inputType을 filterType으로 매핑 + const mapInputTypeToFilterType = ( + inputType: string + ): "text" | "number" | "date" | "select" => { + if (inputType.includes("number") || inputType.includes("decimal")) { + return "number"; + } + if (inputType.includes("date") || inputType.includes("time")) { + return "date"; + } + if ( + inputType.includes("select") || + inputType.includes("code") || + inputType.includes("category") + ) { + return "select"; + } + return "text"; }; + // 전체 선택/해제 + const toggleSelectAll = (checked: boolean) => { + setSelectAll(checked); + setColumnFilters((prev) => + prev.map((filter) => ({ ...filter, enabled: checked })) + ); + }; + + // 개별 필터 토글 + const toggleFilter = (columnName: string) => { + setColumnFilters((prev) => + prev.map((filter) => + filter.columnName === columnName + ? { ...filter, enabled: !filter.enabled } + : filter + ) + ); + }; + + // 필터 타입 변경 + const updateFilterType = ( + columnName: string, + filterType: "text" | "number" | "date" | "select" + ) => { + setColumnFilters((prev) => + prev.map((filter) => + filter.columnName === columnName ? { ...filter, filterType } : filter + ) + ); + }; + + // 저장 + const applyFilters = () => { + // enabled된 필터들만 TableFilter로 변환 + const activeFilters: TableFilter[] = columnFilters + .filter((cf) => cf.enabled) + .map((cf) => ({ + columnName: cf.columnName, + operator: "contains", // 기본 연산자 + value: "", + filterType: cf.filterType, + })); + + // localStorage에 저장 + if (table?.tableName) { + const storageKey = `table_filters_${table.tableName}`; + localStorage.setItem(storageKey, JSON.stringify(columnFilters)); + } + + table?.onFilterChange(activeFilters); + + // 콜백으로 활성화된 필터 정보 전달 + onFiltersApplied?.(activeFilters); + + onClose(); + }; + + // 초기화 + const clearFilters = () => { + setColumnFilters((prev) => + prev.map((filter) => ({ ...filter, enabled: false })) + ); + setSelectAll(false); + }; + + const enabledCount = columnFilters.filter((f) => f.enabled).length; + return ( - + 검색 필터 설정 - 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 - 표시됩니다. + 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
{/* 전체 선택/해제 */} -
-
- 총 {activeFilters.length}개의 검색 필터가 표시됩니다 +
+
+ + toggleSelectAll(checked as boolean) + } + /> + 전체 선택/해제 +
+
+ {enabledCount} / {columnFilters.length}개
-
- {/* 필터 리스트 */} - -
- {activeFilters.map((filter, index) => ( + {/* 컬럼 필터 리스트 */} + +
+ {columnFilters.map((filter) => (
- {/* 컬럼 선택 */} - + {/* 체크박스 */} + toggleFilter(filter.columnName)} + /> - {/* 연산자 선택 */} + {/* 컬럼 정보 */} +
+
+ {filter.columnLabel} +
+
+ {filter.columnName} +
+
+ + {/* 필터 타입 선택 */} - - {/* 값 입력 */} - - updateFilter(index, "value", e.target.value) - } - placeholder="값 입력" - className="h-8 flex-1 text-xs sm:h-9 sm:text-sm" - /> - - {/* 삭제 버튼 */} -
))}
- {/* 필터 추가 버튼 */} - + {/* 안내 메시지 */} +
+ 검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요 +
+ +
+ )} - {/* 테이블 선택 드롭다운 (여러 테이블이 있고, showTableSelector가 true일 때만) */} - {showTableSelector && hasMultipleTables && ( - - )} + {/* 필터가 없을 때는 빈 공간 */} + {activeFilters.length === 0 &&
} - {/* 테이블이 하나만 있을 때는 라벨만 표시 */} - {!hasMultipleTables && tableList.length === 1 && ( -
- {tableList[0].label} -
- )} - - {/* 테이블이 없을 때 */} - {tableList.length === 0 && ( -
- 화면에 테이블 컴포넌트를 추가하면 자동으로 감지됩니다 -
- )} -
- - {/* 오른쪽: 버튼들 */} + {/* 오른쪽: 데이터 건수 + 설정 버튼들 */}
+ {/* 데이터 건수 표시 */} + {currentTable?.dataCount !== undefined && ( +
+ {currentTable.dataCount.toLocaleString()}건 +
+ )} +
); diff --git a/frontend/types/table-options.ts b/frontend/types/table-options.ts index e8727a44..32bc0e6d 100644 --- a/frontend/types/table-options.ts +++ b/frontend/types/table-options.ts @@ -18,6 +18,7 @@ export interface TableFilter { | "lte" | "notEquals"; value: string | number | boolean; + filterType?: "text" | "number" | "date" | "select"; // 필터 입력 타입 } /** @@ -52,11 +53,15 @@ export interface TableRegistration { label: string; // 사용자에게 보이는 이름 (예: "품목 관리") tableName: string; // 실제 DB 테이블명 (예: "item_info") columns: TableColumn[]; + dataCount?: number; // 현재 표시된 데이터 건수 // 콜백 함수들 onFilterChange: (filters: TableFilter[]) => void; onGroupChange: (groups: string[]) => void; onColumnVisibilityChange: (columns: ColumnVisibility[]) => void; + + // 데이터 조회 함수 (선택 타입 필터용) + getColumnUniqueValues?: (columnName: string) => Promise>; } /** @@ -67,6 +72,7 @@ export interface TableOptionsContextValue { registerTable: (registration: TableRegistration) => void; unregisterTable: (tableId: string) => void; getTable: (tableId: string) => TableRegistration | undefined; + updateTableDataCount: (tableId: string, count: number) => void; // 데이터 건수 업데이트 selectedTableId: string | null; setSelectedTableId: (tableId: string | null) => void; }