Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,6 +76,14 @@ const createAppRouter = (queryClient: QueryClient) =>
path: "*",
lazy: () => import("./routes/not-found").then(convert(queryClient))
},
{
path: paths.auth.agreement.path,
element: <AgreementView />
},
{
path: paths.auth.phoneNumber.path,
element: <PhoneNumberInputView />
},
{
path: paths.goal.create.path,
element: <CreateGoalView />
Expand Down
7 changes: 7 additions & 0 deletions src/app/routes/auth/agreement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Agreement from "@/features/auth/components/Agreement";

const AgreementView = () => {
return <Agreement />;
};

export default AgreementView;
7 changes: 7 additions & 0 deletions src/app/routes/auth/phone-number.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import PhoneNumber from "@/features/auth/components/PhoneNumber";

const PhoneNumberInputView = () => {
return <PhoneNumber />;
};

export default PhoneNumberInputView;
5 changes: 5 additions & 0 deletions src/asset/agreement/checked-all.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/asset/agreement/checked-item.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/asset/agreement/unchecked-all.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/asset/agreement/unchecked-item.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/components/ui/header/index.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/navbar/index.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
8 changes: 8 additions & 0 deletions src/config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},

Expand Down
8 changes: 8 additions & 0 deletions src/features/auth/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
173 changes: 173 additions & 0 deletions src/features/auth/components/Agreement.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
onClick={onChange}
style={{ cursor: "pointer", display: "inline-block" }}
>
<img
src={checked ? CheckedIcon : UncheckedIcon}
alt="Checkbox Icon"
width={28}
height={28}
/>
</div>
);
};

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 (
<div className="ml-[20px] mt-[50px] flex h-auto min-h-screen w-[335px] flex-col pb-[74px]">
<h1 className="mb-[40px] h-[58px] w-[335px] text-[24px] font-semibold leading-[28.8px] -tracking-wide text-gray-900">
<span className="text-[#3CC360]">윌고</span> 서비스 이용을 위한 <br />
약관에 동의해주세요
</h1>

<div className="flex h-auto w-[335px] flex-col">
<div className="flex h-[58px] items-center gap-[6px]">
<AgreementCheckbox
checked={checkState.all}
onChange={handleAllCheck}
CheckedIcon={CheckedAllIcon}
UncheckedIcon={UncheckedAllIcon}
/>
<label className="font-pretendard h-[20px] w-[96px] text-[18px] font-medium leading-[20px] -tracking-wide">
전체 동의하기
</label>
</div>

<hr className="mb-6 border-gray-300" />

<div className="flex h-auto w-[335px] flex-col gap-[6px]">
<div className="flex h-[28px] items-center justify-between gap-[6px]">
<div className="flex items-center gap-[6px]">
<AgreementCheckbox
checked={checkState.terms}
onChange={() => handleSingleCheck("terms")}
CheckedIcon={CheckedItemIcon}
UncheckedIcon={UncheckedItemIcon}
/>
<span className="text-[14px] font-medium text-[#3CC360]">
[필수]
</span>
<label className="text-[14px] font-medium text-gray-700">
사용자 이용약관
</label>
</div>

<button
className="font-pretendard h-[14px] w-[41px] text-right text-[12px] font-normal leading-[14px] -tracking-wide text-gray-600 underline decoration-solid"
onClick={() => window.open("약관 URL", "_blank")}
>
전체보기
</button>
</div>

<div className="flex h-[28px] items-center justify-between gap-[6px]">
<div className="flex items-center gap-[6px]">
<AgreementCheckbox
checked={checkState.privacy}
onChange={() => handleSingleCheck("privacy")}
CheckedIcon={CheckedItemIcon}
UncheckedIcon={UncheckedItemIcon}
/>
<span className="text-[14px] font-medium text-[#3CC360]">
[필수]
</span>
<label className="text-[14px] font-medium text-gray-700">
개인정보 처리방침
</label>
</div>

<button
className="font-pretendard h-[14px] w-[41px] text-right text-[12px] font-normal leading-[14px] -tracking-wide text-gray-600 underline decoration-solid"
onClick={() => window.open("약관 URL", "_blank")}
>
전체보기
</button>
</div>
</div>
</div>

<button
disabled={!checkState.all}
onClick={handleSubmit}
className={`fixed bottom-6 ml-[20px] flex h-[56px] w-[335px] items-center justify-center rounded-[8px] py-[10px] text-[16px] font-semibold leading-[20px] ${
!checkState.all
? "cursor-pointer bg-[#3CC360] text-white"
: "cursor-not-allowed bg-gray-300 text-gray-500"
}`}
>
다음
</button>
</div>
);
};

export default Agreement;
81 changes: 81 additions & 0 deletions src/features/auth/components/PhoneNumber.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [isButtonEnabled, setIsButtonEnabled] = useState<boolean>(false);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="flex flex-col items-start space-y-6 p-4">
<div className="mt-[24px]">
<h2 className="font-[Pretendard] text-[20px] font-medium leading-[24px] tracking-[-2.5%] text-gray-800">
리워드 전달을 위해
</h2>
<h2 className="inline font-[Pretendard] text-[24px] font-semibold leading-[28.8px] tracking-[-2.5%] text-gray-800">
휴대폰 번호
</h2>
<h2 className="inline font-[Pretendard] text-[20px] font-medium leading-[24px] tracking-[-2.5%] text-gray-800">
를 입력해주세요.
</h2>
</div>

<div className="mt-[213px] flex items-center gap-[10px]">
<span className="h-[26px] w-[40px] font-[Pretendard] text-[24px] font-medium leading-[26px] tracking-[-2.5%] text-gray-700">
010
</span>
<input
type="tel"
maxLength={11}
placeholder="숫자만 입력"
inputMode="numeric"
className="h-[44px] w-[285px] border-0 border-b border-b-[#BDBDBD] text-center focus:outline-none"
onChange={handleInputChange}
/>
</div>

<div className="mt-[10px]">
<button
onClick={handleSubmit}
disabled={!isButtonEnabled}
className={`h-[56px] w-[335px] justify-between rounded-[8px] p-[10px] text-lg font-semibold text-white ${
isButtonEnabled
? "bg-[#3CC360] text-white"
: "bg-[#E0E0E0] text-gray-400"
}`}
>
완료
</button>
</div>
</div>
);
};

export default PhoneNumber;