-
Notifications
You must be signed in to change notification settings - Fork 2
feat : 알림 기능 구현 + QA #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat : 알림 기능 구현 + QA #114
Changes from 7 commits
d3db2f0
c812c55
46cbb1e
928952a
d9d0d01
f300943
c59001c
9e9bccf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { getNotReadCount } from '@/apis/notification'; | ||
| import { AlarmIcon } from '@/assets/icons'; | ||
| import useNotificationStore from '@/stores/notificationStore'; | ||
| import { useEffect } from 'react'; | ||
| import { Link } from 'react-router'; | ||
| import { twMerge } from 'tailwind-merge'; | ||
|
|
||
| export default function NotificationButton() { | ||
| const notReadCount = useNotificationStore((state) => state.notReadCount); | ||
| const setNotReadCount = useNotificationStore((state) => state.setNotReadCount); | ||
| const notReadStyle = twMerge( | ||
| `absolute -right-1 -bottom-1 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-400 text-[8px] text-white`, | ||
| notReadCount >= 100 && 'w-4 h-4', | ||
| ); | ||
|
|
||
| const handleGetNotReadCount = async () => { | ||
| const res = await getNotReadCount(); | ||
| if (res?.status === 200) { | ||
| const updateNotReadCount: number = res.data.data.notReadCount; | ||
| setNotReadCount(updateNotReadCount); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| handleGetNotReadCount(); | ||
| }); | ||
|
|
||
| return ( | ||
| <Link to="/mypage/notifications" className="relative"> | ||
| {notReadCount > 0 && ( | ||
| <div className={notReadStyle}>{notReadCount < 100 ? notReadCount : '99+'}</div> | ||
| )} | ||
| <AlarmIcon className="h-6 w-6 text-white" /> | ||
| </Link> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,15 +4,55 @@ import { useEffect, useRef } from 'react'; | |
| import useAuthStore from '@/stores/authStore'; | ||
| import useToastStore from '@/stores/toastStore'; | ||
| import { useNavigate } from 'react-router'; | ||
| import useNotificationStore from '@/stores/notificationStore'; | ||
| import { getNewToken } from '@/apis/auth'; | ||
|
|
||
| interface MessageEventData { | ||
| title: string; | ||
| alarmType: AlarmType; | ||
| } | ||
|
|
||
| export const useServerSentEvents = () => { | ||
| let reconnect: number | undefined; | ||
|
|
||
| const navigate = useNavigate(); | ||
| const recallCountRef = useRef(1); | ||
|
|
||
| const accessToken = useAuthStore((state) => state.accessToken); | ||
| const setAccessToken = useAuthStore((state) => state.setAccessToken); | ||
| const sourceRef = useRef<EventSourcePolyfill | null>(null); | ||
|
|
||
| const setToastActive = useToastStore((state) => state.setToastActive); | ||
|
|
||
| const incrementNotReadCount = useNotificationStore((state) => state.incrementNotReadCount); | ||
|
|
||
| const ALARM_TYPE: AlarmType[] = ['SENDING', 'LETTER', 'REPORT', 'SHARE', 'POSTED']; | ||
| const handleOnMessage = async (data: string) => { | ||
| const message: MessageEventData = await JSON.parse(data); | ||
| if (ALARM_TYPE.includes(message.alarmType)) { | ||
| incrementNotReadCount(); | ||
| setToastActive({ | ||
| toastType: 'Info', | ||
| title: message.title, | ||
| position: 'Top', | ||
| time: 5, | ||
| onClick: () => navigate('/mypage/notifications'), | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| // 토큰 재발급 함수 | ||
| const callReissue = async () => { | ||
| try { | ||
| const response = await getNewToken(); | ||
| if (response?.status !== 200) throw new Error('error while fetching newToken'); | ||
| const newToken = response?.data.data.accessToken; | ||
| return setAccessToken(newToken); | ||
| } catch (e) { | ||
| return Promise.reject(e); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (!accessToken) { | ||
| console.log('로그인 정보 확인불가'); | ||
|
|
@@ -34,21 +74,22 @@ export const useServerSentEvents = () => { | |
| sourceRef.current.onmessage = (event) => { | ||
| console.log(event); | ||
| console.log('알림 수신'); | ||
| setToastActive({ | ||
| toastType: 'Info', | ||
| title: '새 알림이 도착했어요!', | ||
| position: 'Top', | ||
| time: 5, | ||
| onClick: () => navigate('/mypage/notifications'), | ||
| }); | ||
| handleOnMessage(event.data); | ||
| }; | ||
|
|
||
| sourceRef.current.onerror = (error) => { | ||
| console.log(error); | ||
| console.log('에러 발생함'); | ||
| sourceRef.current.onerror = () => { | ||
| // 에러 발생시 해당 에러가 45초를 넘어서 발생한 에러인지, 401에러인지 판단할 수 있는게 없어서 그냥 에러 발생하면 reissue 넣는걸로 때움 | ||
| callReissue(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오홍 이렇게 되면 여러번 reissue가 발생할 것 같은데, 괜찮을지 모르겠네요
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 이제 백엔드에서 45초마다 에러 발생하는걸 수정하셔서 401에러나 네트워크 에러가 아닌 이상 에러가 발생 안할거 같습니다! |
||
| closeSSE(); | ||
| recallCountRef.current += 1; | ||
| console.log('SSE연결 에러 발생'); | ||
|
|
||
| // 재연결 로직 추가 가능 | ||
| setTimeout(connectSSE, 5000); // 5초 후 재연결 시도 | ||
| if (recallCountRef.current < 5) { | ||
| reconnect = setTimeout(connectSSE, 5000); | ||
| } else { | ||
| console.log('5회 이상 에러발생으로 구독기능 제거'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약에 제거되면 언제 다시 마운트 되나오ㅛ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저걸 완전히 테스트 해보진 않았지만 일단 반복요청을 막으려고 임시로 넣어뒀습니당 |
||
| } | ||
| }; | ||
| } catch (error) { | ||
| console.error(error); | ||
|
|
@@ -64,6 +105,7 @@ export const useServerSentEvents = () => { | |
| }, [accessToken]); | ||
|
|
||
| const closeSSE = () => { | ||
| if (reconnect) clearTimeout(reconnect); | ||
| sourceRef.current?.close(); | ||
| sourceRef.current = null; | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { create } from 'zustand'; | ||
|
|
||
| interface NotificationStore { | ||
| notReadCount: number; | ||
| incrementNotReadCount: () => void; | ||
| decrementNotReadCount: () => void; | ||
| setNotReadCount: (updateCount: number) => void; | ||
| } | ||
| const useNotificationStore = create<NotificationStore>((set) => ({ | ||
| notReadCount: 0, | ||
| incrementNotReadCount: () => | ||
| set((state) => ({ | ||
| notReadCount: state.notReadCount + 1, | ||
| })), | ||
| decrementNotReadCount: () => | ||
| set((state) => ({ | ||
| notReadCount: state.notReadCount - 1, | ||
| })), | ||
| setNotReadCount: (updateCount) => | ||
| set(() => ({ | ||
| notReadCount: updateCount, | ||
| })), | ||
| })); | ||
|
|
||
| export default useNotificationStore; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오홍 여기 return null은 타입 오류떄문에 추가하신건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네넹 원래는 에러코드를 받아오고 싶었는데.. 뭔가뭔가 오류코드를 받아오는게 안되더라구요 흑흑 일단 임시로 null값 출력하게 해놨습니다!