summaryrefslogtreecommitdiff
path: root/hooks/use-data-table.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-24 01:44:03 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-24 01:44:03 +0000
commit4e63d8427d26d0d1b366ddc53650e15f3481fc75 (patch)
treeddfb69a92db56498ea591eed0f14ed2ce823431c /hooks/use-data-table.ts
parent127185717263ea3162bd192c83b4c7efe0d96e50 (diff)
(대표님/최겸) 20250624 작업사항 10시43분
Diffstat (limited to 'hooks/use-data-table.ts')
-rw-r--r--hooks/use-data-table.ts288
1 files changed, 144 insertions, 144 deletions
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<TData>
+interface InfiniteScrollResponse<TData> {
+ mode: "infinite";
+ data: TData[];
+ hasNextPage: boolean;
+ nextCursor: string | null;
+ total?: number | null;
+}
+
+// ───────────────────────────────────────────────────────────────────────
+// Hook props
+// ───────────────────────────────────────────────────────────────────────
+export interface UseDataTableProps<TData>
extends Omit<
TableOptions<TData>,
| "state"
@@ -63,35 +78,29 @@ interface UseDataTableProps<TData>
| "data"
>,
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
+ 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>
- }
- // 기존 데이터 (페이지네이션 모드용)
- data?: TData[]
- // 무한 스크롤 설정 (pageSize 기반 자동 활성화)
- infiniteScrollConfig?: InfiniteScrollConfig
-}
-
-// 무한 스크롤 응답 타입
-interface InfiniteScrollResponse<TData> {
- mode: 'infinite'
- data: TData[]
- hasNextPage: boolean
- nextCursor: string | null
- total?: number | null
+ sorting?: ExtendedSortingState<TData>;
+ grouping?: string[];
+ expanded?: Record<string, boolean>;
+ };
+ data?: TData[]; // 페이지네이션 모드 초기 데이터
+ infiniteScrollConfig?: InfiniteScrollConfig;
+ /** Table state가 변경되어 URL을 동기화할 때 호출됩니다. 이미 같은 문자열이면 호출되지 않습니다. */
+ onStateToUrl?: (queryString: string) => void;
}
+// ───────────────────────────────────────────────────────────────────────
+// useDataTable
+// ───────────────────────────────────────────────────────────────────────
export function useDataTable<TData>({
pageCount = -1,
filterFields = [],
@@ -106,14 +115,14 @@ export function useDataTable<TData>({
initialState,
data: initialData = [],
infiniteScrollConfig,
+ onStateToUrl,
...props
}: UseDataTableProps<TData>) {
-
- // 공통 URL QueryState 옵션
+ // ───────────────────────── 공통 QueryState 옵션 ───────────────────────
const queryStateOptions = React.useMemo<
Omit<UseQueryStateOptions<string>, "parse">
- >(() => {
- return {
+ >(
+ () => ({
history,
scroll,
shallow,
@@ -121,75 +130,120 @@ export function useDataTable<TData>({
debounceMs,
clearOnDefault,
startTransition,
- }
- }, [
- history,
- scroll,
- shallow,
- throttleMs,
- debounceMs,
- clearOnDefault,
- startTransition,
- ])
+ }),
+ [history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition],
+ );
- // -------- 기존 상태들 --------
+ // ───────────────────────── 로컬 상태들 ────────────────────────────────
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(
- initialState?.rowSelection ?? {}
- )
+ initialState?.rowSelection ?? {},
+ );
const [columnVisibility, setColumnVisibility] =
- React.useState<VisibilityState>(initialState?.columnVisibility ?? {})
+ React.useState<VisibilityState>(initialState?.columnVisibility ?? {});
+ const [expanded, setExpanded] = React.useState<ExpandedState>({});
- // -------- 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<TData>()
.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<ExpandedState>({})
+ // ───────────────────────── 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<PaginationState>) {
+ 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<SortingState>) {
+ 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<TData>);
+ }
+ }
// -------- 무한 스크롤 SWR 설정 --------
const parsedFilters = React.useMemo(() => {
@@ -337,56 +391,6 @@ export function useDataTable<TData>({
// 최종 데이터 결정
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<PaginationState>) {
- 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<SortingState>) {
- const next =
- typeof updater === "function" ? updater(sorting) : updater
- if (!isEqual(next, sorting)) {
- resetInfiniteScroll()
- void setSorting(next as ExtendedSortingState<TData>)
- }
- }
-
-
function onGroupingChange(updaterOrValue: Updater<string[]>) {
if (typeof updaterOrValue === "function") {
const newGrouping = updaterOrValue(grouping)
@@ -400,11 +404,7 @@ export function useDataTable<TData>({
setExpanded((old) => (typeof updater === "function" ? updater(old) : updater))
}
- const pagination: PaginationState = {
- pageIndex: page - 1,
- pageSize: perPage,
- }
-
+
// 기존 필터 로직들... (동일)
const filterParsers = React.useMemo(() => {