access_token 쿠키 확인하여 로그인 상태 유지 처리

ProtectedRoute 로 로그인 필요 페이지 접근 관리
import 문을 src/ 포함된 절대경로로 개선
This commit is contained in:
2026-01-02 11:21:56 +09:00
parent 985ba75d34
commit 767435cad4
19 changed files with 125 additions and 156 deletions

View File

@@ -1,22 +1,23 @@
import React, { Suspense, useEffect } from 'react' import React, { Suspense, useEffect } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter, Route, Routes } from 'react-router-dom'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { RootState } from './store' import { RootState } from 'src/store'
import { CSpinner, useColorModes } from '@coreui/react' import { CSpinner, useColorModes } from '@coreui/react'
import './scss/style.scss' import 'src/scss/style.scss'
// We use those styles to show code examples, you should remove them in your application. // We use those styles to show code examples, you should remove them in your application.
import './scss/examples.scss' import 'src/scss/examples.scss'
// Containers // Containers
const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) const DefaultLayout = React.lazy(() => import('src/layout/DefaultLayout'))
// Pages // Pages
const Login = React.lazy(() => import('./views/pages/login/Login')) const Login = React.lazy(() => import('src/views/pages/login/Login'))
const Register = React.lazy(() => import('./views/pages/register/Register')) const Register = React.lazy(() => import('src/views/pages/register/Register'))
const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) const Page404 = React.lazy(() => import('src/views/pages/page404/Page404'))
const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const Page500 = React.lazy(() => import('src/views/pages/page500/Page500'))
const ProtectedRoute = React.lazy(() => import('src/routes/ProtectedRoute'))
const App = () => { const App = () => {
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
@@ -47,10 +48,24 @@ const App = () => {
> >
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route
path="/register"
element={
<ProtectedRoute>
<Register />
</ProtectedRoute>
}
/>
<Route path="/404" element={<Page404 />} /> <Route path="/404" element={<Page404 />} />
<Route path="/500" element={<Page500 />} /> <Route path="/500" element={<Page500 />} />
<Route path="*" element={<DefaultLayout />} /> <Route
path="*"
element={
<ProtectedRoute>
<DefaultLayout />
</ProtectedRoute>
}
/>
</Routes> </Routes>
</Suspense> </Suspense>
</HashRouter> </HashRouter>

View File

@@ -1,4 +1,4 @@
import axios from './axios'; import axios from 'src/axios/axios';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
export const getAccessTokenFromCookie = (): string | null => { export const getAccessTokenFromCookie = (): string | null => {
@@ -30,7 +30,7 @@ export interface DecodedToken {
encryptedPayload: string; encryptedPayload: string;
memberId: string; memberId: string;
memberName: string; memberName: string;
expiration: number; exp: number;
} }
// 로그인 API // 로그인 API
@@ -48,8 +48,8 @@ export const register = async (data: RegisterData): Promise<{ message: string }>
// 로그아웃 API // 로그아웃 API
export const logout = async (): Promise<void> => { export const logout = async (): Promise<void> => {
await axios.post('/auth/logout'); await axios.post('/auth/logout');
//localStorage.removeItem('access_token'); localStorage.removeItem('accessToken');
//localStorage.removeItem('refresh_token'); localStorage.removeItem('refreshToken');
document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/'; document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/'; document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
}; };
@@ -60,7 +60,9 @@ export const isTokenValid = (token: string): boolean => {
const decoded = jwtDecode<DecodedToken>(token); const decoded = jwtDecode<DecodedToken>(token);
const currentTime = Date.now() / 1000; const currentTime = Date.now() / 1000;
return decoded.expiration > currentTime; if (!decoded.exp) return false; // 만료 정보가 없으면 일단 유효하다고 판단하거나, 정책에 따라 변경 가능
return decoded.exp > currentTime;
} catch (error) { } catch (error) {
return false; return false;
} }
@@ -75,9 +77,3 @@ export const getUserFromToken = (token: string): DecodedToken | null => {
return null; return null;
} }
}; };
// 현재 사용자 정보 가져오기
export const getCurrentUser = async () => {
const response = await axios.get('/auth/me');
return response.data;
};

View File

