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<ApiErrorResponse>) => 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, }); }; diff --git a/client/src/components/inputField/InputField.style.ts b/client/src/components/inputField/InputField.style.ts index d9e8b1cc..5efeb341 100644 --- a/client/src/components/inputField/InputField.style.ts +++ b/client/src/components/inputField/InputField.style.ts @@ -6,6 +6,7 @@ export const formGroup = css({ export const inputContainer = css({ position: "relative", + border: "1px solid white", borderRadius: "md", padding: "1", background: "white/30", diff --git a/client/src/components/inputField/InputField.tsx b/client/src/components/inputField/InputField.tsx index 96ba23b7..2b112ca3 100644 --- a/client/src/components/inputField/InputField.tsx +++ b/client/src/components/inputField/InputField.tsx @@ -7,11 +7,25 @@ interface InputFieldProps { onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; placeholder?: string; Icon?: React.FunctionComponent<React.SVGProps<SVGSVGElement>>; + isError?: boolean; } -export const InputField = ({ type, name, value, onChange, placeholder, Icon }: InputFieldProps) => ( +export const InputField = ({ + type, + name, + value, + onChange, + placeholder, + Icon, + isError, +}: InputFieldProps) => ( <div className={formGroup}> - <div className={inputContainer}> + <div + className={inputContainer} + style={{ + border: isError ? "1px solid #EF4444" : "none", // Using Tailwind's red-500 color + }} + > <input type={type} name={name} @@ -22,6 +36,6 @@ export const InputField = ({ type, name, value, onChange, placeholder, Icon }: I required /> </div> - {Icon && <Icon className={iconBox} />} + {Icon && <Icon className={`${iconBox} ${isError ? "c_red" : ""}`} />} </div> ); 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(); 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", diff --git a/client/src/features/auth/AuthModal.tsx b/client/src/features/auth/AuthModal.tsx index b7d07d57..4d770780 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,67 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { email: "", password: "", }); + const [error, setError] = useState<string>(""); - const { mutate: login } = useLoginMutation(onClose); - const { mutate: signUp } = useSignupMutation(() => login(formData)); + const getErrorMessage = (error: AxiosError<ApiErrorResponse>) => { + switch (error.response?.status) { + case 400: + return "입력하신 정보가 올바르지 않습니다."; + case 401: + return "이메일 또는 비밀번호가 올바르지 않습니다."; + case 409: + return "이미 사용 중인 이메일입니다."; + default: + return "오류가 발생했습니다. 다시 시도해주세요."; + } + }; + + const { mutate: login } = useLoginMutation(onClose, { + onError: (error: AxiosError<ApiErrorResponse>) => { + setError(getErrorMessage(error)); + }, + }); + + const { mutate: signUp } = useSignupMutation( + () => { + // 회원가입 성공 시 자동으로 로그인 시도 + const { email, password } = formData; + login({ email, password }); + }, + { + onError: (error: AxiosError<ApiErrorResponse>) => { + 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 +103,9 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { }; const handleSubmitButtonClick = () => { + if (!validateForm()) { + return; + } if (mode === "register") { signUp(formData); } else { @@ -48,6 +113,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 ( <Modal isOpen={isOpen} @@ -67,6 +158,7 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { onChange={handleInputChange} placeholder="이름" Icon={User} + isError={getFieldError("name")} /> )} <InputField @@ -76,6 +168,7 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { onChange={handleInputChange} placeholder="이메일" Icon={Mail} + isError={getFieldError("email")} /> <InputField type="password" @@ -84,9 +177,10 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => { onChange={handleInputChange} placeholder="비밀번호" Icon={Lock} + isError={getFieldError("password")} /> </div> - + <div className={errorWrapper}>{error && <p className={errorContainer}>{error}</p>}</div> <button onClick={toggleMode} className={toggleButton}> {mode === "login" ? "계정이 없으신가요? 회원가입하기"