diff --git a/README.md b/README.md index 63a8a1e6d..4e6996a7a 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # react-deploy + +# ๐Ÿ“ Requirements + +## 6์ฃผ์ฐจ ์งˆ๋ฌธ + +### ์งˆ๋ฌธ 1. SPA ํŽ˜์ด์ง€๋ฅผ ์ •์  ๋ฐฐํฌ๋ฅผ ํ•˜๋ ค๊ณ  ํ•  ๋•Œ Vercel์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? + +github page๋ฅผ ์ด์šฉํ•˜๊ฑฐ๋‚˜ Vercel ๊ณผ ๋น„์Šทํ•œ Netlify ๋˜ํ•œ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒฝ์šฐ workflow์— yaml ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜์—ฌ CI/CD๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๊ณผ๊ฑฐ์— ec2์— nginx๋ฅผ ์ด์šฉํ•ด ๋ฐฐํฌ๋ฅผ ํ•ด๋ณธ ๊ฒฝํ—˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. + +### ์งˆ๋ฌธ 2. CSRF๋‚˜ XSS ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ผ๊นŒ์š”? + +1. CSRF ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๋ฐฉ๋ฒ• + HttpOnly ์†์„ฑ์„ ์ฟ ํ‚ค์— ์„ค์ •ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์ธก JavaScript์—์„œ ์ฟ ํ‚ค์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. + +2. XXS ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๋ฐฉ๋ฒ• + ์ฟ ํ‚ค์— SameSite ์†์„ฑ์„ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ CSRF ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. + +### ์งˆ๋ฌธ 3. ๋ธŒ๋ผ์šฐ์ € ๋ Œ๋”๋ง ์›๋ฆฌ์—๋Œ€ํ•ด ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. + +๋ธŒ๋ผ์šฐ์ €์˜ ๋ Œ๋”๋ง ์›๋ฆฌ๋Š” HTML์„ ํŒŒ์‹ฑํ•˜์—ฌ DOM ํŠธ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ , CSS๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ CSSOM ํŠธ๋ฆฌ๋ฅผ ๋งŒ๋“  ๋‹ค์Œ ์ด๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ Œ๋”ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ Œ๋” ํŠธ๋ฆฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์š”์†Œ๋“ค์˜ ๋ ˆ์ด์•„์›ƒ์„ ๊ณ„์‚ฐํ•œ ํ›„, ๊ณ„์‚ฐ๋œ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•ด ์š”์†Œ๋“ค์„ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ํ™”๋ฉด์— ๊ทธ๋ฆฌ๋Š” ํŽ˜์ธํŒ…์„ ๊ฑฐ์ณ ์ตœ์ข…์ ์œผ๋กœ ์—ฌ๋Ÿฌ ๋ ˆ์ด์–ด๋ฅผ ํ•ฉ์ณ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค. diff --git a/src/api/hooks/auth/kakao-login.api.ts b/src/api/hooks/auth/kakao-login.api.ts new file mode 100644 index 000000000..04e9c3754 --- /dev/null +++ b/src/api/hooks/auth/kakao-login.api.ts @@ -0,0 +1,32 @@ +import { useMutation } from '@tanstack/react-query'; + +import { BASE_URL, fetchInstance } from '@/api/instance'; + +import type { KakaoResponseData } from './type'; + +export const getKakaoLoginpath = () => `${BASE_URL}/oauth/kakao/login`; + +export const kakaoLoginUser = async (): Promise => { + const response = await fetchInstance.get(getKakaoLoginpath()); + console.log(response); + return response.data; +}; + +export const useKakaoCallback = async (code: string): Promise => { + const response = await fetchInstance.get( + `${BASE_URL}/oauth/kakao/callback?code=${code}`, + ); + return response.data; +}; + +export const useKakaoLogin = () => { + return useMutation({ + mutationFn: kakaoLoginUser, + }); +}; + +export const useKakaoCallbackMutation = () => { + return useMutation({ + mutationFn: useKakaoCallback, + }); +}; diff --git a/src/api/hooks/auth/type.ts b/src/api/hooks/auth/type.ts index e59b4dede..d4f533d44 100644 --- a/src/api/hooks/auth/type.ts +++ b/src/api/hooks/auth/type.ts @@ -7,3 +7,9 @@ export type UserResponseData = { email: string; token: string; }; + +export type KakaoResponseData = { + tokenType: string; + token: string; +}; + diff --git a/src/api/hooks/point/point.api.ts b/src/api/hooks/point/point.api.ts new file mode 100644 index 000000000..4f79c5047 --- /dev/null +++ b/src/api/hooks/point/point.api.ts @@ -0,0 +1,18 @@ +import { useMutation } from '@tanstack/react-query'; + +import { BASE_URL, fetchInstance } from '@/api/instance'; + +import type { PointResponseData } from './type'; + +export const getPointsPath = () => `${BASE_URL}/api/Points`; + +export const getPoints = async () => { + const response = await fetchInstance.post(getPointsPath()); + return response.data; +}; + +export const useGetPoints = () => { + return useMutation({ + mutationFn: getPoints, + }); +}; diff --git a/src/api/hooks/point/type.ts b/src/api/hooks/point/type.ts new file mode 100644 index 000000000..91bc6ed1f --- /dev/null +++ b/src/api/hooks/point/type.ts @@ -0,0 +1,3 @@ +export type PointResponseData = { + point: number; +}; diff --git a/src/assets/kakao_symbol.svg b/src/assets/kakao_symbol.svg new file mode 100644 index 000000000..44ddd6a10 --- /dev/null +++ b/src/assets/kakao_symbol.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/features/Login/KakaoCallbackPage/index.tsx b/src/components/features/Login/KakaoCallbackPage/index.tsx new file mode 100644 index 000000000..c371abad1 --- /dev/null +++ b/src/components/features/Login/KakaoCallbackPage/index.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { useKakaoCallbackMutation } from '@/api/hooks/auth/kakao-login.api'; +import { useAuth } from '@/provider/Auth'; +import { RouterPath } from '@/routes/path'; +import { authSessionStorage } from '@/utils/storage'; + +const KakaoCallbackPage = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { setAuthInfo } = useAuth(); + const mutation = useKakaoCallbackMutation(); + + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const code = queryParams.get('code'); + + if (code) { + mutation.mutate(code, { + onSuccess: (data) => { + if (data.token) { + setAuthInfo({ token: data.token }); + authSessionStorage.set(data.token); + navigate(RouterPath.home); + } else { + alert('๋กœ๊ทธ์ธ ์‹คํŒจ'); + } + }, + onError: (error) => { + console.error('Error during Kakao login:', error); + alert('๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + }, + }); + } + }, [location.search, mutation, navigate, setAuthInfo]); + + return
๋กœ๊ทธ์ธ ์ค‘...
; +}; + +export default KakaoCallbackPage; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index d7e863f2c..7ab10a872 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,80 +1,23 @@ +import { Box, Img, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; -import { useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { useLogin } from '@/api/hooks/auth/login.api'; -import { useRegister } from '@/api/hooks/auth/register.api'; import KAKAO_LOGO from '@/assets/kakao_logo.svg'; -import { Button } from '@/components/common/Button'; -import { UnderlineTextField } from '@/components/common/Form/Input/UnderlineTextField'; -import { Spacing } from '@/components/common/layouts/Spacing'; -import { useAuth } from '@/provider/Auth'; +import Symbol from '@/assets/kakao_symbol.svg'; import { breakpoints } from '@/styles/variants'; export const LoginPage = () => { - const [id, setId] = useState(''); - const [password, setPassword] = useState(''); - const [queryParams] = useSearchParams(); - const { setAuthInfo } = useAuth(); - const loginMutation = useLogin(); - const registerMutation = useRegister(); - - const handleConfirm = async () => { - if (!id || !password) { - alert('์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); - return; - } - try { - const data = await loginMutation.mutateAsync({ email: id, password }); - setAuthInfo({ id: data.email, name: data.email, token: data.token }); - - const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; - window.location.replace(redirectUrl); - } catch (error: unknown) { - alert('๋กœ๊ทธ์ธ ์‹คํŒจ: ' + (error as Error).message); - } - }; - - const handleSignup = async () => { - if (!id || !password) { - alert('์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); - return; - } - - try { - const data = await registerMutation.mutateAsync({ email: id, password }); - setAuthInfo({ id: data.email, name: data.email, token: data.token }); - - alert('ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; - window.location.replace(redirectUrl); - } catch (error: unknown) { - alert('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ: ' + (error as Error).message); - } + const handleKakaoLogin = () => { + window.location.href = '/oauth/kakao/login'; }; return ( - + - setId(e.target.value)} /> - - setPassword(e.target.value)} - /> - - - - - + + + ์นด์นด์˜ค ๋กœ๊ทธ์ธ + ); @@ -104,3 +47,28 @@ const FormWrapper = styled.article` padding: 60px 52px; } `; + +const KakaoLoginButton = styled(Box)` + display: flex; + background-color: #fee500; + width: 100%; + height: 2.5rem; + border-radius: 5px; + text-align: center; + align-items: center; + justify-content: start; + cursor: pointer; +`; + +const LoginText = styled(Text)` + display: flex; + text-align: center; + margin-left: 1rem; +`; + +const StyledImg = styled(Img)` + width: 1.5rem; + text-align: center; + margin-right: 2rem; + margin-left: 1rem; +`; diff --git a/src/pages/MyAccount/index.tsx b/src/pages/MyAccount/index.tsx index 8b0c24588..070c5244f 100644 --- a/src/pages/MyAccount/index.tsx +++ b/src/pages/MyAccount/index.tsx @@ -1,14 +1,20 @@ +import { Box, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; +import { useEffect } from 'react'; +import { useGetPoints } from '@/api/hooks/point/point.api'; import { Button } from '@/components/common/Button'; import { Spacing } from '@/components/common/layouts/Spacing'; import { WishList } from '@/components/features/MyAccount/WishList'; -import { useAuth } from '@/provider/Auth'; import { RouterPath } from '@/routes/path'; import { authSessionStorage } from '@/utils/storage'; export const MyAccountPage = () => { - const { authInfo } = useAuth(); + const { mutate, data, status, error } = useGetPoints(); + + useEffect(() => { + mutate(); + }, [mutate]); const handleLogout = () => { authSessionStorage.set(undefined); @@ -19,7 +25,18 @@ export const MyAccountPage = () => { return ( - {authInfo?.name}๋‹˜ ์•ˆ๋…•ํ•˜์„ธ์š”! + ๊ฐœ๋ฐœ์ž๋‹˜ ์•ˆ๋…•ํ•˜์„ธ์š”! + + + ํฌ์ธํŠธ + {status === 'pending' ? ( + Loading... + ) : status === 'error' ? ( + Error fetching points: {error.message} + ) : ( + {data?.point ?? 0} + )} + - - - ์ด๋ฏธ ๊ฐ€์ž…ํ•˜์…จ์„๊นŒ์š”?   - - - ๋กœ๊ทธ์ธ - - - - - ); -}; - -const Wrapper = styled.div` - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -`; - -const Logo = styled.img` - width: 88px; - color: #333; -`; - -const FormWrapper = styled.article` - width: 100%; - max-width: 580px; - padding: 16px; - - @media screen and (min-width: ${breakpoints.sm}) { - border: 1px solid rgba(0, 0, 0, 0.12); - padding: 60px 52px; - } -`; diff --git a/src/provider/Auth/index.tsx b/src/provider/Auth/index.tsx index 6eb1c1fe5..9dbb0842b 100644 --- a/src/provider/Auth/index.tsx +++ b/src/provider/Auth/index.tsx @@ -4,8 +4,6 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { authSessionStorage } from '@/utils/storage'; type AuthInfo = { - id: string; - name: string; token: string; }; @@ -14,12 +12,11 @@ type AuthContextData = { setAuthInfo: (authInfo: AuthInfo) => void; }; -export const AuthContext = createContext(undefined); +const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { const currentAuthToken = authSessionStorage.get(); - const [isReady, setIsReady] = useState(!currentAuthToken); - + const [isReady, setIsReady] = useState(false); const [authInfo, setAuthInfo] = useState(undefined); const handleAuthInfo = (currentAuthInfo: AuthInfo) => { @@ -29,23 +26,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { if (currentAuthToken) { - setAuthInfo({ - id: currentAuthToken, - name: currentAuthToken, - token: currentAuthToken, - }); - setIsReady(true); + setAuthInfo({ token: currentAuthToken }); } + setIsReady(true); }, [currentAuthToken]); - if (!isReady) return <>; + if (!isReady) return null; + return ( - + {children} ); @@ -53,7 +42,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { export const useAuth = (): AuthContextData => { const context = useContext(AuthContext); - if (context === undefined) { + if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index e2fb73ca2..305954d31 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,14 +1,15 @@ import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { Layout } from '@/components/features/Layout'; +import KakaoCallbackPage from '@/components/features/Login/KakaoCallbackPage'; import { CategoryPage } from '@/pages/Category'; import { GoodsDetailPage } from '@/pages/Goods/Detail'; import { HomePage } from '@/pages/Home'; import { LoginPage } from '@/pages/Login'; import { MyAccountPage } from '@/pages/MyAccount'; import { OrderPage } from '@/pages/Order'; -import { RegisterPage } from '@/pages/Register'; +// import { RegisterPage } from '@/pages/Register'; import { PrivateRoute } from './components/PrivateRoute'; import { RouterPath } from './path'; @@ -60,8 +61,8 @@ const router = createBrowserRouter([ element: , }, { - path: RouterPath.register, - element: , + path: RouterPath.kakaoCallback, + element: , }, ]); diff --git a/src/routes/path.ts b/src/routes/path.ts index 921ff6dcb..00cfc661e 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -6,7 +6,7 @@ export const RouterPath = { productsDetail: '/products/:productId', order: '/order', login: '/login', - register: '/register', + kakaoCallback: '/oauth/kakao/callback', notFound: '*', }; @@ -21,4 +21,5 @@ export const getDynamicPath = { ':productId', typeof goodsId === 'number' ? goodsId.toString() : goodsId, ), + kakaoCallback: (code: string) => `${RouterPath.kakaoCallback}?code=${encodeURIComponent(code)}`, // ๋‹ค์ด๋‚ด๋ฏน ๊ฒฝ๋กœ ์ƒ์„ฑ์„ ์œ„ํ•œ ํ•จ์ˆ˜ ์ถ”๊ฐ€ };