@@ -14,7 +14,7 @@ const instance = axios.create({
// 요청 인터셉터 // 요청 인터셉터
instance.interceptors.request.use( instance.interceptors.request.use(
(config) => { (config) => {
const accessToken = localStorage.getItem('access_token'); const accessToken = localStorage.getItem('accessToken') || localStorage.getItem('access_token');
if (accessToken) { if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`; config.headers.Authorization = `Bearer ${accessToken}`;
} }

View File

@@ -1,23 +1,29 @@
import React from 'react' import React from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import routes from '../routes' import routes from 'src/routes/routes'
import { CBreadcrumb, CBreadcrumbItem } from '@coreui/react' import { CBreadcrumb, CBreadcrumbItem } from '@coreui/react'
interface Route {
path: string;
name: string;
exact?: boolean;
}
const AppBreadcrumb = () => { const AppBreadcrumb = () => {
const currentLocation = useLocation().pathname const currentLocation = useLocation().pathname
const getRouteName = (pathname, routes) => { const getRouteName = (pathname: string, routes: Route[]): string | false => {
const currentRoute = routes.find((route) => route.path === pathname) const currentRoute = routes.find((route) => route.path === pathname)
return currentRoute ? currentRoute.name : false return currentRoute ? currentRoute.name : false
} }
const getBreadcrumbs = (location) => { const getBreadcrumbs = (location: string) => {
const breadcrumbs = [] const breadcrumbs: { pathname: string; name: string; active: boolean }[] = []
location.split('/').reduce((prev, curr, index, array) => { location.split('/').reduce((prev: string, curr: string, index: number, array: string[]) => {
const currentPathname = `${prev}/${curr}` const currentPathname = `${prev}/${curr}`
const routeName = getRouteName(currentPathname, routes) const routeName = getRouteName(currentPathname, routes as Route[])
routeName && routeName &&
breadcrumbs.push({ breadcrumbs.push({
pathname: currentPathname, pathname: currentPathname,

View File

@@ -3,7 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'
import { CContainer, CSpinner } from '@coreui/react' import { CContainer, CSpinner } from '@coreui/react'
// routes config // routes config
import routes from '../routes' import routes from 'src/routes/routes'
const AppContent = () => { const AppContent = () => {
return ( return (

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { RootState } from '../store' import { RootState } from 'src/store'
import { import {
CContainer, CContainer,
CDropdown, CDropdown,
@@ -26,8 +26,8 @@ import {
cilSun, cilSun,
} from '@coreui/icons' } from '@coreui/icons'
import { AppBreadcrumb } from './index' import { AppBreadcrumb } from 'src/components/index'
import { AppHeaderDropdown } from './header/index' import { AppHeaderDropdown } from 'src/components/header/index'
import { useAuth } from 'src/hooks/useAuth' import { useAuth } from 'src/hooks/useAuth'
const AppHeader = () => { const AppHeader = () => {

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { RootState } from '../store' import { RootState } from 'src/store'
import { import {
CCloseButton, CCloseButton,
@@ -13,13 +13,13 @@ import {
} from '@coreui/react' } from '@coreui/react'
import CIcon from '@coreui/icons-react' import CIcon from '@coreui/icons-react'
import { AppSidebarNav } from './AppSidebarNav' import { AppSidebarNav } from 'src/components/AppSidebarNav'
import { logo } from 'src/assets/brand/logo' import { logo } from 'src/assets/brand/logo'
import { sygnet } from 'src/assets/brand/sygnet' import { sygnet } from 'src/assets/brand/sygnet'
// sidebar nav config // sidebar nav config
import navigation from '../_nav' import navigation from 'src/_nav'
const AppSidebar = () => { const AppSidebar = () => {
const dispatch = useDispatch() const dispatch = useDispatch()

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types'
import React from 'react'
import ComponentsImg from 'src/assets/images/components.webp'
const DocsComponents = (props) => (
<div className="bg-primary bg-opacity-10 border border-2 border-primary rounded mb-4">
<div className="row d-flex align-items-center p-3 px-xl-4 flex-xl-nowrap">
<div className="col-xl-auto col-12 d-none d-xl-block p-0">
<img
className="img-fluid"
src={ComponentsImg}
width="160px"
height="160px"
alt="CoreUI PRO hexagon"
/>
</div>
<div className="col-md col-12 px-lg-4">
Our Admin Panel isnt just a mix of third-party components. Its{' '}
<strong>
the only open-source React dashboard built on a professional, enterprise-grade UI
Components Library
</strong>
. This component is part of this library, and we present only the basic usage of it here. To
explore extended examples, detailed API documentation, and customization options, refer to
our docs.
</div>
<div className="col-md-auto col-12 mt-3 mt-lg-0">
<a
className="btn btn-primary text-nowrap text-white"
href={`https://coreui.io/react/docs/${props.href}`}
target="_blank"
rel="noopener noreferrer"
>
Explore Documentation
</a>
</div>
</div>
</div>
)
DocsComponents.propTypes = {
href: PropTypes.string,
}
export default DocsComponents

View File

@@ -1,43 +0,0 @@
import PropTypes from 'prop-types'
import React from 'react'
import { CNav, CNavItem, CNavLink, CTabContent, CTabPane } from '@coreui/react'
import CIcon from '@coreui/icons-react'
import { cilCode, cilMediaPlay } from '@coreui/icons'
const DocsExample = (props) => {
const { children, href, tabContentClassName } = props
const _href = `https://coreui.io/react/docs/${href}`
return (
<div className="example">
<CNav variant="underline-border">
<CNavItem>
<CNavLink href="#" active>
<CIcon icon={cilMediaPlay} className="me-2" />
Preview
</CNavLink>
</CNavItem>
<CNavItem>
<CNavLink href={_href} target="_blank">
<CIcon icon={cilCode} className="me-2" />
Code
</CNavLink>
</CNavItem>
</CNav>
<CTabContent className={`rounded-bottom ${tabContentClassName ? tabContentClassName : ''}`}>
<CTabPane className="p-3 preview" visible>
{children}
</CTabPane>
</CTabContent>
</div>
)
}
DocsExample.propTypes = {
children: PropTypes.node,
href: PropTypes.string,
tabContentClassName: PropTypes.string,
}
export default React.memo(DocsExample)

View File

@@ -22,7 +22,7 @@ import {
} from '@coreui/icons' } from '@coreui/icons'
import CIcon from '@coreui/icons-react' import CIcon from '@coreui/icons-react'
import avatar8 from './../../assets/images/avatars/8.jpg' import avatar8 from 'src/assets/images/avatars/8.jpg'
const AppHeaderDropdown = () => { const AppHeaderDropdown = () => {
return ( return (

View File

@@ -1,3 +1,3 @@
import AppHeaderDropdown from './AppHeaderDropdown' import AppHeaderDropdown from 'src/components/header/AppHeaderDropdown'
export { AppHeaderDropdown } export { AppHeaderDropdown }

View File

@@ -1,13 +1,11 @@
import AppBreadcrumb from './AppBreadcrumb' import AppBreadcrumb from 'src/components/AppBreadcrumb'
import AppContent from './AppContent' import AppContent from 'src/components/AppContent'
import AppFooter from './AppFooter' import AppFooter from 'src/components/AppFooter'
import AppHeader from './AppHeader' import AppHeader from 'src/components/AppHeader'
import AppHeaderDropdown from './header/AppHeaderDropdown' import AppHeaderDropdown from 'src/components/header/AppHeaderDropdown'
import AppSidebar from './AppSidebar' import AppSidebar from 'src/components/AppSidebar'
import DocsComponents from './DocsComponents' import DocsIcons from 'src/components/DocsIcons'
import DocsIcons from './DocsIcons' import DocsLink from 'src/components/DocsLink'
import DocsLink from './DocsLink'
import DocsExample from './DocsExample'
export { export {
AppBreadcrumb, AppBreadcrumb,
@@ -16,8 +14,6 @@ export {
AppHeader, AppHeader,
AppHeaderDropdown, AppHeaderDropdown,
AppSidebar, AppSidebar,
DocsComponents,
DocsIcons, DocsIcons,
DocsLink, DocsLink,
DocsExample,
} }

View File

@@ -1,6 +1,6 @@
import React, { createContext, useReducer, useEffect } from 'react'; import React, { createContext, useReducer, useEffect } from 'react';
import { getCurrentUser, isTokenValid } from 'src/axios/authService'; import { isTokenValid, getAccessTokenFromCookie, getUserFromToken } from 'src/axios/authService';
// 사용자 타입 정의 // 사용자 타입 정의
export interface Member { export interface Member {
@@ -33,13 +33,16 @@ const initialState: AuthState = {
error: null error: null
}; };
// Context 생성 // Context 타입 정의
export const AuthContext = createContext<{ export interface AuthContextType {
state: AuthState; state: AuthState;
dispatch: React.Dispatch<AuthAction>; dispatch: React.Dispatch<AuthAction>;
login: (token: string, member: Member) => void; login: (token: string, member: Member) => void;
logout: () => void; logout: () => void;
}>({ }
// Context 생성
export const AuthContext = createContext<AuthContextType>({
state: initialState, state: initialState,
dispatch: () => null, dispatch: () => null,
login: () => null, login: () => null,
@@ -118,7 +121,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 초기 인증 상태 확인 // 초기 인증 상태 확인
useEffect(() => { useEffect(() => {
const loadUser = async () => { const loadUser = async () => {
const token = localStorage.getItem('accessToken'); // localStorage 또는 Cookie에서 토큰 확인
let token = localStorage.getItem('accessToken') || getAccessTokenFromCookie();
if (!token || !isTokenValid(token)) { if (!token || !isTokenValid(token)) {
dispatch({ type: 'LOGOUT' }); dispatch({ type: 'LOGOUT' });
@@ -126,8 +130,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} }
try { try {
const member = await getCurrentUser(); // 토큰이 쿠키에만 있고 localStorage에 없으면 저장해줌 (일관성 유지)
dispatch({ type: 'MEMBER_LOADED', payload: member }); if (!localStorage.getItem('accessToken')) {
localStorage.setItem('accessToken', token);
}
const decodedToken = getUserFromToken(token);
if (decodedToken) {
const member: Member = {
memberId: decodedToken.memberId,
memberName: decodedToken.memberName
};
dispatch({ type: 'MEMBER_LOADED', payload: member });
} else {
dispatch({ type: 'AUTH_ERROR', payload: 'Invalid token' });
}
} catch (error) { } catch (error) {
dispatch({ type: 'AUTH_ERROR', payload: 'Authentication failed' }); dispatch({ type: 'AUTH_ERROR', payload: 'Authentication failed' });
} }

View File

@@ -1,10 +1,10 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { AuthContext } from '../context/AuthContext'; import { AuthContext, AuthContextType } from 'src/context/AuthContext';
export const useAuth = () => { export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (context === undefined) { if (!context) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error('useAuth must be used within an AuthProvider');
} }

View File

@@ -3,9 +3,9 @@ import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import 'core-js' import 'core-js'
import App from './App' import App from 'src/App'
import store from './store' import store from 'src/store'
import { AuthProvider } from './context/AuthContext' import { AuthProvider } from 'src/context/AuthContext'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<Provider store={store}> <Provider store={store}>

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' import { AppContent, AppSidebar, AppFooter, AppHeader } from 'src/components/index'
const DefaultLayout = () => { const DefaultLayout = () => {
return ( return (

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from 'src/hooks/useAuth';
import { CSpinner } from '@coreui/react';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { state } = useAuth();
const { isAuthenticated, loading } = state;
const location = useLocation();
if (loading) {
return (
<div className="pt-3 text-center">
<CSpinner color="primary" variant="grow" />
</div>
);
}
if (!isAuthenticated) {
// 로그인 되지 않았다면 로그인 페이지로 리다이렉트
// 현재 위치를 state에 저장하여 로그인 후 다시 돌아올 수 있게 함
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
};
export default ProtectedRoute;

View File

@@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard'))
const Colors = React.lazy(() => import('./views/theme/colors/Colors')) const Colors = React.lazy(() => import('src/views/theme/colors/Colors'))
const Typography = React.lazy(() => import('./views/theme/typography/Typography')) const Typography = React.lazy(() => import('src/views/theme/typography/Typography'))
const routes = [ const routes = [
{ path: '/', exact: true, name: 'Home' }, { path: '/', exact: true, name: 'Home' },

View File

@@ -50,7 +50,7 @@ import avatar4 from 'src/assets/images/avatars/4.jpg'
import avatar5 from 'src/assets/images/avatars/5.jpg' import avatar5 from 'src/assets/images/avatars/5.jpg'
import avatar6 from 'src/assets/images/avatars/6.jpg' import avatar6 from 'src/assets/images/avatars/6.jpg'
import MainChart from './MainChart' import MainChart from 'src/views/dashboard/MainChart'
const Dashboard = () => { const Dashboard = () => {
const tableExample = [ const tableExample = [