Skip to content

Commit 3f6df80

Browse files
authored
Merge pull request #325 from devpalsPlus/bug/#310
실시간 알림(SSE) 버그 수정 ( issue #310 )
2 parents 51f863e + da0a3f8 commit 3f6df80

File tree

6 files changed

+120
-62
lines changed

6 files changed

+120
-62
lines changed

src/components/common/header/Header.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import loadingImg from '../../../assets/loadingImg.svg';
1111
import { useModal } from '../../../hooks/useModal';
1212
import Modal from '../modal/Modal';
1313
import { formatImgPath } from '../../../util/formatImgPath';
14-
// import bell from '../../../assets/bell.svg';
15-
// import Notification from './Notification/Notification';
16-
// import bellLogined from '../../../assets/bellLogined.svg';
17-
// import { useEffect } from 'react';
18-
// import { testLiveAlarm } from '../../../api/alarm.api';
14+
import bell from '../../../assets/bell.svg';
15+
import Notification from './Notification/Notification';
16+
import bellLogined from '../../../assets/bellLogined.svg';
1917
import { useMyProfileInfo } from '../../../hooks/user/useMyInfo';
18+
import { useNotificationContext } from '../../../context/SseContext';
19+
import { useEffect } from 'react';
20+
import { testLiveAlarm } from '../../../api/alarm.api';
2021
import { ROUTES } from '../../../constants/routes';
2122

2223
function Header() {
@@ -26,24 +27,26 @@ function Header() {
2627
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
2728
const { myData, isLoading } = useMyProfileInfo();
2829

30+
const { signalData, clearSignal } = useNotificationContext();
31+
2932
const handleClickLogout = () => {
3033
userLogout();
3134
useAuthStore.persist.clearStorage();
3235
localStorage.clear();
3336
};
3437

35-
// const { signalData, setSignalData } = useNotification();
36-
37-
// useEffect(() => {
38-
// testLiveAlarm();
39-
// }, []);
40-
4138
const profileImg = myData?.profileImg
4239
? `${import.meta.env.VITE_APP_IMAGE_CDN_URL}/${formatImgPath(
4340
myData.profileImg
4441
)}?w=86&h=86&fit=crop&crop=entropy&auto=format,enhance&q=60`
4542
: DefaultImg;
4643

44+
useEffect(() => {
45+
if (process.env.NODE_ENV === 'deployment') {
46+
testLiveAlarm();
47+
}
48+
}, []);
49+
4750
return (
4851
<S.HeaderContainer>
4952
<Link to={ROUTES.main}>
@@ -58,26 +61,23 @@ function Header() {
5861
<S.HeaderLink>공지사항</S.HeaderLink>
5962
</Link>
6063
</S.HeaderLinkContainer>
61-
{/* <S.Alarm role='button' tabIndex={0} aria-label='알림 메세지'>
64+
<S.Alarm role='button' tabIndex={0} aria-label='알림 메세지'>
6265
{isLoggedIn ? (
6366
<DropDown
6467
toggleButton={
65-
signalData ? (
66-
<S.BellButton onClick={() => setSignalData(null)}>
67-
<img src={bellLogined} alt='알림' />
68-
{signalData && <S.Dot />}
69-
</S.BellButton>
70-
) : (
68+
<S.BellButton onClick={clearSignal}>
7169
<img src={bellLogined} alt='알림' />
72-
)
70+
{signalData && <S.Dot />}
71+
</S.BellButton>
7372
}
73+
comment={false}
7474
>
7575
<Notification />
7676
</DropDown>
7777
) : (
7878
<img src={bell} alt='알림' />
7979
)}
80-
</S.Alarm> */}
80+
</S.Alarm>
8181
<DropDown
8282
aria-label='프로필 드롭다운'
8383
toggleButton={
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import useNotification from '../../../hooks/user/useNotification';
2+
3+
const NotificationInitializer = () => {
4+
useNotification();
5+
6+
return null;
7+
};
8+
9+
export default NotificationInitializer;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ReactNode, useState } from 'react';
2+
import { AlarmLive } from '../../../models/alarm';
3+
import { SseContext } from '../../../context/SseContext';
4+
5+
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
6+
const [signalData, setSignalData] = useState<AlarmLive | null>(null);
7+
8+
const clearSignal = () => setSignalData(null);
9+
const setSignal = (data: AlarmLive | null) => setSignalData(data);
10+
11+
return (
12+
<SseContext.Provider value={{ signalData, clearSignal, setSignal }}>
13+
{children}
14+
</SseContext.Provider>
15+
);
16+
};

src/context/SseContext.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext, useContext } from 'react';
2+
import { AlarmLive } from '../models/alarm';
3+
4+
export interface SseContextProps {
5+
signalData: AlarmLive | null;
6+
clearSignal: () => void;
7+
setSignal: (data: AlarmLive | null) => void;
8+
}
9+
10+
export const SseContext = createContext<SseContextProps>({
11+
signalData: null,
12+
clearSignal: () => {},
13+
setSignal: () => {},
14+
});
15+
16+
export const useNotificationContext = () => useContext(SseContext);

src/hooks/user/useNotification.ts

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import { EventSourcePolyfill, NativeEventSource } from 'event-source-polyfill';
2-
import { useEffect, useRef, useState } from 'react';
1+
import { useEffect, useRef } from 'react';
32
import { useQueryClient } from '@tanstack/react-query';
43
import { AlarmList } from '../queries/user/keys';
54
import type { AlarmLive } from '../../models/alarm';
65
import useAuthStore from '../../store/authStore';
76
import { useToast } from '../useToast';
7+
import { useNotificationContext } from '../../context/SseContext';
88

99
const useNotification = () => {
10-
const [signalData, setSignalData] = useState<AlarmLive | null>(null);
1110
const queryClient = useQueryClient();
1211
const accessToken = useAuthStore.getState().accessToken;
1312
const userId = useAuthStore.getState().userData?.id;
1413
const { showToast } = useToast();
14+
const { setSignal } = useNotificationContext();
1515

1616
const eventSourceRef = useRef<EventSource | null>(null);
17-
const EventSourceImpl = EventSourcePolyfill || NativeEventSource;
1817

1918
useEffect(() => {
2019
if (!userId) {
@@ -27,49 +26,51 @@ const useNotification = () => {
2726

2827
if (eventSourceRef.current) {
2928
return;
30-
}
29+
} else {
30+
// 헤더가 아닌 파라미터 형태로 바꾸면서 Polyfill 제외 하기 -> CORS Preflight를 유발하여 요청 지연의 원인이 될 수 있음.
31+
const eventSource = new EventSource(
32+
`${import.meta.env.VITE_APP_API_BASE_URL}user/sse?accessToken=${
33+
accessToken ? accessToken : ''
34+
}`
35+
);
3136

32-
// 헤더가 아닌 파라미터 형태로 바꾸면서 Polyfill 제외 하기 -> CORS Preflight를 유발하여 요청 지연의 원인이 될 수 있음.
33-
const eventSource = new EventSourceImpl(
34-
`${import.meta.env.VITE_APP_API_BASE_URL}user/sse`,
35-
{
36-
headers: {
37-
Authorization: accessToken ? `Bearer ${accessToken}` : '',
38-
'Content-Type': 'application/json',
39-
},
40-
heartbeatTimeout: 12 * 60 * 1000,
41-
}
42-
);
37+
eventSourceRef.current = eventSource;
38+
39+
eventSource.onopen = () => {
40+
console.log('확인');
41+
console.log(eventSource.readyState);
42+
};
4343

44-
eventSourceRef.current = eventSource;
44+
eventSource.addEventListener('alarm', (e) => {
45+
const event = e as MessageEvent;
46+
try {
47+
const eventData: AlarmLive = JSON.parse(event.data);
48+
console.log(eventData);
4549

46-
eventSource.addEventListener('alarm', (e) => {
47-
const event = e as MessageEvent;
48-
try {
49-
const eventData: AlarmLive = JSON.parse(event.data);
50+
if (eventData) {
51+
queryClient.invalidateQueries({
52+
queryKey: [AlarmList.myAlarmList, userId],
53+
});
54+
}
5055

51-
if (eventData) {
52-
queryClient.invalidateQueries({
53-
queryKey: [AlarmList.myAlarmList, userId],
54-
});
56+
setSignal(eventData);
57+
showToast(eventData, 3000);
58+
} catch (error) {
59+
console.error(error);
5560
}
61+
});
62+
eventSource.onerror = (e) => {
63+
console.error(e);
64+
};
65+
}
5666

57-
setSignalData(eventData);
58-
} catch (error) {
59-
console.error(error);
67+
return () => {
68+
if (eventSourceRef.current) {
69+
eventSourceRef.current.close();
70+
eventSourceRef.current = null;
6071
}
61-
});
62-
eventSource.onerror = (e) => {
63-
console.error(e);
6472
};
65-
}, [queryClient, userId, accessToken, EventSourceImpl]);
66-
67-
useEffect(() => {
68-
if (signalData) {
69-
showToast(signalData, 3000);
70-
}
71-
}, [signalData, showToast]);
72-
return { signalData, setSignalData };
73+
}, [queryClient, userId, accessToken]);
7374
};
7475

7576
export default useNotification;

src/routes/AppRoutes.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import {
55
RouterProvider,
66
} from 'react-router-dom';
77
import { lazy, Suspense } from 'react';
8-
98
import LoadingSpinner from '../components/common/loadingSpinner/LoadingSpinner';
109
import useAuthStore from '../store/authStore';
1110
import ProtectRoute from '../components/common/ProtectRoute';
1211
import NotFoundPage from '../pages/notFoundPage/NotFoundPage';
1312
import QueryErrorBoundary from '../components/common/error/QueryErrorBoundary';
13+
import { ToastProvider } from '../components/common/Toast/ToastProvider';
14+
import NotificationInitializer from '../components/user/notificationLive/NotificationInitializer';
15+
import { NotificationProvider } from '../components/user/notificationLive/NotificationProvider';
1416
import { ADMIN_ROUTE, ROUTES } from '../constants/routes';
15-
1617
const Login = lazy(() => import('../pages/login/Login'));
1718
const LoginSuccess = lazy(() => import('../pages/login/LoginSuccess'));
1819
const LoginApi = lazy(() => import('../pages/login/LoginApi'));
@@ -383,7 +384,22 @@ export const AppRoutes = () => {
383384
};
384385
});
385386

386-
return newRouteList;
387+
const router = createBrowserRouter([
388+
{
389+
element: (
390+
<NotificationProvider>
391+
<ToastProvider>
392+
<NotificationInitializer />
393+
<Outlet />
394+
</ToastProvider>
395+
</NotificationProvider>
396+
),
397+
398+
children: [...newRouteList, { path: '*', element: <NotFoundPage /> }],
399+
},
400+
]);
401+
402+
return <RouterProvider router={router} />;
387403
};
388404

389405
export default AppRoutes;

0 commit comments

Comments
 (0)