diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/frontend/.husky/pre-push b/frontend/.husky/pre-push old mode 100644 new mode 100755 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a935faf9f..0b8dd5cc7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,8 @@ import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider } from 'styled-components'; +import { AuthProvider } from '@hooks/context/auth'; + import router from '@routes/router'; import { GlobalStyle } from '@styles/globalStyle'; @@ -11,12 +13,14 @@ import { theme } from '@styles/theme'; const queryClient = new QueryClient(); const App = () => ( - - - - - - + + + + + + + + ); export default App; diff --git a/frontend/src/api/post.ts b/frontend/src/api/post.ts index d68361c67..4666b886b 100644 --- a/frontend/src/api/post.ts +++ b/frontend/src/api/post.ts @@ -9,6 +9,8 @@ import { deleteFetch, } from '@utils/fetch'; +const BASE_URL = process.env.VOTOGETHER_BASE_URL; + export const votePost = async (postId: number, optionId: number) => { return await postFetch(`/posts/${postId}/options/${optionId}`, ''); }; @@ -29,11 +31,11 @@ export const getPost = async (postId: number): Promise => { }; export const createPost = async (newPost: FormData) => { - return await multiPostFetch('/posts', newPost); + return await multiPostFetch(`${BASE_URL}/posts`, newPost); }; export const editPost = async (postId: number, updatedPost: FormData) => { - return await multiPutFetch(`/posts/${postId}`, updatedPost); + return await multiPutFetch(`http://3.35.232.54/api/posts/${postId}`, updatedPost); }; export const removePost = async (postId: number) => { diff --git a/frontend/src/api/wus/userInfo.ts b/frontend/src/api/wus/userInfo.ts index 69d8ff0df..e712fc403 100644 --- a/frontend/src/api/wus/userInfo.ts +++ b/frontend/src/api/wus/userInfo.ts @@ -14,7 +14,7 @@ export const transformUserInfoResponse = (userInfo: UserInfoResponse): User => { }; }; -const BASE_URL = process.env.VOTOGETHER_MOCKING_URL; +const BASE_URL = process.env.VOTOGETHER_BASE_URL; export const getUserInfo = async () => { const userInfo = await getFetch(`${BASE_URL}/members/me`); diff --git a/frontend/src/assets/kakao_login_medium_wide.svg b/frontend/src/assets/kakao_login_medium_wide.svg new file mode 100644 index 000000000..cbdb3098f --- /dev/null +++ b/frontend/src/assets/kakao_login_medium_wide.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 54b8dd850..30f32ca06 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -121,7 +121,7 @@ export default function PostForm({ data, mutate, isError, error }: PostFormProps // 글 수정의 경우 작성시간을 기준으로 마감시간 옵션을 더한다. // 마감시간 옵션을 선택 안했다면 기존의 마감 시간을 유지한다. }; - formData.append('texts', JSON.stringify(updatedPostTexts)); + formData.append('request', JSON.stringify(updatedPostTexts)); mutate(formData); diff --git a/frontend/src/components/common/Layout/index.tsx b/frontend/src/components/common/Layout/index.tsx index fdafd2376..9438d9aa7 100644 --- a/frontend/src/components/common/Layout/index.tsx +++ b/frontend/src/components/common/Layout/index.tsx @@ -1,6 +1,8 @@ -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; +import { AuthContext } from '@hooks/context/auth'; + import Dashboard from '@components/common/Dashboard'; import WideHeader from '@components/common/WideHeader'; @@ -15,8 +17,8 @@ interface LayoutProps extends PropsWithChildren { export default function Layout({ children, isSidebarVisible }: LayoutProps) { const navigate = useNavigate(); - //추후 구현 예정 - const userInfo = undefined; + const { loggedInfo } = useContext(AuthContext); + const categoryList = MOCK_FAVORITE_CATEGORIES; const selectedCategory = undefined; const handleLogoutClick = () => {}; @@ -34,7 +36,7 @@ export default function Layout({ children, isSidebarVisible }: LayoutProps) { {isSidebarVisible && ( {}; diff --git a/frontend/src/hooks/context/auth.tsx b/frontend/src/hooks/context/auth.tsx new file mode 100644 index 000000000..f94924615 --- /dev/null +++ b/frontend/src/hooks/context/auth.tsx @@ -0,0 +1,37 @@ +import React, { Dispatch, SetStateAction, createContext, useEffect, useState } from 'react'; + +import { LoggedInfo } from '@type/user'; + +import { useUserInfo } from '@hooks/query/user/useUserInfo'; + +import { getCookieToken } from '@utils/cookie'; + +interface Auth { + loggedInfo: LoggedInfo; + setLoggedInfo: Dispatch>; +} + +const notLoggedInfo: LoggedInfo = { + isLogged: false, + accessToken: '', +}; + +export const AuthContext = createContext({} as Auth); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [loggedInfo, setLoggedInfo] = useState(notLoggedInfo); + const { data: userInfo } = useUserInfo(loggedInfo.isLogged); + + useEffect(() => { + if (userInfo) setLoggedInfo(origin => ({ ...origin, userInfo })); + }, [userInfo]); + + useEffect(() => { + const accessToken = getCookieToken().accessToken; + if (accessToken) setLoggedInfo(origin => ({ ...origin, accessToken })); + }, []); + + return ( + {children} + ); +} diff --git a/frontend/src/hooks/query/user/useUserInfo.ts b/frontend/src/hooks/query/user/useUserInfo.ts index fe6eb8899..27a2e22de 100644 --- a/frontend/src/hooks/query/user/useUserInfo.ts +++ b/frontend/src/hooks/query/user/useUserInfo.ts @@ -6,9 +6,9 @@ import { getUserInfo } from '@api/wus/userInfo'; import { QUERY_KEY } from '@constants/queryKey'; -export const useUserInfo = () => { +export const useUserInfo = (isLogged: boolean) => { const { data, error, isLoading, isError } = useQuery( - [QUERY_KEY.USER_INFO], + [QUERY_KEY.USER_INFO, isLogged], getUserInfo ); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index e494a44a0..0e00ec7b4 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -5,13 +5,13 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import { worker } from './mocks/worker'; -if (process.env.NODE_ENV === 'development') { - worker.start(); -} +// if (process.env.NODE_ENV === 'development') { +// worker.start(); +// } const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - - - + // + + // ); diff --git a/frontend/src/pages/auth/Login.tsx b/frontend/src/pages/auth/Login.tsx new file mode 100644 index 000000000..8a94bb392 --- /dev/null +++ b/frontend/src/pages/auth/Login.tsx @@ -0,0 +1,60 @@ +import { useNavigate } from 'react-router-dom'; + +import styled from 'styled-components'; + +import LogoButton from '@components/common/LogoButton'; +import SquareButton from '@components/common/SquareButton'; + +import kakaoLogin from '@assets/kakao_login_medium_wide.svg'; + +export default function Login() { + const navigate = useNavigate(); + const CLIENT_ID = `${process.env.VOTOGETHER_REST_API_KEY}`; + const REDIRECT_URI = `${process.env.VOTOGETHER_CLIENT_REDIRECT_URL}`; + const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`; + + return ( + + + + (window.location.href = kakaoURL)} + /> + navigate('/')} theme="blank" style={{ height: '35px' }}> + 비회원으로 이용하기 + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + gap: 150px; + width: 320px; + height: 1vh; + margin-top: 300px; + position: fixed; + left: 10%; + @media (min-width: 576px) { + left: 30%; + } +`; + +const KaKaoLoginImg = styled.img` + width: 255px; + height: 35px; +`; + +const ButtonWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; + gap: 20px; + width: 230px; +`; diff --git a/frontend/src/pages/auth/Redirection.tsx b/frontend/src/pages/auth/Redirection.tsx new file mode 100644 index 000000000..af890c64f --- /dev/null +++ b/frontend/src/pages/auth/Redirection.tsx @@ -0,0 +1,61 @@ +import { useContext, useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import { AuthResponse } from '@type/auth'; + +import { AuthContext } from '@hooks/context/auth'; + +import { getCookieToken, setCookieToken } from '@utils/cookie'; +import { getFetch } from '@utils/fetch'; + +const getAuthInfo = async (url: string): Promise => { + return await getFetch(url); +}; + +export default function Redirection() { + const { loggedInfo, setLoggedInfo } = useContext(AuthContext); + const [errorMessage, setErrorMessage] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [params] = useSearchParams(); + + const navigate = useNavigate(); + + useEffect(() => { + (async () => { + setIsLoading(true); + setErrorMessage(''); + + const code = params.get('code'); + const REGISTER_API_URL = `${process.env.VOTOGETHER_BASE_URL}/auth/kakao/callback?code=${code}`; + + await getAuthInfo(REGISTER_API_URL) + .finally(() => { + setIsLoading(false); + }) + .catch(error => { + setErrorMessage(error.message); + }) + .then(res => { + if (!res) return setErrorMessage('잘못된 형식의 response'); + + const { accessToken } = res; + setCookieToken('accessToken', accessToken); + + setLoggedInfo({ + ...loggedInfo, + accessToken: getCookieToken().accessToken, + isLogged: true, + }); + + navigate('/'); + }); + })(); + }, [navigate, loggedInfo, setLoggedInfo]); + + return ( +
+ {isLoading && '로그인 중입니다...'} + {errorMessage && errorMessage} +
+ ); +} diff --git a/frontend/src/routes/router.tsx b/frontend/src/routes/router.tsx index 8f01b522f..dedb88f69 100644 --- a/frontend/src/routes/router.tsx +++ b/frontend/src/routes/router.tsx @@ -1,5 +1,7 @@ import { createBrowserRouter } from 'react-router-dom'; +import Login from '@pages/auth/Login'; +import Redirection from '@pages/auth/Redirection'; import Home from '@pages/Home'; import MyInfo from '@pages/MyInfo'; import CreatePost from '@pages/post/CreatePost'; @@ -13,10 +15,15 @@ const router = createBrowserRouter([ { path: PATH.HOME, element: , - children: [ - { path: 'search', element: }, - { path: 'login', element: }, - ], + children: [{ path: 'search', element: }], + }, + { + path: PATH.LOGIN, + element: , + }, + { + path: 'auth/kakao/callback', + element: , }, { path: PATH.POST, @@ -34,7 +41,7 @@ const router = createBrowserRouter([ path: 'result/:postId', element: , }, - { path: 'category/:categoryId', element: }, + { path: 'posts/category/:categoryId', element: }, ], }, { diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 000000000..015668185 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,3 @@ +export interface AuthResponse { + accessToken: string; +} diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index 7e6cf2a8b..2bc5ed5fe 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -17,3 +17,10 @@ export interface UserInfoResponse { export interface ModifyNicknameRequest { nickname: string; } + + +export interface LoggedInfo { + accessToken: string; + isLogged: boolean; + userInfo?: UserInfoResponse; +} diff --git a/frontend/src/utils/cookie/index.ts b/frontend/src/utils/cookie/index.ts new file mode 100644 index 000000000..bb7cededd --- /dev/null +++ b/frontend/src/utils/cookie/index.ts @@ -0,0 +1,17 @@ +type CookieKey = 'accessToken' | 'refreshToken'; + +export const setCookieToken = (key: CookieKey, token: string) => { + //secure 속성은 현재 dev에서는 http로 진행중이기 때문에 사용할 수 없음 + document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(token)}; path=/`; +}; + +// token형식 = "key=value; key=value; key=value" +export function getCookieToken() { + const cookie = document.cookie; + const cookieContent = {} as { [key: string]: any }; + cookie.split('; ').forEach(pair => { + const [key, value] = pair.split('='); + cookieContent[key] = value; + }); + return cookieContent as Record; +} diff --git a/frontend/src/utils/fetch.ts b/frontend/src/utils/fetch.ts index 32a065c5c..c705a80a2 100644 --- a/frontend/src/utils/fetch.ts +++ b/frontend/src/utils/fetch.ts @@ -1,17 +1,31 @@ +import { getCookieToken } from './cookie'; + const headers = { 'Content-Type': 'application/json;charset=utf-8', Authorization: `Bearer `, }; -const multiHeaders = { - 'Content-Type': 'multipart/form-data', - Authorization: `Bearer `, +const makeFetchHeaders = () => { + const cookie = getCookieToken(); + + return { + ...headers, + Authorization: `Bearer ${cookie.accessToken}`, + }; +}; + +const makeFetchMultiHeaders = () => { + const cookie = getCookieToken(); + + return { + Authorization: `Bearer ${cookie.accessToken}`, + }; }; export const getFetch = async (url: string): Promise => { const response = await fetch(url, { method: 'GET', - headers, + headers: makeFetchHeaders(), }); const data = await response.json(); @@ -27,7 +41,7 @@ export const postFetch = async (url: string, body: T): Promise = const response = await fetch(url, { method: 'POST', body: JSON.stringify(body), - headers, + headers: makeFetchHeaders(), }); const data = await response.json(); @@ -43,7 +57,7 @@ export const putFetch = async (url: string, body: T): Promise => const response = await fetch(url, { method: 'PUT', body: JSON.stringify(body), - headers, + headers: makeFetchHeaders(), }); const data = await response.json(); @@ -58,7 +72,7 @@ export const putFetch = async (url: string, body: T): Promise => export const patchFetch = async (url: string, body?: T) => { const response = await fetch(url, { method: 'PATCH', - headers, + headers: makeFetchHeaders(), body: JSON.stringify(body), }); @@ -74,7 +88,7 @@ export const patchFetch = async (url: string, body?: T) => { export const deleteFetch = async (url: string) => { const response = await fetch(url, { method: 'DELETE', - headers, + headers: makeFetchHeaders(), }); return response; @@ -84,7 +98,7 @@ export const multiPostFetch = async (url: string, body: FormData) => { const response = await fetch(url, { method: 'POST', body, - headers: multiHeaders, + headers: makeFetchMultiHeaders(), }); const data = await response.json(); @@ -100,7 +114,7 @@ export const multiPutFetch = async (url: string, body: FormData) => { const response = await fetch(url, { method: 'PUT', body, - headers: multiHeaders, + headers: makeFetchMultiHeaders(), }); const data = await response.json();