"use client" import * as React from "react" import type { DataTableAdvancedFilterField, Filter, FilterOperator, JoinOperator, StringKeyOf, } from "@/types/table" import { type Table } from "@tanstack/react-table" import { CalendarIcon, Check, ChevronsUpDown, GripVertical, ListFilter, Trash2, } from "lucide-react" import { customAlphabet } from "nanoid" import { parseAsStringEnum, useQueryState } from "nuqs" import { dataTableConfig } from "@/config/data-table" import { getDefaultFilterOperator, getFilterOperators } from "@/lib/data-table" import { getFiltersStateParser } from "@/lib/parsers" import { cn, formatDate } from "@/lib/utils" import { useDebouncedCallback } from "@/hooks/use-debounced-callback" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { FacetedFilter, FacetedFilterContent, FacetedFilterEmpty, FacetedFilterGroup, FacetedFilterInput, FacetedFilterItem, FacetedFilterList, FacetedFilterTrigger, } from "@/components/ui/faceted-filter" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sortable, SortableDragHandle, SortableItem, } from "@/components/ui/sortable" import { useParams } from 'next/navigation'; import { useTranslation } from '@/i18n/client' import deepEqual from "fast-deep-equal" interface DataTableFilterListProps { table: Table filterFields: DataTableAdvancedFilterField[] debounceMs: number shallow?: boolean // ✅ 외부에서 전달받은 필터를 적용하기 위한 props externalFilters?: Filter[] externalJoinOperator?: JoinOperator onFiltersChange?: (filters: Filter[], joinOperator: JoinOperator) => void } export function isSame(a: unknown, b: unknown) { return JSON.stringify(a) === JSON.stringify(b) } export function DataTableFilterList({ table, filterFields, debounceMs, shallow, externalFilters, externalJoinOperator, onFiltersChange, }: DataTableFilterListProps) { const prevRef = React.useRef<{ filters: Filter[] join: JoinOperator } | null>(null) const params = useParams(); const lng = params ? (params.lng as string) : 'en'; const { t, i18n } = useTranslation(lng); const id = React.useId() // ✅ 기존 URL 상태 관리 const [filters, setFilters] = useQueryState( "filters", getFiltersStateParser(table.getRowModel().rows[0]?.original) .withDefault([]) .withOptions({ clearOnDefault: true, shallow, }) ) const [joinOperator, setJoinOperator] = useQueryState( "joinOperator", parseAsStringEnum(["and", "or"]).withDefault("and").withOptions({ clearOnDefault: true, shallow, }) ) // Page state to reset when filters change const [, setPage] = useQueryState("page", { parse: (str) => str ? parseInt(str) : 1, serialize: (val) => val.toString(), eq: (a, b) => a === b, clearOnDefault: true, shallow, }) const safeSetFilters = React.useCallback( (next: Filter[] | ((p: Filter[]) => Filter[])) => { setFilters((prev) => { const value = typeof next === "function" ? next(prev) : next if (!deepEqual(prev, value)) { // Reset page to 1 when filters change void setPage(1); } return deepEqual(prev, value) ? prev : value // <─ 달라진 게 없으면 그대로 }) }, [setFilters, setPage] ) // ✅ 외부 필터가 전달되면 URL 상태를 업데이트 React.useEffect(() => { if (externalFilters && !deepEqual(externalFilters, filters)) { console.log("=== 외부 필터 적용 ===", externalFilters); safeSetFilters(externalFilters); } }, [externalFilters, setFilters, safeSetFilters]); React.useEffect(() => { if (externalJoinOperator && externalJoinOperator !== joinOperator) { console.log("=== 외부 조인 연산자 적용 ===", externalJoinOperator); void setPage(1); // Reset page when join operator changes setJoinOperator(externalJoinOperator); } }, [externalJoinOperator, setJoinOperator, joinOperator, setPage]); // ✅ 필터 변경 시 부모에게 알림 React.useEffect(() => { const prev = prevRef.current const changed = !prev || !deepEqual(prev.filters, filters) || prev.join !== joinOperator if (changed) { prevRef.current = { filters, join: joinOperator } onFiltersChange?.(filters, joinOperator) } }, [filters, joinOperator, onFiltersChange]) const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs) function addFilter() { const filterField = filterFields[0] if (!filterField) return void setFilters([ ...filters, { id: filterField.id, value: "", type: filterField.type, operator: getDefaultFilterOperator(filterField.type), rowId: customAlphabet( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6 )(), }, ]) } function updateFilter({ rowId, field, debounced = false, }: { rowId: string field: Omit>, "rowId"> debounced?: boolean }) { const updateFunction = debounced ? debouncedSetFilters : setFilters updateFunction((prevFilters) => { const updatedFilters = prevFilters.map((filter) => { if (filter.rowId === rowId) { return { ...filter, ...field } } return filter }) return updatedFilters }) } function removeFilter(rowId: string) { const updatedFilters = filters.filter((filter) => filter.rowId !== rowId) void setFilters(updatedFilters) } function moveFilter(activeIndex: number, overIndex: number) { void setFilters((prevFilters) => { const newFilters = [...prevFilters] const [removed] = newFilters.splice(activeIndex, 1) if (!removed) return prevFilters newFilters.splice(overIndex, 0, removed) return newFilters }) } // ✅ 모든 필터 초기화 (외부 필터 포함) function resetAllFilters() { void setFilters([]) void setJoinOperator("and") } function renderFilterInput({ filter, inputId, }: { filter: Filter inputId: string }) { const filterField = filterFields.find((f) => f.id === filter.id) if (!filterField) return null if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty") { return (
) } switch (filter.type) { case "text": case "number": return ( updateFilter({ rowId: filter.rowId, field: { value: event.target.value }, debounced: true, }) } /> ) case "select": return ( No options found. {filterField?.options?.map((option) => ( { updateFilter({ rowId: filter.rowId, field: { value } }) setTimeout(() => { document.getElementById(inputId)?.click() }, 0) }} > {option.icon && ( ))} ) case "multi-select": const selectedValues = new Set( Array.isArray(filter.value) ? filter.value : [] ) return ( No options found. {filterField?.options?.map((option) => ( { const currentValue = Array.isArray(filter.value) ? filter.value : [] const newValue = currentValue.includes(value) ? currentValue.filter((v) => v !== value) : [...currentValue, value] updateFilter({ rowId: filter.rowId, field: { value: newValue }, }) }} > {option.icon && ( ))} ) case "date": const dateValue = Array.isArray(filter.value) ? filter.value.filter(Boolean) : [filter.value, filter.value].filter(Boolean) const displayValue = filter.operator === "isBetween" && dateValue.length === 2 ? `${formatDate(dateValue[0] ?? new Date())} - ${formatDate( dateValue[1] ?? new Date() )}` : dateValue[0] ? formatDate(dateValue[0]) : "Pick a date" return ( {filter.operator === "isBetween" ? ( { updateFilter({ rowId: filter.rowId, field: { value: date ? [ date.from?.toISOString() ?? "", date.to?.toISOString() ?? "", ] : [], }, }) }} initialFocus numberOfMonths={1} /> ) : ( { updateFilter({ rowId: filter.rowId, field: { value: date?.toISOString() ?? "" }, }) setTimeout(() => { document.getElementById(inputId)?.click() }, 0) }} initialFocus /> )} ) case "boolean": { if (Array.isArray(filter.value)) return null return ( ) } default: return null } } return ( ({ id: item.rowId }))} onMove={({ activeIndex, overIndex }) => moveFilter(activeIndex, overIndex) } overlay={
} > 0 ? "gap-3.5" : "gap-2" )} > {filters.length > 0 ? (

{t("tableToolBar.filters")}

) : (

{t("nofilters")}

{t("addfilters")}

)}
{filters.map((filter, index) => { const filterId = `${id}-filter-${filter.rowId}` const joinOperatorListboxId = `${filterId}-join-operator-listbox` const fieldListboxId = `${filterId}-field-listbox` const fieldTriggerId = `${filterId}-field-trigger` const operatorListboxId = `${filterId}-operator-listbox` const inputId = `${filterId}-input` return (
{index === 0 ? ( {t("Where")} ) : index === 1 ? ( ) : ( {joinOperator} )}
document.getElementById(fieldTriggerId)?.focus({ preventScroll: true, }) } > {t("noFields")} {filterFields.map((field) => ( { const filterField = filterFields.find( (col) => col.id === value ) if (!filterField) return updateFilter({ rowId: filter.rowId, field: { id: value as StringKeyOf, type: filterField.type, operator: getDefaultFilterOperator( filterField.type ), value: "", }, }) document .getElementById(fieldTriggerId) ?.click() }} > {field.label} ))}
{renderFilterInput({ filter, inputId })}
) })}
{filters.length > 0 ? ( ) : null}
) }