Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#279 회원가입 유효성 검사 구현 #283

Merged
15 changes: 12 additions & 3 deletions client/src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useUserActions } from "@stores/useUserStore";
import { unAuthorizationFetch, fetch } from "./axios";

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 });

Expand All @@ -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 }) =>
Expand All @@ -33,6 +41,7 @@ export const useLoginMutation = (onSuccess: () => void) => {
setUserInfo(id, name, accessToken);
onSuccess();
},
onError: options?.onError,
});
};

Expand Down
1 change: 1 addition & 0 deletions client/src/components/inputField/InputField.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 17 additions & 3 deletions client/src/components/inputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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>
);
2 changes: 1 addition & 1 deletion client/src/features/auth/AuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
15 changes: 14 additions & 1 deletion client/src/features/auth/AuthModal.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
106 changes: 100 additions & 6 deletions client/src/features/auth/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = () => {
Expand All @@ -41,13 +103,42 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => {
};

const handleSubmitButtonClick = () => {
if (!validateForm()) {
return;
}
if (mode === "register") {
signUp(formData);
} else {
login(formData);
}
};

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}
Expand All @@ -67,6 +158,7 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => {
onChange={handleInputChange}
placeholder="이름"
Icon={User}
isError={getFieldError("name")}
/>
)}
<InputField
Expand All @@ -76,6 +168,7 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => {
onChange={handleInputChange}
placeholder="이메일"
Icon={Mail}
isError={getFieldError("email")}
/>
<InputField
type="password"
Expand All @@ -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"
? "계정이 없으신가요? 회원가입하기"
Expand Down
Loading