Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"event-source-polyfill": "^1.0.31",
"gsap": "^3.12.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -31,6 +32,7 @@
"devDependencies": {
"@eslint/js": "^9.19.0",
"@tanstack/eslint-plugin-query": "^5.66.1",
"@types/event-source-polyfill": "^1.0.5",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

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

2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Route, Routes } from 'react-router';

import { useServerSentEvents } from './hooks/useServerSentEvents';
import useViewport from './hooks/useViewport';
import Layout from './layouts/Layout';
import MobileLayout from './layouts/MobileLayout';
Expand Down Expand Up @@ -28,6 +29,7 @@ import WritePage from './pages/Write';

const App = () => {
useViewport();
useServerSentEvents();

return (
<Routes>
Expand Down
34 changes: 34 additions & 0 deletions src/apis/notification.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • !res 대신 res.status를 체크하는 게 좋아보입니당!
  • throw error 로 에러를 다시 던져주는 게 좋을 것 같아요
  • 사소한 거지만 patchReadNotificationpatchReadNotificationAll 의 에러 메시지가 같아서, 구분하기 쉽도록 수정하면 어떨까 합니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import client from './client';

const getTimeLines = async () => {
try {
const res = await client.get('/api/timelines');
if (!res) throw new Error('타임라인을 받아오는 도중 오류가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.error(error);
}
};

const patchReadNotification = async (timelineId: number) => {
try {
const res = await client.patch(`/api/notifications/${timelineId}/read`);
if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

const patchReadNotificationAll = async () => {
try {
const res = await client.patch(`/api/notifications/read`);
if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

export { getTimeLines, patchReadNotification, patchReadNotificationAll };
65 changes: 65 additions & 0 deletions src/hooks/useServerSentEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { EventSourcePolyfill } from 'event-source-polyfill';
import { useEffect, useRef } from 'react';

import useAuthStore from '@/stores/authStore';

export const useServerSentEvents = () => {
const accessToken = useAuthStore.getState().accessToken;
const sourceRef = useRef<EventSourcePolyfill | null>(null);

useEffect(() => {
if (!accessToken) {
console.log('로그인 정보 확인불가');
return;
}

const connectSSE = () => {
try {
console.log('구독 시작');
sourceRef.current = new EventSourcePolyfill(
`${import.meta.env.VITE_API_URL}/api/notifications/sub`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);

sourceRef.current.onmessage = (event) => {
console.log(event);
console.log('알림 전송');
};

sourceRef.current.addEventListener('notification', (event) => {
console.log(event);
console.log('알림 전송 dd');
});

sourceRef.current.onerror = (error) => {
console.log(error);
console.log('에러 발생함');
Copy link
Collaborator

Choose a reason for hiding this comment

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

console.log('에러 발생함' , error) 이런 식으로 한 줄에 작성하는 건 어떠세용?

sourceRef.current?.close();
// 재연결 로직 추가 가능
setTimeout(connectSSE, 5000); // 5초 후 재연결 시도
};
} catch (error) {
console.error(error);
}
};

connectSSE();

return () => {
console.log('컴포넌트 언마운트로 인한 구독해제');
closeSSE();
};
}, [accessToken]);

// 바깥으로 보낼 closeSSE 함수
const closeSSE = () => {
sourceRef.current?.close();
sourceRef.current = null;
};

return { closeSSE };
};
12 changes: 6 additions & 6 deletions src/pages/Notifications/components/NotificationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { NOTIFICATION_ICON } from '../constants';

interface NotificationItemProps {
type: string;
message: string;
isRead: boolean;
title: string;
read: boolean;
onClick: () => void;
}

const NotificationItem = ({ type, message, isRead, onClick }: NotificationItemProps) => {
const NotificationItem = ({ type, title, read, onClick }: NotificationItemProps) => {
const Icon = NOTIFICATION_ICON[type];

const handleClick = (e: React.MouseEvent<HTMLElement>) => {
Expand All @@ -18,11 +18,11 @@ const NotificationItem = ({ type, message, isRead, onClick }: NotificationItemPr
};

return (
<LetterWrapper isSender={type === 'warning'} onClick={handleClick}>
<LetterWrapper isSender={type === 'REPORT'} onClick={handleClick}>
<div className="flex items-center gap-3">
{isRead && <div className="absolute inset-0 z-10 bg-white/60" />}
{read && <div className="absolute inset-0 z-10 bg-white/60" />}
<Icon className="z-0 h-6 w-6 text-white" />
<p className="body-m text-gray-80 z-0">{message}</p>
<p className="body-m text-gray-80 z-0">{title}</p>
</div>
</LetterWrapper>
);
Expand Down
7 changes: 6 additions & 1 deletion src/pages/Notifications/components/WarningModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import ModalOverlay from '@/components/ModalOverlay';

interface WarningModalProps {
isOpen: boolean;
adminText: string;
onClose: () => void;
}

const WarningModal = ({ isOpen, onClose }: WarningModalProps) => {
const WarningModal = ({ isOpen, adminText, onClose }: WarningModalProps) => {
if (!isOpen) return null;

return (
Expand All @@ -20,11 +21,15 @@ const WarningModal = ({ isOpen, onClose }: WarningModalProps) => {
>
<div className="absolute inset-0 h-full w-full bg-white/90 blur-[25px]" />
<div className="relative">
<h2 className="body-sb mb-1.5 text-gray-100">관리자 코멘트</h2>
<p className="caption-r mb-5 text-black">{adminText}</p>

<h2 className="body-sb mb-1.5 text-gray-100">경고 안내</h2>
<p className="caption-r mb-5 text-black">
따사로운 서비스 이용을 위해, 부적절하다고 판단되는 편지는 반려하고 있어요. 서로를
존중하는 따뜻한 공간을 만들기 위해 협조 부탁드립니다.
</p>

<h2 className="body-sb mb-1.5 text-gray-100">경고 규칙</h2>
<p className="caption-r text-black">
1회 경고: 주의 안내
Expand Down
7 changes: 4 additions & 3 deletions src/pages/Notifications/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export const NOTIFICATION_ICON: Record<
string,
React.ComponentType<React.SVGProps<SVGSVGElement>>
> = {
letter: EnvelopeIcon,
warning: SirenFilledIcon,
board: BoardIcon,
LETTER: EnvelopeIcon,
REPORT: SirenFilledIcon,
SHARE: BoardIcon,
POSTED: BoardIcon,
};
95 changes: 69 additions & 26 deletions src/pages/Notifications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,93 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';

import { getTimeLines, patchReadNotification, patchReadNotificationAll } from '@/apis/notification';
import PageTitle from '@/components/PageTitle';

import NotificationItem from './components/NotificationItem';
import WarningModal from './components/WarningModal';

const DUMMY_NOTI = [
{ id: 1, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false },
{ id: 2, type: 'warning', message: '따숨님, 욕설로 인해 경고를 받으셨어요.', isRead: false },
{ id: 3, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false },
{ id: 4, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: true },
{ id: 5, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false },
{ id: 6, type: 'board', message: '12E31님과의 대화가 게시판에 공유되었어요.', isRead: false },
{
id: 7,
type: 'board',
message: '12E31님과의 게시글에 대한 공유요청을 보냈어요.',
isRead: false,
},
];

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

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

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

const handleClickItem = (type: string) => {
if (type === 'warning') {
const [adminText, setAdmintext] = useState<string>('');

// MEMO : 편지 데이터 전송중 데이터도 추가될건데 나중에 데이터 추가되면 코드 업데이트 하긔
const handleClickItem = (alarmType: string, content?: string | number) => {
if (alarmType === 'LETTER') {
navigate(`/letter/${content}`);
}
if (alarmType === 'REPORT') {
setIsOpenWarningModal(true);
if (typeof content === 'string') setAdmintext(content);
}
if (alarmType === 'SHARE') {
navigate(`/board/letter/${content}`, { state: { isShareLetterPreview: true } });
}
if (alarmType === 'POSTED') {
navigate(`/board/letter/${content}`);
}
};

const handleGetTimeLines = async () => {
const res = await getTimeLines();
if (res?.status === 200) {
console.log(res);
setNoti(res.data.data.content);
}
};

const handlePatchReadNotification = async (timelineId: number) => {
const res = await patchReadNotification(timelineId);
if (res?.status !== 200) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이것도 아마 api 호출 catch 문에서 잡힐 것 같습니다!

console.log('읽음처리 에러 발생');
}
};

const handlePatchReadNotificationAll = async () => {
const res = await patchReadNotificationAll();
if (res?.status !== 200) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

얘도 잡힐 것 같습니다!

console.log('모두 읽음처리 에러 발생');
}
};

useEffect(() => {
handleGetTimeLines();
}, []);

return (
<>
<WarningModal isOpen={isOpenWarningModal} onClose={() => setIsOpenWarningModal(false)} />
<WarningModal
isOpen={isOpenWarningModal}
adminText={adminText}
onClose={() => setIsOpenWarningModal(false)}
/>
<main className="flex grow flex-col items-center px-5 pt-20 pb-9">
<PageTitle className="mb-10">알림</PageTitle>
<button type="button" className="body-sb text-gray-60 place-self-end">
<button
type="button"
className="body-sb text-gray-60 place-self-end"
onClick={() => {
handlePatchReadNotificationAll();
}}
>
모두 읽음
</button>
Copy link
Collaborator

Choose a reason for hiding this comment

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

aria-label 😇

<ul className="mt-2 flex h-full w-full flex-col gap-2 pb-10">
{DUMMY_NOTI.map((notification) => (
<li key={notification.id}>
{noti.map((notification) => (
<li key={notification.timelineId}>
<NotificationItem
type={notification.type}
message={notification.message}
isRead={notification.isRead}
onClick={() => handleClickItem(notification.type)}
type={notification.alarmType}
title={notification.title}
read={notification.read}
onClick={() => {
handleClickItem(notification.alarmType, notification.content);
handlePatchReadNotification(notification.timelineId);
}}
/>
</li>
))}
Expand Down
Loading