From 1a81fe6cf8f67b9839b04db4e2c7b953dc17fd04 Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Fri, 7 Mar 2025 18:57:06 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=97=90=20SENDING=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Notifications/constants/index.ts | 1 + src/pages/Notifications/index.tsx | 3 +++ 2 files changed, 4 insertions(+) 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..fc044cc 100644 --- a/src/pages/Notifications/index.tsx +++ b/src/pages/Notifications/index.tsx @@ -18,6 +18,9 @@ const NotificationsPage = () => { // MEMO : 편지 데이터 전송중 데이터도 추가될건데 나중에 데이터 추가되면 코드 업데이트 하긔 const handleClickItem = (alarmType: string, content?: string | number) => { + if (alarmType === 'SENDING') { + navigate('/'); + } if (alarmType === 'LETTER') { navigate(`/letter/${content}`); } From 68c79e3dcef7b829d40824294fabd48c02953d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Sat, 8 Mar 2025 16:31:02 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=84=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B0=94=EC=9D=B8=EB=94=A9=20+=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=ED=83=80=EC=9E=85=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20UI=EC=9E=91=EC=97=85=20=EC=99=84=EB=A3=8C(=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20UI=EC=9E=91=EC=97=85=EB=A7=8C=20=EB=82=A8=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useServerSentEvents.tsx | 5 --- .../Notifications/components/SendingModal.tsx | 36 +++++++++++++++++++ .../Notifications/components/WarningModal.tsx | 15 ++++---- src/pages/Notifications/index.tsx | 25 ++++++++++--- 4 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 src/pages/Notifications/components/SendingModal.tsx diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index 2db424f..62f6396 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -30,11 +30,6 @@ export const useServerSentEvents = () => { console.log('알림 전송'); }; - sourceRef.current.addEventListener('notification', (event) => { - console.log(event); - console.log('알림 전송 dd'); - }); - sourceRef.current.onerror = (error) => { console.log(error); console.log('에러 발생함'); 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/index.tsx b/src/pages/Notifications/index.tsx index fc044cc..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,20 +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') { - navigate('/'); + 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 } }); @@ -53,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('모두 읽음처리 에러 발생'); } }; @@ -66,9 +77,13 @@ const NotificationsPage = () => { <> setIsOpenWarningModal(false)} /> +

알림
); }; diff --git a/src/stores/toastStore.ts b/src/stores/toastStore.ts new file mode 100644 index 0000000..768e28b --- /dev/null +++ b/src/stores/toastStore.ts @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; +import { create } from 'zustand'; + +interface ToastObj { + time: number; + toastType: 'Warning' | 'Success' | 'Error' | 'Info'; + children: ReactNode; +} + +interface ToastStore { + isActive: boolean; + toastObj: ToastObj; + setToastActive: (prompt: Partial) => void; + setToastUnActive: () => void; +} +const useToastStore = create((set) => ({ + isActive: false, + toastObj: { + time: 1, + toastType: 'Info', + children: '', + }, + setToastActive: (prompt) => + set((state) => ({ + isActive: true, + toastObj: { ...state.toastObj, ...prompt }, + })), + setToastUnActive: () => { + set(() => ({ + isActive: false, + })); + }, +})); + +export default useToastStore; From e74e0c451682a8189ad58869e6c58924fd27ae44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Sat, 8 Mar 2025 23:11:36 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20SSE=ED=9B=85=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=9C=84=EC=B9=98=20App=EC=97=90=EC=84=9C=20Privat?= =?UTF-8?q?eRouter=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20+=20Home=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8A=B8=20PrivateRouter=20=EC=95=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 4 +-- src/hooks/useServerSentEvents.tsx | 5 ++- src/layouts/PrivateRoute.tsx | 2 ++ src/stores/sseStore.ts | 55 ------------------------------- 4 files changed, 5 insertions(+), 61 deletions(-) delete mode 100644 src/stores/sseStore.ts 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/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index 62f6396..e18216e 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react'; import useAuthStore from '@/stores/authStore'; export const useServerSentEvents = () => { - const accessToken = useAuthStore.getState().accessToken; + const accessToken = useAuthStore((state) => state.accessToken); const sourceRef = useRef(null); useEffect(() => { @@ -50,11 +50,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..7ae4ea9 100644 --- a/src/layouts/PrivateRoute.tsx +++ b/src/layouts/PrivateRoute.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { useNavigate, Outlet } from 'react-router'; import useAuthStore from '@/stores/authStore'; +import { useServerSentEvents } from '@/hooks/useServerSentEvents'; export default function PrivateRoute() { + useServerSentEvents(); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const navigate = useNavigate(); const [shouldRender, setShouldRender] = useState(false); diff --git a/src/stores/sseStore.ts b/src/stores/sseStore.ts deleted file mode 100644 index 3d52593..0000000 --- a/src/stores/sseStore.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { EventSourcePolyfill } from 'event-source-polyfill'; -import { create } from 'zustand'; - -import useAuthStore from '@/stores/authStore'; // 액세스 토큰을 가져올 Zustand 스토어 - -interface SSEState { - messages: string[]; - connectSSE: () => void; - closeSSE: () => void; -} - -export const useSSEStore = create((set, get) => { - let source: EventSourcePolyfill | null = null; // SSE 인스턴스 저장 - - return { - messages: [], - - connectSSE: () => { - const accessToken = useAuthStore.getState().accessToken; // authStore에서 변수 가져오기 - if (!accessToken) { - console.log('엑세스 토큰이 존재하지 않습니다. 구독 불가'); - return; - } - - console.log('🟢 SSE 구독 시작'); - - // 기존 SSE 연결 종료 - get().closeSSE(); - - // 새로운 SSE 연결 생성 - source = new EventSourcePolyfill(`${import.meta.env.VITE_API_URL}/api/notifications/sub`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - source.onmessage = (event) => { - console.log('SSE 메시지 수신:', event.data); - set((state) => ({ messages: [...state.messages, event.data] })); // 메시지 전역 저장 - }; - - source.onerror = (error) => { - console.log('SSE 오류 발생:', error); - get().closeSSE(); - setTimeout(() => get().connectSSE(), 5000); // 5초 후 재연결 - }; - }, - - // 🔥 SSE 종료 함수 (로그아웃 시 실행) - closeSSE: () => { - console.log('SSE 수동 종료'); - source?.close(); - source = null; - set({ messages: [] }); // 상태 초기화 - }, - }; -}); From 2ff73f65f95c38d800595fcf374a749e4ecbb4bd Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Sun, 9 Mar 2025 12:53:55 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=201=EC=B0=A8=20=EA=B5=AC=ED=98=84(=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EA=B8=B0=EB=8A=A5=20=EC=95=8C=EB=A6=BC=EB=8F=84?= =?UTF-8?q?=EC=B0=A9,=20=ED=8E=B8=EC=A7=80=EC=9E=91=EC=84=B1=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EB=AF=B8=EC=9E=85=EB=A0=A5=EC=8B=9C=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=84=A3=EC=96=B4=EB=91=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/write.ts | 15 +-------------- src/components/Toast.tsx | 14 +++++++++++--- src/hooks/useServerSentEvents.tsx | 6 +++++- src/layouts/MobileLayout.tsx | 2 -- src/layouts/PrivateRoute.tsx | 8 +++++++- src/pages/Write/LetterEditor.tsx | 14 +++++++++++--- src/stores/toastStore.ts | 14 +++++++++++--- src/styles/animations.css | 17 +++++++++++++++++ src/types/write.d.ts | 6 ------ 9 files changed, 63 insertions(+), 33 deletions(-) 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 index 3135a8c..a531b6c 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -15,17 +15,25 @@ export default function Toast({}: Toast) { Info: { style: 'bg-[#FFFFFF]' }, }; + const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; const toastStyle = twMerge( - 'fixed top-10 left-1/2 z-40 flex h-[50px] min-w-[335px] w-[85%] -translate-1/2 items-center justify-center rounded-2xl caption-sb', + 'fixed bottom-20 left-1/2 z-40 flex h-[40px] min-w-[335px] w-[85%] -translate-1/2 items-center justify-center rounded-2xl caption-sb', TOAST_DESIGN[toastObj.toastType].style, ); const activeTime = toastObj.time * 1000; useEffect(() => { - setTimeout(() => { + const closeToast = setTimeout(() => { setToastUnActive(); }, activeTime); + + return () => clearTimeout(closeToast); }); + if (!isActive) return null; - return
{toastObj.children}
; + return ( +
setToastUnActive()}> + {toastObj.content} +
+ ); } diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index e18216e..c750071 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -2,11 +2,14 @@ import { EventSourcePolyfill } from 'event-source-polyfill'; import { useEffect, useRef } from 'react'; import useAuthStore from '@/stores/authStore'; +import useToastStore from '@/stores/toastStore'; export const useServerSentEvents = () => { const accessToken = useAuthStore((state) => state.accessToken); const sourceRef = useRef(null); + const setToastActive = useToastStore((state) => state.setToastActive); + useEffect(() => { if (!accessToken) { console.log('로그인 정보 확인불가'); @@ -27,13 +30,14 @@ export const useServerSentEvents = () => { sourceRef.current.onmessage = (event) => { console.log(event); + setToastActive({ toastType: 'Success', content: '새 알림이 도착했어요!' }); console.log('알림 전송'); }; sourceRef.current.onerror = (error) => { console.log(error); console.log('에러 발생함'); - sourceRef.current?.close(); + closeSSE(); // 재연결 로직 추가 가능 setTimeout(connectSSE, 5000); // 5초 후 재연결 시도 }; diff --git a/src/layouts/MobileLayout.tsx b/src/layouts/MobileLayout.tsx index b8b1e7d..3229467 100644 --- a/src/layouts/MobileLayout.tsx +++ b/src/layouts/MobileLayout.tsx @@ -1,4 +1,3 @@ -import Toast from '@/components/Toast'; import { Outlet } from 'react-router'; const MobileLayout = () => { @@ -7,7 +6,6 @@ const MobileLayout = () => {
-
); }; diff --git a/src/layouts/PrivateRoute.tsx b/src/layouts/PrivateRoute.tsx index 7ae4ea9..a607485 100644 --- a/src/layouts/PrivateRoute.tsx +++ b/src/layouts/PrivateRoute.tsx @@ -3,6 +3,7 @@ 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(); @@ -22,5 +23,10 @@ export default function PrivateRoute() { return null; } - return ; + return ( + <> + + + + ); } diff --git a/src/pages/Write/LetterEditor.tsx b/src/pages/Write/LetterEditor.tsx index c22f206..85d0a82 100644 --- a/src/pages/Write/LetterEditor.tsx +++ b/src/pages/Write/LetterEditor.tsx @@ -10,6 +10,7 @@ import { FONT_TYPE_OBJ } from '@/pages/Write/constants'; import OptionSlide from '@/pages/Write/OptionSlide'; import useWrite from '@/stores/writeStore'; import { removeProperty } from '@/utils/removeProperty'; +import useToastStore from '@/stores/toastStore'; export default function LetterEditor({ letterId, @@ -32,6 +33,8 @@ export default function LetterEditor({ const letterRequest = useWrite((state) => state.letterRequest); const setLetterRequest = useWrite((state) => state.setLetterRequest); + const setToastActive = useToastStore((state) => state.setToastActive); + const handlePostFirstReply = async (firstReplyRequest: Omit) => { const res = await postFirstReply(firstReplyRequest); if (res?.status === 200) { @@ -42,7 +45,6 @@ export default function LetterEditor({ } }; - // MEMO : 답장 전송 matchingId가 undefined로 나오는데 뭐 때문인지 내일 찾아보자 ㅎ const handlePostReply = async (letterRequest: LetterRequest) => { const res = await postLetter(letterRequest); if (res?.status === 200) { @@ -127,7 +129,10 @@ export default function LetterEditor({ handlePostReply(letterRequest); } } else { - alert('편지 제목, 내용이 작성되었는지 확인해주세요'); + setToastActive({ + toastType: 'Warning', + content: '편지 제목, 내용이 작성되었는지 확인해주세요', + }); } }} /> @@ -139,7 +144,10 @@ export default function LetterEditor({ if (letterRequest.title.trim() !== '' && letterRequest.content.trim() !== '') { setStep('category'); } else { - alert('편지 제목, 내용이 작성되었는지 확인해주세요'); + setToastActive({ + toastType: 'Warning', + content: '편지 제목, 내용이 작성되었는지 확인해주세요', + }); } }} /> diff --git a/src/stores/toastStore.ts b/src/stores/toastStore.ts index 768e28b..d0e856c 100644 --- a/src/stores/toastStore.ts +++ b/src/stores/toastStore.ts @@ -4,7 +4,8 @@ import { create } from 'zustand'; interface ToastObj { time: number; toastType: 'Warning' | 'Success' | 'Error' | 'Info'; - children: ReactNode; + content: ReactNode; + onClick?: () => void; } interface ToastStore { @@ -16,9 +17,10 @@ interface ToastStore { const useToastStore = create((set) => ({ isActive: false, toastObj: { - time: 1, + time: 3, toastType: 'Info', - children: '', + content: '', + onClick: () => {}, }, setToastActive: (prompt) => set((state) => ({ @@ -28,6 +30,12 @@ const useToastStore = create((set) => ({ setToastUnActive: () => { set(() => ({ isActive: false, + toastObj: { + time: 2, + toastType: 'Info', + content: '', + onClick: () => {}, + }, })); }, })); diff --git a/src/styles/animations.css b/src/styles/animations.css index 702eca7..293b084 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -168,4 +168,21 @@ opacity: 0; } } + + /* Toast */ + /* show and hide */ + @keyframes toast-blink { + 0% { + opacity: 0; + } + 20% { + opacity: 1; + } + 80% { + opacity: 1; + } + 100% { + opacity: 0; + } + } } diff --git a/src/types/write.d.ts b/src/types/write.d.ts index ebcaded..4e608b9 100644 --- a/src/types/write.d.ts +++ b/src/types/write.d.ts @@ -72,9 +72,3 @@ interface FirstReplyRequest { paperType: PaperType; fontType: FontType; } - -// 기존 설정 타입들 -// type Stamp = '위로와 공감' | '축하와 응원' | '고민 상담' | '기타' | '답변자'; -// type Option = '편지지' | '글꼴' | '이전 편지 내용' | null; -// type Step = 'edit' | 'category'; -// type Theme = '기본' | '축하' | '위로' | '빈티지' | '들판'; From b99a781b3bfdb6822c1a6c1915cee2e1118a4d88 Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Sun, 9 Mar 2025 13:04:16 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=B5=9C=EB=8C=80=EB=84=93=EC=9D=B4=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index a531b6c..c0201cb 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -17,7 +17,7 @@ export default function Toast({}: Toast) { 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] min-w-[335px] w-[85%] -translate-1/2 items-center justify-center rounded-2xl caption-sb', + '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_DESIGN[toastObj.toastType].style, ); From ce6b37bfeaefa8b097db9c0d91db9e62f8b7af4c Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Sun, 9 Mar 2025 13:19:21 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BB=A8=ED=85=90=EC=B8=A0=20=ED=83=80=EC=9E=85=EB=B3=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index c0201cb..23a8be0 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -9,10 +9,10 @@ export default function Toast({}: Toast) { const setToastUnActive = useToastStore((state) => state.setToastUnActive); const TOAST_DESIGN = { - Warning: { style: 'bg-primary-3' }, - Success: { style: 'bg-[#38d9a9] text-[#FFFFFF]' }, - Error: { style: 'bg-[#FFDCD8] text-[#FF0000]' }, - Info: { style: 'bg-[#FFFFFF]' }, + 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 animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; @@ -33,7 +33,7 @@ export default function Toast({}: Toast) { if (!isActive) return null; return (
setToastUnActive()}> - {toastObj.content} + {`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.content} ${TOAST_DESIGN[toastObj.toastType].imoji}`}
); } From 0d39b7b04b19944b4d10a7dcfb45f41a8e2f935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Sun, 9 Mar 2025 16:58:22 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8U?= =?UTF-8?q?I=20=EC=95=8C=EB=A6=BC=201=EA=B0=9C=EB=A7=8C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=3D>=20=EC=95=8C=EB=A6=BC=201=EA=B0=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=83=81=20=ED=91=9C=EC=8B=9C=20=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C(=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=9D=EC=B2=B4=20->=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=EB=A1=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast.tsx | 39 +++++----------------- src/components/ToastItem.tsx | 54 +++++++++++++++++++++++++++++++ src/hooks/useServerSentEvents.tsx | 13 ++++++-- src/pages/Write/LetterEditor.tsx | 4 +-- src/stores/toastStore.ts | 47 ++++++++++++++------------- 5 files changed, 99 insertions(+), 58 deletions(-) create mode 100644 src/components/ToastItem.tsx diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 23a8be0..c09223c 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,39 +1,16 @@ import useToastStore from '@/stores/toastStore'; -import { useEffect } from 'react'; -import { twMerge } from 'tailwind-merge'; +import ToastItem from './ToastItem'; interface Toast {} export default function Toast({}: Toast) { - const isActive = useToastStore((state) => state.isActive); - const toastObj = useToastStore((state) => state.toastObj); - const setToastUnActive = useToastStore((state) => state.setToastUnActive); + const toastObjects = useToastStore((state) => state.toastObjects); - 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 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_DESIGN[toastObj.toastType].style, - ); - - const activeTime = toastObj.time * 1000; - useEffect(() => { - const closeToast = setTimeout(() => { - setToastUnActive(); - }, activeTime); - - return () => clearTimeout(closeToast); - }); - - if (!isActive) return null; + if (toastObjects.length <= 0) return; return ( -
setToastUnActive()}> - {`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.content} ${TOAST_DESIGN[toastObj.toastType].imoji}`} -
+ <> + {toastObjects.map((toastObj, index) => ( + + ))} + ); } diff --git a/src/components/ToastItem.tsx b/src/components/ToastItem.tsx new file mode 100644 index 0000000..bb02711 --- /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 c750071..5927614 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -3,8 +3,11 @@ import { useEffect, useRef } from 'react'; import useAuthStore from '@/stores/authStore'; import useToastStore from '@/stores/toastStore'; +import { useNavigate } from 'react-router'; export const useServerSentEvents = () => { + const navigate = useNavigate(); + const accessToken = useAuthStore((state) => state.accessToken); const sourceRef = useRef(null); @@ -30,8 +33,14 @@ export const useServerSentEvents = () => { sourceRef.current.onmessage = (event) => { console.log(event); - setToastActive({ toastType: 'Success', content: '새 알림이 도착했어요!' }); - console.log('알림 전송'); + console.log('알림 수신'); + setToastActive({ + toastType: 'Info', + title: '새 알림이 도착했어요!', + position: 'TOP', + time: 5, + onClick: () => navigate('/mypage/notifications'), + }); }; sourceRef.current.onerror = (error) => { diff --git a/src/pages/Write/LetterEditor.tsx b/src/pages/Write/LetterEditor.tsx index 85d0a82..fd4cfb0 100644 --- a/src/pages/Write/LetterEditor.tsx +++ b/src/pages/Write/LetterEditor.tsx @@ -131,7 +131,7 @@ export default function LetterEditor({ } else { setToastActive({ toastType: 'Warning', - content: '편지 제목, 내용이 작성되었는지 확인해주세요', + title: '편지 제목, 내용이 작성되었는지 확인해주세요', }); } }} @@ -146,7 +146,7 @@ export default function LetterEditor({ } else { setToastActive({ toastType: 'Warning', - content: '편지 제목, 내용이 작성되었는지 확인해주세요', + title: '편지 제목, 내용이 작성되었는지 확인해주세요', }); } }} diff --git a/src/stores/toastStore.ts b/src/stores/toastStore.ts index d0e856c..5e1850c 100644 --- a/src/stores/toastStore.ts +++ b/src/stores/toastStore.ts @@ -1,41 +1,42 @@ -import { ReactNode } from 'react'; import { create } from 'zustand'; interface ToastObj { time: number; toastType: 'Warning' | 'Success' | 'Error' | 'Info'; - content: ReactNode; + position: 'TOP' | 'BOTTOM'; + title: string; onClick?: () => void; } interface ToastStore { - isActive: boolean; - toastObj: ToastObj; + toastObjects: ToastObj[] | []; setToastActive: (prompt: Partial) => void; - setToastUnActive: () => void; + setToastUnActive: (idx: number) => void; } + +// 토스트 기본형 +const toastObjFormat: ToastObj = { + time: 2, + toastType: 'Info', + position: 'BOTTOM', + title: '', + onClick: () => {}, +}; + const useToastStore = create((set) => ({ - isActive: false, - toastObj: { - time: 3, - toastType: 'Info', - content: '', - onClick: () => {}, - }, + toastObjects: [], setToastActive: (prompt) => set((state) => ({ - isActive: true, - toastObj: { ...state.toastObj, ...prompt }, + toastObjects: [...state.toastObjects, { ...toastObjFormat, ...prompt }], })), - setToastUnActive: () => { - set(() => ({ - isActive: false, - toastObj: { - time: 2, - toastType: 'Info', - content: '', - onClick: () => {}, - }, + setToastUnActive: (idx) => { + set((state) => ({ + toastObjects: state.toastObjects.filter((target, currentIdx) => { + if (currentIdx === idx) { + return null; + } + return target; + }), })); }, })); From bf389cf4582d7dbbd670cffe057b216d2f34db02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Sun, 9 Mar 2025 17:14:11 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8U?= =?UTF-8?q?I=20position=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ToastItem.tsx | 6 +++--- src/hooks/useServerSentEvents.tsx | 2 +- src/stores/toastStore.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ToastItem.tsx b/src/components/ToastItem.tsx index bb02711..d4b917e 100644 --- a/src/components/ToastItem.tsx +++ b/src/components/ToastItem.tsx @@ -5,7 +5,7 @@ import { twMerge } from 'tailwind-merge'; interface ToastObj { time: number; toastType: 'Warning' | 'Success' | 'Error' | 'Info'; - position: 'TOP' | 'BOTTOM'; + position: 'Top' | 'Bottom'; title: string; onClick?: () => void; } @@ -20,8 +20,8 @@ export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; ind }; const TOAST_POSITION = { - TOP: 'top-20', - BOTTOM: 'bottom-20', + Top: 'top-20', + Bottom: 'bottom-20', }; const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index 5927614..1ad9850 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -37,7 +37,7 @@ export const useServerSentEvents = () => { setToastActive({ toastType: 'Info', title: '새 알림이 도착했어요!', - position: 'TOP', + position: 'Top', time: 5, onClick: () => navigate('/mypage/notifications'), }); diff --git a/src/stores/toastStore.ts b/src/stores/toastStore.ts index 5e1850c..00f2abf 100644 --- a/src/stores/toastStore.ts +++ b/src/stores/toastStore.ts @@ -3,7 +3,7 @@ import { create } from 'zustand'; interface ToastObj { time: number; toastType: 'Warning' | 'Success' | 'Error' | 'Info'; - position: 'TOP' | 'BOTTOM'; + position: 'Top' | 'Bottom'; title: string; onClick?: () => void; } @@ -18,7 +18,7 @@ interface ToastStore { const toastObjFormat: ToastObj = { time: 2, toastType: 'Info', - position: 'BOTTOM', + position: 'Bottom', title: '', onClick: () => {}, };