diff --git a/.env.development b/.env.development index e69de29..9c4f7f1 100644 --- a/.env.development +++ b/.env.development @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_URL=https://api.modumozu.com/ +NEXT_PUBLIC_KAKAO_KEY="5d83cd0432c767f46c20dceb0f8f3947" \ No newline at end of file diff --git a/.env.production b/.env.production index 99172a4..f3616f6 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=URL \ No newline at end of file +NEXT_PUBLIC_API_URL=https://api.modumozu.com/ \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de0ca1d..0d60de2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@storybook/addon-viewport': specifier: ^7.4.0 @@ -12456,3 +12452,7 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/public/images/logo_1.png b/public/images/logo_1.png new file mode 100644 index 0000000..ce948a8 Binary files /dev/null and b/public/images/logo_1.png differ diff --git a/public/images/step_1.png b/public/images/step_1.png new file mode 100644 index 0000000..98b87bd Binary files /dev/null and b/public/images/step_1.png differ diff --git a/public/images/step_2.png b/public/images/step_2.png new file mode 100644 index 0000000..653a36c Binary files /dev/null and b/public/images/step_2.png differ diff --git a/public/images/step_3.png b/public/images/step_3.png new file mode 100644 index 0000000..a4b550b Binary files /dev/null and b/public/images/step_3.png differ diff --git a/src/api/common.ts b/src/api/common.ts index c546334..1eab289 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -1,8 +1,14 @@ -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { getStorage } from "@/util/storage"; +import axios from "axios"; -const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL }); +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { + Authorization: `Bearer ${getStorage("ACCESS_TOKEN")}`, + }, +}); -instance.interceptors.request.use( +api.interceptors.request.use( (instance) => { return instance; }, @@ -10,7 +16,7 @@ instance.interceptors.request.use( throw new Error(error); }, ); -instance.interceptors.response.use( +api.interceptors.response.use( (instance) => { return instance; }, @@ -19,4 +25,4 @@ instance.interceptors.response.use( }, ); -export default instance; +export default api; diff --git a/src/app/api/kakao/route.ts b/src/app/api/kakao/route.ts new file mode 100644 index 0000000..35a9989 --- /dev/null +++ b/src/app/api/kakao/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + const res = await fetch("https://api.modumozu.com/oauth2/authorization/kakao", { + headers: { + Accept: "application / json", + }, + }); + console.log("res ==> ", res); + const data = await res.json(); + console.log("data ==> ", data); + return NextResponse.json({ data }); +} diff --git a/src/app/globals.css b/src/app/globals.css index 8effc08..5bcef02 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -93,6 +93,7 @@ video { border: 0; font-family: pretendard, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + box-sizing: border-box; } article, @@ -111,6 +112,10 @@ section { body { line-height: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } ol, diff --git a/src/app/kakao/page.tsx b/src/app/kakao/page.tsx new file mode 100644 index 0000000..091fc9d --- /dev/null +++ b/src/app/kakao/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import Button from "@/components/common/Button"; +import { BottomSheet } from "@/components/common/bottomSheet/BottomSheet"; +import AgreeAllButton from "@/components/kakao/AgreeAllButton"; +import Term from "@/components/kakao/Term"; +import TERMS from "@/constants/terms"; +import { setStorage } from "@/util/storage"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import styled from "styled-components"; + +const Kakao = () => { + const router = useRouter(); + const [agreedTerms, setAgreeTerms] = useState([]); + + const setTokens = () => { + const searchParams = new URL(window.location.href).searchParams; + const accessToken = searchParams.get("accessToken"); + const refreshToken = searchParams.get("refreshToken"); + + if (accessToken && refreshToken) { + setStorage("ACCESS_TOKEN", accessToken); + setStorage("REFRESH_TOKEN", refreshToken); + } + }; + const handleCheckAllClick = () => { + if (agreedTerms.length === TERMS.length) { + setAgreeTerms([]); + } else { + setAgreeTerms(TERMS.map((_, index) => index)); + } + }; + const handleCheckClick = (index: number) => { + if (agreedTerms.includes(index)) { + setAgreeTerms((prev) => prev.filter((prevIndex) => prevIndex !== index)); + } else { + setAgreeTerms((prev) => prev.concat([index])); + } + }; + const handleSignUpClick = async () => { + if (agreedTerms.length === TERMS.length) { + setTokens(); + router.push("/on-boarding"); + } + }; + + useEffect(() => { + let token = new URL(window.location.href).searchParams.get("accessToken"); + + if (!token) { + router.push("/"); + } + }, []); + + return ( + {}}> + + + + {TERMS.map((term, index) => { + return ( + handleCheckClick(index)} + checked={agreedTerms.includes(index)} + /> + ); + })} + + + + + ); +}; + +export default Kakao; + +const BottomSheetWrap = styled.div` + padding-bottom: 20px; +`; +const TermList = styled.div` + padding: 12px 16px 24px 16px; + > * + * { + margin-top: 9px; + } +`; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c37a979..1d0bec2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,6 +19,10 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + + + + diff --git a/src/app/on-boarding/page.tsx b/src/app/on-boarding/page.tsx new file mode 100644 index 0000000..d417193 --- /dev/null +++ b/src/app/on-boarding/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useEffect } from "react"; + +const OnBoarding = () => { + useEffect(() => {}, []); + + return ( +
+ 온보딩 + +
+ ); +}; + +export default OnBoarding; diff --git a/src/app/page.tsx b/src/app/page.tsx index 098e2c4..d4ca425 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,5 @@ +import WorkThrough from "@/components/workThrough"; + export default function Home() { - return
모두모주
; + return ; } diff --git a/src/components/common/CheckBox.tsx b/src/components/common/CheckBox.tsx index 708fb64..a1a5cbe 100644 --- a/src/components/common/CheckBox.tsx +++ b/src/components/common/CheckBox.tsx @@ -37,6 +37,8 @@ const CheckBox: FC = (props) => { export default CheckBox; const CheckBoxWrap = styled.label` + display: flex; + align-items: center; input { display: none; } diff --git a/src/components/kakao/AgreeAllButton.tsx b/src/components/kakao/AgreeAllButton.tsx new file mode 100644 index 0000000..b17caf1 --- /dev/null +++ b/src/components/kakao/AgreeAllButton.tsx @@ -0,0 +1,34 @@ +"use client"; + +import styled from "styled-components"; +import CheckBox from "../common/CheckBox"; +import colors from "@/styles/colors"; +import { getFonts } from "@/styles/fonts"; +import { FC } from "react"; + +interface AgreeAllButtonProps { + onClick: () => void; + checked: boolean; +} + +const AgreeAllButton: FC = ({ onClick, checked }) => { + return ( + + + 전체 동의 + + ); +}; + +export default AgreeAllButton; +const AgreeAllButtonWrap = styled.div` + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + width: 100%; + border: 1px solid ${colors.GRAY[2]}; + border-radius: 12px; + ${getFonts("H4_SEMIBOLD")} + color:${colors.FONT_LIGHT.PRIMARY}; +`; diff --git a/src/components/kakao/Term.tsx b/src/components/kakao/Term.tsx new file mode 100644 index 0000000..de55367 --- /dev/null +++ b/src/components/kakao/Term.tsx @@ -0,0 +1,47 @@ +"use client"; + +import colors from "@/styles/colors"; +import { getFonts } from "@/styles/fonts"; +import CaretIcon from "@/svg/CaretIcon"; +import { FC } from "react"; +import styled from "styled-components"; +import CheckBox from "../common/CheckBox"; + +interface TermProps { + title: string; + link: string; + checked: boolean; + onCheckClick: () => void; +} + +const Term: FC = ({ title, link, checked, onCheckClick }) => { + const handleShowDetailClick = () => {}; + + return ( + + + + {title} + + + + ); +}; + +export default Term; + +const TermWrap = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; +const TermTitleWrap = styled.div` + display: flex; + align-items: center; + gap: 0 12px; + ${getFonts("H5_REGULAR")} + color:${colors.FONT_LIGHT.PRIMARY}; +`; +const TermDetailButton = styled(CaretIcon.right)` + cursor: pointer; +`; diff --git a/src/components/workThrough/KakaoLoginButton.tsx b/src/components/workThrough/KakaoLoginButton.tsx new file mode 100644 index 0000000..d42c748 --- /dev/null +++ b/src/components/workThrough/KakaoLoginButton.tsx @@ -0,0 +1,34 @@ +"use client"; + +import colors from "@/styles/colors"; +import { getFonts } from "@/styles/fonts"; +import KakaoLoginIcon from "@/svg/KakaoLoginIcon"; +import { FC } from "react"; +import styled from "styled-components"; + +interface KakaoLoginButtonProps { + onClick: () => void; +} + +const KakaoLoginButton: FC = ({ onClick }) => { + return ( + + + 카카오로 간편 로그인 + + ); +}; + +export default KakaoLoginButton; + +const KakaoLoginButtonWrap = styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 6px 0; + background-color: #fee500; + border-radius: 26px; + color: ${colors.FONT_LIGHT.PRIMARY}; + ${getFonts("BUTTON1_BOLD")} +`; diff --git a/src/components/workThrough/WorkThroughContent.tsx b/src/components/workThrough/WorkThroughContent.tsx new file mode 100644 index 0000000..755bb57 --- /dev/null +++ b/src/components/workThrough/WorkThroughContent.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { FC, ReactNode } from "react"; +import styled from "styled-components"; + +interface WorkThroughContentProps { + header: ReactNode; + content: ReactNode; + blur?: boolean; +} + +const WorkThroughContent: FC = ({ header, content, blur = true }) => { + return ( + <> + {header} + + {content} + {blur && } + + + ); +}; + +export default Object.assign(WorkThroughContent); + +const WorkThroughWrap = styled.div` + flex: 1; + position: relative; + margin-top: 20px; + height: 30px; + overflow: hidden; +`; +const Content = styled.div` + margin: 60px auto 0px auto; + width: 70%; +`; +const Blur = styled.div` + position: absolute; + height: 375px; + bottom: 0; + width: 100%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0) -11.99%, #fff 27.38%); +`; diff --git a/src/components/workThrough/WorkThroughMain.tsx b/src/components/workThrough/WorkThroughMain.tsx new file mode 100644 index 0000000..858f7e8 --- /dev/null +++ b/src/components/workThrough/WorkThroughMain.tsx @@ -0,0 +1,42 @@ +"use client"; + +import colors from "@/styles/colors"; +import { getFonts } from "@/styles/fonts"; +import styled from "styled-components"; + +const Main = () => { + return ( + + + + 모두를 위한 공모주 + 복잡한 신청 과정을 한 큐에! 누구나 쉽게 시작하는 공모주 + + + ); +}; + +export default Main; + +const MainWrap = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: calc(100% - 120px); +`; +const MainContentWrap = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 220px; +`; +const MainTitle = styled.h1` + margin-top: 20px; + ${getFonts("H1_BOLD")} + color:${colors.FONT_LIGHT.PRIMARY}; +`; +const MainDescription = styled.div` + margin-top: 8px; + ${getFonts("H3_REGULAR")}; + color: ${colors.FONT_LIGHT.SECONDARY}; +`; diff --git a/src/components/workThrough/index.tsx b/src/components/workThrough/index.tsx new file mode 100644 index 0000000..581a096 --- /dev/null +++ b/src/components/workThrough/index.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useEffect, useState } from "react"; +import styled from "styled-components"; +import colors from "@/styles/colors"; +import { getFonts } from "@/styles/fonts"; +import Button from "../common/Button"; +import WorkThroughContent from "./WorkThroughContent"; +import KakaoLoginButton from "./KakaoLoginButton"; +import WorkThroughMain from "./WorkThroughMain"; + +const WorkThrough = () => { + const [step, setStep] = useState(1); + + const handleNextStepClick = () => { + setStep((step) => step + 1); + }; + + const renderPage = () => { + switch (step) { + case 1: + return ; + case 2: + return ( + 보유 계좌를 선택하면,} + content={} + /> + ); + case 3: + return ( + + 내 계좌에 따라 +
+ 청약 가능한 공모주를 +
한 눈에 알 수 있어요 + + } + content={} + /> + ); + case 4: + return ( + + 계좌 개설의 제약에 대해 +
+ 필요한 그 때 바로 +
+ 안내해드려요 + + } + content={} + /> + ); + default: + return null; + } + }; + + const handleKakaoClick = () => { + window.location.href = "https://api.modumozu.com/oauth2/authorization/kakao"; + }; + + return ( + + {renderPage()} + + {step <= 3 ? ( + <> + + + + ) : ( + <> + + + )} + + + ); +}; +export default WorkThrough; +const WorkThroughWrap = styled.div` + position: relative; + display: flex; + flex-direction: column; + min-height: calc(var(--var, 1vh) * 100); + height: 1vh; + background-color: ${colors.WHITE}; + overflow: hidden; +`; +const ContentWrap = styled.div` + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +`; +const ButtonWrap = styled.div` + gap: 8px 0; + width: calc(100%); + height: 100px; + position: absolute; + bottom: 20px; + > * + * { + margin-top: 8px; + } + padding: 0px 16px; +`; +const Description = styled.div` + ${getFonts("H2_SEMIBOLD")} + color:${colors.FONT_LIGHT.PRIMARY}; + padding: 20px; + white-space: pre-wrap; +`; +const StrongText = styled.span` + color: ${colors.FONT.PRIMARY}; +`; diff --git a/src/constants/storage.ts b/src/constants/storage.ts new file mode 100644 index 0000000..900fcea --- /dev/null +++ b/src/constants/storage.ts @@ -0,0 +1,6 @@ +const StorageKeys = { + ACCESS_TOKEN: "accessToken", + REFRESH_TOKEN: "refreshToken", +}; + +export default StorageKeys; diff --git a/src/constants/terms.ts b/src/constants/terms.ts new file mode 100644 index 0000000..862f9dc --- /dev/null +++ b/src/constants/terms.ts @@ -0,0 +1,7 @@ +const TERMS = [ + { title: "서비스 이용 동의 (필수)", link: "" }, + { title: "개인정보 처리방침 (필수)", link: "" }, + { title: "만 14세 이상 확인 (필수)", link: "" }, +]; + +export default TERMS; diff --git a/src/stories/KakaoLoginButton.tsx b/src/stories/KakaoLoginButton.tsx new file mode 100644 index 0000000..0f82ab2 --- /dev/null +++ b/src/stories/KakaoLoginButton.tsx @@ -0,0 +1,19 @@ +import KakaoLoginButton from "@/components/workThrough/KakaoLoginButton"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Common/KakaoLoginButton", + component: KakaoLoginButton, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + onClick: () => { + console.log("카카오 로그인으로~"); + }, + }, +}; diff --git a/src/svg/DeleteIcon.tsx b/src/svg/DeleteIcon.tsx new file mode 100644 index 0000000..fb7927e --- /dev/null +++ b/src/svg/DeleteIcon.tsx @@ -0,0 +1,18 @@ +import { FC, SVGProps } from "react"; + +interface DeleteIconProps extends SVGProps {} + +const DeleteIcon: FC = (props) => { + return ( + + + + ); +}; + +export default DeleteIcon; diff --git a/src/svg/KakaoLoginIcon.tsx b/src/svg/KakaoLoginIcon.tsx new file mode 100644 index 0000000..53c7e04 --- /dev/null +++ b/src/svg/KakaoLoginIcon.tsx @@ -0,0 +1,14 @@ +const KakaoLoginIcon = () => { + return ( + + + + ); +}; + +export default KakaoLoginIcon; diff --git a/src/util/kakaoLogin.ts b/src/util/kakaoLogin.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/util/storage.ts b/src/util/storage.ts new file mode 100644 index 0000000..549bdd0 --- /dev/null +++ b/src/util/storage.ts @@ -0,0 +1,8 @@ +import StorageKeys from "@/constants/storage"; + +export const getStorage = (key: keyof typeof StorageKeys) => { + return localStorage.getItem(StorageKeys[key]); +}; +export const setStorage = (key: keyof typeof StorageKeys, value: any) => { + localStorage.setItem(StorageKeys[key], value); +};