Repository: Recruiting-Your-Club/ryc
Pull request #122: [FE] Toast 컴포넌트 구현
Author: login: github-actions[bot]
Labels: name: ⭐️ feature
description: 기능 이슈
name: 🚀FE
description: 프론트 이슈
Created: 2025-03-11T07:25:44Z | Merged: 2025-03-27T06:13:40Z

## 📌 관련 이슈
closes #121

## 🛠️ 작업 내용
- [x] 토스트 컴포넌트 구현
- [x] 스토리북 작성

## 🎯 리뷰 포인트

## Toast 설계 과정
사용자에게 상태를 알려주기 위한 Toast 컴포넌트를 구현했습니다. 컴포넌트 설계 시 다음 기능을 중점적으로 고려했습니다:

1. 지속시간 설정
2. 위치 지정
3. 최대 표시 개수 제한
4. 다양한 타입 지원(성공, 실패 등)
6. autoClose

불필요한 기능은 제외하고 핵심 기능만 구현했으며, UI와 로직을 분리하여 설계했습니다. 어느 페이지에서든 쉽게 접근할 수 있도록 ContextAPI를 활용해 전역 상태로 관리하고, Toast 목록과 로직은 ToastProvider에서, 실제 UI는 컴포넌트 파일에서 구현했습니다.

**주요 구현사항** 
1. 부모 태그의 CSS 영향을 받지 않도록 react-dom의 createPortal을 활용해 root DOM과 분리했습니다.
2. A페이지에서 토스트 발행 후 B페이지로 이동 시 토스트가 없어지는 문제가 생기지 않도록
    ToastContext를 App 컴포넌트에 감싸서 토스트 발행 후 어떤 페이지로 이동하는지 상관없이 유지되도록 구현하였습니다.
3. white, black, colored 세 가지 테마를 지원하며, 사용자가 그 외 커스텀을 원할 시 직접 커스텀 DOM을 주입할 수도 있습니다.
4. UI와 렌더링 로직을 분리하여 렌더링 로직은 provider에, UI 관련 로직은 각 toast와 container 컴포넌트에 구현했습니다.
5. 6개의 위치별 toastContainer를 생성하여 각 위치에 맞는 toast를 스택으로 쌓아 렌더링하도록 했습니다.
6. 발행된 toast가 언제 사라질지 시각적으로 보여줄 수 있도록 progressBar도 함께 구현하였습니다.

**개선 필요사항**
1. 현재 프로젝트에서 발행할 수 있는 최대 toast수를 container를 통해서 제어를 할 수 있는데, 현재 방식은 초기 프로젝트 렌더링 시 설정되는 방식이 아닌, toast를 발행할 때 container에 최대 발행 수를 직접 입력해줘야 그 이후에 적용이되기 때문에 직접 제어하기 전까진 3개의 토스트로 고정이 되고있습니다. 이는 후에 방법을 찾고 리팩토링을 하는 방향으로 가야할 것 같습니다.
-> 현재는 전역에서 토스트 개수 제한할 수 있도록 수정

