Skip to content
1 change: 1 addition & 0 deletions src/apis/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const postReports = async (postReportRequest: PostReportRequest) => {
return res;
} catch (error) {
console.error(error);
return null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍 여기 return null은 타입 오류떄문에 추가하신건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넹 원래는 에러코드를 받아오고 싶었는데.. 뭔가뭔가 오류코드를 받아오는게 안되더라구요 흑흑 일단 임시로 null값 출력하게 해놨습니다!

}
};

Expand Down
13 changes: 12 additions & 1 deletion src/apis/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,15 @@ const patchReadNotificationAll = async () => {
}
};

export { getTimeLines, patchReadNotification, patchReadNotificationAll };
const getNotReadCount = async () => {
try {
const res = await client.get('/api/notifications/not-read');
if (!res) throw new Error('안 읽은 알림 수를 가져오는 도중 오류가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.error(error);
}
};

export { getTimeLines, patchReadNotification, patchReadNotificationAll, getNotReadCount };
36 changes: 36 additions & 0 deletions src/components/NotificationButton.tsx
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>
);
}
9 changes: 6 additions & 3 deletions src/components/ReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { postReports } from '@/apis/admin';

import ConfirmModal from './ConfirmModal';
import TextareaField from './TextareaField';
import useToastStore from '@/stores/toastStore';

interface ReportModalProps {
reportType: ReportType;
Expand Down Expand Up @@ -40,14 +41,16 @@ const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => {
else setPostReportRequest((cur) => ({ ...cur, reasonType: reason }));
};

const setToastActive = useToastStore((state) => state.setToastActive);

const handleSubmit = async () => {
const res = await postReports(postReportRequest);
if (res?.status === 200) {
alert('신고 처리되었습니다.');
setToastActive({ title: '신고가 접수되었습니다.', toastType: 'Success' });
console.log(res);
onClose();
} else if (res?.status === 409) {
alert('신고한 이력이 있습니다.');
} else {
setToastActive({ title: '신고한 이력이 있습니다.', toastType: 'Error' });
onClose();
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/ToastItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; ind

const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`;
const toastStyle = twMerge(
'fixed bottom-5 left-1/2 z-40 flex h-[40px] max-w-150 min-w-[300px] w-[100%] -translate-1/2 items-center justify-center rounded-lg caption-sb shadow-[0_1px_6px_rgba(200,200,200,0.2)]',
'fixed bottom-5 left-1/2 z-40 flex h-[40px] max-w-150 min-w-[300px] w-[80%] -translate-1/2 items-center justify-center rounded-lg caption-sb shadow-[0_1px_6px_rgba(200,200,200,0.2)]',
TOAST_POSITION[toastObj.position],
TOAST_DESIGN[toastObj.toastType].style,
);
Expand Down
64 changes: 53 additions & 11 deletions src/hooks/useServerSentEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('로그인 정보 확인불가');
Expand All @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍 이렇게 되면 여러번 reissue가 발생할 것 같은데, 괜찮을지 모르겠네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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회 이상 에러발생으로 구독기능 제거');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약에 제거되면 언제 다시 마운트 되나오ㅛ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저걸 완전히 테스트 해보진 않았지만 일단 반복요청을 막으려고 임시로 넣어뒀습니당

}
};
} catch (error) {
console.error(error);
Expand All @@ -64,6 +105,7 @@ export const useServerSentEvents = () => {
}, [accessToken]);

const closeSSE = () => {
if (reconnect) clearTimeout(reconnect);
sourceRef.current?.close();
sourceRef.current = null;
};
Expand Down
8 changes: 4 additions & 4 deletions src/layouts/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Link, useNavigate } from 'react-router';

import { AlarmIcon, ArrowLeftIcon, PersonIcon } from '@/assets/icons';
import { ArrowLeftIcon, PersonIcon } from '@/assets/icons';
import NotificationButton from '@/components/NotificationButton';

const Header = () => {
// TODO: 뒤로 가기 버튼이 보이는 조건 추가
// TODO: 스크롤 발생 시, 어떻게 보여져야 하는지
const navigate = useNavigate();

return (
<header className="fixed top-0 z-40 flex h-16 w-full max-w-150 items-center justify-between p-5">
<button onClick={() => navigate(-1)}>
<ArrowLeftIcon className="h-6 w-6 text-white" />
</button>
<div className="flex items-center gap-3">
<Link to="/mypage/notifications">
<AlarmIcon className="h-6 w-6 text-white" />
</Link>
<NotificationButton />
<Link to="/mypage">
<PersonIcon className="h-6 w-6 text-white" />
</Link>
Expand Down
7 changes: 3 additions & 4 deletions src/pages/Home/components/HomeHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Link } from 'react-router';

import { AlarmIcon, PersonIcon } from '@/assets/icons';
import { PersonIcon } from '@/assets/icons';
import NotificationButton from '@/components/NotificationButton';

const HomeHeader = () => {
return (
<header className="fixed top-0 z-40 flex h-16 w-full max-w-150 items-center justify-end p-5">
<div className="flex items-center gap-3">
<Link to="/mypage/notifications">
<AlarmIcon className="h-6 w-6 text-white" />
</Link>
<NotificationButton />
<Link to="/mypage">
<PersonIcon className="h-6 w-6 text-white" />
</Link>
Expand Down
8 changes: 1 addition & 7 deletions src/pages/Notifications/components/WarningModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ const WarningModal = ({ isOpen, reportContent, onClose }: WarningModalProps) =>
<p className="caption-r mb-5 text-black">{`${divideContents[1]} 회`}</p>

<h2 className="body-sb mb-1.5 text-gray-100">경고 규칙</h2>
<p className="caption-r text-black">
1회 경고: 주의 안내
<br />
2회 경고: 7일 동안 서비스 이용 제한
<br />
3회 경고: 서비스 이용 불가능
</p>
<p className="caption-r text-black">3회 경고: 서비스 이용 불가능</p>
</div>
</article>
</ModalOverlay>
Expand Down
8 changes: 7 additions & 1 deletion src/pages/Notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import PageTitle from '@/components/PageTitle';
import NotificationItem from './components/NotificationItem';
import WarningModal from './components/WarningModal';
import SendingModal from './components/SendingModal';
import useNotificationStore from '@/stores/notificationStore';

const NotificationsPage = () => {
const navigate = useNavigate();

const decrementNotReadCount = useNotificationStore((state) => state.decrementNotReadCount);
const setNotReadCount = useNotificationStore((state) => state.setNotReadCount);

const [noti, setNoti] = useState<Noti[]>([]);

const [isOpenWarningModal, setIsOpenWarningModal] = useState(false);
Expand Down Expand Up @@ -51,7 +55,8 @@ const NotificationsPage = () => {
if (res?.status === 200) {
setNoti((curNoti) =>
curNoti.map((noti) => {
if (noti.timelineId === timelineId) {
if (noti.timelineId === timelineId && !noti.read) {
decrementNotReadCount();
return { ...noti, read: true };
}
return noti;
Expand All @@ -73,6 +78,7 @@ const NotificationsPage = () => {
return noti;
});
});
setNotReadCount(0);
} else {
console.log('모두 읽음처리 에러 발생');
}
Expand Down
25 changes: 25 additions & 0 deletions src/stores/notificationStore.ts
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;
4 changes: 3 additions & 1 deletion src/types/notifications.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
type AlarmType = 'SENDING' | 'LETTER' | 'REPORT' | 'SHARE' | 'POSTED';

interface Noti {
timelineId: number;
alarmType: string;
alarmType: AlarmType;
content: string | number;
title: string;
read: boolean;
Expand Down