diff --git a/package.json b/package.json index efebac6..c646e42 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@emotion/styled": "^11.13.0", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.58.0", + "@use-funnel/react-router-dom": "^0.0.7", "husky": "^9.1.6", "ky": "^1.7.2", "react": "^18.3.1", diff --git a/src/Router.tsx b/src/Router.tsx index d436fcc..14631fb 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,9 +1,14 @@ import { createBrowserRouter } from "react-router-dom"; import Home from "./pages/Home"; +import Step1 from "./components/Register/Step1"; export const router = createBrowserRouter([ { path: "/", element: , }, + { + path: "/register", + element: , + }, ]); diff --git a/src/assets/index.ts b/src/assets/index.ts index 053f512..6ec2587 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -1,5 +1,32 @@ /// import IcRabbit from "./icons/ic_rabbit.svg?react"; +import IcCheckGrey from "./svg/check/ic_check-grey.svg?react"; +import IcCheckMint from "./svg/check/ic_check-mint.svg?react"; +import IcCheckComplete from "./svg/check/ic_check-complete.svg?react"; +import IcCheckWhiteGrey from "./svg/check/ic_check-whitegrey.svg?react"; +import IcCheckTransparent from "./svg/check/ic_check-transparent.svg?react"; -export { IcRabbit }; +import IcStep1 from "./svg/register/ic_step1.svg?react"; +import IcStep2 from "./svg/register/ic_step2.svg?react"; +import IcStep3 from "./svg/register/ic_step3.svg?react"; +import IcStep4 from "./svg/register/ic_step4.svg?react"; +import IcGreyCircle from "./svg/register/ic_grey-circle.svg?react"; +import IcBack from "./svg/ic_back.svg?react"; +import IcNext from "./svg/ic_next.svg?react"; + +export { + IcRabbit, + IcCheckGrey, + IcCheckMint, + IcCheckComplete, + IcCheckWhiteGrey, + IcCheckTransparent, + IcStep1, + IcStep2, + IcStep3, + IcStep4, + IcBack, + IcNext, + IcGreyCircle, +}; diff --git a/src/assets/svg/check/ic_check-complete.svg b/src/assets/svg/check/ic_check-complete.svg new file mode 100644 index 0000000..c14b9d6 --- /dev/null +++ b/src/assets/svg/check/ic_check-complete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/check/ic_check-grey.svg b/src/assets/svg/check/ic_check-grey.svg new file mode 100644 index 0000000..7b7577b --- /dev/null +++ b/src/assets/svg/check/ic_check-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/check/ic_check-mint.svg b/src/assets/svg/check/ic_check-mint.svg new file mode 100644 index 0000000..20ecc9d --- /dev/null +++ b/src/assets/svg/check/ic_check-mint.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/check/ic_check-transparent.svg b/src/assets/svg/check/ic_check-transparent.svg new file mode 100644 index 0000000..08c760e --- /dev/null +++ b/src/assets/svg/check/ic_check-transparent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/check/ic_check-whitegrey.svg b/src/assets/svg/check/ic_check-whitegrey.svg new file mode 100644 index 0000000..0dfae4e --- /dev/null +++ b/src/assets/svg/check/ic_check-whitegrey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/ic_back.svg b/src/assets/svg/ic_back.svg new file mode 100644 index 0000000..5af108f --- /dev/null +++ b/src/assets/svg/ic_back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/ic_next.svg b/src/assets/svg/ic_next.svg new file mode 100644 index 0000000..947b45c --- /dev/null +++ b/src/assets/svg/ic_next.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/register/ic_grey-circle.svg b/src/assets/svg/register/ic_grey-circle.svg new file mode 100644 index 0000000..78c0c95 --- /dev/null +++ b/src/assets/svg/register/ic_grey-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/register/ic_step1.svg b/src/assets/svg/register/ic_step1.svg new file mode 100644 index 0000000..7bfe074 --- /dev/null +++ b/src/assets/svg/register/ic_step1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/register/ic_step2.svg b/src/assets/svg/register/ic_step2.svg new file mode 100644 index 0000000..92eb14a --- /dev/null +++ b/src/assets/svg/register/ic_step2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/register/ic_step3.svg b/src/assets/svg/register/ic_step3.svg new file mode 100644 index 0000000..1dbaf07 --- /dev/null +++ b/src/assets/svg/register/ic_step3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/register/ic_step4.svg b/src/assets/svg/register/ic_step4.svg new file mode 100644 index 0000000..aff526f --- /dev/null +++ b/src/assets/svg/register/ic_step4.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Register/CheckBox.tsx b/src/components/Register/CheckBox.tsx new file mode 100644 index 0000000..3cf233e --- /dev/null +++ b/src/components/Register/CheckBox.tsx @@ -0,0 +1,38 @@ +import styled from "@emotion/styled"; +import { InputHTMLAttributes } from "react"; +import { IcCheckComplete, IcCheckWhiteGrey } from "../../assets"; + +interface CheckBoxProps extends InputHTMLAttributes { + onClick?: () => void; +} + +function CheckBox({ onClick, checked }: CheckBoxProps): JSX.Element { + return ( + + + {checked ? : } + 네, 동의합니다. + + + ); +} + +const CheckBoxWrapper = styled.div<{ disabled?: boolean; isChecked?: boolean }>` + padding: 1.5rem; + + background-color: ${({ theme }) => theme.colors.light_6}; + border-radius: 1.2rem; +`; + +const CheckBoxContent = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +const CheckBoxText = styled.span` + ${({ theme }) => theme.fonts.subtitle2}; + color: ${({ theme }) => theme.colors.black_2}; +`; + +export default CheckBox; diff --git a/src/components/Register/Step1.tsx b/src/components/Register/Step1.tsx new file mode 100644 index 0000000..285b095 --- /dev/null +++ b/src/components/Register/Step1.tsx @@ -0,0 +1,129 @@ +import styled from "@emotion/styled"; +import { IcBack, IcCheckGrey, IcCheckWhiteGrey, IcNext, IcCheckComplete, IcCheckMint } from "../../assets"; +import Button from "../Common/Button/Button"; +import { AGREE_DATA, REGISTER_TEXT } from "../../constant"; +import { useState } from "react"; +import CheckBox from "./CheckBox"; +import StepIcon from "../../utils/StepIcon"; +import { useNavigate } from "react-router-dom"; + +interface AgreeTextProps { + isRequired: boolean; +} + +function Step1() { + const navigate = useNavigate(); + const [checkedItems, setCheckedItems] = useState([]); + + const handleClickNextButton = () => { + navigate("/register/step2"); + }; + + // 전체 선택 + const handleAllChecked = (checked: boolean) => { + setCheckedItems(checked ? AGREE_DATA.map((item) => item.key) : []); + }; + + // 개별 선택 + const handleSingleChecked = (checked: boolean, key: string) => { + setCheckedItems((prev) => (checked ? [...prev, key] : prev.filter((item) => item !== key))); + }; + + return ( + + + + + + + + handleAllChecked(checkedItems.length !== AGREE_DATA.length)} + checked={checkedItems.length === AGREE_DATA.length} + > + {checkedItems.length === AGREE_DATA.length ? : } + + + + {AGREE_DATA.map((data) => ( + handleSingleChecked(!checkedItems.includes(data.key), data.key)} + > + + {checkedItems.includes(data.key) ? : } + {data.text} + + + + ))} + + + + key.includes("required") && !checkedItems.includes(key))} + onClick={handleClickNextButton} + > + 다음 + + + + ); +} + +export default Step1; + +const Step1Container = styled.div` + height: 100dvh; + + padding: 2rem; + position: relative; +`; + +const Step1Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +const TitleWrapper = styled.div` + margin-left: 1rem; + ${({ theme }) => theme.fonts.title1}; +`; + +const AgreeContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +const NextButton = styled(Button)<{ disabled?: boolean }>` + width: calc(100% - 4rem); + position: absolute; + bottom: 3rem; +`; + +const OptionalAgreeContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1.8rem; + padding: 0 2rem; +`; + +const OptionalAgreeWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const AgreeWrapper = styled.div` + display: flex; + align-items: center; + gap: 1.5rem; +`; + +const AgreeText = styled.div` + ${({ theme }) => theme.fonts.body2}; + + color: ${({ theme, isRequired }) => (isRequired ? theme.colors.main_mint : theme.colors.gray_3)}; +`; diff --git a/src/constant/index.ts b/src/constant/index.ts new file mode 100644 index 0000000..26e61c0 --- /dev/null +++ b/src/constant/index.ts @@ -0,0 +1,15 @@ +export const REGISTER_TEXT = { + STEP1: "간편 회원가입을 위해
약관 동의가 필요해요", + STEP2: " 학교 계정 인증을 위한
이메일을 입력하세요", + DESCRIPTION_STEP2: " 최초 1회 이메일 인증이 필요해요.", + STEP3: "나만의 프로필을
설정하세요", + DESCRIPTION_STEP3: "마이페이지에서 언제든 수정 가능해요.", + STEP4: "추가적인 정보를
입력하세요", + DESCRIPTION_STEP4: "추가정보를 입력하고 신뢰도를 높여보세요!", +} as const; + +export const AGREE_DATA = [ + { text: "[필수] TinU 서비스 이용약관", key: "required1" }, + { text: "[필수] 개인정보 수집 및 이용 동의", key: "required2" }, + { text: "[선택] 광고성 정보 수신 동의", key: "optional" }, +]; diff --git a/src/styles/GlobalStyles.ts b/src/styles/GlobalStyles.ts index ff46a55..694e46e 100644 --- a/src/styles/GlobalStyles.ts +++ b/src/styles/GlobalStyles.ts @@ -173,8 +173,6 @@ const resetCss = css` const gStyle = css` ${resetCss} - #root, - body, html { margin: 0 auto; @@ -202,9 +200,6 @@ const gStyle = css` } body { - display: flex; - justify-content: center; - scroll-behavior: smooth; } input, diff --git a/src/utils/StepIcon.tsx b/src/utils/StepIcon.tsx new file mode 100644 index 0000000..fbae58a --- /dev/null +++ b/src/utils/StepIcon.tsx @@ -0,0 +1,56 @@ +import styled from "@emotion/styled"; +import { memo } from "react"; +import { IcStep1, IcStep2, IcStep3, IcStep4, IcCheckTransparent, IcGreyCircle } from "../assets"; + +interface StepIconProps { + step: 1 | 2 | 3 | 4; +} + +const STEP_CONFIG = { + 1: { text: "약관동의", Icon: IcStep1 }, + 2: { text: "메일인증", Icon: IcStep2 }, + 3: { text: "정보입력", Icon: IcStep3 }, + 4: { text: "상세정보", Icon: IcStep4 }, +} as const; + +function StepIcon({ step }: StepIconProps) { + const renderIcons = () => { + return Array(4) + .fill(null) + .map((_, index) => { + const currentStep = index + 1; + + if (currentStep === step) { + const StepComponent = STEP_CONFIG[step].Icon; + return ( + + + {STEP_CONFIG[step].text} + + ); + } + + return currentStep < step ? : ; + }); + }; + return {renderIcons()}; +} + +const StepIconContainer = styled.div` + display: flex; + gap: 0.5rem; +`; + +const StepIconWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const StepText = styled.span` + margin-top: 0.3rem; + ${({ theme }) => theme.fonts.description3}; + color: ${({ theme }) => theme.colors.gray_3}; +`; + +export default memo(StepIcon); diff --git a/yarn.lock b/yarn.lock index e5bf068..c28268b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2404,6 +2404,27 @@ __metadata: languageName: node linkType: hard +"@use-funnel/core@npm:^0.0.7": + version: 0.0.7 + resolution: "@use-funnel/core@npm:0.0.7" + peerDependencies: + react: ">=16.8" + checksum: 10c0/edf90214a37d9952f5e76ff4075c74ef1663c849d9cd524ecb5476a55ac451cd7f8cbcd6fba012b241cfa4af1690d553c23bce0d7585492cd57049ce3476b174 + languageName: node + linkType: hard + +"@use-funnel/react-router-dom@npm:^0.0.7": + version: 0.0.7 + resolution: "@use-funnel/react-router-dom@npm:0.0.7" + dependencies: + "@use-funnel/core": "npm:^0.0.7" + peerDependencies: + react: ">=16.8" + react-router-dom: ">=6" + checksum: 10c0/a529384e32655c0936464ec0926b0fd9ae4560b4cae7d706d2bd2924a030579f62c3b440573555ebb709a9bf48429f9b93ca25f727d6c0e9157c76397103b651 + languageName: node + linkType: hard + "@vitejs/plugin-react-swc@npm:^3.5.0": version: 3.7.0 resolution: "@vitejs/plugin-react-swc@npm:3.7.0" @@ -5821,6 +5842,7 @@ __metadata: "@tanstack/react-query-devtools": "npm:^5.58.0" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" + "@use-funnel/react-router-dom": "npm:^0.0.7" "@vitejs/plugin-react-swc": "npm:^3.5.0" eslint: "npm:^9.9.0" eslint-plugin-react-hooks: "npm:^5.1.0-rc.0"