From 489a925b5cf3f1102d17536f6feae73caca8fe89 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EB=82=A8=EC=A0=95=EC=9A=B1?=
<113816822+HelloWook@users.noreply.github.com>
Date: Thu, 31 Oct 2024 16:04:01 +0900
Subject: [PATCH] =?UTF-8?q?64=20feat=20toast=20=EC=BB=B4=ED=8F=AC=EB=84=8C?=
=?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EA=B5=AC=ED=98=84=20(#88)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat : Toast 컴포넌트 구현
* feat : Toast 컨테이너 구현
* feat : 전역적으로 토스트 관리
---
src/app/App.tsx | 2 +
src/features/Toast/hooks/useToastStore.ts | 39 +++++++++++
src/features/Toast/model/type.ts | 5 ++
src/features/Toast/ui/Toast.stories.ts | 25 ++++++++
src/features/Toast/ui/Toast.styeld.ts | 64 +++++++++++++++++++
src/features/Toast/ui/Toast.tsx | 48 ++++++++++++++
src/features/Toast/ui/ToastContainer.tsx | 27 ++++++++
.../Toast/ui/toastContainer.styled.tsx | 8 +++
src/shared/ui/Modal/Modal.styled.ts | 19 +++++-
9 files changed, 235 insertions(+), 2 deletions(-)
create mode 100644 src/features/Toast/hooks/useToastStore.ts
create mode 100644 src/features/Toast/model/type.ts
create mode 100644 src/features/Toast/ui/Toast.stories.ts
create mode 100644 src/features/Toast/ui/Toast.styeld.ts
create mode 100644 src/features/Toast/ui/Toast.tsx
create mode 100644 src/features/Toast/ui/ToastContainer.tsx
create mode 100644 src/features/Toast/ui/toastContainer.styled.tsx
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 865deab..10bead7 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import GlobalStyles from './styles/globalStyles';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import ToastContainer from '@/features/Toast/ui/ToastContainer';
const queryClient = new QueryClient();
@@ -8,6 +9,7 @@ const App: React.FC = () => {
return (
+
);
};
diff --git a/src/features/Toast/hooks/useToastStore.ts b/src/features/Toast/hooks/useToastStore.ts
new file mode 100644
index 0000000..186e85c
--- /dev/null
+++ b/src/features/Toast/hooks/useToastStore.ts
@@ -0,0 +1,39 @@
+import { create } from 'zustand';
+import { Toast } from '../model/type';
+
+interface ToastStore {
+ toasts: Toast[];
+ addToast: (
+ message: string,
+ variant: 'success' | 'warning' | 'error'
+ ) => void;
+ removeToast: (id: number) => void;
+}
+
+/**
+ * addToasdt새로운 Toast 메시지를 추가하고 2초 후 자동 제거
+ * 호출 시
+ * const { addToast } = useToastStore(); 불러와서
+ * () => addToast('Success message!', 'success') 형식으로 사용해주세요
+ * @param message - 표시할 메시지 내용
+ * @param variant - 메시지 타입 ('success' | 'warning' | 'error')
+ */
+export const useToastStore = create((set) => ({
+ toasts: [],
+ addToast: (message, variant) => {
+ const id = Date.now();
+ set((state) => ({
+ toasts: [...state.toasts, { id, message, variant }]
+ }));
+
+ setTimeout(() => {
+ set((state) => ({
+ toasts: state.toasts.filter((toast) => toast.id !== id)
+ }));
+ }, 2000);
+ },
+ removeToast: (id) =>
+ set((state) => ({
+ toasts: state.toasts.filter((toast) => toast.id !== id)
+ }))
+}));
diff --git a/src/features/Toast/model/type.ts b/src/features/Toast/model/type.ts
new file mode 100644
index 0000000..56db3b4
--- /dev/null
+++ b/src/features/Toast/model/type.ts
@@ -0,0 +1,5 @@
+export interface Toast {
+ id: number;
+ variant: 'success' | 'warning' | 'error';
+ message: string;
+}
diff --git a/src/features/Toast/ui/Toast.stories.ts b/src/features/Toast/ui/Toast.stories.ts
new file mode 100644
index 0000000..0b510f0
--- /dev/null
+++ b/src/features/Toast/ui/Toast.stories.ts
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Toast from './Toast';
+
+const meta: Meta = {
+ component: Toast,
+ title: 'features/UI/Toast',
+ tags: ['autodocs'],
+ argTypes: {},
+};
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {children :"화이팅"},
+};
+
+export const Waring: Story = {
+ args: {children :"화이팅",variant: "warning"},
+ };
+
+ export const Error: Story = {
+ args: {children :"화이팅", variant : 'error'},
+ };
\ No newline at end of file
diff --git a/src/features/Toast/ui/Toast.styeld.ts b/src/features/Toast/ui/Toast.styeld.ts
new file mode 100644
index 0000000..23a0e92
--- /dev/null
+++ b/src/features/Toast/ui/Toast.styeld.ts
@@ -0,0 +1,64 @@
+import React from 'react';
+import styled, { keyframes } from 'styled-components';
+
+interface ToastStyledProps {
+ variant: 'success' | 'warning' | 'error';
+}
+
+const slideIn = keyframes`
+ 0% {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ 10% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ 90% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+`;
+
+export const ToastStyled = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ffffff;
+ width: 514.05px;
+ height: 77px;
+ padding: 0 20px;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
+ border-radius: 6px;
+ font-family: 'Lato', sans-serif;
+ font-weight: 400;
+ font-size: 22px;
+ letter-spacing: 0.7125px;
+ position: relative;
+ margin-bottom: 10px;
+ background-color: ${({ variant }) => {
+ switch (variant) {
+ case 'success':
+ return '#3BDE86';
+ case 'warning':
+ return '#FB4242';
+ case 'error':
+ return '#FED046';
+ default:
+ return '#ffffff';
+ }
+ }};
+ animation: ${slideIn} 2s ease forwards;
+`;
+export const ToastMessageStlyed = styled.span`
+ text-align: center;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ width: 350px;
+`;
diff --git a/src/features/Toast/ui/Toast.tsx b/src/features/Toast/ui/Toast.tsx
new file mode 100644
index 0000000..d245b4b
--- /dev/null
+++ b/src/features/Toast/ui/Toast.tsx
@@ -0,0 +1,48 @@
+// components/Toast.tsx
+import React from 'react';
+import { ToastStyled, ToastMessageStlyed } from './Toast.styeld';
+import { IoIosWarning, IoMdClose } from 'react-icons/io';
+import { FaCheck } from 'react-icons/fa6';
+import { MdOutlineError } from 'react-icons/md';
+
+interface ToastProps {
+ children: React.ReactNode;
+ variant: 'success' | 'warning' | 'error';
+ onClose: () => void;
+}
+
+const IconStyle = {
+ width: 35,
+ height: 35,
+ position: 'absolute' as const,
+ left: 30,
+ top: 20
+};
+
+const CloseStlye = {
+ width: 35,
+ height: 35,
+ position: 'absolute' as const,
+ right: 30,
+ top: 20
+};
+
+const iconComponents = {
+ success: ,
+ warning: ,
+ error:
+};
+
+const Toast = ({ children, variant = 'success', onClose }: ToastProps) => {
+ const Icon = iconComponents[variant];
+
+ return (
+
+ {Icon}
+ {children}
+
+
+ );
+};
+
+export default Toast;
diff --git a/src/features/Toast/ui/ToastContainer.tsx b/src/features/Toast/ui/ToastContainer.tsx
new file mode 100644
index 0000000..6f98d08
--- /dev/null
+++ b/src/features/Toast/ui/ToastContainer.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { useToastStore } from '../hooks/useToastStore';
+import Toast from './Toast';
+import { ToastContainerStyled } from './toastContainer.styled';
+
+const ToastContainer = () => {
+ const { toasts, removeToast } = useToastStore();
+
+ return (
+
+ {toasts
+ .slice()
+ .reverse()
+ .map((toast) => (
+ removeToast(toast.id)}
+ >
+ {toast.message}
+
+ ))}
+
+ );
+};
+
+export default ToastContainer;
diff --git a/src/features/Toast/ui/toastContainer.styled.tsx b/src/features/Toast/ui/toastContainer.styled.tsx
new file mode 100644
index 0000000..2a9ffd9
--- /dev/null
+++ b/src/features/Toast/ui/toastContainer.styled.tsx
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const ToastContainerStyled = styled.div`
+ position: fixed;
+ margin-bottom: 20px;
+ top: 20px;
+ right: 20px;
+`;
diff --git a/src/shared/ui/Modal/Modal.styled.ts b/src/shared/ui/Modal/Modal.styled.ts
index 5c6baa5..77a5662 100644
--- a/src/shared/ui/Modal/Modal.styled.ts
+++ b/src/shared/ui/Modal/Modal.styled.ts
@@ -1,22 +1,37 @@
-import styled from 'styled-components';
+import styled, { keyframes } from 'styled-components';
+
+const fadeIn = keyframes`
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+`;
export const ModalStyled = styled.div`
background: white;
width: 370px;
height: 200px;
border-radius: 20px;
- box-shadow: inset;
padding: 8px;
position: relative;
+ box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
+ animation: ${fadeIn} 2 ease forwards;
+
h1 {
margin-bottom: 30px;
}
+
svg {
font-size: 30px;
position: absolute;
top: 18px;
right: 20px;
color: #838383;
+ cursor: pointer;
}
`;