mailbox/frontend/src/hooks/useSSE.js
2025-12-16 08:18:15 +09:00

156 lines
4.6 KiB
JavaScript

/**
* 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;