From e7b45c3b465a47152c3d107a651282083dcf1a20 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Wed, 4 Dec 2024 02:29:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #279 --- client/src/apis/auth.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/src/apis/auth.ts b/client/src/apis/auth.ts index cb7887de..bce93c0d 100644 --- a/client/src/apis/auth.ts +++ b/client/src/apis/auth.ts @@ -1,4 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import { AxiosError } from "axios"; import { useUserActions } from "@stores/useUserStore"; import { unAuthorizationFetch, fetch } from "./axios"; @@ -6,8 +7,15 @@ const authKey = { all: ["auth"] as const, refresh: () => [...authKey.all, "refresh"] as const, }; +export interface ApiErrorResponse { + message: string; + code?: string; +} -export const useSignupMutation = (onSuccess: () => void) => { +interface MutationOptions { + onError?: (error: AxiosError) => void; +} +export const useSignupMutation = (onSuccess: () => void, options?: MutationOptions) => { const fetcher = ({ name, email, password }: { name: string; email: string; password: string }) => unAuthorizationFetch.post("/auth/register", { name, email, password }); @@ -16,10 +24,10 @@ export const useSignupMutation = (onSuccess: () => void) => { onSuccess: () => { onSuccess(); }, + onError: options?.onError, }); }; - -export const useLoginMutation = (onSuccess: () => void) => { +export const useLoginMutation = (onSuccess: () => void, options?: MutationOptions) => { const { setUserInfo } = useUserActions(); const fetcher = ({ email, password }: { email: string; password: string }) => @@ -33,6 +41,7 @@ export const useLoginMutation = (onSuccess: () => void) => { setUserInfo(id, name, accessToken); onSuccess(); }, + onError: options?.onError, }); }; From 95d1b14cd39e28241aa534118956e2fef250fa86 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Wed, 4 Dec 2024 02:29:56 +0900 Subject: [PATCH 2/7] =?UTF-8?q?style:=20inputField=EC=97=90=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=9D=B4=20=EC=95=88=EB=90=9C=EA=B2=BD=EC=9A=B0=20bor?= =?UTF-8?q?der=20red=EB=A1=9C=20=EB=B3=80=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #279 --- client/src/components/inputField/InputField.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/src/components/inputField/InputField.tsx b/client/src/components/inputField/InputField.tsx index 96ba23b7..929333de 100644 --- a/client/src/components/inputField/InputField.tsx +++ b/client/src/components/inputField/InputField.tsx @@ -7,11 +7,20 @@ interface InputFieldProps { onChange: (e: React.ChangeEvent) => void; placeholder?: string; Icon?: React.FunctionComponent>; + isError?: boolean; } -export const InputField = ({ type, name, value, onChange, placeholder, Icon }: InputFieldProps) => ( +export const InputField = ({ + type, + name, + value, + onChange, + placeholder, + Icon, + isError, +}: InputFieldProps) => (
-
+
- {Icon && } + {Icon && }
); From 697cb5d13c5df1fe28db5c9424b49947bca2b017 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Wed, 4 Dec 2024 02:30:06 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=EB=A6=B0=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/features/auth/AuthButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/features/auth/AuthButton.tsx b/client/src/features/auth/AuthButton.tsx index 8a3aadae..3f500edb 100644 --- a/client/src/features/auth/AuthButton.tsx +++ b/client/src/features/auth/AuthButton.tsx @@ -3,8 +3,8 @@ import { TextButton } from "@components/button/textButton"; import { Modal } from "@components/modal/modal"; import { useModal } from "@components/modal/useModal"; import { useCheckLogin } from "@stores/useUserStore"; -import { AuthModal } from "./AuthModal"; import { container } from "./AuthButton.style"; +import { AuthModal } from "./AuthModal"; export const AuthButton = () => { const isLogin = useCheckLogin(); From 38a789ab5670a21505c1669abdbb640036b67b54 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Wed, 4 Dec 2024 02:30:34 +0900 Subject: [PATCH 4/7] =?UTF-8?q?style:=20=EC=97=90=EB=9F=AC=20=EC=95=8C?= =?UTF-8?q?=EB=A0=A4=EC=A3=BC=EB=8A=94=20Caution=20=EA=B8=80=EC=9E=90=20di?= =?UTF-8?q?v=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #279 --- client/src/features/auth/AuthModal.style.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/src/features/auth/AuthModal.style.ts b/client/src/features/auth/AuthModal.style.ts index a0f56136..09d82cd6 100644 --- a/client/src/features/auth/AuthModal.style.ts +++ b/client/src/features/auth/AuthModal.style.ts @@ -16,6 +16,13 @@ export const title = css({ textShadow: "0 2px 4px rgba(0,0,0,0.1)", }); +export const errorWrapper = css({ + display: "flex", + justifyContent: "center", + width: "100%", + height: "20px", + paddingBottom: "40px", +}); export const toggleButton = css({ marginBottom: "md", color: "white", @@ -24,7 +31,13 @@ export const toggleButton = css({ textDecoration: "underline", }, }); - +export const errorContainer = css({ + display: "flex", + position: "relative", + alignContent: "center", + alignItems: "center", + color: "red", +}); export const formContainer = css({ display: "flex", gap: "md", From e6de4dcbb0e433b962234814bcb14a8a77d91a49 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Wed, 4 Dec 2024 02:31:00 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20auth=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=99=80=EC=84=9C=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=ED=9B=84,=20modal=EC=97=90=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #279 --- client/src/features/auth/AuthModal.tsx | 110 +++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 6 deletions(-) diff --git a/client/src/features/auth/AuthModal.tsx b/client/src/features/auth/AuthModal.tsx index b7d07d57..fa78caa5 100644 --- a/client/src/features/auth/AuthModal.tsx +++ b/client/src/features/auth/AuthModal.tsx @@ -1,12 +1,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { useLoginMutation, useSignupMutation } from "@apis/auth"; -import { useState } from "react"; +import { AxiosError } from "axios"; +import { useState, useEffect } from "react"; +import { useLoginMutation, useSignupMutation, ApiErrorResponse } from "@apis/auth"; import Lock from "@assets/icons/lock.svg?react"; import Mail from "@assets/icons/mail.svg?react"; import User from "@assets/icons/user.svg?react"; import { InputField } from "@components/inputField/InputField"; import { Modal } from "@components/modal/modal"; -import { container, formContainer, title, toggleButton } from "./AuthModal.style"; +import { + container, + formContainer, + title, + toggleButton, + errorContainer, + errorWrapper, +} from "./AuthModal.style"; interface AuthModalProps { isOpen: boolean; @@ -20,13 +28,71 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { email: "", password: "", }); + const [error, setError] = useState(""); - const { mutate: login } = useLoginMutation(onClose); - const { mutate: signUp } = useSignupMutation(() => login(formData)); + const getErrorMessage = (error: AxiosError) => { + // 서버에서 보낸 구체적인 에러 메시지가 있다면 사용 + const serverMessage = error.response?.data?.message; + if (serverMessage) return serverMessage; + // 상태 코드별 기본 에러 메시지 + switch (error.response?.status) { + case 400: + return "입력하신 정보가 올바르지 않습니다."; + case 401: + return "이메일 또는 비밀번호가 올바르지 않습니다."; + case 409: + return "이미 사용 중인 이메일입니다."; + default: + return "오류가 발생했습니다. 다시 시도해주세요."; + } + }; + + const { mutate: login } = useLoginMutation(onClose, { + onError: (error: AxiosError) => { + setError(getErrorMessage(error)); + }, + }); + + const { mutate: signUp } = useSignupMutation( + () => { + // 회원가입 성공 시 자동으로 로그인 시도 + const { email, password } = formData; + login({ email, password }); + }, + { + onError: (error: AxiosError) => { + setError(getErrorMessage(error)); + }, + }, + ); + + const validateForm = (): boolean => { + // 이메일 유효성 검사 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + setError("올바른 이메일 형식이 아닙니다."); + return false; + } + + // 비밀번호 유효성 검사 (최소 8자) + if (formData.password.length < 1) { + setError("비밀번호를 입력해주세요."); + return false; + } + + // 회원가입 시 이름 필드 검사 + if (mode === "register" && !formData.name.trim()) { + setError("이름을 입력해주세요."); + return false; + } + + return true; + }; const toggleMode = () => { setMode(mode === "login" ? "register" : "login"); setFormData({ email: "", password: "", name: "" }); + setError(""); }; const closeModal = () => { @@ -41,6 +107,9 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { }; const handleSubmitButtonClick = () => { + if (!validateForm()) { + return; + } if (mode === "register") { signUp(formData); } else { @@ -48,6 +117,32 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { } }; + const getFieldError = (fieldName: string): boolean => { + if (!error) return false; + if (error === "올바른 이메일 형식이 아닙니다." && fieldName === "email") { + return true; + } + if (error === "비밀번호를 입력해주세요." && fieldName === "password") { + return true; + } + if (error === "이름을 입력해주세요." && fieldName === "name") { + return true; + } + return false; + }; + + useEffect(() => { + if (isOpen) { + // 모달이 열릴 때마다 초기화 + setFormData({ + name: "", + email: "", + password: "", + }); + setError(""); + setMode("login"); + } + }, [isOpen]); // isOpen이 변경될 때마다 실행 return ( { onChange={handleInputChange} placeholder="이름" Icon={User} + isError={getFieldError("name")} /> )} { onChange={handleInputChange} placeholder="이메일" Icon={Mail} + isError={getFieldError("email")} /> { onChange={handleInputChange} placeholder="비밀번호" Icon={Lock} + isError={getFieldError("password")} />
- +
{error &&

{error}

}