156 lines
4.6 KiB
JavaScript
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;
|