diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 3e1a4a17..7c4cbf51 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -63,13 +63,19 @@ const TokenManager = { setToken: (token: string): void => { if (typeof window !== "undefined") { + // localStorage에 저장 localStorage.setItem("authToken", token); + // 쿠키에도 저장 (미들웨어에서 사용) + document.cookie = `authToken=${token}; path=/; max-age=86400; SameSite=Lax`; } }, removeToken: (): void => { if (typeof window !== "undefined") { + // localStorage에서 제거 localStorage.removeItem("authToken"); + // 쿠키에서도 제거 + document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; } }, diff --git a/frontend/hooks/useLogin.ts b/frontend/hooks/useLogin.ts index 63318de1..1a7513e9 100644 --- a/frontend/hooks/useLogin.ts +++ b/frontend/hooks/useLogin.ts @@ -61,11 +61,15 @@ export const useLogin = () => { * API 호출 공통 함수 */ const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}): Promise => { + // 로컬 스토리지에서 토큰 가져오기 + const token = localStorage.getItem("authToken"); + const response = await fetch(`${API_BASE_URL}${endpoint}`, { credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json", + ...(token && { Authorization: `Bearer ${token}` }), ...options.headers, }, ...options, @@ -90,16 +94,19 @@ export const useLogin = () => { // 토큰이 있으면 API 호출로 유효성 확인 const result = await apiCall(AUTH_CONFIG.ENDPOINTS.STATUS); - if (result.success && result.data?.isLoggedIn) { + // 백엔드가 isAuthenticated 필드를 반환함 + if (result.success && result.data?.isAuthenticated) { // 이미 로그인된 경우 메인으로 리다이렉트 router.push(AUTH_CONFIG.ROUTES.MAIN); } else { // 토큰이 유효하지 않으면 제거 localStorage.removeItem("authToken"); + document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; } } catch (error) { // 에러가 발생하면 토큰 제거 localStorage.removeItem("authToken"); + document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; console.debug("기존 인증 체크 중 오류 (정상):", error); } }, [apiCall, router]); @@ -128,9 +135,12 @@ export const useLogin = () => { }); if (result.success && result.data?.token) { - // JWT 토큰 저장 + // JWT 토큰 저장 (localStorage와 쿠키 모두에 저장) localStorage.setItem("authToken", result.data.token); + // 쿠키에도 저장 (미들웨어에서 사용) + document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`; + // 로그인 성공 router.push(AUTH_CONFIG.ROUTES.MAIN); } else { @@ -148,13 +158,10 @@ export const useLogin = () => { [formData, validateForm, apiCall, router], ); - // 컴포넌트 마운트 시 기존 인증 상태 확인 (한 번만 실행) + // 컴포넌트 마운트 시 기존 인증 상태 확인 useEffect(() => { - // 로그인 페이지에서만 실행 - if (window.location.pathname === "/login") { - checkExistingAuth(); - } - }, []); // 의존성 배열을 비워서 한 번만 실행 + checkExistingAuth(); + }, [checkExistingAuth]); return { // 상태 diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 00000000..eb42b4c2 --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,55 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +/** + * Next.js 미들웨어 + * 페이지 렌더링 전에 실행되어 인증 상태를 확인하고 리다이렉트 처리 + */ +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // 인증 토큰 확인 + const token = request.cookies.get("authToken")?.value || request.headers.get("authorization")?.replace("Bearer ", ""); + + // /login 페이지 접근 시 + if (pathname === "/login") { + // 토큰이 있으면 메인 페이지로 리다이렉트 + if (token) { + const url = request.nextUrl.clone(); + url.pathname = "/main"; + return NextResponse.redirect(url); + } + // 토큰이 없으면 로그인 페이지 표시 + return NextResponse.next(); + } + + // 인증이 필요한 페이지들 + const protectedPaths = ["/main", "/admin", "/dashboard", "/settings"]; + const isProtectedPath = protectedPaths.some((path) => pathname.startsWith(path)); + + if (isProtectedPath && !token) { + // 인증되지 않은 사용자는 로그인 페이지로 리다이렉트 + const url = request.nextUrl.clone(); + url.pathname = "/login"; + return NextResponse.redirect(url); + } + + return NextResponse.next(); +} + +/** + * 미들웨어가 실행될 경로 설정 + */ +export const config = { + matcher: [ + /* + * 다음 경로를 제외한 모든 요청에 대해 실행: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public 폴더의 파일들 + */ + "/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$|.*\\.jpg$|.*\\.jpeg$|.*\\.svg$).*)", + ], +};