diff --git a/src/ui/page/account/resetPassword/ResetPasswordPage.tsx b/src/ui/page/account/resetPassword/ResetPasswordPage.tsx new file mode 100644 index 0000000..13930ca --- /dev/null +++ b/src/ui/page/account/resetPassword/ResetPasswordPage.tsx @@ -0,0 +1,415 @@ +import { Card, CardContent, CardHeader, CardFooter } from '@/components/ui/card'; +import { ResetPasswordSchema } from '@/data/form'; +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 { useState, useCallback } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { PageRouting } from '@/const/PageRouting'; +import { Stepper, StepperContent, StepperIndicator, StepperItem, StepperNav, StepperPanel, StepperSeparator, StepperTrigger } from '@/components/ui/stepper'; +import * as z from 'zod'; +import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'; +import { Validator } from '@/util/Validator'; +import { Eye, EyeOff, LoaderCircleIcon, CircleCheckBigIcon } from 'lucide-react'; +import { AccountNetwork } from '@/network/AccountNetwork'; +import { Label } from '@/components/ui/label'; + +const steps = [1, 2, 3, 4]; + +export default function ResetPasswordPage() { + const [currentStep, setCurrentStep] = useState(4); + const [showPassword, setShowPassword] = useState(false); + const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const accountNetwork = new AccountNetwork(); + + const resetPasswordForm = useForm>({ + resolver: zodResolver(ResetPasswordSchema), + defaultValues: { + email: "", + code: "", + password: "", + passwordConfirm: "" + } + }); + + const { email, code, password, passwordConfirm } = resetPasswordForm.watch(); + + const moveToLoginPage = useCallback(() => { + navigate(PageRouting["LOGIN"].path); + }, []); + + const handleClickFirstStepButton = async () => { + if (isLoading) return; + if (!email || email.trim().length === 0) { + resetPasswordForm.setError('email', { + type: 'manual', + message: '이메일을 입력해주십시오' + }); + return; + } + const isEmailValid = await resetPasswordForm.trigger('email'); + if (!isEmailValid) { + resetPasswordForm.setError('email', { + type: 'validate', + message: '이메일 형식이 올바르지 않습니다' + }); + return; + } + + setIsLoading(true); + + try { + + const response = await accountNetwork.sendResetPasswordCode({ email: email }); + const resData = response.data; + + if (!resData.success) { + resetPasswordForm.setError('email', { + message: '서버 오류로 코드 발송에 실패하였습니다. 잠시 후 다시 시도해주십시오.' + }); + return; + } + + setCurrentStep(current => current + 1); + + } catch (err) { + resetPasswordForm.setError('email', { + message: '서버 오류로 코드 발송에 실패하였습니다. 잠시 후 다시 시도해주십시오.' + }); + } finally { + setIsLoading(false); + } + } + + const handleSecondStepOTPCompleted = async () => { + if (isLoading) return; + if (!Validator.validatePasswordFormat(code)) { + resetPasswordForm.setError('code', { + type: 'pattern', + message: '올바른 코드 형식이 아닙니다.' + }); + return; + } + + const data = { + email: email, + code: code + } + + setIsLoading(true); + + try { + const response = await accountNetwork.verifyResetPasswordCode(data); + const resData = response.data; + + if (!resData.success || !resData.verified) { + resetPasswordForm.setError('code', { + type: 'value', + message: resData.error + }); + return; + } + + setCurrentStep(current => current + 1); + } catch (err) { + resetPasswordForm.setError('code', { + type: 'value', + message: '서버 오류로 코드 인증에 실패하였습니다. 잠시 후 다시 시도해주십시오.' + }); + } finally { + setIsLoading(false); + } + } + + const handleThirdStepButton = async () => { + if (isLoading) return; + const passwordValid = await resetPasswordForm.trigger('password'); + if (!passwordValid) return; + + const passwordConfirmValid = await resetPasswordForm.trigger('passwordConfirm'); + if (!passwordConfirmValid) return; + + const data = { + email: email, + password: password + } + + setIsLoading(true); + + try { + const response = await accountNetwork.resetPassword(data); + const resData = response.data; + + if (!resData.success) { + resetPasswordForm.setError('password', { + message: '서버 오류로 비밀번호 변경에 실패하였습니다. 잠시 후 다시 시도해주십시오.' + }); + return; + } + + setCurrentStep(current => current + 1); + } catch (err) { + resetPasswordForm.setError('password', { + message: '서버 오류로 비밀번호 변경에 실패하였습니다. 잠시 후 다시 시도해주십시오.' + }); + } finally { + setIsLoading(false); + } + + } + + return ( + + }} + > + + + 비밀번호 초기화 + + + + {steps.map((step) => ( + + + + {step} + + + { + steps.length > step + && + } + + ))} + + + + + + + 이메일 + ( + <> + { + if (e.key === 'Enter') { + e.preventDefault(); + if (email.length === 0) return; + handleClickFirstStepButton(); + } + }} + /> + + + )} + /> + + + + ( +
+ 코드 입력 +
+ field.onChange(value)} + onBlur={field.onBlur} + required + > + + { + [0, 1, 2, 3, 4, 5, 6, 7].map((idx) => ( + + )) + } + + +
+ +
+ ) + } + /> +
+ +
+ 새 비밀번호 + ( + <> +
+ + +
+ + + )} + /> + 새 비밀번호 확인 + ( + <> +
+ + +
+ + + )} + /> +
+
+ +
+ + +
+
+
+
+ + + +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ +
+
+
+
+
+
+ ) +} \ No newline at end of file