diff --git a/src/App.tsx b/src/App.tsx index 69b8a86..9a6de83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,9 +55,7 @@ export default function App() { }> } /> - } /> - } /> - } /> + } /> }> diff --git a/src/api/Authentication.ts b/src/api/Authentication.ts index 9909f01..a87cc92 100644 --- a/src/api/Authentication.ts +++ b/src/api/Authentication.ts @@ -11,7 +11,8 @@ export interface ILoginResponseBody { }; } -export interface ISignupRequestBody extends Omit { +export interface ISignupRequestBody + extends Omit { interest_categories: string[]; } diff --git a/src/components/auth-page/CategoryGrid.module.scss b/src/components/auth-page/CategoryGrid.module.scss index 15e6d0a..a733dda 100644 --- a/src/components/auth-page/CategoryGrid.module.scss +++ b/src/components/auth-page/CategoryGrid.module.scss @@ -3,6 +3,9 @@ .category_grid_container { margin: 0px auto; + max-height: 80%; + overflow: scroll; + display: grid; grid-template-rows: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr); diff --git a/src/interfaces/forms/Input.module.scss b/src/interfaces/forms/Input.module.scss index c097592..910bf4d 100644 --- a/src/interfaces/forms/Input.module.scss +++ b/src/interfaces/forms/Input.module.scss @@ -17,6 +17,14 @@ p { margin: 5px 0px; font-weight: bold; + + display: flex; + justify-content: space-between; + + span:last-child { + font-weight: normal; + color: red; + } } } diff --git a/src/interfaces/forms/Input.tsx b/src/interfaces/forms/Input.tsx index fd09784..82e3184 100644 --- a/src/interfaces/forms/Input.tsx +++ b/src/interfaces/forms/Input.tsx @@ -9,6 +9,7 @@ export interface IInput { rest?: unknown; placeholder?: string; disabled?: boolean; + inlineStyle?: React.CSSProperties; onChange?: React.ChangeEventHandler; } @@ -16,6 +17,10 @@ export interface IInputContainer extends IInput { label: string; } +export interface IInputVerifyContainer extends IInputContainer { + verifyLabel: string; +} + export interface ITextArea { width?: string; height?: string; @@ -33,8 +38,17 @@ export interface IInputButtonContainer extends IInputContainer { onClick: MouseEventHandler; } -export const Input = forwardRef(({ width, height, onChange, disabled, ...rest }, ref) => { - return ; +export const Input = forwardRef(({ width, height, onChange, disabled, inlineStyle, ...rest }, ref) => { + return ( + + ); }); export const InputContainer = forwardRef(({ width, height, label, ...rest }, ref) => { @@ -46,6 +60,20 @@ export const InputContainer = forwardRef(({ w ); }); +export const InputVerificationContainer = forwardRef( + ({ width, height, label, verifyLabel, ...rest }, ref) => { + return ( +
+

+ {label} + {verifyLabel} +

+ +
+ ); + }, +); + export const InputButtonContainer = forwardRef( ({ width, height, label, btnWidth, btnLabel, onClick, ...rest }, ref) => { return ( diff --git a/src/layouts/AuthLayout.module.scss b/src/layouts/AuthLayout.module.scss index 3daae11..e0d55ee 100644 --- a/src/layouts/AuthLayout.module.scss +++ b/src/layouts/AuthLayout.module.scss @@ -48,6 +48,6 @@ @include mobile { .auth_page_wrapper { border-radius: 0px; - padding: $p-mobile; + padding: 20px; } } diff --git a/src/pages/auth-page/SigninPage.tsx b/src/pages/auth-page/SigninPage.tsx index 8d1c8f9..68e9ce1 100644 --- a/src/pages/auth-page/SigninPage.tsx +++ b/src/pages/auth-page/SigninPage.tsx @@ -8,10 +8,15 @@ import { InputContainer } from "../../interfaces/forms/Input"; import { Title } from "../../components/auth-page/Title"; import { handleLogin } from "../../api/Authentication"; +import { Dispatch } from "@reduxjs/toolkit"; +import { useDispatch } from "react-redux"; +import { UserInterfaceActions } from "../../store/user-interface-slice"; // import { SocialLoginProviders } from "../../constants/SocialLogin"; export default function SigninPage() { + const dispatch: Dispatch = useDispatch(); + const naviagte = useNavigate(); const idRef = useRef(null); const pwRef = useRef(null); @@ -20,6 +25,7 @@ export default function SigninPage() { // 로그인 요청 & 쿠키 저장 try { await handleLogin(idRef.current?.value as string, pwRef.current?.value as string); + dispatch(UserInterfaceActions.closeNav()); naviagte("/"); } catch (err) { alert("아이디 혹은 비밀번호가 일치하지 않습니다"); @@ -35,7 +41,7 @@ export default function SigninPage() {
- +
@@ -54,7 +60,7 @@ export default function SigninPage() {
아직 계정이 없으신가요? - 회원가입 + 회원가입
diff --git a/src/pages/auth-page/SignupPage.module.scss b/src/pages/auth-page/SignupPage.module.scss index 529b76b..cba9549 100644 --- a/src/pages/auth-page/SignupPage.module.scss +++ b/src/pages/auth-page/SignupPage.module.scss @@ -1,25 +1,25 @@ @import "@/styles/utils.scss"; -.signin_page { - width: 100%; - height: 100%; -} +// .signin_page { +// width: 100%; +// height: 100%; +// } -.signin_background { - @include place-center; - position: absolute; - top: 0; - right: 0; - width: 40%; - height: 100%; - z-index: -1; +// .signin_background { +// @include place-center; +// position: absolute; +// top: 0; +// right: 0; +// width: 40%; +// height: 100%; +// z-index: -1; - img { - width: 100%; - height: 100%; - object-fit: cover; - } -} +// img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } .btn_group { display: flex; @@ -28,3 +28,11 @@ width: min(100%, 700px); margin: 0px auto; } + +.input_wrapper { + display: flex; + flex-direction: column; + gap: 10px; + + width: 100%; +} diff --git a/src/pages/auth-page/SignupPage.tsx b/src/pages/auth-page/SignupPage.tsx index dc42ac6..fe2c178 100644 --- a/src/pages/auth-page/SignupPage.tsx +++ b/src/pages/auth-page/SignupPage.tsx @@ -1,14 +1,15 @@ +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { Button } from "../../interfaces/forms/Button"; -import { InputContainer, InputButtonContainer } from "../../interfaces/forms/Input"; +import { InputContainer, InputButtonContainer, InputVerificationContainer } from "../../interfaces/forms/Input"; import { Title } from "../../components/auth-page/Title"; import { CategoryGrid } from "../../components/auth-page/CategoryGrid"; import { Categories } from "../../constants/Categories"; -import { useRef } from "react"; +import { ChangeEventHandler, useRef } from "react"; import { Dispatch } from "@reduxjs/toolkit"; import { useDispatch, useSelector } from "react-redux"; import { UserSignupActions } from "../../store/user-signup-slice"; @@ -16,12 +17,27 @@ import { handleSignup, verifyEmail, verifyNickName } from "../../api/Authenticat import { RootState } from "../../store/store"; import styles from "./SignupPage.module.scss"; +import { verifyBirth, verifyPassword, verifyPhone } from "../../utils/verify"; -const SignupPage = { - One: () => { +export default function SignupPage() { + const [step, setStep] = useState(1); + + if (step === 1) return ; + else if (step === 2) return ; + else if (step === 3) return ; +} + +interface ISignupStep { + setStep: React.Dispatch>; +} + +const SignupStep = { + One: ({ setStep }: ISignupStep) => { const navigate = useNavigate(); const dispatch: Dispatch = useDispatch(); - const { isEmailVerified } = useSelector((state: RootState) => state.UserSignup); + + // ❓ 변경시 리렌더링 필요없다면 useRef 훅 사용해도 되지 않을까 ?? + const { isEmailVerified, isPasswordVerified } = useSelector((state: RootState) => state.UserSignup); const nameRef = useRef(null); const emailRef = useRef(null); @@ -37,42 +53,70 @@ const SignupPage = { } }; - const onEmailChange = () => { + const onEmailChange: ChangeEventHandler = () => { // 이메일 변경시 재검사 필요 dispatch(UserSignupActions.unVerifyEmail()); }; + const onPasswordChange: ChangeEventHandler = (e) => { + const pwVerification = verifyPassword(e.target.value); + if (pwVerification) { + dispatch(UserSignupActions.verifyPassword()); + } else { + dispatch(UserSignupActions.unVerifyPassword()); + } + }; + const onPrevBtnClick = () => { - navigate("/auth/signin"); + navigate("/"); }; const onNextBtnClick = () => { - if (isEmailVerified) { - dispatch(UserSignupActions.setName(nameRef.current?.value as string)); - dispatch(UserSignupActions.setEmail(emailRef.current?.value as string)); - dispatch(UserSignupActions.setPassword(passwordRef.current?.value as string)); - navigate("/auth/signup/step2"); - } else { + if (!isEmailVerified) { alert("이메일 중복검사를 해주세요"); + return; + } + if (!isPasswordVerified) { + alert("비밀번호는 영문 숫자 특수기호 조합 8 ~ 15자리 이어야 합니다"); + return; } + if (!nameRef.current?.value) { + alert("이름을 입력해주세요"); + return; + } + + dispatch(UserSignupActions.setName(nameRef.current?.value)); + dispatch(UserSignupActions.setEmail(emailRef.current?.value as string)); + dispatch(UserSignupActions.setPassword(passwordRef.current?.value as string)); + setStep((step) => step + 1); }; return ( <> - <InputContainer ref={nameRef} label="이름" type="text" width="100%" height="50px"></InputContainer> - <InputButtonContainer - ref={emailRef} - label="이메일 주소(아이디)" - type="text" - width="100%" - height="50px" - btnWidth="100px" - btnLabel="중복확인" - onChange={onEmailChange} - onClick={onEmailVerificationClick} - ></InputButtonContainer> - <InputContainer ref={passwordRef} label="비밀번호" type="password" width="100%" height="50px"></InputContainer> + <div className={styles.input_wrapper}> + <InputContainer ref={nameRef} label="이름" type="text" width="100%" height="50px"></InputContainer> + <InputButtonContainer + ref={emailRef} + label="이메일 주소(아이디)" + type="text" + width="100%" + height="50px" + btnWidth="100px" + btnLabel="중복확인" + onChange={onEmailChange} + onClick={onEmailVerificationClick} + /> + <InputVerificationContainer + ref={passwordRef} + label="비밀번호" + verifyLabel={!isPasswordVerified ? "비밀번호는 영문 숫자 특수기호 조합 8 ~ 15자리 이어야 합니다" : ""} + type="password" + width="100%" + height="50px" + onChange={onPasswordChange} + /> + </div> <div className={styles.btn_group}> <Button type="primary-stroke" width="80px" height="80px" onClick={onPrevBtnClick}> @@ -87,8 +131,7 @@ const SignupPage = { ); }, - Two: () => { - const navigate = useNavigate(); + Two: ({ setStep }: ISignupStep) => { const dispatch: Dispatch = useDispatch(); const { isNickNameVerified } = useSelector((state: RootState) => state.UserSignup); @@ -111,43 +154,73 @@ const SignupPage = { dispatch(UserSignupActions.unVerifyNickName()); }; + const onPrevBtnClick = () => { + setStep((step) => step - 1); + }; + const onNextBtnClick = () => { - if (isNickNameVerified) { - dispatch(UserSignupActions.setNickName(nicknameRef.current?.value as string)); - dispatch(UserSignupActions.setBirthDate(birthRef.current?.value as string)); - navigate("/auth/signup/step3"); - } else { + if (!isNickNameVerified) { alert("닉네임 중복확인을 해주세요"); + return; + } + if (!phoneRef.current?.value) { + alert("휴대폰 번호를 입력해주세요"); + return; + } + if (!verifyPhone(phoneRef.current.value)) { + alert("휴대폰 번호 형식이 알맞지 않습니다"); + return; } + if (!birthRef.current?.value) { + alert("생년월일을 입력해주세요"); + return; + } + if (!verifyBirth(birthRef.current.value)) { + alert("생년월일이 형식에 알맞지 않습니다"); + return; + } + + dispatch(UserSignupActions.setNickName(nicknameRef.current?.value as string)); + dispatch(UserSignupActions.setBirthDate(birthRef.current?.value)); + setStep((step) => step + 1); }; return ( <> <Title title="Stocodi 에 오신걸 환영해요!" /> - <InputButtonContainer - ref={nicknameRef} - label="닉네임" - type="text" - width="100%" - height="50px" - btnWidth="100px" - btnLabel="중복확인" - onChange={onNickNameChange} - onClick={onNicknameVerificationClick} - ></InputButtonContainer> - <InputContainer - ref={phoneRef} - label="휴대폰 번호" - type="text" - width="100%" - height="50px" - placeholder="하이픈(-)을 제외한 숫자만 입력해주세요" - ></InputContainer> - <InputContainer ref={birthRef} label="생년월일" type="text" width="100%" height="50px" placeholder="ex) 20001212"></InputContainer> + <div className={styles.input_wrapper}> + <InputButtonContainer + ref={nicknameRef} + label="닉네임" + type="text" + width="100%" + height="50px" + btnWidth="100px" + btnLabel="중복확인" + onChange={onNickNameChange} + onClick={onNicknameVerificationClick} + ></InputButtonContainer> + <InputContainer + ref={phoneRef} + label="휴대폰 번호" + type="text" + width="100%" + height="50px" + placeholder="하이픈(-)을 제외한 숫자만 입력해주세요" + ></InputContainer> + <InputContainer + ref={birthRef} + label="생년월일" + type="text" + width="100%" + height="50px" + placeholder="ex) 20001212" + ></InputContainer> + </div> <div className={styles.btn_group}> - <Button type="primary-stroke" width="80px" height="80px" onClick={() => navigate("/auth/signup/step1")}> + <Button type="primary-stroke" width="80px" height="80px" onClick={onPrevBtnClick}> <FontAwesomeIcon icon={faChevronLeft} size="xl" /> </Button> @@ -159,12 +232,12 @@ const SignupPage = { ); }, - Three: () => { + Three: ({ setStep }: ISignupStep) => { const navigate = useNavigate(); const signupData = useSelector((state: RootState) => state.UserSignup); const onPrevBtnClick = () => { - navigate("/auth/signup/step2"); + setStep((step) => step - 1); }; const onSignupBtnClicked = async () => { @@ -207,5 +280,3 @@ const SignupPage = { ); }, }; - -export default SignupPage; diff --git a/src/store/user-interface-slice.ts b/src/store/user-interface-slice.ts index 7adced9..85d526d 100644 --- a/src/store/user-interface-slice.ts +++ b/src/store/user-interface-slice.ts @@ -18,6 +18,9 @@ export const UserInterfaceSlice = createSlice({ if (state.isNavOpen) state.isNavOpen = false; else state.isNavOpen = true; }, + closeNav: (state) => { + state.isNavOpen = false; + }, }, }); diff --git a/src/store/user-signup-slice.ts b/src/store/user-signup-slice.ts index 9cda2a5..8d32bb7 100644 --- a/src/store/user-signup-slice.ts +++ b/src/store/user-signup-slice.ts @@ -2,6 +2,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; export interface IUserSignup { isEmailVerified: boolean; + isPasswordVerified: boolean; isNickNameVerified: boolean; email: string; @@ -15,6 +16,7 @@ export interface IUserSignup { export const initialState: IUserSignup = { isEmailVerified: false, + isPasswordVerified: false, isNickNameVerified: false, email: "", @@ -35,12 +37,19 @@ export const UserSignupSlice = createSlice({ verifyEmail: (state) => { state.isEmailVerified = true; }, + verifyPassword: (state) => { + state.isPasswordVerified = true; + }, verifyNickName: (state) => { state.isNickNameVerified = true; }, + unVerifyEmail: (state) => { state.isEmailVerified = false; }, + unVerifyPassword: (state) => { + state.isPasswordVerified = false; + }, unVerifyNickName: (state) => { state.isNickNameVerified = false; }, diff --git a/src/utils/verify.ts b/src/utils/verify.ts new file mode 100644 index 0000000..b114782 --- /dev/null +++ b/src/utils/verify.ts @@ -0,0 +1,31 @@ +export const emailRegExp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; +/** + * 이메일 형식 검사 + */ +export const verifyEmail = (email: string): boolean => { + if (!emailRegExp.exec(email)) return false; + return true; +}; + +export const pwRegExp = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$/; +/** + * 영문 숫자 특수기호 조합 8~15 자리 + */ +export const verifyPassword = (password: string): boolean => { + if (!pwRegExp.exec(password)) return false; + return true; +}; + +export const phoneRegExp = /^[0-9]{3}[0-9]{4}[0-9]{4}$/; + +export const verifyPhone = (phone: string): boolean => { + if (!phoneRegExp.exec(phone)) return false; + return true; +}; + +export const birthRegExp = /^[0-9]{4}[0-1][0-9][0-3][1-9]/; + +export const verifyBirth = (birth: string): boolean => { + if (!birthRegExp.exec(birth)) return false; + return true; +};