diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a67342f..b31a113 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,5 @@ + + ## 💻 작업 내용 diff --git a/src/app/App.tsx b/src/app/App.tsx index b80f1d8..95a2a52 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,10 +1,6 @@ -import { useState } from 'react'; +import { useEffect } from 'react'; import { ThemeProvider } from 'styled-components'; -import { - QueryCache, - QueryClient, - QueryClientProvider, -} from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import GlobalStyles from '@/shared/styles/globalStyles'; import theme from '@/shared/styles/theme'; import AppRouter from '@/app/Router'; @@ -12,29 +8,18 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import GlobalErrorBoundary from '@/app/GlobalErrorBoundary'; import Toast from '@/shared/ui/Toast'; import useApiError from '@/shared/hooks/useApiError'; +import { queryClient, setupQueryClient } from '@/shared/api/queryClient'; import Layout from './Layout'; import 'react-toastify/dist/ReactToastify.css'; function App() { const { handleError: handleQueryError } = useApiError({}); const { handleError: handleMutationError } = useApiError({}); - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - mutations: { - onError: handleMutationError, - throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다. - }, - queries: { - throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다. - }, - }, - queryCache: new QueryCache({ - onError: handleQueryError, - }), - }) - ); + + useEffect(() => { + setupQueryClient(handleQueryError, handleMutationError); + }, [handleMutationError, handleQueryError]); + return ( diff --git a/src/app/Router.tsx b/src/app/Router.tsx index 39e08cb..6f1cfff 100644 --- a/src/app/Router.tsx +++ b/src/app/Router.tsx @@ -52,10 +52,12 @@ const NotFound = lazy(() => function AppRouter() { const router = createBrowserRouter([ { - path: '', + id: 'root', element: ( - + loading...}> + + ), errorElement: , @@ -65,24 +67,29 @@ function AppRouter() { element: , }, { - path: ROUTE.onboarding, - element: , - }, - { - path: ROUTE.home, - element: , - loader: checkAuth, - }, - { - path: ROUTE.selectGroup, - element: , - loader: checkAuth, - }, - { - path: ROUTE.groupSetup, - element: , + id: 'protected', loader: checkAuth, + element: , + children: [ + { + path: ROUTE.onboarding, + element: , + }, + { + path: ROUTE.home, + element: , + }, + { + path: ROUTE.selectGroup, + element: , + }, + { + path: ROUTE.groupSetup, + element: , + }, + ], }, + // TODO : 로그인 기능으로 변경될 예정 { path: ROUTE.createBill, element: , @@ -98,20 +105,15 @@ function AppRouter() { element: , loader: groupTokenUrlLoader, }, + { + path: '*', + element: , + }, ], }, - { - path: '*', - element: , - }, ]); - return ( - // TODO : 로딩 페이지 추가하기 - loading...}> - - - ); + return ; } export default AppRouter; diff --git a/src/entities/auth/api/auth.ts b/src/entities/auth/api/auth.ts index a342663..e955802 100644 --- a/src/entities/auth/api/auth.ts +++ b/src/entities/auth/api/auth.ts @@ -1,5 +1,6 @@ import axiosInstance from '@/shared/api/axios'; +// CHECK - 게스트 토큰 정책 제거 가능성 있음 export interface GuestTokenData { accessToken: string; refreshToken: string; @@ -11,3 +12,12 @@ export const getGuestToken = async (): Promise => { const response = await axiosInstance.get('/user/guest/token'); return response.data; }; + +// ========== + +export const getAuth = async () => { + const response = await axiosInstance.get('/user/auth/check', { + useMock: true, + }); + return response.data; +}; diff --git a/src/entities/auth/lib/checkAuth.ts b/src/entities/auth/lib/checkAuth.ts index fe55c3a..3f87391 100644 --- a/src/entities/auth/lib/checkAuth.ts +++ b/src/entities/auth/lib/checkAuth.ts @@ -1,21 +1,31 @@ import { redirect } from 'react-router'; import { ROUTE } from '@/shared/config/route'; +import { queryClient } from '@/shared/api/queryClient'; +import { getAuth } from '../api/auth'; /** * 페이지에 접근하기 전에 실행되는 함수 - * -> accessToken이 있는지 확인하고, 없으면 login 으로 redirect - * - * @Todo accessToken 및 refreshToken 저장 방식 수정 후 로직 추가 - * 1. 메모리(전역상태변수로 관리)의 accessToken 존재 여부 확인 - * 2. 메모리에 accessToken이 없다면 httpOnly 쿠키를 확인 -> refreshToken API 호출(refreshToken으로 accessToken 재발급) - * 3. 쿠키에 refreshToken이 없다면 로그인 페이지로 redirect * */ -const checkAuth = () => { - const token = localStorage.getItem('accessToken'); - if (!token) { +const checkAuth = async () => { + try { + const user = await queryClient.ensureQueryData({ + queryKey: ['auth', 'user'], + queryFn: getAuth, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }); + + if (!user || !user.authenticated) { + throw new Error('Unauthorized'); + } + + return user; + } catch { + // NOTE - 로그인 성공 후 이전 페이지로 돌아가기 위한 로직 + // const redirectTo = new URL(request.url).pathname; + // return redirect(`${ROUTE.login}?redirectTo=${redirectTo}`); return redirect(ROUTE.login); } - return null; }; export default checkAuth; diff --git a/src/entities/auth/lib/kakaoLogin.ts b/src/entities/auth/lib/kakaoLogin.ts new file mode 100644 index 0000000..668ad59 --- /dev/null +++ b/src/entities/auth/lib/kakaoLogin.ts @@ -0,0 +1,17 @@ +const KAKAO_CLIENT_ID = import.meta.env.VITE_KAKAO_CLIENT_ID; +const KAKAO_REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI; + +if (!KAKAO_CLIENT_ID || !KAKAO_REDIRECT_URI) { + throw new Error('카카오 OAuth에 필요한 환경 변수가 설정되지 않았습니다.'); +} + +function kakaoLogin(url?: string) { + const defaultRedirectUrl = window.location.origin; + const redirectUrl = url || defaultRedirectUrl; + + window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URI}&response_type=code&state=${encodeURIComponent( + redirectUrl + )}`; +} + +export default kakaoLogin; diff --git a/src/mocks/handlers/auth.ts b/src/mocks/handlers/auth.ts index 1c085b5..f3af13a 100644 --- a/src/mocks/handlers/auth.ts +++ b/src/mocks/handlers/auth.ts @@ -11,6 +11,17 @@ const authHandlers = [ isMember: false, }); }), + http.get('/api/v1/user/auth/check', ({ request }) => { + const isMocked = request.headers.get('X-Mock-Request'); + if (!isMocked || isMocked !== 'true') return passthrough(); + + console.log('유저 인증 체크 API 호출 - Mocked Response'); + + return HttpResponse.json({ + authenticated: true, + user: { id: 'mock-test-user-id' }, + }); + }), ]; export default authHandlers; diff --git a/src/pages/login/LoginPage.tsx b/src/pages/login/LoginPage.tsx index 1e4a9c9..4b90ad3 100644 --- a/src/pages/login/LoginPage.tsx +++ b/src/pages/login/LoginPage.tsx @@ -8,6 +8,7 @@ import Button from '@/shared/ui/Button'; import { Kakao } from '@/shared/assets/svgs/icon'; import Flex from '@/shared/ui/Flex'; import { useGetGuestToken } from '@/entities/auth/api/useGetGuestToken'; +import kakaoLogin from '@/entities/auth/lib/kakaoLogin'; import LoginEntranceView from './LoginEntranceView'; import * as S from './LoginPage.styles'; @@ -19,7 +20,7 @@ function LoginPage() { const handleLoginButtonClick = (loginType: 'KAKAO' | 'GUEST') => { const token = localStorage.getItem('accessToken'); if (loginType === 'KAKAO') { - console.log('카카오 로그인'); + kakaoLogin(); } else if (!token) { getGuestToken(); } else { @@ -60,7 +61,6 @@ function LoginPage() { background: '#FEE500', }} onClick={() => handleLoginButtonClick('KAKAO')} - disabled > diff --git a/src/shared/api/queryClient.ts b/src/shared/api/queryClient.ts new file mode 100644 index 0000000..dd030da --- /dev/null +++ b/src/shared/api/queryClient.ts @@ -0,0 +1,19 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient(); + +export const setupQueryClient = ( + handleQueryError: (error: Error) => void, + handleMutationError: (error: Error) => void +) => { + queryClient.setDefaultOptions({ + mutations: { + onError: handleMutationError, + throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다. + }, + queries: { + throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다. + }, + }); + queryClient.getQueryCache().config.onError = handleQueryError; +};