Skip to content

Commit 0d39b7b

Browse files
committed
refactor : 토스트UI 알림 1개만 표시 => 알림 1개 이상 표시 되도록 업그레이드(단일 객체 -> 객체 배열로 데이터값 수정)
1 parent ce6b37b commit 0d39b7b

File tree

5 files changed

+99
-58
lines changed

5 files changed

+99
-58
lines changed

src/components/Toast.tsx

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,16 @@
11
import useToastStore from '@/stores/toastStore';
2-
import { useEffect } from 'react';
3-
import { twMerge } from 'tailwind-merge';
2+
import ToastItem from './ToastItem';
43

54
interface Toast {}
65
export default function Toast({}: Toast) {
7-
const isActive = useToastStore((state) => state.isActive);
8-
const toastObj = useToastStore((state) => state.toastObj);
9-
const setToastUnActive = useToastStore((state) => state.setToastUnActive);
6+
const toastObjects = useToastStore((state) => state.toastObjects);
107

11-
const TOAST_DESIGN = {
12-
Warning: { style: 'bg-primary-4', imoji: '⚠️' },
13-
Success: { style: 'bg-[#38d9a9] text-[#FFFFFF]', imoji: '✅' },
14-
Error: { style: 'bg-[#FFDCD8] text-[#FF0000]', imoji: '🚨' },
15-
Info: { style: 'bg-[#FFFFFF]', imoji: 'ℹ️' },
16-
};
17-
18-
const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`;
19-
const toastStyle = twMerge(
20-
'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',
21-
TOAST_DESIGN[toastObj.toastType].style,
22-
);
23-
24-
const activeTime = toastObj.time * 1000;
25-
useEffect(() => {
26-
const closeToast = setTimeout(() => {
27-
setToastUnActive();
28-
}, activeTime);
29-
30-
return () => clearTimeout(closeToast);
31-
});
32-
33-
if (!isActive) return null;
8+
if (toastObjects.length <= 0) return;
349
return (
35-
<div className={toastStyle} style={{ animation: animation }} onClick={() => setToastUnActive()}>
36-
{`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.content} ${TOAST_DESIGN[toastObj.toastType].imoji}`}
37-
</div>
10+
<>
11+
{toastObjects.map((toastObj, index) => (
12+
<ToastItem toastObj={toastObj} index={index} key={index} />
13+
))}
14+
</>
3815
);
3916
}

src/components/ToastItem.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import useToastStore from '@/stores/toastStore';
2+
import { useEffect } from 'react';
3+
import { twMerge } from 'tailwind-merge';
4+
5+
interface ToastObj {
6+
time: number;
7+
toastType: 'Warning' | 'Success' | 'Error' | 'Info';
8+
position: 'TOP' | 'BOTTOM';
9+
title: string;
10+
onClick?: () => void;
11+
}
12+
export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; index: number }) {
13+
const setToastUnActive = useToastStore((state) => state.setToastUnActive);
14+
15+
const TOAST_DESIGN = {
16+
Warning: { style: 'bg-primary-4', imoji: '⚠️' },
17+
Success: { style: 'bg-[#38d9a9] text-[#FFFFFF]', imoji: '✅' },
18+
Error: { style: 'bg-[#FFDCD8] text-[#FF0000]', imoji: '🚨' },
19+
Info: { style: 'bg-[#FFFFFF]', imoji: '📫' },
20+
};
21+
22+
const TOAST_POSITION = {
23+
TOP: 'top-20',
24+
BOTTOM: 'bottom-20',
25+
};
26+
27+
const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`;
28+
const toastStyle = twMerge(
29+
'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',
30+
TOAST_POSITION[toastObj.position],
31+
TOAST_DESIGN[toastObj.toastType].style,
32+
);
33+
34+
const activeTime = toastObj.time * 1000;
35+
useEffect(() => {
36+
const closeToast = setTimeout(() => {
37+
setToastUnActive(index);
38+
}, activeTime);
39+
40+
return () => clearTimeout(closeToast);
41+
});
42+
return (
43+
<div
44+
className={toastStyle}
45+
style={{ animation: animation }}
46+
onClick={() => {
47+
setToastUnActive(index);
48+
if (toastObj.onClick) toastObj.onClick();
49+
}}
50+
>
51+
{`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.title} ${TOAST_DESIGN[toastObj.toastType].imoji}`}
52+
</div>
53+
);
54+
}

