feat(frontend): 공통 토스트(sonner)·모달 컴포넌트 추가
- sonner 기반 토스트 래퍼(lib/toast): error/info/success/warning, 프로젝트 디자인 토큰으로 스타일 통일. Toaster 를 provider 에 마운트 - 공통 Modal 컴포넌트: 반투명 배경 + 배경 클릭/ESC 닫기 - core/Provider import 케이싱 정리(provider) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a867daa98f
commit
7bdd10207a
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"slate": "^0.124.1",
|
"slate": "^0.124.1",
|
||||||
"slate-history": "^0.113.1",
|
"slate-history": "^0.113.1",
|
||||||
"slate-react": "^0.124.2",
|
"slate-react": "^0.124.2",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -3260,6 +3261,16 @@
|
|||||||
"slate-dom": ">=0.119.1"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"slate": "^0.124.1",
|
"slate": "^0.124.1",
|
||||||
"slate-history": "^0.113.1",
|
"slate-history": "^0.113.1",
|
||||||
"slate-react": "^0.124.2",
|
"slate-react": "^0.124.2",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
30
frontend/src/components/Modal.tsx
Normal file
30
frontend/src/components/Modal.tsx
Normal file
@ -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<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(0,0,0,0.40)]"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
export { Button } from '@/components/Button'
|
export { Button } from '@/components/Button'
|
||||||
export type { ButtonProps, ButtonVariant, ButtonSize } from '@/components/Button'
|
export type { ButtonProps, ButtonVariant, ButtonSize } from '@/components/Button'
|
||||||
export { Input } from '@/components/Input'
|
export { Input } from '@/components/Input'
|
||||||
|
export { Modal } from '@/components/Modal'
|
||||||
|
export type { ModalProps } from '@/components/Modal'
|
||||||
export { Logo } from '@/components/Logo'
|
export { Logo } from '@/components/Logo'
|
||||||
export type { LogoProps, LogoVariant } from '@/components/Logo'
|
export type { LogoProps, LogoVariant } from '@/components/Logo'
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { Toaster } from 'sonner'
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@ -12,5 +13,10 @@ const queryClient = new QueryClient({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({ children }: { children: ReactNode }) {
|
export function Provider({ children }: { children: ReactNode }) {
|
||||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
<Toaster position="top-center" />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,3 +2,4 @@
|
|||||||
export { cn } from '@/lib/cn'
|
export { cn } from '@/lib/cn'
|
||||||
export type { ClassValue } from '@/lib/cn'
|
export type { ClassValue } from '@/lib/cn'
|
||||||
export { interactive } from '@/lib/interactive'
|
export { interactive } from '@/lib/interactive'
|
||||||
|
export { toast } from '@/lib/toast'
|
||||||
|
|||||||
77
frontend/src/lib/toast.ts
Normal file
77
frontend/src/lib/toast.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 전역 토스트 (sonner 래퍼). 프로젝트 디자인 토큰으로 스타일을 맞춘다.
|
||||||
|
// <Toaster /> 는 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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Provider } from '@/core/Provider'
|
import { Provider } from '@/core/provider'
|
||||||
import '@/index.css'
|
import '@/index.css'
|
||||||
import App from '@/App'
|
import App from '@/App'
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user