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'