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 { HashRouter, Route, Routes } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from './store'
import { RootState } from 'src/store'
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.
import './scss/examples.scss'
import 'src/scss/examples.scss'
// Containers
const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout'))
const DefaultLayout = React.lazy(() => import('src/layout/DefaultLayout'))
// Pages
const Login = React.lazy(() => import('./views/pages/login/Login'))
const Register = React.lazy(() => import('./views/pages/register/Register'))
const Page404 = React.lazy(() => import('./views/pages/page404/Page404'))
const Page500 = React.lazy(() => import('./views/pages/page500/Page500'))
const Login = React.lazy(() => import('src/views/pages/login/Login'))
const Register = React.lazy(() => import('src/views/pages/register/Register'))
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'))
const App = () => {
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
@@ -47,10 +48,24 @@ const App = () => {
>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/register"
element={
<ProtectedRoute>
<Register />
</ProtectedRoute>
}
/>
<Route path="/404" element={<Page404 />} />
<Route path="/500" element={<Page500 />} />
<Route path="*" element={<DefaultLayout />} />
<Route
path="*"
element={
<ProtectedRoute>
<DefaultLayout />
</ProtectedRoute>
}
/>
</Routes>
</Suspense>
</HashRouter>

View File

@@ -1,4 +1,4 @@
import axios from './axios';
import axios from 'src/axios/axios';
import { jwtDecode } from 'jwt-decode';
export const getAccessTokenFromCookie = (): string | null => {
@@ -30,7 +30,7 @@ export interface DecodedToken {
encryptedPayload: string;
memberId: string;
memberName: string;
expiration: number;
exp: number;
}
// 로그인 API
@@ -48,8 +48,8 @@ export const register = async (data: RegisterData): Promise<{ message: string }>
// 로그아웃 API
export const logout = async (): Promise<void> => {
await axios.post('/auth/logout');
//localStorage.removeItem('access_token');
//localStorage.removeItem('refresh_token');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
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=/';
};
@@ -60,7 +60,9 @@ export const isTokenValid = (token: string): boolean => {
const decoded = jwtDecode<DecodedToken>(token);
const currentTime = Date.now() / 1000;
return decoded.expiration > currentTime;
if (!decoded.exp) return false; // 만료 정보가 없으면 일단 유효하다고 판단하거나, 정책에 따라 변경 가능
return decoded.exp > currentTime;
} catch (error) {
return false;
}
@@ -75,9 +77,3 @@ export const getUserFromToken = (token: string): DecodedToken | 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(
(config) => {
const accessToken = localStorage.getItem('access_token');
const accessToken = localStorage.getItem('accessToken') || localStorage.getItem('access_token');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from '../store'
import { RootState } from 'src/store'
import {
CCloseButton,
@@ -13,13 +13,13 @@ import {
} from '@coreui/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 { sygnet } from 'src/assets/brand/sygnet'
// sidebar nav config
import navigation from '../_nav'
import navigation from 'src/_nav'
const AppSidebar = () => {
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'
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 = () => {
return (

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
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);
if (context === undefined) {
if (!context) {
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 'core-js'
import App from './App'
import store from './store'
import { AuthProvider } from './context/AuthContext'
import App from 'src/App'
import store from 'src/store'
import { AuthProvider } from 'src/context/AuthContext'
createRoot(document.getElementById('root')!).render(
<Provider store={store}>

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index'
import { AppContent, AppSidebar, AppFooter, AppHeader } from 'src/components/index'
const DefaultLayout = () => {
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'
const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard'))
const Colors = React.lazy(() => import('./views/theme/colors/Colors'))
const Typography = React.lazy(() => import('./views/theme/typography/Typography'))
const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard'))
const Colors = React.lazy(() => import('src/views/theme/colors/Colors'))
const Typography = React.lazy(() => import('src/views/theme/typography/Typography'))
const routes = [
{ 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 avatar6 from 'src/assets/images/avatars/6.jpg'
import MainChart from './MainChart'
import MainChart from 'src/views/dashboard/MainChart'
const Dashboard = () => {
const tableExample = [