2026-02-06 10:43:16 +09:00
|
|
|
/**
|
|
|
|
|
* @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";
|
2026-03-12 09:26:27 +09:00
|
|
|
import { KIS_REMEMBER_LOCAL_STORAGE_KEYS } from "@/features/settings/lib/kis-remember-storage";
|
2026-02-06 10:43:16 +09:00
|
|
|
// import { toast } from "sonner"; // Unused for now
|
|
|
|
|
|
|
|
|
|
// 설정: 경고 표시 시간 (타임아웃 1분 전) - 현재 미사용 (즉시 로그아웃)
|
|
|
|
|
// const WARNING_MS = 60 * 1000;
|
|
|
|
|
|
2026-02-12 14:20:07 +09:00
|
|
|
const SESSION_RELATED_STORAGE_KEYS = [
|
|
|
|
|
"session-storage",
|
|
|
|
|
"auth-storage",
|
|
|
|
|
"autotrade-kis-runtime-store",
|
2026-03-12 09:26:27 +09:00
|
|
|
...KIS_REMEMBER_LOCAL_STORAGE_KEYS,
|
2026-02-12 14:20:07 +09:00
|
|
|
] as const;
|
|
|
|
|
|
2026-02-06 10:43:16 +09:00
|
|
|
/**
|
|
|
|
|
* 세션 관리자 컴포넌트
|
|
|
|
|
* 사용자 활동을 감지하여 세션 연장 및 타임아웃 처리
|
|
|
|
|
* @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();
|
|
|
|
|
|
2026-02-12 14:20:07 +09:00
|
|
|
/**
|
|
|
|
|
* @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);
|
2026-02-26 09:05:17 +09:00
|
|
|
window.sessionStorage.removeItem(key);
|
2026-02-12 14:20:07 +09:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-06 10:43:16 +09:00
|
|
|
/**
|
|
|
|
|
* 로그아웃 처리 핸들러
|
|
|
|
|
* @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();
|
2026-02-12 14:20:07 +09:00
|
|
|
clearSessionRelatedStorage();
|
2026-02-06 10:43:16 +09:00
|
|
|
|
|
|
|
|
// [Step 4] 로그인 페이지로 리다이렉트 및 메시지 표시
|
|
|
|
|
router.push("/login?message=세션이 만료되었습니다. 다시 로그인해주세요.");
|
|
|
|
|
router.refresh();
|
2026-02-12 14:20:07 +09:00
|
|
|
}, [clearSessionRelatedStorage, router]);
|
2026-02-06 10:43:16 +09:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isAuthPage) return;
|
|
|
|
|
|
|
|
|
|
// 마지막 활동 시간 업데이트 함수
|
|
|
|
|
const updateLastActive = () => {
|
|
|
|
|
setLastActive(Date.now());
|
|
|
|
|
if (showWarning) setShowWarning(false);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 14:20:07 +09:00
|
|
|
// [Step 0] 인증 페이지에서 메인 페이지로 진입한 직후를 "활동"으로 간주해
|
|
|
|
|
// 이전 세션 잔여 시간(예: 00:00)으로 즉시 로그아웃되는 현상을 방지합니다.
|
|
|
|
|
updateLastActive();
|
|
|
|
|
|
2026-02-06 10:43:16 +09:00
|
|
|
// [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 (
|
|
|
|
|
<AlertDialog open={showWarning} onOpenChange={setShowWarning}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
{/* ========== 헤더: 제목 및 설명 ========== */}
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>로그아웃 예정</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
장시간 활동이 없어 1분 뒤 로그아웃됩니다. 계속 하시려면 아무 키나
|
|
|
|
|
누르거나 클릭해주세요.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
|
|
|
|
{/* ========== 하단: 액션 버튼 ========== */}
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogAction onClick={() => setShowWarning(false)}>
|
|
|
|
|
로그인 연장
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
);
|
|
|
|
|
}
|