From 767435cad4c3820cbbd2ca442115a605f532c09f Mon Sep 17 00:00:00 2001 From: artwork21c Date: Fri, 2 Jan 2026 11:21:56 +0900 Subject: [PATCH] =?UTF-8?q?access=5Ftoken=20=EC=BF=A0=ED=82=A4=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EC=97=AC=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=9C=A0=EC=A7=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?ProtectedRoute=20=EB=A1=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EA=B4=80=EB=A6=AC=20import=20=EB=AC=B8=EC=9D=84=20?= =?UTF-8?q?src/=20=ED=8F=AC=ED=95=A8=EB=90=9C=20=EC=A0=88=EB=8C=80?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 35 +++++++++++----- src/axios/authService.ts | 18 ++++---- src/axios/axios.ts | 2 +- src/components/AppBreadcrumb.tsx | 18 +++++--- src/components/AppContent.tsx | 2 +- src/components/AppHeader.tsx | 6 +-- src/components/AppSidebar.tsx | 6 +-- src/components/DocsComponents.tsx | 46 --------------------- src/components/DocsExample.tsx | 43 ------------------- src/components/header/AppHeaderDropdown.tsx | 2 +- src/components/header/index.tsx | 2 +- src/components/index.ts | 20 ++++----- src/context/AuthContext.tsx | 31 ++++++++++---- src/hooks/useAuth.ts | 6 +-- src/index.tsx | 6 +-- src/layout/DefaultLayout.tsx | 2 +- src/routes/ProtectedRoute.tsx | 28 +++++++++++++ src/{ => routes}/routes.ts | 6 +-- src/views/dashboard/Dashboard.tsx | 2 +- 19 files changed, 125 insertions(+), 156 deletions(-) delete mode 100644 src/components/DocsComponents.tsx delete mode 100644 src/components/DocsExample.tsx create mode 100644 src/routes/ProtectedRoute.tsx rename src/{ => routes}/routes.ts (62%) diff --git a/src/App.tsx b/src/App.tsx index 93554bf..d9a075c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => { > } /> - } /> + + + + } + /> } /> } /> - } /> + + + + } + /> diff --git a/src/axios/authService.ts b/src/axios/authService.ts index 7a7c3e7..9c3c17d 100644 --- a/src/axios/authService.ts +++ b/src/axios/authService.ts @@ -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 => { 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(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; -}; \ No newline at end of file diff --git a/src/axios/axios.ts b/src/axios/axios.ts index e077bfb..19bcba3 100644 --- a/src/axios/axios.ts +++ b/src/axios/axios.ts @@ -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}`; } diff --git a/src/components/AppBreadcrumb.tsx b/src/components/AppBreadcrumb.tsx index d37de8c..49d7668 100644 --- a/src/components/AppBreadcrumb.tsx +++ b/src/components/AppBreadcrumb.tsx @@ -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, diff --git a/src/components/AppContent.tsx b/src/components/AppContent.tsx index ef4424e..34f48fb 100644 --- a/src/components/AppContent.tsx +++ b/src/components/AppContent.tsx @@ -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 ( diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 16d83ec..87a884f 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -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 = () => { diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index d75d4a9..d313728 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -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() diff --git a/src/components/DocsComponents.tsx b/src/components/DocsComponents.tsx deleted file mode 100644 index b0968b8..0000000 --- a/src/components/DocsComponents.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -import ComponentsImg from 'src/assets/images/components.webp' - -const DocsComponents = (props) => ( -
-
-
- CoreUI PRO hexagon -
-
- Our Admin Panel isn’t just a mix of third-party components. It’s{' '} - - the only open-source React dashboard built on a professional, enterprise-grade UI - Components Library - - . 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. -
- -
-
-) - -DocsComponents.propTypes = { - href: PropTypes.string, -} - -export default DocsComponents diff --git a/src/components/DocsExample.tsx b/src/components/DocsExample.tsx deleted file mode 100644 index 2adedad..0000000 --- a/src/components/DocsExample.tsx +++ /dev/null @@ -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 ( -
- - - - - Preview - - - - - - Code - - - - - - {children} - - -
- ) -} - -DocsExample.propTypes = { - children: PropTypes.node, - href: PropTypes.string, - tabContentClassName: PropTypes.string, -} - -export default React.memo(DocsExample) diff --git a/src/components/header/AppHeaderDropdown.tsx b/src/components/header/AppHeaderDropdown.tsx index 9021a10..aac5acd 100644 --- a/src/components/header/AppHeaderDropdown.tsx +++ b/src/components/header/AppHeaderDropdown.tsx @@ -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 ( diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index bf8af6c..aeb6cf5 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -1,3 +1,3 @@ -import AppHeaderDropdown from './AppHeaderDropdown' +import AppHeaderDropdown from 'src/components/header/AppHeaderDropdown' export { AppHeaderDropdown } diff --git a/src/components/index.ts b/src/components/index.ts index 93fb52c..9dab379 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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, } diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 440f223..e54be68 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -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; login: (token: string, member: Member) => void; logout: () => void; -}>({ +} + +// Context 생성 +export const AuthContext = createContext({ 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(); - dispatch({ type: 'MEMBER_LOADED', payload: member }); + // 토큰이 쿠키에만 있고 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' }); } diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index efca210..1c291bd 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -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'); } diff --git a/src/index.tsx b/src/index.tsx index 14542f6..97006da 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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( diff --git a/src/layout/DefaultLayout.tsx b/src/layout/DefaultLayout.tsx index 19fbf22..1e94820 100644 --- a/src/layout/DefaultLayout.tsx +++ b/src/layout/DefaultLayout.tsx @@ -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 ( diff --git a/src/routes/ProtectedRoute.tsx b/src/routes/ProtectedRoute.tsx new file mode 100644 index 0000000..68a0214 --- /dev/null +++ b/src/routes/ProtectedRoute.tsx @@ -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 ( +
+ +
+ ); + } + + if (!isAuthenticated) { + // 로그인 되지 않았다면 로그인 페이지로 리다이렉트 + // 현재 위치를 state에 저장하여 로그인 후 다시 돌아올 수 있게 함 + return ; + } + + return <>{children}; +}; + +export default ProtectedRoute; diff --git a/src/routes.ts b/src/routes/routes.ts similarity index 62% rename from src/routes.ts rename to src/routes/routes.ts index ad97fbb..da4b3aa 100644 --- a/src/routes.ts +++ b/src/routes/routes.ts @@ -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' }, diff --git a/src/views/dashboard/Dashboard.tsx b/src/views/dashboard/Dashboard.tsx index 82ddd12..63d6f5a 100644 --- a/src/views/dashboard/Dashboard.tsx +++ b/src/views/dashboard/Dashboard.tsx @@ -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 = [