/** * SSE (Server-Sent Events) 실시간 알림 훅 * 새 메일 도착 알림 및 자동 재연결 처리 */ import { useEffect, useRef } from "react"; import toast from "react-hot-toast"; /** * SSE 연결 및 새 메일 알림 기능을 제공하는 훅 * @param {Object} deps - 의존성 객체 * @param {Object} deps.user - 현재 사용자 * @param {string} deps.selectedBox - 현재 선택된 메일함 * @param {number} deps.page - 현재 페이지 * @param {Function} deps.fetchEmails - 이메일 목록 새로고침 * @param {Function} deps.fetchCounts - 카운트 새로고침 */ export const useSSE = (deps) => { const { user, selectedBox, page, fetchEmails, fetchCounts } = deps; // SSE 연결 상태 및 재연결 관련 ref const eventSourceRef = useRef(null); const reconnectTimeoutRef = useRef(null); const reconnectAttemptRef = useRef(0); // 최신 값을 참조하기 위한 ref (클로저 문제 해결) const selectedBoxRef = useRef(selectedBox); const pageRef = useRef(page); useEffect(() => { selectedBoxRef.current = selectedBox; pageRef.current = page; }, [selectedBox, page]); /** * SSE 연결 함수 (재연결 로직 포함) */ const connectSSE = () => { const token = localStorage.getItem("email_token"); if (!token) return; // 기존 연결이 있으면 정리 if (eventSourceRef.current) { eventSourceRef.current.close(); } const eventSource = new EventSource(`/api/events?token=${token}`); eventSourceRef.current = eventSource; eventSource.onopen = () => { console.log("[SSE] 연결됨"); // 연결 성공 시 재연결 시도 횟수 초기화 reconnectAttemptRef.current = 0; }; eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === "new-mail") { const userEmail = user?.email?.toLowerCase(); const fromString = data.data.from || ""; const toString = data.data.to || ""; // 발신자 이메일 추출 let fromEmail = ""; const emailMatch = fromString.match(/<([^>]+)>/); if (emailMatch) { fromEmail = emailMatch[1].toLowerCase(); } else { fromEmail = fromString.trim().toLowerCase(); } // 받는 사람 확인 - 자신이 받는 사람인지 체크 const isRecipient = toString.toLowerCase().includes(userEmail); // 자신이 받는 사람이고, 발신자가 자신이 아닌 경우에만 알림 표시 if (isRecipient && fromEmail !== userEmail) { toast.success(`새로운 메일이 도착했습니다!\n발신자: ${fromEmail}`, { id: "new-mail", duration: 5000, style: { background: "#333", color: "#fff", borderRadius: "8px", padding: "12px 16px", }, }); // INBOX에 있으면 메일 목록도 자동 새로고침 if (selectedBoxRef.current === "INBOX") { fetchEmails("INBOX", pageRef.current); } } // 카운트 새로고침 fetchCounts(); } } catch (error) { console.error("[SSE] 메시지 파싱 오류:", error); } }; eventSource.onerror = () => { console.warn("[SSE] 연결 끊김, 재연결 예약..."); eventSource.close(); eventSourceRef.current = null; // 지수 백오프로 재연결 시도 (최대 30초) const baseDelay = 1000; // 1초 const maxDelay = 30000; // 30초 const delay = Math.min( baseDelay * Math.pow(2, reconnectAttemptRef.current), maxDelay ); reconnectAttemptRef.current += 1; console.log( `[SSE] ${delay / 1000}초 후 재연결 시도 (시도 횟수: ${ reconnectAttemptRef.current })` ); reconnectTimeoutRef.current = setTimeout(() => { if (user) { connectSSE(); } }, delay); }; }; // SSE 연결 시작 및 정리 useEffect(() => { if (!user) return; connectSSE(); return () => { console.log("[SSE] 연결 종료"); if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } }; }, [user]); // user만 의존성으로 return { eventSourceRef, reconnectAttemptRef, }; }; export default useSSE;