From 4e63d8427d26d0d1b366ddc53650e15f3481fc75 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 24 Jun 2025 01:44:03 +0000 Subject: (대표님/최겸) 20250624 작업사항 10시43분 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/use-data-table.ts | 288 ++++++++++++++++++++++++------------------------ 1 file changed, 144 insertions(+), 144 deletions(-) (limited to 'hooks') diff --git a/hooks/use-data-table.ts b/hooks/use-data-table.ts index d0521a7e..a93b25c0 100644 --- a/hooks/use-data-table.ts +++ b/hooks/use-data-table.ts @@ -1,8 +1,11 @@ -// hooks/use-data-table.ts (무한 렌더링 수정) -"use client" - -import * as React from "react" -import type { DataTableFilterField, ExtendedSortingState } from "@/types/table" +// ====================== hooks/use-data-table.ts ====================== +"use client"; + +import * as React from "react"; +import type { + DataTableFilterField, + ExtendedSortingState, +} from "@/types/table"; import { getCoreRowModel, getFacetedRowModel, @@ -22,7 +25,7 @@ import { type Updater, type VisibilityState, type ExpandedState, -} from "@tanstack/react-table" +} from "@tanstack/react-table"; import { parseAsArrayOf, parseAsInteger, @@ -31,24 +34,36 @@ import { useQueryStates, type Parser, type UseQueryStateOptions, -} from "nuqs" -import useSWRInfinite from "swr/infinite" +} from "nuqs"; +import useSWRInfinite from "swr/infinite"; -import { getSortingStateParser } from "@/lib/parsers" -import { useDebouncedCallback } from "@/hooks/use-debounced-callback" -import isEqual from "fast-deep-equal" +import { getSortingStateParser } from "@/lib/parsers"; +import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; +import isEqual from "fast-deep-equal"; -// 무한 스크롤 임계값 (이 값 이상이면 무한 스크롤 모드) -const INFINITE_SCROLL_THRESHOLD = 1_000_000 +// ─────────────────────────────────────────────────────────────────────── +// 무한 스크롤 관련 상수 및 타입 +// ─────────────────────────────────────────────────────────────────────── +const INFINITE_SCROLL_THRESHOLD = 1_000_000; -// 무한 스크롤 설정 interface InfiniteScrollConfig { - apiEndpoint: string - tableName: string - maxPageSize?: number + apiEndpoint: string; + tableName: string; + maxPageSize?: number; } -interface UseDataTableProps +interface InfiniteScrollResponse { + mode: "infinite"; + data: TData[]; + hasNextPage: boolean; + nextCursor: string | null; + total?: number | null; +} + +// ─────────────────────────────────────────────────────────────────────── +// Hook props +// ─────────────────────────────────────────────────────────────────────── +export interface UseDataTableProps extends Omit< TableOptions, | "state" @@ -63,35 +78,29 @@ interface UseDataTableProps | "data" >, Required, "pageCount">> { - filterFields?: DataTableFilterField[] - enableAdvancedFilter?: boolean - history?: "push" | "replace" - scroll?: boolean - shallow?: boolean - throttleMs?: number - debounceMs?: number - startTransition?: React.TransitionStartFunction - clearOnDefault?: boolean + filterFields?: DataTableFilterField[]; + enableAdvancedFilter?: boolean; + history?: "push" | "replace"; + scroll?: boolean; + shallow?: boolean; + throttleMs?: number; + debounceMs?: number; + startTransition?: React.TransitionStartFunction; + clearOnDefault?: boolean; initialState?: Omit, "sorting"> & { - sorting?: ExtendedSortingState - grouping?: string[] - expanded?: Record - } - // 기존 데이터 (페이지네이션 모드용) - data?: TData[] - // 무한 스크롤 설정 (pageSize 기반 자동 활성화) - infiniteScrollConfig?: InfiniteScrollConfig -} - -// 무한 스크롤 응답 타입 -interface InfiniteScrollResponse { - mode: 'infinite' - data: TData[] - hasNextPage: boolean - nextCursor: string | null - total?: number | null + sorting?: ExtendedSortingState; + grouping?: string[]; + expanded?: Record; + }; + data?: TData[]; // 페이지네이션 모드 초기 데이터 + infiniteScrollConfig?: InfiniteScrollConfig; + /** Table state가 변경되어 URL을 동기화할 때 호출됩니다. 이미 같은 문자열이면 호출되지 않습니다. */ + onStateToUrl?: (queryString: string) => void; } +// ─────────────────────────────────────────────────────────────────────── +// useDataTable +// ─────────────────────────────────────────────────────────────────────── export function useDataTable({ pageCount = -1, filterFields = [], @@ -106,14 +115,14 @@ export function useDataTable({ initialState, data: initialData = [], infiniteScrollConfig, + onStateToUrl, ...props }: UseDataTableProps) { - - // 공통 URL QueryState 옵션 + // ───────────────────────── 공통 QueryState 옵션 ─────────────────────── const queryStateOptions = React.useMemo< Omit, "parse"> - >(() => { - return { + >( + () => ({ history, scroll, shallow, @@ -121,75 +130,120 @@ export function useDataTable({ debounceMs, clearOnDefault, startTransition, - } - }, [ - history, - scroll, - shallow, - throttleMs, - debounceMs, - clearOnDefault, - startTransition, - ]) + }), + [history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition], + ); - // -------- 기존 상태들 -------- + // ───────────────────────── 로컬 상태들 ──────────────────────────────── const [rowSelection, setRowSelection] = React.useState( - initialState?.rowSelection ?? {} - ) + initialState?.rowSelection ?? {}, + ); const [columnVisibility, setColumnVisibility] = - React.useState(initialState?.columnVisibility ?? {}) + React.useState(initialState?.columnVisibility ?? {}); + const [expanded, setExpanded] = React.useState({}); - // -------- Pagination URL 동기화 -------- + // ───────────────────────── URL ↔️ Pagination 동기화 ─────────────────── const [page, setPage] = useQueryState( "page", - parseAsInteger.withOptions(queryStateOptions).withDefault(1) - ) + parseAsInteger.withOptions(queryStateOptions).withDefault(1), + ); const [perPage, setPerPage] = useQueryState( "perPage", parseAsInteger .withOptions(queryStateOptions) - .withDefault(initialState?.pagination?.pageSize ?? 10) - ) - - // pageSize 기반 무한 스크롤 모드 자동 결정 - // const isInfiniteMode = perPage >= INFINITE_SCROLL_THRESHOLD - - const isInfiniteMode = !!(infiniteScrollConfig && perPage >= INFINITE_SCROLL_THRESHOLD) + .withDefault(initialState?.pagination?.pageSize ?? 10), + ); + const isInfiniteMode = !!( + infiniteScrollConfig && perPage >= INFINITE_SCROLL_THRESHOLD + ); - // -------- Sorting URL 동기화 -------- + // ───────────────────────── URL ↔️ Sorting 동기화 ───────────────────── const [sorting, setSorting] = useQueryState( "sort", getSortingStateParser() .withOptions(queryStateOptions) - .withDefault(initialState?.sorting ?? []) - ) + .withDefault(initialState?.sorting ?? []), + ); - // -------- Advanced Filters URL 동기화 -------- + // ───────────────────────── URL ↔️ 기타 파라미터 ────────────────────── const [filters, setFilters] = useQueryState( "filters", - parseAsString.withOptions(queryStateOptions).withDefault("[]") - ) - + parseAsString.withOptions(queryStateOptions).withDefault("[]"), + ); const [joinOperator, setJoinOperator] = useQueryState( "joinOperator", - parseAsString.withOptions(queryStateOptions).withDefault("and") - ) - + parseAsString.withOptions(queryStateOptions).withDefault("and"), + ); const [search, setSearch] = useQueryState( "search", - parseAsString.withOptions(queryStateOptions).withDefault("") - ) - - // -------- Grouping URL 동기화 -------- + parseAsString.withOptions(queryStateOptions).withDefault(""), + ); const [grouping, setGrouping] = useQueryState( "group", parseAsArrayOf(parseAsString, ",") .withOptions(queryStateOptions) - .withDefault(initialState?.grouping ?? []) - ) + .withDefault(initialState?.grouping ?? []), + ); - const [expanded, setExpanded] = React.useState({}) + // ───────────────────────── Pagination helper ───────────────────────── + const pagination: PaginationState = { + pageIndex: page - 1, + pageSize: perPage, + }; + + // ───────────────────────── Table → URL 직렬화 ──────────────────────── + const toQueryString = React.useCallback( + (pg: PaginationState, sort: SortingState) => { + const p = new URLSearchParams(); + if (pg.pageIndex > 0) p.set("page", String(pg.pageIndex + 1)); + if (pg.pageSize !== perPage) p.set("perPage", String(pg.pageSize)); + if (sort.length) p.set("sort", JSON.stringify(sort)); + return p.toString(); + }, + [perPage], + ); + + // ───────────────────────── 페이징 변경 ─────────────────────────────── + const handlePageSizeChange = React.useCallback( + (newPageSize: number) => { + void setPerPage(newPageSize); + const wasInfinite = perPage >= INFINITE_SCROLL_THRESHOLD; + const willBeInfinite = newPageSize >= INFINITE_SCROLL_THRESHOLD; + if (wasInfinite !== willBeInfinite) { + void setPage(1); + } + }, + [perPage, setPerPage, setPage], + ); + + function onPaginationChange(updater: Updater) { + if (isInfiniteMode) return; + const next = typeof updater === "function" ? updater(pagination) : updater; + + // URL 동기화 (diff) + if (onStateToUrl) { + const qs = toQueryString(next, sorting); + if (qs !== window.location.search.slice(1)) onStateToUrl(qs); + } + + void setPage(next.pageIndex + 1); + if (next.pageSize !== perPage) handlePageSizeChange(next.pageSize); + } + + // ───────────────────────── Sorting 변경 ────────────────────────────── + function onSortingChange(updater: Updater) { + const next = typeof updater === "function" ? updater(sorting) : updater; + + if (onStateToUrl) { + const qs = toQueryString(pagination, next); + if (qs !== window.location.search.slice(1)) onStateToUrl(qs); + } + + if (!isEqual(next, sorting)) { + void setSorting(next as ExtendedSortingState); + } + } // -------- 무한 스크롤 SWR 설정 -------- const parsedFilters = React.useMemo(() => { @@ -337,56 +391,6 @@ export function useDataTable({ // 최종 데이터 결정 const finalData = isInfiniteMode ? infiniteData : initialData - // pageSize 변경 핸들러 - const handlePageSizeChange = React.useCallback((newPageSize: number) => { - // URL 상태 업데이트 (이게 핵심!) - void setPerPage(newPageSize) - - // 모드 전환 시 첫 페이지로 리셋 - const wasInfiniteMode = perPage >= INFINITE_SCROLL_THRESHOLD - const willBeInfiniteMode = newPageSize >= INFINITE_SCROLL_THRESHOLD - - if (wasInfiniteMode !== willBeInfiniteMode) { - void setPage(1) - - // 무한 스크롤에서 페이지네이션으로 전환 시 무한 스크롤 데이터 리셋 - if (wasInfiniteMode && infiniteScrollActions) { - infiniteScrollActions.reset() - } - } - }, [setPerPage, setPage, perPage, infiniteScrollActions]) - - // 그리고 onPaginationChange 함수도 수정 - function onPaginationChange(updaterOrValue: Updater) { - if (isInfiniteMode) return // 무한 스크롤 모드에서는 페이지네이션 변경 무시 - - if (typeof updaterOrValue === "function") { - const newPagination = updaterOrValue(pagination) - void setPage(newPagination.pageIndex + 1) - // perPage 변경은 handlePageSizeChange를 통해서만! - if (newPagination.pageSize !== perPage) { - handlePageSizeChange(newPagination.pageSize) - } - } else { - void setPage(updaterOrValue.pageIndex + 1) - // perPage 변경은 handlePageSizeChange를 통해서만! - if (updaterOrValue.pageSize !== perPage) { - handlePageSizeChange(updaterOrValue.pageSize) - } - } - } - - // -------- 나머지 기존 로직들 -------- - function onSortingChange(updater: Updater) { - const next = - typeof updater === "function" ? updater(sorting) : updater - if (!isEqual(next, sorting)) { - resetInfiniteScroll() - void setSorting(next as ExtendedSortingState) - } - } - - function onGroupingChange(updaterOrValue: Updater) { if (typeof updaterOrValue === "function") { const newGrouping = updaterOrValue(grouping) @@ -400,11 +404,7 @@ export function useDataTable({ setExpanded((old) => (typeof updater === "function" ? updater(old) : updater)) } - const pagination: PaginationState = { - pageIndex: page - 1, - pageSize: perPage, - } - + // 기존 필터 로직들... (동일) const filterParsers = React.useMemo(() => { -- cgit v1.2.3