feat(frontend): 인증 연동 (로그인/로그아웃/내정보/라우트 가드)
- 로그인 실연동: 성공 시 access/refresh 토큰 저장, me 캐시 무효화 - SidebarFooter: useMeQuery 로 공급사명 표시, useLogoutMutation 로 로그아웃 - RequireAuth 가드로 /list·/chat 보호, 토큰 없으면 로그인으로 - 인증 만료/폐기 시 로그인 페이지로 이동(setUnauthorizedHandler) - LoginPage: 이미 로그인 상태면 목록으로 리다이렉트 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bdd10207a
commit
c0d224ef21
@ -1,14 +1,35 @@
|
|||||||
import { createBrowserRouter, RouterProvider } from 'react-router'
|
import { createBrowserRouter, RouterProvider } from 'react-router'
|
||||||
|
import { setUnauthorizedHandler } from '@/apis'
|
||||||
|
import { RequireAuth } from '@/features/auth'
|
||||||
import LoginPage from '@/pages/LoginPage'
|
import LoginPage from '@/pages/LoginPage'
|
||||||
import ListPage from '@/pages/ListPage'
|
import ListPage from '@/pages/ListPage'
|
||||||
import ChatPage from '@/pages/ChatPage'
|
import ChatPage from '@/pages/ChatPage'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: '/', element: <LoginPage /> },
|
{ path: '/', element: <LoginPage /> },
|
||||||
{ path: '/list', element: <ListPage /> },
|
{
|
||||||
{ path: '/chat', element: <ChatPage /> },
|
path: '/list',
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ListPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/chat',
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ChatPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 토큰 만료/폐기로 인증이 끊기면 로그인 페이지로 이동시킨다.
|
||||||
|
setUnauthorizedHandler(() => {
|
||||||
|
void router.navigate('/')
|
||||||
|
})
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <RouterProvider router={router} />
|
return <RouterProvider router={router} />
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/src/features/auth/components/RequireAuth.tsx
Normal file
12
frontend/src/features/auth/components/RequireAuth.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Navigate } from 'react-router'
|
||||||
|
import { tokenStorage } from '@/apis'
|
||||||
|
|
||||||
|
// 토큰이 없으면 로그인 페이지로 보낸다 (인증 영역 가드).
|
||||||
|
// 토큰이 있으나 만료/폐기된 경우는 요청 시 인터셉터가 세션을 종료시킨다.
|
||||||
|
export function RequireAuth({ children }: { children: ReactNode }) {
|
||||||
|
if (!tokenStorage.hasToken()) {
|
||||||
|
return <Navigate to="/" replace />
|
||||||
|
}
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@ -1,18 +1,30 @@
|
|||||||
|
import { useNavigate } from 'react-router'
|
||||||
|
import { useLogoutMutation, useMeQuery } from '@/apis'
|
||||||
import { Button } from '@/components'
|
import { Button } from '@/components'
|
||||||
|
|
||||||
// 사이드바 하단: 공급사명 + 로그아웃
|
// 사이드바 하단: 공급사명 + 로그아웃
|
||||||
export function SidebarFooter() {
|
export function SidebarFooter() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { data: user } = useMeQuery()
|
||||||
|
const logout = useLogoutMutation()
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout.mutate(undefined, {
|
||||||
|
onSuccess: () => navigate('/'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full h-[60px] py-3 px-8 gap-3 items-center">
|
<div className="flex w-full h-[60px] py-3 px-8 gap-3 items-center">
|
||||||
<div className="flex-1 flex items-center min-w-0 text-sm text-foreground">
|
<div className="flex-1 flex items-center min-w-0 text-sm text-foreground">
|
||||||
{/* TODO: 공급사명 (auth store 연동) */}
|
<span className="truncate">{user?.supplierName ?? '-'}</span>
|
||||||
<span className="truncate">-</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[61px] px-2 rounded-[4px] text-xs flex-shrink-0"
|
className="w-[61px] px-2 rounded-[4px] text-xs flex-shrink-0"
|
||||||
// TODO: 로그아웃 mutation 연동
|
onClick={handleLogout}
|
||||||
|
disabled={logout.isPending}
|
||||||
>
|
>
|
||||||
로그아웃
|
로그아웃
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,13 +1,2 @@
|
|||||||
import { useMutation } from '@tanstack/react-query'
|
// 실제 로그인 API 연동은 apis/auth 로 이전됨. 기존 import 경로 호환을 위해 재노출한다.
|
||||||
|
export { useLoginMutation, type LoginParams } from '@/apis/auth'
|
||||||
export interface LoginParams {
|
|
||||||
id: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 임시 stub (검증만 통과하면 성공). TODO: 로그인 API 연동
|
|
||||||
export function useLoginMutation() {
|
|
||||||
return useMutation<void, Error, LoginParams>({
|
|
||||||
mutationFn: async () => {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export { LoginForm } from '@/features/auth/components/LoginForm'
|
export { LoginForm } from '@/features/auth/components/LoginForm'
|
||||||
export { SidebarFooter } from '@/features/auth/components/SidebarFooter'
|
export { SidebarFooter } from '@/features/auth/components/SidebarFooter'
|
||||||
|
export { RequireAuth } from '@/features/auth/components/RequireAuth'
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
|
import { Navigate } from 'react-router'
|
||||||
|
import { tokenStorage } from '@/apis'
|
||||||
import { Logo } from '@/components'
|
import { Logo } from '@/components'
|
||||||
import { LoginForm } from '@/features/auth'
|
import { LoginForm } from '@/features/auth'
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
|
// 이미 로그인된 상태면 목록으로
|
||||||
|
if (tokenStorage.hasToken()) {
|
||||||
|
return <Navigate to="/list" replace />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-svh w-full items-center justify-center px-6">
|
<main className="flex min-h-svh w-full items-center justify-center px-6">
|
||||||
<div className="flex w-full max-w-100 flex-col items-center gap-10">
|
<div className="flex w-full max-w-100 flex-col items-center gap-10">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user