diff --git a/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx b/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx index d27ed0e72..7c55edea7 100644 --- a/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx +++ b/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx @@ -3,7 +3,7 @@ import { container } from './recipeFavoriteButton.css'; import { SvgIcon, Text } from '@/components/Common'; import { useTimeout } from '@/hooks/common'; import { useMemberQuery } from '@/hooks/queries/members'; -import { useRecipeFavoriteMutation } from '@/hooks/queries/recipe'; +import { useRecipeBookmarkMutation, useRecipeFavoriteMutation } from '@/hooks/queries/recipe'; interface RecipeFavoriteProps { recipeId: number; @@ -12,11 +12,13 @@ interface RecipeFavoriteProps { } const RecipeFavoriteButton = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => { - const { mutate } = useRecipeFavoriteMutation(Number(recipeId)); + const { mutate: favoriteMutate } = useRecipeFavoriteMutation(Number(recipeId)); + const { mutate: bookmarkMutate } = useRecipeBookmarkMutation(Number(recipeId)); const { data: member } = useMemberQuery(); const handleToggleFavorite = async () => { - mutate({ favorite: !favorite }); + favoriteMutate({ favorite: !favorite }); + bookmarkMutate({ bookmark: !favorite }); }; const [debouncedToggleFavorite] = useTimeout(handleToggleFavorite, 200); diff --git a/src/hooks/queries/members/useInfiniteMemberRecipeBookmarkQuery.ts b/src/hooks/queries/members/useInfiniteMemberRecipeBookmarkQuery.ts index bfbf9b929..349eb6ffa 100644 --- a/src/hooks/queries/members/useInfiniteMemberRecipeBookmarkQuery.ts +++ b/src/hooks/queries/members/useInfiniteMemberRecipeBookmarkQuery.ts @@ -5,8 +5,7 @@ import type { MemberRecipeResponse } from '@/types/response'; const fetchMemberRecipeBookmark = async (pageParam: number) => { const response = await memberApi.get({ - // 임시로 적은거라 params 확정되면 수정할 것 (json도 수정해야 함) - params: '/bookmarkRecipes', + params: '/recipes/bookmark', queries: `?page=${pageParam}`, credentials: true, }); @@ -16,7 +15,7 @@ const fetchMemberRecipeBookmark = async (pageParam: number) => { const useInfiniteMemberRecipeBookmarkQuery = () => { return useSuspendedInfiniteQuery( - ['member', 'bookmarkRecipes'], + ['member', 'bookmark'], ({ pageParam = 0 }) => fetchMemberRecipeBookmark(pageParam), { getNextPageParam: (prevResponse: MemberRecipeResponse) => { diff --git a/src/hooks/queries/recipe/index.ts b/src/hooks/queries/recipe/index.ts index 0cd5db9f6..d508a16b1 100644 --- a/src/hooks/queries/recipe/index.ts +++ b/src/hooks/queries/recipe/index.ts @@ -4,3 +4,4 @@ export { default as useRecipeFavoriteMutation } from './useRecipeFavoriteMutatio export { default as useInfiniteRecipesQuery } from './useInfiniteRecipesQuery'; export { default as useInfiniteRecipeCommentQuery } from './useInfiniteRecipeCommentQuery'; export { default as useRecipeCommentMutation } from './useRecipeCommentMutation'; +export { default as useRecipeBookmarkMutation } from './useRecipeBookmarkMutation'; diff --git a/src/hooks/queries/recipe/useRecipeBookmarkMutation.ts b/src/hooks/queries/recipe/useRecipeBookmarkMutation.ts new file mode 100644 index 000000000..e6ca486ab --- /dev/null +++ b/src/hooks/queries/recipe/useRecipeBookmarkMutation.ts @@ -0,0 +1,46 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { recipeApi } from '@/apis'; +import type { RecipeBookmarkRequestBody, RecipeDetail } from '@/types/recipe'; + +const headers = { 'Content-Type': 'application/json' }; + +const patchRecipeBookmark = (recipeId: number, body: RecipeBookmarkRequestBody) => { + return recipeApi.patch({ params: `/${recipeId}/bookmark`, credentials: true }, headers, body); +}; + +const useRecipeBookmarkMutation = (recipeId: number) => { + const queryClient = useQueryClient(); + + const queryKey = ['recipeDetail', recipeId]; + + return useMutation({ + mutationFn: (body: RecipeBookmarkRequestBody) => patchRecipeBookmark(recipeId, body), + onMutate: async (newBookmarkRequest) => { + await queryClient.cancelQueries({ queryKey: queryKey }); + + const previousRequest = queryClient.getQueryData(queryKey); + + if (previousRequest) { + queryClient.setQueryData(queryKey, () => ({ + ...previousRequest, + bookmark: newBookmarkRequest.bookmark, + })); + } + + return { previousRequest }; + }, + onError: (error, _, context) => { + queryClient.setQueryData(queryKey, context?.previousRequest); + + if (error instanceof Error) { + throw new Error(error.message); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKey }); + }, + }); +}; + +export default useRecipeBookmarkMutation; diff --git a/src/mocks/handlers/memberHandlers.ts b/src/mocks/handlers/memberHandlers.ts index ffe0eb901..fae7513ae 100644 --- a/src/mocks/handlers/memberHandlers.ts +++ b/src/mocks/handlers/memberHandlers.ts @@ -22,7 +22,7 @@ export const memberHandlers = [ return res(ctx.status(200), ctx.json(mockMemberRecipes)); }), - rest.get('/api/members/bookmarkRecipes', (req, res, ctx) => { + rest.get('/api/members/recipes/bookmark', (req, res, ctx) => { return res(ctx.status(200), ctx.json(mockMemberRecipeBookmark)); }), diff --git a/src/mocks/handlers/recipeHandlers.ts b/src/mocks/handlers/recipeHandlers.ts index fff47a879..14019cefc 100644 --- a/src/mocks/handlers/recipeHandlers.ts +++ b/src/mocks/handlers/recipeHandlers.ts @@ -30,6 +30,10 @@ export const recipeHandlers = [ return res(ctx.status(200)); }), + rest.patch('/api/recipes/:recipeId/bookmark', (_, res, ctx) => { + return res(ctx.status(200)); + }), + rest.get('/api/recipes', (req, res, ctx) => { const sortOptions = req.url.searchParams.get('sort'); const page = Number(req.url.searchParams.get('page')); diff --git a/src/types/recipe.ts b/src/types/recipe.ts index d412e8d69..77912a964 100644 --- a/src/types/recipe.ts +++ b/src/types/recipe.ts @@ -41,3 +41,7 @@ export interface Comment { comment: string; createdAt: string; } + +export interface RecipeBookmarkRequestBody { + bookmark: boolean; +}