Skip to content

Commit 93562ff

Browse files
authored
Merge pull request #29 from next-engineer/feature/13-route-guard
Feature/13 route guard
2 parents 90e03ed + c916215 commit 93562ff

File tree

5 files changed

+189
-89
lines changed

5 files changed

+189
-89
lines changed

src/App.tsx

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,99 @@
1-
import { RouterProvider } from 'react-router-dom';
2-
import { ThemeProvider, CssBaseline } from '@mui/material';
3-
import { theme } from './styles/theme';
4-
import { router } from './routes';
1+
import { useEffect } from 'react';
2+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
3+
4+
import { ProtectedRoute, AdminRoute, PublicOnlyRoute } from './components/RouteGuards';
5+
import { useAuthStore } from './store/useAuthStore';
6+
7+
// common page
8+
import AppLayout from './components/AppLayout';
9+
import HomePage from './pages/HomePage';
10+
import NotFoundPage from './pages/NotFoundPage';
11+
import MatchesPage from './pages/MatchesPage';
12+
import AuthCallbackPage from './pages/AuthCallbackPage';
13+
14+
// public only page
15+
import SignupPage from './pages/SignupPage';
16+
import LoginPage from './pages/LoginPage';
17+
18+
// protected page
19+
import ProfilePage from './pages/ProfilePage';
20+
import MyMatchesPage from './pages/MyMatchesPage';
21+
22+
// admin page
23+
import CreateMatchPage from './pages/CreateMatchPage';
524

625
export default function App() {
26+
useEffect(() => {
27+
useAuthStore.getState().syncFromStorage();
28+
}, []);
29+
730
return (
8-
<ThemeProvider theme={theme}>
9-
<CssBaseline />
10-
<RouterProvider router={router} />
11-
</ThemeProvider>
31+
<BrowserRouter>
32+
<Routes>
33+
<Route path="/auth/callback" element={<AuthCallbackPage />} />
34+
35+
{/* AppLayout을 적용하는 페이지 */}
36+
<Route path="/" element={<AppLayout />}>
37+
{/* 누구나 접근 가능한 페이지 */}
38+
<Route path="/" element={<HomePage />} />
39+
<Route path="/home" element={<HomePage />} />
40+
<Route path="/notfound" element={<NotFoundPage />} />
41+
<Route path="/matches" element={<MatchesPage />} />
42+
43+
{/* 로그인하지 않은 사용자만 접근 가능 */}
44+
<Route
45+
path="/login"
46+
element={
47+
<PublicOnlyRoute>
48+
<LoginPage />
49+
</PublicOnlyRoute>
50+
}
51+
/>
52+
<Route
53+
path="/signup"
54+
element={
55+
<PublicOnlyRoute>
56+
<SignupPage />
57+
</PublicOnlyRoute>
58+
}
59+
/>
60+
{/* <Route
61+
path="/auth/callback"
62+
element={
63+
<ProtectedRoute>
64+
<AuthCallbackPage />
65+
</ProtectedRoute>}
66+
/> */}
67+
68+
{/* 로그인한 사용자만 접근 가능 */}
69+
<Route
70+
path="/profile"
71+
element={
72+
<ProtectedRoute>
73+
<ProfilePage />
74+
</ProtectedRoute>
75+
}
76+
/>
77+
<Route
78+
path="/mymatches"
79+
element={
80+
<ProtectedRoute>
81+
<MyMatchesPage />
82+
</ProtectedRoute>
83+
}
84+
/>
85+
86+
{/* 관리자만 접근 가능 */}
87+
<Route
88+
path="/createMatch"
89+
element={
90+
<AdminRoute>
91+
<CreateMatchPage />
92+
</AdminRoute>
93+
}
94+
/>
95+
</Route>
96+
</Routes>
97+
</BrowserRouter>
1298
);
1399
}

