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 (
+
+

+
+ );
+};
+
+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;