From b0b79c20972f5227f5e3afb6e0e4195317d4f940 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 16:58:07 +0900 Subject: [PATCH 01/16] =?UTF-8?q?refactor(admin):=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/routes/_GNBLayout/component/index.tsx | 190 ------------------ 1 file changed, 190 deletions(-) delete mode 100644 apps/admin/src/routes/_GNBLayout/component/index.tsx diff --git a/apps/admin/src/routes/_GNBLayout/component/index.tsx b/apps/admin/src/routes/_GNBLayout/component/index.tsx deleted file mode 100644 index 0d331da7..00000000 --- a/apps/admin/src/routes/_GNBLayout/component/index.tsx +++ /dev/null @@ -1,190 +0,0 @@ -// import { -// AnswerInput, -// Button, -// Calendar, -// DeleteButton, -// ErrorModalTemplate, -// FloatingButton, -// IconButton, -// ImageUpload, -// Input, -// LevelSelect, -// Modal, -// PlusButton, -// PrevPageButton, -// ProblemCard, -// ProblemPreview, -// SearchInput, -// StatusToggle, -// Tag, -// TagSelect, -// } from '@components'; -// import { useAnswerInput, useModal, useSelectTag } from '@hooks'; -import { createFileRoute } from '@tanstack/react-router'; -// import { LevelType } from '@types'; -// import { useState } from 'react'; - -export const Route = createFileRoute('/_GNBLayout/component/')({ - component: RouteComponent, -}); - -function RouteComponent() { - // const { isOpen, openModal, closeModal } = useModal(); - // const { selectedList, unselectedList, onClickSelectTag, onClickRemoveTag } = useSelectTag(); - // const { problemType, answer, handleClickProblemType, handleChangeAnswer } = useAnswerInput(); - // const [level, setLevel] = useState(); - - return ( -
- //
- // {/*
- // - // - // 발행 - // - // - // - // 세트 - // - // - // - // 문제 - // - //
*/} - //
- //
- // - // - //
- // - // - // - // - // - // - // - // - //
- // - // - // - //
- // {}} /> - // {}} /> - //
- //
- // - //
- // - // - // - //
- // {}} removable={false} /> - // {}} removable={true} /> - //
- //
- //
- // - // {}} /> - // - // - // - // - // - // - // - // - - // - // - // - // - - // - - // - // - // - // - // - // - // - // - // - // - // - - // - - // - // - // - // - // - // - //
- //
- // - //
- //
- // - // - //
- //
- // - // - //
- //
- // - //
- //
- // { - // setLevel(level); - // }} - // /> - //
- //
- // - //
- //
- // - //
- //
- // - //
- //
- // {}}>저장하기 - //
- ); -} From 2c44b5cc8c2e33505f4c891a285fa769dfdab2dc Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 16:58:44 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor(admin):=20checkIsLoggedIn,=20rei?= =?UTF-8?q?ssueToken=20=ED=95=A8=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/apis/authMiddleware.ts | 26 ++------------------ apps/admin/src/utils/auth/checkIsLoggedIn.ts | 16 ++++++++++++ apps/admin/src/utils/auth/index.ts | 4 +++ apps/admin/src/utils/auth/reissueToken.ts | 23 +++++++++++++++++ apps/admin/src/utils/index.ts | 1 + 5 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 apps/admin/src/utils/auth/checkIsLoggedIn.ts create mode 100644 apps/admin/src/utils/auth/index.ts create mode 100644 apps/admin/src/utils/auth/reissueToken.ts diff --git a/apps/admin/src/apis/authMiddleware.ts b/apps/admin/src/apis/authMiddleware.ts index 6b23be5f..b2243f19 100644 --- a/apps/admin/src/apis/authMiddleware.ts +++ b/apps/admin/src/apis/authMiddleware.ts @@ -1,29 +1,9 @@ -import { getAccessToken, setAccessToken } from '@contexts/AuthContext'; +import { getAccessToken } from '@contexts/AuthContext'; +import { reissueToken } from '@utils'; import { Middleware } from 'openapi-fetch'; const UNPROTECTED_ROUTES = ['/api/v1/auth/admin/login']; -const reissueToken = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/v1/auth/reissue`, { - method: 'GET', - credentials: 'include', - }); - - if (!response.ok) throw new Error('Token reissue failed'); - - const data = await response.json(); - const accessToken = data.data.accessToken; - setAccessToken(accessToken); - return accessToken; - } catch (error) { - console.error('Reissue failed:', error); - setAccessToken(null); - window.location.href = '/login'; - return null; - } -}; - const authMiddleware: Middleware = { async onRequest({ schemaPath, request }: { schemaPath: string; request: Request }) { if (UNPROTECTED_ROUTES.some((pathname) => schemaPath.startsWith(pathname))) { @@ -59,8 +39,6 @@ const authMiddleware: Middleware = { request.headers.set('Authorization', `Bearer ${newAccessToken}`); return fetch(request); } - // const cloneJson = await response.clone().json(); - // return cloneJson.data ? new Response(JSON.stringify(cloneJson.data)) : response; return response; }, }; diff --git a/apps/admin/src/utils/auth/checkIsLoggedIn.ts b/apps/admin/src/utils/auth/checkIsLoggedIn.ts new file mode 100644 index 00000000..19085dcd --- /dev/null +++ b/apps/admin/src/utils/auth/checkIsLoggedIn.ts @@ -0,0 +1,16 @@ +import { getAccessToken } from '@contexts/AuthContext'; + +import { reissueToken } from './reissueToken'; + +// 로그인 상태를 확인하는 함수 +export const checkIsLoggedIn = async (): Promise => { + // 액세스 토큰이 있으면 로그인된 것으로 간주 + let accessToken = getAccessToken(); + + // 액세스 토큰이 없으면 리프레시 토큰으로 재발급 시도 + if (!accessToken) { + accessToken = await reissueToken(); + } + + return !!accessToken; +}; diff --git a/apps/admin/src/utils/auth/index.ts b/apps/admin/src/utils/auth/index.ts new file mode 100644 index 00000000..e051c549 --- /dev/null +++ b/apps/admin/src/utils/auth/index.ts @@ -0,0 +1,4 @@ +import { reissueToken } from './reissueToken'; +import { checkIsLoggedIn } from './checkIsLoggedIn'; + +export { reissueToken, checkIsLoggedIn }; diff --git a/apps/admin/src/utils/auth/reissueToken.ts b/apps/admin/src/utils/auth/reissueToken.ts new file mode 100644 index 00000000..2ae08d98 --- /dev/null +++ b/apps/admin/src/utils/auth/reissueToken.ts @@ -0,0 +1,23 @@ +import { setAccessToken } from '@contexts/AuthContext'; + +// 리프레시 토큰을 이용한 액세스 토큰 재발급 +export const reissueToken = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/v1/auth/reissue`, { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) throw new Error('Token reissue failed'); + + const data = await response.json(); + const accessToken = data.data.accessToken; + setAccessToken(accessToken); + return accessToken; + } catch (error) { + console.error('Reissue failed:', error); + setAccessToken(null); + window.location.href = '/login'; + return null; + } +}; diff --git a/apps/admin/src/utils/index.ts b/apps/admin/src/utils/index.ts index b1c13e73..da8e9b97 100644 --- a/apps/admin/src/utils/index.ts +++ b/apps/admin/src/utils/index.ts @@ -1 +1,2 @@ export * from './api'; +export * from './auth'; From a2abd17ed2446c2385418030ae3c285279d43ba2 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 17:12:22 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor(admin):=20tokenStorage=EB=A1=9C?= =?UTF-8?q?=20accessToken=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/apis/authMiddleware.ts | 5 ++-- apps/admin/src/contexts/AuthContext.tsx | 29 -------------------- apps/admin/src/hooks/index.ts | 2 -- apps/admin/src/hooks/useAuth.ts | 12 -------- apps/admin/src/main.tsx | 9 ++---- apps/admin/src/routes/login/index.tsx | 8 +++--- apps/admin/src/utils/auth/checkIsLoggedIn.ts | 5 ++-- apps/admin/src/utils/auth/index.ts | 7 ++--- apps/admin/src/utils/auth/reissueToken.ts | 6 ++-- apps/admin/src/utils/auth/tokenStorage.ts | 15 ++++++++++ 10 files changed, 32 insertions(+), 66 deletions(-) delete mode 100644 apps/admin/src/contexts/AuthContext.tsx delete mode 100644 apps/admin/src/hooks/useAuth.ts create mode 100644 apps/admin/src/utils/auth/tokenStorage.ts diff --git a/apps/admin/src/apis/authMiddleware.ts b/apps/admin/src/apis/authMiddleware.ts index b2243f19..f1919722 100644 --- a/apps/admin/src/apis/authMiddleware.ts +++ b/apps/admin/src/apis/authMiddleware.ts @@ -1,6 +1,5 @@ -import { getAccessToken } from '@contexts/AuthContext'; -import { reissueToken } from '@utils'; import { Middleware } from 'openapi-fetch'; +import { tokenStorage, reissueToken } from '@utils'; const UNPROTECTED_ROUTES = ['/api/v1/auth/admin/login']; @@ -10,7 +9,7 @@ const authMiddleware: Middleware = { return undefined; } - let accessToken = getAccessToken(); + let accessToken = tokenStorage.getToken(); if (!accessToken) { accessToken = await reissueToken(); diff --git a/apps/admin/src/contexts/AuthContext.tsx b/apps/admin/src/contexts/AuthContext.tsx deleted file mode 100644 index d9ef9d83..00000000 --- a/apps/admin/src/contexts/AuthContext.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { createContext, useState, ReactNode } from 'react'; - -export interface AuthContextType { - accessToken: string | null; - setAccessToken: (token: string | null) => void; -} - -const tokenStore = { - accessToken: null as string | null, - setAccessToken: (_: string | null) => {}, -}; - -export const AuthContext = createContext(undefined); - -export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [accessToken, setAccessTokenState] = useState(null); - - tokenStore.accessToken = accessToken; - tokenStore.setAccessToken = setAccessTokenState; - - return ( - - {children} - - ); -}; - -export const getAccessToken = () => tokenStore.accessToken; -export const setAccessToken = (token: string | null) => tokenStore.setAccessToken(token); diff --git a/apps/admin/src/hooks/index.ts b/apps/admin/src/hooks/index.ts index d9125912..6850b434 100644 --- a/apps/admin/src/hooks/index.ts +++ b/apps/admin/src/hooks/index.ts @@ -2,7 +2,6 @@ import useModal from './useModal'; import useSelectTag from './useSelectTag'; import useAnswerInput from './useAnswerInput'; import useNavigation from './useNavigation'; -import useAuth from './useAuth'; import useProblemEssentialInput from './useProblemEssentialInput'; import useInvalidate from './useInvalidate'; @@ -11,7 +10,6 @@ export { useSelectTag, useAnswerInput, useNavigation, - useAuth, useProblemEssentialInput, useInvalidate, }; diff --git a/apps/admin/src/hooks/useAuth.ts b/apps/admin/src/hooks/useAuth.ts deleted file mode 100644 index 9acfd4b3..00000000 --- a/apps/admin/src/hooks/useAuth.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AuthContext } from '@contexts/AuthContext'; -import { useContext } from 'react'; - -const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; - -export default useAuth; diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx index 23a783e6..d563eb0f 100644 --- a/apps/admin/src/main.tsx +++ b/apps/admin/src/main.tsx @@ -2,7 +2,6 @@ import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { RouterProvider, createRouter } from '@tanstack/react-router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { AuthProvider } from '@contexts/AuthContext'; import { routeTree } from './routeTree.gen'; @@ -40,11 +39,9 @@ if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - - - - - + + + ); } diff --git a/apps/admin/src/routes/login/index.tsx b/apps/admin/src/routes/login/index.tsx index 9af98312..27ec690a 100644 --- a/apps/admin/src/routes/login/index.tsx +++ b/apps/admin/src/routes/login/index.tsx @@ -1,8 +1,9 @@ +import { SubmitHandler, useForm } from 'react-hook-form'; import { postLogin } from '@apis'; import { Button, SearchInput } from '@components'; -import { useAuth, useNavigation } from '@hooks'; +import { useNavigation } from '@hooks'; import { createFileRoute } from '@tanstack/react-router'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { tokenStorage } from '@utils'; export const Route = createFileRoute('/login/')({ component: RouteComponent, @@ -14,7 +15,6 @@ interface LoginType { } function RouteComponent() { - const { setAccessToken } = useAuth(); const { mutate } = postLogin(); const { goPublish } = useNavigation(); @@ -36,7 +36,7 @@ function RouteComponent() { onSuccess: (data) => { const { accessToken } = data.data; if (accessToken) { - setAccessToken(accessToken); + tokenStorage.setToken(accessToken); goPublish(); } }, diff --git a/apps/admin/src/utils/auth/checkIsLoggedIn.ts b/apps/admin/src/utils/auth/checkIsLoggedIn.ts index 19085dcd..3e822718 100644 --- a/apps/admin/src/utils/auth/checkIsLoggedIn.ts +++ b/apps/admin/src/utils/auth/checkIsLoggedIn.ts @@ -1,11 +1,10 @@ -import { getAccessToken } from '@contexts/AuthContext'; - +import { tokenStorage } from './tokenStorage'; import { reissueToken } from './reissueToken'; // 로그인 상태를 확인하는 함수 export const checkIsLoggedIn = async (): Promise => { // 액세스 토큰이 있으면 로그인된 것으로 간주 - let accessToken = getAccessToken(); + let accessToken = tokenStorage.getToken(); // 액세스 토큰이 없으면 리프레시 토큰으로 재발급 시도 if (!accessToken) { diff --git a/apps/admin/src/utils/auth/index.ts b/apps/admin/src/utils/auth/index.ts index e051c549..cdb7fa0f 100644 --- a/apps/admin/src/utils/auth/index.ts +++ b/apps/admin/src/utils/auth/index.ts @@ -1,4 +1,3 @@ -import { reissueToken } from './reissueToken'; -import { checkIsLoggedIn } from './checkIsLoggedIn'; - -export { reissueToken, checkIsLoggedIn }; +export * from './tokenStorage'; +export * from './checkIsLoggedIn'; +export * from './reissueToken'; diff --git a/apps/admin/src/utils/auth/reissueToken.ts b/apps/admin/src/utils/auth/reissueToken.ts index 2ae08d98..9e54a3da 100644 --- a/apps/admin/src/utils/auth/reissueToken.ts +++ b/apps/admin/src/utils/auth/reissueToken.ts @@ -1,4 +1,4 @@ -import { setAccessToken } from '@contexts/AuthContext'; +import { tokenStorage } from './tokenStorage'; // 리프레시 토큰을 이용한 액세스 토큰 재발급 export const reissueToken = async () => { @@ -12,11 +12,11 @@ export const reissueToken = async () => { const data = await response.json(); const accessToken = data.data.accessToken; - setAccessToken(accessToken); + tokenStorage.setToken(accessToken); return accessToken; } catch (error) { console.error('Reissue failed:', error); - setAccessToken(null); + tokenStorage.clearToken(); window.location.href = '/login'; return null; } diff --git a/apps/admin/src/utils/auth/tokenStorage.ts b/apps/admin/src/utils/auth/tokenStorage.ts new file mode 100644 index 00000000..3a6f56a2 --- /dev/null +++ b/apps/admin/src/utils/auth/tokenStorage.ts @@ -0,0 +1,15 @@ +const createTokenStorage = () => { + let accessToken: string | null = null; + + return { + getToken: (): string | null => accessToken, + setToken: (token: string | null): void => { + accessToken = token; + }, + clearToken: (): void => { + accessToken = null; + }, + }; +}; + +export const tokenStorage = createTokenStorage(); From 36045c6a83a6936dc98122db0917244da7e679b8 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 17:25:59 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor(admin):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=97=AC=EB=B6=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/routes/__root.tsx | 21 ++++++++++++++++++--- apps/admin/src/routes/login/index.tsx | 9 ++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/admin/src/routes/__root.tsx b/apps/admin/src/routes/__root.tsx index 5e972cec..2517c4c4 100644 --- a/apps/admin/src/routes/__root.tsx +++ b/apps/admin/src/routes/__root.tsx @@ -1,17 +1,32 @@ -import { createRootRoute, Outlet } from '@tanstack/react-router'; +import { lazy } from 'react'; +import { createRootRoute, Outlet, redirect } from '@tanstack/react-router'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import React from 'react'; + +import { checkIsLoggedIn } from '../utils/auth'; const TanStackRouterDevtools = import.meta.env.MODE === 'production' ? () => null - : React.lazy(() => + : lazy(() => import('@tanstack/router-devtools').then((res) => ({ default: res.TanStackRouterDevtools, })) ); export const Route = createRootRoute({ + beforeLoad: async ({ location }) => { + if (location.pathname === '/login') { + return; + } + + const isLoggedIn = await checkIsLoggedIn(); + if (!isLoggedIn) { + throw redirect({ + to: '/login', + }); + } + }, + component: () => { return ( <> diff --git a/apps/admin/src/routes/login/index.tsx b/apps/admin/src/routes/login/index.tsx index 27ec690a..fa13015f 100644 --- a/apps/admin/src/routes/login/index.tsx +++ b/apps/admin/src/routes/login/index.tsx @@ -2,10 +2,17 @@ import { SubmitHandler, useForm } from 'react-hook-form'; import { postLogin } from '@apis'; import { Button, SearchInput } from '@components'; import { useNavigation } from '@hooks'; -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, redirect } from '@tanstack/react-router'; import { tokenStorage } from '@utils'; export const Route = createFileRoute('/login/')({ + beforeLoad: async () => { + if (tokenStorage.getToken()) { + throw redirect({ + to: '/publish', + }); + } + }, component: RouteComponent, }); From cb00d4de776d9d312ed2e0125a85a383ae20cfc9 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 17:27:57 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix(admin):=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=A1=9C=EA=B3=A0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/routes/login/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin/src/routes/login/index.tsx b/apps/admin/src/routes/login/index.tsx index fa13015f..2c5ada1d 100644 --- a/apps/admin/src/routes/login/index.tsx +++ b/apps/admin/src/routes/login/index.tsx @@ -53,7 +53,7 @@ function RouteComponent() { return (
- 로고이미지 + 로고이미지
From 4c857ad09607808f9010c879bd2c6cd2472b67b2 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 17:38:27 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor(admin):=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=85=EB=A0=A5=20=EC=96=B8=EC=96=B4=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/routes/login/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/admin/src/routes/login/index.tsx b/apps/admin/src/routes/login/index.tsx index 2c5ada1d..91f2c5ca 100644 --- a/apps/admin/src/routes/login/index.tsx +++ b/apps/admin/src/routes/login/index.tsx @@ -74,6 +74,8 @@ function RouteComponent() { placeholder='비밀번호를 입력해주세요' type='password' autoComplete='current-password' + lang='en' + inputMode='text' {...register('password', { required: true, pattern: { From 82100fa5a6ee98ebb99389df8be3d16f124bcb43 Mon Sep 17 00:00:00 2001 From: Yoo TaeSeung Date: Mon, 7 Apr 2025 17:43:20 +0900 Subject: [PATCH 07/16] =?UTF-8?q?fix(admin):=20GNB=20=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/components/GNB.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin/src/components/GNB.tsx b/apps/admin/src/components/GNB.tsx index cdc7ab0a..e8eccb7e 100644 --- a/apps/admin/src/components/GNB.tsx +++ b/apps/admin/src/components/GNB.tsx @@ -6,7 +6,7 @@ const GNB = () => { return (
- 로고이미지 + 로고이미지