2. 처음 toast를 설계했을 땐 toast 발행 후 사용자가 임의로 선택한 toast를 x아이콘을 통해 지울 수 있는 것을 고려하였지만,
현재로서 중요도가 떨어지고 시간이 많이 소모되는 것으로 판단하여 프로젝트 진행 후 기능을 추가할 수 있도록 하겠습니다.
후에 id값을 지정해서 발행된 토스트를 지워야 하기 때문에 id가 toast의 props로 들어가있고, autoClose는 false일 경우 사용자가 닫을 수 없기 때문에 true로 고정값을 정해두었습니다.
 
 
![image](https://github.com/user-attachments/assets/14bfb5e6-93fe-40c6-82ce-2ce39303f9e7)

https://github.com/user-attachments/assets/ed3df0d3-eb64-4a4c-bdb4-6db91c76edb1





## ⏳ 작업 시간
추정 시간:  50시간
실제 시간:  100시간

**오래 걸린 이유..**

1. 초기 설계에서는 하나의 toastContainer로 모든 toast를 관리했으나, 위치가 다른 toast 발행 시 기존 toast의 위치도 함께 변경되는 문제가 발생했습니다. 이를 해결하기 위해 6개의 위치별 컨테이너를 생성하여 각 위치에 맞는 toast를 독립적으로 관리하도록 수정했습니다.

2. 코드를 작성하면서 어떻게 구현해야 할지 고민하는 시간이 많이 소요됐고, 제 코드가 맘에 들지 않아서 계속 고치면서 코드를 작성하느라 오래걸렸던 것 같습니다. 지금도 제 코드를 보면 provider에 로직이 너무 많이 들어가있는 등 역할을 잘 분리하지 못한다고 판단해서 후에 리팩토링 할 수 있도록 하겠습니다.


참고자료 :
https://www.npmjs.com/package/react-toastify
https://chakra-ui.com/docs/components/toast
https://v4.mui.com/components/snackbars/
https://ant.design/components/message
https://github.com/fkhadra/react-toastify

Linked issue #121: [FE] Toast 컴포넌트 구현
## 🔥 구현 기능
SnackBar 컴포넌트 구현

## 🚧 작업목록
- [x] 각 위치를 고려하여 렌더링 할 수 있도록 컴포넌트 구현

## 🥸 담당자
@aiminghee3 


## 🔗 관련 링크
https://mui.com/material-ui/react-snackbar/

Linked issue comments: user: login: github-actions[bot]
created_at: 2025-03-11T07:25:41Z
body: [FE] SnackBar 컴포넌트 구현 이슈에 대한 feature/issue-121 브랜치가 생성 되었습니다.
author_association: CONTRIBUTOR

Diff:
diff --git a/client/src/assets/images/alert-filled.svg b/client/src/assets/images/alert-filled.svg
new file mode 100644
index 000000000..f17005a70
--- /dev/null
+++ b/client/src/assets/images/alert-filled.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path  d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm0 319.91a20 20 0 1120-20 20 20 0 01-20 20zm21.72-201.15l-5.74 122a16 16 0 01-32 0l-5.74-121.94v-.05a21.74 21.74 0 1143.44 0z"/></svg>
\ No newline at end of file
diff --git a/client/src/assets/images/check-filled.svg b/client/src/assets/images/check-filled.svg
new file mode 100644
index 000000000..482458739
--- /dev/null
+++ b/client/src/assets/images/check-filled.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm108.25 138.29l-134.4 160a16 16 0 01-12 5.71h-.27a16 16 0 01-11.89-5.3l-57.6-64a16 16 0 1123.78-21.4l45.29 50.32 122.59-145.91a16 16 0 0124.5 20.58z"/></svg>
\ No newline at end of file
diff --git a/client/src/components/Toast/Toast.stories.tsx b/client/src/components/Toast/Toast.stories.tsx
new file mode 100644
index 000000000..1aa0f6a50
--- /dev/null
+++ b/client/src/components/Toast/Toast.stories.tsx
@@ -0,0 +1,262 @@
+import React, { useState } from 'react';
+import { Button } from '@components/Button';
+import { Text } from '@components/Text';
+import { Toast } from '.';
+import type { Meta, StoryObj } from '@storybook/react';
+import { ToastProvider } from './ToastProvider';
+import { useToast } from '@hooks/useToast';
+
+type Story = StoryObj<typeof Toast>;
+
+const meta: Meta<typeof Toast> = {
+    title: 'Toast',
+    component: Toast,
+    parameters: {
+        docs: {
+            description: {
+                component: '토스트입니다.',
+            },
+        },
+    },
+    decorators: [
+        (Story) => {
+            return (
+                <ToastProvider>
+                    <div style={{ width: '100%', height: '100%', padding: '100px' }}>
+                        <Story />
+                    </div>
+                </ToastProvider>
+            );
+        },
+    ],
+};
+
+export const CustomDefaultToast: Story = {
+    render: (args) => {
+        // eslint-disable-next-line react-hooks/rules-of-hooks
+        const { toast } = useToast();
+        const handleToast = () => {
+            toast(args.content, {
+                toastTheme: args.toastTheme,
+                type: args.type,
+                duration: args.duration,
+                autoClose: args.autoClose,
+                status: args.status,
+                position: args.position,
+                progressBar: args.progressBar,
+                sx: args.sx,
+            });
+        };
+        return (
+            <Button size="full" variant="primary" onClick={handleToast}>
+                커스텀 해보세여
+            </Button>
+        );
+    },
+    args: {
+        content: '내용물(React Node)',
+        type: 'info',
+        toastTheme: 'black',
+        autoClose: true,
+        duration: 3000,
+        status: 'entering',
+        progressBar: true,
+        sx: {},
+    },
+    argTypes: {
+        content: {
+            control: 'text',
+        },
+        type: {
+            options: ['info', 'success', 'error', 'default'],
+            control: { type: 'radio' },
+        },
+        toastTheme: {
+            options: ['white', 'black', 'colored'],
+            control: { type: 'radio' },
+        },
+        duration: {
+            control: { type: 'number' },
+        },
+        autoClose: {
+            control: { type: 'boolean' },
+        },
+        status: {
+            control: { type: 'text' },
+        },
+        position: {
+            options: [
+                'topRight',
+                'topCenter',
+                'topLeft',
+                'bottomRight',
+                'bottomCenter',
+                'bottomLeft',
+            ],
+            control: { type: 'radio' },
+        },
+        progressBar: {
+            control: { type: 'boolean' },
+        },
+        sx: {
+            control: { type: 'object' },
+        },
+    },
+};
+export const ToastCollection: Story = {
+    render: (args) => {
+        // eslint-disable-next-line react-hooks/rules-of-hooks
+        const { toast } = useToast();
+        return (
+            <>
+                <Text type="h1Bold">Theme : White입니다.</Text>
+                <div style={{ width: '100%', display: 'flex', marginBottom: '30px' }}>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() => toast('Hello world', { toastTheme: 'white' })}
+                    >
+                        기본 Toast 입니다.
+                    </Button>
+
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.info('메일을 성공적으로 전송하였습니다.', {
+                                toastTheme: 'white',
+                                position: 'topLeft',
+                            })
+                        }
+                    >
+                        info 입니다.
+                    </Button>
+
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.success('Hello world', {
+                                toastTheme: 'white',
+                                position: 'topCenter',
+                                duration: 3000,
+                            })
+                        }
+                    >
+                        success입니다.
+                    </Button>
+
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.error('Hello world', {
+                                toastTheme: 'white',
+                                position: 'topRight',
+                                duration: 3000,
+                            })
+                        }
+                    >
+                        error입니다.
+                    </Button>
+                </div>
+
+                <Text type="h1Bold">Theme : black입니다.</Text>
+                <div style={{ width: '100%', display: 'flex', marginBottom: '30px' }}>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() => toast('Hello world', { toastTheme: 'black' })}
+                    >
+                        기본 Toast 입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.info('Hello world', {
+                                toastTheme: 'black',
+                                position: 'topLeft',
+                            })
+                        }
+                    >
+                        info입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.success('Hello world', {
+                                toastTheme: 'black',
+                                position: 'topCenter',
+                            })
+                        }
+                    >
+                        success입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.error('Hello world', {
+                                toastTheme: 'black',
+                                position: 'topRight',
+                            })
+                        }
+                    >
+                        error입니다.
+                    </Button>
+                </div>
+
+                <Text type="h1Bold">Theme : Colored입니다.</Text>
+                <div style={{ width: '100%', display: 'flex', marginBottom: '30px' }}>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() => toast('기본 Toast', { toastTheme: 'colored' })}
+                    >
+                        기본 Toast 입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.info('메일을 성공적으로 전송하였습니다.', {
+                                toastTheme: 'colored',
+                                position: 'topLeft',
+                            })
+                        }
+                    >
+                        info 입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.success('Hello world', {
+                                toastTheme: 'colored',
+                                position: 'topCenter',
+                            })
+                        }
+                    >
+                        success입니다.
+                    </Button>
+                    <Button
+                        size="full"
+                        variant="primary"
+                        onClick={() =>
+                            toast.error('Hello world', {
+                                toastTheme: 'colored',
+                                position: 'topRight',
+                            })
+                        }
+                    >
+                        error입니다.
+                    </Button>
+                </div>
+            </>
+        );
+    },
+};
+
+export default meta;
diff --git a/client/src/components/Toast/Toast.style.tsx b/client/src/components/Toast/Toast.style.tsx
new file mode 100644
index 000000000..b384697fe
--- /dev/null
+++ b/client/src/components/Toast/Toast.style.tsx
@@ -0,0 +1,214 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type, TypeColor } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme) => {
+    switch (toastTheme) {
+        case 'white':
+            return css`
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return css`
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return css`
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return css`
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return css`
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return css`
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return css`
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return css`
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+const ToastType: Record<Type, TypeColor> = {
+    info: {
+        backgroundColor: theme.colors.blue[300],
+        coloredBackgroundColor: theme.colors.blue[200],
+    },
+    success: {
+        backgroundColor: theme.colors.green[200],
+        coloredBackgroundColor: theme.colors.green[100],
+    },
+    error: {
+        backgroundColor: theme.colors.red[800],
+        coloredBackgroundColor: theme.colors.red[600],
+    },
+    default: {
+        backgroundColor: theme.colors.white,
+        coloredBackgroundColor: theme.colors.black,
+    },
+};
+
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        return css`
+            background-color: ${ToastType[type].coloredBackgroundColor};
+        `;
+    }
+    if (toastTheme === 'black' && type === 'default') {
+        return css`
+            background-color: ${ToastType[type].backgroundColor};
+        `;
+    }
+    if (toastTheme === 'white' && type === 'default') {
+        return css`
+            background-color: ${ToastType[type].coloredBackgroundColor};
+        `;
+    }
+    return css`
+        background-color: ${ToastType[type].backgroundColor};
+    `;
+};
+
+const toastFadeIn = keyframes`
+    from {
+        opacity: 0;
+        transform: translateY(2rem);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+`;
+
+const toastFadeOut = keyframes`
+    from {
+        opacity: 1;
+        transform: translateY(0);
+    }
+    to {
+        opacity: 0;
+        transform: translateY(-2rem);
+    }
+`;
+
+const progressBarAnimation = keyframes`
+    0% {
+        width: 100%;
+    }
+    100% {
+        width: 0%;
+    }
+`;
+
+export const Container = css`
+    position: fixed;
+    padding: 0.4rem 0.4rem;
+`;
+
+export const ContainerPosition: Record<ToastPosition, CSSObject> = {
+    topRight: {
+        top: '2rem',
+        right: '2rem',
+    },
+    topCenter: {
+        top: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    topLeft: {
+        top: '2rem',
+        left: '2rem',
+    },
+    bottomRight: {
+        bottom: '2rem',
+        right: '2rem',
+    },
+    bottomCenter: {
+        bottom: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    bottomLeft: {
+        bottom: '2rem',
+        left: '2rem',
+    },
+};
+
+export const toastStyle = (status: string, toastTheme: ToastTheme, type: Type) => css`
+    ${getToastBackgroundStyles(type, toastTheme)}
+    ${theme.typography.bodySemibold}
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: start;
+    padding: 1rem 1rem;
+    gap: 0.5rem;
+    margin-bottom: 1rem;
+    border-radius: 5px;
+    min-width: 30rem;
+    min-height: 5rem;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    animation: ${status === 'entering' ? toastFadeIn : status === 'exiting' ? toastFadeOut : 'none'}
+        1000ms ease forwards;
+`;
+
+export const svgStyle = (type: Type) => css`
+    width: 2rem;
+    height: 2rem;
+    ${svgColor(type)}
+`;
+
+export const progressBarStyle = (toastTheme: ToastTheme, type: Type, duration: number) => css`
+    position: absolute;
+    bottom: 0;
+    left: 0.1rem;
+    border-radius: 0 0 0 7px;
+    height: 0.3rem;
+    width: 100%;
+    ${progressBarColor(toastTheme, type)}
+    animation: ${progressBarAnimation} ${duration}ms linear forwards;
+`;
diff --git a/client/src/components/Toast/Toast.tsx b/client/src/components/Toast/Toast.tsx
new file mode 100644
index 000000000..feeb593e1
--- /dev/null
+++ b/client/src/components/Toast/Toast.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import type { Type, ToastProps, ToastTheme } from './type';
+import { toastStyle, svgStyle, progressBarStyle } from './Toast.style';
+import Alert from '@assets/images/alert.svg';
+import Check from '@assets/images/check.svg';
+import CheckFilled from '@assets/images/check-filled.svg';
+import AlertFilled from '@assets/images/alert-filled.svg';
+
+function Toast({
+    type = 'default',
+    toastTheme = 'white',
+    status = 'entering',
+    duration = 3000,
+    autoClose = true,
+    progressBar = true,
+    content,
+    sx,
+}: ToastProps) {
+    return (
+        <div css={[toastStyle(status, toastTheme, type), sx]}>
+            {type !== 'default' && getSVG(type, toastTheme)}
+            {autoClose && progressBar && <div css={progressBarStyle(toastTheme, type, duration)} />}
+            <div>{content}</div>
+        </div>
+    );
+}
+
+const iconMap = {
+    info: { outline: Alert, filled: AlertFilled },
+    success: { outline: Check, filled: CheckFilled },
+    error: { outline: Alert, filled: AlertFilled },
+    default: null,
+};
+function getSVG(type: Type, toastTheme: ToastTheme) {
+    const Icon = iconMap[type]?.[toastTheme === 'colored' ? 'filled' : 'outline'];
+    return <Icon css={svgStyle(type)} />;
+}
+export { Toast };
diff --git a/client/src/components/Toast/ToastContainer.tsx b/client/src/components/Toast/ToastContainer.tsx
new file mode 100644
index 000000000..ecfabcba1
--- /dev/null
+++ b/client/src/components/Toast/ToastContainer.tsx
@@ -0,0 +1,27 @@
+import React, { useEffect, useState } from 'react';
+import { Container, ContainerPosition } from './Toast.style';
+import { createPortal } from 'react-dom';
+import type { ToastPosition } from './type';
+import { Toast } from './Toast';
+import type { ToastProps, getToastAndPositionProps } from './type';
+
+function ToastContainer({ getToastPosition }: getToastAndPositionProps) {
+    return (
+        <>
+            {Object.entries(getToastPosition()).map(([position, toasts]) =>
+                createPortal(
+                    <div
+                        key={position}
+                        css={[Container, ContainerPosition[position as ToastPosition]]}
+                    >
+                        {toasts.map((toast: ToastProps) => (
+                            <Toast key={toast.id} {...toast} />
+                        ))}
+                    </div>,
+                    document.body,
+                ),
+            )}
+        </>
+    );
+}
+export { ToastContainer };
diff --git a/client/src/components/Toast/ToastProvider.tsx b/client/src/components/Toast/ToastProvider.tsx
new file mode 100644
index 000000000..818a267e6
--- /dev/null
+++ b/client/src/components/Toast/ToastProvider.tsx
@@ -0,0 +1,124 @@
+import React, { createContext, useState } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children, limit }: ToastContainerProps) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(content?: ReactNode, options?: ToastProps) {
+        return createToast(content, mergeOptions('default', options));
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (content?: ReactNode, options?: ToastProps) =>
+            createToast(content, mergeOptions(type, options));
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(content?: ReactNode, options?: ToastProps) {
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (limit || 3)) {
+            setToasts((prev) =>
+                prev.map((toast, index) => (index === 0 ? { ...toast, status: 'exiting' } : toast)),
+            );
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            setTimeout(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        setTimeout(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        setTimeout(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // Container에서 toast 렌더링할 때 각 위치의 컨테이너에 토스트 스택으로 쌓아줌
+    function getToastPosition() {
+        const positionMap: Record<ToastPosition, ToastProps[]> = {
+            topRight: [],
+            topCenter: [],
+            topLeft: [],
+            bottomRight: [],
+            bottomCenter: [],
+            bottomLeft: [],
+        };
+
+        // 각 토스트를 key값에 맞는 위치에 추가
+        toasts.forEach((toast) => {
+            const position = toast.position || 'topCenter';
+            positionMap[position].push(toast);
+        });
+
+        return positionMap;
+    }
+
+    toast.info = createToastByType('info');
+    toast.success = createToastByType('success');
+    toast.error = createToastByType('error');
+
+    return (
+        <ToastContext.Provider value={{ toast }}>
+            {children}
+            <ToastContainer getToastPosition={getToastPosition} />
+        </ToastContext.Provider>
+    );
+}
+
+export { ToastProvider, ToastContext };
diff --git a/client/src/components/Toast/index.tsx b/client/src/components/Toast/index.tsx
new file mode 100644
index 000000000..1b794ee78
--- /dev/null
+++ b/client/src/components/Toast/index.tsx
@@ -0,0 +1 @@
+export * from './Toast';
diff --git a/client/src/components/Toast/type.ts b/client/src/components/Toast/type.ts
new file mode 100644
index 000000000..d8c7e0c7e
--- /dev/null
+++ b/client/src/components/Toast/type.ts
@@ -0,0 +1,50 @@
+import type { PropsWithChildren, ReactNode } from 'react';
+import type { CSSObject } from '@emotion/react';
+import type { CSSProperties } from 'react';
+
+export type ToastPosition =
+    | 'topRight'
+    | 'topCenter'
+    | 'topLeft'
+    | 'bottomRight'
+    | 'bottomCenter'
+    | 'bottomLeft';
+
+export type ToastTheme = 'white' | 'black' | 'colored';
+export type Type = 'info' | 'success' | 'error' | 'default';
+export type Status = 'entering' | 'exiting';
+
+export type ToastMethod = (content?: ReactNode, options?: ToastProps) => void;
+
+export interface ToastType {
+    (content?: ReactNode, options?: ToastProps): void;
+    info: ToastMethod;
+    success: ToastMethod;
+    error: ToastMethod;
+}
+
+export interface ToastContainerProps extends PropsWithChildren {
+    sx?: CSSObject;
+    limit?: number;
+}
+
+export interface ToastProps {
+    id?: number;
+    type?: Type;
+    toastTheme?: ToastTheme;
+    content?: ReactNode;
+    duration?: number;
+    autoClose?: boolean;
+    status?: Status;
+    position?: ToastPosition;
+    progressBar?: boolean;
+    sx?: CSSObject;
+}
+export interface getToastAndPositionProps {
+    getToastPosition: () => Record<ToastPosition, ToastProps[]>;
+}
+
+export interface TypeColor {
+    backgroundColor?: CSSProperties['backgroundColor'];
+    coloredBackgroundColor?: CSSProperties['backgroundColor'];
+}
diff --git a/client/src/hooks/useToast.tsx b/client/src/hooks/useToast.tsx
new file mode 100644
index 000000000..ec8d627e6
--- /dev/null
+++ b/client/src/hooks/useToast.tsx
@@ -0,0 +1,17 @@
+import { useContext } from 'react';
+import { ToastContext } from '@components/Toast/ToastProvider';
+
+const useToast = () => {
+    const toastContext = useContext(ToastContext);
+
+    if (!toastContext) {
+        throw new Error('ToastContext를 사용할 수 없는 컴포넌트입니다.');
+    }
+
+    const { toast } = toastContext;
+
+    return {
+        toast,
+    };
+};
+export { useToast };
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 0e8356f12..450d3d9bc 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -4,13 +4,16 @@ import App from './App';
 import { Global, ThemeProvider } from '@emotion/react';
 import globalStyles from '@styles/global';
 import theme from '@styles/theme';
+import { ToastProvider } from '@components/Toast/ToastProvider';
 
 const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
 root.render(
     <React.StrictMode>
         <Global styles={globalStyles} />
         <ThemeProvider theme={theme}>
-            <App />
+            <ToastProvider limit={5}>
+                <App />
+            </ToastProvider>
         </ThemeProvider>
     </React.StrictMode>,
 );
diff --git a/client/src/pages/TestPage/index.tsx b/client/src/pages/TestPage/index.tsx
index 94760d5c6..ed41b9ae6 100644
--- a/client/src/pages/TestPage/index.tsx
+++ b/client/src/pages/TestPage/index.tsx
@@ -1,32 +1,176 @@
 import React, { useState } from 'react';
-import { Select } from '@components/Select/Select';
+import { useToast } from '@hooks/useToast';
+import { Button } from '@components/Button';
+import { Text } from '@components/Text';
 function TestPage() {
-    const [value, setValue] = useState('');
+    const { toast } = useToast();
 
     return (
-        <div>
-            <p style={{ marginBottom: '20px' }}>Selected value: {value}</p>
-            <Select size="md">
-                <Select.Trigger>
-                    <Select.Value placeholder="Select a fruit" />
-                </Select.Trigger>
-                <Select.Content>
-                    <Select.Group>
-                        <Select.Label>Fruits</Select.Label>
-                        <Select.Item value="apple">안녕1</Select.Item>
-                        <Select.Item value="banana">안녕2</Select.Item>
-                        <Select.Item value="orange">안녕3</Select.Item>
-                    </Select.Group>
-                    <Select.Separator />
-                    <Select.Group>
-                        <Select.Label>Vegetables</Select.Label>
-                        <Select.Item value="carrot">Carrot</Select.Item>
-                        <Select.Item value="potato">Potato</Select.Item>
-                        <Select.Item value="broccoli">Broccoli</Select.Item>
-                    </Select.Group>
-                </Select.Content>
-            </Select>
-        </div>
+        <>
+            <div style={{ marginBottom: '200px' }} />
+
+            <Text type="h1Bold">Theme : White입니다.</Text>
+            <div style={{ width: '800px', display: 'flex', marginBottom: '30px' }}>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() => toast('Hello world', { toastTheme: 'white' })}
+                >
+                    기본 Toast 입니다.
+                </Button>
+
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.info('메일을 성공적으로 전송하였습니다.', {
+                            toastTheme: 'white',
+                            position: 'topLeft',
+                        })
+                    }
+                >
+                    info 입니다.
+                </Button>
+
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.success('Hello world', {
+                            toastTheme: 'white',
+                            position: 'topCenter',
+                            duration: 3000,
+                        })
+                    }
+                >
+                    success입니다.
+                </Button>
+
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.error('Hello world', {
+                            toastTheme: 'white',
+                            position: 'topRight',
+                            duration: 3000,
+                        })
+                    }
+                >
+                    error입니다.
+                </Button>
+            </div>
+
+            <Text type="h1Bold">Theme : black입니다.</Text>
+            <div style={{ width: '100%', display: 'flex', marginBottom: '30px' }}>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() => toast('Hello world', { toastTheme: 'black' })}
+                >
+                    기본 Toast 입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.info('Hello world', {
+                            toastTheme: 'black',
+                            position: 'topLeft',
+                        })
+                    }
+                >
+                    info입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.success('Hello world', {
+                            toastTheme: 'black',
+                            position: 'topCenter',
+                        })
+                    }
+                >
+                    success입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.error('Hello world', {
+                            toastTheme: 'black',
+                            position: 'topRight',
+                        })
+                    }
+                >
+                    error입니다.
+                </Button>
+            </div>
+
+            <Text type="h1Bold">Theme : Colored입니다.</Text>
+            <div style={{ width: '100%', display: 'flex', marginBottom: '30px' }}>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() => toast('기본 Toast', { toastTheme: 'colored' })}
+                >
+                    기본 Toast 입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.info('메일을 성공적으로 전송하였습니다.', {
+                            toastTheme: 'colored',
+                            position: 'topLeft',
+                        })
+                    }
+                >
+                    info 입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.success('Hello world', {
+                            toastTheme: 'colored',
+                            position: 'topCenter',
+                        })
+                    }
+                >
+                    success입니다.
+                </Button>
+                <Button
+                    size="full"
+                    variant="primary"
+                    onClick={() =>
+                        toast.error('Hello world', {
+                            toastTheme: 'colored',
+                            position: 'topRight',
+                        })
+                    }
+                >
+                    error입니다.
+                </Button>
+            </div>
+            <Button
+                size="full"
+                variant="primary"
+                onClick={() =>
+                    toast.error(
+                        'Hello world',
+                        {
+                            toastTheme: 'colored',
+                            position: 'topRight',
+                        },
+                        { limit: 3 }, // 최대 toast 발행 수
+                    )
+                }
+            >
+                컨테이너 제어하는 방법은 이 코드 참고
+            </Button>
+        </>
     );
 }
 export { TestPage };


