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
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!--PR 제목에 "@coderabbitai 제목" 이 들어있으면 CodeRabbit이 자동으로 제목을 생성해줍니다!-->

## 💻 작업 내용

<!--구현한 내용을 설명해주세요.-->
Expand Down
31 changes: 8 additions & 23 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,25 @@
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';
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 (
<ThemeProvider theme={theme}>
<GlobalErrorBoundary>
Expand Down
58 changes: 30 additions & 28 deletions src/app/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ const NotFound = lazy(() =>
function AppRouter() {
const router = createBrowserRouter([
{
path: '',
id: 'root',
element: (
<RouteErrorBoundary>
<Outlet />
<Suspense fallback={<div>loading...</div>}>
<Outlet />
</Suspense>
</RouteErrorBoundary>
),
errorElement: <RouteErrorElement />,
Expand All @@ -65,24 +67,29 @@ function AppRouter() {
element: <Login />,
},
{
path: ROUTE.onboarding,
element: <Onboarding />,
},
{
path: ROUTE.home,
element: <Home />,
loader: checkAuth,
},
{
path: ROUTE.selectGroup,
element: <SelectGroup />,
loader: checkAuth,
},
{
path: ROUTE.groupSetup,
element: <GroupSetup />,
id: 'protected',
loader: checkAuth,
element: <Outlet />,
children: [
{
path: ROUTE.onboarding,
element: <Onboarding />,
},
{
path: ROUTE.home,
element: <Home />,
},
{
path: ROUTE.selectGroup,
element: <SelectGroup />,
},
{
path: ROUTE.groupSetup,
element: <GroupSetup />,
},
],
},
// TODO : 로그인 기능으로 변경될 예정
{
path: ROUTE.createBill,
element: <CreateBill />,
Expand All @@ -98,20 +105,15 @@ function AppRouter() {
element: <CharacterShare />,
loader: groupTokenUrlLoader,
},
{
path: '*',
element: <NotFound />,
},
],
},
{
path: '*',
element: <NotFound />,
},
]);

return (
// TODO : 로딩 페이지 추가하기
<Suspense fallback={<div>loading...</div>}>
<RouterProvider router={router} />
</Suspense>
);
return <RouterProvider router={router} />;
}

export default AppRouter;
10 changes: 10 additions & 0 deletions src/entities/auth/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axiosInstance from '@/shared/api/axios';

// CHECK - 게스트 토큰 정책 제거 가능성 있음
export interface GuestTokenData {
accessToken: string;
refreshToken: string;
Expand All @@ -11,3 +12,12 @@ export const getGuestToken = async (): Promise<GuestTokenData> => {
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;
};
30 changes: 20 additions & 10 deletions src/entities/auth/lib/checkAuth.ts
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 17 additions & 0 deletions src/entities/auth/lib/kakaoLogin.ts
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions src/mocks/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 2 additions & 2 deletions src/pages/login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 {
Expand Down Expand Up @@ -60,7 +61,6 @@ function LoginPage() {
background: '#FEE500',
}}
onClick={() => handleLoginButtonClick('KAKAO')}
disabled
>
<Kakao width={theme.unit[24]} />
<Text variant="body1Sb" color="semantic.text.strong">
Expand Down
19 changes: 19 additions & 0 deletions src/shared/api/queryClient.ts
Original file line number Diff line number Diff line change
@@ -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;
};