카테고리 무한 스크롤 구현
This commit is contained in:
@@ -46,8 +46,8 @@ export function useUpdateCategory() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.categories.detail(variables.categoryCode),
|
||||
});
|
||||
// 모든 카테고리 목록 쿼리 무효화
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.categories.lists() });
|
||||
// 모든 카테고리 관련 쿼리 무효화 (일반 목록 + 무한 스크롤)
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.categories.all });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("카테고리 수정 실패:", error);
|
||||
|
||||
42
frontend/hooks/queries/useCategoriesInfinite.ts
Normal file
42
frontend/hooks/queries/useCategoriesInfinite.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
|
||||
import type { CategoryFilter } from "@/lib/schemas/commonCode";
|
||||
import type { CodeCategory } from "@/types/commonCode";
|
||||
|
||||
/**
|
||||
* 카테고리 무한 스크롤 훅
|
||||
*/
|
||||
export function useCategoriesInfinite(filters?: CategoryFilter) {
|
||||
return useInfiniteScroll<CodeCategory, CategoryFilter>({
|
||||
queryKey: queryKeys.categories.infinite(filters),
|
||||
queryFn: async ({ pageParam, ...params }) => {
|
||||
// API 호출 시 페이지 정보 포함
|
||||
const expectedSize = pageParam === 1 ? 20 : 10; // 첫 페이지는 20개, 이후는 10개씩
|
||||
const response = await commonCodeApi.categories.getList({
|
||||
...params,
|
||||
page: pageParam,
|
||||
size: expectedSize,
|
||||
});
|
||||
|
||||
return {
|
||||
data: response.data || [],
|
||||
total: response.total,
|
||||
hasMore: (response.data?.length || 0) >= expectedSize, // 예상 크기와 같거나 크면 더 있을 수 있음
|
||||
};
|
||||
},
|
||||
initialPageParam: 1,
|
||||
pageSize: 20, // 첫 페이지 기준
|
||||
params: filters,
|
||||
staleTime: 5 * 60 * 1000, // 5분 캐싱
|
||||
// 커스텀 getNextPageParam 제공
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||
// 마지막 페이지의 데이터 개수가 요청한 크기보다 작으면 더 이상 페이지 없음
|
||||
const expectedSize = lastPageParam === 1 ? 20 : 10;
|
||||
if ((lastPage.data?.length || 0) < expectedSize) {
|
||||
return undefined;
|
||||
}
|
||||
return lastPageParam + 1;
|
||||
},
|
||||
});
|
||||
}
|
||||
107
frontend/hooks/useInfiniteScroll.ts
Normal file
107
frontend/hooks/useInfiniteScroll.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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(() => {
|
||||
return infiniteQuery.data?.pages.flatMap((page) => page.data) || [];
|
||||
}, [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>>;
|
||||
Reference in New Issue
Block a user