diff --git a/src/App.tsx b/src/App.tsx
index d9a075c..56f09f7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
import React, { Suspense, useEffect } from 'react'
-import { HashRouter, Route, Routes } from 'react-router-dom'
+import { HashRouter, Route, Routes, Navigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from 'src/store'
@@ -19,9 +19,13 @@ const Page404 = React.lazy(() => import('src/views/pages/page404/Page404'))
const Page500 = React.lazy(() => import('src/views/pages/page500/Page500'))
const ProtectedRoute = React.lazy(() => import('src/routes/ProtectedRoute'))
+import { useAuth } from 'src/hooks/useAuth'
+
const App = () => {
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
const storedTheme = useSelector((state: RootState) => state.theme)
+ const { state: authState } = useAuth()
+ const { isAuthenticated, loading: authLoading } = authState
useEffect(() => {
const urlParams = new URLSearchParams(window.location.href.split('?')[1])
@@ -37,6 +41,14 @@ const App = () => {
setColorMode(storedTheme)
}, []) // eslint-disable-line react-hooks/exhaustive-deps
+ if (authLoading) {
+ return (
+
+
+
+ )
+ }
+
return (
{
}
>
- } />
-
-
-
- }
- />
+ {/* 1. 로그인 여부와 관계없이 항상 독립적으로 표시되는 페이지 */}
+ } />
} />
} />
-
-
-
- }
- />
+
+ {/* 2. 인증 상태에 따른 조건부 라우팅 */}
+ {!isAuthenticated ? (
+ <>
+ } />
+ } />
+ >
+ ) : (
+ } />
+ )}
diff --git a/src/axios/authService.ts b/src/axios/authService.ts
index 9c3c17d..9a4c6e3 100644
--- a/src/axios/authService.ts
+++ b/src/axios/authService.ts
@@ -9,6 +9,14 @@ export const getAccessTokenFromCookie = (): string | null => {
return null;
};
+export const getRefreshTokenFromCookie = (): string | null => {
+ const name = 'refresh_token';
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
+ return null;
+};
+
export interface LoginCredentials {
memberId: string;
password: string;
@@ -77,3 +85,14 @@ export const getUserFromToken = (token: string): DecodedToken | null => {
return null;
}
};
+
+// Access Token 갱신 API
+export const renewAccessToken = async (refreshToken: string): Promise => {
+ // Refresh Token을 Header에 담아 전송 (Authorization: Bearer )
+ const response = await axios.post('/auth/renewAccessToken', null, {
+ headers: {
+ 'Authorization': `Bearer ${refreshToken}`
+ }
+ });
+ return response.data;
+};
diff --git a/src/axios/axios.ts b/src/axios/axios.ts
index 19bcba3..55d5cef 100644
--- a/src/axios/axios.ts
+++ b/src/axios/axios.ts
@@ -6,6 +6,7 @@ const API_URL = '/api/v1/';
const instance = axios.create({
baseURL: API_URL,
timeout: 10000,
+ withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
diff --git a/src/components/AppContent.tsx b/src/components/AppContent.tsx
index 34f48fb..76b0be1 100644
--- a/src/components/AppContent.tsx
+++ b/src/components/AppContent.tsx
@@ -21,7 +21,8 @@ const AppContent = () => {
)
)
})}
- } />
+ } />
+ } />
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index e54be68..967e451 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -1,6 +1,6 @@
import React, { createContext, useReducer, useEffect } from 'react';
-import { isTokenValid, getAccessTokenFromCookie, getUserFromToken } from 'src/axios/authService';
+import { isTokenValid, getAccessTokenFromCookie, getUserFromToken, getRefreshTokenFromCookie, renewAccessToken } from 'src/axios/authService';
// 사용자 타입 정의
export interface Member {
@@ -122,7 +122,28 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => {
const loadUser = async () => {
// localStorage 또는 Cookie에서 토큰 확인
- let token = localStorage.getItem('accessToken') || getAccessTokenFromCookie();
+ let token = localStorage.getItem('accessToken') || localStorage.getItem('access_token') || getAccessTokenFromCookie();
+
+ if (!token || !isTokenValid(token)) {
+ // Access Token이 없거나 만료된 경우 Refresh Token 확인
+ let refreshToken = localStorage.getItem('refreshToken') || localStorage.getItem('refresh_token') || getRefreshTokenFromCookie();
+
+ // refreshToken으로 Access Token 갱신 시도
+ try {
+ const response = await renewAccessToken(refreshToken || '');
+ if (response.resultCode === '200') {
+ // 갱신 성공 시 새로운 Access Token 가져오기
+ const newToken = response.resultData || getAccessTokenFromCookie() || localStorage.getItem('accessToken');
+ if (newToken && isTokenValid(newToken)) {
+ token = newToken;
+ // 갱신된 토큰을 localStorage에 즉시 저장
+ localStorage.setItem('accessToken', token);
+ }
+ }
+ } catch (error) {
+ // 갱신 실패 무시
+ }
+ }
if (!token || !isTokenValid(token)) {
dispatch({ type: 'LOGOUT' });
@@ -130,12 +151,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
try {
- // 토큰이 쿠키에만 있고 localStorage에 없으면 저장해줌 (일관성 유지)
- if (!localStorage.getItem('accessToken')) {
+ // 토큰이 존재하고 유효한 경우, localStorage와 동기화 (갱신 등으로 변경되었을 수 있음)
+ if (token && localStorage.getItem('accessToken') !== token) {
localStorage.setItem('accessToken', token);
}
- const decodedToken = getUserFromToken(token);
+ const decodedToken = getUserFromToken(token!); // 위에서 유효성 검사 완료됨
if (decodedToken) {
const member: Member = {
memberId: decodedToken.memberId,
diff --git a/src/views/pages/login/Login.tsx b/src/views/pages/login/Login.tsx
index 36de7ab..f3215f0 100644
--- a/src/views/pages/login/Login.tsx
+++ b/src/views/pages/login/Login.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react'
-import { Link, useNavigate } from 'react-router-dom'
+import React, { useState, useEffect } from 'react'
+import { Link, useNavigate, Navigate } from 'react-router-dom'
import {
CButton,
CCard,
@@ -12,6 +12,7 @@ import {
CInputGroup,
CInputGroupText,
CRow,
+ CSpinner,
} from '@coreui/react'
import CIcon from '@coreui/icons-react'
import { cilLockLocked, cilUser } from '@coreui/icons'