All checks were successful
Test CI / build (push) Successful in 19s
- 로그인 요청 후 응답을 AuthData 로 저장 로직 구현 - Access/Refresh 토큰 구현 중
277 lines
11 KiB
TypeScript
277 lines
11 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardFooter
|
|
} from '@/components/ui/card';
|
|
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 '@/const/PageRouting';
|
|
|
|
export default function SignUpPage() {
|
|
const [emailVerificationModalOpen, setEmailVerificationModalOpen] = useState<boolean>(false);
|
|
const [isCheckedEmailDuplication, setIsCheckedEmailDuplication] = useState<boolean>(false);
|
|
const [isCheckedAccountIdDuplication, setIsCheckedAccountIdDuplication] = useState<boolean>(false);
|
|
const [duplicationCheckedEmail, setDuplicationCheckedEmail] = useState<string>("");
|
|
const [duplicationCheckedAccountId, setDuplicationCheckedAccountId] = useState<string>("");
|
|
|
|
const accountNetwork = new AccountNetwork();
|
|
const navigate = useNavigate();
|
|
|
|
const signUpForm = useForm<z.infer<typeof SignUpSchema>>({
|
|
resolver: zodResolver(SignUpSchema),
|
|
defaultValues: {
|
|
accountId: "",
|
|
email: "",
|
|
password: "",
|
|
passwordConfirm: "",
|
|
name: "",
|
|
nickname: "",
|
|
}
|
|
});
|
|
|
|
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 <SuccessToast onClose={goToLogin} />
|
|
},
|
|
error: "회원가입에 실패하였습니다.\n잠시 후 다시 시도해주십시오.",
|
|
}
|
|
);
|
|
}
|
|
|
|
const handleOnChangeAccountId = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setIsCheckedAccountIdDuplication(
|
|
e.currentTarget.value === duplicationCheckedAccountId
|
|
);
|
|
}
|
|
|
|
const handleOnChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setIsCheckedEmailDuplication(
|
|
e.currentTarget.value === duplicationCheckedEmail
|
|
);
|
|
}
|
|
|
|
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 handleOnSignUpButtonClick = () => {
|
|
if (!isCheckedAccountIdDuplication) {
|
|
signUpForm.setError("accountId", { message: "아이디 중복 확인이 필요합니다."});
|
|
return;
|
|
}
|
|
|
|
if (!isCheckedEmailDuplication) {
|
|
signUpForm.setError("email", { message: "이메일 중복 확인이 필요합니다." });
|
|
return;
|
|
}
|
|
|
|
setEmailVerificationModalOpen(true);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col justify-center items-center">
|
|
<Card className="w-md pl-2 pr-2">
|
|
<CardHeader>회원가입</CardHeader>
|
|
<CardContent>
|
|
<form id="form-signup">
|
|
<FieldGroup>
|
|
<Controller
|
|
name="accountId"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-account-id">아이디</FieldLabel>
|
|
<div id="accountId-group" className="w-full flex flex-row justify-between gap-2.5">
|
|
<Input
|
|
{...field}
|
|
id="form-signup-account-id"
|
|
aria-invalid={fieldState.invalid}
|
|
onInput={handleOnChangeAccountId}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleDuplicationCheckButtonClick('accountId')}
|
|
className="bg-indigo-500 hover:bg-indigo-400"
|
|
>
|
|
중복 확인
|
|
</Button>
|
|
</div>
|
|
{ isCheckedAccountIdDuplication && <p className="text-green-500 text-sm font-normal">사용할 수 있는 아이디입니다</p> }
|
|
<FieldError errors={[fieldState.error]}/>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="name"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-name">이름</FieldLabel>
|
|
<Input
|
|
{...field}
|
|
id="form-signup-name"
|
|
aria-invalid={fieldState.invalid}
|
|
/>
|
|
<FieldError errors={[fieldState.error]} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="nickname"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-nickname">닉네임</FieldLabel>
|
|
<Input
|
|
{...field}
|
|
id="form-signup-nickname"
|
|
aria-invalid={fieldState.invalid}
|
|
/>
|
|
<FieldError errors={[fieldState.error]} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="email"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-email">이메일</FieldLabel>
|
|
<div id="email-group" className="w-full flex flex-row justify-between gap-2.5">
|
|
<Input
|
|
{...field}
|
|
id="form-signup-email"
|
|
aria-invalid={fieldState.invalid}
|
|
placeholder="example@domain.com"
|
|
type="email"
|
|
onInput={handleOnChangeEmail}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleDuplicationCheckButtonClick('email')}
|
|
className="bg-indigo-500 hover:bg-indigo-400"
|
|
>
|
|
중복 확인
|
|
</Button>
|
|
</div>
|
|
{ isCheckedEmailDuplication && <p className="text-green-500 text-sm font-normal">사용할 수 있는 이메일입니다</p> }
|
|
<FieldError errors={[fieldState.error]}/>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="password"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-password">비밀번호</FieldLabel>
|
|
<Input
|
|
{...field}
|
|
id="form-signup-password"
|
|
aria-invalid={fieldState.invalid}
|
|
type="password"
|
|
/>
|
|
<FieldError errors={[fieldState.error]} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="passwordConfirm"
|
|
control={signUpForm.control}
|
|
render={({ field, fieldState }) => (
|
|
<Field data-invalid={fieldState.invalid}>
|
|
<FieldLabel htmlFor="form-signup-password-confirm">비밀번호 확인</FieldLabel>
|
|
<Input
|
|
{...field}
|
|
id="form-signup-password-confirm"
|
|
aria-invalid={fieldState.invalid}
|
|
type="password"
|
|
/>
|
|
<FieldError errors={[fieldState.error]} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
</FieldGroup>
|
|
</form>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<EmailVerificationModal
|
|
trigger={
|
|
<Button type="button" onClick={handleOnSignUpButtonClick} className="0">
|
|
회원가입
|
|
</Button>
|
|
}
|
|
email={duplicationCheckedEmail}
|
|
open={emailVerificationModalOpen} // ✅ 부모 상태 연결
|
|
setOpen={setEmailVerificationModalOpen} // ✅ 부모 상태 변경 함수 전달
|
|
onVerifySuccess={signup} // ✅ 인증 성공 시 signup 호출
|
|
/>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SuccessToast({ onClose }: { onClose: () => void }) {
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => onClose(), 3000); // 3초 후 이동
|
|
return () => clearTimeout(timer);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<div className="w-full flex flex-row justify-between items-center">
|
|
회원가입 성공!
|
|
<button onClick={onClose}>로그인 페이지로 이동</button>
|
|
</div>
|
|
);
|
|
} |