Reviews:
user: login: littleemm
body: 컴포넌트 구현하시느라 정말 고생많으셨습니다..!
코드 통해서 새로운 개념들을 많이 알게 되면서 같이 공부하는 마음으로 리뷰했습니다!
다만 아직 많이 부족해서... 이렇게 리뷰를 마무리합니다..  앞으로 더 많이 공부해서 더 좋은 리뷰를 해보겠습니다..!


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T08:53:07Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T09:57:49Z
user: login: ohsung0722
body: 코드 작성하시느라 너무 고생 많으셨습니다..!
하나 하나 이해하면서 리뷰 작성하느라 시간이 너무 오래걸린 점 죄송합니다ㅠㅜ
덕분에 공부 정말 많이 했고, 코드 작성 방식도 참고가 많이 된 것 같아요!
개선사항 등 리뷰 포인트에 작성해주신 점 참고해서 코드 읽어봤는데, 개선 필요사항 첫번째 부분이 잘 이해가 안가서요!
Toast를 발행할 때마다 containerOptions.limit을 직접 넘겨주어야, 그 후부터 적용이 된다는 문제점이라는게 혹시 
애초에 초기 설정을 한번만 해두고, 그 후에는 toast() 호출 시마다 넘기지 않아도 되도록 하고싶다는 말씀이실까요...?

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:20:21Z
user: login: Dochanii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T07:44:13Z
user: login: Dochanii
body: 컴포넌트 만드느라 너무 고생하셨습니다!!
코드는 최대한 가독성 좋게 짜신거같은데 이번 컴포넌트가 저에게 좀 어렵게 다가와서 코드 이해하는데 오래걸려서 리뷰가 늦은점 그리고 지식 부족으로 많은 리뷰를 드리지 못헤 죄송합니다...ㅠ
항상 코드 보면서 많은 공부가 됩니다!! 감사합니다!!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T08:28:49Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T02:15:31Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T02:18:15Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:31:06Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:31:41Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:35:44Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:35:49Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:35:51Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:35:55Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:35:56Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:49:24Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:49:27Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:49:36Z
user: login: aiminghee3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T05:20:47Z
user: login: littleemm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T03:11:39Z
user: login: ohsung0722
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T05:53:29Z

