From e9897d416b3e7327bbd4d4aef887eee37751ae82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 27 Jun 2025 01:16:20 +0000 Subject: (대표님) 20250627 오전 10시 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/layout/SessionManager.tsx | 361 +++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 components/layout/SessionManager.tsx (limited to 'components/layout/SessionManager.tsx') diff --git a/components/layout/SessionManager.tsx b/components/layout/SessionManager.tsx new file mode 100644 index 00000000..c917c5f3 --- /dev/null +++ b/components/layout/SessionManager.tsx @@ -0,0 +1,361 @@ +// components/layout/SessionManager.tsx +'use client' + +import { useSession } from "next-auth/react" +import { useEffect, useState, useCallback } from "react" +import { useRouter } from "next/navigation" +import { AlertCircle, Clock, RefreshCw, X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Progress } from "@/components/ui/progress" +import { useToast } from "@/hooks/use-toast" +import { Card, CardContent } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +interface SessionManagerProps { + lng: string; +} + +// 다국어 메시지 +const messages = { + ko: { + sessionExpiring: "세션 만료 경고", + sessionWillExpire: "세션이 {minutes}분 후에 만료됩니다.", + sessionExpired: "세션이 만료되었습니다", + pleaseRelogin: "다시 로그인해주세요.", + extend: "연장", + extending: "연장 중...", + close: "닫기", + sessionExtended: "세션이 연장되었습니다", + sessionExtendFailed: "세션 연장에 실패했습니다", + autoLogoutIn: "{seconds}초 후 자동 로그아웃됩니다", + staySignedIn: "로그인 유지", + logout: "로그아웃" + }, + en: { + sessionExpiring: "Session Expiring", + sessionWillExpire: "Your session will expire in {minutes} minute(s).", + sessionExpired: "Session Expired", + pleaseRelogin: "Please log in again.", + extend: "Extend", + extending: "Extending...", + close: "Close", + sessionExtended: "Session has been extended", + sessionExtendFailed: "Failed to extend session", + autoLogoutIn: "Auto logout in {seconds} seconds", + staySignedIn: "Stay Signed In", + logout: "Logout" + } +} as const; + +export function SessionManager({ lng }: SessionManagerProps) { + const { data: session, update } = useSession() + const router = useRouter() + const { toast } = useToast() + + const [showWarning, setShowWarning] = useState(false) + const [showExpiredModal, setShowExpiredModal] = useState(false) + const [isExtending, setIsExtending] = useState(false) + const [autoLogoutCountdown, setAutoLogoutCountdown] = useState(0) + const [timeLeft, setTimeLeft] = useState(null) + + const t = messages[lng as keyof typeof messages] || messages.en + + // 세션 연장 함수 + const extendSession = useCallback(async () => { + if (isExtending) return; + + setIsExtending(true) + try { + await update({ + reAuthTime: Date.now() + }) + + setShowWarning(false) + setTimeLeft(null) + + toast({ + title: t.sessionExtended, + description: "세션이 성공적으로 연장되었습니다.", + duration: 3000, + }) + } catch (error) { + console.error('Failed to extend session:', error) + toast({ + title: t.sessionExtendFailed, + description: "다시 시도해주세요.", + variant: "destructive", + duration: 5000, + }) + } finally { + setIsExtending(false) + } + }, [isExtending, update, toast, t]) + + // 자동 로그아웃 처리 + const handleAutoLogout = useCallback(() => { + setShowExpiredModal(false) + setShowWarning(false) + window.location.href = `/${lng}/evcp?reason=expired` + }, [lng]) + + // 세션 만료 체크 + useEffect(() => { + if (!session?.user?.sessionExpiredAt) return + + const checkSession = () => { + const now = Date.now() + const expiresAt = session.user.sessionExpiredAt! + const timeUntilExpiry = expiresAt - now + const warningThreshold = 5 * 60 * 1000 // 5분 + const criticalThreshold = 1 * 60 * 1000 // 1분 + + setTimeLeft(timeUntilExpiry) + + // 세션 만료됨 + if (timeUntilExpiry <= 0) { + setShowWarning(false) + setShowExpiredModal(true) + setAutoLogoutCountdown(10) // 10초 후 자동 로그아웃 + return + } + + // 1분 이내 - 긴급 경고 + if (timeUntilExpiry <= criticalThreshold) { + setShowWarning(true) + return + } + + // 5분 이내 - 일반 경고 + if (timeUntilExpiry <= warningThreshold && !showWarning) { + setShowWarning(true) + return + } + + // 경고 해제 + if (timeUntilExpiry > warningThreshold && showWarning) { + setShowWarning(false) + } + } + + // 즉시 체크 + checkSession() + + // 5초마다 체크 (더 정확한 카운트다운을 위해) + const interval = setInterval(checkSession, 5000) + + return () => clearInterval(interval) + }, [session, showWarning]) + + // 자동 로그아웃 카운트다운 + useEffect(() => { + if (autoLogoutCountdown <= 0) return + + const timer = setTimeout(() => { + if (autoLogoutCountdown === 1) { + handleAutoLogout() + } else { + setAutoLogoutCountdown(prev => prev - 1) + } + }, 1000) + + return () => clearTimeout(timer) + }, [autoLogoutCountdown, handleAutoLogout]) + + // 사용자 활동 감지 + useEffect(() => { + let activityTimer: NodeJS.Timeout + let lastActivity = Date.now() + + const resetActivityTimer = () => { + const now = Date.now() + const timeSinceLastActivity = now - lastActivity + + // 5분 이상 비활성 후 첫 활동이면 세션 연장 + if (timeSinceLastActivity > 5 * 60 * 1000) { + extendSession() + } + + lastActivity = now + clearTimeout(activityTimer) + + // 10분간 비활성이면 경고 표시 + activityTimer = setTimeout(() => { + if (!showWarning && session?.user?.sessionExpiredAt) { + const timeUntilExpiry = session.user.sessionExpiredAt - Date.now() + if (timeUntilExpiry > 0 && timeUntilExpiry <= 10 * 60 * 1000) { + setShowWarning(true) + } + } + }, 10 * 60 * 1000) + } + + const activities = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click'] + + activities.forEach(activity => { + document.addEventListener(activity, resetActivityTimer, true) + }) + + resetActivityTimer() // 초기 타이머 설정 + + return () => { + clearTimeout(activityTimer) + activities.forEach(activity => { + document.removeEventListener(activity, resetActivityTimer, true) + }) + } + }, [extendSession, showWarning, session]) + + const formatTime = (ms: number) => { + const minutes = Math.floor(ms / (1000 * 60)) + const seconds = Math.floor((ms % (1000 * 60)) / 1000) + return { minutes, seconds } + } + + // 세션 만료 모달 + if (showExpiredModal) { + return ( +
+ + +
+ +

{t.sessionExpired}

+
+ +

+ {t.pleaseRelogin} +

+ + {autoLogoutCountdown > 0 && ( +
+

+ {t.autoLogoutIn.replace('{seconds}', autoLogoutCountdown.toString())} +

+ +
+ )} + +
+ + +
+
+
+
+ ) + } + + // 세션 경고 알림 + if (showWarning && timeLeft) { + const { minutes, seconds } = formatTime(timeLeft) + const isCritical = timeLeft <= 60000 // 1분 이내 + const progressValue = Math.max(0, Math.min(100, (timeLeft / (5 * 60 * 1000)) * 100)) + + return ( +
+ + + +
+
+

+ {t.sessionExpiring} +

+ +
+ + + {minutes > 0 + ? t.sessionWillExpire.replace('{minutes}', minutes.toString()) + : `${seconds}초 후 세션이 만료됩니다.` + } + + +
+ + +
+ + +
+
+
+
+
+ ) + } + + return null +} \ No newline at end of file -- cgit v1.2.3