summaryrefslogtreecommitdiff
path: root/hooks/use-data-table copy.ts
diff options
context:
space:
mode:
Diffstat (limited to 'hooks/use-data-table copy.ts')
-rw-r--r--hooks/use-data-table copy.ts334
1 files changed, 334 insertions, 0 deletions
diff --git a/hooks/use-data-table copy.ts b/hooks/use-data-table copy.ts
new file mode 100644
index 00000000..a3301067
--- /dev/null
+++ b/hooks/use-data-table copy.ts
@@ -0,0 +1,334 @@
+"use client"
+
+import * as React from "react"
+import type { DataTableFilterField, ExtendedSortingState } from "@/types/table"
+import {
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ getGroupedRowModel,
+ getExpandedRowModel,
+ useReactTable,
+ type ColumnFiltersState,
+ type PaginationState,
+ type RowSelectionState,
+ type SortingState,
+ type TableOptions,
+ type TableState,
+ type Updater,
+ type VisibilityState,
+ type ExpandedState,
+} from "@tanstack/react-table"
+import {
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ useQueryState,
+ useQueryStates,
+ type Parser,
+ type UseQueryStateOptions,
+} from "nuqs"
+
+import { getSortingStateParser } from "@/lib/parsers"
+import { useDebouncedCallback } from "@/hooks/use-debounced-callback"
+
+interface UseDataTableProps<TData>
+ extends Omit<
+ TableOptions<TData>,
+ | "state"
+ | "pageCount"
+ | "getCoreRowModel"
+ | "manualFiltering"
+ | "manualPagination"
+ | "manualSorting"
+ | "onGroupingChange"
+ | "onExpandedChange"
+ | "getExpandedRowModel"
+ >,
+ Required<Pick<TableOptions<TData>, "pageCount">> {
+ filterFields?: DataTableFilterField<TData>[]
+ enableAdvancedFilter?: boolean
+ history?: "push" | "replace"
+ scroll?: boolean
+ shallow?: boolean
+ throttleMs?: number
+ debounceMs?: number
+ startTransition?: React.TransitionStartFunction
+ clearOnDefault?: boolean
+ initialState?: Omit<Partial<TableState>, "sorting"> & {
+ sorting?: ExtendedSortingState<TData>
+ /**
+ * 기본 그룹핑 컬럼 배열 (멀티 그룹핑)
+ */
+ grouping?: string[]
+ /**
+ * 그룹 확장/접기 상태
+ */
+ expanded?: Record<string, boolean>
+ }
+}
+
+export function useDataTable<TData>({
+ pageCount = -1,
+ filterFields = [],
+ enableAdvancedFilter = false,
+ history = "replace",
+ scroll = false,
+ shallow = true,
+ throttleMs = 50,
+ debounceMs = 300,
+ clearOnDefault = false,
+ startTransition,
+ initialState,
+ ...props
+}: UseDataTableProps<TData>) {
+ // 공통 URL QueryState 옵션
+ const queryStateOptions = React.useMemo<
+ Omit<UseQueryStateOptions<string>, "parse">
+ >(() => {
+ return {
+ history,
+ scroll,
+ shallow,
+ throttleMs,
+ debounceMs,
+ clearOnDefault,
+ startTransition,
+ }
+ }, [
+ history,
+ scroll,
+ shallow,
+ throttleMs,
+ debounceMs,
+ clearOnDefault,
+ startTransition,
+ ])
+
+ // -------- RowSelection & ColumnVisibility는 URL 동기화 없이 로컬 상태 ----------
+ const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(
+ initialState?.rowSelection ?? {}
+ )
+ const [columnVisibility, setColumnVisibility] =
+ React.useState<VisibilityState>(initialState?.columnVisibility ?? {})
+
+ // -------- Pagination (page, perPage) URL 동기화 --------
+ const [page, setPage] = useQueryState(
+ "page",
+ parseAsInteger.withOptions(queryStateOptions).withDefault(1)
+ )
+ const [perPage, setPerPage] = useQueryState(
+ "perPage",
+ parseAsInteger
+ .withOptions(queryStateOptions)
+ .withDefault(initialState?.pagination?.pageSize ?? 10)
+ )
+
+ // -------- Sorting (sort) URL 동기화 --------
+ const [sorting, setSorting] = useQueryState(
+ "sort",
+ getSortingStateParser<TData>()
+ .withOptions(queryStateOptions)
+ .withDefault(initialState?.sorting ?? [])
+ )
+ function onSortingChange(updaterOrValue: Updater<SortingState>) {
+ if (typeof updaterOrValue === "function") {
+ const newSorting = updaterOrValue(sorting) as ExtendedSortingState<TData>
+ void setSorting(newSorting)
+ } else {
+ void setSorting(updaterOrValue as ExtendedSortingState<TData>)
+ }
+ }
+
+ // -------- Grouping (group) URL 동기화 (멀티 컬럼) --------
+ const [grouping, setGrouping] = useQueryState(
+ "group",
+ parseAsArrayOf(parseAsString, ",")
+ .withOptions(queryStateOptions)
+ .withDefault(initialState?.grouping ?? [])
+ )
+ function onGroupingChange(updaterOrValue: Updater<string[]>) {
+ if (typeof updaterOrValue === "function") {
+ const newGrouping = updaterOrValue(grouping)
+ void setGrouping(newGrouping)
+ } else {
+ void setGrouping(updaterOrValue)
+ }
+ }
+
+ // -------- Group Expand/Collapse --------
+ const [expanded, setExpanded] = React.useState<ExpandedState>({}) // or true/false
+
+ function onExpandedChange(updater: Updater<ExpandedState>) {
+ setExpanded((old) => (typeof updater === "function" ? updater(old) : updater))
+ }
+
+ // -------- Filters (search/faceted) URL 동기화 --------
+ const filterParsers = React.useMemo(() => {
+ return filterFields.reduce<
+ Record<string, Parser<string> | Parser<string[]>>
+ >((acc, field) => {
+ if (field.options) {
+ // Faceted filter -> 여러 값 가능
+ acc[field.id] = parseAsArrayOf(parseAsString, ",").withOptions(
+ queryStateOptions
+ )
+ } else {
+ // Search filter -> 단일 값
+ acc[field.id] = parseAsString.withOptions(queryStateOptions)
+ }
+ return acc
+ }, {})
+ }, [filterFields, queryStateOptions])
+
+ const [filterValues, setFilterValues] = useQueryStates(filterParsers)
+ const debouncedSetFilterValues = useDebouncedCallback(
+ setFilterValues,
+ debounceMs
+ )
+
+ // -------- PaginationState 객체 --------
+ const pagination: PaginationState = {
+ pageIndex: page - 1,
+ pageSize: perPage,
+ }
+ function onPaginationChange(updaterOrValue: Updater<PaginationState>) {
+ if (typeof updaterOrValue === "function") {
+ const newPagination = updaterOrValue(pagination)
+ void setPage(newPagination.pageIndex + 1)
+ void setPerPage(newPagination.pageSize)
+ } else {
+ void setPage(updaterOrValue.pageIndex + 1)
+ void setPerPage(updaterOrValue.pageSize)
+ }
+ }
+
+ // -------- ColumnFiltersState --------
+ const initialColumnFilters: ColumnFiltersState = React.useMemo(() => {
+ // AdvancedFilter 모드가 아니면 URL에서 온 filterValues를 그대로 적용
+ return enableAdvancedFilter
+ ? []
+ : Object.entries(filterValues).reduce<ColumnFiltersState>(
+ (filters, [key, value]) => {
+ if (value !== null) {
+ filters.push({
+ id: key,
+ value: Array.isArray(value) ? value : [value],
+ })
+ }
+ return filters
+ },
+ []
+ )
+ }, [filterValues, enableAdvancedFilter])
+
+ const [columnFilters, setColumnFilters] =
+ React.useState<ColumnFiltersState>(initialColumnFilters)
+
+ // 검색용 / Facet 필터용 컬럼 구분
+ const { searchableColumns, filterableColumns } = React.useMemo(() => {
+ return enableAdvancedFilter
+ ? { searchableColumns: [], filterableColumns: [] }
+ : {
+ searchableColumns: filterFields.filter((field) => !field.options),
+ filterableColumns: filterFields.filter((field) => field.options),
+ }
+ }, [filterFields, enableAdvancedFilter])
+
+ const onColumnFiltersChange = React.useCallback(
+ (updaterOrValue: Updater<ColumnFiltersState>) => {
+ if (enableAdvancedFilter) return
+ setColumnFilters((prev) => {
+ const next =
+ typeof updaterOrValue === "function"
+ ? updaterOrValue(prev)
+ : updaterOrValue
+
+ const filterUpdates = next.reduce<
+ Record<string, string | string[] | null>
+ >((acc, filter) => {
+ if (searchableColumns.find((col) => col.id === filter.id)) {
+ acc[filter.id] = filter.value as string
+ } else if (filterableColumns.find((col) => col.id === filter.id)) {
+ acc[filter.id] = filter.value as string[]
+ }
+ return acc
+ }, {})
+
+ // 빠진 필터는 null로 설정
+ prev.forEach((prevFilter) => {
+ if (!next.some((filter) => filter.id === prevFilter.id)) {
+ filterUpdates[prevFilter.id] = null
+ }
+ })
+
+ // 필터가 바뀌면 첫 페이지로
+ void setPage(1)
+ debouncedSetFilterValues(filterUpdates)
+ return next
+ })
+ },
+ [
+ debouncedSetFilterValues,
+ enableAdvancedFilter,
+ filterableColumns,
+ searchableColumns,
+ setPage,
+ ]
+ )
+
+ // -------- TanStack Table 인스턴스 생성 --------
+ const table = useReactTable({
+ ...props,
+ initialState,
+ pageCount,
+ state: {
+ pagination,
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters: enableAdvancedFilter ? [] : columnFilters,
+ grouping,
+ expanded,
+ },
+
+ // 콜백들
+ onRowSelectionChange: setRowSelection,
+ onPaginationChange,
+ onSortingChange,
+ onColumnFiltersChange,
+ onColumnVisibilityChange: setColumnVisibility,
+ onGroupingChange,
+ onExpandedChange,
+
+ // 기본 모델
+ getCoreRowModel: getCoreRowModel(),
+ // 필터 (Advanced 모드 아니면 사용)
+ getFilteredRowModel: enableAdvancedFilter ? undefined : getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ // 그룹 + 확장
+ getGroupedRowModel: getGroupedRowModel(),
+ getExpandedRowModel: getExpandedRowModel(),
+
+ // Faceted (Facet 필터용)
+ getFacetedRowModel: enableAdvancedFilter ? undefined : getFacetedRowModel(),
+ getFacetedUniqueValues: enableAdvancedFilter
+ ? undefined
+ : getFacetedUniqueValues(),
+
+ // 서버 사이드 사용
+ manualPagination: true,
+ manualSorting: true,
+ manualFiltering: true,
+ // 그룹핑도 서버에서 처리한다면 별도 로직이 필요하지만,
+ // TanStack Table v8에는 manualGrouping 옵션은 없음
+ // (그룹핑을 서버에서 이미 해서 내려받는다면 row 구조 처리 필요)
+
+ })
+
+ return { table }
+} \ No newline at end of file