From 8f5b48b0fc9fe3c826283ac01c3b81e0216d5628 Mon Sep 17 00:00:00 2001 From: strdeok Date: Thu, 30 Oct 2025 16:21:19 +0900 Subject: [PATCH 1/2] feat: add login&join form schema --- src/app/join/_components/JoinForm.tsx | 254 +++++++++--------------- src/app/login/_components/loginForm.tsx | 84 +++----- src/lib/schemas/authSchema.ts | 50 +++++ 3 files changed, 171 insertions(+), 217 deletions(-) create mode 100644 src/lib/schemas/authSchema.ts diff --git a/src/app/join/_components/JoinForm.tsx b/src/app/join/_components/JoinForm.tsx index 392ef18..18d0886 100644 --- a/src/app/join/_components/JoinForm.tsx +++ b/src/app/join/_components/JoinForm.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import EmptyCheckCircle from "@/assets/EmptyCheckCircle.svg"; import FilledCheckCircle from "@/assets/FilledCheckCircle.svg"; import EyeInvisible from "@/assets/EyeInvisible.svg"; @@ -9,134 +9,74 @@ import ServiceTermsSection from "./ServiceTermsSection"; import { useRouter } from "next/navigation"; import { AxiosError } from "axios"; import { useSignUp } from "@/hooks/useAuth"; +import { joinFormSchema } from "@/lib/schemas/authSchema"; +import { JoinFormData } from "@/lib/schemas/authSchema"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; export default function JoinForm() { const router = useRouter(); const { mutate, isPending } = useSignUp(); - - const [email, setEmail] = useState(""); - const [emailError, setEmailError] = useState(""); - - const [name, setName] = useState(""); - - const [password, setPassword] = useState(""); - const [passwordError, setPasswordError] = useState(""); - const [password2, setPassword2] = useState(""); - const [password2Error, setPassword2Error] = useState(""); const [isVisiblePassword, setIsVisiblePassword] = useState(false); - - const [allAgree, setAllAgree] = useState(false); - const [firstAgree, setFirstAgree] = useState(false); - const [secondAgree, setSecondAgree] = useState(false); - const [showServiceTerms, setShowServiceTerms] = useState(false); const [showPrivacyTerms, setShowPrivacyTerms] = useState(false); - // 이메일 유효성 검사 - const validateEmail = (email: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - }; - - // 닉네임 유효성 검사 - const validateName = (name: string) => { - return name.trim().length > 0; - }; - - // 비밀번호 유효성 검사 - const validatePassword = (password: string) => { - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w\W]{8,20}$/; - return passwordRegex.test(password); - }; - - // 이메일 블러 시 에러 처리 - const handleEmailBlur = () => { - if (!email) { - setEmailError("이메일을 입력해주세요."); - } else if (!validateEmail(email)) { - setEmailError("이메일 형식이 올바르지 않습니다."); - } else { - setEmailError(""); - } - }; - - // 비밀번호 블러 시 에러 처리 - const passwordBlur = () => { - if (!password) { - setPasswordError("비밀번호를 입력해주세요."); - } else if (!validatePassword(password)) { - setPasswordError("비밀번호 형식이 올바르지 않습니다."); - } else setPasswordError(""); + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(joinFormSchema), + mode: "onBlur", + defaultValues: { + email: "", + nickname: "", + password: "", + password2: "", + firstAgree: false, + secondAgree: false, + }, + }); + + const onSubmit: SubmitHandler = (data) => { + const { email, nickname, password } = data; + + mutate( + { email, password, nickname }, + { + onSuccess: () => { + alert("인증 메일을 발송했습니다. 메일함을 확인해주세요."); + router.replace("/login"); + }, + onError: (err) => { + const error = err as AxiosError<{ message: string }>; + const message = error.response?.data?.message; + + switch (message) { + case "이미 가입된 이메일입니다.": + alert("이미 가입된 이메일입니다."); + break; + case "이미 사용중인 닉네임입니다.": + alert("이미 사용중인 닉네임입니다."); + default: + break; + } + }, + } + ); }; - // 비밀번호 일치 확인 - const password2Blur = () => { - if (password !== password2) { - setPassword2Error("비밀번호가 일치하지 않습니다."); - } else { - setPassword2Error(""); - } - }; + const firstAgree = watch("firstAgree"); + const secondAgree = watch("secondAgree"); + const allAgree = firstAgree && secondAgree; - // 전체동의 -> 개별 동의 모두 적용 const toggleAllAgree = () => { const next = !allAgree; - setAllAgree(next); - setFirstAgree(next); - setSecondAgree(next); - }; - - // 개별 동의 변경 시 전체 동의 자동 반영 - useEffect(() => { - if (firstAgree && secondAgree) { - setAllAgree(true); - } else { - setAllAgree(false); - } - }, [firstAgree, secondAgree]); - - // 제출 조건: 이메일/비번 유효 + 비밀번호 일치 + 필수 동의 두 개 - const isDisabledSubmit = - !validateEmail(email) || - !validateName(name) || - !validatePassword(password) || - password2Error !== "" || - !firstAgree || - !secondAgree || - isPending; - - const submitSignUp = async (e: React.FormEvent) => { - e.preventDefault(); - const data = { - email, - password, - nickname: name, - }; - - mutate(data, { - onSuccess: () => { - alert("인증 메일을 발송했습니다. 메일함을 확인해주세요."); - router.replace("/login"); - }, - onError: (err) => { - const error = err as AxiosError<{ message: string }>; - const message = error.response?.data?.message; - - switch (message) { - case "이미 가입된 이메일입니다.": - alert("이미 가입된 이메일입니다."); - break; - case "이미 사용중인 닉네임입니다.": - alert("이미 사용중인 닉네임입니다."); - - default: - break; - } - }, - }); + setValue("firstAgree", next, { shouldValidate: true }); + setValue("secondAgree", next, { shouldValidate: true }); }; - - // 스타일 변수 const inputWrapper = "flex flex-col gap-2"; const inputStyle = "border border-[#D9D9D9] rounded-xs h-8 px-2"; const checkBoxWrapper = "flex flex-row justify-between"; @@ -146,9 +86,7 @@ export default function JoinForm() { return (
{ - submitSignUp(e); - }} + onSubmit={handleSubmit(onSubmit)} > {/* 이메일 */}
@@ -159,11 +97,11 @@ export default function JoinForm() { type="text" id="id" className={inputStyle} - onBlur={handleEmailBlur} - value={email} - onChange={(e) => setEmail(e.target.value)} + {...register("email")} /> - {emailError && {emailError}} + {errors.email && ( + {errors.email.message} + )}
{/* 이름 */} @@ -176,10 +114,11 @@ export default function JoinForm() { id="name" placeholder="닉네임을 입력하세요." className={inputStyle} - onChange={(e) => { - setName(e.target.value.trim()); - }} + {...register("nickname")} /> + {errors.nickname && ( + {errors.nickname.message} + )} {/* 비밀번호 */} @@ -190,46 +129,35 @@ export default function JoinForm() { setPassword(e.target.value)} - value={password} className={inputStyle} - onBlur={passwordBlur} + {...register("password")} /> - {passwordError && {passwordError}} + {errors.password && ( + {errors.password.message} + )} {/* 비밀번호 확인 */}
setPassword2(e.target.value)} - onBlur={password2Blur} - disabled={!validatePassword(password)} - className={`w-full - ${ - !validatePassword(password) - ? `${inputStyle} bg-[#F5F5F5]` - : inputStyle - } - `} + className={inputStyle + " w-full " + (!watch("password") ? "bg-[#F5F5F5]" : "")} + {...register("password2")} />
- {password2Error && ( - {password2Error} + {errors.password2 && ( + {errors.password2.message} )} - {/* 전체동의 */}