Review comments:
diff_hunk: @@ -0,0 +1,53 @@
+import React from 'react';
+import type { Type, ToastProps, ToastTheme } from './type';
+import { toastStyle, svgStyle, progressBarStyle } from './Toast.style';
+import Alert from '@assets/images/alert.svg';
+import Check from '@assets/images/check.svg';
+import CheckFilled from '@assets/images/check-filled.svg';
+import AlertFilled from '@assets/images/alert-filled.svg';
+
+function Toast({
+    type = 'default',
+    toastTheme = 'white',
+    status = 'entering',
+    duration = 3000,
+    autoClose = true,
+    progressBar = true,
+    content,
+    sx,
+}: ToastProps) {
+    return (
+        <div css={[toastStyle(status, toastTheme, type), sx]}>
+            {type !== 'default' && getSVG(type, toastTheme)}
+            {autoClose && progressBar && <div css={progressBarStyle(toastTheme, type, duration)} />}
+            <div>{content}</div>
+        </div>
+    );
+}
+
+function getSVG(type: Type, toastTheme: ToastTheme) {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return <AlertFilled css={svgStyle(type)} />;
+            case 'success':
+                return <CheckFilled css={svgStyle(type)} />;
+            case 'error':
+                return <AlertFilled css={svgStyle(type)} />;
+            default:
+                return null;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return <Alert css={svgStyle(type)} />;
+            case 'success':
+                return <Check css={svgStyle(type)} />;
+            case 'error':
+                return <Alert css={svgStyle(type)} />;
+            default:
+                return null;
+        }
path: client/src/components/Toast/Toast.tsx
user: login: littleemm
body: switch 문을 조금 더 간결하게 해보면 좋을 것 같아 남겨봅니다!

1. case 'info'와 'error'의 경우에는 return 값이 동일하기 때문에 아래처럼 작성해도 좋을 것 같아요.
`case 'info':
case 'error': return ~` 
2. 삼항연산자
switch 문을 안쓰고 아래처럼 삼항연산자를 사용하면 코드가 다섯줄로 줄어들더라구요!
`return type === 'info' || type === 'error' ? (
            <Alert css={svgStyle(type)} />
        ) : type === 'success' ? (
            <Check css={svgStyle(type)} />
        ) : null;`
        
두 개 중에 하나 사용하시면 코드가 좀 더 깔끔할 것 같아요!
개인적으로는 조건이 많지 않아서 삼항연산자를 써도 괜찮을 것 같습니당!
created_at: 2025-03-19T17:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
path: client/src/components/Toast/Toast.style.tsx
user: login: littleemm
body: 이 부분 string으로 쓰신 이유가 궁금합니다!

저희가 
` ${type === 'default' &&
                css`
                    color: ${theme.colors.white};
                `} ${type === 'default' &&
                css`
                    color: ${theme.colors.white};
                `}`
이런 식으로 코드 작성을 통일한 이유가 실수 방지였었는데 string으로 설정하면 살짝 통일이 안되는 것 같기도 해서요..!
                
created_at: 2025-03-19T17:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
path: client/src/components/Toast/Toast.style.tsx
user: login: littleemm
body: 제 생각에는 아래 예시처럼 interface랑 Record 타입 설정해두고 if문 작성하시면 훨씬 코드 길이가 줄어들 것 같습니다.
type을 기준으로 ui가 변경되는 로직이 많은 것 같아서 그걸 다 포괄하는 interface를 만드시면 다른 부분도 코드 길이 줄일 수 있을 것 같아요!

