diff --git a/.yarn/cache/fsevents-patch-21ad2b1333-8.zip b/.yarn/cache/fsevents-patch-21ad2b1333-8.zip new file mode 100644 index 00000000..c6a96dfc Binary files /dev/null and b/.yarn/cache/fsevents-patch-21ad2b1333-8.zip differ diff --git a/src/App.tsx b/src/App.tsx index 8562f280..e5c7f820 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,6 @@ import OwnerLayout from 'layout/OwnerLayout'; import CoopLayout from 'layout/CoopLayout'; import Login from 'page/Auth/Login'; import Signup from 'page/Auth/Signup'; -import FindPassword from 'page/Auth/FindPassword/SendAuthNumber'; -import NewPassword from 'page/Auth/FindPassword/NewPassword'; -import CompleteChangePassword from 'page/Auth/FindPassword/CompleteChangePassword'; import AuthLayout from 'layout/AuthLayout'; import MyShopPage from 'page/MyShopPage'; import ShopRegistration from 'page/ShopRegistration'; @@ -22,6 +19,8 @@ import useUserTypeStore from 'store/useUserTypeStore'; import AddingEvent from 'page/ManageEvent/AddingEvent'; import ModifyEvent from 'page/ManageEvent/ModifyEvent'; import LogPage from 'component/common/PageLog'; +import CommonLayout from 'page/Auth/components/Common'; +import FindPassword from 'page/Auth/FindPassword'; interface ProtectedRouteProps { userTypeRequired: UserType; @@ -74,12 +73,12 @@ function App() { }> }> } /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + - } /> - } /> diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 1e670088..937bd61a 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -1,5 +1,6 @@ import { accessClient, client } from 'api'; import { + CertificationResponse, LoginParams, LoginResponse, OwnerResponse, UserTypeResponse, } from 'model/auth'; @@ -34,3 +35,16 @@ export const findPassword = ({ }); export const newPassword = ({ address, password }: { address: string, password: string }) => client.put('/owners/password/reset', { address, password }); + +export const sendVerifyCode = (phone_number: string) => client.post('/owners/password/reset/verification/sms', { + phone_number, +}); + +export const verifyCode = ({ phone_number, certification_code } : { phone_number:string, certification_code:string }) => client.post('/owners/password/reset/send/sms', { + phone_number, certification_code, +}); + +export const changePassword = ({ phone_number, password } : { phone_number:string, password:string }) => client.put('/owners/password/reset/sms', { + phone_number, + password, +}); diff --git a/src/assets/svg/auth/done.svg b/src/assets/svg/auth/done.svg new file mode 100644 index 00000000..bda159b9 --- /dev/null +++ b/src/assets/svg/auth/done.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/model/auth/index.ts b/src/model/auth/index.ts index c2d410aa..5edb3e1a 100644 --- a/src/model/auth/index.ts +++ b/src/model/auth/index.ts @@ -77,3 +77,29 @@ export type User = z.infer; export interface LoginForm extends LoginParams { isAutoLogin: boolean; } + +export interface CertificationResponse { + token: string; +} + +export interface ChangePasswordForm { + password: string; + passwordCheck: string; + phone_number: string; +} + +interface FindPassword { + phone_number: string; + certification_code: string; + password: string; +} + +export interface Register extends FindPassword { + company_number: string, + name: string, + shop_id: number, + shop_name: string, + attachment_urls: { + file_url: string + }[], +} diff --git a/src/page/Auth/FindPassword/ChangePassword/index.tsx b/src/page/Auth/FindPassword/ChangePassword/index.tsx new file mode 100644 index 00000000..4ccefd5c --- /dev/null +++ b/src/page/Auth/FindPassword/ChangePassword/index.tsx @@ -0,0 +1,76 @@ +import { useEffect } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useOutletContext } from 'react-router-dom'; +import { ChangePasswordForm } from 'model/auth'; +import { OutletProps } from 'page/Auth/FindPassword/entity'; +import { ReactComponent as Warning } from 'assets/svg/auth/warning.svg'; +import styles from 'page/Auth/FindPassword/index.module.scss'; + +const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,18}$/; + +export default function ChangePassword() { + const method = useFormContext(); + const { + register, formState: { errors, isValid }, getValues, + } = method; + const steps: OutletProps = useOutletContext(); + const { setIsStepComplete } = steps; + + useEffect(() => { + if (isValid) { + setIsStepComplete(true); + } else { + setIsStepComplete(false); + } + }); + + return ( + + + 새 비밀번호 + + {errors.password + ? ( + + + {errors.password.message} + + ) : * 특수문자 포함 영어와 숫자 6~18 자리} + + + + 새 비밀번호 확인 + value === getValues('password') || '비밀번호가 일치하지 않습니다.', + })} + /> + + {errors.passwordCheck + && ( + + + {errors.passwordCheck.message} + + )} + + + ); +} diff --git a/src/page/Auth/FindPassword/CompleteChangePassword/CompleteChangePassword.module.scss b/src/page/Auth/FindPassword/CompleteChangePassword/CompleteChangePassword.module.scss deleted file mode 100644 index bf6a6cc7..00000000 --- a/src/page/Auth/FindPassword/CompleteChangePassword/CompleteChangePassword.module.scss +++ /dev/null @@ -1,51 +0,0 @@ -.template { - display: flex; - justify-content: center; - align-items: center; - padding-top: 220px; - padding-bottom: 120px; - flex-direction: column; -} - -.circle-icon { - width: 160px; - height: 160px; - border-radius: 160px; - border: 8px solid #f7941e; - display: flex; - justify-content: center; - align-items: center; -} - -.content { - font-style: normal; - line-height: normal; - - &__title { - margin-top: 70px; - color: #175c8e; - text-align: center; - font-size: 36px; - font-weight: 700; - } - - &__description { - margin-top: 20px; - color: #858585; - text-align: center; - font-size: 18px; - font-weight: 400; - } - - &__button { - margin-top: 80px; - width: 368px; - height: 48px; - background-color: #175c8e; - color: white; - font-size: 16px; - font-style: normal; - font-weight: 500; - cursor: pointer; - } -} diff --git a/src/page/Auth/FindPassword/CompleteChangePassword/index.tsx b/src/page/Auth/FindPassword/CompleteChangePassword/index.tsx deleted file mode 100644 index 25cb16ba..00000000 --- a/src/page/Auth/FindPassword/CompleteChangePassword/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ReactComponent as Check } from 'assets/svg/auth/check.svg'; -import { useNavigate } from 'react-router-dom'; -import { useRouteCheck } from 'page/Auth/FindPassword/hooks/useRouteCheck'; -import styles from './CompleteChangePassword.module.scss'; - -export default function CompleteChangePassword() { - const navigate = useNavigate(); - useRouteCheck('new-password', '/new-password'); - - return ( - - - - - - 비밀번호 변경 완료 - - 비밀번호가 변경되었습니다. - - 새로운 비밀번호로 로그인 부탁드립니다. - - navigate('/login')} - > - 로그인 화면 바로가기 - - - - ); -} diff --git a/src/page/Auth/FindPassword/NewPassword/NewPassword.module.scss b/src/page/Auth/FindPassword/NewPassword/NewPassword.module.scss deleted file mode 100644 index ca890b73..00000000 --- a/src/page/Auth/FindPassword/NewPassword/NewPassword.module.scss +++ /dev/null @@ -1,107 +0,0 @@ -.template { - display: flex; - justify-content: center; - align-items: center; - padding-top: 220px; - padding-bottom: 120px; - flex-direction: column; -} - -.logo { - margin: auto; -} - -.cursor-pointer { - cursor: pointer; - background: none; - border: none; -} - -.form { - width: 368px; - margin-top: 48px; - display: flex; - flex-direction: column; - gap: 16px; - - &__input { - width: 368px; - height: 48px; - padding: 14px 16px; - color: #222; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: normal; - box-sizing: border-box; - outline: none; - border: none; - } - - &__input-container { - display: flex; - align-items: center; - border: 1px solid #d2dae2; - padding-right: 16px; - - &--error { - border: 1px solid #f7941e; - } - - &--normal { - border: 1px solid #d2dae2; - } - } - - &__label { - color: #252525; - font-size: 18px; - font-style: normal; - font-weight: 500; - line-height: normal; - display: flex; - flex-direction: column; - gap: 8px; - } - - &__tip { - color: #d2dae2; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: normal; - - &--error { - display: none; - } - } - - &__button { - width: 368px; - height: 48px; - color: white; - background: #175c8e; - cursor: pointer; - margin-top: 250px; - box-sizing: border-box; - - &:disabled { - background-color: #c4c4c4; - } - } - - &__error { - display: flex; - color: #f7941e; - font-size: 12px; - align-items: center; - gap: 8px; - } -} - -.input-container { - display: flex; - align-items: center; - border: 1px solid #d2dae2; - padding-right: 16px; -} diff --git a/src/page/Auth/FindPassword/NewPassword/index.tsx b/src/page/Auth/FindPassword/NewPassword/index.tsx deleted file mode 100644 index 91ca1666..00000000 --- a/src/page/Auth/FindPassword/NewPassword/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { ReactComponent as KoinLogo } from 'assets/svg/auth/koin-logo.svg'; -import { ReactComponent as ShowIcon } from 'assets/svg/auth/show.svg'; -import { ReactComponent as BlindIcon } from 'assets/svg/auth/blind.svg'; -import { ReactComponent as ErrorIcon } from 'assets/svg/error/auth-error.svg'; -import { useRouteCheck } from 'page/Auth/FindPassword/hooks/useRouteCheck'; -import useBooleanState from 'utils/hooks/useBooleanState'; -import { useState } from 'react'; -import { useNewPassword } from 'query/auth'; -import useEmailAuthStore from 'store/useEmailAuth'; -import cn from 'utils/ts/className'; -import sha256 from 'utils/ts/SHA-256'; -import styles from './NewPassword.module.scss'; - -export default function NewPassword() { - const [password, setPassword] = useState(''); - const [passwordCheck, setPasswordCheck] = useState(''); - const [passwordError, setPasswordError] = useState(''); - const [passwordCheckError, setPasswordCheckError] = useState(''); - const { value: isBlind, changeValue: changeIsBlind } = useBooleanState(true); - const { email } = useEmailAuthStore(); - const submit = useNewPassword(); - useRouteCheck('find-password', '/find-password'); - - const handlePasswordChange = (e: React.ChangeEvent) => { - const { value } = e.target; - setPassword(value); - - const PASSWORD_REG_EX = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{6,18}$/; - if (!PASSWORD_REG_EX.test(value)) { - setPasswordError('비밀번호가 조건에 충족하지 않습니다.'); - } else { - setPasswordError(''); - } - }; - - const handlePasswordCheckChange = (e: React.ChangeEvent) => { - const { value } = e.target; - setPasswordCheck(value); - - if (password !== value) { - setPasswordCheckError('비밀번호가 일치하지 않습니다.'); - } else { - setPasswordCheckError(''); - } - }; - - const handleSubmit = async (e: React.MouseEvent) => { - e.preventDefault(); - const hashedPassword = await sha256(password); - if (!passwordError) { - submit({ email, password: hashedPassword }); - } - }; - - return ( - - - - - 새 비밀번호 - - - - {isBlind ? : } - - - {passwordError && ( - - - {passwordError} - - )} - - - * 특수문자 포함 영어와 숫자 조합 6~18자리 - - - 비밀번호 확인 - - - - {isBlind ? : } - - - {passwordCheckError && ( - - - {passwordCheckError} - - )} - - - 다음 - - - - ); -} diff --git a/src/page/Auth/FindPassword/SendAuthNumber/SendAuthNumber.module.scss b/src/page/Auth/FindPassword/SendAuthNumber/SendAuthNumber.module.scss deleted file mode 100644 index ae3d1172..00000000 --- a/src/page/Auth/FindPassword/SendAuthNumber/SendAuthNumber.module.scss +++ /dev/null @@ -1,94 +0,0 @@ -.template { - display: flex; - justify-content: center; - align-items: center; - padding-top: 220px; - padding-bottom: 120px; - flex-direction: column; -} - -.logo { - margin: auto; -} - -.form { - width: 368px; - margin-top: 48px; - display: flex; - flex-direction: column; - gap: 20px; - - &__input { - width: 368px; - height: 48px; - padding: 14px 16px; - border: 1px solid #d2dae2; - color: #222; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: normal; - box-sizing: border-box; - - &--auth { - width: 255px; - } - - &--error { - border: 1px solid #f7941e; - } - - &--normal { - border: 1px solid #d2dae2; - } - } - - &__label { - color: #252525; - font-size: 18px; - font-style: normal; - font-weight: 500; - line-height: normal; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 8px; - } - - &__error { - display: flex; - color: #f7941e; - font-size: 12px; - align-items: center; - gap: 8px; - } -} - -.auth-button { - width: 97px; - height: 48px; - background-color: #175c8e; - color: white; - cursor: pointer; -} - -.submit { - width: 368px; - height: 48px; - color: white; - background: #175c8e; - cursor: pointer; - margin-top: 250px; - box-sizing: border-box; - - &:disabled { - background-color: #c4c4c4; - } -} - -.auth-container { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; -} diff --git a/src/page/Auth/FindPassword/SendAuthNumber/index.tsx b/src/page/Auth/FindPassword/SendAuthNumber/index.tsx deleted file mode 100644 index 40a250dc..00000000 --- a/src/page/Auth/FindPassword/SendAuthNumber/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Outlet } from 'react-router-dom'; -import { ReactComponent as KoinLogo } from 'assets/svg/auth/koin-logo.svg'; -import { ReactComponent as ErrorIcon } from 'assets/svg/error/auth-error.svg'; -import { useEffect, useState } from 'react'; -import cn from 'utils/ts/className'; -import { useVerifyEmail, useSubmit } from 'query/auth'; -import useEmailAuthStore from 'store/useEmailAuth'; -import styles from './SendAuthNumber.module.scss'; - -export default function FindPassword() { - const { email, setEmail } = useEmailAuthStore(); - const [verify, setVerify] = useState(''); - const { verifyEmail } = useVerifyEmail(); - const { authNumber } = useSubmit(); - - useEffect(() => { - const handleBeforeUnload = () => { - sessionStorage.removeItem('email-storage'); - setEmail(''); - }; - window.addEventListener('beforeunload', handleBeforeUnload); - - return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); - }; - }, [setEmail]); - - return ( - <> - - - - - 이메일 입력 - setEmail(e.target.value)} - type="text" - id="email" - /> - {!verifyEmail.isSuccess && ( - - {verifyEmail.errorMessage && } - {verifyEmail.errorMessage} - - )} - - - 인증번호 입력 - - setVerify(e.target.value)} - /> - verifyEmail.mutate(email)} - disabled={verifyEmail.isPending} - > - {verifyEmail.isSuccess ? '재발송' : '인증번호 발송'} - - - {!authNumber.isSuccess && ( - - {authNumber.errorMessage && } - {authNumber.errorMessage} - - )} - - { - e.preventDefault(); - authNumber.submit({ email, verify }); - }} - className={styles.submit} - disabled={!verifyEmail.isSuccess} - > - 다음 - - - - - > - ); -} diff --git a/src/page/Auth/FindPassword/Verify/index.module.scss b/src/page/Auth/FindPassword/Verify/index.module.scss new file mode 100644 index 00000000..6478dae6 --- /dev/null +++ b/src/page/Auth/FindPassword/Verify/index.module.scss @@ -0,0 +1,75 @@ +.container { + height: calc(100vh - 30vh); +} + +.section { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; + margin-top: 25px; +} + +.title { + font-weight: bold; +} + +.verify { + display: flex; + justify-content: space-between; +} + +.input { + background-color: #f5f5f5; + border-radius: 4px; + border: none; + height: 50px; + padding: 2px 10px; + box-sizing: border-box; + + &--verify { + background-color: #f5f5f5; + border-radius: 4px; + border: none; + height: 50px; + padding: 2px 10px; + box-sizing: border-box; + width: 55%; + } +} + +.button { + background-color: #eeeeee; + border-radius: 4px; + height: 50px; + width: 40%; + + &--active { + background-color: #175C8E; + color: white; + border-radius: 4px; + height: 50px; + width: 40%; + } + + &--error { + background-color: #F7941E; + color: white; + border-radius: 4px; + height: 50px; + width: 40%; + } +} + +.error { + color: #f7941e; + font-size: 11px; + display: flex; + align-items: center; + gap: 5px; +} + +.comment { + color: #cacaca; + font-size: 11px; +} \ No newline at end of file diff --git a/src/page/Auth/FindPassword/Verify/index.tsx b/src/page/Auth/FindPassword/Verify/index.tsx new file mode 100644 index 00000000..06927be2 --- /dev/null +++ b/src/page/Auth/FindPassword/Verify/index.tsx @@ -0,0 +1,159 @@ +import { isKoinError } from '@bcsdlab/koin'; +import { sendVerifyCode, verifyCode } from 'api/auth'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { + UseFormClearErrors, + useFormContext, UseFormGetValues, UseFormSetError, +} from 'react-hook-form'; +import { useOutletContext } from 'react-router-dom'; +import cn from 'utils/ts/className'; +import { ReactComponent as Warning } from 'assets/svg/auth/warning.svg'; +import styles from 'page/Auth/FindPassword/index.module.scss'; +import { OutletProps } from 'page/Auth/FindPassword/entity'; + +// 코드 발송 및 에러 처리 +const code = ( + getValues: UseFormGetValues, + setError: UseFormSetError, + setIsSent: React.Dispatch>, +) => { + sendVerifyCode(getValues('phone_number')) + .then(() => setIsSent(true)) + .catch((e) => { + if (isKoinError(e)) { + setError('phone_number', { type: 'custom', message: e.message }); + } + }); +}; + +const useCheckCode = ( + setIsStepComplete: React.Dispatch>, + getValues: UseFormGetValues, + setError: UseFormSetError, + clearErrors: UseFormClearErrors, +) => { + const [certificationCode, setCertificationCode] = useState(''); + const [isCertified, setIsCertified] = useState(false); + + useEffect(() => { + if (certificationCode.length === 6) { + verifyCode({ + certification_code: certificationCode, + phone_number: getValues('phone_number'), + }).then((data) => { + setIsStepComplete(true); + setIsCertified(true); + clearErrors(); + sessionStorage.setItem('accessToken', data.data.token); + }) + .catch((e) => { + if (isKoinError(e)) { + setError('certification_code', { type: 'error', message: e.message }); + } + setIsStepComplete(false); + }); + } + }, [certificationCode, setIsStepComplete, getValues, setError, clearErrors]); + + return { setCertificationCode, isCertified }; +}; + +interface Verify { + phone_number: string; + certification_code: string; +} + +export default function Verify() { + const method = useFormContext(); + const { + register, getValues, setError, formState: { errors }, watch, clearErrors, + } = method; + const [isSent, setIsSent] = useState(false); + const [id, setId] = useState(null); + const steps: OutletProps = useOutletContext(); + + const { setCertificationCode, isCertified } = useCheckCode( + steps.setIsStepComplete, + getValues, + setError, + clearErrors, + ); + + // 디바운싱 + const debounce = () => { + if (id) clearTimeout(id); + const timeId = setTimeout(() => code(getValues, setError, setIsSent), 200); + setId(timeId); + }; + + const sendCode = () => { + if (getValues('phone_number').length === 0) { + setError('phone_number', { type: 'custom', message: '필수 입력 항목입니다.' }); + return; + } + debounce(); + }; + + const setCode = (e: ChangeEvent) => setCertificationCode(e.target.value); + + return ( + + + 휴대폰 번호 + + + {errors.phone_number + && ( + + + {errors.phone_number.message} + + )} + + + 인증번호 + + + + {isSent ? '인증번호 재발송' : '인증번호 발송'} + + + {errors.certification_code + && ( + + + {errors.certification_code.message} + + )} + + + ); +} diff --git a/src/page/Auth/FindPassword/entity.ts b/src/page/Auth/FindPassword/entity.ts new file mode 100644 index 00000000..661e5788 --- /dev/null +++ b/src/page/Auth/FindPassword/entity.ts @@ -0,0 +1,11 @@ +export interface OutletProps { + nextStep: () => void; + previousStep: () => void; + currentStep: string; + index: number; + totalStep: number; + isComplete: boolean; + setIsComplete: React.Dispatch>; + isStepComplete: boolean; + setIsStepComplete: React.Dispatch>; +} diff --git a/src/page/Auth/FindPassword/hooks/useRouteCheck.ts b/src/page/Auth/FindPassword/hooks/useRouteCheck.ts deleted file mode 100644 index a6fc4f97..00000000 --- a/src/page/Auth/FindPassword/hooks/useRouteCheck.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - -type Route = 'new-password' | 'find-password'; - -export const useRouteCheck = (prevRoute: Route, entryRoute: string) => { - const location = useLocation(); - const navigate = useNavigate(); - - useEffect(() => { - const hasPrevState = location.state && (prevRoute in location.state); - if (!hasPrevState) { - navigate(entryRoute, { replace: true }); - } - }, [location.state, navigate, entryRoute, prevRoute]); -}; diff --git a/src/page/Auth/FindPassword/index.module.scss b/src/page/Auth/FindPassword/index.module.scss new file mode 100644 index 00000000..6478dae6 --- /dev/null +++ b/src/page/Auth/FindPassword/index.module.scss @@ -0,0 +1,75 @@ +.container { + height: calc(100vh - 30vh); +} + +.section { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; + margin-top: 25px; +} + +.title { + font-weight: bold; +} + +.verify { + display: flex; + justify-content: space-between; +} + +.input { + background-color: #f5f5f5; + border-radius: 4px; + border: none; + height: 50px; + padding: 2px 10px; + box-sizing: border-box; + + &--verify { + background-color: #f5f5f5; + border-radius: 4px; + border: none; + height: 50px; + padding: 2px 10px; + box-sizing: border-box; + width: 55%; + } +} + +.button { + background-color: #eeeeee; + border-radius: 4px; + height: 50px; + width: 40%; + + &--active { + background-color: #175C8E; + color: white; + border-radius: 4px; + height: 50px; + width: 40%; + } + + &--error { + background-color: #F7941E; + color: white; + border-radius: 4px; + height: 50px; + width: 40%; + } +} + +.error { + color: #f7941e; + font-size: 11px; + display: flex; + align-items: center; + gap: 5px; +} + +.comment { + color: #cacaca; + font-size: 11px; +} \ No newline at end of file diff --git a/src/page/Auth/FindPassword/index.tsx b/src/page/Auth/FindPassword/index.tsx new file mode 100644 index 00000000..a5418a56 --- /dev/null +++ b/src/page/Auth/FindPassword/index.tsx @@ -0,0 +1,15 @@ +import { useOutletContext } from 'react-router-dom'; +import ChangePassword from './ChangePassword'; +import { OutletProps } from './entity'; +import Verify from './Verify'; + +export default function FindPassword() { + const steps: OutletProps = useOutletContext(); + const { index } = steps; + return ( + <> + {index === 0 && } + {index === 1 && } + > + ); +} diff --git a/src/page/Auth/components/Common/index.module.scss b/src/page/Auth/components/Common/index.module.scss new file mode 100644 index 00000000..2e0ee9ab --- /dev/null +++ b/src/page/Auth/components/Common/index.module.scss @@ -0,0 +1,88 @@ +@use "src/utils/styles/mediaQuery" as media; + +.container { + display: flex; + align-items: center; + flex-direction: column; +} + +.top { + display: flex; + position: relative; + justify-content: center; + align-items: center; + width: 50%; + margin-top: 20px; + + @include media.media-breakpoint-down(mobile) { + width: 90%; + } + + &__back { + position: absolute; + left: 0px; + cursor: pointer; + + @include media.media-breakpoint-up(mobile) { + display: none; + } + } +} + +.step { + display: flex; + flex-direction: column; + width: 50%; + margin-top: 20px; + + @include media.media-breakpoint-down(mobile) { + width: 90%; + } + + &__progress { + display: flex; + justify-content: space-between; + color: #175C8E; + font-weight: 600; + margin: 10px 0; + } +} + +.step-container { + position: relative; +} + +.progress-bar { + border: none; + height: 4px; + background-color: #eeeeee; +} + +.title { + font-weight: 600; + font-size: 20px; +} + +.content { + height: calc(100vh - 40vh); + + @include media.media-breakpoint-down(mobile) { + height: calc(100vh - 28vh); + } +} + +.button { + width: 100%; + background-color: #e1e1e1; + height: 50px; + border-radius: 4px; + font-weight: 600; + + &--active { + width: 100%; + background-color: #175C8E; + height: 50px; + border-radius: 4px; + font-weight: 600; + } +} \ No newline at end of file diff --git a/src/page/Auth/components/Common/index.tsx b/src/page/Auth/components/Common/index.tsx new file mode 100644 index 00000000..5d042f8c --- /dev/null +++ b/src/page/Auth/components/Common/index.tsx @@ -0,0 +1,113 @@ +import { ReactComponent as BackArrow } from 'assets/svg/common/back-arrow.svg'; +import { FormProvider, useForm, UseFormSetError } from 'react-hook-form'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import cn from 'utils/ts/className'; +import { Register } from 'model/auth'; +// eslint-disable-next-line +import { changePassword } from 'api/auth'; +import { isKoinError } from '@bcsdlab/koin'; +import { useStep } from 'page/Auth/hook/useStep'; +// eslint-disable-next-line +import Done from '../Done/index'; +import styles from './index.module.scss'; + +const setNewPassword = ( + phone_number: string, + password: string, + setError: UseFormSetError, +) => { + changePassword({ phone_number, password }) + .then(() => sessionStorage.removeItem('accessToken')) + .catch((e) => { + if (isKoinError(e)) { + setError('password', { type: 'custom', message: e.message }); + } + }); +}; + +export default function CommonLayout() { + const location = useLocation(); + const navigate = useNavigate(); + + const isFindPassword = location.pathname.includes('find'); + const title = isFindPassword ? '비밀번호 찾기' : '회원가입'; + + const method = useForm({ + mode: 'onChange', + }); + const { formState: { errors }, setError, getValues } = method; + + const steps = useStep(isFindPassword ? 'find' : 'register'); + const { + nextStep, previousStep, currentStep, index, totalStep, isComplete, isStepComplete, + } = steps; + + // eslint-disable-next-line + const progressPercentage = (index + 1) / totalStep * 100; + + // form에 error가 없으면 다음 단계로 넘어감 + const stepCheck = () => { + if (isComplete) navigate('/login'); + if (!errors.root) { + if (index + 1 === totalStep && isFindPassword) setNewPassword(getValues('phone_number'), getValues('password'), setError); + nextStep(); + } + }; + + return ( + + + + + {title} + + + + + + {index + 1} + . + {' '} + + {currentStep} + + + {`${index + 1}/${totalStep}`} + + + + + + + + {isComplete ? : } + + + {!isComplete + ? {!isComplete && (index + 1 === totalStep) ? '완료' : '다음'} + : '로그인 화면 바로 가기'} + + + + + ); +} diff --git a/src/page/Auth/components/Done/index.module.scss b/src/page/Auth/components/Done/index.module.scss new file mode 100644 index 00000000..6697498c --- /dev/null +++ b/src/page/Auth/components/Done/index.module.scss @@ -0,0 +1,21 @@ +@use "src/utils/styles/mediaQuery" as media; + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + gap: 3vh; +} + +.title { + color: #175C8E; + font-weight: bold; +} + +.content { + color: #8e8e8e; + text-align: center; +} \ No newline at end of file diff --git a/src/page/Auth/components/Done/index.tsx b/src/page/Auth/components/Done/index.tsx new file mode 100644 index 00000000..eafa6f63 --- /dev/null +++ b/src/page/Auth/components/Done/index.tsx @@ -0,0 +1,37 @@ +import { ReactComponent as Success } from 'assets/svg/auth/done.svg'; +import styles from './index.module.scss'; + +const completeFindPassword = { + title: '비밀번호 변경 완료', + content: '비밀번호 변경이 완료되었습니다!', + final: '새로운 비밀번호로 로그인해주세요:)', +}; + +const completeRegister = { + title: '회원가입 완료', + content: '회원가입이 완료되었습니다!', + final: '가입 승인 시 로그인이 가능합니다.', +}; + +interface Props { + isFindPassword: boolean; +} +export default function Done({ isFindPassword }: Props) { + const completeObject = isFindPassword ? completeFindPassword : completeRegister; + return ( + + + + {completeObject.title} + + + + {completeObject.content} + + + {completeObject.final} + + + + ); +} diff --git a/src/page/Auth/hook/useStep.ts b/src/page/Auth/hook/useStep.ts new file mode 100644 index 00000000..870a081d --- /dev/null +++ b/src/page/Auth/hook/useStep.ts @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +type Type = 'find' | 'register'; + +const findPassword = [ + '계정 인증', '비밀번호 변경', +]; + +const register = ['약관 동의', '기본 정보 입력', '사업자 인증']; + +export const useStep = (type: Type) => { + const target = type === 'find' ? findPassword : register; + const [index, setIndex] = useState(0); + const [isComplete, setIsComplete] = useState(false); + const [isStepComplete, setIsStepComplete] = useState(false); + const navigate = useNavigate(); + + const nextStep = () => { + if (isStepComplete && index + 1 < target.length) { + setIndex((prev) => prev + 1); + setIsStepComplete(false); + } else if (isStepComplete && index + 1 === target.length) { + setIsComplete(true); + } + }; + + const previousStep = () => { + if (index > 0) { + setIndex((prev) => prev - 1); + setIsStepComplete(true); // step을 통과한 사람만 뒤로 갈 수 있음 + } else navigate(-1); + }; + + const currentStep = target[index]; + const totalStep = target.length; + + return { + nextStep, + previousStep, + currentStep, + index, + totalStep, + isComplete, + setIsComplete, + isStepComplete, + setIsStepComplete, + }; +};