src/hooks/useServerSentEvents.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { useEffect, useRef } from 'react';
33

44
import useAuthStore from '@/stores/authStore';
55
import useToastStore from '@/stores/toastStore';
6+
import { useNavigate } from 'react-router';
67

78
export const useServerSentEvents = () => {
9+
const navigate = useNavigate();
10+
811
const accessToken = useAuthStore((state) => state.accessToken);
912
const sourceRef = useRef<EventSourcePolyfill | null>(null);
1013

@@ -30,8 +33,14 @@ export const useServerSentEvents = () => {
3033

3134
sourceRef.current.onmessage = (event) => {
3235
console.log(event);
33-
setToastActive({ toastType: 'Success', content: '새 알림이 도착했어요!' });
34-
console.log('알림 전송');
36+
console.log('알림 수신');
37+
setToastActive({
38+
toastType: 'Info',
39+
title: '새 알림이 도착했어요!',
40+
position: 'TOP',
41+
time: 5,
42+
onClick: () => navigate('/mypage/notifications'),
43+
});
3544
};
3645

3746
sourceRef.current.onerror = (error) => {

src/pages/Write/LetterEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export default function LetterEditor({
131131
} else {
132132
setToastActive({
133133
toastType: 'Warning',
134-
content: '편지 제목, 내용이 작성되었는지 확인해주세요',
134+
title: '편지 제목, 내용이 작성되었는지 확인해주세요',
135135
});
136136
}
137137
}}
@@ -146,7 +146,7 @@ export default function LetterEditor({
146146
} else {
147147
setToastActive({
148148
toastType: 'Warning',
149-
content: '편지 제목, 내용이 작성되었는지 확인해주세요',
149+
title: '편지 제목, 내용이 작성되었는지 확인해주세요',
150150
});
151151
}
152152
}}

src/stores/toastStore.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,42 @@
1-
import { ReactNode } from 'react';
21
import { create } from 'zustand';
32

43
interface ToastObj {
54
time: number;
65
toastType: 'Warning' | 'Success' | 'Error' | 'Info';
7-
content: ReactNode;
6+
position: 'TOP' | 'BOTTOM';
7+
title: string;
88
onClick?: () => void;
99
}
1010

1111
interface ToastStore {
12-
isActive: boolean;
13-
toastObj: ToastObj;
12+
toastObjects: ToastObj[] | [];
1413
setToastActive: (prompt: Partial<ToastObj>) => void;
15-
setToastUnActive: () => void;
14+
setToastUnActive: (idx: number) => void;
1615
}
16+
17+
// 토스트 기본형
18+
const toastObjFormat: ToastObj = {
19+
time: 2,
20+
toastType: 'Info',
21+
position: 'BOTTOM',
22+
title: '',
23+
onClick: () => {},
24+
};
25+
1726
const useToastStore = create<ToastStore>((set) => ({
18-
isActive: false,
19-
toastObj: {
20-
time: 3,
21-
toastType: 'Info',
22-
content: '',
23-
onClick: () => {},
24-
},
27+
toastObjects: [],
2528
setToastActive: (prompt) =>
2629
set((state) => ({
27-
isActive: true,
28-
toastObj: { ...state.toastObj, ...prompt },
30+
toastObjects: [...state.toastObjects, { ...toastObjFormat, ...prompt }],
2931
})),
30-
setToastUnActive: () => {
31-
set(() => ({
32-
isActive: false,
33-
toastObj: {
34-
time: 2,
35-
toastType: 'Info',
36-
content: '',
37-
onClick: () => {},
38-
},
32+
setToastUnActive: (idx) => {
33+
set((state) => ({
34+
toastObjects: state.toastObjects.filter((target, currentIdx) => {
35+
if (currentIdx === idx) {
36+
return null;
37+
}
38+
return target;
39+
}),
3940
}));
4041
},
4142
}));

0 commit comments

Comments
 (0)