Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const Providers = ({ children, hasRefreshToken }: Props) => {
<QueryProvider>
<MSWProvider>
<AuthProvider hasRefreshToken={hasRefreshToken}>
<NotificationProvider>
<NotificationProvider hasRefreshToken={hasRefreshToken}>
<LazyMotionProvider>
<ToastProvider>
<ModalProvider>{children}</ModalProvider>
Expand Down
158 changes: 92 additions & 66 deletions src/hooks/use-notification/use-notification-connect-sse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventSource | null>(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 };
};
16 changes: 13 additions & 3 deletions src/providers/provider-notification/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<NotificationContext.Provider value={{ unReadCount, receivedNewNotification }}>
{children}
Expand Down