diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index 91d2092..d4f0869 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -1,7 +1,8 @@ import { getApi } from '@/app/api/config/appConfig'; import { useAuthStore } from '@/domains/shared/store/auth'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; import { Cocktail, Sort } from '../types/types'; +import { useEffect, useRef } from 'react'; interface CocktailResponse { data: Cocktail[]; @@ -22,6 +23,11 @@ interface CocktailFilter extends SearchFilters { sortBy?: Sort; } +interface PageParam { + lastId: number; + lastValue: number | string; +} + const fetchKeep = async (): Promise> => { const res = await fetch(`${getApi}/me/bar`, { method: 'GET', @@ -36,15 +42,15 @@ const fetchKeep = async (): Promise> => { }; const fetchRecipe = async ( - lastId: number | null, + pageParam: PageParam | null, size: number, sortBy?: Sort ): Promise => { const url = new URL(`${getApi}/cocktails`); url.searchParams.set('size', String(size)); - if (lastId !== null) { - url.searchParams.set('lastId', String(lastId)); - url.searchParams.set('lastValue', String(lastId)); + if (pageParam) { + url.searchParams.set('lastId', String(pageParam.lastId)); + url.searchParams.set('lastValue', String(pageParam.lastValue)); } if (sortBy) { @@ -95,6 +101,18 @@ const hasActiveFilters = (filters: SearchFilters): boolean => { export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { const user = useAuthStore((state) => state.user); + const queryClient = useQueryClient(); + const prevSortBy = useRef(sortBy); + + useEffect(() => { + if (prevSortBy.current !== undefined && prevSortBy.current !== sortBy) { + queryClient.removeQueries({ + queryKey: ['cocktails', 'infinite', prevSortBy.current], + }); + } + prevSortBy.current = sortBy; + }, [sortBy, queryClient]); + return useInfiniteQuery({ queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], queryFn: async ({ pageParam }) => { @@ -110,11 +128,37 @@ export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { return cocktails; }, - getNextPageParam: (lastpage) => { - if (lastpage.length < size) return undefined; - return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; + getNextPageParam: (lastPage) => { + if (lastPage.length < size) { + return undefined; + } + + const lastItem = lastPage[lastPage.length - 1]; + if (!lastItem) return undefined; + + let lastValue: number | string; + + switch (sortBy) { + case 'keeps': + lastValue = lastItem.keepCount ?? lastItem.cocktailId; + break; + case 'comments': + lastValue = lastItem.commentCount ?? lastItem.cocktailId; + break; + case 'recent': + default: + lastValue = lastItem.cocktailId; + break; + } + + return { + lastId: lastItem.cocktailId, + lastValue: lastValue, + }; }, - initialPageParam: null as number | null, + initialPageParam: null as PageParam | null, + refetchOnMount: false, + refetchOnWindowFocus: false, }); }; @@ -161,13 +205,17 @@ export const useCocktails = ( } const allCocktails = infiniteQuery.data?.pages.flatMap((page) => page) ?? []; + const uniqueCocktails = allCocktails.filter( + (cocktail, index, self) => index === self.findIndex((c) => c.cocktailId === cocktail.cocktailId) + ); + const hasDuplicates = allCocktails.length !== uniqueCocktails.length; return { - data: allCocktails, + data: uniqueCocktails, noResults: false, isSearchMode: false, fetchNextPage: infiniteQuery.fetchNextPage, - hasNextPage: infiniteQuery.hasNextPage, + hasNextPage: hasDuplicates ? false : infiniteQuery.hasNextPage, isFetchingNextPage: infiniteQuery.isFetchingNextPage, }; }; diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index ce74336..550c2cf 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -4,6 +4,10 @@ import { useAuthStore } from '@/domains/shared/store/auth'; import { useToast } from '@/shared/hook/useToast'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +interface Comment { + userNickName: string; +} + export const postRecipeComment = async (cocktailId: number, content: string) => { const body = { cocktailId, @@ -18,6 +22,8 @@ export const postRecipeComment = async (cocktailId: number, content: string) => body: JSON.stringify(body), }); + if (res.status === 401) throw new Error('unauth'); + const text = await res.text(); const data = JSON.parse(text); return data; @@ -73,20 +79,18 @@ export function useRecipeComment({ cocktailId }: { cocktailId: number }) { staleTime: 30_000, }); + const hasComment = comments.some((c: Comment) => c.userNickName === user?.nickname); const createMut = useMutation({ mutationFn: (content: string) => { if (!user?.id) { toastInfo('로그인 후 이용 가능합니다.'); - return Promise.reject(new Error('unauth')); + return Promise.resolve(null); + } else if (hasComment) { + toastInfo('댓글은 한 개만 작성 가능합니다.'); } return postRecipeComment(cocktailId, content); }, onSuccess: () => refetch(), - onError: (e) => { - if (e.message !== 'unauth') { - toastInfo('댓글은 한개만 작성 가능합니다'); - } - }, }); const updateMut = useMutation({ diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 209b94a..2d8c323 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,5 +1,4 @@ import SelectBox from '@/shared/components/select-box/SelectBox'; -import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; interface Props { @@ -12,14 +11,12 @@ function CocktailFilter({ cocktailsEA }: Props) { 인기순: 'keeps', 댓글순: 'comments', }; - const queryClient = useQueryClient(); + const router = useRouter(); - const handleChange = async (selectTitle: string) => { + + const handleChange = (selectTitle: string) => { const sortValue = sortMap[selectTitle as keyof typeof sortMap]; - queryClient.removeQueries({ - queryKey: ['cocktails', 'infinite'], - exact: false, - }); + router.push(`?sortBy=${sortValue}`); }; diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 1a29861..f3d22ee 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -13,10 +13,11 @@ import { Sort } from '../../types/types'; function Cocktails() { const searchParams = useSearchParams(); - const sortBy = searchParams.get('sortBy') as Sort; + const sortByParam = searchParams.get('sortBy') || 'recent'; const [keyword, setKeyword] = useState(''); const [input, setInput] = useState(''); + const [sortBy, setSortBy] = useState(sortByParam as Sort); const [alcoholStrengths, setAlcoholStrengths] = useState([]); const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]); const [cocktailTypes, setCocktailTypes] = useState([]); @@ -42,6 +43,10 @@ function Cocktails() { } }, [inView, hasNextPage, fetchNextPage]); + useEffect(() => { + setSortBy(sortByParam as Sort); + }, [sortByParam]); + const debounceKeyword = useMemo(() => debounce((v: string) => setKeyword(v), 300), []); const handleSearch = (v: string) => { setInput(v); diff --git a/src/domains/recipe/types/types.ts b/src/domains/recipe/types/types.ts index 0818ef9..22e0354 100644 --- a/src/domains/recipe/types/types.ts +++ b/src/domains/recipe/types/types.ts @@ -5,6 +5,8 @@ export interface Cocktail { cocktailImgUrl: string; cocktailNameKo: string; isKeep: boolean; + keepCount?: number; + commentCount?: number; } export interface RecommendCocktail {