diff --git a/frontend/design-system.html b/frontend/design-system.html new file mode 100644 index 0000000..01e35ec --- /dev/null +++ b/frontend/design-system.html @@ -0,0 +1,12 @@ + + + + + + KCG AI Monitoring — Design System + + +
+ + + diff --git a/frontend/src/design-system/DesignSystemApp.css b/frontend/src/design-system/DesignSystemApp.css new file mode 100644 index 0000000..a8a8893 --- /dev/null +++ b/frontend/src/design-system/DesignSystemApp.css @@ -0,0 +1,138 @@ +/* 디자인 쇼케이스 전용 스타일 */ + +.ds-shell { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--background, #0b1220); + color: var(--foreground, #e2e8f0); +} + +.ds-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid rgb(51 65 85 / 0.5); + flex-shrink: 0; + background: var(--surface-overlay, rgb(15 23 42 / 0.6)); + backdrop-filter: blur(8px); +} + +.ds-body { + display: flex; + flex: 1; + min-height: 0; +} + +.ds-nav { + width: 220px; + flex-shrink: 0; + padding: 1rem 0.75rem; + border-right: 1px solid rgb(51 65 85 / 0.5); + overflow-y: auto; + position: sticky; + top: 0; +} + +.ds-main { + flex: 1; + overflow-y: auto; + padding: 2rem 2.5rem 6rem; + scroll-behavior: smooth; +} + +.ds-main section { + margin-bottom: 4rem; +} + +/* 추적 ID 시스템 */ +.trk-item { + position: relative; + transition: outline-color 0.2s; +} + +.trk-copyable { + cursor: pointer; +} + +.trk-copyable:hover { + outline: 1px dashed rgb(59 130 246 / 0.5); + outline-offset: 4px; +} + +.trk-active { + outline: 2px solid rgb(59 130 246); + outline-offset: 4px; + animation: trk-pulse 1.2s ease-out; +} + +.trk-item[data-copied='true'] { + outline: 2px solid rgb(34 197 94) !important; + outline-offset: 4px; +} + +.trk-item[data-copied='true']::after { + content: '복사됨 ✓'; + position: absolute; + top: -1.5rem; + left: 0; + font-size: 0.625rem; + color: rgb(34 197 94); + background: rgb(34 197 94 / 0.15); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + pointer-events: none; + z-index: 10; +} + +@keyframes trk-pulse { + 0% { + outline-color: rgb(59 130 246); + } + 50% { + outline-color: rgb(59 130 246 / 0.3); + } + 100% { + outline-color: rgb(59 130 246); + } +} + +/* 쇼케이스 그리드 */ +.ds-grid { + display: grid; + gap: 0.75rem; +} + +.ds-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.ds-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.ds-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.ds-grid-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); } + +.ds-sample { + padding: 1rem; + background: var(--surface-raised, rgb(30 41 59 / 0.4)); + border: 1px solid rgb(51 65 85 / 0.4); + border-radius: 0.5rem; +} + +.ds-sample-label { + font-size: 0.625rem; + color: var(--text-hint, rgb(148 163 184)); + font-family: ui-monospace, monospace; + margin-top: 0.5rem; + word-break: break-all; +} + +.ds-code { + display: block; + padding: 0.75rem 1rem; + background: rgb(15 23 42 / 0.7); + border: 1px solid rgb(51 65 85 / 0.4); + border-radius: 0.375rem; + font-family: ui-monospace, monospace; + font-size: 0.75rem; + color: rgb(203 213 225); + white-space: pre; + overflow-x: auto; +} diff --git a/frontend/src/design-system/DesignSystemApp.tsx b/frontend/src/design-system/DesignSystemApp.tsx new file mode 100644 index 0000000..edb916a --- /dev/null +++ b/frontend/src/design-system/DesignSystemApp.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from 'react'; +import { TrkProvider, useTrk } from './lib/TrkContext'; +import { IntroSection } from './sections/IntroSection'; +import { TokenSection } from './sections/TokenSection'; +import { TypographySection } from './sections/TypographySection'; +import { BadgeSection } from './sections/BadgeSection'; +import { ButtonSection } from './sections/ButtonSection'; +import { FormSection } from './sections/FormSection'; +import { CardSectionShowcase } from './sections/CardSection'; +import { LayoutSection } from './sections/LayoutSection'; +import { CatalogSection } from './sections/CatalogSection'; +import { GuideSection } from './sections/GuideSection'; +import './DesignSystemApp.css'; + +interface NavItem { + id: string; + label: string; + anchor: string; +} + +const NAV_ITEMS: NavItem[] = [ + { id: 'intro', label: '1. 소개', anchor: 'TRK-SEC-intro' }, + { id: 'token', label: '2. 테마 · 토큰', anchor: 'TRK-SEC-token' }, + { id: 'typography', label: '3. 타이포그래피', anchor: 'TRK-SEC-typography' }, + { id: 'badge', label: '4. Badge', anchor: 'TRK-SEC-badge' }, + { id: 'button', label: '5. Button', anchor: 'TRK-SEC-button' }, + { id: 'form', label: '6. Form', anchor: 'TRK-SEC-form' }, + { id: 'card', label: '7. Card / Section', anchor: 'TRK-SEC-card' }, + { id: 'layout', label: '8. Layout', anchor: 'TRK-SEC-layout' }, + { id: 'catalog', label: '9. 분류 카탈로그', anchor: 'TRK-SEC-catalog' }, + { id: 'guide', label: '10. 예외 / 가이드', anchor: 'TRK-SEC-guide' }, +]; + +function DesignSystemShell() { + const { copyMode, setCopyMode } = useTrk(); + const [theme, setTheme] = useState<'dark' | 'light'>('dark'); + const [activeNav, setActiveNav] = useState('intro'); + + // 테마 토글 + useEffect(() => { + const root = document.documentElement; + root.classList.remove('dark', 'light'); + root.classList.add(theme); + }, [theme]); + + // 스크롤 감지로 현재 네비 하이라이트 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + const id = entry.target.getAttribute('data-section-id'); + if (id) setActiveNav(id); + } + } + }, + { rootMargin: '-40% 0px -55% 0px', threshold: 0 }, + ); + const sections = document.querySelectorAll('[data-section-id]'); + sections.forEach((s) => observer.observe(s)); + return () => observer.disconnect(); + }, []); + + const scrollTo = (anchor: string) => { + const el = document.querySelector(`[data-trk="${anchor}"]`); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + + return ( +
+ {/* 고정 헤더 */} +
+
+

KCG Design System

+ v0.1.0 · 쇼케이스 +
+
+ + +
+
+ +
+ {/* 좌측 네비 */} + + + {/* 우측 컨텐츠 */} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ ); +} + +export function DesignSystemApp() { + return ( + + + + ); +} diff --git a/frontend/src/design-system/lib/Trk.tsx b/frontend/src/design-system/lib/Trk.tsx new file mode 100644 index 0000000..0d3e2a1 --- /dev/null +++ b/frontend/src/design-system/lib/Trk.tsx @@ -0,0 +1,71 @@ +import { type ReactNode, type CSSProperties, type MouseEvent } from 'react'; +import { useTrk } from './TrkContext'; + +/** + * 쇼케이스 추적 ID 시스템 + * 각 쇼케이스 항목에 고유 ID를 부여하여: + * 1. 호버 시 툴팁으로 ID 노출 + * 2. "ID 복사 모드"에서 클릭 시 클립보드 복사 + * 3. URL hash 딥링크 (#trk=TRK-BADGE-critical-sm) 지원 + */ +interface TrkProps { + id: string; + children: ReactNode; + className?: string; + style?: CSSProperties; + /** 인라인 요소로 렌더할지 여부 (기본: block) */ + inline?: boolean; +} + +export function Trk({ id, children, className = '', style, inline = false }: TrkProps) { + const { copyMode, activeId } = useTrk(); + const isActive = activeId === id; + + const handleClick = async (e: MouseEvent) => { + if (!copyMode) return; + e.preventDefault(); + e.stopPropagation(); + try { + await navigator.clipboard.writeText(id); + const el = e.currentTarget as HTMLElement; + el.dataset.copied = 'true'; + setTimeout(() => delete el.dataset.copied, 800); + } catch { + // clipboard API 미지원 시 무시 + } + }; + + const Wrapper = inline ? 'span' : 'div'; + + return ( + + {children} + + ); +} + +export function TrkSectionHeader({ + id, + title, + description, +}: { + id: string; + title: string; + description?: string; +}) { + return ( +
+
+

{title}

+ {id} +
+ {description &&

{description}

} +
+ ); +} diff --git a/frontend/src/design-system/lib/TrkContext.tsx b/frontend/src/design-system/lib/TrkContext.tsx new file mode 100644 index 0000000..a30d305 --- /dev/null +++ b/frontend/src/design-system/lib/TrkContext.tsx @@ -0,0 +1,50 @@ +import { useContext, createContext, useState, useEffect, type ReactNode } from 'react'; + +interface TrkContextValue { + copyMode: boolean; + setCopyMode: (v: boolean) => void; + activeId: string | null; + setActiveId: (id: string | null) => void; +} + +// eslint-disable-next-line react-refresh/only-export-components +export const TrkContext = createContext(null); + +export function TrkProvider({ children }: { children: ReactNode }) { + const [copyMode, setCopyMode] = useState(false); + const [activeId, setActiveId] = useState(null); + + // 딥링크 처리: #trk=TRK-BADGE-critical-sm + useEffect(() => { + const applyHash = () => { + const hash = window.location.hash; + const match = hash.match(/#trk=([\w-]+)/); + if (match) { + const id = match[1]; + setActiveId(id); + setTimeout(() => { + const el = document.querySelector(`[data-trk="${id}"]`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 50); + } + }; + applyHash(); + window.addEventListener('hashchange', applyHash); + return () => window.removeEventListener('hashchange', applyHash); + }, []); + + return ( + + {children} + + ); +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useTrk() { + const ctx = useContext(TrkContext); + if (!ctx) throw new Error('useTrk must be used within TrkProvider'); + return ctx; +} diff --git a/frontend/src/design-system/sections/BadgeSection.tsx b/frontend/src/design-system/sections/BadgeSection.tsx new file mode 100644 index 0000000..2af48bc --- /dev/null +++ b/frontend/src/design-system/sections/BadgeSection.tsx @@ -0,0 +1,114 @@ +import { TrkSectionHeader, Trk } from '../lib/Trk'; +import { Badge } from '@shared/components/ui/badge'; +import type { BadgeIntent, BadgeSize } from '@lib/theme/variants'; + +const INTENTS: BadgeIntent[] = ['critical', 'high', 'warning', 'info', 'success', 'muted', 'purple', 'cyan']; +const SIZES: BadgeSize[] = ['xs', 'sm', 'md', 'lg']; + +const INTENT_USAGE: Record = { + critical: '심각 · 긴급 · 위험 (빨강 계열)', + high: '높음 · 경고 (주황 계열)', + warning: '주의 · 보류 (노랑 계열)', + info: '일반 · 정보 (파랑 계열, 기본값)', + success: '성공 · 완료 · 정상 (초록 계열)', + muted: '비활성 · 중립 · 기타 (회색 계열)', + purple: '분석 · AI · 특수 (보라 계열)', + cyan: '모니터링 · 스트림 (청록 계열)', +}; + +export function BadgeSection() { + return ( + <> + + + {/* 32 변형 그리드 */} +

32 변형 매트릭스

+ +
+ + + + + {SIZES.map((size) => ( + + ))} + + + + {INTENTS.map((intent) => ( + + + {SIZES.map((size) => ( + + ))} + + ))} + +
intent ↓ / size → + {size} +
{intent} + + + {intent.toUpperCase()} + + +
+
+
+ + {/* intent별 의미 가이드 */} +

Intent 의미 가이드

+
+ {INTENTS.map((intent) => ( + +
+ + {intent.toUpperCase()} + + {INTENT_USAGE[intent]} +
+
+ ))} +
+ + {/* 사용 예시 코드 */} +

사용 예시

+ + + {`import { Badge } from '@shared/components/ui/badge'; +import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels'; + +// 카탈로그 API와 결합 + + {getAlertLevelLabel('CRITICAL', t, lang)} + + +// className override (tailwind-merge가 같은 그룹 충돌 감지) + + 커스텀 둥근 배지 +`} + + + + {/* 금지 패턴 */} +

금지 패턴

+ +
❌ 아래 패턴은 사용하지 마세요
+ + {`// ❌ className 직접 작성 (intent prop 무시) +... + +// ❌ !important 사용 +... + +// ❌
→ Badge 컴포넌트 사용 필수 +
위험
`} +
+ + + ); +} diff --git a/frontend/src/design-system/sections/ButtonSection.tsx b/frontend/src/design-system/sections/ButtonSection.tsx new file mode 100644 index 0000000..21233bf --- /dev/null +++ b/frontend/src/design-system/sections/ButtonSection.tsx @@ -0,0 +1,131 @@ +import { TrkSectionHeader, Trk } from '../lib/Trk'; +import { Button } from '@shared/components/ui/button'; +import type { ButtonVariant, ButtonSize } from '@lib/theme/variants'; +import { Plus, Download, Trash2, Search, Save } from 'lucide-react'; + +const VARIANTS: ButtonVariant[] = ['primary', 'secondary', 'ghost', 'outline', 'destructive']; +const SIZES: ButtonSize[] = ['sm', 'md', 'lg']; + +const VARIANT_USAGE: Record = { + primary: '주요 액션 · 기본 CTA (페이지당 1개 권장)', + secondary: '보조 액션 · 툴바 버튼 (기본값)', + ghost: '고요한 액션 · 리스트 행 내부', + outline: '강조 보조 · 필터 활성화 상태', + destructive: '삭제 · 비활성화 등 위험 액션', +}; + +export function ButtonSection() { + return ( + <> + + + {/* 매트릭스 */} +

15 변형 매트릭스

+ +
+ + + + + {SIZES.map((size) => ( + + ))} + + + + {VARIANTS.map((variant) => ( + + + {SIZES.map((size) => ( + + ))} + + ))} + +
variant ↓ / size → + {size} +
{variant} + + + +
+
+
+ + {/* 아이콘 버튼 */} +

아이콘 포함 패턴

+ +
+ + + + + +
+
+ + {/* 상태 */} +

상태

+ +
+ + +
+
+ + {/* variant 의미 */} +

Variant 의미 가이드

+
+ {VARIANTS.map((variant) => ( + +
+ + {VARIANT_USAGE[variant]} +
+
+ ))} +
+ + {/* 사용 예시 */} +

사용 예시

+ + + {`import { Button } from '@shared/components/ui/button'; +import { Plus } from 'lucide-react'; + + + + + +// 금지 +// ❌