import { useInfiniteQuery } from "@tanstack/react-query"; import { useMemo, useCallback } from "react"; export interface InfiniteScrollConfig> { queryKey: any[]; queryFn: (params: { pageParam: number } & TParams) => Promise<{ data: TData[]; total?: number; hasMore?: boolean; }>; initialPageParam?: number; getNextPageParam?: (lastPage: any, allPages: any[], lastPageParam: number) => number | undefined; pageSize?: number; enabled?: boolean; staleTime?: number; params?: TParams; } export function useInfiniteScroll>({ queryKey, queryFn, initialPageParam = 1, getNextPageParam, pageSize = 20, enabled = true, staleTime = 5 * 60 * 1000, // 5분 params = {} as TParams, }: InfiniteScrollConfig) { // React Query의 useInfiniteQuery 사용 const infiniteQuery = useInfiniteQuery({ queryKey: [...queryKey, params], queryFn: ({ pageParam }) => queryFn({ pageParam, ...params }), initialPageParam, getNextPageParam: getNextPageParam || ((lastPage, allPages, lastPageParam) => { // 기본 페이지네이션 로직 if (lastPage.data.length < pageSize) { return undefined; // 더 이상 페이지 없음 } return lastPageParam + 1; }), enabled, staleTime, }); // 모든 페이지의 데이터를 평탄화 const flatData = useMemo(() => { if (!infiniteQuery.data?.pages) return []; const allData = infiniteQuery.data.pages.flatMap((page) => page.data); // 중복 제거 - code_value 또는 category_code를 기준으로 const uniqueData = allData.filter((item, index, self) => { const key = (item as any).code_value || (item as any).category_code; if (!key) return true; // key가 없으면 그대로 유지 return ( index === self.findIndex((t) => { const tKey = (t as any).code_value || (t as any).category_code; return tKey === key; }) ); }); return uniqueData; }, [infiniteQuery.data]); // 총 개수 계산 (첫 번째 페이지의 total 사용) const totalCount = useMemo(() => { return infiniteQuery.data?.pages[0]?.total || 0; }, [infiniteQuery.data]); // 다음 페이지 로드 함수 const loadMore = useCallback(() => { if (infiniteQuery.hasNextPage && !infiniteQuery.isFetchingNextPage) { infiniteQuery.fetchNextPage(); } }, [infiniteQuery]); // 스크롤 이벤트 핸들러 const handleScroll = useCallback( (e: React.UIEvent) => { const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; // 스크롤이 하단에서 100px 이내에 도달하면 다음 페이지 로드 if (scrollHeight - scrollTop <= clientHeight + 100) { loadMore(); } }, [loadMore], ); // 무한 스크롤 상태 정보 const infiniteScrollState = { // 데이터 data: flatData, totalCount, // 로딩 상태 isLoading: infiniteQuery.isLoading, isFetchingNextPage: infiniteQuery.isFetchingNextPage, hasNextPage: infiniteQuery.hasNextPage, // 에러 상태 error: infiniteQuery.error, isError: infiniteQuery.isError, // 기타 상태 isSuccess: infiniteQuery.isSuccess, isFetching: infiniteQuery.isFetching, }; return { ...infiniteScrollState, loadMore, handleScroll, refetch: infiniteQuery.refetch, invalidate: infiniteQuery.refetch, }; } // 편의를 위한 타입 정의 export type InfiniteScrollReturn = ReturnType>;