Feat: 세션 유지 컴포넌트 추가 및 주석 디자인 다크테마 적용

This commit is contained in:
2026-02-06 10:43:16 +09:00
parent d31e3f9bc9
commit d2c66a639d
19 changed files with 1341 additions and 273 deletions

View File

@@ -0,0 +1,148 @@
/**
* @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;
/**
* 세션 관리자 컴포넌트
* 사용자 활동을 감지하여 세션 연장 및 타임아웃 처리
* @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();
/**
* 로그아웃 처리 핸들러
* @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();
// [Step 4] 로그인 페이지로 리다이렉트 및 메시지 표시
router.push("/login?message=세션이 만료되었습니다. 다시 로그인해주세요.");
router.refresh();
}, [router]);
useEffect(() => {
if (isAuthPage) return;
// 마지막 활동 시간 업데이트 함수
const updateLastActive = () => {
setLastActive(Date.now());
if (showWarning) setShowWarning(false);
};
// [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>
);
}