diff --git a/src/app/router.tsx b/src/app/router.tsx index e0264b1..10681f2 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -2,8 +2,10 @@ import Layout from "@/components/layouts/layout"; import { useMemo } from "react"; +import AgreementView from "@/app/routes/auth/agreement"; import KakaoCallback from "@/app/routes/auth/kakao-callback"; import LoginView from "@/app/routes/auth/login"; +import PhoneNumberInputView from "@/app/routes/auth/phone-number"; import CreateGoalView from "@/app/routes/goal/create-goal"; import { default as AppRoot, @@ -74,6 +76,14 @@ const createAppRouter = (queryClient: QueryClient) => path: "*", lazy: () => import("./routes/not-found").then(convert(queryClient)) }, + { + path: paths.auth.agreement.path, + element: + }, + { + path: paths.auth.phoneNumber.path, + element: + }, { path: paths.goal.create.path, element: diff --git a/src/app/routes/auth/agreement.tsx b/src/app/routes/auth/agreement.tsx new file mode 100644 index 0000000..640856d --- /dev/null +++ b/src/app/routes/auth/agreement.tsx @@ -0,0 +1,7 @@ +import Agreement from "@/features/auth/components/Agreement"; + +const AgreementView = () => { + return ; +}; + +export default AgreementView; diff --git a/src/app/routes/auth/phone-number.tsx b/src/app/routes/auth/phone-number.tsx new file mode 100644 index 0000000..5a470a1 --- /dev/null +++ b/src/app/routes/auth/phone-number.tsx @@ -0,0 +1,7 @@ +import PhoneNumber from "@/features/auth/components/PhoneNumber"; + +const PhoneNumberInputView = () => { + return ; +}; + +export default PhoneNumberInputView; diff --git a/src/asset/agreement/checked-all.svg b/src/asset/agreement/checked-all.svg new file mode 100644 index 0000000..686545f --- /dev/null +++ b/src/asset/agreement/checked-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/asset/agreement/checked-item.svg b/src/asset/agreement/checked-item.svg new file mode 100644 index 0000000..ecdd10b --- /dev/null +++ b/src/asset/agreement/checked-item.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/asset/agreement/unchecked-all.svg b/src/asset/agreement/unchecked-all.svg new file mode 100644 index 0000000..2fa079d --- /dev/null +++ b/src/asset/agreement/unchecked-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/asset/agreement/unchecked-item.svg b/src/asset/agreement/unchecked-item.svg new file mode 100644 index 0000000..0a25d0c --- /dev/null +++ b/src/asset/agreement/unchecked-item.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/ui/header/index.const.ts b/src/components/ui/header/index.const.ts index d9d62f9..b4fc1ce 100644 --- a/src/components/ui/header/index.const.ts +++ b/src/components/ui/header/index.const.ts @@ -15,7 +15,9 @@ export const HEADER_TITLE_MAP = new Map([ [paths.map.certification.getHref(), "목표인증"], [paths.goal.root.getHref(), "목표"], [paths.goal.create.getHref(), "목표추가"], - [paths.profile.reward.getHref(), "리워드 신청"] + [paths.profile.reward.getHref(), "리워드 신청"], + [paths.auth.agreement.getHref(), "약관동의"], + [paths.auth.phoneNumber.getHref(), "정보입력"] ]); export const backgroundImages = [ diff --git a/src/components/ui/navbar/index.const.ts b/src/components/ui/navbar/index.const.ts index 43909cd..7882c08 100644 --- a/src/components/ui/navbar/index.const.ts +++ b/src/components/ui/navbar/index.const.ts @@ -8,9 +8,11 @@ import User from "@/asset/navbar/user.svg?react"; import { paths } from "@/config/paths"; export const NOT_VISIBLE_NAVBAR_PAGES = [ + paths.profile.reward.getHref(), + paths.auth.agreement.getHref(), + paths.auth.phoneNumber.getHref(), paths.map.search.getHref(), - paths.map.certification.getHref(), - paths.profile.reward.getHref() + paths.map.certification.getHref() ]; const isEqualPath = (locationPath: string, path: string) => { diff --git a/src/config/paths.ts b/src/config/paths.ts index 4c411d1..3c76bb0 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -54,6 +54,14 @@ export const paths = { googleCallback: { path: "/auth/callback/google", getHref: () => "/auth/callback/google" + }, + agreement: { + path: "/auth/agreement", + getHref: () => "/auth/agreement" + }, + phoneNumber: { + path: "/auth/phoneNumber", + getHref: () => "/auth/phoneNumber" } }, diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts index 3ab491a..2a5a49c 100644 --- a/src/features/auth/api/auth.ts +++ b/src/features/auth/api/auth.ts @@ -17,3 +17,11 @@ export function postRefreshAccessToken({ }) { return POST({ url: `${BASE_PATH}/refresh`, data }); } + +export function postPhoneNumber({ data }: { data: { phoneNumber: string } }) { + return POST({ url: `${BASE_PATH}/user/phone`, data }); +} + +export function postTermsAgree({ data }: { data: { userId: number } }) { + return POST({ url: `/terms/agree`, data }); +} diff --git a/src/features/auth/components/Agreement.tsx b/src/features/auth/components/Agreement.tsx new file mode 100644 index 0000000..f9d285d --- /dev/null +++ b/src/features/auth/components/Agreement.tsx @@ -0,0 +1,173 @@ +import { useState } from "react"; + +import CheckedAllIcon from "@/asset/agreement/checked-all.svg?url"; +import CheckedItemIcon from "@/asset/agreement/checked-item.svg?url"; +import UncheckedAllIcon from "@/asset/agreement/unchecked-all.svg?url"; +import UncheckedItemIcon from "@/asset/agreement/unchecked-item.svg?url"; +import { postTermsAgree } from "@/features/auth/api/auth"; +import { useNavigate } from "react-router"; + +import { useAuthStore } from "@/stores/auth-store"; +import { paths } from "@/config/paths"; + +const AgreementCheckbox = ({ + checked, + onChange, + CheckedIcon, + UncheckedIcon +}) => { + return ( +
+ Checkbox Icon +
+ ); +}; + +const Agreement = () => { + const navigate = useNavigate(); + const userId = useAuthStore((state) => state.userId); + + const [checkState, setCheckState] = useState({ + all: false, + terms: false, + privacy: false + }); + + const handleAllCheck = () => { + setCheckState((prev) => { + const newChecked = !prev.all; + return { + all: newChecked, + terms: newChecked, + privacy: newChecked + }; + }); + }; + const handleSingleCheck = (key: "terms" | "privacy") => { + setCheckState((prev) => { + const newState = { + ...prev, + [key]: !prev[key] + }; + + newState.all = newState.terms && newState.privacy; + + return newState; + }); + }; + + const handleSubmit = async () => { + if (!userId) return; + + try { + const response = await postTermsAgree({ + data: { userId } + }); + + if (response.data.success) { + navigate(paths.auth.phoneNumber.path); + } else { + throw new Error("약관 동의에 실패했습니다."); + } + } catch (error) { + console.error(error); + } + }; + + return ( +
+

+ 윌고 서비스 이용을 위한
+ 약관에 동의해주세요 +

+ +
+
+ + +
+ +
+ +
+
+
+ handleSingleCheck("terms")} + CheckedIcon={CheckedItemIcon} + UncheckedIcon={UncheckedItemIcon} + /> + + [필수] + + +
+ + +
+ +
+
+ handleSingleCheck("privacy")} + CheckedIcon={CheckedItemIcon} + UncheckedIcon={UncheckedItemIcon} + /> + + [필수] + + +
+ + +
+
+
+ + +
+ ); +}; + +export default Agreement; diff --git a/src/features/auth/components/PhoneNumber.tsx b/src/features/auth/components/PhoneNumber.tsx new file mode 100644 index 0000000..d9cecab --- /dev/null +++ b/src/features/auth/components/PhoneNumber.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; + +import { postPhoneNumber } from "@/features/auth/api/auth"; +import { useNavigate } from "react-router"; + +import { paths } from "@/config/paths"; + +const PhoneNumber = () => { + const navigate = useNavigate(); + + const [phoneNumber, setPhoneNumber] = useState(""); + const [isButtonEnabled, setIsButtonEnabled] = useState(false); + + const handleInputChange = (e: React.ChangeEvent) => { + const input = e.target.value; + setPhoneNumber(input); + setIsButtonEnabled(input.length === 8); + }; + + const handleSubmit = async () => { + try { + const response = await postPhoneNumber({ + data: { phoneNumber } + }); + + if (response.data.success) { + navigate(paths.home.path); + } else { + throw new Error("약관 동의에 실패했습니다."); + } + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+

+ 리워드 전달을 위해 +

+

+ 휴대폰 번호 +

+

+ 를 입력해주세요. +

+
+ +
+ + 010 + + +
+ +
+ +
+
+ ); +}; + +export default PhoneNumber;