From 2a3320d2e962991cc3cbead36fbde6004ad3cb9a Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 30 Apr 2026 17:13:16 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor(web):=20react-hot-toast=20?= =?UTF-8?q?=EC=A0=84=EB=A9=B4=20=EC=A0=84=ED=99=98=20=EB=B0=8F=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + apps/web/src/apis/Auth/deleteAccount.ts | 4 - apps/web/src/apis/Auth/postAppleAuth.ts | 3 +- apps/web/src/apis/Auth/postEmailLogin.ts | 2 +- .../src/apis/Auth/postEmailVerification.ts | 4 - apps/web/src/apis/Auth/postKakaoAuth.ts | 3 +- apps/web/src/apis/Auth/postSignUp.ts | 4 - apps/web/src/apis/MyPage/patchPassword.ts | 6 +- apps/web/src/apis/MyPage/patchProfile.ts | 6 +- apps/web/src/apis/Scores/postCreateGpa.ts | 6 +- .../src/apis/Scores/postCreateLanguageTest.ts | 6 +- .../applications/postSubmitApplication.ts | 5 -- apps/web/src/apis/community/deleteComment.ts | 5 +- apps/web/src/apis/community/deleteLikePost.ts | 4 - apps/web/src/apis/community/deletePost.ts | 6 +- .../web/src/apis/community/patchUpdatePost.ts | 6 +- .../src/apis/community/postCreateComment.ts | 5 +- apps/web/src/apis/community/postCreatePost.ts | 6 +- apps/web/src/apis/community/postLikePost.ts | 4 - .../postUploadProfileImageBeforeSignup.ts | 4 - .../web/src/apis/mentor/postApplyMentoring.ts | 4 - .../src/apis/mentor/postMentorApplication.ts | 4 - apps/web/src/apis/news/deleteNews.ts | 4 +- apps/web/src/apis/news/postCreateNews.ts | 5 +- apps/web/src/apis/news/putUpdateNews.ts | 5 +- apps/web/src/apis/reports/postReport.ts | 5 +- apps/web/src/apis/universities/postAddWish.ts | 2 - .../(home)/_ui/FindLastYearScoreBar/index.tsx | 2 +- .../[boardCode]/[postId]/KebabMenu.tsx | 2 +- .../[postId]/modify/PostModifyForm.tsx | 3 +- .../community/[boardCode]/create/PostForm.tsx | 2 +- .../_hooks/useCommunityImageUpload.ts | 2 +- apps/web/src/app/layout.tsx | 11 ++- apps/web/src/app/login/LoginContent.tsx | 7 +- .../chat/[chatId]/_ui/ChatContent/index.tsx | 3 +- .../src/app/my/_ui/MyProfileContent/index.tsx | 5 +- .../_components/UniversityScreen/index.tsx | 3 +- apps/web/src/app/my/apply-mentor/page.tsx | 2 +- .../_hooks/useSelectUniversities.ts | 6 +- .../src/app/sign-up/email/EmailSignUpForm.tsx | 6 +- .../UniversityDetail/_ui/UniversityBtns.tsx | 3 +- .../application/apply/ApplyPageContent.tsx | 2 +- .../university/application/apply/GpaStep.tsx | 2 +- .../application/apply/LanguageStep.tsx | 2 +- .../src/app/university/score/ScoreCard.tsx | 2 +- .../language-test/LanguageTestSubmitForm.tsx | 6 +- .../GlobalLayout/ui/AIInspectorFab/index.tsx | 2 +- .../login/signup/SignupPrepareScreen.tsx | 3 +- .../login/signup/SignupProfileScreen.tsx | 4 +- .../login/signup/SignupRegionScreen.tsx | 5 +- .../components/login/signup/SignupSurvey.tsx | 18 +---- apps/web/src/components/modal/SurveyModal.tsx | 2 +- .../_hooks/useSelectReportHandler.ts | 2 - apps/web/src/components/ui/Toast/index.tsx | 81 ------------------- apps/web/src/lib/react-query/queryClient.ts | 43 +++++++--- apps/web/src/lib/toast/options.ts | 24 ++++++ apps/web/src/lib/zustand/useToastStore.ts | 44 ---------- apps/web/src/utils/authUtils.ts | 2 +- apps/web/src/utils/axiosInstance.ts | 5 +- apps/web/src/utils/errorHandler.ts | 46 ----------- pnpm-lock.yaml | 17 ++++ 61 files changed, 134 insertions(+), 354 deletions(-) delete mode 100644 apps/web/src/components/ui/Toast/index.tsx create mode 100644 apps/web/src/lib/toast/options.ts delete mode 100644 apps/web/src/lib/zustand/useToastStore.ts delete mode 100644 apps/web/src/utils/errorHandler.ts diff --git a/apps/web/package.json b/apps/web/package.json index 93bfbf01..051704f8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.60.0", + "react-hot-toast": "^2.6.0", "sockjs-client": "^1.6.1", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", diff --git a/apps/web/src/apis/Auth/deleteAccount.ts b/apps/web/src/apis/Auth/deleteAccount.ts index 546282a4..9cadadc9 100644 --- a/apps/web/src/apis/Auth/deleteAccount.ts +++ b/apps/web/src/apis/Auth/deleteAccount.ts @@ -4,7 +4,6 @@ import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { type AccountResponse, authApi } from "./api"; /** @@ -26,9 +25,6 @@ const useDeleteUserAccount = () => { clearAccessToken(); queryClient.clear(); }, - onError: () => { - toast.error("회원탈퇴에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/Auth/postAppleAuth.ts b/apps/web/src/apis/Auth/postAppleAuth.ts index 81a1a9a1..b43041ad 100644 --- a/apps/web/src/apis/Auth/postAppleAuth.ts +++ b/apps/web/src/apis/Auth/postAppleAuth.ts @@ -2,8 +2,8 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { type AppleAuthRequest, type AppleAuthResponse, authApi } from "./api"; /** @@ -31,7 +31,6 @@ const usePostAppleAuth = () => { } }, onError: () => { - toast.error("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); router.push("/login"); }, }); diff --git a/apps/web/src/apis/Auth/postEmailLogin.ts b/apps/web/src/apis/Auth/postEmailLogin.ts index 4d34a728..760e18d7 100644 --- a/apps/web/src/apis/Auth/postEmailLogin.ts +++ b/apps/web/src/apis/Auth/postEmailLogin.ts @@ -2,8 +2,8 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { authApi, type EmailLoginRequest, type EmailLoginResponse } from "./api"; /** diff --git a/apps/web/src/apis/Auth/postEmailVerification.ts b/apps/web/src/apis/Auth/postEmailVerification.ts index dfae1119..59c6612d 100644 --- a/apps/web/src/apis/Auth/postEmailVerification.ts +++ b/apps/web/src/apis/Auth/postEmailVerification.ts @@ -1,7 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { authApi, type EmailSignUpRequest, type EmailSignUpResponse } from "./api"; /** @@ -10,9 +9,6 @@ import { authApi, type EmailSignUpRequest, type EmailSignUpResponse } from "./ap const usePostEmailSignUp = () => { return useMutation({ mutationFn: (data) => authApi.postEmailSignUp(data), - onError: (error) => { - toast.error("회원가입에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/Auth/postKakaoAuth.ts b/apps/web/src/apis/Auth/postKakaoAuth.ts index 3ead72ae..a25793c7 100644 --- a/apps/web/src/apis/Auth/postKakaoAuth.ts +++ b/apps/web/src/apis/Auth/postKakaoAuth.ts @@ -2,8 +2,8 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { authApi, type KakaoAuthRequest, type KakaoAuthResponse } from "./api"; /** @@ -31,7 +31,6 @@ const usePostKakaoAuth = () => { } }, onError: () => { - toast.error("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); router.push("/login"); }, }); diff --git a/apps/web/src/apis/Auth/postSignUp.ts b/apps/web/src/apis/Auth/postSignUp.ts index c907c7f2..72860bde 100644 --- a/apps/web/src/apis/Auth/postSignUp.ts +++ b/apps/web/src/apis/Auth/postSignUp.ts @@ -1,7 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { authApi, type SignUpRequest, type SignUpResponse } from "./api"; /** @@ -10,9 +9,6 @@ import { authApi, type SignUpRequest, type SignUpResponse } from "./api"; const usePostSignUp = () => { return useMutation({ mutationFn: (data) => authApi.postSignUp(data), - onError: (error) => { - toast.error("회원가입에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/MyPage/patchPassword.ts b/apps/web/src/apis/MyPage/patchPassword.ts index ac8096f8..542c8d51 100644 --- a/apps/web/src/apis/MyPage/patchPassword.ts +++ b/apps/web/src/apis/MyPage/patchPassword.ts @@ -2,8 +2,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { QueryKeys } from "../queryKeys"; import { myPageApi, type PasswordPatchRequest } from "./api"; @@ -21,10 +21,6 @@ const usePatchMyPassword = () => { toast.success("비밀번호가 성공적으로 변경되었습니다."); router.replace("/"); }, - onError: (error) => { - const errorMessage = error.response?.data?.message; - toast.error(errorMessage || "비밀번호 변경에 실패했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/MyPage/patchProfile.ts b/apps/web/src/apis/MyPage/patchProfile.ts index ce26560c..778732b2 100644 --- a/apps/web/src/apis/MyPage/patchProfile.ts +++ b/apps/web/src/apis/MyPage/patchProfile.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { QueryKeys } from "../queryKeys"; import { myPageApi, type ProfilePatchRequest } from "./api"; @@ -18,10 +18,6 @@ const usePatchMyInfo = () => { onSuccess: () => { toast.success("프로필이 성공적으로 수정되었습니다."); }, - onError: (error) => { - const errorMessage = error.response?.data?.message; - toast.error(errorMessage || "프로필 수정에 실패했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/Scores/postCreateGpa.ts b/apps/web/src/apis/Scores/postCreateGpa.ts index fa4fbb6b..e138cab9 100644 --- a/apps/web/src/apis/Scores/postCreateGpa.ts +++ b/apps/web/src/apis/Scores/postCreateGpa.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { ScoresQueryKeys, scoresApi, type UsePostGpaScoreRequest } from "./api"; /** @@ -16,10 +16,6 @@ export const usePostGpaScore = () => { toast.success("학점 정보가 성공적으로 제출되었습니다."); queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myGpaScore] }); }, - - onError: (error) => { - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/Scores/postCreateLanguageTest.ts b/apps/web/src/apis/Scores/postCreateLanguageTest.ts index 9b1f49fc..450a1e68 100644 --- a/apps/web/src/apis/Scores/postCreateLanguageTest.ts +++ b/apps/web/src/apis/Scores/postCreateLanguageTest.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { ScoresQueryKeys, scoresApi, type UsePostLanguageTestScoreRequest } from "./api"; /** @@ -16,10 +16,6 @@ export const usePostLanguageTestScore = () => { toast.success("어학 성적이 성공적으로 제출되었습니다."); queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myLanguageTestScore] }); }, - - onError: (error) => { - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/applications/postSubmitApplication.ts b/apps/web/src/apis/applications/postSubmitApplication.ts index 4c8d374a..8a3ab1ef 100644 --- a/apps/web/src/apis/applications/postSubmitApplication.ts +++ b/apps/web/src/apis/applications/postSubmitApplication.ts @@ -1,7 +1,6 @@ import { type UseMutationOptions, type UseMutationResult, useMutation } from "@tanstack/react-query"; import type { AxiosError, AxiosResponse } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { applicationsApi, type UseSubmitApplicationRequest, type UseSubmitApplicationResponse } from "./api"; /** @@ -27,10 +26,6 @@ const usePostSubmitApplication = ( >({ ...props, mutationFn: applicationsApi.postSubmitApplication, - onError: (error) => { - const errorMessage = error?.response?.data?.message; - toast.error(errorMessage || "지원 중 오류가 발생했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/community/deleteComment.ts b/apps/web/src/apis/community/deleteComment.ts index 33f49dd8..596f7f25 100644 --- a/apps/web/src/apis/community/deleteComment.ts +++ b/apps/web/src/apis/community/deleteComment.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { type CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; interface DeleteCommentRequest { @@ -22,9 +22,6 @@ const useDeleteComment = () => { queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); toast.success("댓글이 삭제되었습니다."); }, - onError: (error) => { - toast.error("댓글 삭제에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/community/deleteLikePost.ts b/apps/web/src/apis/community/deleteLikePost.ts index 4d33e765..82c4027d 100644 --- a/apps/web/src/apis/community/deleteLikePost.ts +++ b/apps/web/src/apis/community/deleteLikePost.ts @@ -1,7 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { CommunityQueryKeys, communityApi, type PostLikeResponse } from "./api"; /** @@ -16,9 +15,6 @@ const useDeleteLike = () => { // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); }, - onError: (error) => { - toast.error("좋아요 취소 처리에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/community/deletePost.ts b/apps/web/src/apis/community/deletePost.ts index 6bbe7717..e843d3a0 100644 --- a/apps/web/src/apis/community/deletePost.ts +++ b/apps/web/src/apis/community/deletePost.ts @@ -2,9 +2,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError, AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; - +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { CommunityQueryKeys, communityApi, type DeletePostResponse } from "./api"; interface DeletePostVariables { @@ -59,9 +58,6 @@ const useDeletePost = () => { // 게시글 목록 페이지 이동 router.replace(`/community/${variables.boardCode || "FREE"}`); }, - onError: (error) => { - toast.error("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/community/patchUpdatePost.ts b/apps/web/src/apis/community/patchUpdatePost.ts index 168a60b3..784260ca 100644 --- a/apps/web/src/apis/community/patchUpdatePost.ts +++ b/apps/web/src/apis/community/patchUpdatePost.ts @@ -1,8 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; - +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { CommunityQueryKeys, communityApi, type PostIdResponse, type PostUpdateRequest } from "./api"; interface UpdatePostVariables { @@ -54,9 +53,6 @@ const useUpdatePost = () => { toast.success("게시글이 수정되었습니다."); }, - onError: (error) => { - toast.error("게시글 수정에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/community/postCreateComment.ts b/apps/web/src/apis/community/postCreateComment.ts index cb22339f..d80730b9 100644 --- a/apps/web/src/apis/community/postCreateComment.ts +++ b/apps/web/src/apis/community/postCreateComment.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { type CommentCreateRequest, type CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; /** @@ -17,9 +17,6 @@ const useCreateComment = () => { queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); toast.success("댓글이 등록되었습니다."); }, - onError: (error) => { - toast.error("댓글 등록에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/community/postCreatePost.ts b/apps/web/src/apis/community/postCreatePost.ts index f4f6c1a3..5339936a 100644 --- a/apps/web/src/apis/community/postCreatePost.ts +++ b/apps/web/src/apis/community/postCreatePost.ts @@ -1,8 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; - +import { toast } from "react-hot-toast"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { CommunityQueryKeys, communityApi, type PostCreateRequest, type PostIdResponse } from "./api"; /** @@ -47,9 +46,6 @@ const useCreatePost = () => { toast.success("게시글이 등록되었습니다."); }, - onError: (error) => { - toast.error("게시글 등록에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/community/postLikePost.ts b/apps/web/src/apis/community/postLikePost.ts index da274aac..6ddfb937 100644 --- a/apps/web/src/apis/community/postLikePost.ts +++ b/apps/web/src/apis/community/postLikePost.ts @@ -1,7 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { CommunityQueryKeys, communityApi, type PostLikeResponse } from "./api"; /** @@ -16,9 +15,6 @@ const usePostLike = () => { // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); }, - onError: (error) => { - toast.error("좋아요 처리에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts b/apps/web/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts index c223dfd0..67fa0122 100644 --- a/apps/web/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts +++ b/apps/web/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts @@ -1,6 +1,5 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import type { FileResponse } from "@/types/file"; import { imageUploadApi } from "./api"; @@ -10,9 +9,6 @@ import { imageUploadApi } from "./api"; const useUploadProfileImagePublic = () => { return useMutation({ mutationFn: imageUploadApi.postUploadProfileImageBeforeSignup, - onError: (error) => { - toast.error("이미지 업로드에 실패했습니다."); - }, }); }; diff --git a/apps/web/src/apis/mentor/postApplyMentoring.ts b/apps/web/src/apis/mentor/postApplyMentoring.ts index 6be451f4..d371f7ef 100644 --- a/apps/web/src/apis/mentor/postApplyMentoring.ts +++ b/apps/web/src/apis/mentor/postApplyMentoring.ts @@ -1,7 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { MentorQueryKeys, mentorApi, type PostApplyMentoringRequest, type PostApplyMentoringResponse } from "./api"; /** @@ -15,9 +14,6 @@ const usePostApplyMentoring = () => { // 멘토링 신청 후 멘토 목록을 새로고침 await queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.applyMentoringList] }); }, - onError: () => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/mentor/postMentorApplication.ts b/apps/web/src/apis/mentor/postMentorApplication.ts index f1657cb5..dfa02e8c 100644 --- a/apps/web/src/apis/mentor/postMentorApplication.ts +++ b/apps/web/src/apis/mentor/postMentorApplication.ts @@ -1,7 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import { mentorApi, type PostMentorApplicationRequest } from "./api"; /** @@ -10,9 +9,6 @@ import { mentorApi, type PostMentorApplicationRequest } from "./api"; const usePostMentorApplication = () => { return useMutation({ mutationFn: mentorApi.postMentorApplication, - onError: (_error) => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/news/deleteNews.ts b/apps/web/src/apis/news/deleteNews.ts index 5b92442e..ebe1be74 100644 --- a/apps/web/src/apis/news/deleteNews.ts +++ b/apps/web/src/apis/news/deleteNews.ts @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import type { Article } from "@/types/news"; import { type ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; @@ -33,11 +32,10 @@ const useDeleteArticle = (userId: number | null) => { return { previousArticleList }; }, - onError: (error, _variables, context) => { + onError: (_error, _variables, context) => { if (context?.previousArticleList) { queryClient.setQueryData(queryKey, context.previousArticleList); } - toast.error("아티클 삭제에 실패했습니다. 다시 시도해주세요."); }, onSettled: () => { diff --git a/apps/web/src/apis/news/postCreateNews.ts b/apps/web/src/apis/news/postCreateNews.ts index 95621848..fdfb2db7 100644 --- a/apps/web/src/apis/news/postCreateNews.ts +++ b/apps/web/src/apis/news/postCreateNews.ts @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import ArticleThumbUrlPng from "@/public/images/article-thumb.png"; import type { Article } from "@/types/news"; import { type ArticleListResponse, NewsQueryKeys, newsApi, type UsePostAddArticleRequest } from "./api"; @@ -41,12 +40,10 @@ const usePostAddArticle = (userId: number | null) => { }); return { previousArticleContainer }; }, - onError: (error, _variables, context) => { - const errorMessage = error.response?.data?.message || ""; + onError: (_error, _variables, context) => { if (context?.previousArticleContainer) { queryClient.setQueryData(queryKey, context.previousArticleContainer); } - toast.error(`아티클 추가에 실패했습니다: ${errorMessage}`); }, onSettled: () => { queryClient.invalidateQueries({ queryKey }); diff --git a/apps/web/src/apis/news/putUpdateNews.ts b/apps/web/src/apis/news/putUpdateNews.ts index 614bc5d6..c0e93d8f 100644 --- a/apps/web/src/apis/news/putUpdateNews.ts +++ b/apps/web/src/apis/news/putUpdateNews.ts @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; import type { Article } from "@/types/news"; import { type ArticleListResponse, NewsQueryKeys, newsApi, type UsePutModifyArticleRequest } from "./api"; @@ -43,12 +42,10 @@ const usePutModifyArticle = (userId: number | null) => { }); return { previousArticleList }; }, - onError: (error, _variables, context) => { - const errorMessage = error.response?.data?.message || ""; + onError: (_error, _variables, context) => { if (context?.previousArticleList) { queryClient.setQueryData(queryKey, context.previousArticleList); } - toast.error(`아티클 수정에 실패했습니다.${errorMessage}`); }, onSettled: () => { queryClient.invalidateQueries({ queryKey }); diff --git a/apps/web/src/apis/reports/postReport.ts b/apps/web/src/apis/reports/postReport.ts index 1a5d4beb..665c61ef 100644 --- a/apps/web/src/apis/reports/postReport.ts +++ b/apps/web/src/apis/reports/postReport.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { reportsApi, type UsePostReportsRequest } from "./api"; /** @@ -14,9 +14,6 @@ const usePostReports = () => { onSuccess: () => { toast.success("신고가 성공적으로 등록되었습니다."); }, - onError: (_error) => { - toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, }); }; diff --git a/apps/web/src/apis/universities/postAddWish.ts b/apps/web/src/apis/universities/postAddWish.ts index 4f0eebce..59645505 100644 --- a/apps/web/src/apis/universities/postAddWish.ts +++ b/apps/web/src/apis/universities/postAddWish.ts @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { createMutationErrorHandler } from "@/utils/errorHandler"; import { QueryKeys } from "../queryKeys"; import { type AddWishResponse, universitiesApi } from "./api"; @@ -16,7 +15,6 @@ const usePostAddWish = () => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); }, - onError: createMutationErrorHandler("위시리스트 추가에 실패했습니다."), }); }; diff --git a/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx b/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx index 680fef92..3bf5b0a5 100644 --- a/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx +++ b/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import { IconGraduationCap, IconRightArrow } from "@/public/svgs/home"; const FindLastYearScoreBar = () => { diff --git a/apps/web/src/app/community/[boardCode]/[postId]/KebabMenu.tsx b/apps/web/src/app/community/[boardCode]/[postId]/KebabMenu.tsx index 3a38239d..6e371726 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/KebabMenu.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/KebabMenu.tsx @@ -2,9 +2,9 @@ import { useRouter } from "next/navigation"; import { type RefObject, useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useDeletePost } from "@/apis/community"; import ReportPanel from "@/components/ui/ReportPanel"; -import { toast } from "@/lib/zustand/useToastStore"; import { IconSetting } from "@/public/svgs/mentor"; const useClickOutside = (ref: RefObject, handler: (event: MouseEvent | TouchEvent) => void) => { diff --git a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index cb96d586..86aae9c4 100644 --- a/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/apps/web/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -2,10 +2,9 @@ import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; - +import { toast } from "react-hot-toast"; import { useUpdatePost } from "@/apis/community"; import useCommunityImageUpload from "@/app/community/_hooks/useCommunityImageUpload"; -import { toast } from "@/lib/zustand/useToastStore"; import { IconArrowBackFilled, IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; type PostModifyFormProps = { diff --git a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx index eaa6e44c..b78fe8d4 100644 --- a/apps/web/src/app/community/[boardCode]/create/PostForm.tsx +++ b/apps/web/src/app/community/[boardCode]/create/PostForm.tsx @@ -2,10 +2,10 @@ import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useCreatePost } from "@/apis/community"; import useCommunityImageUpload from "@/app/community/_hooks/useCommunityImageUpload"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import { toast } from "@/lib/zustand/useToastStore"; import { IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; type PostFormProps = { diff --git a/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts b/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts index 43bfa482..eb8cceda 100644 --- a/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts +++ b/apps/web/src/app/community/_hooks/useCommunityImageUpload.ts @@ -1,8 +1,8 @@ "use client"; import { type ChangeEvent, type DragEvent, useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { COMMUNITY_MAX_UPLOAD_IMAGES } from "@/constants/community"; -import { toast } from "@/lib/zustand/useToastStore"; type UseCommunityImageUploadOptions = { maxImages?: number; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ec21d169..cd9351da 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,9 +2,9 @@ import type { Metadata, Viewport } from "next"; import dynamic from "next/dynamic"; import localFont from "next/font/local"; import type { ReactNode } from "react"; +import { Toaster } from "react-hot-toast"; import GlobalLayout from "@/components/layout/GlobalLayout"; -import ToastContainer from "@/components/ui/Toast"; import QueryProvider from "@/lib/react-query/QueryProvider"; import "@/styles/globals.css"; @@ -76,7 +76,14 @@ const RootLayout = ({ children }: { children: ReactNode }) => ( {children} - + diff --git a/apps/web/src/app/login/LoginContent.tsx b/apps/web/src/app/login/LoginContent.tsx index c6715161..2104d8bd 100644 --- a/apps/web/src/app/login/LoginContent.tsx +++ b/apps/web/src/app/login/LoginContent.tsx @@ -5,10 +5,11 @@ import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; import { z } from "zod"; import { usePostEmailAuth } from "@/apis/Auth"; +import { infoToastOptions } from "@/lib/toast/options"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { IconSolidConnectionFullBlackLogo } from "@/public/svgs"; import { IconAppleLogo, IconEmailIcon, IconKakaoLogo } from "@/public/svgs/auth"; import { appleLogin, kakaoLogin } from "@/utils/authUtils"; @@ -86,7 +87,7 @@ const LoginContent = () => { } hasShownCommunityOnlyToast.current = true; - toast.info("커뮤니티는 회원 전용입니다. 로그인 후 이용해주세요."); + toast("커뮤니티는 회원 전용입니다. 로그인 후 이용해주세요.", infoToastOptions); router.replace(pathname); }, [pathname, router, searchParams]); @@ -105,7 +106,7 @@ const LoginContent = () => { } hasShownNeedLoginToast.current = true; - toast.info("로그인이 필요합니다. 다시 로그인해주세요."); + toast("로그인이 필요합니다. 다시 로그인해주세요.", infoToastOptions); clearNeedLogin(); }, [clearNeedLogin, isNeedLogin]); diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx index 5e2f7d6f..59169586 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx @@ -2,12 +2,11 @@ import clsx from "clsx"; import Link from "next/link"; +import { toast } from "react-hot-toast"; import { useGetPartnerInfo } from "@/apis/chat"; import { useUploadProfileImage } from "@/apis/image-upload"; - import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { ConnectionStatus } from "@/types/chat"; import { UserRole } from "@/types/mentor"; import { tokenParse } from "@/utils/jwtUtils"; diff --git a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx index b7759198..ff6ce465 100644 --- a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx +++ b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx @@ -1,12 +1,13 @@ "use client"; import Link from "next/link"; +import { toast } from "react-hot-toast"; import { useDeleteUserAccount, usePostLogout } from "@/apis/Auth"; import { type MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; import LinkedTextWithIcon from "@/components/ui/LinkedTextWithIcon"; import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; +import { infoToastOptions } from "@/lib/toast/options"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { IconLikeFill } from "@/public/svgs/mentor"; import { IconBook, @@ -89,7 +90,7 @@ const MyProfileContent = () => { */} - - ); -}; - -const ToastContainer = () => { - const toasts = useToastStore((state) => state.toasts); - - if (toasts.length === 0) return null; - - return ( -
- {toasts.map((toast) => ( -
- -
- ))} -
- ); -}; - -export default ToastContainer; diff --git a/apps/web/src/lib/react-query/queryClient.ts b/apps/web/src/lib/react-query/queryClient.ts index 612322f0..d75b4b01 100644 --- a/apps/web/src/lib/react-query/queryClient.ts +++ b/apps/web/src/lib/react-query/queryClient.ts @@ -1,27 +1,46 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; + +type ErrorResponse = { + message?: string; +}; + +const DEFAULT_ERROR_MESSAGE = "오류가 발생했습니다. 다시 시도해주세요."; + +const isUnauthorized = (status?: number) => status === 401; + +const resolveErrorMessage = (error: AxiosError) => + error.response?.data?.message || error.message || DEFAULT_ERROR_MESSAGE; + +const buildToastId = (status: number | undefined, message: string) => + `rq-error:${status ?? "unknown"}:${message.trim().toLowerCase()}`; const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error) => { - // query 실패 시 전역 에러 토스트 (401 제외) - const axiosError = error as AxiosError<{ message?: string }>; + const axiosError = error as AxiosError; const status = axiosError?.response?.status; - if (status === 401) return; // 인증 오류는 토스트 표시 X + if (isUnauthorized(status)) return; - const errorMessage = - axiosError?.response?.data?.message || axiosError?.message || "오류가 발생했습니다. 다시 시도해주세요."; - toast.error(errorMessage); + const errorMessage = resolveErrorMessage(axiosError); + toast.error(errorMessage, { + id: buildToastId(status, errorMessage), + }); }, }), mutationCache: new MutationCache({ onError: (error) => { - // mutation 실패 시 전역 에러 토스트 - const axiosError = error as AxiosError<{ message?: string }>; - const errorMessage = - axiosError?.response?.data?.message || axiosError?.message || "오류가 발생했습니다. 다시 시도해주세요."; - toast.error(errorMessage); + const axiosError = error as AxiosError; + const status = axiosError?.response?.status; + + // 인증 오류는 인터셉터 리다이렉트 토스트에서만 처리 + if (isUnauthorized(status)) return; + + const errorMessage = resolveErrorMessage(axiosError); + toast.error(errorMessage, { + id: buildToastId(status, errorMessage), + }); }, }), defaultOptions: { diff --git a/apps/web/src/lib/toast/options.ts b/apps/web/src/lib/toast/options.ts new file mode 100644 index 00000000..55c33182 --- /dev/null +++ b/apps/web/src/lib/toast/options.ts @@ -0,0 +1,24 @@ +import type { ToastOptions } from "react-hot-toast"; + +const BASE_STYLE = { + borderRadius: "0.5rem", + color: "#ffffff", +}; + +export const infoToastOptions: ToastOptions = { + duration: 3000, + icon: "ℹ", + style: { + ...BASE_STYLE, + background: "#111827", + }, +}; + +export const warningToastOptions: ToastOptions = { + duration: 3000, + icon: "⚠", + style: { + ...BASE_STYLE, + background: "#f59e0b", + }, +}; diff --git a/apps/web/src/lib/zustand/useToastStore.ts b/apps/web/src/lib/zustand/useToastStore.ts deleted file mode 100644 index 4367a1c4..00000000 --- a/apps/web/src/lib/zustand/useToastStore.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { create } from "zustand"; - -export type ToastType = "success" | "error" | "info" | "warning"; - -export interface Toast { - id: string; - message: string; - type: ToastType; - duration?: number; -} - -interface ToastState { - toasts: Toast[]; - addToast: (message: string, type?: ToastType, duration?: number) => void; - removeToast: (id: string) => void; -} - -export const useToastStore = create((set) => ({ - toasts: [], - addToast: (message, type = "info", duration = 3000) => { - const id = `${Date.now()}-${Math.random()}`; - const toast: Toast = { id, message, type, duration }; - - set((state) => ({ toasts: [...state.toasts, toast] })); - - // 자동으로 토스트 제거 - if (duration > 0) { - setTimeout(() => { - set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })); - }, duration); - } - }, - removeToast: (id) => { - set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })); - }, -})); - -// 편리한 헬퍼 함수들 -export const toast = { - success: (message: string, duration?: number) => useToastStore.getState().addToast(message, "success", duration), - error: (message: string, duration?: number) => useToastStore.getState().addToast(message, "error", duration), - info: (message: string, duration?: number) => useToastStore.getState().addToast(message, "info", duration), - warning: (message: string, duration?: number) => useToastStore.getState().addToast(message, "warning", duration), -}; diff --git a/apps/web/src/utils/authUtils.ts b/apps/web/src/utils/authUtils.ts index b45c21b8..981ac939 100644 --- a/apps/web/src/utils/authUtils.ts +++ b/apps/web/src/utils/authUtils.ts @@ -1,4 +1,4 @@ -import { toast } from "@/lib/zustand/useToastStore"; +import { toast } from "react-hot-toast"; import type { appleOAuth2CodeResponse } from "@/types/auth"; export const authProviderName = (provider: "KAKAO" | "APPLE" | "EMAIL"): string => { diff --git a/apps/web/src/utils/axiosInstance.ts b/apps/web/src/utils/axiosInstance.ts index f3a99dc0..d728a450 100644 --- a/apps/web/src/utils/axiosInstance.ts +++ b/apps/web/src/utils/axiosInstance.ts @@ -1,8 +1,7 @@ import axios, { type AxiosError, type AxiosInstance } from "axios"; - +import { toast } from "react-hot-toast"; import { postReissueToken } from "@/apis/Auth/server"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; import { isTokenExpired } from "@/utils/jwtUtils"; // --- 글로벌 변수 --- @@ -28,7 +27,7 @@ const redirectToLogin = (message: string) => { try { // 쿠키 유틸이 클라이언트에서만 동작하므로 window 가드 내에서 호출 } catch {} - toast.error(message); + toast.error(message, { id: "auth-redirect" }); window.location.href = "/login"; } }; diff --git a/apps/web/src/utils/errorHandler.ts b/apps/web/src/utils/errorHandler.ts deleted file mode 100644 index fa94bbe8..00000000 --- a/apps/web/src/utils/errorHandler.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AxiosError } from "axios"; -import { toast } from "@/lib/zustand/useToastStore"; -import { AuthenticationRequiredError } from "@/utils/axiosInstance"; - -/** - * 중앙화된 mutation 에러 처리 함수 - * @param error - 발생한 에러 - * @param defaultMessage - 기본 에러 메시지 - * @param onAuthError - 인증 에러 시 실행할 콜백 (선택적) - */ -export const handleMutationError = ( - error: unknown, - defaultMessage: string = "요청에 실패했습니다.", - onAuthError?: () => void, -): void => { - // 인증 관련 에러는 조용히 처리 (이미 redirectToLogin이 처리함) - if (error instanceof AuthenticationRequiredError) { - onAuthError?.(); - return; - } - - // Axios 에러 처리 - if (error instanceof AxiosError) { - const errorMessage = (error.response?.data as { message?: string })?.message || defaultMessage; - toast.error(errorMessage); - return; - } - - // 기타 에러 처리 - toast.error(defaultMessage); -}; - -/** - * React Query mutation의 onError 핸들러를 위한 헬퍼 함수 - * @param defaultMessage - 기본 에러 메시지 - * @param onAuthError - 인증 에러 시 실행할 콜백 (선택적) - * @returns onError 핸들러 함수 - */ -export const createMutationErrorHandler = ( - defaultMessage: string = "요청에 실패했습니다.", - onAuthError?: () => void, -) => { - return (error: unknown) => { - handleMutationError(error, defaultMessage, onAuthError); - }; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d5efe01..09a7fc75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,6 +228,9 @@ importers: react-hook-form: specifier: ^7.60.0 version: 7.71.1(react@18.3.1) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sockjs-client: specifier: ^1.6.1 version: 1.6.1 @@ -5005,6 +5008,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -11068,6 +11078,13 @@ snapshots: dependencies: react: 18.3.1 + react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.2.3 + goober: 2.1.18(csstype@3.2.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is@17.0.2: {} react-refresh@0.18.0: {} From 4cf3c4483b4cb4dfcf47180636d0e25d40b64ec6 Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 30 Apr 2026 18:18:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(web):=20=EA=B0=9C=EB=B3=84=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=86=A0=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=ED=82=B5=20?= =?UTF-8?q?=EB=A9=94=ED=83=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/apis/image-upload/postUploadProfileImage.ts | 2 ++ apps/web/src/lib/react-query/errorToastMeta.ts | 13 +++++++++++++ apps/web/src/lib/react-query/queryClient.ts | 9 +++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/react-query/errorToastMeta.ts diff --git a/apps/web/src/apis/image-upload/postUploadProfileImage.ts b/apps/web/src/apis/image-upload/postUploadProfileImage.ts index 0b8e35a9..74d8fe5f 100644 --- a/apps/web/src/apis/image-upload/postUploadProfileImage.ts +++ b/apps/web/src/apis/image-upload/postUploadProfileImage.ts @@ -1,10 +1,12 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; +import { SKIP_GLOBAL_ERROR_TOAST_META } from "@/lib/react-query/errorToastMeta"; import { imageUploadApi, type UploadProfileImageResponse } from "./api"; const usePostUploadProfileImage = () => { return useMutation({ mutationFn: (file) => imageUploadApi.postUploadProfileImage(file), + meta: SKIP_GLOBAL_ERROR_TOAST_META, }); }; diff --git a/apps/web/src/lib/react-query/errorToastMeta.ts b/apps/web/src/lib/react-query/errorToastMeta.ts new file mode 100644 index 00000000..05e88526 --- /dev/null +++ b/apps/web/src/lib/react-query/errorToastMeta.ts @@ -0,0 +1,13 @@ +export type ErrorToastMeta = { + skipGlobalErrorToast?: boolean; +}; + +export const SKIP_GLOBAL_ERROR_TOAST_META: ErrorToastMeta = { + skipGlobalErrorToast: true, +}; + +export const shouldSkipGlobalErrorToast = (meta: unknown): boolean => { + if (!meta || typeof meta !== "object") return false; + + return (meta as ErrorToastMeta).skipGlobalErrorToast === true; +}; diff --git a/apps/web/src/lib/react-query/queryClient.ts b/apps/web/src/lib/react-query/queryClient.ts index d75b4b01..ea123168 100644 --- a/apps/web/src/lib/react-query/queryClient.ts +++ b/apps/web/src/lib/react-query/queryClient.ts @@ -1,6 +1,7 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { toast } from "react-hot-toast"; +import { shouldSkipGlobalErrorToast } from "./errorToastMeta"; type ErrorResponse = { message?: string; @@ -18,7 +19,9 @@ const buildToastId = (status: number | undefined, message: string) => const queryClient = new QueryClient({ queryCache: new QueryCache({ - onError: (error) => { + onError: (error, query) => { + if (shouldSkipGlobalErrorToast(query.meta)) return; + const axiosError = error as AxiosError; const status = axiosError?.response?.status; if (isUnauthorized(status)) return; @@ -30,7 +33,9 @@ const queryClient = new QueryClient({ }, }), mutationCache: new MutationCache({ - onError: (error) => { + onError: (error, _variables, _context, mutation) => { + if (shouldSkipGlobalErrorToast(mutation.options.meta)) return; + const axiosError = error as AxiosError; const status = axiosError?.response?.status;