Skip to content

Commit 97f1cb0

Browse files
authored
Merge pull request #614 from solid-connection/fix/login-error-toast
🐛 이메일 로그인 실패 시 토스트가 표시되지 않는 문제 수정
2 parents 3c663ae + 51a4426 commit 97f1cb0

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

apps/web/src/apis/Auth/postEmailLogin.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,45 @@ import { useMutation } from "@tanstack/react-query";
22

33
import type { AxiosError } from "axios";
44
import { useRouter } from "next/navigation";
5+
import { SKIP_GLOBAL_ERROR_TOAST_META } from "@/lib/react-query/errorToastMeta";
56
import { showIconToast } from "@/lib/toast/showIconToast";
67
import useAuthStore from "@/lib/zustand/useAuthStore";
78
import { getCommunityRedirectOrFallback } from "@/utils/authRedirect";
89
import { type AuthRedirectOptions, authApi, type EmailLoginRequest, type EmailLoginResponse } from "./api";
910

11+
const EMAIL_LOGIN_FAILURE_MESSAGE = "이메일 또는 비밀번호를 확인해주세요.";
12+
const EMAIL_LOGIN_REQUEST_FAILURE_MESSAGE = "로그인에 실패했습니다. 잠시 후 다시 시도해주세요.";
13+
14+
type EmailLoginErrorResponse = {
15+
message?: string;
16+
};
17+
18+
const getEmailLoginErrorMessage = (error: AxiosError<EmailLoginErrorResponse>) => {
19+
const responseMessage = error.response?.data?.message;
20+
21+
if (responseMessage) {
22+
return responseMessage;
23+
}
24+
25+
if (error.response?.status === 401) {
26+
return EMAIL_LOGIN_FAILURE_MESSAGE;
27+
}
28+
29+
return EMAIL_LOGIN_REQUEST_FAILURE_MESSAGE;
30+
};
31+
1032
/**
1133
* @description 이메일 로그인을 위한 useMutation 커스텀 훅
1234
*/
1335
const usePostEmailAuth = ({ redirectPath }: AuthRedirectOptions = {}) => {
1436
const { setAccessToken } = useAuthStore();
1537
const router = useRouter();
1638

17-
return useMutation<EmailLoginResponse, AxiosError, EmailLoginRequest>({
39+
return useMutation<EmailLoginResponse, AxiosError<EmailLoginErrorResponse>, EmailLoginRequest>({
1840
mutationFn: (data) => authApi.postEmailLogin(data),
41+
// 로그인 API는 publicAxiosInstance를 사용해 전역 401 인터셉터/토스트가 적용되지 않으므로
42+
// 전역 에러 토스트를 건너뛰고 이 mutation에서 직접 처리한다.
43+
meta: SKIP_GLOBAL_ERROR_TOAST_META,
1944
onSuccess: (data) => {
2045
const { accessToken } = data;
2146

@@ -31,6 +56,13 @@ const usePostEmailAuth = ({ redirectPath }: AuthRedirectOptions = {}) => {
3156
router.push(getCommunityRedirectOrFallback(redirectPath));
3257
}, 100);
3358
},
59+
onError: (error) => {
60+
const message = getEmailLoginErrorMessage(error);
61+
62+
// 로그인 실패는 사용자가 자격증명을 고쳐 곧바로 재시도하는 경우가 많아
63+
// 동일 메시지 억제(dedupe)를 끄고 매 시도마다 토스트를 노출한다.
64+
showIconToast("logo", message, { dedupe: false });
65+
},
3466
});
3567
};
3668

apps/web/src/app/login/LoginContent.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ const LoginContent = () => {
4848
postEmailAuth(data);
4949
};
5050

51-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
52-
if (e.key === "Enter") {
53-
handleSubmit(onSubmit)();
54-
}
55-
};
56-
5751
const handleBack = () => {
5852
if (window.history.length > 1) {
5953
router.back();
@@ -97,7 +91,6 @@ const LoginContent = () => {
9791
{...register("email", {
9892
onChange: handleEmailChange,
9993
})}
100-
onKeyDown={handleKeyDown}
10194
/>
10295
{errors.email && <p className="mt-1 text-red-500 typo-regular-4">{errors.email.message}</p>}
10396
</div>
@@ -112,7 +105,6 @@ const LoginContent = () => {
112105
placeholder="비밀번호"
113106
className="w-full rounded-lg border border-k-100 px-5 py-3 font-serif text-k-400 typo-regular-4 focus:outline-none"
114107
{...register("password")}
115-
onKeyDown={handleKeyDown}
116108
/>
117109
{errors.password && <p className="mt-1 text-red-500 typo-regular-4">{errors.password.message}</p>}
118110
</div>

apps/web/src/lib/toast/showIconToast.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ const TOAST_DURATION = 3000;
1717
const TOAST_COOLDOWN = 3500;
1818
const activeToastKeys = new Set<string>();
1919

20-
export const showIconToast = (icon: ToastIconKey, message: string) => {
20+
export type ShowIconToastOptions = {
21+
/** 동일한 아이콘+메시지 토스트를 TOAST_COOLDOWN 동안 억제할지 여부 (기본 true) */
22+
dedupe?: boolean;
23+
};
24+
25+
export const showIconToast = (icon: ToastIconKey, message: string, options?: ShowIconToastOptions) => {
2126
const Icon = ICONS[icon];
2227
const key = `${icon}:${message}`;
28+
const dedupe = options?.dedupe ?? true;
2329

24-
if (activeToastKeys.has(key)) return;
25-
activeToastKeys.add(key);
30+
if (dedupe) {
31+
if (activeToastKeys.has(key)) return;
32+
activeToastKeys.add(key);
33+
}
2634

2735
toast.custom(
2836
() => (
@@ -36,5 +44,7 @@ export const showIconToast = (icon: ToastIconKey, message: string) => {
3644
{ duration: TOAST_DURATION },
3745
);
3846

39-
setTimeout(() => activeToastKeys.delete(key), TOAST_COOLDOWN);
47+
if (dedupe) {
48+
setTimeout(() => activeToastKeys.delete(key), TOAST_COOLDOWN);
49+
}
4050
};

0 commit comments

Comments
 (0)