diff --git a/src/data/form/emailVerification.schema.ts b/src/data/form/emailVerification.schema.ts index 29993bb..4a5ee1e 100644 --- a/src/data/form/emailVerification.schema.ts +++ b/src/data/form/emailVerification.schema.ts @@ -3,7 +3,7 @@ import * as z from 'zod'; export const EmailVerificationSchema = z.object({ email: z .email() - , verificationCode: z + , code: z .string() .length(6, "이메일 인증 번호 6자리를 입력해주십시오.") }); \ No newline at end of file diff --git a/src/data/form/login.schema.ts b/src/data/form/login.schema.ts index a081ab4..02dfcb7 100644 --- a/src/data/form/login.schema.ts +++ b/src/data/form/login.schema.ts @@ -1,9 +1,8 @@ import * as z from 'zod'; export const LoginSchema = z.object({ - email: z - .email() - .min(1, "이메일을 입력해주십시오.") + id: z + .string() , password: z .string() .min(8, "비밀번호는 8-12 자리여야 합니다.") diff --git a/src/data/form/resetPassword.schema.ts b/src/data/form/resetPassword.schema.ts index 2a9e394..73360b3 100644 --- a/src/data/form/resetPassword.schema.ts +++ b/src/data/form/resetPassword.schema.ts @@ -3,7 +3,4 @@ import * as z from 'zod'; export const ResetPasswordSchema = z.object({ email: z .email() - , resetCode: z - .string() - .length(6) }); \ No newline at end of file diff --git a/src/data/form/signup.schema.ts b/src/data/form/signup.schema.ts index b3fb6df..2ed5766 100644 --- a/src/data/form/signup.schema.ts +++ b/src/data/form/signup.schema.ts @@ -1,9 +1,12 @@ import * as z from 'zod'; export const SignUpSchema = z.object({ - email: z + accountId: z .string() - .min(1, "이메일을 입력해주십시오.") + .min(5, "아이디는 5 자리 이상이어야 합니다.") + , email: z + .string() + .min(5, "이메일을 입력해주십시오.") , password: z .string() .min(8, "비밀번호는 8-12 자리여야 합니다.") diff --git a/src/data/request/account/CheckDuplicationRequest.ts b/src/data/request/account/CheckDuplicationRequest.ts new file mode 100644 index 0000000..7db8365 --- /dev/null +++ b/src/data/request/account/CheckDuplicationRequest.ts @@ -0,0 +1,9 @@ +export class CheckDuplicationRequest { + type: 'email' | 'accountId'; + value: string; + + constructor(type: 'email' | 'accountId', value: string) { + this.type = type; + this.value = value; + } +} \ No newline at end of file diff --git a/src/data/request/account/LoginRequest.ts b/src/data/request/account/LoginRequest.ts new file mode 100644 index 0000000..1363afb --- /dev/null +++ b/src/data/request/account/LoginRequest.ts @@ -0,0 +1,15 @@ +export class LoginRequest { + type: 'email' | 'accountId'; + id: string; + password: string; + + constructor( + type: 'email' | 'accountId', + id: string, + password: string + ) { + this.type = type; + this.id = id; + this.password = password; + } +} \ No newline at end of file diff --git a/src/data/request/account/SendVerificationCodeRequest.ts b/src/data/request/account/SendVerificationCodeRequest.ts new file mode 100644 index 0000000..5c4841c --- /dev/null +++ b/src/data/request/account/SendVerificationCodeRequest.ts @@ -0,0 +1,7 @@ +export class SendVerificationCodeRequest { + email: string; + + constructor(email: string) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/data/request/account/SignupRequest.ts b/src/data/request/account/SignupRequest.ts new file mode 100644 index 0000000..abe402c --- /dev/null +++ b/src/data/request/account/SignupRequest.ts @@ -0,0 +1,15 @@ +export class SignupRequest { + accountId: string; + email: string; + name: string; + nickname: string; + password: string; + + constructor(accountId: string, email: string, name: string, nickname: string, password: string) { + this.accountId = accountId; + this.email = email; + this.name = name; + this.nickname = nickname; + this.password = password; + } +} \ No newline at end of file diff --git a/src/data/request/account/VerifyCodeRequest.ts b/src/data/request/account/VerifyCodeRequest.ts new file mode 100644 index 0000000..9980511 --- /dev/null +++ b/src/data/request/account/VerifyCodeRequest.ts @@ -0,0 +1,9 @@ +export class VerifyCodeRequest { + email: string; + code: string; + + constructor(email: string, code: string) { + this.email = email; + this.code = code; + } +} \ No newline at end of file diff --git a/src/data/request/index.ts b/src/data/request/index.ts new file mode 100644 index 0000000..1663b31 --- /dev/null +++ b/src/data/request/index.ts @@ -0,0 +1,5 @@ +export * from './account/CheckDuplicationRequest'; +export * from './account/SendVerificationCodeRequest'; +export * from './account/VerifyCodeRequest'; +export * from './account/SignupRequest'; +export * from './account/LoginRequest'; \ No newline at end of file diff --git a/src/data/response/BaseResponse.ts b/src/data/response/BaseResponse.ts new file mode 100644 index 0000000..fe27c26 --- /dev/null +++ b/src/data/response/BaseResponse.ts @@ -0,0 +1,4 @@ +export class BaseResponse { + message?: string; + error?: string; +} \ No newline at end of file diff --git a/src/data/response/account/CheckDuplicationResponse.ts b/src/data/response/account/CheckDuplicationResponse.ts new file mode 100644 index 0000000..fd3f533 --- /dev/null +++ b/src/data/response/account/CheckDuplicationResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class CheckDuplicationResponse extends BaseResponse{ + isDuplicated!: boolean; +} \ No newline at end of file diff --git a/src/data/response/account/LoginResponse.ts b/src/data/response/account/LoginResponse.ts new file mode 100644 index 0000000..12f4bd8 --- /dev/null +++ b/src/data/response/account/LoginResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class LoginResponse extends BaseResponse { + success!: boolean; +} \ No newline at end of file diff --git a/src/data/response/account/SendVerificationCodeResponse.ts b/src/data/response/account/SendVerificationCodeResponse.ts new file mode 100644 index 0000000..9a7d6f8 --- /dev/null +++ b/src/data/response/account/SendVerificationCodeResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class SendVerificationCodeResponse extends BaseResponse { + success!: boolean; +} \ No newline at end of file diff --git a/src/data/response/account/SignupResponse.ts b/src/data/response/account/SignupResponse.ts new file mode 100644 index 0000000..f801ba1 --- /dev/null +++ b/src/data/response/account/SignupResponse.ts @@ -0,0 +1,6 @@ +import { BaseResponse } from "../BaseResponse"; + +export class SignupResponse extends BaseResponse { + success!: boolean; + +} \ No newline at end of file diff --git a/src/data/response/account/VerifyCodeResponse.ts b/src/data/response/account/VerifyCodeResponse.ts new file mode 100644 index 0000000..d1b0965 --- /dev/null +++ b/src/data/response/account/VerifyCodeResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class VerifyCodeResponse extends BaseResponse { + verified!: boolean; +} \ No newline at end of file diff --git a/src/data/response/index.ts b/src/data/response/index.ts new file mode 100644 index 0000000..1473a61 --- /dev/null +++ b/src/data/response/index.ts @@ -0,0 +1,5 @@ +export * from './account/CheckDuplicationResponse'; +export * from './account/SendVerificationCodeResponse'; +export * from './account/VerifyCodeResponse'; +export * from './account/SignupResponse'; +export * from './account/LoginResponse'; \ No newline at end of file diff --git a/src/network/AccountNetwork.ts b/src/network/AccountNetwork.ts new file mode 100644 index 0000000..860863d --- /dev/null +++ b/src/network/AccountNetwork.ts @@ -0,0 +1,41 @@ +import { + CheckDuplicationRequest, + SendVerificationCodeRequest, + VerifyCodeRequest, + SignupRequest, + LoginRequest +} from "@/data/request"; +import { + CheckDuplicationResponse, + SendVerificationCodeResponse, + VerifyCodeResponse, + SignupResponse, + LoginResponse +} from "@/data/response"; +import { BaseNetwork } from "./BaseNetwork"; + +export class AccountNetwork extends BaseNetwork { + private baseUrl = "/account"; + + async checkDuplication(data: CheckDuplicationRequest) { + const { type, value } = data; + + return await this.instance.get(`${this.baseUrl}/check-duplication?type=${type}&value=${value}`); + } + + async sendVerificationCode(data: SendVerificationCodeRequest) { + return await this.instance.post(this.baseUrl + "/send-verification-code", data); + } + + async verifyCode(data: VerifyCodeRequest) { + return await this.instance.post(this.baseUrl + "/verify-code", data); + } + + async signup(data: SignupRequest) { + return await this.instance.post(this.baseUrl + "/signup", data); + } + + async login(data: LoginRequest) { + return await this.instance.post(this.baseUrl + "/login", data); + } +} \ No newline at end of file diff --git a/src/network/BaseNetwork.ts b/src/network/BaseNetwork.ts new file mode 100644 index 0000000..632f92f --- /dev/null +++ b/src/network/BaseNetwork.ts @@ -0,0 +1,71 @@ +import axios from 'axios'; +import type { + AxiosInstance, + AxiosRequestConfig, + AxiosError, + AxiosResponse, +} from "axios"; + +export class BaseNetwork { + protected instance: AxiosInstance; + + constructor() { + this.instance = axios.create({ + baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000", + timeout: 10_000, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, + }); + + this.setInterceptors(); + } + + /** + * 요청/응답 인터셉터 설정 + */ + protected setInterceptors() { + // ★ 요청 인터셉터 + this.instance.interceptors.request.use( + (config) => { + // 예: 자동 토큰 추가 + // const token = localStorage.getItem("token"); + // if (token) { + // config.headers.Authorization = `Bearer ${token}`; + // } + + return config; + }, + (error: AxiosError) => Promise.reject(error) + ); + + // ★ 응답 인터셉터 + this.instance.interceptors.response.use( + (response: AxiosResponse) => response, + (error: AxiosError) => { + const message = + (error.response?.data as any)?.message || + error.message || + "Unknown error"; + + return Promise.reject({ + status: error.response?.status, + message, + raw: error, + }); + } + ); + } + + /** + * 기본 CRUD 메서드 + */ + protected async get(url: string, config?: AxiosRequestConfig) { + return await this.instance.get(url, config); + } + + protected async post(url: string, data?: any, config?: AxiosRequestConfig) { + return await this.instance.post(url, data, config); + } +} \ No newline at end of file diff --git a/src/request/user/LoginRequest.ts b/src/request/user/LoginRequest.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/ui/component/modal/EmailVerificationModal.tsx b/src/ui/component/modal/EmailVerificationModal.tsx index 35d3cd0..5912c43 100644 --- a/src/ui/component/modal/EmailVerificationModal.tsx +++ b/src/ui/component/modal/EmailVerificationModal.tsx @@ -1,98 +1,156 @@ -import { Button } from "@/components/ui/button"; -import { useModal } from "@/hooks/useModal"; -import { useState, type ReactNode } from "react"; +import { useState, type ReactNode, useEffect } from "react"; import { EmailVerificationSchema } from "@/data/form"; import { Controller, useForm } from "react-hook-form"; import z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; +import { AccountNetwork } from "@/network/AccountNetwork"; +import { SendVerificationCodeRequest } from "@/data/request"; +import { toast } from "sonner"; +import { DialogHeader, Dialog, DialogTrigger, DialogContent, DialogTitle, DialogFooter } from "@/components/ui/dialog"; type EmailVerificationModalProps = { trigger: ReactNode; email: string; - handler: () => void; + open: boolean; + setOpen: (open: boolean) => void; + onVerifySuccess: () => Promise; } -export default function EmailVerificationModal( - { trigger, email, handler } : EmailVerificationModalProps -) { - const [isVerifying, setIsVerifying] = useState(false); - const { open, close, Modal } = useModal(); +export default function EmailVerificationModal({ + trigger, + email, + open, + setOpen, + onVerifySuccess +}: EmailVerificationModalProps) { + const [isLoading, setIsLoading] = useState(false); + const accountNetwork = new AccountNetwork(); const emailVerificationForm = useForm>({ resolver: zodResolver(EmailVerificationSchema), - defaultValues: { - email: email, - verificationCode: "" - } + mode: "onSubmit", + defaultValues: { email: "", code: "" } }); - const handleOnOpenChange = (isOpen: boolean) => { - if (!isVerifying) { - if (isOpen) { - open(); + useEffect(() => { + if (open) { + init(); + } else { + emailVerificationForm.setValue("code", ""); + } + }, [open]); + + const init = async () => { + try { + const data = new SendVerificationCodeRequest(email); + const result = await accountNetwork.sendVerificationCode(data); + if (!result.data.success) { + openErrorToast(); } else { - emailVerificationForm.clearErrors(); - close(); + emailVerificationForm.setValue("email", email); } + } catch (err) { + openErrorToast(); } } - const verifyCode = () => { - console.log(emailVerificationForm.getValues("verificationCode")); + const openErrorToast = () => { + toast.error("이메일 인증 코드 발송에 실패하였습니다.", { + duration: 3000, + onDismiss: () => setOpen(false) + }); } - const handleOnOTPComplete = (otp: string) => { - setIsVerifying(true); - emailVerificationForm.setValue("verificationCode", otp, { shouldValidate: true }); - emailVerificationForm.handleSubmit(verifyCode)(); + const onSubmit = async () => { + if (isLoading) return; + + const { email, code } = emailVerificationForm.getValues(); + + const verifyCodePromise = accountNetwork.verifyCode({ email, code }); + + toast.promise( + verifyCodePromise, + { + loading: "이메일 인증 확인 중입니다.", + success: (res) => res.data.verified ? "이메일 인증이 완료되었습니다." : "잘못된 인증 코드입니다.", + error: "이메일 인증에 실패하였습니다.", + } + ); + + verifyCodePromise.then((res) => { + if (res.data.verified) { + onVerifySuccess(); + } + }) + } - return ( - - ( - - - 인증 번호 - -
{ + if (otp.length < 6) return; + setIsLoading(true); + emailVerificationForm.setValue("code", otp, { shouldValidate: true }); + const isValid = await emailVerificationForm.trigger("code"); + if (isValid) await onSubmit(); + setIsLoading(false); + } + + return ( + + {trigger} + + + 이메일 인증 + + +
+ + ( + - - - - - - - - - - -
+ + 인증 번호 + +
+ field.onChange(value)} + onBlur={field.onBlur} + required + > + + + + + + + + + +
-
- )} - /> -
- - } - trigger={trigger} - title="이메일 인증" - footer={null} - />); -} \ No newline at end of file + )} + /> + + + + + {/* 필요시 footer 버튼 */} + + + + ); +} diff --git a/src/ui/page/login/LoginPage.tsx b/src/ui/page/login/LoginPage.tsx index 033d6aa..6b72a05 100644 --- a/src/ui/page/login/LoginPage.tsx +++ b/src/ui/page/login/LoginPage.tsx @@ -4,20 +4,25 @@ import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { PageRouting } from '@/data/RoutingData'; import * as z from 'zod'; import { Separator } from '@/components/ui/separator'; +import { Validator } from '@/util/Validator'; +import { LoginRequest } from '@/data/request/account/LoginRequest'; +import { AccountNetwork } from '@/network/AccountNetwork'; +import { toast } from 'sonner'; export default function LoginPage() { + const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); - + const accountNetwork = new AccountNetwork(); const loginForm = useForm>({ resolver: zodResolver(LoginSchema), defaultValues: { - email: "", + id: "", password: "" } }); @@ -30,8 +35,35 @@ export default function LoginPage() { navigate(PageRouting["RESET_PASSWORD"].path); }, []); + const moveToMainPage = useCallback(() => { + + }, []); + + // TODO 33 로그인 기능 구현 const login = async () => { - // + if (isLoading) return; + + const { id, password } = loginForm.getValues(); + const type = Validator.isEmail(id) ? 'email' : 'accountId'; + + const data: LoginRequest = new LoginRequest(type, id, password); + + const loginPromise = accountNetwork.login(data); + + toast.promise( + loginPromise, + { + loading: "로그인 중입니다.", + success: (res) => res.data.success ? "로그인이 완료되었습니다." : res.data.message, + error: "로그인에 실패하였습니다." + } + ); + + loginPromise.then((res) => { + if (res.data.success) { + moveToMainPage(); + } + }); } const TextSeparator = ({ text }: { text: string }) => { @@ -53,15 +85,15 @@ export default function LoginPage() {
( - 이메일 + 이메일 @@ -104,7 +136,7 @@ export default function LoginPage() { type="submit" form="form-login" disabled={ - (loginForm.getValues("email").trim.length < 1) + (loginForm.getValues("id").trim.length < 1) && (loginForm.getValues("password").trim.length < 1) }> 로그인 diff --git a/src/ui/page/resetPassword/ResetPasswordPage.tsx b/src/ui/page/resetPassword/ResetPasswordPage.tsx index 3b25056..bafff7c 100644 --- a/src/ui/page/resetPassword/ResetPasswordPage.tsx +++ b/src/ui/page/resetPassword/ResetPasswordPage.tsx @@ -1,6 +1,6 @@ import { Card, CardContent, CardHeader, CardFooter } from '@/components/ui/card'; import { ResetPasswordSchema } from '@/data/form'; -import { Field, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator } from '@/components/ui/field'; +import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -9,8 +9,6 @@ import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { PageRouting } from '@/data/RoutingData'; import * as z from 'zod'; -import { Separator } from '@/components/ui/separator'; -import { Label } from '@/components/ui/label'; export default function ResetPasswordPage() { const navigate = useNavigate(); @@ -18,8 +16,7 @@ export default function ResetPasswordPage() { const loginForm = useForm>({ resolver: zodResolver(ResetPasswordSchema), defaultValues: { - email: "", - resetCode: "" + email: "" } }); @@ -63,7 +60,6 @@ export default function ResetPasswordPage() { form="form-reset-password" disabled={ (loginForm.getValues("email").trim.length < 1) - && (loginForm.getValues("resetCode").trim.length < 1) }> 인증 번호 발송 diff --git a/src/ui/page/signup/SignUpPage.tsx b/src/ui/page/signup/SignUpPage.tsx index e05dd5b..e186306 100644 --- a/src/ui/page/signup/SignUpPage.tsx +++ b/src/ui/page/signup/SignUpPage.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; -import { Label } from '@/components/ui/label'; +import { useCallback, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -8,21 +7,32 @@ import { CardHeader, CardFooter } from '@/components/ui/card'; -import { Field, FieldError, FieldGroup, FieldLabel, FieldLegend } from '@/components/ui/field'; +import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'; import { SignUpSchema } from '@/data/form'; import { Controller, useForm } from 'react-hook-form'; import * as z from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import EmailVerificationModal from '@/ui/component/modal/EmailVerificationModal'; +import { CheckDuplicationRequest, SignupRequest } from '@/data/request'; +import { AccountNetwork } from '@/network/AccountNetwork'; +import { toast } from 'sonner'; +import { useNavigate } from 'react-router-dom'; +import { PageRouting } from '@/data/RoutingData'; export default function SignUpPage() { - const [isCheckedEmailDuplication, setIsCheckdEmailDupliation] = useState(false); - const [isEmailVerificated, setIsEmailVerificated] = useState(false); + const [emailVerificationModalOpen, setEmailVerificationModalOpen] = useState(false); + const [isCheckedEmailDuplication, setIsCheckedEmailDuplication] = useState(false); + const [isCheckedAccountIdDuplication, setIsCheckedAccountIdDuplication] = useState(false); const [duplicationCheckedEmail, setDuplicationCheckedEmail] = useState(""); + const [duplicationCheckedAccountId, setDuplicationCheckedAccountId] = useState(""); + + const accountNetwork = new AccountNetwork(); + const navigate = useNavigate(); const signUpForm = useForm>({ resolver: zodResolver(SignUpSchema), defaultValues: { + accountId: "", email: "", password: "", passwordConfirm: "", @@ -31,28 +41,81 @@ export default function SignUpPage() { } }); + const goToLogin = useCallback(() => { + navigate(PageRouting["LOGIN"].path); + }, [navigate]); + + const checkDuplication = async (type: 'email' | 'accountId', value: string) => { + const data: CheckDuplicationRequest = new CheckDuplicationRequest(type, value); + return await accountNetwork.checkDuplication(data); + } + + const signup = async () => { + const { email, accountId, name, nickname, password } = signUpForm.getValues(); + const data: SignupRequest = new SignupRequest(accountId, email, name, nickname, password); + + const signupPromise = accountNetwork.signup(data); + + toast.promise( + signupPromise, + { + loading: "회원가입 진행 중입니다.", + success: (res) => { + if (!res.data.success) return "회원가입에 실패하였습니다.\n잠시 후 다시 시도해주십시오."; + + return + }, + error: "회원가입에 실패하였습니다.\n잠시 후 다시 시도해주십시오.", + } + ); + } + + const handleOnChangeAccountId = (e: React.ChangeEvent) => { + setIsCheckedAccountIdDuplication( + e.currentTarget.value === duplicationCheckedAccountId + ); + } + const handleOnChangeEmail = (e: React.ChangeEvent) => { - setIsCheckdEmailDupliation( + setIsCheckedEmailDuplication( e.currentTarget.value === duplicationCheckedEmail ); - // setIsEmailVerificated( - // e.currentTarget.value === duplicationCheckedEmail - // ); } - const handleEmailDuplicationCheckButtonClick = () => { - console.log(signUpForm.getValues("email")); - setDuplicationCheckedEmail(signUpForm.getValues("email")); - setIsCheckdEmailDupliation(true); + const handleDuplicationCheckButtonClick = async (type: 'email' | 'accountId') => { + const value = signUpForm.getValues(type); + const duplicatedMessage = type === 'email' ? '사용할 수 없는 이메일입니다.' : '사용할 수 없는 아이디입니다.'; + + if (!value) return; + + const isDuplicated = (await checkDuplication(type, value)).data.isDuplicated; + + if (isDuplicated) { + signUpForm.setError(type, { message: duplicatedMessage }); + } else { + signUpForm.clearErrors(type); + if (type === 'email') { + setIsCheckedEmailDuplication(true); + setDuplicationCheckedEmail(value); + } else { + setIsCheckedAccountIdDuplication(true); + setDuplicationCheckedAccountId(value); + } + } } - const handleOnSubmitSignUpForm = () => { + const handleOnSignUpButtonClick = () => { + if (!isCheckedAccountIdDuplication) { + signUpForm.setError("accountId", { message: "아이디 중복 확인이 필요합니다."}); + return; + } + if (!isCheckedEmailDuplication) { signUpForm.setError("email", { message: "이메일 중복 확인이 필요합니다." }); + return; } - // if (!isEmailVerificated) { - // signUpForm.setError("email", { message: "이메일 인증이 완료되지 않았습니다." }); - // } + + setEmailVerificationModalOpen(true); } return ( @@ -60,8 +123,34 @@ export default function SignUpPage() { 회원가입 - + + ( + + 아이디 +
+ + +
+ { isCheckedAccountIdDuplication &&

사용할 수 있는 아이디입니다

} + +
+ )} + /> + { isCheckedEmailDuplication &&

사용할 수 있는 이메일입니다

}
)} @@ -155,16 +247,31 @@ export default function SignUpPage() { + } email={duplicationCheckedEmail} - handler={() => {}} + open={emailVerificationModalOpen} // ✅ 부모 상태 연결 + setOpen={setEmailVerificationModalOpen} // ✅ 부모 상태 변경 함수 전달 + onVerifySuccess={signup} // ✅ 인증 성공 시 signup 호출 /> - ); +} + +function SuccessToast({ onClose }: { onClose: () => void }) { + useEffect(() => { + const timer = setTimeout(() => onClose(), 3000); // 3초 후 이동 + return () => clearTimeout(timer); + }, [onClose]); + + return ( +
+ 회원가입 성공! + +
+ ); } \ No newline at end of file diff --git a/src/util/Validator.ts b/src/util/Validator.ts new file mode 100644 index 0000000..e0518a6 --- /dev/null +++ b/src/util/Validator.ts @@ -0,0 +1,5 @@ +export class Validator { + static isEmail = (value: string): boolean => { + return /^[^\s@]+@[^\s@]+\.[*\s@]+$/.test(value); + } +} \ No newline at end of file