From 3ebd34d2de567ebbb15524f53d8fa508ab131339 Mon Sep 17 00:00:00 2001 From: artwork21c Date: Fri, 2 Jan 2026 12:51:07 +0900 Subject: [PATCH] =?UTF-8?q?access=5Ftoken=20=EC=9D=B4=20=EC=97=86=EC=A7=80?= =?UTF-8?q?=EB=A7=8C=20refresh=5Ftoken=20=EC=9D=B4=20=EC=9E=88=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20access=5Ftoken=20=EA=B0=B1=EC=8B=A0=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=B4=EB=AF=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=95=9C=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=95=98=EB=A9=B4=20=EC=B2=AB=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 43 +++++++++++++++++++-------------- src/axios/authService.ts | 19 +++++++++++++++ src/axios/axios.ts | 1 + src/components/AppContent.tsx | 3 ++- src/context/AuthContext.tsx | 31 ++++++++++++++++++++---- src/views/pages/login/Login.tsx | 5 ++-- 6 files changed, 76 insertions(+), 26 deletions(-) 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'