diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index 3bd8513a..8a31f5fb 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -11,12 +11,13 @@ import loadingImg from '../../../assets/loadingImg.svg'; import { useModal } from '../../../hooks/useModal'; import Modal from '../modal/Modal'; import { formatImgPath } from '../../../util/formatImgPath'; -// import bell from '../../../assets/bell.svg'; -// import Notification from './Notification/Notification'; -// import bellLogined from '../../../assets/bellLogined.svg'; -// import { useEffect } from 'react'; -// import { testLiveAlarm } from '../../../api/alarm.api'; +import bell from '../../../assets/bell.svg'; +import Notification from './Notification/Notification'; +import bellLogined from '../../../assets/bellLogined.svg'; import { useMyProfileInfo } from '../../../hooks/user/useMyInfo'; +import { useNotificationContext } from '../../../context/SseContext'; +import { useEffect } from 'react'; +import { testLiveAlarm } from '../../../api/alarm.api'; import { ROUTES } from '../../../constants/routes'; function Header() { @@ -26,24 +27,26 @@ function Header() { const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const { myData, isLoading } = useMyProfileInfo(); + const { signalData, clearSignal } = useNotificationContext(); + const handleClickLogout = () => { userLogout(); useAuthStore.persist.clearStorage(); localStorage.clear(); }; - // const { signalData, setSignalData } = useNotification(); - - // useEffect(() => { - // testLiveAlarm(); - // }, []); - const profileImg = myData?.profileImg ? `${import.meta.env.VITE_APP_IMAGE_CDN_URL}/${formatImgPath( myData.profileImg )}?w=86&h=86&fit=crop&crop=entropy&auto=format,enhance&q=60` : DefaultImg; + useEffect(() => { + if (process.env.NODE_ENV === 'deployment') { + testLiveAlarm(); + } + }, []); + return ( @@ -58,26 +61,23 @@ function Header() { 공지사항 - {/* + {isLoggedIn ? ( setSignalData(null)}> - 알림 - {signalData && } - - ) : ( + 알림 - ) + {signalData && } + } + comment={false} > ) : ( 알림 )} - */} + { + useNotification(); + + return null; +}; + +export default NotificationInitializer; diff --git a/src/components/user/notificationLive/NotificationProvider.tsx b/src/components/user/notificationLive/NotificationProvider.tsx new file mode 100644 index 00000000..307dcdf3 --- /dev/null +++ b/src/components/user/notificationLive/NotificationProvider.tsx @@ -0,0 +1,16 @@ +import { ReactNode, useState } from 'react'; +import { AlarmLive } from '../../../models/alarm'; +import { SseContext } from '../../../context/SseContext'; + +export const NotificationProvider = ({ children }: { children: ReactNode }) => { + const [signalData, setSignalData] = useState(null); + + const clearSignal = () => setSignalData(null); + const setSignal = (data: AlarmLive | null) => setSignalData(data); + + return ( + + {children} + + ); +}; diff --git a/src/context/SseContext.tsx b/src/context/SseContext.tsx new file mode 100644 index 00000000..d30064f8 --- /dev/null +++ b/src/context/SseContext.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; +import { AlarmLive } from '../models/alarm'; + +export interface SseContextProps { + signalData: AlarmLive | null; + clearSignal: () => void; + setSignal: (data: AlarmLive | null) => void; +} + +export const SseContext = createContext({ + signalData: null, + clearSignal: () => {}, + setSignal: () => {}, +}); + +export const useNotificationContext = () => useContext(SseContext); diff --git a/src/hooks/user/useNotification.ts b/src/hooks/user/useNotification.ts index 4dccf75f..c0980469 100644 --- a/src/hooks/user/useNotification.ts +++ b/src/hooks/user/useNotification.ts @@ -1,20 +1,19 @@ -import { EventSourcePolyfill, NativeEventSource } from 'event-source-polyfill'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { AlarmList } from '../queries/user/keys'; import type { AlarmLive } from '../../models/alarm'; import useAuthStore from '../../store/authStore'; import { useToast } from '../useToast'; +import { useNotificationContext } from '../../context/SseContext'; const useNotification = () => { - const [signalData, setSignalData] = useState(null); const queryClient = useQueryClient(); const accessToken = useAuthStore.getState().accessToken; const userId = useAuthStore.getState().userData?.id; const { showToast } = useToast(); + const { setSignal } = useNotificationContext(); const eventSourceRef = useRef(null); - const EventSourceImpl = EventSourcePolyfill || NativeEventSource; useEffect(() => { if (!userId) { @@ -27,49 +26,51 @@ const useNotification = () => { if (eventSourceRef.current) { return; - } + } else { + // 헤더가 아닌 파라미터 형태로 바꾸면서 Polyfill 제외 하기 -> CORS Preflight를 유발하여 요청 지연의 원인이 될 수 있음. + const eventSource = new EventSource( + `${import.meta.env.VITE_APP_API_BASE_URL}user/sse?accessToken=${ + accessToken ? accessToken : '' + }` + ); - // 헤더가 아닌 파라미터 형태로 바꾸면서 Polyfill 제외 하기 -> CORS Preflight를 유발하여 요청 지연의 원인이 될 수 있음. - const eventSource = new EventSourceImpl( - `${import.meta.env.VITE_APP_API_BASE_URL}user/sse`, - { - headers: { - Authorization: accessToken ? `Bearer ${accessToken}` : '', - 'Content-Type': 'application/json', - }, - heartbeatTimeout: 12 * 60 * 1000, - } - ); + eventSourceRef.current = eventSource; + + eventSource.onopen = () => { + console.log('확인'); + console.log(eventSource.readyState); + }; - eventSourceRef.current = eventSource; + eventSource.addEventListener('alarm', (e) => { + const event = e as MessageEvent; + try { + const eventData: AlarmLive = JSON.parse(event.data); + console.log(eventData); - eventSource.addEventListener('alarm', (e) => { - const event = e as MessageEvent; - try { - const eventData: AlarmLive = JSON.parse(event.data); + if (eventData) { + queryClient.invalidateQueries({ + queryKey: [AlarmList.myAlarmList, userId], + }); + } - if (eventData) { - queryClient.invalidateQueries({ - queryKey: [AlarmList.myAlarmList, userId], - }); + setSignal(eventData); + showToast(eventData, 3000); + } catch (error) { + console.error(error); } + }); + eventSource.onerror = (e) => { + console.error(e); + }; + } - setSignalData(eventData); - } catch (error) { - console.error(error); + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; } - }); - eventSource.onerror = (e) => { - console.error(e); }; - }, [queryClient, userId, accessToken, EventSourceImpl]); - - useEffect(() => { - if (signalData) { - showToast(signalData, 3000); - } - }, [signalData, showToast]); - return { signalData, setSignalData }; + }, [queryClient, userId, accessToken]); }; export default useNotification; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 0a26c8d3..d1b80a73 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -5,14 +5,15 @@ import { RouterProvider, } from 'react-router-dom'; import { lazy, Suspense } from 'react'; - import LoadingSpinner from '../components/common/loadingSpinner/LoadingSpinner'; import useAuthStore from '../store/authStore'; import ProtectRoute from '../components/common/ProtectRoute'; import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import QueryErrorBoundary from '../components/common/error/QueryErrorBoundary'; +import { ToastProvider } from '../components/common/Toast/ToastProvider'; +import NotificationInitializer from '../components/user/notificationLive/NotificationInitializer'; +import { NotificationProvider } from '../components/user/notificationLive/NotificationProvider'; import { ADMIN_ROUTE, ROUTES } from '../constants/routes'; - const Login = lazy(() => import('../pages/login/Login')); const LoginSuccess = lazy(() => import('../pages/login/LoginSuccess')); const LoginApi = lazy(() => import('../pages/login/LoginApi')); @@ -383,7 +384,22 @@ export const AppRoutes = () => { }; }); - return newRouteList; + const router = createBrowserRouter([ + { + element: ( + + + + + + + ), + + children: [...newRouteList, { path: '*', element: }], + }, + ]); + + return ; }; export default AppRoutes;