diff --git a/src/App.tsx b/src/App.tsx index 4608fbc..c8af9ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ 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'; @@ -30,7 +29,6 @@ import WritePage from './pages/Write'; const App = () => { useViewport(); - useServerSentEvents(); return ( @@ -39,10 +37,10 @@ const App = () => { } /> } /> } /> - } /> } /> }> + } /> }> } /> diff --git a/src/apis/write.ts b/src/apis/write.ts index 70984de..2a93094 100644 --- a/src/apis/write.ts +++ b/src/apis/write.ts @@ -1,4 +1,3 @@ -// import { AxiosResponse } from 'axios'; import client from './client'; const postLetter = async (data: LetterRequest) => { @@ -33,7 +32,6 @@ const getPrevLetter = async (letterId: string) => { } }; -// 임시저장 최초 생성 const postTemporarySave = async (data: TemporaryRequest) => { try { const res = client.post(`/api/letters/temporary-save`, data); @@ -44,15 +42,4 @@ const postTemporarySave = async (data: TemporaryRequest) => { } }; -// 임시저장 수정 -const PatchTemporarySave = async (data: TemporaryRequest) => { - try { - const res = client.post(`/api/letters/temporary-save`, data); - if (!res) throw new Error('편지 임시저장과정에서 오류가 발생했습니다.'); - return res; - } catch (error) { - console.error(error); - } -}; - -export { postLetter, postFirstReply, getPrevLetter, postTemporarySave, PatchTemporarySave }; +export { postLetter, postFirstReply, getPrevLetter, postTemporarySave }; diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..c09223c --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,16 @@ +import useToastStore from '@/stores/toastStore'; +import ToastItem from './ToastItem'; + +interface Toast {} +export default function Toast({}: Toast) { + const toastObjects = useToastStore((state) => state.toastObjects); + + if (toastObjects.length <= 0) return; + return ( + <> + {toastObjects.map((toastObj, index) => ( + + ))} + + ); +} diff --git a/src/components/ToastItem.tsx b/src/components/ToastItem.tsx new file mode 100644 index 0000000..d4b917e --- /dev/null +++ b/src/components/ToastItem.tsx @@ -0,0 +1,54 @@ +import useToastStore from '@/stores/toastStore'; +import { useEffect } from 'react'; +import { twMerge } from 'tailwind-merge'; + +interface ToastObj { + time: number; + toastType: 'Warning' | 'Success' | 'Error' | 'Info'; + position: 'Top' | 'Bottom'; + title: string; + onClick?: () => void; +} +export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; index: number }) { + const setToastUnActive = useToastStore((state) => state.setToastUnActive); + + const TOAST_DESIGN = { + Warning: { style: 'bg-primary-4', imoji: '⚠️' }, + Success: { style: 'bg-[#38d9a9] text-[#FFFFFF]', imoji: '✅' }, + Error: { style: 'bg-[#FFDCD8] text-[#FF0000]', imoji: '🚨' }, + Info: { style: 'bg-[#FFFFFF]', imoji: '📫' }, + }; + + const TOAST_POSITION = { + Top: 'top-20', + Bottom: 'bottom-20', + }; + + const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; + const toastStyle = twMerge( + 'fixed bottom-20 left-1/2 z-40 flex h-[40px] max-w-150 min-w-[335px] w-[85%] -translate-1/2 items-center justify-center rounded-2xl caption-sb', + TOAST_POSITION[toastObj.position], + TOAST_DESIGN[toastObj.toastType].style, + ); + + const activeTime = toastObj.time * 1000; + useEffect(() => { + const closeToast = setTimeout(() => { + setToastUnActive(index); + }, activeTime); + + return () => clearTimeout(closeToast); + }); + return ( +
{ + setToastUnActive(index); + if (toastObj.onClick) toastObj.onClick(); + }} + > + {`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.title} ${TOAST_DESIGN[toastObj.toastType].imoji}`} +
+ ); +} diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index 2db424f..1ad9850 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -2,11 +2,17 @@ import { EventSourcePolyfill } from 'event-source-polyfill'; import { useEffect, useRef } from 'react'; import useAuthStore from '@/stores/authStore'; +import useToastStore from '@/stores/toastStore'; +import { useNavigate } from 'react-router'; export const useServerSentEvents = () => { - const accessToken = useAuthStore.getState().accessToken; + const navigate = useNavigate(); + + const accessToken = useAuthStore((state) => state.accessToken); const sourceRef = useRef(null); + const setToastActive = useToastStore((state) => state.setToastActive); + useEffect(() => { if (!accessToken) { console.log('로그인 정보 확인불가'); @@ -27,18 +33,20 @@ export const useServerSentEvents = () => { sourceRef.current.onmessage = (event) => { console.log(event); - console.log('알림 전송'); + console.log('알림 수신'); + setToastActive({ + toastType: 'Info', + title: '새 알림이 도착했어요!', + position: 'Top', + time: 5, + onClick: () => navigate('/mypage/notifications'), + }); }; - sourceRef.current.addEventListener('notification', (event) => { - console.log(event); - console.log('알림 전송 dd'); - }); - sourceRef.current.onerror = (error) => { console.log(error); console.log('에러 발생함'); - sourceRef.current?.close(); + closeSSE(); // 재연결 로직 추가 가능 setTimeout(connectSSE, 5000); // 5초 후 재연결 시도 }; @@ -55,11 +63,10 @@ export const useServerSentEvents = () => { }; }, [accessToken]); - // 바깥으로 보낼 closeSSE 함수 const closeSSE = () => { sourceRef.current?.close(); sourceRef.current = null; }; - return { closeSSE }; + // return { closeSSE }; }; diff --git a/src/layouts/PrivateRoute.tsx b/src/layouts/PrivateRoute.tsx index bc78b4a..a607485 100644 --- a/src/layouts/PrivateRoute.tsx +++ b/src/layouts/PrivateRoute.tsx @@ -2,8 +2,11 @@ import { useEffect, useState } from 'react'; import { useNavigate, Outlet } from 'react-router'; import useAuthStore from '@/stores/authStore'; +import { useServerSentEvents } from '@/hooks/useServerSentEvents'; +import Toast from '@/components/Toast'; export default function PrivateRoute() { + useServerSentEvents(); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const navigate = useNavigate(); const [shouldRender, setShouldRender] = useState(false); @@ -20,5 +23,10 @@ export default function PrivateRoute() { return null; } - return ; + return ( + <> + + + + ); } diff --git a/src/pages/Notifications/components/SendingModal.tsx b/src/pages/Notifications/components/SendingModal.tsx new file mode 100644 index 0000000..341532d --- /dev/null +++ b/src/pages/Notifications/components/SendingModal.tsx @@ -0,0 +1,36 @@ +import LetterWrapper from '@/components/LetterWrapper'; +import ModalOverlay from '@/components/ModalOverlay'; +import { useNavigate } from 'react-router'; + +export default function SendingModal({ + isOpenSendingModal, + setIsOpenSendingModal, +}: { + isOpenSendingModal: boolean; + setIsOpenSendingModal: React.Dispatch>; +}) { + const navigate = useNavigate(); + if (!isOpenSendingModal) return null; + const onClose = () => { + setIsOpenSendingModal(false); + }; + return ( + <> + + +
+

편지 도착

+ 편지는 작성된 시점으로 1시간 이후에 도착합니다. + 남은시간은 홈 화면의 편지 도착 시간 버튼을 눌러 확인 가능합니다. + +
+
+
+ + ); +} diff --git a/src/pages/Notifications/components/WarningModal.tsx b/src/pages/Notifications/components/WarningModal.tsx index e76b17d..4470eb3 100644 --- a/src/pages/Notifications/components/WarningModal.tsx +++ b/src/pages/Notifications/components/WarningModal.tsx @@ -4,13 +4,13 @@ import ModalOverlay from '@/components/ModalOverlay'; interface WarningModalProps { isOpen: boolean; - adminText: string; + reportContent: string; onClose: () => void; } -const WarningModal = ({ isOpen, adminText, onClose }: WarningModalProps) => { +const WarningModal = ({ isOpen, reportContent, onClose }: WarningModalProps) => { + const divideContents = reportContent.split('§'); if (!isOpen) return null; - return (
{ >
-

관리자 코멘트

-

{adminText}

-

경고 안내

따사로운 서비스 이용을 위해, 부적절하다고 판단되는 편지는 반려하고 있어요. 서로를 존중하는 따뜻한 공간을 만들기 위해 협조 부탁드립니다.

+

관리자 코멘트

+

{divideContents[0]}

+ +

현재 경고 누적

+

{`${divideContents[1]} 회`}

+

경고 규칙

1회 경고: 주의 안내 diff --git a/src/pages/Notifications/constants/index.ts b/src/pages/Notifications/constants/index.ts index 0b09832..0ab3d29 100644 --- a/src/pages/Notifications/constants/index.ts +++ b/src/pages/Notifications/constants/index.ts @@ -4,6 +4,7 @@ export const NOTIFICATION_ICON: Record< string, React.ComponentType> > = { + SENDING: EnvelopeIcon, LETTER: EnvelopeIcon, REPORT: SirenFilledIcon, SHARE: BoardIcon, diff --git a/src/pages/Notifications/index.tsx b/src/pages/Notifications/index.tsx index d2bc5de..7226f55 100644 --- a/src/pages/Notifications/index.tsx +++ b/src/pages/Notifications/index.tsx @@ -6,6 +6,7 @@ import PageTitle from '@/components/PageTitle'; import NotificationItem from './components/NotificationItem'; import WarningModal from './components/WarningModal'; +import SendingModal from './components/SendingModal'; const NotificationsPage = () => { const navigate = useNavigate(); @@ -13,17 +14,21 @@ const NotificationsPage = () => { const [noti, setNoti] = useState([]); const [isOpenWarningModal, setIsOpenWarningModal] = useState(false); + const [isOpenSendingModal, setIsOpenSendingModal] = useState(false); - const [adminText, setAdmintext] = useState(''); + const [reportContent, setReportContent] = useState(''); // MEMO : 편지 데이터 전송중 데이터도 추가될건데 나중에 데이터 추가되면 코드 업데이트 하긔 const handleClickItem = (alarmType: string, content?: string | number) => { + if (alarmType === 'SENDING') { + setIsOpenSendingModal(true); + } if (alarmType === 'LETTER') { navigate(`/letter/${content}`); } if (alarmType === 'REPORT') { setIsOpenWarningModal(true); - if (typeof content === 'string') setAdmintext(content); + if (typeof content === 'string') setReportContent(content); } if (alarmType === 'SHARE') { navigate(`/board/letter/${content}`, { state: { isShareLetterPreview: true } }); @@ -50,7 +55,16 @@ const NotificationsPage = () => { const handlePatchReadNotificationAll = async () => { const res = await patchReadNotificationAll(); - if (res?.status !== 200) { + if (res?.status === 200) { + setNoti((currentNoti) => { + return currentNoti.map((noti) => { + if (!noti.read) { + return { ...noti, read: true }; + } + return noti; + }); + }); + } else { console.log('모두 읽음처리 에러 발생'); } }; @@ -63,9 +77,13 @@ const NotificationsPage = () => { <> setIsOpenWarningModal(false)} /> +

알림