diff --git a/package-lock.json b/package-lock.json index cc7aade..e6e0d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@headlessui/react": "^2.2.1", "@heroicons/react": "^2.2.0", + "@types/js-cookie": "^3.0.6", "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.6.3", + "js-cookie": "^3.0.5", "lucide-react": "^0.487.0", "next": "15.2.4", "react": "^19.0.0", @@ -1360,6 +1362,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4167,6 +4175,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 907ab8f..e38dfb3 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "dependencies": { "@headlessui/react": "^2.2.1", "@heroicons/react": "^2.2.0", + "@types/js-cookie": "^3.0.6", "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.6.3", + "js-cookie": "^3.0.5", "lucide-react": "^0.487.0", "next": "15.2.4", "react": "^19.0.0", diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7c0520c..006b8e0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -3,13 +3,14 @@ import { useState, useEffect } from 'react'; import { useTheme } from '@/contexts/ThemeContext'; import { UserCircleIcon, KeyIcon, ArrowLeftIcon } from '@heroicons/react/24/outline'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useAuthStore, LoginRequest } from '@/lib/authStore'; import Link from 'next/link'; export default function LoginPage() { const { currentTheme } = useTheme(); const router = useRouter(); + const searchParams = useSearchParams(); // 로그인 폼 데이터 const [loginData, setLoginData] = useState({ @@ -22,15 +23,42 @@ export default function LoginPage() { isLoading, error, isAuthenticated, - login + login, + checkAuth } = useAuthStore(); + // silent 모드 확인 (내부 처리용) + const isSilentMode = searchParams.get('silent') === 'true'; + const callbackUrl = searchParams.get('callbackUrl'); + + // 페이지 로드 시 인증 상태 확인 + useEffect(() => { + // silent 모드일 경우 자동으로 인증 상태 확인 + if (isSilentMode) { + const isAuthed = checkAuth(); + + if (isAuthed) { + // 이미 인증된 경우 callbackUrl로 리다이렉트 + if (callbackUrl) { + router.push(decodeURIComponent(callbackUrl)); + } else { + router.push('/'); + } + } + } + }, [isSilentMode, callbackUrl, router, checkAuth]); + // 로그인 후 리다이렉션 useEffect(() => { if (isAuthenticated) { - router.push('/'); + // callbackUrl 파라미터 확인 + if (callbackUrl) { + router.push(decodeURIComponent(callbackUrl)); + } else { + router.push('/'); + } } - }, [isAuthenticated, router]); + }, [isAuthenticated, router, callbackUrl]); // 입력 필드 변경 처리 const handleInputChange = (e: React.ChangeEvent) => { @@ -52,6 +80,18 @@ export default function LoginPage() { await login(loginData); }; + // silent 모드일 경우 로딩 화면 표시 + if (isSilentMode) { + return ( +
+
+
+

인증 확인 중...

+
+
+ ); + } + return (
diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index aec7bf9..7dbccb7 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useEffect, useState, useRef, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; +import { useEffect, useState, useRef } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useAuthStore } from '@/lib/authStore'; import { useTheme } from '@/contexts/ThemeContext'; import { useVehicleStore, Vehicle } from '@/lib/vehicleStore'; @@ -278,6 +278,7 @@ interface RouteGroup { export default function MonitoringPage() { const router = useRouter(); + const searchParams = useSearchParams(); const { isAuthenticated } = useAuthStore(); const { currentTheme } = useTheme(); const [wsConnected, setWsConnected] = useState(false); @@ -604,14 +605,16 @@ export default function MonitoringPage() { connectWebSocket(); }; - useEffect(() => { - if (!isAuthenticated) { - router.push('/login'); - } - }, [isAuthenticated, router]); - + // 인증되지 않은 경우 로딩 화면 표시 if (!isAuthenticated) { - return null; + return ( +
+
+
+

인증 확인 중...

+
+
+ ); } return ( diff --git a/src/middleware.ts b/src/middleware.ts index 360dd41..b299dba 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -60,9 +60,20 @@ export function middleware(request: NextRequest) { const needsAuth = PROTECTED_PATHS.some(path => pathname === path || pathname.startsWith(`${path}/`)); if (needsAuth && !isAuthenticated) { console.log(`[Middleware] 인증되지 않은 사용자가 보호된 경로 접근: ${pathname} -> /login으로 리다이렉트`); + + // 로그인 페이지로 리다이렉트할 때 특별한 쿼리 파라미터 추가 const url = new URL('/login', request.url); url.searchParams.set('callbackUrl', encodeURI(pathname)); - return NextResponse.redirect(url); + url.searchParams.set('silent', 'true'); // 내부 처리용 플래그 + + // 리다이렉트 응답 생성 + const response = NextResponse.redirect(url); + + // 응답 헤더에 인증 상태 정보 추가 + response.headers.set('x-authenticated', 'false'); + response.headers.set('x-redirect', 'true'); + + return response; } // 인증된 사용자가 로그인/회원가입 페이지에 접근하면 대시보드로 리다이렉트