diff --git a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx similarity index 65% rename from frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx rename to frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx index 703e209c..b4cab603 100644 --- a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx +++ b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { SvgIcon } from '@/components/Common'; import { useTimeout } from '@/hooks/common'; -import { useToastActionContext } from '@/hooks/context'; import { useRecipeFavoriteMutation } from '@/hooks/queries/recipe'; interface RecipeFavoriteProps { @@ -13,23 +12,28 @@ interface RecipeFavoriteProps { recipeId: number; } -const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => { - const [isFavorite, setIsFavorite] = useState(favorite); - const [currentFavoriteCount, setCurrentFavoriteCount] = useState(favoriteCount); - const { toast } = useToastActionContext(); +const RecipeFavoriteButton = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => { + const initialFavoriteState = { + isFavorite: favorite, + currentFavoriteCount: favoriteCount, + }; + + const [favoriteInfo, setFavoriteInfo] = useState(initialFavoriteState); const { mutate } = useRecipeFavoriteMutation(Number(recipeId)); + const { isFavorite, currentFavoriteCount } = favoriteInfo; const handleToggleFavorite = async () => { + setFavoriteInfo((prev) => ({ + isFavorite: !prev.isFavorite, + currentFavoriteCount: isFavorite ? prev.currentFavoriteCount - 1 : prev.currentFavoriteCount + 1, + })); + mutate( { favorite: !isFavorite }, { - onSuccess: () => { - setIsFavorite((prev) => !prev); - setCurrentFavoriteCount((prev) => (isFavorite ? prev - 1 : prev + 1)); - }, onError: () => { - toast.error('꿀조합 좋아요를 다시 시도해주세요.'); + setFavoriteInfo(initialFavoriteState); }, } ); @@ -51,7 +55,7 @@ const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoritePro ); }; -export default RecipeFavorite; +export default RecipeFavoriteButton; const FavoriteButton = styled(Button)` display: flex; diff --git a/frontend/src/components/Recipe/index.ts b/frontend/src/components/Recipe/index.ts index b7976120..ff9566ec 100644 --- a/frontend/src/components/Recipe/index.ts +++ b/frontend/src/components/Recipe/index.ts @@ -4,7 +4,7 @@ export { default as RecipeUsedProducts } from './RecipeUsedProducts/RecipeUsedPr export { default as RecipeItem } from './RecipeItem/RecipeItem'; export { default as RecipeList } from './RecipeList/RecipeList'; export { default as RecipeRegisterForm } from './RecipeRegisterForm/RecipeRegisterForm'; -export { default as RecipeFavorite } from './RecipeFavorite/RecipeFavorite'; +export { default as RecipeFavoriteButton } from './RecipeFavoriteButton/RecipeFavoriteButton'; export { default as CommentItem } from './CommentItem/CommentItem'; export { default as CommentForm } from './CommentForm/CommentForm'; export { default as CommentList } from './CommentList/CommentList'; diff --git a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts index c97b392a..00b0255e 100644 --- a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts +++ b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { recipeApi } from '@/apis'; +import { useToastActionContext } from '@/hooks/context'; import type { RecipeFavoriteRequestBody } from '@/types/recipe'; const headers = { 'Content-Type': 'application/json' }; @@ -11,10 +12,30 @@ const patchRecipeFavorite = (recipeId: number, body: RecipeFavoriteRequestBody) const useRecipeFavoriteMutation = (recipeId: number) => { const queryClient = useQueryClient(); + const { toast } = useToastActionContext(); + + const queryKey = ['recipeDetail', recipeId]; return useMutation({ mutationFn: (body: RecipeFavoriteRequestBody) => patchRecipeFavorite(recipeId, body), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeDetail', recipeId] }), + onMutate: async (newFavoriteRequest) => { + await queryClient.cancelQueries({ queryKey: queryKey }); + + const previousRequest = queryClient.getQueryData(queryKey); + queryClient.setQueryData(queryKey, newFavoriteRequest); + + return { previousRequest }; + }, + onError: (error, _, context) => { + queryClient.setQueryData(queryKey, context?.previousRequest); + if (error instanceof Error) { + toast.error(error.message); + return; + } + + toast.error('꿀조합 좋아요를 다시 시도해주세요.'); + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: queryKey }), }); }; diff --git a/frontend/src/pages/RecipeDetailPage.tsx b/frontend/src/pages/RecipeDetailPage.tsx index 1f98f5d9..4b4fbb66 100644 --- a/frontend/src/pages/RecipeDetailPage.tsx +++ b/frontend/src/pages/RecipeDetailPage.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import RecipePreviewImage from '@/assets/plate.svg'; import { ErrorBoundary, ErrorComponent, Loading, SectionTitle } from '@/components/Common'; -import { CommentForm, CommentList, RecipeFavorite } from '@/components/Recipe'; +import { CommentForm, CommentList, RecipeFavoriteButton } from '@/components/Recipe'; import { useRecipeDetailQuery } from '@/hooks/queries/recipe'; import { getFormattedDate } from '@/utils/date'; @@ -46,7 +46,7 @@ export const RecipeDetailPage = () => { {getFormattedDate(createdAt)} - +