src/components/ProtectedRoute.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/components/RouteGuards.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { ReactNode } from 'react';
2+
import type { UserRole } from '../types/index';
3+
import { useEffect, useState } from 'react';
4+
import { Navigate, useLocation } from 'react-router-dom';
5+
import { useAuthStore } from '../store/useAuthStore';
6+
7+
// 공통 Props 타입
8+
interface RouteGuardProps {
9+
children: ReactNode;
10+
}
11+
12+
// 로그인이 필요한 페이지를 보호하는 컴포넌트
13+
export const ProtectedRoute = ({ children }: RouteGuardProps) => {
14+
const { isAuthenticated, tokens } = useAuthStore();
15+
const location = useLocation();
16+
17+
const [isLoading, setIsLoading] = useState(true);
18+
19+
useEffect(() => {
20+
// 토큰이 있는지 확인하고 로딩 상태 해제
21+
const timer = setTimeout(() => {
22+
setIsLoading(false);
23+
}, 100); // 짧은 지연으로 비동기 처리 대기
24+
25+
return () => clearTimeout(timer);
26+
}, [tokens]);
27+
28+
// 아직 인증 상태를 확인 중이면 로딩 표시
29+
if (isLoading) {
30+
return <div>로딩 중...</div>;
31+
}
32+
33+
// 로그인하지 않았다면 로그인 페이지로 리다이렉트
34+
// state에 현재 위치를 저장해서 로그인 후 다시 돌아올 수 있게 함
35+
if (!isAuthenticated) {
36+
return <Navigate to="/login" state={{ from: location }} replace />;
37+
}
38+
39+
// 로그인했다면 원래 보려던 페이지 표시
40+
return <>{children}</>;
41+
};
42+
43+
// 특정 권한이 필요한 페이지를 보호하는 컴포넌트
44+
interface RoleGuardProps extends RouteGuardProps {
45+
requiredRole: UserRole;
46+
fallbackPath?: string; // 권한이 없을 때 이동할 경로
47+
}
48+
49+
export const RoleGuard = ({ children, requiredRole, fallbackPath = '/' }: RoleGuardProps) => {
50+
const { user, isAuthenticated } = useAuthStore();
51+
const location = useLocation();
52+
53+
// if (isLoading) {
54+
// return <div>로딩 중...</div>;
55+
// }
56+
57+
// 로그인하지 않았다면 로그인 페이지로
58+
if (!isAuthenticated) {
59+
return <Navigate to="/login" state={{ from: location }} replace />;
60+
}
61+
62+
// 로그인했지만 권한이 없다면 fallback 경로로
63+
if (user?.role !== requiredRole) {
64+
return <Navigate to={fallbackPath} replace />;
65+
}
66+
67+
// 모든 조건을 만족하면 원래 페이지 표시
68+
return <>{children}</>;
69+
};
70+
71+
// 관리자 전용 페이지를 보호하는 컴포넌트 (RoleGuard의 특화 버전)
72+
export const AdminRoute = ({ children }: RouteGuardProps) => {
73+
return (
74+
<RoleGuard requiredRole="ADMIN" fallbackPath="/unauthorized">
75+
{children}
76+
</RoleGuard>
77+
);
78+
};
79+
80+
// 이미 로그인한 사용자가 접근하면 안 되는 페이지 (로그인, 회원가입 등)
81+
export const PublicOnlyRoute = ({ children }: RouteGuardProps) => {
82+
const { isAuthenticated } = useAuthStore();
83+
84+
// if (isLoading) {
85+
// return <div>로딩 중...</div>;
86+
// }
87+
88+
// 이미 로그인했다면 홈으로 리다이렉트
89+
if (isAuthenticated) {
90+
return <Navigate to="/" replace />;
91+
}
92+
93+
return <>{children}</>;
94+
};

src/pages/AuthCallbackPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default function AuthCallbackPage() {
1919
} catch (e: unknown) {
2020
const message = e instanceof Error ? e.message : '인증 처리 중 오류가 발생했습니다.';
2121
setError(message);
22+
navigate('/login', { replace: true });
2223
}
2324
})();
2425
}, [navigate]);

src/routes/index.tsx

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)