/** * @file features/auth/components/session-manager.tsx * @description 사용자 세션 타임아웃 및 자동 로그아웃 관리 컴포넌트 * @remarks * - [레이어] Components/Infrastructure * - [사용자 행동] 로그인 -> 활동 감지 -> 비활동 -> (경고) -> 로그아웃 * - [데이터 흐름] Event -> Zustand Store -> Timer -> Logout * - [연관 파일] stores/session-store.ts, features/auth/constants.ts */ "use client"; import { useEffect, useState, useCallback } from "react"; import { createClient } from "@/utils/supabase/client"; import { useRouter, usePathname } from "next/navigation"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useSessionStore } from "@/stores/session-store"; import { SESSION_TIMEOUT_MS } from "@/features/auth/constants"; // import { toast } from "sonner"; // Unused for now // 설정: 경고 표시 시간 (타임아웃 1분 전) - 현재 미사용 (즉시 로그아웃) // const WARNING_MS = 60 * 1000; const SESSION_RELATED_STORAGE_KEYS = [ "session-storage", "auth-storage", "autotrade-kis-runtime-store", ] as const; /** * 세션 관리자 컴포넌트 * 사용자 활동을 감지하여 세션 연장 및 타임아웃 처리 * @returns 숨겨진 기능성 컴포넌트 (Global Layout에 포함) * @remarks RootLayout에 포함되어 전역적으로 동작 * @see layout.tsx - RootLayout에서 렌더링 * @see session-store.ts - 마지막 활동 시간 관리 */ export function SessionManager() { const router = useRouter(); const pathname = usePathname(); // [State] 타임아웃 경고 모달 표시 여부 (현재 미사용) const [showWarning, setShowWarning] = useState(false); // 인증 페이지에서는 동작하지 않음 const isAuthPage = ["/login", "/signup", "/forgot-password"].includes( pathname, ); const { setLastActive } = useSessionStore(); /** * @description 세션 만료 로그아웃 시 세션 관련 로컬 스토리지를 정리합니다. * @see features/layout/components/user-menu.tsx 수동 로그아웃 경로에서도 동일한 키를 제거합니다. */ const clearSessionRelatedStorage = useCallback(() => { if (typeof window === "undefined") return; for (const key of SESSION_RELATED_STORAGE_KEYS) { window.localStorage.removeItem(key); window.sessionStorage.removeItem(key); } }, []); /** * 로그아웃 처리 핸들러 * @see session-timer.tsx - 타임아웃 시 동일한 로직 필요 가능성 있음 */ const handleLogout = useCallback(async () => { // [Step 1] Supabase 클라이언트 생성 const supabase = createClient(); // [Step 2] 서버 사이드 로그아웃 요청 await supabase.auth.signOut(); // [Step 3] 로컬 스토어 및 세션 정보 초기화 useSessionStore.persist.clearStorage(); clearSessionRelatedStorage(); // [Step 4] 로그인 페이지로 리다이렉트 및 메시지 표시 router.push("/login?message=세션이 만료되었습니다. 다시 로그인해주세요."); router.refresh(); }, [clearSessionRelatedStorage, router]); useEffect(() => { if (isAuthPage) return; // 마지막 활동 시간 업데이트 함수 const updateLastActive = () => { setLastActive(Date.now()); if (showWarning) setShowWarning(false); }; // [Step 0] 인증 페이지에서 메인 페이지로 진입한 직후를 "활동"으로 간주해 // 이전 세션 잔여 시간(예: 00:00)으로 즉시 로그아웃되는 현상을 방지합니다. updateLastActive(); // [Step 1] 사용자 활동 이벤트 감지 (마우스, 키보드, 스크롤, 터치) const events = ["mousedown", "keydown", "scroll", "touchstart"]; const handleActivity = () => updateLastActive(); events.forEach((event) => window.addEventListener(event, handleActivity)); // [Step 2] 주기적(1초)으로 세션 만료 여부 확인 const intervalId = setInterval(async () => { const currentLastActive = useSessionStore.getState().lastActive; const now = Date.now(); const timeSinceLastActive = now - currentLastActive; // 타임아웃 초과 시 로그아웃 if (timeSinceLastActive >= SESSION_TIMEOUT_MS) { await handleLogout(); } // 경고 로직 (현재 비활성) // else if (timeSinceLastActive >= TIMEOUT_MS - WARNING_MS) { // setShowWarning(true); // } }, 1000); // [Step 3] 탭 활성화/컴퓨터 깨어남 감지 (절전 모드 대응) const handleVisibilityChange = async () => { if (!document.hidden) { const currentLastActive = useSessionStore.getState().lastActive; const now = Date.now(); // 절전 모드 복귀 시 즉시 만료 체크 if (now - currentLastActive >= SESSION_TIMEOUT_MS) { await handleLogout(); } } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { events.forEach((event) => window.removeEventListener(event, handleActivity), ); clearInterval(intervalId); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [pathname, isAuthPage, showWarning, handleLogout, setLastActive]); return ( {/* ========== 헤더: 제목 및 설명 ========== */} 로그아웃 예정 장시간 활동이 없어 1분 뒤 로그아웃됩니다. 계속 하시려면 아무 키나 누르거나 클릭해주세요. {/* ========== 하단: 액션 버튼 ========== */} setShowWarning(false)}> 로그인 연장 ); }