```
interface TypeColor {
    backgroundColor?: CSSProperties['backgroundColor'];
    coloredBackgroundColor?: CSSProperties['backgroundColor'];
}
export const ToastType: Record<Type, TypeColor> = {
    info: {
        backgroundColor: theme.colors.blue[300],
        coloredBackgroundColor: theme.colors.blue[200],
    },
    success: {
        backgroundColor: theme.colors.green[200],
        coloredBackgroundColor: theme.colors.green[100],
    },
    error: {
        backgroundColor: theme.colors.red[600],
        coloredBackgroundColor: theme.colors.red[800],
    },
    default: { backgroundColor: theme.colors.black, coloredBackgroundColor: theme.colors.black },
};
created_at: 2025-03-19T18:20:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
path: client/src/components/Toast/Toast.style.tsx
user: login: littleemm
body: 그리고 theme이 검정이고 type이 default일 때 프로그래스바 색상도 검정이어서 눈에 잘 안보이더라구요 ㅠㅠ
검정 default일 때 따로 색상을 흰색으로 설정하는 건 어떠실까요??
created_at: 2025-03-20T08:24:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import type { ReactNode } from 'react';
+import type { CSSObject } from '@emotion/react';
+
+export type ToastPosition =
+    | 'topRight'
+    | 'topCenter'
+    | 'topLeft'
+    | 'bottomRight'
+    | 'bottomCenter'
+    | 'bottomLeft';
+
+export type ToastTheme = 'white' | 'black' | 'colored';
+export type Type = 'info' | 'success' | 'error' | 'default';
+
+export type ToastMethod = (
+    content?: ReactNode,
+    options?: ToastProps,
+    containerOptions?: ToastContainerProps,
+) => void;
+
+export interface ToastType {
+    (content?: ReactNode, options?: ToastProps, containerOptions?: ToastContainerProps): void;
+    info: ToastMethod;
+    success: ToastMethod;
+    error: ToastMethod;
+}
+
+export interface ToastContainerProps {
+    sx?: CSSObject;
+    limit?: number;
+}
+
+export interface ToastProps {
+    id?: number;
+    type?: Type;
+    toastTheme?: ToastTheme;
+    content?: ReactNode;
+    duration?: number;
+    autoClose?: boolean;
+    status?: string;
path: client/src/components/Toast/type.ts
user: login: littleemm
body: 이건 별 거 아니지만..! status도 따로 타입을 지정해주면 좋을 것 같습니다!
created_at: 2025-03-20T08:32:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    }
+};
+
+const toastFadeIn = keyframes`
+    from {
+        opacity: 0;
+        transform: translateY(2rem);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+`;
+
+const toastFadeOut = keyframes`
+    from {
+        opacity: 1;
+        transform: translateY(0);
+    }
+    to {
+        opacity: 0;
+        transform: translateY(-2rem);
+    }
+`;
+
+const progressBarAnimation = keyframes`
+    0% {
+        width: 100%;
+    }
+    100% {
+        width: 0%;
+    }
+`;
+
+export const Container = css`
+    position: fixed;
+    padding: 0.4rem 0.4rem;
+`;
+
+export const ContainerPosition: Record<ToastPosition, CSSObject> = {
+    topRight: {
+        top: '2rem',
+        right: '2rem',
+    },
+    topCenter: {
+        top: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    topLeft: {
+        top: '2rem',
+        left: '2rem',
+    },
+    bottomRight: {
+        bottom: '2rem',
+        right: '2rem',
+    },
+    bottomCenter: {
+        bottom: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    bottomLeft: {
+        bottom: '2rem',
+        left: '2rem',
+    },
+};
+
+export const toastStyle = (status: string, toastTheme: ToastTheme, type: Type) => css`
+    ${getToastBackgroundStyles(type, toastTheme)}
+    ${theme.typography.bodySemibold}
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: start;
+    padding: 1rem 1rem;
+    gap: 0.5rem;
+    margin-bottom: 1rem;
+    border-radius: 0.625rem;
+    min-width: 30rem;
+    min-height: 5rem;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    animation: ${status === 'entering' ? toastFadeIn : status === 'exiting' ? toastFadeOut : 'none'}
+        1000ms ease forwards;
+`;
+
+export const svgStyle = (type: Type) => css`
+    width: 2rem;
+    height: 2rem;
+    ${svgColor(type)}
+`;
+
+export const progressBarStyle = (toastTheme: ToastTheme, type: Type, duration: number) => css`
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    height: 0.3rem;
path: client/src/components/Toast/Toast.style.tsx
user: login: littleemm
body: ![image](https://github.com/user-attachments/assets/1a9c1c02-8311-4dc9-951d-bdacd250fa57)

토스트 배경색이 흰색일 때는 괜찮은데 검정이거나 colored일 때는 프로그래스바랑 토스트 하단 사이 빈틈이 생겨서 조금 어색하더라구요..! 길이 조절을 조금 하면 될 것 같습니다!
created_at: 2025-03-20T08:47:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+import React from 'react';
+import type { Type, ToastProps, ToastTheme } from './type';
+import { toastStyle, svgStyle, progressBarStyle } from './Toast.style';
+import Alert from '@assets/images/alert.svg';
+import Check from '@assets/images/check.svg';
+import CheckFilled from '@assets/images/check-filled.svg';
+import AlertFilled from '@assets/images/alert-filled.svg';
+
+function Toast({
+    type = 'default',
+    toastTheme = 'white',
+    status = 'entering',
+    duration = 3000,
+    autoClose = true,
+    progressBar = true,
+    content,
+    sx,
+}: ToastProps) {
+    return (
+        <div css={[toastStyle(status, toastTheme, type), sx]}>
+            {type !== 'default' && getSVG(type, toastTheme)}
+            {autoClose && progressBar && <div css={progressBarStyle(toastTheme, type, duration)} />}
+            <div>{content}</div>
+        </div>
+    );
+}
+
+function getSVG(type: Type, toastTheme: ToastTheme) {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return <AlertFilled css={svgStyle(type)} />;
+            case 'success':
+                return <CheckFilled css={svgStyle(type)} />;
+            case 'error':
+                return <AlertFilled css={svgStyle(type)} />;
+            default:
+                return null;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return <Alert css={svgStyle(type)} />;
+            case 'success':
+                return <Check css={svgStyle(type)} />;
+            case 'error':
+                return <Alert css={svgStyle(type)} />;
+            default:
+                return null;
+        }
path: client/src/components/Toast/Toast.tsx
user: login: ohsung0722
body: 저같은 경우는 중첩 switch의 경우 타입별로 반환하는 객체를 미리 정의해놓는 편입니다!
준희님 코드 같은 경우는
```
const iconMap = {
    info: { default: Alert, colored: AlertFilled },
    success: { default: Check, colored: CheckFilled },
    error: { default: Alert, colored: AlertFilled },
    default: null,
};
```
이런식으로 타입에 따라 반환할 컴포넌트를 미리 정의해놓으면 switch case나 삼항 연산자를 사용하지 않아도 괜찮아서요!
```
function getSVG(type: Type, toastTheme: ToastTheme) {
    const Icon = iconMap[type]?.[toastTheme === 'colored' ? 'colored' : 'default'];
    return <Icon css={svgStyle(type)} />;
}
```
이렇게 될 것 같네요!
created_at: 2025-03-20T09:07:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+import React, { useEffect, useState } from 'react';
+import { Container, ContainerPosition } from './Toast.style';
+import { createPortal } from 'react-dom';
+import type { ToastPosition } from './type';
+import { Toast } from './Toast';
+import type { ToastProps, getToastAndPositionProps } from './type';
+
+function ToastContainer({ getToastPosition }: getToastAndPositionProps) {
+    return (
+        <>
+            {Object.entries(getToastPosition()).map(
+                ([position, toasts]) =>
+                    toasts &&
+                    createPortal(
path: client/src/components/Toast/ToastContainer.tsx
user: login: ohsung0722
body: 여기서 이미 `getToastPosition()` 은 빈 배열을 return하고 있으므로 toasts는 무조건 값이 있거나, 없다면 빈 배열이 들어가서 undefined는 발생하지 않을 것 같습니다! 
즉, `toasts &&` 로 한번 더 점검하지 않아도 괜찮을 것 같아요!

아니면, toasts.length가 0보다 클 때만 렌더링 하도록 조건을 수정해보는건 어떨까요??
created_at: 2025-03-20T09:32:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+import React from 'react';
+import type { Type, ToastProps, ToastTheme } from './type';
+import { toastStyle, svgStyle, progressBarStyle } from './Toast.style';
+import Alert from '@assets/images/alert.svg';
+import Check from '@assets/images/check.svg';
+import CheckFilled from '@assets/images/check-filled.svg';
+import AlertFilled from '@assets/images/alert-filled.svg';
+
+function Toast({
+    type = 'default',
+    toastTheme = 'white',
+    status = 'entering',
+    duration = 3000,
+    autoClose = true,
+    progressBar = true,
+    content,
+    sx,
+}: ToastProps) {
+    return (
+        <div css={[toastStyle(status, toastTheme, type), sx]}>
+            {type !== 'default' && getSVG(type, toastTheme)}
+            {autoClose && progressBar && <div css={progressBarStyle(toastTheme, type, duration)} />}
+            <div>{content}</div>
+        </div>
+    );
+}
+
+function getSVG(type: Type, toastTheme: ToastTheme) {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return <AlertFilled css={svgStyle(type)} />;
+            case 'success':
+                return <CheckFilled css={svgStyle(type)} />;
+            case 'error':
+                return <AlertFilled css={svgStyle(type)} />;
+            default:
+                return null;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return <Alert css={svgStyle(type)} />;
+            case 'success':
+                return <Check css={svgStyle(type)} />;
+            case 'error':
+                return <Alert css={svgStyle(type)} />;
+            default:
+                return null;
+        }
path: client/src/components/Toast/Toast.tsx
user: login: aiminghee3
body: 의견 감사합니다! 
생각해봤는데, switch case나 삼항연산자를 중첩해서 사용할 경우 다른 팀원이 코드를 볼 때 
깔끔하게 보지 못하고 해석하는데 시간을 비교적 많이 쓸 것이라고 생각해서
if else문으로 수정해봤는데, 혹시 수정된 코드 보고 더 손봐야할 부분 있으면 말씀해주세요!
created_at: 2025-03-22T09:57:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+import React, { useEffect, useState } from 'react';
+import { Container, ContainerPosition } from './Toast.style';
+import { createPortal } from 'react-dom';
+import type { ToastPosition } from './type';
+import { Toast } from './Toast';
+import type { ToastProps, getToastAndPositionProps } from './type';
+
+function ToastContainer({ getToastPosition }: getToastAndPositionProps) {
+    return (
+        <>
+            {Object.entries(getToastPosition()).map(
+                ([position, toasts]) =>
+                    toasts &&
+                    createPortal(
+                        <div
+                            key={position}
+                            css={[Container, ContainerPosition[position as ToastPosition]]}
+                        >
+                            {toasts.map((toast: ToastProps) => (
+                                <div key={toast.id}>
+                                    <Toast {...toast} />
+                                </div>
path: client/src/components/Toast/ToastContainer.tsx
user: login: ohsung0722
body: 이부분 div로 감싼 이유가 있으실까요??
없으면 그냥 Toast에 key 속성 추가하고 div는 없애도 괜찮을 것 같아요!
created_at: 2025-03-23T12:19:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
path: client/src/components/Toast/ToastProvider.tsx
user: login: ohsung0722
body: toasts를 useState를 사용해서 관리하고 있는데,
내부 status 값을 setToasts 없이 사용하는게 조금 어색해서요!
아래 removeAnimation이랑 비슷하게 setToasts로 status를 관리하는건 어떨까요??
created_at: 2025-03-23T15:07:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
path: client/src/components/Toast/ToastProvider.tsx
user: login: ohsung0722
body: 사소한거긴 한데, defaultContainerOption을 계속 불러오는 것 보다 `(prev) => ({...prev, ...containerOptions)}` 를 사용하는건 어떨까요??
created_at: 2025-03-23T15:10:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            animation(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        animation(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        animation(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // 애니메이션
+    function animation(method: () => void, duration: number) {
+        setTimeout(method, duration);
+    }
+
+    // Container에서 toast 렌더링할 때 각 위치의 컨테이너에 토스트 스택으로 쌓아줌
+    function getToastPosition() {
+        const positionMap: Record<ToastPosition, ToastProps[]> = {
+            topRight: [],
+            topCenter: [],
+            topLeft: [],
+            bottomRight: [],
+            bottomCenter: [],
+            bottomLeft: [],
+        };
+
+        // 각 토스트를 key값에 맞는 위치에 추가
+        toasts.forEach((toast) => {
+            const position = toast.position || 'topCenter';
+            positionMap[position].push(toast);
+        });
+
+        return positionMap;
+    }
+
+    toast.info = createToastByType('info');
+    toast.success = createToastByType('success');
+    toast.error = createToastByType('error');
path: client/src/components/Toast/ToastProvider.tsx
user: login: ohsung0722
body: 이거도 사소한거긴 한데, 
다른 컴포넌트처럼 Object.assign으로 관리하는건 어떨까요??
created_at: 2025-03-23T15:50:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
path: client/src/components/Toast/ToastProvider.tsx
user: login: Dochanii
body: 저도 오성님과 같은 의견으로 useState를 사용해서 관리하니까 setToasts를 사용해서 관리하면 좋을거 같아여 그리고 지금 state로 관리된 toasts를 직접적으로 수정하는 부분 같은데 복사본을 만들어서 새 객체를 setToasts로 반환하는게 더 바람직할 것 같습니다!
created_at: 2025-03-24T07:44:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
path: client/src/components/Toast/Toast.style.tsx
user: login: Dochanii
body: <img width="311" alt="스크린샷 2025-03-24 오후 4 58 47" src="https://github.com/user-attachments/assets/01beeb72-6e2e-46fb-b1da-ab1c45d754c9" />

배경이 검은색일 때 기본 Toast의 progressBar 색상이 둘 다 검은색이라 보이질 않아서 색상을 바꾸면 좋을 것 같습니다!
created_at: 2025-03-24T08:01:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            animation(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        animation(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        animation(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // 애니메이션
+    function animation(method: () => void, duration: number) {
+        setTimeout(method, duration);
+    }
path: client/src/components/Toast/ToastProvider.tsx
user: login: Dochanii
body: 별거 아니지만.. Toast가 사라질 때 동작하는 함수 같은데 animation이라는 이름은 css느낌이 나는데 하는 일은 setTimeout이라서  initiateToastRemoval 이런식으로 네이밍 설정을 하는건 어떨까요??
created_at: 2025-03-24T08:23:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+import React from 'react';
+import type { Type, ToastProps, ToastTheme } from './type';
+import { toastStyle, svgStyle, progressBarStyle } from './Toast.style';
+import Alert from '@assets/images/alert.svg';
+import Check from '@assets/images/check.svg';
+import CheckFilled from '@assets/images/check-filled.svg';
+import AlertFilled from '@assets/images/alert-filled.svg';
+
+function Toast({
+    type = 'default',
+    toastTheme = 'white',
+    status = 'entering',
+    duration = 3000,
+    autoClose = true,
+    progressBar = true,
+    content,
+    sx,
+}: ToastProps) {
+    return (
+        <div css={[toastStyle(status, toastTheme, type), sx]}>
+            {type !== 'default' && getSVG(type, toastTheme)}
+            {autoClose && progressBar && <div css={progressBarStyle(toastTheme, type, duration)} />}
+            <div>{content}</div>
+        </div>
+    );
+}
+
+function getSVG(type: Type, toastTheme: ToastTheme) {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return <AlertFilled css={svgStyle(type)} />;
+            case 'success':
+                return <CheckFilled css={svgStyle(type)} />;
+            case 'error':
+                return <AlertFilled css={svgStyle(type)} />;
+            default:
+                return null;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return <Alert css={svgStyle(type)} />;
+            case 'success':
+                return <Check css={svgStyle(type)} />;
+            case 'error':
+                return <Alert css={svgStyle(type)} />;
+            default:
+                return null;
+        }
path: client/src/components/Toast/Toast.tsx
user: login: aiminghee3
body: > 의견 감사합니다! 생각해봤는데, switch case나 삼항연산자를 중첩해서 사용할 경우 다른 팀원이 코드를 볼 때 깔끔하게 보지 못하고 해석하는데 시간을 비교적 많이 쓸 것이라고 생각해서 if else문으로 수정해봤는데, 혹시 수정된 코드 보고 더 손봐야할 부분 있으면 말씀해주세요!

map으로 변경하였습니다. 예시로 주신 코드에는 default 변수명이 중복되어서 svg 자체를 나타내는 outline, filled로 변경하였습니다!
created_at: 2025-03-25T02:15:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
path: client/src/components/Toast/Toast.style.tsx
user: login: aiminghee3
body: 음 제가 말씀하신걸 잘 이해 못했을 수 있는데,
이 함수는 css를 적용하는 함수가 아니라서 실제로 css를 적용시키는 함수인 `toastStyle`에서 css를 적용시켜주고 있습니다..!
따라서 문자열만을 반환하고 반환한 문자열을 통해 css를 적용해야 하기 때문에 문자열을 반환하게 됐습니다.
혹시 말씀하시는게 이 뜻이 아니신가요..?
created_at: 2025-03-25T02:18:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            animation(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        animation(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        animation(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // 애니메이션
+    function animation(method: () => void, duration: number) {
+        setTimeout(method, duration);
+    }
+
+    // Container에서 toast 렌더링할 때 각 위치의 컨테이너에 토스트 스택으로 쌓아줌
+    function getToastPosition() {
+        const positionMap: Record<ToastPosition, ToastProps[]> = {
+            topRight: [],
+            topCenter: [],
+            topLeft: [],
+            bottomRight: [],
+            bottomCenter: [],
+            bottomLeft: [],
+        };
+
+        // 각 토스트를 key값에 맞는 위치에 추가
+        toasts.forEach((toast) => {
+            const position = toast.position || 'topCenter';
+            positionMap[position].push(toast);
+        });
+
+        return positionMap;
+    }
+
+    toast.info = createToastByType('info');
+    toast.success = createToastByType('success');
+    toast.error = createToastByType('error');
path: client/src/components/Toast/ToastProvider.tsx
user: login: aiminghee3
body: 음 Object.assign() 방식은 주로 합성 컴포넌트에서 사용되는 패턴으로 알고있는데,
toast 컴포넌트는 토스트의 제목같은 부가적인 속성이 없는 상태라서
toast의 단순한 타입들을 Object로 묶는 건 좀 어색하다고 생각이 들었습니다..!

그래서 사용자가 편리하게 토스트를 발행할 수 있도록 메소드 체이닝 패턴으로 만들어봤고,
사용자가 실제로 토스트를 사용할 때도 Toast 컴포넌트를 import하지 않고 
아래 예시 코드와 같이 단순히 toast 함수를 이용해 발행합니다!
`() => toast('Hello world', { toastTheme: 'white' })`
`() => toast.info('Hello world', { toastTheme: 'white' })`

만약 이 접근 방식이 부자연스럽게 느껴지신다면 언제든 다른 형태의 토스트 발행 방식을 고민해보겠습니다:D
created_at: 2025-03-25T03:31:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
path: client/src/components/Toast/Toast.style.tsx
user: login: aiminghee3
body: 넵 수정하였습니다!
created_at: 2025-03-25T03:31:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            animation(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        animation(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        animation(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // 애니메이션
+    function animation(method: () => void, duration: number) {
+        setTimeout(method, duration);
+    }
path: client/src/components/Toast/ToastProvider.tsx
user: login: aiminghee3
body: 이 부분 리뷰 보고 코드를 다시 봤는데,
setTimeout의 인자와 animation의 인자가 같아서
기존의 animation 함수를 제거하고 setTimeout 함수를 바로 사용하는 방향으로 수정 하였습니다!
created_at: 2025-03-25T03:35:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
path: client/src/components/Toast/ToastProvider.tsx
user: login: aiminghee3
body: 넵 수정하였습니다!
created_at: 2025-03-25T03:35:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
path: client/src/components/Toast/ToastProvider.tsx
user: login: aiminghee3
body: 넵 useState 통해서 상태를 바꿀 수 있도록 수정하였습니다!
created_at: 2025-03-25T03:35:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+import React, { useEffect, useState } from 'react';
+import { Container, ContainerPosition } from './Toast.style';
+import { createPortal } from 'react-dom';
+import type { ToastPosition } from './type';
+import { Toast } from './Toast';
+import type { ToastProps, getToastAndPositionProps } from './type';
+
+function ToastContainer({ getToastPosition }: getToastAndPositionProps) {
+    return (
+        <>
+            {Object.entries(getToastPosition()).map(
+                ([position, toasts]) =>
+                    toasts &&
+                    createPortal(
+                        <div
+                            key={position}
+                            css={[Container, ContainerPosition[position as ToastPosition]]}
+                        >
+                            {toasts.map((toast: ToastProps) => (
+                                <div key={toast.id}>
+                                    <Toast {...toast} />
+                                </div>
path: client/src/components/Toast/ToastContainer.tsx
user: login: aiminghee3
body: 수정하였습니다!
created_at: 2025-03-25T03:35:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+import React, { useEffect, useState } from 'react';
+import { Container, ContainerPosition } from './Toast.style';
+import { createPortal } from 'react-dom';
+import type { ToastPosition } from './type';
+import { Toast } from './Toast';
+import type { ToastProps, getToastAndPositionProps } from './type';
+
+function ToastContainer({ getToastPosition }: getToastAndPositionProps) {
+    return (
+        <>
+            {Object.entries(getToastPosition()).map(
+                ([position, toasts]) =>
+                    toasts &&
+                    createPortal(
path: client/src/components/Toast/ToastContainer.tsx
user: login: aiminghee3
body: 오 그러네요 말씀해주신대로 수정하였습니다!
created_at: 2025-03-25T03:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import type { ReactNode } from 'react';
+import type { CSSObject } from '@emotion/react';
+
+export type ToastPosition =
+    | 'topRight'
+    | 'topCenter'
+    | 'topLeft'
+    | 'bottomRight'
+    | 'bottomCenter'
+    | 'bottomLeft';
+
+export type ToastTheme = 'white' | 'black' | 'colored';
+export type Type = 'info' | 'success' | 'error' | 'default';
+
+export type ToastMethod = (
+    content?: ReactNode,
+    options?: ToastProps,
+    containerOptions?: ToastContainerProps,
+) => void;
+
+export interface ToastType {
+    (content?: ReactNode, options?: ToastProps, containerOptions?: ToastContainerProps): void;
+    info: ToastMethod;
+    success: ToastMethod;
+    error: ToastMethod;
+}
+
+export interface ToastContainerProps {
+    sx?: CSSObject;
+    limit?: number;
+}
+
+export interface ToastProps {
+    id?: number;
+    type?: Type;
+    toastTheme?: ToastTheme;
+    content?: ReactNode;
+    duration?: number;
+    autoClose?: boolean;
+    status?: string;
path: client/src/components/Toast/type.ts
user: login: aiminghee3
body: 넵 수정하였습니다!
created_at: 2025-03-25T03:49:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
path: client/src/components/Toast/Toast.style.tsx
user: login: aiminghee3
body: 수정하였습니다!
created_at: 2025-03-25T03:49:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
+            `;
+        case 'colored':
+            return `
+                background-color: ${getColorByTypeAtColoredTheme(type)};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.black};`}
+            `;
+        default:
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+    }
+};
+
+// 타입에 따른 svg 컬러 추출
+const svgColor = (type: Type) => {
+    switch (type) {
+        case 'info':
+            return `
+                stroke: ${theme.colors.blue[300]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.blue[300]};
+            `;
+        case 'success':
+            return `
+                stroke: ${theme.colors.green[200]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.green[200]};
+            `;
+        case 'error':
+            return `
+                stroke: ${theme.colors.red[800]};
+                fill: ${theme.colors.white};
+                color: ${theme.colors.red[800]};
+            `;
+        default:
+            return `
+                color: ${theme.colors.black};
+            `;
+    }
+};
+//FIXME 이게 로직이 맞나.....
+const progressBarColor = (toastTheme: ToastTheme, type: Type) => {
+    if (toastTheme === 'colored') {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[200]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[100]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[600]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    } else {
+        switch (type) {
+            case 'info':
+                return `
+                    background-color: ${theme.colors.blue[300]};
+                `;
+            case 'success':
+                return `
+                    background-color: ${theme.colors.green[200]};
+                `;
+            case 'error':
+                return `
+                    background-color: ${theme.colors.red[800]};
+                `;
+            default:
+                return `
+                    background-color: ${theme.colors.black};
+                `;
+        }
+    }
+};
+
+const toastFadeIn = keyframes`
+    from {
+        opacity: 0;
+        transform: translateY(2rem);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+`;
+
+const toastFadeOut = keyframes`
+    from {
+        opacity: 1;
+        transform: translateY(0);
+    }
+    to {
+        opacity: 0;
+        transform: translateY(-2rem);
+    }
+`;
+
+const progressBarAnimation = keyframes`
+    0% {
+        width: 100%;
+    }
+    100% {
+        width: 0%;
+    }
+`;
+
+export const Container = css`
+    position: fixed;
+    padding: 0.4rem 0.4rem;
+`;
+
+export const ContainerPosition: Record<ToastPosition, CSSObject> = {
+    topRight: {
+        top: '2rem',
+        right: '2rem',
+    },
+    topCenter: {
+        top: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    topLeft: {
+        top: '2rem',
+        left: '2rem',
+    },
+    bottomRight: {
+        bottom: '2rem',
+        right: '2rem',
+    },
+    bottomCenter: {
+        bottom: '2rem',
+        left: '50%',
+        transform: 'translateX(-50%)',
+    },
+    bottomLeft: {
+        bottom: '2rem',
+        left: '2rem',
+    },
+};
+
+export const toastStyle = (status: string, toastTheme: ToastTheme, type: Type) => css`
+    ${getToastBackgroundStyles(type, toastTheme)}
+    ${theme.typography.bodySemibold}
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: start;
+    padding: 1rem 1rem;
+    gap: 0.5rem;
+    margin-bottom: 1rem;
+    border-radius: 0.625rem;
+    min-width: 30rem;
+    min-height: 5rem;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    animation: ${status === 'entering' ? toastFadeIn : status === 'exiting' ? toastFadeOut : 'none'}
+        1000ms ease forwards;
+`;
+
+export const svgStyle = (type: Type) => css`
+    width: 2rem;
+    height: 2rem;
+    ${svgColor(type)}
+`;
+
+export const progressBarStyle = (toastTheme: ToastTheme, type: Type, duration: number) => css`
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    height: 0.3rem;
path: client/src/components/Toast/Toast.style.tsx
user: login: aiminghee3
body: 수정하였습니다!
created_at: 2025-03-25T03:49:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,215 @@
+import { css, keyframes } from '@emotion/react';
+import theme from '@styles/theme';
+import type { CSSObject } from '@emotion/react';
+import type { ToastPosition, ToastTheme, Type } from './type';
+
+// colored일 때 테마에 따른 색상 추출
+const getColorByTypeAtColoredTheme = (type: Type): string => {
+    switch (type) {
+        case 'error':
+            return theme.colors.red[800];
+        case 'success':
+            return theme.colors.green[200];
+        case 'info':
+            return theme.colors.default;
+        default:
+            return theme.colors.white;
+    }
+};
+
+// 토스트 테마에 따른 배경색 및 글자색 추출
+const getToastBackgroundStyles = (type: Type, toastTheme: ToastTheme): string => {
+    switch (toastTheme) {
+        case 'white':
+            return `
+                background-color: ${theme.colors.white};
+                color: ${theme.colors.black};
+            `;
+        case 'black':
+            return `
+                background-color: ${theme.colors.black};
+                color: ${theme.colors.white};
+                ${type === 'default' && `color: ${theme.colors.white};`}
path: client/src/components/Toast/Toast.style.tsx
user: login: aiminghee3
body: > 음 제가 말씀하신걸 잘 이해 못했을 수 있는데, 이 함수는 css를 적용하는 함수가 아니라서 실제로 css를 적용시키는 함수인 `toastStyle`에서 css를 적용시켜주고 있습니다..! 따라서 문자열만을 반환하고 반환한 문자열을 통해 css를 적용해야 하기 때문에 문자열을 반환하게 됐습니다. 혹시 말씀하시는게 이 뜻이 아니신가요..?

이 코드 뿐만 아니라 다른 부분도 말씀해주신 css로 감싸서 return 할 수 있도록 수정하였습니다!
created_at: 2025-03-25T05:20:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import React, { createContext, useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { Type, ToastProps, ToastContainerProps, ToastType, ToastPosition } from './type';
+import { ToastContainer } from './ToastContainer';
+import type { ReactNode } from 'react';
+
+interface ToastContextType {
+    toast: ToastType;
+}
+
+const defaultOptions: ToastProps = {
+    type: 'default',
+    toastTheme: 'white',
+    status: 'entering',
+    progressBar: true,
+    duration: 3000,
+    autoClose: true,
+};
+
+const defaultContainerOptions: ToastContainerProps = {
+    limit: 3,
+};
+
+const ToastContext = createContext<ToastContextType | null>(null);
+
+function ToastProvider({ children }: PropsWithChildren) {
+    //prop destruction
+    //lib hooks
+    //state, ref, querystring hooks
+    const [toasts, setToasts] = useState<ToastProps[]>([]);
+    const [container, setContainer] = useState<ToastContainerProps>(defaultContainerOptions);
+
+    //form hooks
+    //query hooks
+    //calculated values
+    //effects
+    //handlers
+
+    // 기본 토스트 발행
+    function toast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        return createToast(content, mergeOptions('default', options), containerOptions);
+    }
+
+    // 토스트 타입별로 분류해서 발행
+    function createToastByType(type: Type) {
+        return (
+            content?: ReactNode,
+            options?: ToastProps,
+            containerOptions?: ToastContainerProps,
+        ) => createToast(content, mergeOptions(type, options), containerOptions);
+    }
+
+    // default옵션 + 사용자 정의 옵션 + 타입 합치기
+    function mergeOptions(type: Type, options?: ToastProps) {
+        return {
+            ...defaultOptions,
+            type: type,
+            ...options,
+        };
+    }
+
+    // 토스트 생성
+    function createToast(
+        content?: ReactNode,
+        options?: ToastProps,
+        containerOptions?: ToastContainerProps,
+    ) {
+        // 객체가 존재하면서 객체가 비어있지 않을 때 ex) {}면 불가능 && Container 세팅
+        if (containerOptions && Object.keys(containerOptions).length > 0) {
+            setContainer({ ...defaultContainerOptions, ...containerOptions });
+        }
+        const id = Date.now();
+        const newToast = {
+            id: id,
+            content: content,
+            ...options,
+        };
+
+        checkLimitAndRemoveToast();
+        setToasts((prev) => [...prev, newToast]);
+
+        if (options?.autoClose) AutoRemoveToast(id, options);
+    }
+
+    // 사용자가 제한 둔 toast 개수 넘어가면 삭제
+    function checkLimitAndRemoveToast() {
+        if (toasts.length >= (container?.limit || 3)) {
+            toasts.map((toast, index) => {
+                if (index === 0) {
+                    toast.status = 'exiting';
+                }
+            });
+
+            const removeToast = () => setToasts((prev) => prev.slice(1));
+            animation(removeToast, 500);
+        }
+    }
+
+    // 시간 지나면 자동삭제
+    function AutoRemoveToast(id: number, options: ToastProps) {
+        const removeAnimation = () =>
+            setToasts((prev) =>
+                prev.map((toast) => (toast.id === id ? { ...toast, status: 'exiting' } : toast)),
+            );
+        animation(removeAnimation, options.duration || 3000);
+
+        const removeToast = () => setToasts((prev) => prev.filter((toast) => toast.id !== id));
+        animation(removeToast, (options.duration || 3000) + 1000); // 애니메이션 끝난 후 제거해야해서 1000ms 추가
+    }
+
+    // 애니메이션
+    function animation(method: () => void, duration: number) {
+        setTimeout(method, duration);
+    }
+
+    // Container에서 toast 렌더링할 때 각 위치의 컨테이너에 토스트 스택으로 쌓아줌
+    function getToastPosition() {
+        const positionMap: Record<ToastPosition, ToastProps[]> = {
+            topRight: [],
+            topCenter: [],
+            topLeft: [],
+            bottomRight: [],
+            bottomCenter: [],
+            bottomLeft: [],
+        };
+
+        // 각 토스트를 key값에 맞는 위치에 추가
+        toasts.forEach((toast) => {
+            const position = toast.position || 'topCenter';
+            positionMap[position].push(toast);
+        });
+
+        return positionMap;
+    }
+
+    toast.info = createToastByType('info');
+    toast.success = createToastByType('success');
+    toast.error = createToastByType('error');
path: client/src/components/Toast/ToastProvider.tsx
user: login: ohsung0722
body: 아 넵넵 그럼 괜찮을 것 같아요!! 
부자연스럽진 않고 객체 관리 통일성 때문에 말씀드린건데 설명 들어보니  Object.assign은 좀 어색한 것 같습니다!
created_at: 2025-03-27T05:53:29Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-11T07:25:52Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Bot user detected.
> 
> To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Recruiting-Your-Club/ryc&utm_content=122):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE