126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
import { useMemo, useCallback } from "react";
|
|
|
|
export interface InfiniteScrollConfig<TData, TParams = Record<string, any>> {
|
|
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<TData, TParams = Record<string, any>>({
|
|
queryKey,
|
|
queryFn,
|
|
initialPageParam = 1,
|
|
getNextPageParam,
|
|
pageSize = 20,
|
|
enabled = true,
|
|
staleTime = 5 * 60 * 1000, // 5분
|
|
params = {} as TParams,
|
|
}: InfiniteScrollConfig<TData, TParams>) {
|
|
// 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<HTMLDivElement>) => {
|
|
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<TData> = ReturnType<typeof useInfiniteScroll<TData>>;
|