Skip to content

Commit

Permalink
[Feature] SSE 알림 적용 (#345)
Browse files Browse the repository at this point in the history
* ✨ feature: useNotifcationEvent 훅 생성

* 🥅 chore: enabled 옵션 수정

* ✨ feature: 알림페이지 SSE 적용

* ✨ feature: 알림 수신시 토스트 나오기 적용

* ✨ feature: 알림 아이템에 애니메이션 적용
  • Loading branch information
sxungchxn authored Jan 22, 2023
1 parent 87ff7a0 commit cee8faf
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 4 deletions.
131 changes: 131 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@tanstack/react-query-devtools": "^4.16.1",
"axios": "^1.1.3",
"browser-image-compression": "^2.0.0",
"framer-motion": "6.2.4",
"heic2any": "^0.0.3",
"next": "^12.3.3",
"next-seo": "^5.15.0",
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/common/NotificationToast/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useRouter } from 'next/router';

import useNotificationEvent from '@hooks/useNotificationEvent';
import { showToast } from '@utils/toast';

const NotificationToast = () => {
const { isReady, pathname } = useRouter();

useNotificationEvent({
onNotification: (e) => {
showToast({ title: '알림 도착!', message: '알림 페이지에서 내용을 확인해보세요.' });
},
enabled: isReady && pathname !== '/notification',
});

return <></>;
};

export default NotificationToast;
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Link from 'next/link';
import { useState } from 'react';

import { motion } from 'framer-motion';

import styled from '@emotion/styled';
import { ActionIcon, Text } from '@mantine/core';
import { IconX } from '@tabler/icons';
Expand Down Expand Up @@ -32,7 +34,7 @@ const NotificationItem = ({ notification }: Props) => {
onConfirmButtonClick={() => deleteNotification(notification.id)}
onCancelButtonClick={() => setConfirmModalOpen(false)}
/>
<NotificationWrapper>
<NotificationWrapper layout>
<Link href={`/article/${notification.groupArticleId}`}>
<ContentSection>
<IconWrapper>
Expand Down Expand Up @@ -77,7 +79,7 @@ const NotificationItem = ({ notification }: Props) => {

export default NotificationItem;

const NotificationWrapper = styled.div`
const NotificationWrapper = styled(motion.div)`
align-items: center;
display: flex;
gap: 1.6rem;
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/hooks/queries/useFetchNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { AxiosError } from 'axios';

import useAuthInfiniteQuery from '@hooks/useAuthInfiniteQuery';
import useNotificationEvent from '@hooks/useNotificationEvent';
import { NotificationType } from '@typings/types';
import { clientAxios } from '@utils/commonAxios';

Expand All @@ -29,21 +30,26 @@ const getNotifications = async (currentPage: number) => {
};

const useFetchNotifications = () => {
const { data, ...queryResult } = useAuthInfiniteQuery<
const { data, refetch, ...queryResult } = useAuthInfiniteQuery<
NotificationPagingData,
AxiosError,
NotificationPagingData
>(['notifications'], ({ pageParam = 1 }) => getNotifications(pageParam), {
getNextPageParam: (lastPage) =>
lastPage.data.length === 0 ? undefined : lastPage.currentPage + 1,
refetchInterval: 3000,
});

const notifications = useMemo(
() => (data ? data.pages.flatMap(({ data }) => data) : undefined),
[data]
);

useNotificationEvent({
onNotification: (e) => {
void refetch();
},
});

return { data: notifications, ...queryResult };
};

Expand Down
36 changes: 36 additions & 0 deletions frontend/src/hooks/useNotificationEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect } from 'react';

import useFetchMyInfo from '@hooks/queries/useFetchMyInfo';

interface Props {
onNotification: (e: MessageEvent) => void;
enabled?: boolean;
}

const useNotificationEvent = ({ onNotification, enabled = true }: Props) => {
const { data: myData } = useFetchMyInfo();
useEffect(() => {
if (!myData) return;
let sse: EventSource | null = null;
try {
sse = new EventSource(`${process.env.NEXT_PUBLIC_API_URL}/v1/sse`, {
withCredentials: true,
});

sse.addEventListener('NOTIFICATION', (e) => {
if (enabled) onNotification(e);
});

sse.onerror = (event) => {
sse.close();
};
} catch (err) {
throw Error('Server Sent Event Error');
}
return () => {
if (sse) sse.close();
};
}, [myData, onNotification, enabled]);
};

export default useNotificationEvent;
2 changes: 2 additions & 0 deletions frontend/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ApiErrorBoundary from '@components/common/ErrorBoundary/ApiErrorBoundary'
import AuthErrorBoundary from '@components/common/ErrorBoundary/AuthErrorBoundary';
import ErrorBoundary from '@components/common/ErrorBoundary/ErrorBoundary';
import LoginRedirect from '@components/common/LoginRedirect';
import NotificationToast from '@components/common/NotificationToast';
import RouterTransition from '@components/common/RouterTransition';
import ScrollHandler from '@components/common/ScrollHandler';
import initMockApi from '@mocks/.';
Expand Down Expand Up @@ -60,6 +61,7 @@ export default function App({ Component, pageProps }: AppProps<{ dehydratedState
<AuthErrorBoundary>
<ApiErrorBoundary>
<LoginRedirect />
<NotificationToast />
<ScrollHandler />
<Component {...pageProps} />
</ApiErrorBoundary>
Expand Down

0 comments on commit cee8faf

Please sign in to comment.