diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 1f7f5e3b..5cc02716 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -19,7 +19,7 @@ export const Providers = ({ children, hasRefreshToken }: Props) => { - + {children} diff --git a/src/hooks/use-notification/use-notification-connect-sse/index.ts b/src/hooks/use-notification/use-notification-connect-sse/index.ts index 286efe94..098279e0 100644 --- a/src/hooks/use-notification/use-notification-connect-sse/index.ts +++ b/src/hooks/use-notification/use-notification-connect-sse/index.ts @@ -3,118 +3,144 @@ import { useEffect, useRef, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import Cookies from 'js-cookie'; +import { API } from '@/api'; import { groupKeys } from '@/lib/query-key/query-key-group'; import { notificationKeys } from '@/lib/query-key/query-key-notification'; import { userKeys } from '@/lib/query-key/query-key-user'; import { useAuth } from '@/providers/provider-auth'; import { NotificationItem } from '@/types/service/notification'; -export const useConnectSSE = () => { +export const useConnectSSE = (hasRefreshToken: boolean) => { const [receivedNewNotification, setReceivedNewNotification] = useState(false); - const { isAuthenticated } = useAuth(); + const eventSourceRef = useRef(null); + const retryRefreshRef = useRef(false); const queryClient = useQueryClient(); - // 알림 수신 후 3초 뒤 receivedNewNotification이 false로 변경됨 - useEffect(() => { - if (!receivedNewNotification) return; - - const timer = setTimeout(() => { - setReceivedNewNotification(false); - }, 3000); + // SSE 연결 진입점 + const connect = () => { + if (!isAuthenticated) { + console.log('[DEBUG] SSE - 인증되지 않음'); + return; + } - return () => clearTimeout(timer); - }, [receivedNewNotification]); + const token = Cookies.get('accessToken'); + if (!token) { + console.log('[DEBUG] SSE - 토큰 없음'); + return; + } - // SSE 연결 관련 로직 - useEffect(() => { - if (!isAuthenticated) return; + setupSSEConnection(token); + }; - // 기존 연결이 있으면 정리 + // SSE 연결 해제 함수 + const disconnect = () => { if (eventSourceRef.current) { - console.log('[DEBUG] SSE 기존 연결 정리'); + console.log('[DEBUG] SSE - 연결 정리'); eventSourceRef.current.close(); + eventSourceRef.current = null; } + retryRefreshRef.current = false; + }; - const token = Cookies.get('accessToken'); + // SSE 재연결 시도 함수 + const reconnect = async () => { + if (!hasRefreshToken || retryRefreshRef.current) return; + + retryRefreshRef.current = true; + console.log('[DEBUG] SSE - 토큰 갱신 시도'); + + try { + await API.authService.refresh(); + const token = Cookies.get('accessToken'); + if (token) { + setupSSEConnection(token); + } + } catch (error) { + console.error('[DEBUG] SSE - 토큰 갱신 실패:', error); + disconnect(); + } + }; + + // SSE 연결 설정 함수 + const setupSSEConnection = (token: string) => { + // 기존 연결 정리 + if (eventSourceRef.current) { + console.log('[DEBUG] SSE - 기존 연결 정리'); + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + console.log('[DEBUG] SSE - 연결 시도'); - // SSE 연결 시도 const es = new EventSource( `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/notifications/subscribe?accessToken=${token}`, ); eventSourceRef.current = es; - // SSE 연결 성공 시 es.addEventListener('connect', (event) => { - console.log('[DEBUG] SSE 연결 확인:', event.data); + console.log('[DEBUG] SSE - 연결 확인:', event.data); + retryRefreshRef.current = false; }); - // SSE 알림 수신 시 es.addEventListener('notification', (event) => { try { const data = JSON.parse(event.data) as NotificationItem; - console.log('[DEBUG] SSE 수신 성공:', data); + console.log('[DEBUG] SSE - 수신 성공:', data); setReceivedNewNotification(true); - // Query Key 무효화 - // 공통 queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() }); queryClient.invalidateQueries({ queryKey: notificationKeys.list() }); switch (data.type) { - case 'FOLLOW': // 서버 문제 해결 후 검증 필요 + case 'FOLLOW': queryClient.invalidateQueries({ queryKey: userKeys.me() }); queryClient.invalidateQueries({ queryKey: userKeys.item(data.user.id) }); - return; - case 'GROUP_CREATE': // 모임 목록이 react query 아니라서 업데이트 안됨 + break; + case 'GROUP_CREATE': + case 'GROUP_DELETE': queryClient.invalidateQueries({ queryKey: groupKeys.lists() }); - return; - case 'GROUP_DELETE': // 모임 목록이 react query 아니라서 업데이트 안됨 - queryClient.invalidateQueries({ queryKey: groupKeys.lists() }); - return; - case 'GROUP_JOIN': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); - return; - case 'GROUP_LEAVE': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); - return; - case 'GROUP_JOIN_REQUEST': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ - queryKey: groupKeys.joinRequests(String(data.group.id), 'PENDING'), - }); - case 'GROUP_JOIN_APPROVED': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); - case 'GROUP_JOIN_REJECTED': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); - case 'GROUP_JOIN_KICKED': //OK - if (data.group === null) return; - queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + break; + case 'GROUP_JOIN': + case 'GROUP_LEAVE': + case 'GROUP_JOIN_APPROVED': + case 'GROUP_JOIN_REJECTED': + case 'GROUP_JOIN_KICKED': + if (data.group) { + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + } + break; + case 'GROUP_JOIN_REQUEST': + if (data.group) { + queryClient.invalidateQueries({ + queryKey: groupKeys.joinRequests(String(data.group.id), 'PENDING'), + }); + } + break; } } catch (error) { - console.error('[DEBUG] SSE 데이터 파싱 실패:', error); + console.error('[DEBUG] SSE - 데이터 파싱 실패:', error); } }); - // SSE 연결 실패 시 - es.onerror = (_error) => { - console.log('[DEBUG] SSE 오류 발생:'); - // todo: 재 연결 로직 추가 필요 - }; - - // SSE Cleanup - return () => { - console.log('[DEBUG] SSE 연결 정리'); + es.onerror = async (_error) => { + console.log('[DEBUG] SSE - 연결 오류 발생'); es.close(); - eventSourceRef.current = null; + reconnect(); // ✅ 재연결 함수 호출 }; - }, [isAuthenticated, queryClient]); + }; + + // 알림 수신 후 3초 뒤 receivedNewNotification이 false로 변경됨 + useEffect(() => { + if (!receivedNewNotification) return; + + const timer = setTimeout(() => { + setReceivedNewNotification(false); + }, 3000); + + return () => clearTimeout(timer); + }, [receivedNewNotification]); - return { receivedNewNotification }; + return { receivedNewNotification, connect, disconnect }; }; diff --git a/src/providers/provider-notification/index.tsx b/src/providers/provider-notification/index.tsx index 35e37abb..f12c4377 100644 --- a/src/providers/provider-notification/index.tsx +++ b/src/providers/provider-notification/index.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, useEffect } from 'react'; import { useGetNotificationUnreadCount } from '@/hooks/use-notification'; import { useConnectSSE } from '@/hooks/use-notification/use-notification-connect-sse'; @@ -18,11 +18,21 @@ export const useNotification = () => { interface NotificationProviderProps { children: React.ReactNode; + hasRefreshToken: boolean; } -export const NotificationProvider = ({ children }: NotificationProviderProps) => { +export const NotificationProvider = ({ children, hasRefreshToken }: NotificationProviderProps) => { const { data: unReadCount = 0 } = useGetNotificationUnreadCount(); - const { receivedNewNotification } = useConnectSSE(); + const { receivedNewNotification, connect, disconnect } = useConnectSSE(hasRefreshToken); + + useEffect(() => { + connect(); + return () => { + disconnect(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( {children}