From 7bdd10207adf0d7c47d4d5beec2212d4b970a3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AF=BC=ED=97=8C?= Date: Thu, 18 Jun 2026 13:12:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8(sonner)=C2=B7=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sonner 기반 토스트 래퍼(lib/toast): error/info/success/warning, 프로젝트 디자인 토큰으로 스타일 통일. Toaster 를 provider 에 마운트 - 공통 Modal 컴포넌트: 반투명 배경 + 배경 클릭/ESC 닫기 - core/Provider import 케이싱 정리(provider) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/package-lock.json | 11 +++++ frontend/package.json | 1 + frontend/src/components/Modal.tsx | 30 ++++++++++++ frontend/src/components/index.ts | 2 + frontend/src/core/provider.tsx | 8 +++- frontend/src/lib/index.ts | 1 + frontend/src/lib/toast.ts | 77 +++++++++++++++++++++++++++++++ frontend/src/main.tsx | 2 +- 8 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Modal.tsx create mode 100644 frontend/src/lib/toast.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1dc0fd..a62cc79 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "slate": "^0.124.1", "slate-history": "^0.113.1", "slate-react": "^0.124.2", + "sonner": "^2.0.7", "zustand": "^5.0.14" }, "devDependencies": { @@ -3260,6 +3261,16 @@ "slate-dom": ">=0.119.1" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 37a1496..d25a5a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "slate": "^0.124.1", "slate-history": "^0.113.1", "slate-react": "^0.124.2", + "sonner": "^2.0.7", "zustand": "^5.0.14" }, "devDependencies": { diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..9ace715 --- /dev/null +++ b/frontend/src/components/Modal.tsx @@ -0,0 +1,30 @@ +import { type ReactNode, useEffect } from 'react' + +export interface ModalProps { + children?: ReactNode + onClose: () => void +} + +// 공통 모달: 반투명 배경 + 배경 클릭/ESC 로 닫기. +export function Modal({ children, onClose }: ModalProps) { + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) onClose() + } + + useEffect(() => { + const handleEscKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleEscKey) + return () => document.removeEventListener('keydown', handleEscKey) + }, [onClose]) + + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 78e677c..ac75f31 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,5 +1,7 @@ export { Button } from '@/components/Button' export type { ButtonProps, ButtonVariant, ButtonSize } from '@/components/Button' export { Input } from '@/components/Input' +export { Modal } from '@/components/Modal' +export type { ModalProps } from '@/components/Modal' export { Logo } from '@/components/Logo' export type { LogoProps, LogoVariant } from '@/components/Logo' diff --git a/frontend/src/core/provider.tsx b/frontend/src/core/provider.tsx index 2749278..1419e65 100644 --- a/frontend/src/core/provider.tsx +++ b/frontend/src/core/provider.tsx @@ -1,5 +1,6 @@ import { type ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { Toaster } from 'sonner' const queryClient = new QueryClient({ defaultOptions: { @@ -12,5 +13,10 @@ const queryClient = new QueryClient({ }) export function Provider({ children }: { children: ReactNode }) { - return {children} + return ( + + {children} + + + ) } diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index af3e974..b8bb278 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -2,3 +2,4 @@ export { cn } from '@/lib/cn' export type { ClassValue } from '@/lib/cn' export { interactive } from '@/lib/interactive' +export { toast } from '@/lib/toast' diff --git a/frontend/src/lib/toast.ts b/frontend/src/lib/toast.ts new file mode 100644 index 0000000..a403f0f --- /dev/null +++ b/frontend/src/lib/toast.ts @@ -0,0 +1,77 @@ +// 전역 토스트 (sonner 래퍼). 프로젝트 디자인 토큰으로 스타일을 맞춘다. +// 는 core/provider.tsx 에 마운트되어 있어야 한다. +import { toast as sonnerToast, type ExternalToast } from 'sonner' + +type ToastOptions = ExternalToast + +const baseStyle = { + fontFamily: 'var(--font-sans)', + fontSize: '18px', + fontWeight: 600, + letterSpacing: '-0.28px', + borderRadius: '8px', + padding: '18px 24px', + backgroundColor: 'var(--neutral-00)', + color: 'var(--neutral-90)', + boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.08)', + minWidth: '440px', + wordBreak: 'keep-all' as const, + overflowWrap: 'break-word' as const, + whiteSpace: 'pre-wrap' as const, +} + +const errorStyle = { + ...baseStyle, + border: '1px solid var(--negative)', + color: 'var(--negative)', + backgroundColor: '#fdf2f3', +} + +const infoStyle = { + ...baseStyle, + border: '1px solid var(--info)', + color: 'var(--info)', + backgroundColor: '#eff5ff', +} + +const successStyle = { + ...baseStyle, + border: '1px solid var(--success)', + color: '#0f766e', + backgroundColor: '#f0fdf9', +} + +const warningStyle = { + ...baseStyle, + border: '1px solid var(--neutral-60)', + color: 'var(--neutral-70)', + backgroundColor: 'var(--neutral-10)', +} + +export const toast = { + error: (message: string, options?: ToastOptions) => + sonnerToast.error(message, { + ...options, + style: { ...errorStyle, ...options?.style }, + duration: options?.duration ?? 4000, + }), + info: (message: string, options?: ToastOptions) => + sonnerToast.info(message, { + ...options, + style: { ...infoStyle, ...options?.style }, + duration: options?.duration ?? 3000, + }), + success: (message: string, options?: ToastOptions) => + sonnerToast.success(message, { + ...options, + style: { ...successStyle, ...options?.style }, + duration: options?.duration ?? 3500, + }), + // 완료 안내: 체크 아이콘(success)에 중립 톤 스타일 + warning: (message: string, options?: ToastOptions) => + sonnerToast.success(message, { + ...options, + style: { ...warningStyle, ...options?.style }, + duration: options?.duration ?? 3500, + }), +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index af7833d..b0f2ed4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { Provider } from '@/core/Provider' +import { Provider } from '@/core/provider' import '@/index.css' import App from '@/App'