summaryrefslogtreecommitdiff
path: root/hooks/use-data-table.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
commit02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch)
treee932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /hooks/use-data-table.ts
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'hooks/use-data-table.ts')
-rw-r--r--hooks/use-data-table.ts358
1 files changed, 206 insertions, 152 deletions
diff --git a/hooks/use-data-table.ts b/hooks/use-data-table.ts
index 1cbae9de..5cbf949f 100644
--- a/hooks/use-data-table.ts
+++ b/hooks/use-data-table.ts
@@ -1,4 +1,3 @@
-// ====================== hooks/use-data-table.ts ======================
"use client";
import * as React from "react";
@@ -93,9 +92,8 @@ export interface UseDataTableProps<TData>
grouping?: string[];
expanded?: Record<string, boolean>;
};
- data?: TData[]; // 페이지네이션 모드 초기 데이터
+ data?: TData[];
infiniteScrollConfig?: InfiniteScrollConfig;
- /** Table state가 변경되어 URL을 동기화할 때 호출됩니다. 이미 같은 문자열이면 호출되지 않습니다. */
onStateToUrl?: (queryString: string) => void;
}
@@ -119,52 +117,70 @@ export function useDataTable<TData>({
onStateToUrl,
...props
}: UseDataTableProps<TData>) {
- // ───────────────────────── 공통 QueryState 옵션 ───────────────────────
+
+ // ✅ 수정 1: queryStateOptions를 안정적으로 메모이제이션
const queryStateOptions = React.useMemo<
Omit<UseQueryStateOptions<string>, "parse">
- >(
- () => ({
- history,
- scroll,
- shallow,
- throttleMs,
- debounceMs,
- clearOnDefault,
- startTransition,
- }),
- [history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition],
- );
-
- // ───────────────────────── 로컬 상태들 ────────────────────────────────
+ >(() => ({
+ history,
+ scroll,
+ shallow,
+ throttleMs,
+ debounceMs,
+ clearOnDefault,
+ startTransition,
+ }), [
+ history,
+ scroll,
+ shallow,
+ throttleMs,
+ debounceMs,
+ clearOnDefault,
+ // startTransition 제거 - 함수 참조가 불안정할 수 있음
+ ]);
+
+ // ✅ 수정 2: 초기 상태들을 안정적으로 처리
+ const stableInitialState = React.useMemo(() => initialState || {}, [initialState]);
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(
- initialState?.rowSelection ?? {},
+ () => stableInitialState?.rowSelection ?? {}
+ );
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
+ () => stableInitialState?.columnVisibility ?? {}
);
- const [columnVisibility, setColumnVisibility] =
- React.useState<VisibilityState>(initialState?.columnVisibility ?? {});
const [expanded, setExpanded] = React.useState<ExpandedState>({});
// ───────────────────────── URL ↔️ Pagination 동기화 ───────────────────
+ const defaultPageSize = React.useMemo(() =>
+ stableInitialState?.pagination?.pageSize ?? 10,
+ [stableInitialState?.pagination?.pageSize]
+ );
+
const [page, setPage] = useQueryState(
"page",
parseAsInteger.withOptions(queryStateOptions).withDefault(1),
);
const [perPage, setPerPage] = useQueryState(
"perPage",
- parseAsInteger
- .withOptions(queryStateOptions)
- .withDefault(initialState?.pagination?.pageSize ?? 10),
+ parseAsInteger.withOptions(queryStateOptions).withDefault(defaultPageSize),
);
- const isInfiniteMode = !!(
- infiniteScrollConfig && perPage >= INFINITE_SCROLL_THRESHOLD
+ const isInfiniteMode = React.useMemo(() =>
+ !!(infiniteScrollConfig && perPage >= INFINITE_SCROLL_THRESHOLD),
+ [infiniteScrollConfig, perPage]
);
// ───────────────────────── URL ↔️ Sorting 동기화 ─────────────────────
+ const defaultSorting = React.useMemo(() =>
+ stableInitialState?.sorting ?? [],
+ [stableInitialState?.sorting]
+ );
+
const [sorting, setSorting] = useQueryState(
"sort",
getSortingStateParser<TData>()
.withOptions(queryStateOptions)
- .withDefault(initialState?.sorting ?? []),
+ .withDefault(defaultSorting),
);
// ───────────────────────── URL ↔️ 기타 파라미터 ──────────────────────
@@ -180,32 +196,38 @@ export function useDataTable<TData>({
"search",
parseAsString.withOptions(queryStateOptions).withDefault(""),
);
+
+ const defaultGrouping = React.useMemo(() =>
+ stableInitialState?.grouping ?? [],
+ [stableInitialState?.grouping]
+ );
+
const [grouping, setGrouping] = useQueryState(
"group",
parseAsArrayOf(parseAsString, ",")
.withOptions(queryStateOptions)
- .withDefault(initialState?.grouping ?? []),
+ .withDefault(defaultGrouping),
);
// ───────────────────────── Pagination helper ─────────────────────────
- const pagination: PaginationState = {
+ const pagination: PaginationState = React.useMemo(() => ({
pageIndex: page - 1,
pageSize: perPage,
- };
+ }), [page, perPage]);
- // ───────────────────────── Table → URL 직렬화 ────────────────────────
+ // ✅ 수정 3: toQueryString을 안정적으로 메모이제이션
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 (pg.pageSize !== defaultPageSize) p.set("perPage", String(pg.pageSize));
if (sort.length) p.set("sort", JSON.stringify(sort));
return p.toString();
},
- [perPage],
+ [defaultPageSize], // perPage 대신 defaultPageSize 사용
);
- // ───────────────────────── 페이징 변경 ───────────────────────────────
+ // ✅ 수정 4: 페이징 변경 핸들러 최적화
const handlePageSizeChange = React.useCallback(
(newPageSize: number) => {
void setPerPage(newPageSize);
@@ -218,11 +240,10 @@ export function useDataTable<TData>({
[perPage, setPerPage, setPage],
);
- function onPaginationChange(updater: Updater<PaginationState>) {
+ const onPaginationChange = React.useCallback((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);
@@ -230,10 +251,10 @@ export function useDataTable<TData>({
void setPage(next.pageIndex + 1);
if (next.pageSize !== perPage) handlePageSizeChange(next.pageSize);
- }
+ }, [isInfiniteMode, pagination, sorting, toQueryString, onStateToUrl, setPage, perPage, handlePageSizeChange]);
// ───────────────────────── Sorting 변경 ──────────────────────────────
- function onSortingChange(updater: Updater<SortingState>) {
+ const onSortingChange = React.useCallback((updater: Updater<SortingState>) => {
const next = typeof updater === "function" ? updater(sorting) : updater;
if (onStateToUrl) {
@@ -244,65 +265,79 @@ export function useDataTable<TData>({
if (!isEqual(next, sorting)) {
void setSorting(next as ExtendedSortingState<TData>);
}
- }
+ }, [sorting, pagination, toQueryString, onStateToUrl, setSorting]);
- // -------- 무한 스크롤 SWR 설정 --------
+ // ✅ 수정 5: SWR 관련 값들을 안정적으로 메모이제이션
const parsedFilters = React.useMemo(() => {
try {
return JSON.parse(filters)
} catch {
return []
}
- }, [filters])
+ }, [filters]);
- const sortForSWR = React.useMemo(() => {
- return sorting.map(sort => ({
+ const sortForSWR = React.useMemo(() =>
+ sorting.map(sort => ({
id: sort.id,
desc: sort.desc
- }))
- }, [sorting])
+ })),
+ [sorting]
+ );
- // 실제 페이지 크기 계산 (무한 스크롤 시)
const effectivePageSize = React.useMemo(() => {
if (!isInfiniteMode) return perPage
-
- // 무한 스크롤 모드에서는 적절한 청크 크기 사용
const maxSize = infiniteScrollConfig?.maxPageSize || 100
- return Math.min(50, maxSize) // 기본 50개씩 로드
- }, [isInfiniteMode, perPage, infiniteScrollConfig?.maxPageSize])
+ return Math.min(50, maxSize)
+ }, [isInfiniteMode, perPage, infiniteScrollConfig?.maxPageSize]);
+
+ // ✅ 수정 6: getKey 함수의 의존성 최적화
+ const swrKeyParams = React.useMemo(() => ({
+ endpoint: infiniteScrollConfig?.apiEndpoint,
+ pageSize: effectivePageSize,
+ search: search || undefined,
+ filters: parsedFilters.length ? JSON.stringify(parsedFilters) : undefined,
+ joinOperator: joinOperator !== "and" ? joinOperator : undefined,
+ sort: sortForSWR.length ? JSON.stringify(sortForSWR) : undefined,
+ }), [
+ infiniteScrollConfig?.apiEndpoint,
+ effectivePageSize,
+ search,
+ parsedFilters.length,
+ parsedFilters,
+ joinOperator,
+ sortForSWR.length,
+ sortForSWR
+ ]);
- // SWR 키 생성 함수 - 안정화를 위해 useCallback 사용
const getKey = React.useCallback(
(pageIndex: number, previousPageData: InfiniteScrollResponse<TData> | null) => {
- if (!isInfiniteMode || !infiniteScrollConfig) return null
+ if (!isInfiniteMode || !swrKeyParams.endpoint) return null
const params = new URLSearchParams()
if (pageIndex === 0) {
- // 첫 페이지
- params.set("limit", String(effectivePageSize))
- if (search) params.set("search", search)
- if (parsedFilters.length) params.set("filters", JSON.stringify(parsedFilters))
- if (joinOperator !== "and") params.set("joinOperator", joinOperator)
- if (sortForSWR.length) params.set("sort", JSON.stringify(sortForSWR))
+ params.set("limit", String(swrKeyParams.pageSize))
+ if (swrKeyParams.search) params.set("search", swrKeyParams.search)
+ if (swrKeyParams.filters) params.set("filters", swrKeyParams.filters)
+ if (swrKeyParams.joinOperator) params.set("joinOperator", swrKeyParams.joinOperator)
+ if (swrKeyParams.sort) params.set("sort", swrKeyParams.sort)
- return `${infiniteScrollConfig.apiEndpoint}?${params.toString()}`
+ return `${swrKeyParams.endpoint}?${params.toString()}`
}
- // 다음 페이지
if (!previousPageData || !previousPageData.hasNextPage) return null
params.set("cursor", previousPageData.nextCursor || "")
- params.set("limit", String(effectivePageSize))
- if (search) params.set("search", search)
- if (parsedFilters.length) params.set("filters", JSON.stringify(parsedFilters))
- if (joinOperator !== "and") params.set("joinOperator", joinOperator)
- if (sortForSWR.length) params.set("sort", JSON.stringify(sortForSWR))
+ params.set("limit", String(swrKeyParams.pageSize))
+ if (swrKeyParams.search) params.set("search", swrKeyParams.search)
+ if (swrKeyParams.filters) params.set("filters", swrKeyParams.filters)
+ if (swrKeyParams.joinOperator) params.set("joinOperator", swrKeyParams.joinOperator)
+ if (swrKeyParams.sort) params.set("sort", swrKeyParams.sort)
- return `${infiniteScrollConfig.apiEndpoint}?${params.toString()}`
+ return `${swrKeyParams.endpoint}?${params.toString()}`
},
- [isInfiniteMode, infiniteScrollConfig, effectivePageSize, search, parsedFilters, joinOperator, sortForSWR]
- )
+ [isInfiniteMode, swrKeyParams]
+ );
// SWR Infinite 사용
const {
@@ -325,15 +360,14 @@ export function useDataTable<TData>({
revalidateOnFocus: false,
revalidateOnReconnect: true,
}
- )
+ );
- // 무한 스크롤 데이터 병합
+ // ✅ 수정 7: 무한 스크롤 데이터와 액션들을 안정적으로 메모이제이션
const infiniteData = React.useMemo(() => {
if (!isInfiniteMode || !swrData) return []
return swrData.flatMap(page => page.data)
- }, [swrData, isInfiniteMode])
+ }, [swrData, isInfiniteMode]);
- // 무한 스크롤 메타 정보 - 안정화를 위해 useCallback 사용
const infiniteScrollActions = React.useMemo(() => ({
loadMore: () => {
if (swrData && swrData[swrData.length - 1]?.hasNextPage && !swrIsValidating) {
@@ -345,7 +379,7 @@ export function useDataTable<TData>({
swrMutate()
},
refresh: () => swrMutate(),
- }), [swrData, swrIsValidating, swrSetSize, swrMutate])
+ }), [swrData, swrIsValidating, swrSetSize, swrMutate]);
const infiniteMeta = React.useMemo(() => {
if (!isInfiniteMode || !infiniteScrollConfig) return null
@@ -375,39 +409,45 @@ export function useDataTable<TData>({
swrError,
swrIsLoading,
infiniteScrollActions
- ])
+ ]);
- // 검색어나 필터 변경 시 무한 스크롤 리셋 - infiniteMeta 의존성 제거
- const resetInfiniteScroll = React.useCallback(() => {
+ // ✅ 수정 8: 리셋 함수 최적화
+ const resetInfiniteScrollRef = React.useRef<() => void>()
+ resetInfiniteScrollRef.current = () => {
if (isInfiniteMode && infiniteScrollActions) {
infiniteScrollActions.reset()
}
- }, [isInfiniteMode, infiniteScrollActions])
+ }
+
+ const resetInfiniteScroll = React.useCallback(() => {
+ resetInfiniteScrollRef.current?.()
+ }, [])
- // 필터 변경 시 리셋 - useEffect dependency 최소화
+ // 필터 변경 시 리셋
React.useEffect(() => {
resetInfiniteScroll()
- }, [search, filters, joinOperator])
+ }, [search, filters, joinOperator, resetInfiniteScroll]);
// 최종 데이터 결정
- const finalData = isInfiniteMode ? infiniteData : initialData
+ const finalData = React.useMemo(() =>
+ isInfiniteMode ? infiniteData : initialData,
+ [isInfiniteMode, infiniteData, initialData]
+ );
- function onGroupingChange(updaterOrValue: Updater<string[]>) {
+ const onGroupingChange = React.useCallback((updaterOrValue: Updater<string[]>) => {
if (typeof updaterOrValue === "function") {
const newGrouping = updaterOrValue(grouping)
void setGrouping(newGrouping)
} else {
void setGrouping(updaterOrValue)
}
- }
+ }, [grouping, setGrouping]);
- function onExpandedChange(updater: Updater<ExpandedState>) {
+ const onExpandedChange = React.useCallback((updater: Updater<ExpandedState>) => {
setExpanded((old) => (typeof updater === "function" ? updater(old) : updater))
- }
+ }, []);
-
-
- // 기존 필터 로직들... (동일)
+ // ✅ 수정 9: filterParsers 안정화
const filterParsers = React.useMemo(() => {
return filterFields.reduce<
Record<string, Parser<string> | Parser<string[]>>
@@ -421,7 +461,7 @@ export function useDataTable<TData>({
}
return acc
}, {})
- }, [filterFields, queryStateOptions])
+ }, [filterFields, queryStateOptions]);
const [filterValues, setFilterValues] = useQueryStates(filterParsers)
const debouncedSetFilterValues = useDebouncedCallback(
@@ -458,62 +498,70 @@ export function useDataTable<TData>({
}
}, [filterFields, enableAdvancedFilter])
-// -------- column-filters 변경 핸들러 (루프 차단 버전) --------
-const onColumnFiltersChange = React.useCallback(
- (updater: Updater<ColumnFiltersState>) => {
- setColumnFilters(prev => {
- const next =
- typeof updater === "function" ? updater(prev) : updater
-
- /* 변동이 없으면 바로 종료 */
- if (deepEqual(prev, next)) return prev
-
- /* ---------- URL 동기화: 고급필터 OFF 때만 ---------- */
- if (!enableAdvancedFilter) {
- const updates: Record<string, string | string[] | null> = {}
-
- next.forEach(f => {
- if (searchableColumns.some(c => c.id === f.id))
- updates[f.id] = f.value as string
- else if (filterableColumns.some(c => c.id === f.id))
- updates[f.id] = f.value as string[]
- })
- prev.forEach(pf => {
- if (!next.some(nf => nf.id === pf.id)) updates[pf.id] = null
- })
-
- void setPage(1)
- debouncedSetFilterValues(updates)
- }
+ // ✅ 수정 10: onColumnFiltersChange 최적화 (deepEqual 의존성 제거)
+ const onColumnFiltersChange = React.useCallback(
+ (updater: Updater<ColumnFiltersState>) => {
+ setColumnFilters(prev => {
+ const next = typeof updater === "function" ? updater(prev) : updater
+
+ // deepEqual을 직접 호출 (의존성에서 제거)
+ if (deepEqual(prev, next)) return prev
+
+ if (!enableAdvancedFilter) {
+ const updates: Record<string, string | string[] | null> = {}
+
+ next.forEach(f => {
+ if (searchableColumns.some(c => c.id === f.id))
+ updates[f.id] = f.value as string
+ else if (filterableColumns.some(c => c.id === f.id))
+ updates[f.id] = f.value as string[]
+ })
+ prev.forEach(pf => {
+ if (!next.some(nf => nf.id === pf.id)) updates[pf.id] = null
+ })
+
+ void setPage(1)
+ debouncedSetFilterValues(updates)
+ }
- return next // ★ 항상 state 를 반영
- })
- },
- [
+ return next
+ })
+ },
+ [
+ enableAdvancedFilter,
+ searchableColumns,
+ filterableColumns,
+ debouncedSetFilterValues,
+ setPage,
+ ]
+ )
+
+ // ✅ 수정 11: 테이블 설정을 안정적으로 메모이제이션
+ const tableState = React.useMemo(() => ({
+ pagination,
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters: enableAdvancedFilter ? [] : columnFilters,
+ grouping,
+ expanded,
+ }), [
+ pagination,
+ sorting,
+ columnVisibility,
+ rowSelection,
enableAdvancedFilter,
- searchableColumns,
- filterableColumns,
- deepEqual, // fast-deep-equal
- debouncedSetFilterValues,
- setPage,
- ]
-)
-
- // -------- TanStack Table 인스턴스 생성 --------
+ columnFilters,
+ grouping,
+ expanded,
+ ])
+
const table = useReactTable({
...props,
data: finalData,
- initialState,
+ initialState: stableInitialState,
pageCount: isInfiniteMode ? -1 : pageCount,
- state: {
- pagination,
- sorting,
- columnVisibility,
- rowSelection,
- columnFilters: enableAdvancedFilter ? [] : columnFilters,
- grouping,
- expanded,
- },
+ state: tableState,
onRowSelectionChange: setRowSelection,
onPaginationChange,
@@ -538,23 +586,29 @@ const onColumnFiltersChange = React.useCallback(
manualFiltering: true,
})
- return {
+ // ✅ 수정 12: 반환값 안정화
+ const urlState = React.useMemo(() => ({
+ search,
+ setSearch,
+ filters: parsedFilters,
+ setFilters: (newFilters: any[]) => setFilters(JSON.stringify(newFilters)),
+ joinOperator,
+ setJoinOperator,
+ }), [search, setSearch, parsedFilters, setFilters, joinOperator, setJoinOperator])
+
+ return React.useMemo(() => ({
table,
- // 무한 스크롤 정보
infiniteScroll: infiniteMeta,
- // 모드 정보
isInfiniteMode,
effectivePageSize,
- // 페이지 크기 변경 핸들러
handlePageSizeChange,
- // URL 상태 관리 함수들
- urlState: {
- search,
- setSearch,
- filters: parsedFilters,
- setFilters: (newFilters: any[]) => setFilters(JSON.stringify(newFilters)),
- joinOperator,
- setJoinOperator,
- }
- }
+ urlState,
+ }), [
+ table,
+ infiniteMeta,
+ isInfiniteMode,
+ effectivePageSize,
+ handlePageSizeChange,
+ urlState,
+ ])
} \ No newline at end of file