diff --git a/src/components/checkbox/constants/constants.ts b/src/components/checkbox/constants/constants.ts
new file mode 100644
index 0000000..a6d999c
--- /dev/null
+++ b/src/components/checkbox/constants/constants.ts
@@ -0,0 +1,20 @@
+import checkedLarge from '@/assets/icons/check/checkedLarge.svg';
+import checkedSmall from '@/assets/icons/check/checkedSmall.svg';
+import nonCheckedLarge from '@/assets/icons/check/nonCheckedLarge.svg';
+import nonCheckedSmall from '@/assets/icons/check/nonCheckedSmall.svg';
+
+export const CHECKBOX_ICON = {
+ checked: {
+ large: checkedLarge,
+ small: checkedSmall,
+ },
+ unchecked: {
+ large: nonCheckedLarge,
+ small: nonCheckedSmall,
+ },
+} as const;
+
+export const ICON_SIZE = {
+ large: 18,
+ small: 16,
+} as const;
diff --git a/src/components/toast/Toast.tsx b/src/components/toast/Toast.tsx
new file mode 100644
index 0000000..c0c67a6
--- /dev/null
+++ b/src/components/toast/Toast.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import clsx from 'clsx';
+
+import useToastLifecycle from './hooks/useToastLifecycle';
+import styles from './styles/Toast.module.css';
+import type { ToastProps } from './types/types';
+
+const DEFAULT_AUTO_DISMISS_MS = 3000;
+const DEFAULT_ANIMATION_MS = 600;
+
+export default function Toast({
+ message = '저장하지 않은 변경사항이 있어요!',
+ actionLabel = '변경사항 저장하기',
+ isOpen = true,
+ autoDismissMs = DEFAULT_AUTO_DISMISS_MS,
+ enterDurationMs = DEFAULT_ANIMATION_MS,
+ exitDurationMs = DEFAULT_ANIMATION_MS,
+ className,
+ actionClassName,
+ onAction,
+ onDismiss,
+}: ToastProps) {
+ const { isRendered, isClosing } = useToastLifecycle({
+ isOpen,
+ autoDismissMs,
+ exitDurationMs,
+ onDismiss,
+ });
+
+ return isRendered ? (
+
+
+
+
+
+ {message}
+
+ {actionLabel ? (
+
+ ) : null}
+
+ ) : null;
+}
diff --git a/src/components/toast/hooks/useToastLifecycle.ts b/src/components/toast/hooks/useToastLifecycle.ts
new file mode 100644
index 0000000..a479fc2
--- /dev/null
+++ b/src/components/toast/hooks/useToastLifecycle.ts
@@ -0,0 +1,83 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+interface UseToastLifecycleOptions {
+ isOpen: boolean;
+ autoDismissMs: number;
+ exitDurationMs: number;
+ onDismiss?: () => void;
+}
+
+export default function useToastLifecycle({
+ isOpen,
+ autoDismissMs,
+ exitDurationMs,
+ onDismiss,
+}: UseToastLifecycleOptions) {
+ const [isRendered, setIsRendered] = useState(isOpen);
+ const [isClosing, setIsClosing] = useState(false);
+ const autoTimerRef = useRef(null);
+ const exitTimerRef = useRef(null);
+ const prevIsOpenRef = useRef(isOpen);
+
+ const clearTimers = useCallback(() => {
+ if (autoTimerRef.current) {
+ window.clearTimeout(autoTimerRef.current);
+ autoTimerRef.current = null;
+ }
+ if (exitTimerRef.current) {
+ window.clearTimeout(exitTimerRef.current);
+ exitTimerRef.current = null;
+ }
+ }, []);
+
+ const startDismiss = useCallback(() => {
+ if (exitTimerRef.current) return;
+ setIsClosing(true);
+ exitTimerRef.current = window.setTimeout(() => {
+ setIsRendered(false);
+ setIsClosing(false);
+ exitTimerRef.current = null;
+ onDismiss?.();
+ }, exitDurationMs);
+ }, [exitDurationMs, onDismiss]);
+
+ useEffect(() => {
+ let rafId: number | null = null;
+ const wasOpen = prevIsOpenRef.current;
+
+ if (isOpen) {
+ prevIsOpenRef.current = true;
+ const isOpening = !wasOpen;
+ if (isOpening) {
+ clearTimers();
+ rafId = window.requestAnimationFrame(() => {
+ setIsRendered(true);
+ setIsClosing(false);
+ });
+ }
+
+ if (isRendered && !isClosing && autoDismissMs > 0) {
+ clearTimers();
+ autoTimerRef.current = window.setTimeout(startDismiss, autoDismissMs);
+ }
+
+ return () => {
+ if (rafId !== null) window.cancelAnimationFrame(rafId);
+ };
+ }
+
+ if (isRendered && !isClosing) {
+ clearTimers();
+ rafId = window.requestAnimationFrame(() => startDismiss());
+ }
+ prevIsOpenRef.current = false;
+
+ return () => {
+ if (rafId !== null) window.cancelAnimationFrame(rafId);
+ };
+ }, [autoDismissMs, clearTimers, isClosing, isOpen, isRendered, startDismiss]);
+
+ useEffect(() => () => clearTimers(), [clearTimers]);
+
+ return { isRendered, isClosing };
+}
diff --git a/src/components/toast/styles/Toast.module.css b/src/components/toast/styles/Toast.module.css
new file mode 100644
index 0000000..fdcf741
--- /dev/null
+++ b/src/components/toast/styles/Toast.module.css
@@ -0,0 +1,116 @@
+.toast {
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ width: 868px;
+ height: 57px;
+ padding: 12px 12px 12px 24px;
+ border-radius: 16px;
+ box-sizing: border-box;
+ background: var(--Color-Brand-Primary, #5189fa);
+ color: #ffffff;
+}
+
+.content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ flex: 0 0 auto;
+}
+
+.message {
+ font-size: 14px;
+ line-height: 1.4;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.action {
+ flex: 0 0 auto;
+ border: none;
+ border-radius: 12px;
+ padding: 8px 12px;
+ background: #ffffff;
+ color: #3f6fe5;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.action:focus-visible {
+ outline: 2px solid #ffffff;
+ outline-offset: 2px;
+}
+
+.enter {
+ animation: toast-pop-in 600ms ease-out;
+}
+
+.exit {
+ animation: toast-fade-out 600ms ease-in forwards;
+}
+
+@keyframes toast-pop-in {
+ 0% {
+ opacity: 0;
+ transform: translateY(-8px) scale(0.96);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@keyframes toast-fade-out {
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(-6px) scale(0.98);
+ }
+}
+
+@media (max-width: 768px) {
+ .toast {
+ top: 97px;
+ left: 20px;
+ width: 343px;
+ height: 49px;
+ padding: 8px 8px 8px 12px;
+ gap: 8px;
+ }
+
+ .message {
+ font-size: 13px;
+ }
+
+ .action {
+ padding: 6px 10px;
+ font-size: 12px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .enter,
+ .exit {
+ animation: none;
+ }
+}
diff --git a/src/components/toast/types/types.ts b/src/components/toast/types/types.ts
new file mode 100644
index 0000000..bfe1bad
--- /dev/null
+++ b/src/components/toast/types/types.ts
@@ -0,0 +1,14 @@
+import type { ReactNode } from 'react';
+
+export interface ToastProps {
+ message?: ReactNode;
+ actionLabel?: ReactNode;
+ isOpen?: boolean;
+ autoDismissMs?: number;
+ enterDurationMs?: number;
+ exitDurationMs?: number;
+ className?: string;
+ actionClassName?: string;
+ onAction?: () => void;
+ onDismiss?: () => void;
+}