From bacbaa7b6df8a6041b3fc92608108107b5a5978d Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 1 Aug 2026 13:15:14 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20showIconToast=EC=97=90=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=96=B5=EC=A0=9C(dedupe)=20opt-out=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/lib/toast/showIconToast.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/web/src/lib/toast/showIconToast.tsx b/apps/web/src/lib/toast/showIconToast.tsx index 055e6800..99415d94 100644 --- a/apps/web/src/lib/toast/showIconToast.tsx +++ b/apps/web/src/lib/toast/showIconToast.tsx @@ -17,12 +17,20 @@ const TOAST_DURATION = 3000; const TOAST_COOLDOWN = 3500; const activeToastKeys = new Set(); -export const showIconToast = (icon: ToastIconKey, message: string) => { +export type ShowIconToastOptions = { + /** 동일한 아이콘+메시지 토스트를 TOAST_COOLDOWN 동안 억제할지 여부 (기본 true) */ + dedupe?: boolean; +}; + +export const showIconToast = (icon: ToastIconKey, message: string, options?: ShowIconToastOptions) => { const Icon = ICONS[icon]; const key = `${icon}:${message}`; + const dedupe = options?.dedupe ?? true; - if (activeToastKeys.has(key)) return; - activeToastKeys.add(key); + if (dedupe) { + if (activeToastKeys.has(key)) return; + activeToastKeys.add(key); + } toast.custom( () => ( @@ -36,5 +44,7 @@ export const showIconToast = (icon: ToastIconKey, message: string) => { { duration: TOAST_DURATION }, ); - setTimeout(() => activeToastKeys.delete(key), TOAST_COOLDOWN); + if (dedupe) { + setTimeout(() => activeToastKeys.delete(key), TOAST_COOLDOWN); + } }; From e96a187960d35acb916c2fc570ef7ecf9b5ad18f Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 1 Aug 2026 13:15:22 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C?= =?UTF-8?q?=20=ED=86=A0=EC=8A=A4=ED=8A=B8=EA=B0=80=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/Auth/postEmailLogin.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/web/src/apis/Auth/postEmailLogin.ts b/apps/web/src/apis/Auth/postEmailLogin.ts index d8e53c14..92fdfbf0 100644 --- a/apps/web/src/apis/Auth/postEmailLogin.ts +++ b/apps/web/src/apis/Auth/postEmailLogin.ts @@ -2,11 +2,18 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { SKIP_GLOBAL_ERROR_TOAST_META } from "@/lib/react-query/errorToastMeta"; import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { getCommunityRedirectOrFallback } from "@/utils/authRedirect"; import { type AuthRedirectOptions, authApi, type EmailLoginRequest, type EmailLoginResponse } from "./api"; +const EMAIL_LOGIN_FAILURE_MESSAGE = "이메일 또는 비밀번호를 확인해주세요."; + +type EmailLoginErrorResponse = { + message?: string; +}; + /** * @description 이메일 로그인을 위한 useMutation 커스텀 훅 */ @@ -14,8 +21,11 @@ const usePostEmailAuth = ({ redirectPath }: AuthRedirectOptions = {}) => { const { setAccessToken } = useAuthStore(); const router = useRouter(); - return useMutation({ + return useMutation, EmailLoginRequest>({ mutationFn: (data) => authApi.postEmailLogin(data), + // 로그인 API는 publicAxiosInstance를 사용해 전역 401 인터셉터/토스트가 적용되지 않으므로 + // 전역 에러 토스트를 건너뛰고 이 mutation에서 직접 처리한다. + meta: SKIP_GLOBAL_ERROR_TOAST_META, onSuccess: (data) => { const { accessToken } = data; @@ -31,6 +41,13 @@ const usePostEmailAuth = ({ redirectPath }: AuthRedirectOptions = {}) => { router.push(getCommunityRedirectOrFallback(redirectPath)); }, 100); }, + onError: (error) => { + const message = error.response?.data?.message || EMAIL_LOGIN_FAILURE_MESSAGE; + + // 로그인 실패는 사용자가 자격증명을 고쳐 곧바로 재시도하는 경우가 많아 + // 동일 메시지 억제(dedupe)를 끄고 매 시도마다 토스트를 노출한다. + showIconToast("logo", message, { dedupe: false }); + }, }); }; From 51a4426d862a818a0f41069bff8a7b1043427284 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 1 Aug 2026 15:36:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=A4=EB=A5=98=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/Auth/postEmailLogin.ts | 17 ++++++++++++++++- apps/web/src/app/login/LoginContent.tsx | 8 -------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/web/src/apis/Auth/postEmailLogin.ts b/apps/web/src/apis/Auth/postEmailLogin.ts index 92fdfbf0..a5064d5c 100644 --- a/apps/web/src/apis/Auth/postEmailLogin.ts +++ b/apps/web/src/apis/Auth/postEmailLogin.ts @@ -9,11 +9,26 @@ import { getCommunityRedirectOrFallback } from "@/utils/authRedirect"; import { type AuthRedirectOptions, authApi, type EmailLoginRequest, type EmailLoginResponse } from "./api"; const EMAIL_LOGIN_FAILURE_MESSAGE = "이메일 또는 비밀번호를 확인해주세요."; +const EMAIL_LOGIN_REQUEST_FAILURE_MESSAGE = "로그인에 실패했습니다. 잠시 후 다시 시도해주세요."; type EmailLoginErrorResponse = { message?: string; }; +const getEmailLoginErrorMessage = (error: AxiosError) => { + const responseMessage = error.response?.data?.message; + + if (responseMessage) { + return responseMessage; + } + + if (error.response?.status === 401) { + return EMAIL_LOGIN_FAILURE_MESSAGE; + } + + return EMAIL_LOGIN_REQUEST_FAILURE_MESSAGE; +}; + /** * @description 이메일 로그인을 위한 useMutation 커스텀 훅 */ @@ -42,7 +57,7 @@ const usePostEmailAuth = ({ redirectPath }: AuthRedirectOptions = {}) => { }, 100); }, onError: (error) => { - const message = error.response?.data?.message || EMAIL_LOGIN_FAILURE_MESSAGE; + const message = getEmailLoginErrorMessage(error); // 로그인 실패는 사용자가 자격증명을 고쳐 곧바로 재시도하는 경우가 많아 // 동일 메시지 억제(dedupe)를 끄고 매 시도마다 토스트를 노출한다. diff --git a/apps/web/src/app/login/LoginContent.tsx b/apps/web/src/app/login/LoginContent.tsx index d3a76397..aca12d36 100644 --- a/apps/web/src/app/login/LoginContent.tsx +++ b/apps/web/src/app/login/LoginContent.tsx @@ -48,12 +48,6 @@ const LoginContent = () => { postEmailAuth(data); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleSubmit(onSubmit)(); - } - }; - const handleBack = () => { if (window.history.length > 1) { router.back(); @@ -97,7 +91,6 @@ const LoginContent = () => { {...register("email", { onChange: handleEmailChange, })} - onKeyDown={handleKeyDown} /> {errors.email &&

{errors.email.message}

} @@ -112,7 +105,6 @@ const LoginContent = () => { placeholder="비밀번호" className="w-full rounded-lg border border-k-100 px-5 py-3 font-serif text-k-400 typo-regular-4 focus:outline-none" {...register("password")} - onKeyDown={handleKeyDown} /> {errors.password &&

{errors.password.message}

}