Skip to content

Commit 9dc3f83

Browse files
committed
feat : SSE구현시도
1 parent 665f7b4 commit 9dc3f83

File tree

7 files changed

+137
-66
lines changed

7 files changed

+137
-66
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@tailwindcss/vite": "^4.0.6",
1919
"@tanstack/react-query": "^5.66.0",
2020
"axios": "^1.7.9",
21+
"event-source-polyfill": "^1.0.31",
2122
"gsap": "^3.12.7",
2223
"react": "^18.3.1",
2324
"react-dom": "^18.3.1",
@@ -31,6 +32,7 @@
3132
"devDependencies": {
3233
"@eslint/js": "^9.19.0",
3334
"@tanstack/eslint-plugin-query": "^5.66.1",
35+
"@types/event-source-polyfill": "^1.0.5",
3436
"@types/react": "^19.0.8",
3537
"@types/react-dom": "^19.0.3",
3638
"@vitejs/plugin-react-swc": "^3.5.0",

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/apis/notification.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
import client from './client';
22

3-
const getTimeLines = async (getNoti: React.Dispatch<React.SetStateAction<Noti[]>>) => {
3+
const getTimeLines = async () => {
44
try {
55
const res = await client.get('/api/timelines');
66
if (!res) throw new Error('타임라인을 받아오는 도중 오류가 발생했습니다.');
7-
getNoti(res.data.data);
87
console.log(res);
8+
return res;
99
} catch (error) {
1010
console.error(error);
1111
}
1212
};
1313

14-
export { getTimeLines };
14+
const patchReadNotification = async (timelineId: number) => {
15+
try {
16+
const res = await client.patch(`/api/notifications/${timelineId}/read`);
17+
if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.');
18+
return res;
19+
} catch (error) {
20+
console.error(error);
21+
}
22+
};
23+
24+
const patchReadNotificationAll = async () => {
25+
try {
26+
const res = await client.patch(`/api/notifications/read`);
27+
if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.');
28+
return res;
29+
} catch (error) {
30+
console.error(error);
31+
}
32+
};
33+
34+
export { getTimeLines, patchReadNotification, patchReadNotificationAll };

src/hooks/useServerSentEvents.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { EventSourcePolyfill } from 'event-source-polyfill';
2+
import { useEffect } from 'react';
3+
4+
import useAuthStore from '@/stores/authStore';
5+
6+
export const useServerSentEvents = () => {
7+
const accessToken = useAuthStore.getState().accessToken;
8+
9+
useEffect(() => {
10+
if (!accessToken) return console.log('구독 연결 실패');
11+
12+
let source: EventSourcePolyfill | null = null;
13+
14+
const connectSSE = () => {
15+
try {
16+
console.log('구독 시작');
17+
source = new EventSourcePolyfill(`${import.meta.env.VITE_API_URL}/api/notifications/sub`, {
18+
headers: {
19+
Authorization: `Bearer ${accessToken}`,
20+
},
21+
});
22+
23+
source.onmessage = (payload) => {
24+
console.log(payload);
25+
console.log('알림 전송');
26+
};
27+
28+
source.addEventListener('notification', (event) => {
29+
console.log(event);
30+
console.log('알림 전송 dd');
31+
});
32+
33+
source.onerror = (error) => {
34+
console.log(error);
35+
console.log('에러 발생함');
36+
source?.close();
37+
// 재연결 로직 추가 가능
38+
setTimeout(connectSSE, 5000); // 5초 후 재연결 시도
39+
};
40+
} catch (error) {
41+
console.error(error);
42+
}
43+
};
44+
45+
connectSSE();
46+
47+
return () => {
48+
source?.close();
49+
};
50+
}, [accessToken]);
51+
};

src/pages/Notifications/components/NotificationItem.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { NOTIFICATION_ICON } from '../constants';
44

55
interface NotificationItemProps {
66
type: string;
7-
message: string;
7+
title: string;
88
read: boolean;
99
onClick: () => void;
1010
}
1111

12-
const NotificationItem = ({ type, message, read, onClick }: NotificationItemProps) => {
12+
const NotificationItem = ({ type, title, read, onClick }: NotificationItemProps) => {
1313
const Icon = NOTIFICATION_ICON[type];
1414

1515
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
@@ -22,7 +22,7 @@ const NotificationItem = ({ type, message, read, onClick }: NotificationItemProp
2222
<div className="flex items-center gap-3">
2323
{read && <div className="absolute inset-0 z-10 bg-white/60" />}
2424
<Icon className="z-0 h-6 w-6 text-white" />
25-
<p className="body-m text-gray-80 z-0">{message}</p>
25+
<p className="body-m text-gray-80 z-0">{title}</p>
2626
</div>
2727
</LetterWrapper>
2828
);

src/pages/Notifications/index.tsx

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,19 @@
11
import { useEffect, useState } from 'react';
22
import { useNavigate } from 'react-router';
33

4-
import { getTimeLines } from '@/apis/notification';
4+
import { getTimeLines, patchReadNotification, patchReadNotificationAll } from '@/apis/notification';
55
import PageTitle from '@/components/PageTitle';
6+
import { useServerSentEvents } from '@/hooks/useServerSentEvents';
67

78
import NotificationItem from './components/NotificationItem';
89
import WarningModal from './components/WarningModal';
910

10-
const DUMMY_NOTI: Noti[] = [
11-
{
12-
timelineId: 1,
13-
alarmType: 'LETTER',
14-
content: 1,
15-
message: '12E31님이 편지를 보냈습니다.',
16-
read: false,
17-
},
18-
{
19-
timelineId: 2,
20-
alarmType: 'REPORT',
21-
content: '욕설 확인되어 경고 조치함.',
22-
message: '따숨님, 욕설로 인해 경고를 받으셨어요.',
23-
read: false,
24-
},
25-
{
26-
timelineId: 3,
27-
alarmType: 'LETTER',
28-
content: 1,
29-
message: '12E31님이 편지를 보냈습니다.',
30-
read: false,
31-
},
32-
{
33-
timelineId: 4,
34-
alarmType: 'LETTER',
35-
content: 1,
36-
message: '12E31님이 편지를 보냈습니다.',
37-
read: true,
38-
},
39-
{
40-
timelineId: 5,
41-
alarmType: 'LETTER',
42-
content: 1,
43-
message: '12E31님이 편지를 보냈습니다.',
44-
read: false,
45-
},
46-
{
47-
timelineId: 6,
48-
alarmType: 'POSTED',
49-
content: 1,
50-
message: '12E31님과의 대화가 게시판에 공유되었어요.',
51-
read: false,
52-
},
53-
{
54-
timelineId: 7,
55-
alarmType: 'SHARE',
56-
content: 1,
57-
message: '12E31님과의 게시글에 대한 공유요청을 보냈어요.',
58-
read: false,
59-
},
60-
];
61-
6211
const NotificationsPage = () => {
12+
useServerSentEvents();
13+
6314
const navigate = useNavigate();
6415

65-
const [noti, getNoti] = useState<Noti[]>([]);
16+
const [noti, setNoti] = useState<Noti[]>([]);
6617

6718
const [isOpenWarningModal, setIsOpenWarningModal] = useState(false);
6819

@@ -84,8 +35,30 @@ const NotificationsPage = () => {
8435
}
8536
};
8637

38+
const handleGetTimeLines = async () => {
39+
const res = await getTimeLines();
40+
if (res?.status === 200) {
41+
console.log(res);
42+
setNoti(res.data.data.content);
43+
}
44+
};
45+
46+
const handlePatchReadNotification = async (timelineId: number) => {
47+
const res = await patchReadNotification(timelineId);
48+
if (res?.status !== 200) {
49+
console.log('읽음처리 에러 발생');
50+
}
51+
};
52+
53+
const handlePatchReadNotificationAll = async () => {
54+
const res = await patchReadNotificationAll();
55+
if (res?.status !== 200) {
56+
console.log('모두 읽음처리 에러 발생');
57+
}
58+
};
59+
8760
useEffect(() => {
88-
getTimeLines(getNoti);
61+
handleGetTimeLines();
8962
}, []);
9063

9164
return (
@@ -97,17 +70,26 @@ const NotificationsPage = () => {
9770
/>
9871
<main className="flex grow flex-col items-center px-5 pt-20 pb-9">
9972
<PageTitle className="mb-10">알림</PageTitle>
100-
<button type="button" className="body-sb text-gray-60 place-self-end">
73+
<button
74+
type="button"
75+
className="body-sb text-gray-60 place-self-end"
76+
onClick={() => {
77+
handlePatchReadNotificationAll();
78+
}}
79+
>
10180
모두 읽음
10281
</button>
10382
<ul className="mt-2 flex h-full w-full flex-col gap-2 pb-10">
104-
{DUMMY_NOTI.map((notification) => (
83+
{noti.map((notification) => (
10584
<li key={notification.timelineId}>
10685
<NotificationItem
10786
type={notification.alarmType}
108-
message={notification.message}
87+
title={notification.title}
10988
read={notification.read}
110-
onClick={() => handleClickItem(notification.alarmType, notification.content)}
89+
onClick={() => {
90+
handleClickItem(notification.alarmType, notification.content);
91+
handlePatchReadNotification(notification.timelineId);
92+
}}
11193
/>
11294
</li>
11395
))}

src/types/notifications.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ interface Noti {
22
timelineId: number;
33
alarmType: string;
44
content: string | number;
5-
message: string;
5+
title: string;
66
read: boolean;
77
}

0 commit comments

Comments
 (0)