Initial commit: HMDesignSystem baseline

This commit is contained in:
shkim4 committed 2026-10-08 16:58:09 +09:00
commit b6a0a61103
486 files changed
+68432

No files matched your search

@@ -0,0 +1,99 @@
# Component / Pattern Reference Index
AI 화면 구현 Quick Start의 Component·Pattern 검색에 사용하는 파일 인덱스다. 오전 `HMDesignTest.zip`과 현재 개발 저장소는 별도 스냅샷이다. 현재 검증된 최신 테스트 배포 기준 `HMDesignTest-Portable-Test-r3.zip`에는 아래 44개 Component와 8개 Pattern 모듈, Dashboard Status 소스/export/CSS/문서/정적 예제와 사전 빌드된 Storybook을 모두 포함한다. `HMDesignTest-Portable-Test-r2.zip`, 기존 `HMDesignTest-Portable-Test.zip` 및 오전 ZIP은 이전 스냅샷이며, 아래 오전 ZIP 수량은 역사적 비교로 표시한다. 공개 심볼의 Source of Truth는 [`packages/ui/src/index.ts`](../../packages/ui/src/index.ts)이며, Props와 상태는 각 연결 문서 및 구현을 확인한다. 파일 수, 공개 API 심볼 수, Story 그룹 수는 서로 다른 집계 단위다.
## Component
현재 `packages/ui/src/components/ui/`에는 구현 TSX 44개가 있다. 각 구현 파일은 `packages/ui/src/index.ts`의 공개 export를 통해 소비한다. 아래 Story는 실제 Story 파일의 대표 그룹이다.
| Component | 구현·공개 API (`@hm-design/ui`) | 사용 문서 | 대표 Story |
|---|---|---|---|
| Accordion | `Accordion`, `AccordionContent`, `AccordionItem`, `AccordionTrigger`<br>[구현](../../packages/ui/src/components/ui/accordion.tsx) | [문서](../components/accordion.md) | [Disclosure/Accordion](../../src/components/accordion.stories.tsx) |
| Alert | `Alert`, `AlertDescription`, `AlertTitle`<br>[구현](../../packages/ui/src/components/ui/alert.tsx) | [문서](../components/alert.md) | [Feedback/Alert](../../src/components/alert.stories.tsx) |
| Alert Dialog | `AlertDialog`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogContent`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogHeader`, `AlertDialogOverlay`, `AlertDialogPortal`, `AlertDialogTitle`, `AlertDialogTrigger`<br>[구현](../../packages/ui/src/components/ui/alert-dialog.tsx) | [문서](../components/alert-dialog.md) | [Overlay/Alert Dialog](../../src/components/alert-dialog.stories.tsx) |
| Aspect Ratio | `AspectRatio`<br>[구현](../../packages/ui/src/components/ui/aspect-ratio.tsx) | [문서](../components/aspect-ratio.md) | [Data & Display/Aspect Ratio](../../src/components/aspect-ratio.stories.tsx) |
| Avatar | `Avatar`, `AvatarFallback`, `AvatarImage`<br>[구현](../../packages/ui/src/components/ui/avatar.tsx) | [문서](../components/avatar.md) | [Data & Display/Avatar](../../src/components/avatar.stories.tsx) |
| Badge | `Badge`, `badgeVariants`<br>[구현](../../packages/ui/src/components/ui/badge.tsx) | [문서](../components/badge.md) | [Data & Display/Badge](../../src/components/badge.stories.tsx) |
| Breadcrumb | `Breadcrumb`, `BreadcrumbEllipsis`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbList`, `BreadcrumbPage`, `BreadcrumbSeparator`<br>[구현](../../packages/ui/src/components/ui/breadcrumb.tsx) | [문서](../components/breadcrumb.md) | [Navigation/Breadcrumb](../../src/components/breadcrumb.stories.tsx) |
| Button | `Button`, `buttonVariants`, `ButtonAppearance`<br>[구현](../../packages/ui/src/components/ui/button.tsx) | [문서](../components/button.md) | [Form/Button](../../src/components/button.stories.tsx) |
| Calendar | `Calendar`<br>[구현](../../packages/ui/src/components/ui/calendar.tsx) | [문서](../components/calendar.md) | [Search & Date/Calendar](../../src/components/calendar.stories.tsx) |
| Card | `Card`, `CardAction`, `CardContent`, `CardDescription`, `CardFooter`, `CardHeader`, `CardTitle`, `CardSurface`<br>[구현](../../packages/ui/src/components/ui/card.tsx) | [문서](../components/card.md) | [Data & Display/Card](../../src/components/card.stories.tsx) |
| Carousel | `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselNext`, `CarouselPrevious`, `useCarousel`, `CarouselApi`<br>[구현](../../packages/ui/src/components/ui/carousel.tsx) | [문서](../components/carousel.md) | [Data & Display/Carousel](../../src/components/carousel.stories.tsx) |
| Checkbox | `Checkbox`<br>[구현](../../packages/ui/src/components/ui/checkbox.tsx) | [문서](../components/checkbox.md) | [Form/Checkbox](../../src/components/checkbox.stories.tsx) |
| Collapsible | `Collapsible`, `CollapsibleContent`, `CollapsibleTrigger`<br>[구현](../../packages/ui/src/components/ui/collapsible.tsx) | [문서](../components/collapsible.md) | [Disclosure/Collapsible](../../src/components/collapsible.stories.tsx) |
| Combobox | `Combobox`, `ComboboxOption`, `ComboboxSize`<br>[구현](../../packages/ui/src/components/ui/combobox.tsx) | [문서](../components/combobox.md) | [Search & Date/Combobox](../../src/components/combobox.stories.tsx) |
| Command | `Command`, `CommandEmpty`, `CommandGroup`, `CommandInput`, `CommandItem`, `CommandList`, `CommandSeparator`, `CommandShortcut`<br>[구현](../../packages/ui/src/components/ui/command.tsx) | [문서](../components/command.md) | [Search & Date/Command](../../src/components/command.stories.tsx) |
| Context Menu | `ContextMenu`, `ContextMenuCheckboxItem`, `ContextMenuContent`, `ContextMenuGroup`, `ContextMenuItem`, `ContextMenuLabel`, `ContextMenuPortal`, `ContextMenuRadioGroup`, `ContextMenuRadioItem`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuSub`, `ContextMenuSubContent`, `ContextMenuSubTrigger`, `ContextMenuTrigger`<br>[구현](../../packages/ui/src/components/ui/context-menu.tsx) | [문서](../components/context-menu.md) | [Overlay/Context Menu](../../src/components/context-menu.stories.tsx) |
| Date Picker | `DatePicker`, `DatePickerSize`<br>[구현](../../packages/ui/src/components/ui/date-picker.tsx) | [문서](../components/date-picker.md) | [Search & Date/Date Picker](../../src/components/date-picker.stories.tsx) |
| Dialog | `Dialog`, `DialogBody`, `DialogClose`, `DialogContent`, `DialogDescription`, `DialogFooter`, `DialogHeader`, `DialogOverlay`, `DialogPortal`, `DialogTitle`, `DialogTrigger`<br>[구현](../../packages/ui/src/components/ui/dialog.tsx) | [문서](../components/dialog.md) | [Overlay/Dialog](../../src/components/dialog.stories.tsx) |
| Drawer | `Drawer`, `DrawerBody`, `DrawerClose`, `DrawerContent`, `DrawerDescription`, `DrawerFooter`, `DrawerHeader`, `DrawerOverlay`, `DrawerPortal`, `DrawerTitle`, `DrawerTrigger`<br>[구현](../../packages/ui/src/components/ui/drawer.tsx) | [문서](../components/drawer.md) | [Overlay/Drawer](../../src/components/drawer.stories.tsx) |
| Dropdown Menu | `DropdownMenu`, `DropdownMenuCheckboxItem`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuPortal`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuSub`, `DropdownMenuSubContent`, `DropdownMenuSubTrigger`, `DropdownMenuTrigger`<br>[구현](../../packages/ui/src/components/ui/dropdown-menu.tsx) | [문서](../components/dropdown-menu.md) | [Overlay/Dropdown Menu](../../src/components/dropdown-menu.stories.tsx) |
| Hover Card | `HoverCard`, `HoverCardContent`, `HoverCardTrigger`<br>[구현](../../packages/ui/src/components/ui/hover-card.tsx) | [문서](../components/hover-card.md) | [Overlay/Hover Card](../../src/components/hover-card.stories.tsx) |
| Input | `Input`, `InputSize`<br>[구현](../../packages/ui/src/components/ui/input.tsx) | [문서](../components/input.md) | [Form/Input](../../src/components/input.stories.tsx) |
| Label | `Label`<br>[구현](../../packages/ui/src/components/ui/label.tsx) | [문서](../components/label.md) | [Form/Label](../../src/components/label.stories.tsx) |
| Menubar | `Menubar`, `MenubarCheckboxItem`, `MenubarContent`, `MenubarGroup`, `MenubarItem`, `MenubarLabel`, `MenubarMenu`, `MenubarPortal`, `MenubarRadioGroup`, `MenubarRadioItem`, `MenubarSeparator`, `MenubarShortcut`, `MenubarSub`, `MenubarSubContent`, `MenubarSubTrigger`, `MenubarTrigger`<br>[구현](../../packages/ui/src/components/ui/menubar.tsx) | [문서](../components/menubar.md) | [Navigation/Menubar](../../src/components/menubar.stories.tsx) |
| Navigation Menu | `NavigationMenu`, `NavigationMenuContent`, `NavigationMenuIndicator`, `NavigationMenuItem`, `NavigationMenuLink`, `NavigationMenuList`, `NavigationMenuTrigger`, `NavigationMenuViewport`<br>[구현](../../packages/ui/src/components/ui/navigation-menu.tsx) | [문서](../components/navigation-menu.md) | [Navigation/Navigation Menu](../../src/components/navigation-menu.stories.tsx) |
| Pagination | `Pagination`, `PaginationContent`, `PaginationEllipsis`, `PaginationItem`, `PaginationLink`, `PaginationNext`, `PaginationPrevious`<br>[구현](../../packages/ui/src/components/ui/pagination.tsx) | [문서](../components/pagination.md) | [Navigation/Pagination](../../src/components/pagination.stories.tsx) |
| Popover | `Popover`, `PopoverAnchor`, `PopoverClose`, `PopoverContent`, `PopoverTrigger`<br>[구현](../../packages/ui/src/components/ui/popover.tsx) | [문서](../components/popover.md) | [Overlay/Popover](../../src/components/popover.stories.tsx) |
| Progress | `Progress`<br>[구현](../../packages/ui/src/components/ui/progress.tsx) | [문서](../components/progress.md) | [Data & Display/Progress](../../src/components/progress.stories.tsx) |
| Radio Group | `RadioGroup`, `RadioGroupItem`<br>[구현](../../packages/ui/src/components/ui/radio-group.tsx) | [문서](../components/radio-group.md) | [Form/Radio Group](../../src/components/radio-group.stories.tsx) |
| Resizable | `ResizableHandle`, `ResizablePanel`, `ResizablePanelGroup`<br>[구현](../../packages/ui/src/components/ui/resizable.tsx) | [문서](../components/resizable.md) | [Disclosure/Resizable](../../src/components/resizable.stories.tsx) |
| Scroll Area | `ScrollArea`, `ScrollBar`<br>[구현](../../packages/ui/src/components/ui/scroll-area.tsx) | [문서](../components/scroll-area.md) | [Data & Display/Scroll Area](../../src/components/scroll-area.stories.tsx) |
| Select | `Select`, `SelectContent`, `SelectGroup`, `SelectItem`, `SelectLabel`, `SelectScrollDownButton`, `SelectScrollUpButton`, `SelectSeparator`, `SelectTrigger`, `SelectValue`, `SelectSize`<br>[구현](../../packages/ui/src/components/ui/select.tsx) | [문서](../components/select.md) | [Form/Select](../../src/components/select.stories.tsx) |
| Separator | `Separator`<br>[구현](../../packages/ui/src/components/ui/separator.tsx) | [문서](../components/separator.md) | [Data & Display/Separator](../../src/components/separator.stories.tsx) |
| Sheet | `Sheet`, `SheetBody`, `SheetClose`, `SheetContent`, `SheetDescription`, `SheetFooter`, `SheetHeader`, `SheetOverlay`, `SheetPortal`, `SheetTitle`, `SheetTrigger`<br>[구현](../../packages/ui/src/components/ui/sheet.tsx) | [문서](../components/sheet.md) | [Overlay/Sheet](../../src/components/sheet.stories.tsx) |
| Skeleton | `Skeleton`<br>[구현](../../packages/ui/src/components/ui/skeleton.tsx) | [문서](../components/skeleton.md) | [Data & Display/Skeleton](../../src/components/skeleton.stories.tsx) |
| Slider | `Slider`<br>[구현](../../packages/ui/src/components/ui/slider.tsx) | [문서](../components/slider.md) | [Form/Slider](../../src/components/slider.stories.tsx) |
| Sonner | `Toaster`, `toast`, `ExternalToast`, `ToasterProps`<br>[구현](../../packages/ui/src/components/ui/sonner.tsx) | [문서](../components/sonner.md) | [Feedback/Sonner](../../src/components/sonner.stories.tsx) |
| Switch | `Switch`<br>[구현](../../packages/ui/src/components/ui/switch.tsx) | [문서](../components/switch.md) | [Form/Switch](../../src/components/switch.stories.tsx) |
| Table | `Table`, `TableBody`, `TableCaption`, `TableCell`, `TableFooter`, `TableHead`, `TableHeader`, `TableRow`<br>[구현](../../packages/ui/src/components/ui/table.tsx) | [문서](../components/table.md) | [Data & Display/Table](../../src/components/table.stories.tsx) |
| Tabs | `Tabs`, `TabsContent`, `TabsList`, `TabsTrigger`, `TabsPanelSurface`<br>[구현](../../packages/ui/src/components/ui/tabs.tsx)<br>기본 `<TabsList>` = Primary (`fill=true`, `panelSurface="content"`). Secondary: `fill panelSurface="surface"`. Legacy: `fill={false}` (panelSurface 시각 무시). 패널은 Primary `bg-background`, Secondary / Legacy `bg-card`로 조합한다 | [문서·기본값 변경 영향](../components/tabs.md) | [Navigation/Tabs](../../src/components/tabs.stories.tsx) |
| Textarea | `Textarea`, `TextareaResize`<br>[구현](../../packages/ui/src/components/ui/textarea.tsx) | [문서](../components/textarea.md) | [Form/Textarea](../../src/components/textarea.stories.tsx) |
| Toggle | `Toggle`, `toggleVariants`<br>[구현](../../packages/ui/src/components/ui/toggle.tsx) | [문서](../components/toggle.md) | [Form/Toggle](../../src/components/toggle.stories.tsx) |
| Toggle Group | `ToggleGroup`, `ToggleGroupItem`<br>[구현](../../packages/ui/src/components/ui/toggle-group.tsx) | [문서](../components/toggle-group.md) | [Form/Toggle Group](../../src/components/toggle-group.stories.tsx) |
| Tooltip | `Tooltip`, `TooltipContent`, `TooltipProvider`, `TooltipTrigger`<br>[구현](../../packages/ui/src/components/ui/tooltip.tsx) | [문서](../components/tooltip.md) | [Overlay/Tooltip](../../src/components/tooltip.stories.tsx) |
`packages/ui/src/index.ts`에는 위 44개 Component 모듈의 named export 222개(값과 type 포함)가 연결되어 있다. 사용 시 API·Props·Variant·주요 상태·Theme 연결은 해당 Component 문서의 API/상태/Token/Storybook 절을 따른다. 문서 45개 중 `field.md`는 별도 `field.tsx` 공개 Component를 나타내지 않는 Field 소비 가이드다.
## Pattern
현재 개발 저장소와 포터블 r3의 `packages/ui/src/patterns/`에는 8개 디렉터리와 62개 named export(값과 type 포함)가 있다. 역사적 오전 배포 기준 `HMDesignTest.zip`에는 7개 Pattern 모듈과 50개 named export가 있었다. Pattern 소스 디렉터리, Story 파일, Story 그룹 수는 별도 집계다.
| Pattern / 분류 | 공개 API 위치 | 사용 문서 | 대표 Reference / 계약 |
|---|---|---|---|
| Dashboard Layout — Layout Pattern | 운영: `DashboardLayout`, `DashboardHeader`, `DashboardMain`, `DashboardContent`, `DashboardLayoutProps`.<br>기존 호환성·검증용: `DashboardSidebar`, 선택적 `sidebar` slot. 공개 Export 존재는 운영 허용을 뜻하지 않음 | [dashboard-layout.md](../patterns/dashboard-layout.md) | [Patterns/Layout/Dashboard → Dashboard](../../src/patterns/dashboard-layout.stories.tsx) — Sidebar 없는 48px Header·업무 상태 필터·960px 최소 폭 |
| Dashboard Status — Runtime 상태 계약 | `countDashboardFilter`, `dashboardFilterDefinitions`, `dashboardFilterIds`, `dashboardStatusAttributes`, `dashboardStatusDefinitions`, `matchesDashboardFilter`, `DashboardFilterDefinition`, `DashboardFilterId`, `DashboardStatusDefinition`, `DashboardStatusId`, `DashboardStatusRecord`, `DashboardStatusTone` | [Dashboard 문서의 상태 계약 절](../patterns/dashboard-layout.md#3-storybook) | **최신 포터블 테스트 ZIP에 export·소스·CSS·Static HTML·빌드 결과 포함. 오전 ZIP에는 없음.** [현재 contract.ts](../../packages/ui/src/patterns/dashboard-status/contract.ts), [Runtime-neutral CSS](../../src/patterns/dashboard-status-contract.css), [React 대표 Story](../../src/patterns/dashboard-layout.stories.tsx), [Static HTML 예제](../patterns/dashboard-status-static.html) |
| Data Table — 여러 Component 조합 Pattern | `DataTable`, `DataTableColumnHeader`, `DataTablePagination`, `DataTableViewOptions`, `dataTableSelectColumn`, `useDataTable`, `DataTableColumnDef`, `DataTableInstance`, `DataTableOptions`, `DataTableProps` | [data-table.md](../patterns/data-table.md) | [Patterns/Data/Data Table → Data Table](../../src/patterns/data-table.stories.tsx) |
| Filter Bar — 여러 Component 조합 Pattern | `FilterBar`, `FilterBarActions`, `FilterBarSearch` 및 `FilterBarSearchProps` | [filter-bar.md](../patterns/filter-bar.md) | [Patterns/Data/Filter Bar → Filter Bar](../../src/patterns/filter-bar.stories.tsx) |
| GNB — Navigation Pattern | `GNB`, `GNBBrand`, `GNBMobileGroup`, `GNBMobileLink`, `GNBMobileMenu`, `GNBNav`, `GNBUser`, `GNBUtilities`, `GNBMobileMenuProps`, `GNBUserProps` | [gnb.md](../patterns/gnb.md) | [Patterns/Layout/GNB](../../src/patterns/gnb.stories.tsx) |
| LNB — Navigation Pattern | `LNB`, `LNBBrand`, `LNBGroup`, `LNBItem`, `LNBNav`, `LNBProfile`, `LNBMobileHeader`, `LNBMobileBrand`, `LNBMobileMenu`, `LNBMobileLink`, `LNBMobileGroup`, `LNBMobileMenuProps`, `LNBGroupProps`, `LNBProfileProps` | [lnb.md](../patterns/lnb.md) | [Patterns/Layout/LNB](../../src/patterns/lnb.stories.tsx) |
| Loading Screen — 화면 UI Pattern | `LoadingScreen` | [loading-screen.md](../patterns/loading-screen.md) | [Patterns/Layout/Loading Screen](../../src/patterns/loading-screen.stories.tsx) |
| Page Header — 화면 구성 Pattern | `PageHeader`, `PageHeaderActions`, `PageHeaderContent`, `PageHeaderDescription`, `PageHeaderTitle` | [page-header.md](../patterns/page-header.md) | [Patterns/Layout/Page Header → Page Header](../../src/patterns/page-header.stories.tsx) (`patterns-layout-page-header--all`). Breadcrumb + Title + Description + 단일 Brand Action. [Dashboard 조합](../../src/patterns/dashboard-layout.stories.tsx)은 Dashboard Layout 계약을 따름 |
Data Table은 기존 Table·Pagination·Checkbox·Select·Dropdown Menu 등을 조합한다. 연동 예제가 필요하면 Data Table Story 안의 `With Filter Bar`를 참고하되 숨김 `verification` Story이므로 일반 화면의 단독 기본 Reference로 삼지 않는다. Filter Bar는 단독으로도 사용 가능하며 검색·Select·Combobox·Date Picker 등 조회 조건의 layout이다. Dashboard 빠른 상태 필터는 Filter Bar API가 아니라 Dashboard Status 계약을 따른다.
오전 소스 ZIP을 받은 AI는 Dashboard Status 행의 현재 개발 경로를 사용할 수 있다고 가정하지 않는다. 최신 포터블 ZIP에서는 해당 상대경로와 공개 API를 사용할 수 있다. ZIP에 실제 포함된 Dashboard 구현·Story와 해당 ZIP 안의 Dashboard 문서를 확인하고, Status 공개 API가 없으면 없다고 보고한다.
## Reference 읽는 순서
### 화면 생성 시 놓치기 쉬운 소비 계약
- Navigation 미지정: [선택 근거](navigation-single-mode.md#선택-근거)를 기록한다. GNB/LNB의 목적과 대표 화면 구조로 판단하며 일반 목록의 기본 모드를 임의 확정하지 않는다.
- GNB ONLY: [Desktop·Mobile 구성](../patterns/gnb.md#8-desktop--mobile)을 함께 구현한다. 대표 `patterns-layout-gnb--all`의 Overview·Usage · GNB Sidebar를 확인하고 [Sidebar 예제](../patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08)도 Sheet 하나만 사용한다.
- LNB ONLY: 기존 독립 LNB 구조와 [LNB Mobile Header/Menu](../patterns/lnb.md#3b-lnb-mobile-header-2026-10-07)를 유지한다.
- DASHBOARD ONLY: Sidebar를 생성하지 않는다. 대표 `patterns-layout-dashboard-layout--all`의 전용 Header(48px)·업무 상태 필터 + Content, 최소 폭 960px·내부 가로 스크롤을 따른다. `DashboardSidebar`와 `sidebar` slot은 호환성·검증용이며 과거 Sidebar verification Story는 신규 운영 화면 구현 기준이 아니다.
- Dashboard Status: 공개 `@hm-design/ui/dashboard-status.css`는 변수만 제공한다. [Theme → Status CSS → 기존 Outline Badge mapping·React 소비 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)를 사용한다. `issue`는 독립 flag다. [Static HTML Reference](../patterns/dashboard-status-static.html)도 같은 계약을 소비한다.
- FilterBar: [입력 draft / 조회 applied·초기화 예제](../patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 확인한다. 대표 `patterns-data-filter-bar--all`은 조회 callback 없는 조합 예시이며 DataTable의 `patterns-data-data-table--with-filter-bar-story`는 즉시 적용 verification이다.
- React 소비 CSS: [Tailwind v4 검색 범위와 HM source 유지](runtime-portability.md#21-tailwind-v4-소비자-검색-범위), [Runtime Theme 연결](runtime-theme.md)을 확인한다.
- 실행 위치별 Asset: [Noto Sans KR·폰트 URL·단일 HTML 내장](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)을 확인한다.
### 탐색 절차
1. 이 인덱스에서 후보를 찾고 실제 파일·공개 API가 존재하는지 확인한다.
2. 사용 문서에서 Props/Variant, 상태, Theme Token, 조합 규칙을 확인한다.
3. 대표 Story에서 시각 언어와 실제 composition을 확인한다. Storybook을 실행할 수 없으면 Story 소스에서 확인한다.
4. `verification` Story는 계약·상태 검사로, Compatibility 예제는 호환성 확인으로, Fixture는 시각 테스트 데이터로 분류한다. 이들을 Production API나 운영 화면 예제로 일반화하지 않는다.
5. Dashboard는 GNB/LNB와 함께 사용하지 않는다. 허용되는 페이지 모드와 Responsive 규칙은 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다.
6. Theme Source of Truth는 `themes/default.json` 및 공개 Runtime 계약이다. Component Token/상태 mapping은 연결된 Theme 문서를 확인한다.
정적 Storybook 빌드가 없거나 실행할 수 없는 환경에서는 확인된 Story 소스와 문서만 근거로 삼고, 검증하지 않은 시각 동작은 미확인으로 보고한다.
@@ -0,0 +1,93 @@
# HMDesignSystem 전체 정합성 감사 — 2026-10-06
> HISTORICAL_RECORD: 2026-10-06 감사 시점의 저장소 상태·실측·보류 판단을 보존한다. 이후 상태 정책과 구현을 이 기록으로 대체하지 않는다. 현재 Component/Pattern 상태는 각 [Component 문서](../components/)/[Pattern 문서](../patterns/), Runtime 적용은 [Runtime Theme](runtime-theme.md), Designer 사용은 [Theme 가이드](designer-theme-guide.md)를 따른다.
현재 Production 코드와 실제 Storybook을 1차 기준으로 새로 대조했다. 문서/CLAUDE.md는 비교 대상이며 과거 작업 보고서의 검증 수치를 이번 결과로 재사용하지 않았다. 수정 전 분류를 먼저 보고한 뒤 명백한 불일치만 수정했다.
## 범위와 현재 구조
- `packages/ui`: private source export 패키지. Component, 7개 Pattern, CSS Theme와 `@hm-design/ui/theme` Runtime API를 소유한다.
- `packages/center-graphic`: UI와 서로 의존하지 않는 private source export 패키지. 통합 Storybook에서 별도 그룹으로 제공한다.
- 루트 `src`, `.storybook`: Demo와 Component/Pattern/Theme/CENTER Story. UI 44개 그룹, Pattern 6개 그룹(Page Header는 Dashboard에서 조합), 전체 56개 Story 파일을 확인했다.
- 대조 문서: Component 45개(Field 소비 가이드 포함), Pattern 7개, 기존 guideline 9개 및 CLAUDE.md/README.md. 현재 API·상태·Token·Story 경로를 비교하고 날짜가 있는 과거 검증 기록은 이력으로 읽었다.
- 현재 루트에 `.git`이 없다. Git diff/커밋 검증 대신 수정 전 확보한 파일과 로컬 변경 기록을 사용했다. 과거 기준 화면 snapshot과 보고서/CSV를 재생성하지 않았다.
Theme 흐름은 `primitives.css` → `default.css`의 Semantic/Theme palette 및 shadcn adapter → Component Mapping → CSS selector/utility → 실제 UI다. `theme.css`가 Default를 먼저 import한다. Component Mapping은 `:root, [data-hm-theme]`에서 다시 계산된다.
Runtime 계약은 `contract.ts`의 73개 입력이며 `runtime.ts`가 unknown 입력 전체를 검증한다. 색상은 6자리 불투명 HEX, input/container radius는 2–12px 정수, gradient는 세트 전체 또는 미지정이다. Partial/empty, atomic invalid fallback, 교체/cleanup, host inline value/priority 복원을 지원한다. Global 아래 명시적 Scoped Theme는 독립 HM Default + 자기 override이며 미지정 값이 Global override를 이어받지 않는다. 네트워크 Loader와 운영 Publish는 아직 없다.
## MATCHED — 유지한 구현
- **Active / Focus 분리**: Checkbox/Radio/Switch/Slider/Progress/Calendar selected·range edge는 Active, 일반 focus border/ring은 Focus를 사용한다. Runtime에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`을 분리해 기존 자동 검증을 새로 실행했고 실제 Checkbox/Calendar 채움과 Focus shadow 검사를 통과했다. Default에서는 Active/일반 Focus 모두 `#266651`이나 서로 alias하지 않는다.
- **Calendar / Date Picker**: selected와 range edge는 Active, hover/range middle은 Accent 45%, today는 Subtle Surface다. Global External에서 실제 Date Picker를 열면 body Portal의 selected day가 `rgb(0,127,115)`이고, 10/16 선택 후 닫히며 트리거로 Focus가 복귀했다. Wrapper Scoped popup의 제한은 아래 FOLLOW_UP이다.
- **Badge / Pagination**: Default Badge와 Pagination Current는 Primary Green(Default `#123328`). Global External에서는 둘 다 Primary purple로 바뀌고 Active teal과 구분된다. 실제 Pagination 일반 링크 hover는 Accent yellow 45%, Current는 Primary를 유지했다.
- **Table / Data Table**: row hover/selected는 공통 Accent 45%. Global Table에서 실제 hover/selected를 같은 `color(srgb 1 0.827451 0.427451 / 0.45)`로 확인했다. Data Table에서 행 선택 후 선택 면은 Default Accent 45%, Checkbox는 전환 종료 후 `rgb(38,102,81)`(Active), Current 페이지는 `rgb(18,51,40)`(Primary), 선택 요약은 `1개 선택됨 · 전체 47개`였다. Header 팔레트 장식은 별도 정책이다.
- **LNB 3Depth**: 기존 LNBGroup 중첩/Collapsible 조합을 유지한다. 3Depth 행 48px, padding-start 88px, weight 500/current 700, connector 40%와 current 배경·상위 노랑을 코드 및 실제 DOM으로 대조했다. Enter/Space 접기·펼치기와 소비자가 제어하는 중첩 open 상태가 유지된다.
- **Brand / Mobile / Scroll**: GNB+LNB는 GNBBrand 1개와 LNBBrand 0개. Standalone은 desktop LNBBrand, mobile Local bar의 서비스명으로 대체한다. 1023px에서는 GNB 56px/LNB hidden, 1024px에서는 GNB 70px/LNB 표시, 문서 폭은 viewport와 같다. 높이 700에서 LNB nav는 y=98→610, client 512/scroll 526; 실제 scrollTop=14에도 Profile y=610, GNB y=0을 유지했다. nav RTL/내용 LTR로 scrollbar를 시작 쪽에 배치한다. Standalone Mobile Sheet의 지역 3Depth 메뉴, Escape 닫힘과 `서비스 메뉴` 트리거 Focus 복귀를 확인했다.
- **Dashboard composition**: header/sidebar/children slot과 main scroll 소유권, desktop GNB/LNB 접점 및 mobile 전환이 현재 문서와 일치한다. KPI/Filter Bar/Data Table은 공개 primitive/pattern 조합이다. GNB+LNB 조립은 이미 구현되어 있다.
- **Dialog Wide Table**: 390px viewport에서 Dialog 358px, 문서 scrollWidth 390px, Table container client 308px/scrollWidth 1600px/overflow-x auto. 가로 스크롤은 내부 Table container가 소유한다. Header 13/20, Cell 14/20, padding 14/12를 유지하고 Escape 후 `넓은 표` 트리거 Focus 복귀를 확인했다.
- **Filter Bar Search Input**: 기본 padding-start 36px, focus 상태 35.5px 보정으로 아이콘 뒤 텍스트 시작점을 유지한다. 실제 검색 입력/조건 수/지우기와 좁은 화면 overflow를 확인했다. Input 구현/CSS는 수정하지 않았다.
- **Storybook**: favicon override는 실제 Manager DOM에서 icon link 1개, href `data:,`로 확인했다. 최종 빌드 index는 254개 Story/56개 그룹이며 UI 44개와 Pattern 6개 그룹은 각각 대표 Story 하나다. Theme 4개 Story는 verification으로 기본 숨김. 새 Progress NonDefaultMax도 숨김 verification이며 대표 Story에 같은 specimen이 있다.
## DOC_FIX — 수정 완료
- Component/Pattern의 오래된 Story 경로를 현재 category/title로 갱신했다. Page Header는 별도 Story 없이 `Patterns/Layout/Dashboard Layout`에서 사용한다.
- Primitive 직접 참조 설명을 현재 Semantic/Theme palette 연결에 맞췄다. Input/Textarea/Radio의 Destructive, Table/Subtle, Slider/Progress track, Tabs/Separator/Scroll Area, GNB/LNB/Loading Screen을 대조했다.
- Button의 로컬 HEX 잔존 설명, Select indicator 리터럴 설명, Select/Menu/Command의 full Accent 설명을 실제 palette/Accent 45% 연결로 고쳤다. Toggle on/full Accent와 Select selected/45%의 차이를 명시했다.
- Runtime API 미구현/Preview 전용, root 아래 Production 경로, toolbar 전환, 초기 5개 Component 상태를 현재 workspaces/API/Story 구조로 정정했다. Library build/registry/Loader/Publish 목표 절차와 현재 구현을 구분했다.
- CLAUDE.md의 Portal container API 설명을 실제 HM popup API 제한으로 바로잡았다. general Focus 설명에 기존 Slider/GNB/LNB 예외를 명시했다.
- Data Table의 With Filter Bar를 두 번째 대표 Story라고 한 설명을 숨김 verification으로 정정했다. Filter Bar 문서의 연동 대상도 Data Table로 유지했다.
- 과거 검증 절의 당시 색상·측정·실패/미확인 기록은 현재 결과로 덮지 않았다. Progress의 이전 max 제한에는 이번 수정 기록을 추가했다.
## CODE_FIX — 수정 완료
**Progress의 max 비율 계산 한 곳**을 수정했다(`packages/ui/src/components/ui/progress.tsx`). 이전에는 value를 백분율로 간주하므로 max=200/value=50에서 ARIA는 50/200인데 채움은 50%였다. 이제 유효 value/max 비율로 25% 채우며 Radix 기본 max/유효 범위 및 값 없음 상태를 따른다. 디자인/CSS/공개 API는 유지했다.
`src/components/progress.stories.tsx`에 max=200의 0/50/200/값 없음 specimen과 ARIA/Indicator 회귀 검증을 추가했다. 실제 DOM은 각각 transform -100%/-75%/0%/-100%, ARIA 0/50/200/valuenow 없음으로 일치했다. 기존 max=100 대표 상태와 interactive specimen도 유지했다.
Production 수정은 위 파일 1개, Story 수정은 Progress 파일 1개다. 기존 문서는 58개 파일에 경로/Token/현재 상태를 정정했으며 Component·Pattern 구현을 재작성하지 않았다.
## STALE — 현재 설명에서 정리
- 초기 5개 Component/Medium Preview만 구현된 상태.
- Storybook toolbar Default/Draft 전환과 Runtime API 공개 여부를 미래 결정으로 적은 설명.
- Dashboard가 다음 작업이거나 GNB+LNB 조립 자체가 아직 없는 상태.
- Alert Dialog 미구현, Dialog 이후 구조 안정화 대기, Select의 근거 없는 동시 작업 추측.
날짜가 있는 이전 작업 보고서, CSV, ui-viz 비교표는 폐기하지 않았다. 현재 구현 증거로 재사용하지 않고 당시 이력으로 구분했다.
## AMBIGUOUS — 이번에 변경하지 않음
- Slider Primary 20% halo, desktop GNB/LNB inverse outline(current는 Primary)까지 일반 Focus Semantic으로 통합할지. 현재 component 문서와 Runtime 문서는 예외를 명시하지만 일반 Focus 표현을 한 가지로 확대하면 디자인 결정이 필요하다. 일반 Focus의 Light 2 자체도 가이드에 시험 적용값으로 남아 있다.
- GNB+LNB gradient 이음새/콘텐츠 모서리의 최종 시각 승인. 조립과 Brand ownership은 구현/확정되었고, 이 부분은 Dashboard/GNB 문서에서 확정 대기로 남아 있다.
- 선택 경로 전체 connector 강조와 모바일 LNB의 14px 지역 메뉴/상위 current 표현 등 기존 미확정 Layout 항목. 현재 문서에 없는 최종 정책을 추정해 바꾸지 않았다.
## FOLLOW_UP — 별도 작업
- HM SelectContent/PopoverContent/DatePicker의 wrapper Scoped Theme 전달 API. 현재 body Portal은 Global Theme를 따르며 자동 Scoped 전달을 제공하지 않는다.
- Date Picker/Combobox를 undefined로 비우는 controlled reset 제약(현재 문서대로 key remount 사용). 새 API/상태 모델 작업으로 분리한다.
- FilterBarSearch에 disabled/readOnly를 전달할 때 별도 지우기 버튼의 활성 정책. 현재 input props는 Input에 전달되고 clear callback은 독립적으로 실행된다. 이번에 새 상태 계약을 만들지 않았다.
- Center Graphic/Facility의 7개 노출 Story는 서로 다른 시설물 export를 묶은 그룹이다. UI와 같은 단일 집계 대표 Story로 정리할지 검수 구조를 별도로 결정한다. 기존 CENTER 구현은 이번에 변경하지 않았다.
- 문서 예시를 자동 탐색해 생기는 CSS warning, Storybook chunk size와 docgen callback 성능 안내. 빌드 실패는 아니며 source 탐색/번들 최적화는 별도 범위다.
- Library 산출물/Registry, 네트워크 Loader, Theme Publish 운영은 아직 미구현이다.
## 최종 통합 검증
- `npm run typecheck`: PASS / exit 0. 루트와 두 workspace 포함.
- `npm run build`: PASS / exit 0. CSS 예시 warning 1개 유지.
- `npm run build-storybook`: PASS / exit 0. CSS 예시 warning, 500kB chunk 안내 및 PLUGIN_TIMINGS 성능 안내가 출력되었다.
- 현재 실제 Storybook Runtime 자동 검증: **PASS — 136 checks; 63 rendered parts**. Default parity, 73개 공개 입력, 의미 독립성, partial/empty/invalid, switching/cleanup, Global/Scoped, geometry를 새로 실행했다.
- CSS 변수 의존성/현재 문서 Token·Story 경로/73개 계약표를 정적으로 대조했다. Loading spinner index는 TSX inline style로 선언되며 LNB collapsed-width는 문서에서 명시적으로 없는 변수다.
- 최종 빌드 index에서 대표/verification 태그와 신규 NonDefaultMax 포함을 확인했다. 전체 254개 Story의 모든 play/interaction을 실행했다는 의미는 아니다. 실제 동작 검수는 위 핵심 대상에 집중했으며 다른 브라우저/실기기는 미검증이다.
로그/변경 기록: `_local/consistency-typecheck.log`, `consistency-build.log`, `consistency-storybook-build.log`, `consistency-audit-data.json`, `consistency-audit-diff.txt`. 로컬 비교용 파일은 `_local/consistency-audit-before/`에 확보 시점 기준으로 보관했다. Page Header 경로 및 Loading Screen Token의 단일 문구 수동 수정은 위 수정 범위에 별도로 기록했다.
화면 증거: `_local/consistency-progress.png`, `_local/consistency-progress-final.png`(실제 Manager에서 대표 Story 하나와 max=200), `_local/consistency-dialog-mobile.png`. Runtime 검증의 PASS 수치는 현재 브라우저의 result output에서 직접 읽었다.
## 작업 시간 (KST)
- Started At: 2026-10-06 15:03:01
- Estimated Duration: 40–60분(분석 20–30 / 최소 수정 10–15 / 통합 검증 10–15)
- Completed At: 2026-10-06 15:25:46
- Actual Duration: 22분 45초
- Variance: 예상보다 17분 15초–37분 15초 단축
+52
View File
@@ -0,0 +1,52 @@
# 디자인팀 컬러 Theme 사용 방법
Storybook에서 **Theme → Designer Playground → Editor**를 엽니다.
로컬 기본 주소: http://localhost:6007/?path=/story/theme-designer-playground--editor
1. 처음 열면 `themes/default.json` 기반 HM Default 컬러가 표시됩니다. **Load Default JSON**, **Load Service Example JSON**으로 repository JSON을 URL에서 불러오거나 **Theme JSON URL**에 CDN URL을 입력한 뒤 **Load URL**을 누릅니다. 같은 탭에 저장값이 있다면 마지막 유효 Theme를 다시 표시합니다. **Reset to HM Default**로 기본값을 다시 불러옵니다.
2. Primary / Active / Focus / Accent / Destructive 등 역할의 컬러 피커 또는 HEX 입력을 수정합니다. 불투명 6자리 HEX(`#123abc`)를 사용합니다. 유효한 값은 입력 즉시 적용되며 별도의 Apply 버튼이 필요 없습니다. 미완성 입력은 마지막 유효 색을 유지합니다.
3. 오른쪽 Live Preview에서 여러 컴포넌트를 함께 확인합니다. 화면이 좁으면 편집기 아래에 Preview가 이어집니다. Focus 확인은 Tab 키로 Checkbox/Input 등에 이동합니다. Select, Date Picker, Combobox 및 메뉴를 열면 Global Theme가 팝업에도 적용됩니다.
4. 다른 Component / Pattern Story로 이동해도 같은 Theme를 확인할 수 있습니다. 값 변경 자체에는 reload, 파일 저장, Vite HMR, build, Storybook 재시작이 필요 없습니다. 기존 Runtime Contract 검증 Story는 독립 검증을 위해 편집 Theme를 적용하지 않습니다.
5. **Reset to HM Default**는 현재 override를 해제하고 기본값을 즉시 복원합니다. 컴포넌트 API와 레이아웃은 바꾸지 않습니다.
6. **Show JSON**으로 현재 데이터를 표시하고 **Copy JSON**으로 복사하거나 **Export JSON**으로 `hm-color-theme.json`을 내려받습니다. 클립보드 접근이 막히면 JSON 입력란에서 직접 복사합니다. 수정한 키만 저장하므로 나머지는 HM Default입니다.
7. JSON을 입력란에 붙여 넣고 **Import JSON**을 누르거나 파일 입력으로 JSON을 선택하면 바로 적용됩니다. 이전 Theme와 병합하지 않고 새 Theme로 교체합니다. 잘못된 JSON/키/값/불완전한 gradient 그룹은 거부하고 마지막 유효 Theme를 유지합니다. 이 편집기는 컬러 전용이므로 radius 입력을 거부합니다.
8. Export한 JSON을 `themes/<서비스명>.json`에 저장하고 검증·Git diff/review·commit/push 후 Theme artifact를 CDN에 별도 배포합니다. 개발팀은 설정한 Theme URL을 `loadRuntimeTheme`에 전달하거나 API/로컬 JSON을 `applyRuntimeTheme`에 전달하면 됩니다. 이미 지원하는 Theme JSON의 color-only 변경에는 UI package 재빌드·version up·재배포가 필요 없습니다. Component CSS 규칙이나 Runtime 계약 변경은 package 릴리스 경로를 따릅니다. 자세한 배포 절차: [Theme delivery](theme-delivery.md).
```ts
import "@hm-design/ui/theme.css"
import { applyRuntimeTheme, resetRuntimeTheme, isRuntimeTheme } from "@hm-design/ui/theme"
// 설정/API/사용자 입력에서 받아온 공식 v1 Theme JSON.
const theme = { schemaVersion: 1, tokens: { "color.state.active": "#007f73" } }
if (isRuntimeTheme(theme)) applyRuntimeTheme(document.documentElement, theme)
// 같은 root에 새로운 Theme를 전달하면 즉시 교체된다.
// isRuntimeTheme은 DOM을 건드리지 않는 사전 검증이다.
resetRuntimeTheme(document.documentElement)
```
## 컬러 역할과 적용 범위
- Primary는 Badge default, Pagination current, Button outline 및 내비게이션 강조 글자에 적용합니다. Brand Button/GNB의 gradient와 일부 palette 장식은 별도 기존 계약을 유지합니다. Primary 하나로 모든 gradient stop을 재설계하지 않습니다.
- Active는 Checkbox, Radio, Switch, Slider, Progress 및 Calendar/Date Picker의 선택 면을 바꿉니다. Focus와 독립적입니다. On Active는 그 위 아이콘/텍스트입니다.
- Focus는 일반 입력 테두리와 Checkbox/Calendar 등의 ring, Slider Keyboard Focus의 Focus Semantic 20% halo입니다. Slider Hover/Drag halo는 Primary 20%이며 GNB/LNB는 contextual navigation focus 예외를 유지합니다.
- Accent 45%는 Select/메뉴 Item/Calendar range middle·hover/Pagination hover/Table hover·selected의 Soft Highlight 면입니다. Combobox는 Command를 조합하므로 같은 강조 역할을 사용합니다. Toggle/Toggle Group Selected, Menubar 상위 Highlighted/Open 및 Navigation Menu Trigger/Open은 Accent 100% + On Accent를 사용합니다. Navigation Current는 Primary + contextual surface로 Control Active와 구분합니다.
- Destructive는 오류, invalid 및 destructive 표현입니다. Primary/Active/Focus와 독립적입니다.
- 각 입력란은 Default JSON + partial override의 실제 계산된 역할 값을 표시합니다. Export는 표시값 전체가 아니라 명시적으로 수정·Import한 데이터를 저장합니다. Primary와 On Accent는 sibling Semantic이며 Default JSON과 CSS-only package fallback 모두 독립값을 유지합니다. 기본값이 같아도 Primary override가 On Accent를 함께 바꾸지는 않습니다.
## 저장과 알려진 제한
- 서버/DB/Publish 기능은 없습니다. 같은 브라우저 탭의 Storybook preview origin에 `sessionStorage`로 보관하며, 세션 저장을 사용할 수 없는 환경에서도 현재 화면의 편집/Export는 동작합니다. 장기 보관은 JSON Export를 사용하세요.
- Global Theme는 `document.documentElement`에 적용하며 body Portal도 같은 Theme를 사용합니다. wrapper 밖으로 Portal된 popup은 Scoped Theme를 자동 전달받지 않습니다. 이 제한은 기존 FOLLOW_UP입니다.
- 전체 Runtime Contract는 73개 입력을 유지합니다. 배포하는 Default JSON은 71개 컬러이며 radius/geometry는 package에서 관리합니다. 편집 화면은 상위 컬러 역할 17개만 노출하며 Component Token, radius, palette/gradient 개별 입력 UI를 만들지 않습니다. 기존 컬러 계약의 palette/gradient는 유효 JSON으로 Import할 수 있으며 gradient는 세트 전체를 제공해야 합니다.
- 명시적 `[data-hm-theme]` scope는 독립 HM preset이므로 Global Theme를 이어받지 않습니다. Production에서 같은 root는 한 owner가 Apply/Reset을 관리합니다.
- Storybook 편집기 설치는 이번 코드 변경을 적용하는 최초 빌드가 필요합니다. 설치 후 컬러 값을 바꾸는 운영에는 재빌드가 필요 없습니다.
## 개발팀의 Default 유지보수
Default 컬러의 사람이 관리하는 원본은 `themes/default.json`입니다. `contract.ts`는 공개 key/property/type을 정의하고 Default literal을 중복 소유하지 않습니다. `scripts/theme-templates/`는 컬러 literal 없이 alias/geometry만 유지합니다.
Storybook의 `src/theme/designer-preview.tsx`는 `themes/default.json`을 직접 import합니다. 디자인팀이 `default.json`을 수정하면 개발 서버의 Storybook HMR로 기본값이 갱신됩니다. Preview는 이 Default와 partial override를 병합하며, Reset은 override를 비워 현재 Default를 확인합니다.
Theme-only 배포는 `npm run build:themes`로 독립 JSON artifact만 생성합니다. UI package의 fallback snapshot까지 새 버전에서 갱신할 때 `npm run generate:theme-defaults`로 `primitives.css`, `default.css`, `default-values.generated.ts`를 생성합니다. `typecheck`, `build`, `build-storybook`의 pre hook은 생성 결과의 일치를 검사합니다. 이미 배포한 UI package를 그대로 두고 CDN JSON만 바꿀 수 있습니다.
공식 전체 입력표: [Runtime Theme Contract](runtime-theme-contract.md). Runtime 적용·제한: [Runtime Theme](runtime-theme.md).
+150
View File
@@ -0,0 +1,150 @@
# 디자인팀 External Runtime Color Theme 운영 결과
> HISTORICAL_RECORD: 작성·검증 시점(2026-10-06)의 기록이다. 아래 baseline·실측·당시 Slider/On Accent 설명을 보존한다. 현재 Slider Focus, 독립 On Accent fallback, Default JSON 직접 import/Preview/Reset 흐름은 [디자인팀 Theme 가이드](designer-theme-guide.md), Runtime 계약은 [Runtime Theme](runtime-theme.md), 배포는 [Theme delivery](theme-delivery.md)를 따른다.
기준: `main` / `d1015309ab43fba69c8959a3dda660358e0e8fa1`. 시작 시 HEAD 일치, working tree clean을 실제 확인했다. 이전 감사 문서의 수치는 이번 증거로 재사용하지 않았다.
## 1. 기존 Runtime Theme 구조
`packages/ui/src/theme/runtime.ts`의 framework-independent Apply/Reset은 이미 `@hm-design/ui/theme`에서 공개되어 있었다. `src/theme/preview.ts`는 이 API를 재수출하는 Storybook 호환 adapter였다. Production API와 Preview를 동일시하지 않고 실제 package export를 확인했다. CSS의 Primitive → Default/Semantic → Component Mapping을 유지한다.
## 2. 기존에 가능했던 디자인팀 작업
공식 v1 JSON과 CSS variable override, Default preset 데이터, 부분/빈 Theme, invalid fallback, Global/Scoped, 원래 inline 값/priority cleanup이 가능했다. Comparison/GlobalExternal/Verification/Dashboard는 개발·검증용 숨김 Story였다. 디자인팀의 직접 편집·전달 workflow는 없었다.
## 3. 부족했던 것과 구현
JSON을 컬러 원본으로 연결하고 URL fetch/검증·Apply 분리, Default/partial resolve, CDN 실패 fallback, Theme JSON 독립 산출을 추가했다. 기존 편집기를 JSON URL/파일 Preview 소비자로 재사용하고 Default 표시/같은 세션의 여러 Story 확인/즉시 Reset/Copy·Export·Import를 제공한다. DOM을 변경하지 않는 `isRuntimeTheme`은 기존 validator에서 분리했다. 새 Theme engine/React Production Provider/서버/DB/Portal API는 만들지 않았다.
## 4. Default HM Theme Source of Truth
운영 컬러의 원본은 **`themes/default.json` / 서비스별 `themes/*.json`**이다. Default는 기존 계약의 71개 컬러 값을 포함하며 Component Token은 없다. `contract.ts`의 반복된 Default literal을 제거했다. `scripts/theme-defaults.mjs`가 JSON + 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `defaultRuntimeTheme`는 이 데이터로 구성된다. 2개 radius/geometry는 package template에 유지한다. 현재 생성 CSS의 실제 선언값/alias는 baseline과 동일하며 diff는 생성 설명 주석뿐이다.
`npm run build:themes`는 Theme-only JSON 산출이며 UI/Graphic build·version up을 수행하지 않는다. 새 UI package fallback snapshot을 갱신할 때만 `npm run generate:theme-defaults`; `typecheck/build/build-storybook` pre hook은 stale 생성 데이터를 거부한다. 이미 배포한 package fallback은 해당 버전의 Default snapshot이고 최신 Default JSON은 CDN으로 독립 적용할 수 있다. 자세한 저장/배포/캐시 정책은 [Theme delivery](theme-delivery.md)에 정리했다.
## 5. Public Runtime Theme Contract
전체 공식 계약은 기존 73개 그대로이며 [전체 표](runtime-theme-contract.md)를 유지한다. 편집기는 아래 17개 상위 역할만 노출한다. Component Token은 노출하지 않는다. 각 입력에는 실제 계산된 컬러를 표시한다.
| Theme Role | Token | Default | Runtime Editable | 영향 |
|---|---|---|---|---|
| Primary / Brand | `color.action.primary` | `#123328` | Yes | Badge, Pagination current, Button outline, navigation current ink |
| Active / Selected | `color.state.active` | `#266651` | Yes | Checkbox, Radio, Switch, Slider, Progress, Calendar/Date Picker selected |
| On Active | `color.on.state.active` | `#ffffff` | Yes | 선택 아이콘/텍스트 |
| Focus | `color.stroke.input.focus` | `#266651` | Yes | 일반 입력/Checkbox/Calendar ring |
| Accent | `color.accent` | `#bddfd4` | Yes | 45% soft highlight |
| On Accent | `color.on.accent` | `#123328` | Yes | 강조 면 글자; JSON Default에서는 독립값 |
| Destructive | `color.action.destructive` | `#d32f2f` | Yes | 오류, invalid, destructive |
| Surface | `color.bg.surface` | `#ffffff` | Yes | Input, Card, Popup |
| Content Surface | `color.bg.content` | `#f5f0e5` | Yes | 콘텐츠 면 |
| Subtle Surface | `color.bg.subtle` | `#f4f1ea` | Yes | 은은한 면, Calendar today |
| Text Primary | `color.fg.primary` | `#0a221d` | Yes | 주요 텍스트 |
| Text Secondary | `color.fg.secondary` | `#6e6759` | Yes | 보조 텍스트 |
| Placeholder | `color.fg.placeholder` | `#999999` | Yes | 입력 안내 |
| Inverse Text | `color.fg.inverse` | `#ffffff` | Yes | 반전 텍스트 |
| Default Border | `color.stroke.default` | `#9a9284` | Yes | 컨트롤/Badge/Scroll Area |
| Input Border | `color.stroke.input.default` | `#dedbd2` | Yes | 입력/패널 |
| Cancel | `color.action.cancel` | `#6e6759` | Yes | 취소 동작 |
## 6. Live Theme Apply 경로
Designer JSON URL/file/HEX → 공식 v1 `RuntimeTheme` → Storybook 세션 store → `resolveRuntimeTheme(theme, defaultJson)` → Global decorator → Production `applyRuntimeTheme(document.documentElement, theme)` → inline CSS Custom Properties → 기존 Semantic/Derived/Component Mapping → Components/body Portal.
Production은 서비스 설정 URL → `fetchRuntimeTheme` → 기존 validation → 선택적 Default/partial resolve → Apply이며, 편의 함수 `loadRuntimeTheme`도 같은 경로를 사용한다. Fetch와 Apply는 각각 따로 사용할 수 있다. 회사 CDN URL은 하드코딩하지 않는다.
Storybook 전용 UI/저장과 Production 계약/주입 함수를 분리한다. Component는 Theme 출처를 알지 못한다.
## 7. Default Theme Load
처음에는 `themes/default.json` + 빈 override를 같은 Runtime 경로로 적용하고 실제 계산값으로 입력을 채운다. 같은 탭 저장값은 Default JSON 위 partial로 사용한다. Reset으로 JSON Default를 다시 적용한다. Load Default JSON 버튼은 `/themes/default.json`을 실제 fetch/검증하여 적용한다. CSS minifier의 HEX3 결과는 편집기의 공식 HEX6 표현으로 정규화한다.
## 8. Live Update
유효한 컬러 입력을 받은 순간 Global CSS 변수가 바뀐다. 값 변경에는 페이지 reload, 파일 저장, Vite HMR, build, Storybook 재시작/재빌드가 필요 없다. 같은 세션의 다른 Story에서도 적용한다. 기존 `Theme/Runtime Contract` 검증군은 고정 baseline과 분리하기 위해 editor override를 사용하지 않는다.
## 9. Reset
`Reset to HM Default`는 session override를 비우고 JSON Default를 즉시 복원한다. invalid HEX draft와 pending URL 요청도 초기화한다. Production `resetRuntimeTheme`은 원래 host inline 값/priority와 package CSS fallback을 복원하며 pending load를 무효화한다.
## 10. Partial Override
Export/Import는 입력 문서/명시한 key만 포함하며 부분 JSON을 지원한다. 새 Theme로 교체하여 이전 key를 남기지 않는다. Storybook과 supplied Default를 사용하는 Production resolve는 JSON Default + partial을 합친다. 별도 Default를 전달하지 않는 URL Loader는 package CSS fallback + partial을 사용한다. Primary/Active 두 역할만 전달할 수 있으며 전체 73개 값은 필요하지 않다.
## 11. Export / Import
Show JSON, Copy JSON(클립보드 제한 시 직접 복사), JSON 파일 다운로드, JSON 텍스트/파일 Import를 제공한다. 모두 동일한 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. 유효하지 않은 Import는 현재 Theme를 유지한다. 컬러 작업 범위의 layout 회귀를 막기 위해 편집기에서는 radius를 거부한다. Production의 기존 radius 계약은 유지한다. 서버/DB 없음; 임시 보관은 sessionStorage, 전달/장기 보관은 Export다.
## 12. Production 적용 방법
`import "@hm-design/ui/theme.css"` 후 `loadRuntimeTheme(document.documentElement, serviceConfig.themeUrl)`을 호출한다. 별도로 받은 Default JSON + 서비스 partial은 `resolveRuntimeTheme(serviceTheme, defaults)` 후 Apply한다. JSON fetch/validation은 `fetchRuntimeTheme`, 데이터 Apply/Reset은 기존 API로 분리한다. HTTP/JSON/validation/abort 실패 시 package HM fallback이 유지된다. 오래된 요청 응답은 새로운 Apply/Load/Reset을 덮지 않는다. 상세 실제 API 예제와 갱신/캐시 정책은 [Theme delivery](theme-delivery.md)에 기록했다. CDN 인프라/credentials/publish는 이번 범위에서 만들지 않았다.
## 13. Active / Focus / Primary / Accent 독립성
Verification 컬러는 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`, Destructive `#d41466`로 서로 다르다. 다섯 역할을 하나씩 입력하며 다른 네 역할이 유지되는지 확인한다. 기존 일반 Focus 검증은 실제 Checkbox/Calendar shadow의 색을 검사한다. 기존 Slider Primary 20% halo와 GNB/LNB inverse outline 예외는 보존한다.
## 14. Component 검증
공유 Specimens는 Button/Input/Checkbox/Radio/Switch/Slider/Progress/Select/Calendar single·range/DatePicker/Pagination/Badge/Table/GNB/LNB/Command를 기존 API 그대로 렌더링한다. Designer extras에 실제 DataTable, Combobox, DropdownMenu, ContextMenu, Menubar를 추가했다. 기존 Specimens 내용과 고정 `verification-baseline.json`은 변경하지 않고 파일로 추출했다.
새 LiveVerification은 실제 input/Import/Reset 버튼으로 Default 17개 표시, 핵심 역할 독립성, 실제 주요 컴포넌트 색, Accent 45%의 10개 Mapping, Export→Import, invalid 보존, partial 교체, 17개 입력, geometry 및 동일 document를 검사한다. Loader 검증은 실제 정적 `/themes/*.json` HTTP 요청, 71개 Default와 생성 데이터 일치, partial/merge/reset/invalid/HTTP 실패/abort와 delayed response race simulation을 검사한다. 기존 Runtime Verification은 모든 73개 입력/Default visual parity/실제 Focus/cleanup/Global·Scoped 등을 다시 검사한다. 정적 빌드 HEX3 정규화만 검증 코드에 추가했다. baseline snapshot은 변경하지 않았다.
## 15. Scoped Portal 제한
Global document Theme는 body Portal에도 적용한다. 명시적 scope는 독립 HM Default + 자기 override이며 wrapper 밖으로 Portal된 UI는 Global을 사용한다. scope/Portal 자동 전달은 기존 FOLLOW_UP으로 유지한다.
## 16. 수정 파일
- Production: `packages/ui/src/theme/contract.ts`, `default-values.generated.ts`, `runtime.ts`, `requests.ts`, `loader.ts`, `index.ts`, 생성 `default.css`/`primitives.css`(현재 선언은 baseline과 동일).
- 원본/산출: `themes/default.json`, `service-example.json`; `scripts/theme-contract.mjs`, `theme-defaults.mjs`, `theme-artifacts.mjs`, `theme-templates/`; 루트 `package.json`/`.gitignore`.
- Storybook: `.storybook/main.ts`/`preview.tsx`, `src/theme/designer-preview.tsx`, `designer-theme.stories.tsx`, `designer-theme.css`, `designer-extras.tsx`, `specimens.tsx`, `runtime-loader.stories.tsx`, 기존 `runtime-theme.stories.tsx`(Specimens 추출/검증 색 정규화).
- 문서: 이 보고서/사용법/Theme delivery, README/CLAUDE, 기존 contract/report/테마 토큰 가이드의 후속 섹션 및 runtime/개발 적용/운영 배포/프로젝트 지침의 현재 상태.
## 17. Git diff 요약
baseline에서 Theme 운영 관련 변경만 관리했다. Component/Pattern 구현, Component API, Primitive 값/Default CSS 선언, viewport/breakpoint 및 layout 토큰을 변경하지 않았다. 새 editor만 자신의 responsive layout CSS를 갖는다. Git commit/push는 수행하지 않았다. 추적되지 않은 신규 파일도 검토 범위에 포함한다.
## 18. 문서 및 감사 이력
[디자인팀 사용법](designer-theme-guide.md)과 [Theme delivery](theme-delivery.md)에 JSON 원본/Preview/Runtime/CDN 배포/캐시/롤백/개발팀 workflow/제한을 정리했다. 기존 Runtime 계약/보고서/테마 토큰 가이드에는 추가 결정을 기록했다. 기존 hardcode audit CSV와 consistency audit는 당시 이력이며 Component 색상 Mapping을 바꾸지 않아 재작성하지 않았다.
## 19. Build / 최종 브라우저 검증
- `npm run typecheck`: PASS / exit 0, 루트와 UI/CENTER workspaces.
- `npm run build`: PASS / exit 0.
- `npm run build-storybook`: PASS / exit 0.
- `npm run build:themes`: PASS, 독립 JSON 2개를 `theme-dist/themes/`에 산출.
- **1280 / 768 / 390px 각각**: 기존 Runtime **136 checks / 63 rendered parts**, Designer workflow **96 checks**, URL Loader **119 checks** 모두 PASS. 정적 빌드를 사용하여 Vite HMR이 없는 환경에서 검증했다. 해상도 간 이동 시 문서를 새로 열었고, 각 검증 안의 Theme 변경은 동일 document에서 reload 0으로 수행했다.
- Default baseline 색상/geometry parity, 전체 73개 기존 계약, 실제 Checkbox/Calendar Focus, 독립 핵심 역할, partial/reset/invalid/cleanup, JSON Export→Import, HTTP Default/External load, Abort와 응답 race를 확인했다.
- 실제 Editor의 Load Default JSON / Load Service Example JSON 버튼을 클릭해 즉시 반영을 확인했다. 기존 Badge Story로 이동한 뒤에도 Primary `rgb(104,55,168)`과 Destructive `rgb(212,20,102)`가 세션에서 유지되었다.
- 실제 body Select Portal의 selected 면은 Accent yellow 45% `color(srgb 1 0.827451 0.427451 / 0.45)`, Date Picker selected는 Active `rgb(0,127,115)`였다. Combobox/Dropdown/Context/MenuBar의 실제 highlight와 Table/DataTable selected도 같은 Accent 45%였다.
- 별도 임시 fixture의 **8개 generation/artifact checks** PASS: stale fallback 검사, JSON 역할→CSS 생성, 나머지 geometry/Default 유지, Theme-only 산출 시 UI 파일 불변, 최신 운영 JSON 포함, 삭제된 source의 stale artifact 제거, 실제 workspace 원본 미변경.
- JSON 텍스트 Export→Import는 검증했다. 파일 Export는 DOM download 링크와 Blob 수명 처리를 구현했으나 내장 브라우저가 download 완료 이벤트를 반환하지 않아 파일 저장 완료는 확인하지 못했다. UI는 다운로드 요청으로 표시하고 Show JSON 직접 복사 경로를 제공한다. 클립보드 권한도 환경에 따라 제한될 수 있다.
- 실제 회사 CDN/CORS/캐시·registry publish는 미검증이다. HTTP 검증은 localhost의 정적 Theme JSON으로 CDN delivery를 모사했다. 브라우저/실기기 전체 호환성을 검증했다는 의미는 아니다.
기존 CSS 예시 최적화 warning 1개, Storybook chunk 크기/성능 안내는 비차단으로 유지한다. 처음 sandbox에서 esbuild spawn EPERM이 발생한 빌드는 승인된 실행 환경에서 다시 수행하여 통과했다. 검증 중 HEX3 직렬화/검사 대기 문제는 검증 코드에 보완했고 최종 PASS 결과만 기록했다.
증거: `_local/designer-typecheck.log`, `designer-build.log`, `designer-storybook-build.log`, `designer-runtime-verification.json`, `theme-generation-verification.json`, `designer-external-preview.jpg`. 이전 감사 CSV/136 checks 결과를 복사한 것이 아니라 현재 정적 빌드 output에서 직접 읽었다.
## 20. 최종 판정
**COMPLETE — 요청한 Runtime/JSON/독립 artifact/Preview 구현 범위.**
- External override: PASS.
- 디자인팀 실시간 JSON Preview/편집/Reset/공식 JSON 전달: PASS. JSON 텍스트 Export/Import로 전달 가능하다. 파일 다운로드 완료는 위 환경 제한을 구분한다.
- A. 한 Git Repository인가: **YES**, baseline main/commit 확인.
- B. UI와 Theme 배포 단위가 분리되는가: **YES**, `build:themes`는 JSON만 산출한다. UI/CENTER는 독립 workspaces.
- C. 컬러 Source of Truth: **`themes/default.json` / 서비스별 top-level `themes/*.json`**. package CSS/Default 데이터는 생성물이고 template는 alias/geometry만 소유한다.
- D. UI package 재배포 없이 Theme 변경 가능한가: **YES**, 변경 JSON을 독립 배포하고 Loader를 다시 호출한다.
- E. CDN JSON으로 적용 가능한가: **YES**, 소비 서비스의 설정 URL을 공식 fetch/load API에 전달한다. 실제 회사 CDN은 미정/미배포.
- F. CDN 실패 시 Default 유지되는가: **YES**, 기존 host override가 없는 통상 root에서 package의 HM Default snapshot을 복원한다. host 소유 inline 값이 있었다면 기존 정책대로 그 값을 복원한다.
- G. Default/External이 같은 계약/Loader를 사용하는가: **YES**, 동일 v1 JSON과 fetch/validation/Apply 경로; supplied Default + partial resolve도 제공한다.
실제 CDN 인프라/업로드, registry release, Git commit/push는 실행하지 않았다. 이러한 운영 배포는 이번 범위와 분리하며 Theme-only artifact와 절차는 준비했다. Scoped Portal 자동 전달은 기존 FOLLOW_UP을 유지한다.
## 작업 시간 (KST)
- Started At: 2026-10-06 15:41:55 KST
- Completed At: 2026-10-06 16:16:53 KST
- Actual Duration: 34분 58초
- Estimated Duration: 45~70분 (범위 수정 후 50~75분)
- Variance: 최초 예상보다 10분 2초~35분 2초 단축
+270
View File
@@ -0,0 +1,270 @@
# HM Custom Implementation Inventory
> 조사 기준일: 2026-10-06. 실제 코드(`packages/ui/src`) 기준의 현황 기록이며 확정 규격이 아니다. 구현은 바꾸지 않았다.
> 신규 구현은 [AI/Component 구현 원칙](shadcn-기반-구현-원칙.md)과 [Navigation Single Mode Policy](navigation-single-mode.md)의 2026-10-07 확정 정책을 따른다. 아래 조사 이력은 운영 예제나 정책 예외를 허용하는 근거가 아니다.
> Select의 `UNDER_REVIEW`는 2026-10-06 조사 당시 설계 평가이며 현재 작업 진행 상태를 뜻하지 않는다. 현재 API·동작은 `docs/components/select.md`와 실제 구현·대표 Story를 따른다.
> **현재 탐색 기준 (2026-10-08 개발 저장소)**: UI 구현 TSX 44개, 공개 Component 모듈 44개(222 named export), Component Story 파일/그룹 44개, Component 문서 45개(`field.md`는 독립 구현이 아닌 소비 가이드)다. Pattern은 소스 디렉터리/공개 모듈 8개(62 named export), Story 파일/그룹 6개, Pattern 문서 7개다. 확정 배포 기준 `HMDesignTest.zip`에는 Component 구성은 동일하고 Pattern 모듈 7개(50 named export)가 포함되며 Dashboard Status 모듈은 없다. 파일·심볼·그룹은 서로 다른 집계 단위다. 현재 경로 연결은 [Component / Pattern Reference Index](component-pattern-reference.md)를 따른다. 아래 표와 분석은 2026-10-06 당시의 분류 기록이다.
## 0. 한 줄 요약
shadcn/Radix를 그대로 쓰지 않고 **실제로 API·State·Behavior·Structure가 추가된 Component는 44개 중 15개**다. 나머지 29개는 shadcn 구조를 유지하고 CSS/Token만 얹었다. Pattern 7종은 모두 기존 Component의 조합이다.
| 질문 | 답 |
|---|---|
| 꼭 필요한가? | Component 기준 15개 중 `REQUIRED` 13, `CAN_SIMPLIFY` 1(Textarea), `UNDER_REVIEW` 1(Select). 항목 단위로는 `CAN_SIMPLIFY` 3건, `LEGACY` 1건(Button alias) (§4, §11, §12) |
| Public API에 영향을 주는가? | 12건이 consumer에게 노출된다 (§8). 내부 전용은 6건 (§7) |
| shadcn 업데이트 시 관리해야 하는가? | **Tabs(HIGH)** 우선, 이어서 Button·Select(MEDIUM). 나머지는 LOW |
> 위 숫자는 아래 표의 합이다. 표를 고치면 이 요약도 함께 고친다.
## 1. 목적
- shadcn 업데이트·재생성 시 "무엇을 손으로 지켜야 하는가"를 한 문서에서 확인한다.
- HM 스타일이 들어갔다는 이유만으로 Custom으로 부르지 않는다. Behavior / Structure를 유지하고 CSS/Token만 바꾼 것은 `HM_THEME`다.
- 이 문서는 가시화·분류·유지보수 기준만 다룬다. 제거·단순화 판단은 별도 작업이다.
- Visual 값(height, radius, color 등)은 각 `docs/components/<name>.md`, `docs/patterns/<name>.md`가 담당하므로 여기에 나열하지 않는다.
## 2. 분류 기준
| 분류 | 정의 |
|---|---|
| `SHADCN_BASE` | shadcn/Radix 구조·API 그대로. `.tsx`에 HM 전용 코드가 없고 Theme CSS도 얇은 Token 매핑 수준 |
| `HM_THEME` | Behavior / Structure는 shadcn/Radix 그대로, Visual만 HM CSS / Token. 독립 Custom 아님 |
| `HM_CUSTOM` | shadcn에 없는 Public API, 상태/behavior, Radix 동작 확장, 추가 Context/state, helper logic, 구조 확장 중 하나 이상 |
| `HM_COMPOSITION` | 기존 HM/shadcn Component를 조합한 Pattern |
| `STORY_ONLY` | 검증용 Story, StateBoard 등. Production Custom 아님 |
조사 한계(반드시 읽을 것):
- 현재 저장소는 Git으로 관리한다. 2026-10-06 조사는 shadcn 원본 사본과의 Git diff를 수행하지 않았다. 따라서 당시 분류는 "shadcn 원본과의 diff"가 아니라 **현재 코드와 코드 주석을 읽고 shadcn v4(Radix) 구조에 대한 지식과 대조**해 판정했다. 경계 항목은 `shadcn add`로 원본을 받아 diff하면 확정할 수 있다.
- `SHADCN_BASE`와 `HM_THEME`의 경계는 휴리스틱이다: `.tsx`가 shadcn 그대로이면서 `packages/ui/src/theme/<name>.css`가 약 70줄 이하이면 `SHADCN_BASE`, 그보다 크면 `HM_THEME`. 둘 다 "Custom 아님"이므로 유지보수 판단에는 영향이 없다.
- Story는 전수 검토하지 않고 공용 helper와 파일 구성만 확인했다.
## 3. 전체 요약
| Category | Count | 비고 |
|---|---:|---|
| Component 총계 | 44 | `packages/ui/src/components/ui/` |
| `SHADCN_BASE` | 16 | §5 |
| `HM_THEME` | 13 | §5 |
| `HM_CUSTOM` | 15 | §4 (Public 12건 + Internal 6건이 이 15개에 걸쳐 있음) |
| `HM_COMPOSITION` | 7 | Pattern (§6, Component 44개와 별도) |
| `STORY_ONLY` | 4 묶음 | §9 |
## 4. Components — HM_CUSTOM
한 Component 안에 Visual(HM_THEME)과 구조 Custom이 함께 있을 수 있다. 아래는 구조·API·상태 부분만 적었다. 평가: `REQUIRED` 현재 필요 / `CAN_SIMPLIFY` shadcn 방식으로 단순화 가능 / `LEGACY` 필요성 불명확 / `UNDER_REVIEW` 작업 중·미결정.
| Component | Base | Custom 종류 | Scope | 평가 | Risk |
|---|---|---|---|---|---|
| Button | shadcn + Radix Slot + CVA | API, Structure | Public | REQUIRED (+ alias는 LEGACY) | MEDIUM |
| Input | shadcn | API | Public | REQUIRED | LOW |
| Textarea | shadcn | API | Public | CAN_SIMPLIFY | LOW |
| Card | shadcn | API | Public | REQUIRED | LOW |
| Select | Radix Select | API, State, Structure | Public + Internal | UNDER_REVIEW | MEDIUM |
| Tabs | Radix Tabs | API, State, Behavior, Structure | Public + Internal | REQUIRED | HIGH |
| Dialog | Radix Dialog | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
| Sheet | Radix Dialog | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
| Drawer | vaul | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
| Alert Dialog | Radix Alert Dialog | API | Public | REQUIRED | LOW |
| Combobox | Popover + Command (cmdk) | API, State, Composition | Public | REQUIRED | LOW |
| Date Picker | Popover + Calendar (react-day-picker) | API, State, Behavior | Public + Internal | REQUIRED | LOW |
| Popover | Radix Popover | Behavior | Internal | REQUIRED | LOW |
| Collapsible | Radix Collapsible | Behavior | Internal | REQUIRED | LOW |
| Slider | Radix Slider | Behavior | Internal | REQUIRED | LOW |
### 4.1 항목별 기록
**Button** — `button.tsx`, `theme/button.css`
- 추가: `variant`를 역할(`primary|secondary|modify|destructive|cancel`)로 교체, `appearance`(`brand|solid|outline`) 추가, `data-variant/data-appearance/data-size`. `default`·`outline`은 shadcn 호환 alias로 `primary`/`primary+outline`에 매핑, `ghost`·`link`는 shadcn 기본 유지.
- 이유: ui_viz 역할/처리(treatment) 2축 규격. `<Button />`만으로 HM 디자인 적용.
- Public API 영향: 있음(`variant` 값 집합, `appearance`). 기존 shadcn `variant="default|outline"` 코드는 alias로 동작.
- 구조 주의: `data-slot="button"`을 `{...props}` **뒤에** 둔다. 앞에 두면 `<XTrigger asChild><Button/></XTrigger>`가 trigger의 `data-slot`으로 덮어써 HM 스타일이 사라진다(§7 I3).
- 대체 가능한 shadcn 방식: shadcn `variant` 값에 HM 역할을 추가하는 방식은 가능하나 `appearance` 2축은 표현 불가.
- 리스크 사유: `buttonVariants` CVA 정의를 통째로 다시 썼다. shadcn이 `buttonVariants`를 바꾸면 수동 병합이 필요하다. 코드에 `dark:` 잔재 2곳(§11).
- 유지 필요성: REQUIRED. 단, `default`/`outline` alias는 LEGACY(호환 목적이며 소비처가 정리되면 제거 후보).
**Input** — `input.tsx`
- 추가: `size="small|medium|large"`(기본 large). 네이티브 `size`(문자 폭) 속성은 `Omit`으로 가려져 **사용 불가**.
- Public API 영향: 있음, 네이티브 속성 가림. 대체: `className` 높이 지정(2축 상태 규칙은 불가). 유지 REQUIRED. Risk LOW.
**Textarea** — `textarea.tsx`
- 추가: `resize="vertical|none|both"`(기본 vertical)를 `data-resize`로 노출. readonly→none은 CSS에서 처리.
- 대체: `className="resize-none"` 등. 기본 정책을 컴포넌트가 갖게 하려는 목적이라 단순화 가능. CAN_SIMPLIFY, Risk LOW.
**Card** — `card.tsx`
- 추가: `surface="surface|content"`(`data-surface`). `size="sm"`은 shadcn 기존 API.
- 유지 REQUIRED(ui_viz Content Card 면 구분). Risk LOW.
**Select** — `select.tsx`, `theme/select.css` (size 계약 구현됨; 구조 선택은 아래 UNDER_REVIEW)
- 추가(조사 시점 코드):
- `SelectTrigger`의 `size="medium|large"`(shadcn `sm|default` 대체), `data-size`.
- `SelectSizeContext` + `Select` Root를 Provider로 감싸 `useState`로 size 보관, Trigger가 `useLayoutEffect`로 등록. Portal 안의 `SelectContent`가 `data-size`를 읽는다. 즉 **Root가 shadcn처럼 순수 `SelectPrimitive.Root`가 아니라 Provider 래퍼**다.
- 아이콘을 Radix 기본 대신 18×18 SVG로 교체, `SelectItem`의 check indicator 제거(선택 표시는 배경 면), `SelectContent` 기본값 `position="popper"`, `sideOffset=-1`(border 1px 공유 seam).
- long text ellipsis와 seam(`--hm-select-seam-color`)은 CSS 처리(`select.css`).
- Public API 영향: `size` prop(Public). Context·`setSize`는 노출되지 않는 Internal.
- 대체 가능한 shadcn 방식: shadcn은 Content가 `size`를 몰라도 되게 Trigger 쪽만 `data-size`를 둔다. Content까지 size 연동이 필요한 이유가 Portal 때문이므로 대안은 (a) Content에도 `size` prop을 직접 전달, (b) CSS만으로 처리, (c) 현 Context 유지. **어느 쪽이 맞는지 결정되지 않았다.**
- 유지 필요성: UNDER_REVIEW. Risk MEDIUM(Root 래퍼 + 렌더 후 `useLayoutEffect`로 state를 갱신하는 구조라, 첫 렌더에서 Content가 기본 `large`로 보일 수 있는 타이밍 의존이 있음).
- 이 항목은 Final Spec이 아니다.
**Tabs** — `tabs.tsx`, `theme/tabs.css`
- 추가:
- `TabsList`에 `fill`(동일 폭·초록 선택 border·곡선 연결)과 `panelSurface="surface|content"` prop.
- `TabsList`가 Radix List를 `<div data-slot="tabs-frame">`로 **한 겹 감싼다**(DOM 구조 변경).
- 넘침 감지(`ResizeObserver`, `MutationObserver`, scroll 리스너, `document.fonts.ready`), 이전/다음 이동 버튼(`aria-label` 한국어 고정), 선택 탭 가시 유지(`keepSelectedVisible`), `prefers-reduced-motion` 분기, `useId`로 List id 생성, 외부 `ref` 병합.
- 연결 곡선은 `tabs.css`의 `::before/::after` + `radial-gradient`(HM 고유 구조 CSS).
- Public API 영향: `fill`, `panelSurface`(Public). 나머지는 Internal.
- 대체: shadcn Tabs는 위 기능이 전혀 없다. 넘침 버튼 없이 가로 스크롤만 허용하는 단순화는 가능하나 ui_viz D-090 규격을 포기해야 한다.
- 유지 REQUIRED. **Risk HIGH**: 원본 `TabsList` 구조를 크게 바꿨고 Observer 기반 로직과 한국어 문구가 컴포넌트 안에 있다.
- 2026-10-06: 대표 형태를 Primary(`fill` + `panelSurface="content"`)·Secondary(`fill` + `panelSurface="surface"`) 두 가지로 정리했다. Story 노출만 줄였고 코드·API·CSS branch는 그대로라 **Custom complexity 변화 없음**. non-fill branch는 REMOVE_CANDIDATE(사용자 확정 대기).
- 2026-10-08 추가 개선 E 승인: 당시 `fill=false`, `panelSurface="surface"` 기본값을 현재 `fill=true`, `panelSurface="content"`(Primary)로 변경했다. Props 이름·타입·기존 CSS 디자인 값·선택자·Observer 동작은 유지하므로 **Custom complexity 변화 없음**. Legacy non-fill은 `fill={false}`로 유지하고 OLD_TEST_ONLY / REMOVE_CANDIDATE 삭제 대기 이력도 보존한다. 외부 기본 렌더링·명시적 Props 조합·CSS Override 영향은 [Tabs 문서 §15](../components/tabs.md#15-외부-소비자-영향--호환성-2026-10-08)를 따른다. 외부 소비자 전체 호환성은 미검증이다.
**Dialog / Sheet / Drawer** — `dialog.tsx`, `sheet.tsx`, `drawer.tsx`
- 추가(Public): `DialogBody`, `SheetBody`, `DrawerBody` — 스크롤 소유 영역(Header/Footer 고정, Body만 스크롤). shadcn에는 없는 구조 API.
- 추가(Internal): `*Close`가 `asChild`일 때 `data-slot`을 넘기지 않는 규칙(§7 I3).
- Drawer 추가: `autoFocus` 기본값을 `true`로(vaul 기본은 `false`라 modal에서 포커스가 trigger에 남음). 소비자가 `autoFocus={false}`로 되돌릴 수 있다 → Public 동작 차이.
- `showCloseButton`, `side`는 shadcn 기존 API(Custom 아님). Radius·shadow 등은 HM_THEME.
- 대체: Body는 `className`이 붙은 `div`로 대체 가능하나 `data-slot` 기반 CSS 선택이 사라진다. REQUIRED, Risk LOW.
**Alert Dialog** — `alert-dialog.tsx`
- 추가: `AlertDialogAction`/`AlertDialogCancel`이 `variant|appearance|size`를 받고 `Button asChild`로 합성. 기본값 Action=`primary/solid`, Cancel=`cancel/solid`. shadcn도 Button 스타일을 입히지만 `appearance`는 HM 전용.
- REQUIRED, Risk LOW.
**Combobox** — `combobox.tsx`
- shadcn에는 파일이 아닌 recipe(Popover+Command)만 있고 HM은 이를 **`options` 기반 컴포넌트**로 만들었다. Public: `options`(value/label/disabled), `value/defaultValue/onValueChange`, `placeholder/searchPlaceholder/emptyText`, `size="medium|large"`. State: `open`, 비제어용 `inner`(제어/비제어 분기 직접 구현). 한국어 기본 문구 포함.
- 대체: Popover+Command를 소비자가 직접 조합(shadcn 방식). REQUIRED(서비스 공통 필요), Risk LOW(원본 파일이 없어 업스트림 충돌 없음).
- 내부: `PopoverContent`에 인라인 `--hm-popover-padding/width` 주입 → CAN_SIMPLIFY(§11).
**Date Picker** — `date-picker.tsx`
- 같은 성격. Public: `value/defaultValue/onValueChange`, `placeholder`, `size`, `format`, `calendarProps`(Calendar 옵션 일부). 제어/비제어 상태 직접 구현. Behavior: `onOpenAutoFocus`에서 Calendar의 tab stop 날짜로 포커스 이동(Internal). 한국어 기본 포맷.
- 대체: Popover+Calendar 직접 조합. REQUIRED, Risk LOW.
**Popover / Collapsible** — `popover.tsx`, `collapsible.tsx`
- `PopoverClose`, `CollapsibleTrigger`의 `asChild` data-slot 규칙만 추가(§7 I3). Risk LOW.
**Slider** — `slider.tsx`
- 추가: `aria-label`을 Root가 아니라 **각 Thumb**에 전달(Range는 `"라벨 1/2"`). `role="slider"`가 Thumb에 있기 때문. Internal. shadcn 원본에는 없음. REQUIRED(접근성), Risk LOW.
## 5. Components — HM_THEME / SHADCN_BASE
`.tsx`에 HM 구조·API·State 추가가 없는 Component. 시각 값은 각 Component 문서가 관리한다.
**SHADCN_BASE (16)**: Accordion, Alert, Aspect Ratio, Avatar, Badge, Breadcrumb, Checkbox, Hover Card, Label, Progress, Radio Group, Scroll Area, Separator, Skeleton, Switch, Tooltip
**HM_THEME (13)**: Calendar, Carousel, Command, Context Menu, Dropdown Menu, Menubar, Navigation Menu, Pagination, Resizable, Sonner, Table, Toggle, Toggle Group
참고(Custom으로 보지 않은 이유):
- Alert·Badge: 사용 가능한 `variant`를 HM Theme에 있는 의미로 **줄였다**(Alert=default/destructive, Badge=4종). API 축소이며 추가가 아니다.
- Calendar: 기본 `locale`을 한국어(`ko`)로 두었다. shadcn도 `Chevron` 컴포넌트를 교체한다. 기본값 하나의 차이라 THEME로 둔다.
- Command: shadcn의 `CommandDialog`가 **없다**(의도적 제외, 코드 주석 명시). 기능 제거이며 추가 Custom 아님.
- Toggle Group: shadcn 그대로 Context를 쓰며 `spacing`을 CSS 변수(`--hm-toggle-group-gap`)로 넘기는 한 줄이 추가됐다.
- Carousel: shadcn 원본 로직(Context, Embla, 키보드)이 그대로다.
- Tooltip: 원본처럼 `Tooltip`이 자체 `TooltipProvider`를 가진다(shadcn v4).
## 6. Patterns — HM_COMPOSITION
`packages/ui/src/patterns/`. 모두 기존 HM Component의 조합이며 별도 primitive가 아니다. 전부 shadcn에 없는 **Public API**다(Pattern 이름 자체).
| Pattern | Composition | Custom Logic | Public API |
|---|---|---|---|
| GNB | Navigation Menu, Dropdown Menu, Avatar, Sheet, Button | 있음: `GNBMobileMenu`(open state, Context, Sheet 합성), `GNBMobileLink`가 `preventDefault`된 클릭에도 Sheet를 닫음, `GNBUser` 기본 `aria-label` | `GNB`(`sticky`), `GNBBrand`, `GNBNav`, `GNBUtilities`, `GNBUser`, `GNBMobileMenu`, `GNBMobileLink`, `GNBMobileGroup` |
| LNB | Collapsible, Slot | 없음(`LNBGroup`은 Collapsible 합성) | `LNB`, `LNBBrand`, `LNBNav`, `LNBItem`, `LNBGroup`, `LNBProfile` |
| Dashboard Layout | 현재는 Dashboard 내부 Header/Menu/Sidebar 조합만 허용 | 없음(레이아웃) | `DashboardLayout`(`header`, `sidebar`), `DashboardMain`, `DashboardContent` |
| Page Header | Breadcrumb·Button을 child로 받음 | 없음 | `PageHeader`, `PageHeaderContent/Title/Description/Actions` |
| Loading Screen | 없음(자체 요소) | 24개 막대 렌더, CSS 애니메이션·reduced-motion, `role="status"` | `LoadingScreen`(`fullscreen`) |
| Filter Bar | Input, Select·Combobox·Date Picker·Button을 child로 받음 | 있음: `FilterBarSearch`(검색 아이콘 + Input + 지우기 버튼, `aria-label` 기본값) | `FilterBar`, `FilterBarSearch`, `FilterBarActions` |
| Data Table | Table, Checkbox, Dropdown Menu, Pagination, Select, Skeleton, Button + TanStack Table v9 | 있음: 기능(feature) 등록, `useDataTable`, `dataTableSelectColumn`, `DataTableColumnHeader`(정렬 순환·`aria-sort`), `DataTableViewOptions`, `DataTablePagination`(`pageRange`), loading/empty 상태 | `DataTable`, `useDataTable`, `dataTableSelectColumn`, `DataTableColumnHeader`, `DataTableViewOptions`, `DataTablePagination` |
CSS 자체가 구조를 구현하는 항목(HM 고유): GNB 아래쪽 concave join과 current 항목 연결 곡선(`gnb.css`), LNB와 콘텐츠 join(`lnb.css`, `--hm-lnb-join`), Loading Screen 막대 애니메이션(`loading-screen.css`). 각 Navigation Mode는 독립 레이아웃을 소유하며 Dashboard에 외부 GNB/LNB를 넣지 않는다.
과거 조사에서는 GNB+LNB 동시 조립과 Brand ownership을 기록했으나 현재 단일 Navigation 정책으로 대체되었다. 혼합 Story는 기술 검증 목적의 `verification` 태그와 **VERIFICATION ONLY** 표시가 있을 때만 유지할 수 있으며 운영 Reference로 사용하지 않는다(`docs/patterns/gnb.md`, `lnb.md`).
## 7. Internal Custom
consumer에게 노출되지 않는 내부 구현. 6건.
| # | 항목 | 위치 | 평가 | Risk |
|---|---|---|---|---|
| I1 | Select size Context + `useLayoutEffect` 등록 (**작업 중일 수 있음**) | `select.tsx` | UNDER_REVIEW | MEDIUM |
| I2 | Tabs 넘침 측정(Observer·scroll·state) 과 이동 버튼 | `tabs.tsx` | REQUIRED | HIGH |
| I3 | `asChild` 시 `data-slot`을 child에 남기는 규칙(Dialog/Sheet/Drawer/Popover/Collapsible의 Close·Trigger, Button의 `data-slot` 후순위 배치) | 여러 파일 | REQUIRED | LOW |
| I4 | Slider thumb별 `aria-label` 전달 | `slider.tsx` | REQUIRED | LOW |
| I5 | Date Picker `onOpenAutoFocus` 날짜 포커스 | `date-picker.tsx` | REQUIRED | LOW |
| I6 | Combobox·Date Picker의 `PopoverContent` 인라인 `--hm-popover-*` 변수 | `combobox.tsx`, `date-picker.tsx` | CAN_SIMPLIFY | LOW |
## 8. Public API Custom
shadcn 원본에 없는 consumer-facing 확장. Component 12건 + Pattern 전체(§6).
| # | Component | HM API | shadcn 원본 | Risk |
|---|---|---|---|---|
| P1 | Button | `variant`(역할 5종+alias), `appearance` | `variant` 값이 다름 | MEDIUM |
| P2 | Input | `size="small|medium|large"` | 네이티브 `size` 가림 | LOW |
| P3 | Textarea | `resize` | 없음 | LOW |
| P4 | Card | `surface` | 없음(`size`는 있음) | LOW |
| P5 | SelectTrigger | `size="medium|large"` (**작업 중일 수 있음**) | `sm|default` | MEDIUM |
| P6 | TabsList | `fill`, `panelSurface` (+ 프레임 DOM) | 없음 | HIGH |
| P7 | Dialog | `DialogBody` | 없음 | LOW |
| P8 | Sheet | `SheetBody` | 없음 | LOW |
| P9 | Drawer | `DrawerBody`, `autoFocus` 기본 `true` | 없음 / vaul 기본 `false` | LOW |
| P10 | AlertDialogAction / Cancel | `appearance` (`variant`·`size` 포함) | `appearance` 없음 | LOW |
| P11 | Combobox | `options` 등 전체 API | 파일 없음(recipe) | LOW |
| P12 | Date Picker | `format`, `calendarProps` 등 전체 API | 파일 없음(recipe) | LOW |
## 9. Storybook-only Custom (`STORY_ONLY`)
Production Custom 목록에 포함하지 않는다. 4묶음.
| 묶음 | 위치 | 내용 |
|---|---|---|
| StateBoard helper | `src/components/story-board.tsx` | `BoardState`(`note`, `noteAfter`, `wide`), `StateBoard`. 39개 Story 파일이 사용 |
| 고정 상태·검증 Story | `src/components/*.stories.tsx`, `src/patterns/*.stories.tsx` | `States`, `StandardStates`, `ListSizeComparison` 등 비교·검증용 export. 전수 검토하지 않았음 |
| 샘플 데이터 | `src/patterns/sample-users.ts` | Data Table Story용 |
| Demo / CENTER Story | `src/hm-theme.css`, `src/theme/preview.ts`, `src/center/*` | Demo 스타일, preview 설정, `center-graphic` 전용 Story(UI 규격과 분리) |
## 10. External Library Base
| Library | 사용처 | 구분 |
|---|---|---|
| `radix-ui` | 대부분의 primitive | Base. Custom은 §4 |
| `class-variance-authority` | Button, Badge, Alert, Toggle | shadcn 구조. Button만 CVA 정의를 HM 값으로 교체 |
| `cmdk` | Command, Combobox | Library Base + HM Theme (Combobox는 HM_CUSTOM 합성) |
| `vaul` | Drawer | Library Base + HM Custom(`DrawerBody`, `autoFocus` 기본값) |
| `embla-carousel-react` | Carousel | Library Base + HM Theme (shadcn 로직 그대로) |
| `react-resizable-panels` | Resizable | Library Base + HM Theme |
| `react-day-picker` | Calendar, Date Picker | Library Base + HM Theme (Date Picker는 HM_CUSTOM 합성) |
| `sonner` | Sonner(Toast) | Library Base + HM Theme |
| `@tanstack/react-table` v9 | Data Table | Library Base + HM Composition (v9 feature 등록 방식) |
| `lucide-react` | 아이콘 전반 | shadcn 기본 |
## 11. CAN_SIMPLIFY 후보
평가만 한다. 변경은 하지 않았다.
| 항목 | 단순화 방향 | 비고 |
|---|---|---|
| Textarea `resize` (P3) | `className`(`resize-none` 등)로 대체 가능 | 기본 정책을 컴포넌트에 두려는 의도라면 유지 |
| Combobox·Date Picker 인라인 `--hm-popover-*` (I6) | CSS 선택자·`data-slot`으로 이동 | |
| `dark:` 잔재 | 현재 `button.tsx` 2개 행에만 남아 있고 `select.tsx`에는 없다. 기존 upstream `dark:` 분기는 보존할 수 있으며 새 HM Dark 분기는 추가하지 않는다 | HM_CUSTOM이 아니라 shadcn 원본 잔재. 정리 후보 |
## 12. UNDER_REVIEW / LEGACY
| 항목 | 상태 | 비고 |
|---|---|---|
| Select `size` Context (P5 + I1) | UNDER_REVIEW | 조사 당시 미결 설계 평가. 대안은 §4.1 Select 참조. 현재 작업 중이라는 뜻이 아니며 현재 API는 Select 문서·코드를 따른다 |
| Button `default`/`outline` alias (P1) | LEGACY | shadcn 호환용. 소비처 사용 여부 미확인 |
## 13. 유지 원칙
1. HM_THEME·SHADCN_BASE는 CSS/Token만 바꾼다. `.tsx`에 구조·상태를 추가하려면 `shadcn-기반-구현-원칙.md` §6의 Custom 허용 조건과 기록 요건을 확인하고 이 문서에 행을 추가한다.
2. shadcn 재생성·업데이트 시 우선 확인 순서: **Tabs → Button → Select → 나머지 Body/Close 규칙(I3)**. 이 파일들은 덮어쓰지 않고 수동 병합한다.
3. `data-slot`을 child에 남기는 규칙(I3)은 새 `asChild` 지원 컴포넌트에도 같은 방식으로 적용한다.
4. 새 Pattern은 §6 표에 "Composition / Custom Logic / Public API"를 한 행으로 기록한다.
5. 이 문서의 수치는 조사 시점(2026-10-06) 기준이다. Select 설계 평가가 변경되면 §4·§7·§8·§12를 함께 갱신한다.
## 2026-10-06 Theme infrastructure
Default/External 동일 Runtime 계약, 패키지 theme API, Semantic/팔레트 연결과 Accent derived 공통화는 HM_THEME / Theme infrastructure다. Component DOM/props/state/Portal/breakpoint를 바꾸지 않았으므로 위 HM_CUSTOM Inventory 수를 늘리지 않는다. 상세: [Runtime Theme](runtime-theme.md).
+64
View File
@@ -0,0 +1,64 @@
# Navigation Single Mode Policy
2026-10-07 사용자 확정 정책. 이전 GNB/LNB/Dashboard 조합 기준을 대체한다. 2026-10-08 GNB ONLY 내부 Sidebar 허용 및 DASHBOARD ONLY Sidebar 운영 사용 금지를 명확히 했다.
**ONE PAGE = ONE NAVIGATION SYSTEM = ONE HEADER/NAVIGATION EXPERIENCE**
## 적용 범위
AI 기반 코드 생성과 제안, 신규 페이지 구현, 기존 화면 수정, 프로토타입, Storybook 대표/PRIMARY Story, 실제 Production 구현 전체에 동일하게 적용한다. AI는 별도 요청이 없는 한 Navigation 조합을 생성하거나 제안하지 않는다. 별도 요청도 혼합 운영 구조를 자동 허용하지 않으며 정책 변경 또는 기술 검증의 목적을 먼저 구분한다.
## 모드 선택
페이지 구현 전에 `navigationMode = GNB | LNB | DASHBOARD` 중 정확히 하나를 결정한다. 이 표기는 페이지 설계 계약이며 현재 공통 Page 컴포넌트의 prop이 아니다.
| 선택 | 허용 구조 | 함께 사용하지 않는 체계 |
|---|---|---|
| GNB | Page → GNB Header + Content, 또는 GNB Header + 내부 Sidebar + Content | 독립 LNB Mode, Dashboard |
| LNB | Page → LNB + Content | GNB, Dashboard |
| DASHBOARD | Page → Dashboard 전용 Header(48px)·업무 상태 필터 + Content, Sidebar 없음 | 모든 Sidebar, 외부 GNB, 일반 LNB |
독립 Navigation Mode를 결합하는 GNB + LNB, GNB + Dashboard, LNB + Dashboard, GNB + LNB + Dashboard는 모두 금지한다. GNB ONLY 내부 Sidebar의 구현 부품 재사용은 아래 범위에서 허용한다. 콘텐츠의 Breadcrumb, Filter Bar, 사용자 Dropdown 등은 새로운 페이지 Navigation Mode가 아니다.
## 선택 근거
1. 사용자 요청에 모드가 있으면 우선 적용한다. 요청이 독립 모드 혼합을 포함하면 이 정책과의 충돌을 기록하고 정책 변경인지 기술 검증인지 확인한다.
2. 미지정이면 제공된 화면의 Navigation 구조와 현재 대표 Story를 대조한다. 서비스 전역 상단 메뉴·브랜드·사용자 영역이 중심이면 [GNB](../patterns/gnb.md), 현재 영역의 계층 메뉴·독립 Sidebar와 모바일 Header가 중심이면 [LNB](../patterns/lnb.md), Dashboard 내부 Header/상태 메뉴·최소 폭 프레임이 중심이면 [Dashboard](../patterns/dashboard-layout.md)를 선택한다.
3. 일반 업무 목록이나 DataTable 사용 여부만으로 GNB/LNB를 확정하지 않는다. GNB에서도 내부 Sidebar Composition을 사용할 수 있고, Dashboard Header는 GNB가 아니다.
4. 선택 근거가 부족하면 가정한 모드와 이유를 보고한다. 기존 화면과의 일관성을 판단할 정보가 없으면 사용자 확인 필요 사항으로 남긴다. 모든 일반 화면에 적용하는 기본 모드는 없다.
## GNB ONLY 내부 Sidebar
- GNB Sidebar는 GNB ONLY의 하위 화면 구성이다. 공개 `LNB / LNBNav / LNBGroup / LNBItem`과 필요한 모바일 부품을 재사용해도 독립 LNB ONLY Mode를 추가한 것이 아니다.
- 메뉴 DOM/data-slot, 폭, 폰트, 아이콘, 간격, Depth, Hover/Current, 그룹 펼침·접힘과 메뉴 스크롤은 기존 LNB 계약을 유지한다. Sidebar 범위의 `--hm-lnb-background`만 `var(--hm-sem-palette-green-light-2)`로 지정한다. 공통 Theme 값과 LNB 기본 스타일은 변경하지 않는다.
- 브랜드·사용자 영역은 GNB가 담당한다. 기본 Sidebar에는 `LNBBrand / LNBProfile`을 넣지 않는다. Desktop/Mobile 모두 현재 경로는 하나로 관리하며, Sidebar 그룹과 GNB 팝업의 열림 상태는 독립적이다. 선택에 따른 새로운 자동 펼침·접힘은 추가하지 않는다.
- 이 허용은 GNB 내부 Sidebar에 한정한다. 독립 LNB Header/브랜드 체계의 동시 추가, Dashboard와의 조합, 기존 혼합 verification Story의 운영 승격은 허용하지 않는다. Dashboard Header는 GNB가 아니다.
## Responsive
- GNB는 자체 Responsive / Mobile Header를 사용한다.
- GNB Sidebar는 ≥1024px에서 표시하고 ≤1023px에서 숨긴다. 모바일은 기존 GNB Header의 메뉴 버튼 하나와 Sheet 하나에 LNB 모바일 링크/그룹을 재사용한다. `LNBMobileHeader`나 두 번째 Sheet를 추가하지 않는다. Sheet 기본 배경, 링크 높이·폰트·선택 동작, Escape·포커스 복귀를 유지한다. 그룹은 기존 모바일 링크 목록이며 새 disclosure 동작을 만들지 않는다.
- LNB는 ≥1024px에서 Desktop LNB만, ≤1023px에서 LNB Mobile Header만 표시한다. Mobile Header와 Sheet는 같은 메뉴·선택 상태를 공유하는 LNB의 모바일 표현이며 별도의 GNB가 아니다.
- 독립 LNB ONLY를 Desktop LNB → Mobile GNB Mode로 전환하거나 모바일 두 종류를 동시에 노출하지 않는다. GNB 내부 Sidebar의 모바일 표현은 처음부터 끝까지 GNB ONLY다.
- Dashboard 운영 화면은 모든 viewport에서 Sidebar 없이 전용 Header(48px)·업무 상태 필터·Content를 유지한다. 최소 폭은 960px이며 그 이하에서는 Dashboard 내부에서 가로 스크롤한다. 일반 LNB의 모바일 숨김이나 GNB breakpoint에 의존하지 않는다.
## Layout 및 Storybook
Navigation 간 height/width 계산이나 CSS 변수·breakpoint·spacing 교차 의존성을 만들지 않는다. Dashboard 운영 화면의 `header`에는 전용 DashboardHeader를 넣고 `sidebar`는 전달하지 않는다. 각각의 모드는 자기 변수와 공통 Semantic Token을 사용한다.
`DashboardSidebar` 공개 Export와 선택적 `sidebar` slot은 기존 소비자 호환성·검증을 위해 유지하는 API다. 존재가 운영 허용을 뜻하지 않으며 신규 Dashboard 운영 화면에는 사용하지 않는다. Sidebar 포함 1232px 규칙은 해당 호환성·검증 구성에만 남는 값으로 현재 운영 기준이 아니다. 기능 제거·런타임 차단은 소비처 영향 조사와 사용자 결정을 거치는 별도 작업이다.
GNB 내부 Sidebar의 Composition은 기존 LNB 구현을 재사용하기 위해 `--hm-lnb-width / --hm-lnb-join`과 LNB 부품의 토큰을 소비할 수 있다. 이는 독립 Navigation 간 결합이 아니다. 높이는 소비자 프레임의 Grid 행으로 배분하고 GNB/LNB/Dashboard의 토큰 정의나 breakpoint를 서로 수정하지 않는다. 사용 구조는 [GNB §15](../patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08)를 따른다.
대표/PRIMARY Story는 선택된 모드의 운영 예제만 보여준다. 기존 혼합 Story와 Dashboard Sidebar Story가 기술 검증에 필요한 경우에만 `verification` 태그와 화면·Story 이름의 **VERIFICATION ONLY — 운영 사용 금지** 표시로 유지한다. 운영 예제, 신규 코드 생성의 근거로 사용하지 않는다. 과거 검증 기록은 HISTORICAL로 보존하며 현재 운영 계약으로 재해석하지 않는다.
## 구현·리뷰 완료 조건
- 페이지 모드가 하나로 결정되어 있고 다른 Navigation 체계를 추가하지 않았는가?
- 모바일도 같은 모드의 Responsive Variant이며 Navigation 두 종류가 동시에 보이지 않는가?
- LNB의 1024/1023 경계와 Sidebar 없는 Dashboard의 48px Header·업무 상태 필터·960px 최소 폭·내부 스크롤 계약을 지키는가?
- Navigation 간 CSS 변수 및 조합 전용 breakpoint/spacing 의존성을 추가하지 않았는가?
- 대표 Story 및 AI 생성 지침이 같은 정책을 따르는가?
- GNB Sidebar가 기존 LNB 부품·단일 현재 경로를 재사용하며 모바일 Header/Sheet/메뉴 버튼이 중복되지 않는가?
관련 구현: [GNB](../patterns/gnb.md), [LNB](../patterns/lnb.md), [Dashboard](../patterns/dashboard-layout.md).
@@ -0,0 +1,39 @@
# 설치 없는 Windows 테스트 배포
현재 검증된 테스트 배포 기준은 `HMDesignTest-Portable-Test-r3.zip`이며 내부 테스트용이다. 이하 실행 절차·포함 범위는 r3 기준이다. 루트 폴더는 `HMDesignTest/`이며 기존 오전 `HMDesignTest.zip`을 대체하거나 덮어쓰지 않는다.
## 테스트 사용자 실행
1. ZIP을 압축 해제한다.
2. `HMDesignTest/start-preview.bat`을 더블클릭한다.
3. 콘솔에 표시된 `http://127.0.0.1:<port>/` 주소를 브라우저에서 연다.
Windows x64용 공식 Node.js 22.23.3 런타임을 동봉한다. Node.js/npm 설치, npm install/ci/run, 패키지/Storybook 빌드, Git Clone/Pull은 필요 없다. 서버는 127.0.0.1에만 바인딩하며 기본 포트는 6007이고(개발 저장소 Storybook 6024와 별개) 6007이 사용 중이면 다음 포트 또는 OS가 배정한 사용 가능한 포트를 안내한다. 기존 프로세스를 종료하지 않는다. 한글·공백 경로를 지원한다. 콘솔에서 Ctrl+C로 종료한다.
## 포함 범위와 Reference
최신 UI Component 44개, 8개 Pattern 모듈(Dashboard Status 포함), 공개 API 및 declarations, UI/CENTER 패키지 dist, Theme/Token/Runtime, Story 소스/설정, 완전한 정적 Storybook, 문서/AI Reference, 기존 화면 예제, 폰트/Asset 및 라이선스를 포함한다. 소스의 저장소 상대 구조를 보존한다.
Dashboard Status의 `active | hold | done`, 필터 `all | active | hold | done | issue`, 독립 issue flag와 CSS/export/문서를 함께 제공한다. 대표 Dashboard Story는 Header 48px, 상태 필터, DASHBOARD ONLY 기준이다. `docs/patterns/dashboard-status-static.html`은 상태 계약 정적 예제이며 전체 Dashboard 대체 실행본이 아니다.
독립 Dashboard export는 포터블 ZIP의 필수 구성이 아니다. 기존 `dist-dashboard/`는 H-1(독립 단일 HTML 미리보기 POC, PASS) 성공 산출물과 동일하지 않다. STEP 5에서 UI Preview를 표시한 미검증 `dist-dashboard/`는 제외하고 검증 대상은 정적 Storybook의 대표 Dashboard로 한정한다. export 소스와 개발자 계약은 보존한다.
`.git`, 인증 정보, node_modules, 캐시, 로그, 로컬 설정, 오래된 중복 ZIP/배포 결과를 제외한다. `FILE-INVENTORY.json`에는 포함 파일의 상대경로·크기·SHA-256이 있다(인벤토리 자신은 제외). 외부 ZIP 해시 파일과 검증 보고서를 함께 제공한다.
## 현재 ZIP의 시점
현재 검증된 기준은 `HMDesignTest-Portable-Test-r3.zip`이다. 아래 r2와 기존 `HMDesignTest-Portable-Test.zip`은 과거 기록으로만 보존하며 현재 기준이 아니다. r3의 정확한 내용·해시는 r3의 BUILD-INFO, Inventory 및 Verification 자료를 기준으로 확인한다.
과거 기록(r2): `HMDesignTest-Portable-Test-r2.zip`은 2026-10-08 최종 통합 PASS 빌드를 재사용하여 Button·Tabs·DatePicker/Combobox·Slider·Checkbox·Tooltip·Popover 개선, GNB 내부 Sidebar, Dashboard Status CSS 공개 경로, PageHeader 대표 Story를 반영한다. 당시 스냅샷·산출물 해시와 독립 실행 결과는 r2의 BUILD-INFO, Inventory 및 Verification 자료를 기준으로 확인한다. 사용자 실행 절차(위 3단계)와 AI/개발자 내부 빌드 절차(README-DEVELOPER의 `npm ci`, `build:packages`)는 서로 다르며 테스트 사용자는 후자를 수행하지 않는다.
## 개발자와 운영 문서의 구분
ZIP 루트 `README.md`는 위 세 단계만 테스트 실행 필수 절차로 안내한다. `README-DEVELOPER.md`와 기존 개발·운영 문서는 소스 수정, 재빌드, 장기 운영용이다. 개발자 절차의 npm 설치/빌드와 Registry/CDN/운영 업로드는 테스트 사용자의 실행 조건이 아니다. 이번 배포에서 운영 서버 업로드, STEP 7 AI 재현 테스트는 수행하지 않는다.
## AI 구현 재현성 검증 상태
STEP 7에서 ZIP 파일 접근 정책으로 실행하지 못한 모델 테스트는 실행 환경의 접근 제한으로 기록한다. 이를 AI의 HM 구현 실패나 지침 불이행 결과로 판정하지 않는다. 실제 모델별 화면 구현·재현성 비교 검증은 완료되지 않았으며, STEP 6의 ZIP 실행·Storybook 검증 통과가 모델별 구현 성공을 보증하지 않는다. STEP 8에서는 모델 테스트를 반복하지 않고 이 미검증 상태만 명시한다.
## 라이선스
공식 런타임의 전체 LICENSE와 README, 다운로드 URL 및 공식 SHA-256 증거를 `runtime/node/`에 보존한다. Node 라이선스는 저작권·허가 고지 보존 조건으로 배포를 허용한다. 원문: https://raw.githubusercontent.com/nodejs/node/v22.23.3/LICENSE . 검증 목록: https://nodejs.org/download/release/v22.23.3/SHASUMS256.txt . 폰트 OFL과 번들 의존성의 라이선스/NOTICE도 포함한다. HM/CENTER의 기존 내부 테스트 범위를 유지하며 별도 오픈소스 라이선스를 부여하지 않는다.
+172
View File
@@ -0,0 +1,172 @@
# Runtime / AI Independent Portability Policy
이 문서는 HM Design System 결과물을 React 외 실행 형식으로 옮길 때 지켜야 하는 공통 계약이다. 특정 AI 모델, IDE, 유료 기능, 프레임워크 또는 빌드 도구를 전제로 하지 않는다.
## 1. Source of Truth와 Target Runtime
디자인 시스템의 기준은 다음 순서다.
1. Design Token과 `themes/*.json`
2. CSS Custom Properties와 Runtime Theme 계약
3. Component visual state 및 interaction contract
4. Component implementation
5. Storybook/Preview
작업 시작 전에 Target Runtime을 기록한다. Source of Truth 우선순위는 `themes/default.json`/공개 Theme contract → Runtime-neutral Component/Pattern contract → CSS Custom Properties → Runtime adapter → Storybook/Preview 순서다.
- `REACT_VITE`: 현재 React/Vite workspace와 `@hm-design/ui` 공개 API를 사용한다.
- `STATIC_HTML`: HTML을 `file://`로 직접 열 수 있어야 한다. JSX/TSX/TypeScript, Vite 전용 기능, bare package import를 사용하지 않는다. 저장소 내 샘플은 승인된 상대 Theme artifact와 상대 font/compatibility CSS를 사용하며, `file://`에서 `/src/...` 절대 경로를 사용하지 않는다.
- `VANILLA_WEB`: 일반 웹 서버 또는 정적 호스팅에서 HTML/CSS/JavaScript로 실행한다.
- `OTHER_RUNTIME`: Vue, Svelte, Angular, Web Component 등으로 변환하더라도 HM Token·Theme·Component Contract를 유지한다.
Runtime이 달라져도 디자인을 재해석하거나 React 구현을 삭제하지 않는다. 기존 구현을 Reference로 삼아 같은 Design Contract와 Theme Contract를 소비하는 adapter/standalone implementation을 추가한다.
## 2. 실제 HM 계약과 파일 경로
다음 파일을 추측으로 복제하거나 별도 값으로 재작성하지 않는다.
- `themes/default.json`: HM Default 컬러의 공식 원본
- `packages/ui/src/theme/contract.ts`: 공개 Runtime Token, CSS property, 타입·검증 계약
- `packages/ui/src/theme/runtime.ts`: Global/Scoped 적용·해제와 atomic validation
- `packages/ui/src/theme/loader.ts`: URL fetch/resolve/load 경로
- `packages/ui/src/theme.css`: package Theme CSS 진입점
- `packages/ui/src/theme/default.css`, `primitives.css`, `<component>.css`: fallback, Semantic, Component Mapping
- `packages/ui/package.json`: 공개 export인 `@hm-design/ui/theme`, `@hm-design/ui/theme.css`, `@hm-design/ui/tailwind.css`
- `docs/guidelines/runtime-theme-contract.md`: Token별 CSS variable, 기본값, 영향 범위
React 소비 앱은 다음 순서로 HM CSS를 로드한다.
```css
@import "tailwindcss";
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
```
Standalone 결과물은 승인된 Theme CSS artifact를 상대 경로로 연결하거나, 프로젝트가 생성한 standalone artifact를 포함한다. AI가 `#...` 색상, 임의 spacing/radius, Theme variable을 추측해 복제하지 않는다. 실제 변수명과 허용 입력은 Runtime 계약표를 기준으로 한다.
### 2.1 Tailwind v4 소비자 검색 범위
현재 저장소의 `package.json`과 lockfile은 `tailwindcss / @tailwindcss/vite` **4.3.3**을 사용하며, [Vite 설정](../../vite.config.ts)은 `react()`와 `tailwindcss()` plugin을 연결한다. 앱은 [src/index.css](../../src/index.css)의 Tailwind/HM Tailwind와 [src/hm-theme.css](../../src/hm-theme.css)의 HM Theme를 [main.tsx](../../src/main.tsx)에서 로드한다. 외부 React 소비 앱도 CSS entry를 실제 bootstrap에서 import해야 한다.
Tailwind는 기본적으로 실행 cwd를 기준으로 자동 검색하고 `node_modules`, `.gitignore` 대상 등을 제외한다. 외부 프로젝트의 junction 또는 넓은 실행 루트에서 검색 지연이 발생하면 실제 CSS 위치·실행 cwd·연결 폴더를 확인한 뒤 앱 소스 범위만 지정한다. `source("./")`는 CSS 파일의 위치를 기준으로 자동 검색 기준 경로를 정하는 지원 구문이며, 모든 junction 지연을 해결한다고 보증하지 않는다. 아래는 **소비 앱 CSS가 `src/index.css`이고 앱 코드가 `src/`에 있는 경우**의 예다.
```css
@import "tailwindcss" source("./");
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
/* Dashboard 상태를 표시하는 화면만 Theme 다음에 추가 */
@import "@hm-design/ui/dashboard-status.css";
@source "../index.html";
```
CSS entry가 루트에 있으면 같은 `source("./")`는 루트 전체를 뜻하므로 제한 효과가 다르다. `src/` 밖 공유 UI가 있다면 그 실제 경로를 `@source`로 추가한다. 명시 목록으로 제한할 때는 `source(none)` + 필요한 `@source`도 지원하지만, 앱·공유 UI·HM source를 빠짐없이 지정해야 한다. [공식 Tailwind 소스 검색 문서](https://tailwindcss.com/docs/detecting-classes-in-source-files).
**HM Component 클래스 보존**: 공개 `@hm-design/ui/tailwind.css`를 생략하지 않는다. [패키지 소스](../../packages/ui/src/tailwind.css)는 `./components / ./patterns`를 검색하고, [기존 package build](../../scripts/build-package.mjs)는 배포 CSS에서 이를 `@source "./index.js"`로 바꾼다. 따라서 앱 자동 검색을 `src/`로 좁혀도 HM compiled 클래스는 해당 CSS의 명시 source를 통해 검색한다. 소비자가 소스 alias로 직접 연결한다면 배포 CSS와 경로가 같다고 가정하지 말고 실제 source 파일 존재를 확인한다. HM package 전체나 외부 `node_modules` 전체를 추가 검색 대상으로 삼지 않는다.
완료 시 앱 CSS import 순서, HM `@source` 대상 존재, 필요한 utility 생성 여부(예: GNB 모바일 trigger의 `lg:hidden`, Badge의 flex·크기 class)를 실제 소비 빌드에서 확인한다. 이 문서 작업에서는 버전·설정·구문 지원·source 연결만 대조했다. junction 지연 재현과 외부 앱 utility 생성은 미검증이며, 새 설정 파일·의존성·빌드는 추가하지 않았다.
## 3. 변환 시 보존해야 하는 계약
layout, spacing, typography, border, radius, icon 크기·정렬, responsive behavior와 hover/focus/active/selected/disabled/checked/error/loading 상태를 유지한다. 접근성 semantic element, keyboard behavior, ARIA 속성도 Target Runtime에 맞는 방식으로 보존한다.
프레임워크 기능은 Target Runtime의 최소 구현으로 변환한다.
- React state/props → 명시적 DOM state와 Vanilla JavaScript state
- event handler → `addEventListener`
- conditional className → class/attribute toggle
- component composition → semantic HTML structure
- package icon → 배포 가능한 SVG 또는 허용된 icon artifact
단순 layout·typography·color·responsive 표현은 JavaScript에 의존하지 않는다. JavaScript는 accordion, dropdown, modal, tabs 등 실제 interaction에만 추가한다.
### 3.1 Dashboard 상태 계약
Dashboard 업무 상태의 Runtime-neutral 기준은 `packages/ui/src/patterns/dashboard-status/contract.ts`다. 별도 Runtime은 다음을 동일하게 유지한다.
- 상태 ID: `active`, `hold`, `done`; 필터 ID: `all`, `active`, `hold`, `done`, `issue`
- `issue`는 독립 Boolean flag이며 상태 값과 동시에 존재할 수 있음
- `done` label은 `완료`
- `all`은 모든 record, `issue`는 `issue === true`, 나머지는 `record.status === filter`로 매칭
- 상태 표시에는 `data-status`, 이슈에는 `data-issue="true"`, 필터 컨트롤에는 `data-filter`, 선택에는 `aria-pressed="true|false"` 사용
- CSS 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며, 패키지 소비자는 `@hm-design/ui/theme.css` 다음에 `@hm-design/ui/dashboard-status.css`를 로드한다. 기존 `src/patterns/dashboard-status-contract.css`는 같은 원본을 import하는 호환 경로다. 상태 CSS는 Custom Property만 정의하므로 Badge와 필터 숫자 등의 색상 mapping은 Runtime adapter에서 연결하며 색상 literal을 복제하지 않는다. active/hold/issue 값은 기존 Story 시각 보존용 compatibility mapping이며 HM Theme JSON을 변경하지 않는다. React/HTML의 `done`은 모두 `--hm-dashboard-status-done`을 사용하고, `:root` 및 `[data-hm-theme]`에서 해당 scope의 HM white token으로 재계산한다. [적용 순서와 Badge mapping](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약).
React는 이 계약을 `Badge`와 `Button`/기존 `FilterBar` 조합으로 표현한다. Static HTML은 같은 attribute를 가진 semantic button/span과 `addEventListener` 기반 필터링으로 표현하며, `aria-controls`와 결과 count의 갱신도 유지한다. `selected`, `hover`, `focus-visible`, `disabled`는 CSS와 native button semantics로 보존한다.
### 3.2 Static HTML 실행 체크리스트
1. `docs/patterns/dashboard-status-static.html`을 브라우저에서 직접 `file:///...`로 연다.
2. `../../packages/ui/dist/theme.css`, `../../src/patterns/dashboard-status-contract.css`, `../../public/fonts/NotoSansKR-variable.ttf`가 모두 로드되는지 확인한다.
3. 전체/진행/보류/완료/이슈 버튼의 `aria-pressed`와 결과 count를 확인한다.
4. 이슈 record가 상태 Badge와 독립 이슈 flag를 동시에 표시하는지 확인한다.
5. 결과가 0건인 필터에서 Empty State와 `aria-live` 갱신을 확인한다.
6. DevTools Console에 오류가 없는지 확인한다. 자동화 환경이 `file://`를 차단하면 PASS로 기록하지 않고 미검증으로 남긴다.
### 3.3 AI 독립 재현 체크리스트
이전 대화 없이도 다음 파일만으로 재현 가능해야 한다: `AGENTS.md`, `packages/ui/src/patterns/dashboard-status/contract.ts`, `src/patterns/dashboard-status-contract.css`, `src/patterns/dashboard-layout.stories.tsx`, `docs/patterns/dashboard-status-static.html`, `packages/ui/dist/theme.css`, `docs/guidelines/runtime-theme-contract.md`. 구현 후 상태 ID/label, matching/count, DOM attributes, Theme artifact 상대 경로, React/Static HTML 차이, `file://` 검증 결과를 각각 보고한다.
### 3.2 Button 사용 계약
Button의 공통 기준은 [Button 문서](../components/button.md)의 역할·조합 규칙을 따른다. `brand`는 실제 화면당 최대 1개이고 팝업에는 사용하지 않으며, 일반 확인/작성은 같은 `size`의 Solid 세트, 삭제 확인과 반복·보조 작업은 같은 `size`의 Outline 세트를 사용한다. Static HTML과 다른 프레임워크는 React API를 복제하지 않아도 되지만, semantic button, `data-slot`/`data-variant`/`data-appearance`/`data-size` 계약, 기존 Theme CSS variable, native disabled 및 keyboard/modal 동작을 보존해야 한다.
## 4. Asset과 실행 위치
`STATIC_HTML`은 Vite asset resolution을 사용하지 않는다. HTML과 함께 이동 가능한 상대 경로를 사용한다.
```text
./assets/
./styles/
./fonts/
./icons/
```
CSS, JavaScript, SVG/image/icon, font, theme stylesheet를 모두 확인한다. package의 compiled CSS가 사용하는 font 경로와 source 개발 서버의 경로가 다를 수 있으므로, package 산출물 사용 시 package `dist`와 export 계약을 우선한다. 현재 source `packages/ui/src/theme.css`의 개발용 font 경로는 `/fonts/NotoSansKR-variable.ttf`이며 App/Storybook은 `public/fonts/`를 사용한다.
### 4.1 Noto Sans KR 경로: 실행 환경별 확인
- **HMDesignSystem 개발 저장소**: 원본은 `public/fonts/NotoSansKR-variable.ttf`, 라이선스는 같은 폴더의 `NotoSansKR-OFL.txt`다. Theme source의 `@font-face`는 `/fonts/NotoSansKR-variable.ttf`를 사용한다. Vite는 `public/`을 루트에서 제공하고 Storybook은 [.storybook/main.ts](../../.storybook/main.ts)의 `staticDirs`로 `/fonts`에 연결한다. 이 경로는 해당 서버 환경의 계약이다.
- **포터블 테스트 ZIP(현재 기준 r3, 아래는 r2 시점 확인 기록)**: r2 ZIP 안 `HMDesignTest/public/fonts/`, `HMDesignTest/packages/ui/dist/fonts/`, `HMDesignTest/storybook-static/fonts/`에 기존 폰트와 라이선스가 모두 있다. 포터블 실행기는 사전 빌드된 Storybook을 제공하므로 실행 화면은 그 안의 font/CSS asset을 사용한다. 개발 폴더의 `/fonts` 경로를 다른 실행 위치에 그대로 적용한다고 가정하지 않는다. r2는 문서 개선 이전 과거 스냅샷으로 보존하며 현재 기준이 아니다. r3의 폰트 포함 여부는 r3 Inventory로 별도 확인한다.
- **별도 React 소비자**: `@hm-design/ui/theme.css`는 공개 export인 `dist/theme.css`다. 기존 build가 원본 font-face URL을 `./fonts/NotoSansKR-variable.ttf`로 바꾸고 폰트·OFL 파일을 `dist/fonts/`로 복사한다. 정상 package를 CSS import로 소비할 때 별도 앱 `/fonts` 복사에 의존하지 않는다. Vite가 CSS `url()` 자산을 처리하므로 최종 배포 `base`와 출력 URL을 확인한다. Theme 소스 파일만 복사하거나 CSS만 분리하면 이 계약이 사라지므로 실제 폰트와 상대 경로도 함께 배치한다. [Vite 공식 Asset 문서](https://vite.dev/guide/assets.html).
- **단일 `preview.html`**: `file://`에서는 `/fonts/...`가 HTML 옆 폴더를 뜻하지 않는다. 파일과 폴더를 함께 전달한다면 상대 CSS/font URL을 사용하고, CSS 안 URL은 HTML이 아닌 해당 CSS 파일 위치 기준으로 계산한다. [기존 Static HTML](../patterns/dashboard-status-static.html)은 상대 package Theme와 별도 상대 font-face를 사용하는 **다중 파일** Reference다. HTML 하나만 전달하려면 승인된 Theme·소비 CSS의 import를 모두 펼쳐 inline하고 폰트·아이콘·이미지 등 외부 자산도 내장한다. Tailwind 입력 CSS의 `@source / @apply`나 bare package import를 브라우저에 그대로 전달하지 않는다.
단일 HTML의 font-face는 기존 TTF를 data URL로 내장할 수 있다. 아래는 저장소 루트에서 기존 폰트로 **삽입할 CSS 문자열만 만드는** PowerShell 예이며, Theme/다른 Asset의 내장을 대신하지 않는다.
```powershell
$fontPath = Join-Path (Get-Location) 'public/fonts/NotoSansKR-variable.ttf'
$fontData = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($fontPath))
$fontRule = '@font-face { font-family: "Noto Sans KR"; src: url("data:font/ttf;base64,' + $fontData + '") format("truetype"); font-style: normal; font-weight: 100 900; font-display: swap; }'
# 기존 font-face를 $fontRule로 교체해 preview.html의 <style>에 삽입한다.
```
Font Family `Noto Sans KR`, 기존 variable weight `100 900`, 폰트 파일·라이선스를 유지한다. 폴더 배포에는 OFL을 함께 전달하고 단일 HTML 배포에는 기존 OFL 전문을 HTML comment 등으로 포함한다. base64는 파일 크기를 늘리므로 내장 여부는 전달 형식에 따라 선택한다. 최종 font 요청·fallback 여부·다른 Asset 로드와 `file://` 실행은 소비 결과물에서 검증하며, 이번 문서 작업에서 새 HTML 생성·실행은 하지 않았다.
## 5. Target Runtime별 최소 검증
### 5.1 React Dashboard standalone export
설치 없는 테스트 ZIP은 사전 빌드된 Storybook의 Dashboard 대표 Story를 제공한다. 독립 Dashboard export는 기존 배포 필수 구성이 아니며, STEP 5에서 UI Preview로 표시된 미검증 `dist-dashboard/`는 포터블 ZIP에서 제외한다. 아래는 개발자용 별도 export 계약이며 해당 빌드 결과의 성공을 보증하지 않는다. [포터블 테스트 안내](portable-test-distribution.md). 이 `dist-dashboard/` export는 H-1(독립 단일 HTML 미리보기 POC, PASS)의 성공 산출물과 동일하지 않다. H-2(AI 생성 화면의 자동 빌드·캡처·검증)는 Storybook 배포와 별도 기능이며 구현 완료 여부를 이 문서가 보증하지 않는다.
Dashboard Production 화면은 Storybook Story를 import하지 않는 독립 React composition을 소비하는 Vite entry로 생성한다. Storybook은 구조·상태의 Reference이며 Production export의 CSS 또는 bootstrap source가 아니다.
- Source: `src/patterns/dashboard-composition.tsx`의 `DashboardScreen`
- Entry: `src/exports/dashboard/index.html` + `src/exports/dashboard/main.tsx`
- Build: `npm run build:dashboard-export`
- Output: `dist-dashboard/`
- CSS entry 순서: `@/index.css` → `@/hm-theme.css` → `@hm-design/ui/dashboard-status.css` → `./dashboard-export.css`. `@/index.css`가 Tailwind 본체와 HM Tailwind CSS를 로드하므로 `@hm-design/ui/tailwind.css`만 단독 import하지 않는다.
- Runtime: React compiled IIFE bundle, classic `defer` script, dynamic import disabled, relative asset base
이 export는 Storybook manager·Story CSS·개발 서버에 의존하지 않는다. `DashboardScreen`은 Dashboard Production Composition을 직접 사용하며, `dist-dashboard/index.html`은 HTTP 정적 서버와 `file://` 직접 실행을 각각 별도로 검증한다. HTTP 성공은 `file://` 성공을 의미하지 않는다. 빌드 성공 또는 CSS 텍스트의 선택자 존재만으로 화면 검증을 통과시키지 말고, 실제 렌더링에서 `.flex`·`.grid` 등의 utility, Theme CSS Variables, Dashboard Status CSS 및 Header/Layout/Card/FilterBar 레이아웃 적용을 확인한다.
- `REACT_VITE`: Vite 실행, Theme 적용, component rendering, interaction, responsive behavior를 확인한다.
- `STATIC_HTML`: 실제 HTML 파일을 `file://`로 직접 열어 CSS/JavaScript/font/SVG/image/icon 로드, HM CSS Custom Properties, responsive behavior, interaction, console runtime error를 확인한다. Vite dev server 성공만으로 완료 처리하지 않는다.
- `VANILLA_WEB`/`OTHER_RUNTIME`: 실제 실행 환경에서 Theme Contract, visual/state/interaction/responsive parity와 asset resolution을 확인한다.
검증은 전체 회귀보다 변환된 범위의 최소 실행 검증을 우선한다. 확인이 불가능한 항목은 성공으로 간주하지 말고 차이와 제한을 기록한다.
## 6. 구현 및 AI 독립성
작업에 필요한 정보는 저장소의 코드, Theme artifact, 문서, package export와 Reference Story에 있어야 한다. 이전 AI 대화, Memory, 특정 IDE/Agent 전용 기능을 전제로 하지 않는다. 구현 편의를 위해 Theme이나 Component visual contract를 변경하지 않는다.
우선순위는 `HM Theme/Token 정확성 → visual parity → state parity → responsive behavior → accessibility → interaction → runtime 편의성`이다.
관련 문서: [Runtime Theme](runtime-theme.md), [Runtime 계약표](runtime-theme-contract.md), [Theme delivery](theme-delivery.md), [Theme/Token 가이드](테마-토큰-가이드.md), [개발 적용 가이드](개발-적용-가이드.md).
+98
View File
@@ -0,0 +1,98 @@
# Public Runtime Theme Contract
계약 Source of Truth: `packages/ui/src/theme/contract.ts`. 모든 입력은 선택적이며 외부 override 가능. Component/derived 변수는 공개 입력이 아니다.
| Runtime JSON Token | CSS Token | Default | 의미 | 외부 override | 주요 영향 |
|---|---|---|---|---|---|
| `color.bg.content` | `--hm-sem-bg-content` | `#f5f0e5` | 콘텐츠 면 | Yes | gnb, lnb |
| `color.bg.surface` | `--hm-sem-bg-surface` | `#ffffff` | 기본/카드/입력 면 | Yes | checkbox, radio, switch, slider, badge, table, input, select, button, gnb |
| `color.fg.primary` | `--hm-sem-fg-primary` | `#0a221d` | 주요 텍스트 | Yes | calendar, pagination, badge, table, input, select, gnb |
| `color.fg.secondary` | `--hm-sem-fg-secondary` | `#6e6759` | 보조 텍스트 | Yes | calendar, pagination, badge, table, gnb |
| `color.fg.placeholder` | `--hm-sem-fg-placeholder` | `#999999` | Placeholder | Yes | input, select |
| `color.action.primary` | `--hm-sem-action-primary` | `#123328` | Strong / Primary ink | Yes | checkbox/radio hover, slider Hover/Drag halo, pagination current, badge default, button outline, navigation current ink |
| `color.action.cancel` | `--hm-sem-action-cancel` | `#6e6759` | 공개 취소 역할 | Yes | Button `variant="cancel"` + `appearance="outline"` |
| `color.action.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
| `color.stroke.default` | `--hm-sem-stroke-default` | `#9a9284` | 기본 border | Yes | checkbox, radio, switch, badge |
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select |
| `color.stroke.input.focus` | `--hm-sem-stroke-input-focus` | `#266651` | 공통 Focus border/ring | Yes | input, select, button, checkbox, calendar, slider Keyboard Focus halo 등; GNB/LNB는 contextual 예외 |
| `color.bg.brand.stop.0` | `--hm-sem-brand-bg-stop-0` | `#0a221d` | 기존 gradient stop: color.bg.brand.stop.0 | Yes | Theme palette / adapter |
| `color.bg.brand.stop.12` | `--hm-sem-brand-bg-stop-12` | `#071e18` | 기존 gradient stop: color.bg.brand.stop.12 | Yes | Theme palette / adapter |
| `color.bg.brand.stop.42` | `--hm-sem-brand-bg-stop-42` | `#123328` | 기존 gradient stop: color.bg.brand.stop.42 | Yes | Theme palette / adapter |
| `color.bg.brand.stop.78` | `--hm-sem-brand-bg-stop-78` | `#266651` | 기존 gradient stop: color.bg.brand.stop.78 | Yes | Theme palette / adapter |
| `color.bg.brand.stop.100` | `--hm-sem-brand-bg-stop-100` | `#205241` | 기존 gradient stop: color.bg.brand.stop.100 | Yes | Theme palette / adapter |
| `surface.action.primary.stop.0` | `--hm-sem-action-surface-stop-0` | `#1f8265` | 기존 gradient stop: surface.action.primary.stop.0 | Yes | button, gnb |
| `surface.action.primary.stop.30` | `--hm-sem-action-surface-stop-30` | `#165c47` | 기존 gradient stop: surface.action.primary.stop.30 | Yes | button, gnb |
| `surface.action.primary.stop.65` | `--hm-sem-action-surface-stop-65` | `#114838` | 기존 gradient stop: surface.action.primary.stop.65 | Yes | button, gnb |
| `surface.action.primary.stop.100` | `--hm-sem-action-surface-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.stop.100 | Yes | button, gnb |
| `surface.action.primary.border.stop.0` | `--hm-sem-action-border-stop-0` | `#0a221d` | 기존 gradient stop: surface.action.primary.border.stop.0 | Yes | button |
| `surface.action.primary.border.stop.50` | `--hm-sem-action-border-stop-50` | `#34daaa` | 기존 gradient stop: surface.action.primary.border.stop.50 | Yes | button |
| `surface.action.primary.border.stop.100` | `--hm-sem-action-border-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.border.stop.100 | Yes | button |
| `radius.input.default` | `--hm-radius-input` | `4px` | Input/Select radius | Yes | input, select |
| `radius.container.default` | `--hm-radius-container` | `4px` | Container radius | Yes | Theme palette / adapter |
| `color.state.active` | `--hm-sem-state-active` | `#266651` | Checked / Selected / ON fill | Yes | checkbox, radio, switch, slider, progress, calendar |
| `color.on.state.active` | `--hm-sem-on-state-active` | `#ffffff` | Active 위 텍스트/indicator | Yes | checkbox, calendar |
| `color.fg.inverse` | `--hm-sem-on-action-primary` | `#ffffff` | 반전 텍스트 | Yes | pagination, badge, button, gnb, lnb |
| `color.bg.subtle` | `--hm-sem-bg-subtle` | `#f4f1ea` | 은은한 면 | Yes | calendar, badge, table |
| `color.accent` | `--hm-sem-accent` | `#bddfd4` | Soft Highlight 45% / Selected·Open surface 100% 원색 | Yes | select, combobox, menu item, table, calendar, pagination hover / toggle, menubar, navigation-menu trigger |
| `color.on.accent` | `--hm-sem-on-accent` | `#123328` | Accent 위 텍스트; Primary와 독립 sibling | Yes | select, combobox, menu item, calendar, pagination hover, toggle, menubar, navigation-menu |
| `color.palette.green-light-4` | `--hm-sem-palette-green-light-4` | `#34daaa` | 선택적 Theme 팔레트: green-light-4 | Yes | button |
| `color.palette.green-light-3` | `--hm-sem-palette-green-light-3` | `#249473` | 선택적 Theme 팔레트: green-light-3 | Yes | data-table Sort Direction Accent, tabs 장식, lnb gradient |
| `color.palette.green-light-2` | `--hm-sem-palette-green-light-2` | `#266651` | 선택적 Theme 팔레트: green-light-2 | Yes | checkbox, radio, switch, slider, progress, calendar, table, input, select, gnb, lnb |
| `color.palette.green-light-1` | `--hm-sem-palette-green-light-1` | `#205241` | 선택적 Theme 팔레트: green-light-1 | Yes | table, button, lnb |
| `color.palette.green-default` | `--hm-sem-palette-green-default` | `#123328` | 선택적 Theme 팔레트: green-default | Yes | checkbox, radio, slider, calendar, pagination, badge, select, button, gnb, lnb |
| `color.palette.green-dark-1` | `--hm-sem-palette-green-dark-1` | `#0a221d` | 선택적 Theme 팔레트: green-dark-1 | Yes | calendar, pagination, badge, table, input, select, button, gnb |
| `color.palette.green-dark-2` | `--hm-sem-palette-green-dark-2` | `#071e18` | 선택적 Theme 팔레트: green-dark-2 | Yes | Theme palette / adapter |
| `color.palette.brown-light-2` | `--hm-sem-palette-brown-light-2` | `#897445` | 선택적 Theme 팔레트: brown-light-2 | Yes | button |
| `color.palette.brown-light-1` | `--hm-sem-palette-brown-light-1` | `#604f32` | 선택적 Theme 팔레트: brown-light-1 | Yes | button |
| `color.palette.brown-default` | `--hm-sem-palette-brown-default` | `#3e3523` | 선택적 Theme 팔레트: brown-default | Yes | button |
| `color.palette.brown-dark-1` | `--hm-sem-palette-brown-dark-1` | `#352d1d` | 선택적 Theme 팔레트: brown-dark-1 | Yes | button |
| `color.palette.brown-light-4` | `--hm-sem-palette-brown-light-4` | `#ecd4a2` | 선택적 Theme 팔레트: brown-light-4 | Yes | Theme palette / adapter |
| `color.palette.orange-light-4` | `--hm-sem-palette-orange-light-4` | `#feba88` | 선택적 Theme 팔레트: orange-light-4 | Yes | button |
| `color.palette.orange-default` | `--hm-sem-palette-orange-default` | `#cc5200` | 선택적 Theme 팔레트: orange-default | Yes | button |
| `color.palette.warm-neutral-light` | `--hm-sem-palette-warm-neutral-light` | `#e0dace` | 선택적 Theme 팔레트: warm-neutral-light | Yes | slider, progress |
| `color.palette.warm-neutral-mid` | `--hm-sem-palette-warm-neutral-mid` | `#9a9284` | 선택적 Theme 팔레트: warm-neutral-mid | Yes | checkbox, radio, switch, badge |
| `color.palette.warm-neutral-dark` | `--hm-sem-palette-warm-neutral-dark` | `#6e6759` | 선택적 Theme 팔레트: warm-neutral-dark | Yes | calendar, pagination, badge, table, gnb |
| `color.palette.beige-light` | `--hm-sem-palette-beige-light` | `#f5f0e5` | 선택적 Theme 팔레트: beige-light | Yes | gnb, lnb |
| `color.palette.red-danger` | `--hm-sem-palette-red-danger` | `#d32f2f` | 선택적 Theme 팔레트: red-danger | Yes | checkbox, radio, badge, input, button |
| `color.palette.white` | `--hm-sem-palette-white` | `#ffffff` | 선택적 Theme 팔레트: white | Yes | checkbox, radio, switch, slider, calendar, pagination, badge, table, input, select, button, gnb, lnb |
| `color.palette.black` | `--hm-sem-palette-black` | `#000000` | 선택적 Theme 팔레트: black | Yes | checkbox, switch, select, button, gnb, lnb |
| `color.palette.neutral-gray-500` | `--hm-sem-palette-neutral-gray-500` | `#999999` | 선택적 Theme 팔레트: neutral-gray-500 | Yes | input, select |
| `color.palette.green-tone-1` | `--hm-sem-palette-green-tone-1` | `#1f8265` | 선택적 Theme 팔레트: green-tone-1 | Yes | button, gnb |
| `color.palette.green-tone-2` | `--hm-sem-palette-green-tone-2` | `#165c47` | 선택적 Theme 팔레트: green-tone-2 | Yes | button, gnb |
| `color.palette.green-tone-3` | `--hm-sem-palette-green-tone-3` | `#114838` | 선택적 Theme 팔레트: green-tone-3 | Yes | button, gnb |
| `color.palette.green-tone-4` | `--hm-sem-palette-green-tone-4` | `#08241c` | 선택적 Theme 팔레트: green-tone-4 | Yes | button, gnb |
| `color.palette.warm-neutral-pale` | `--hm-sem-palette-warm-neutral-pale` | `#dedbd2` | 선택적 Theme 팔레트: warm-neutral-pale | Yes | table, input, select |
| `color.palette.warm-neutral-secondary` | `--hm-sem-palette-warm-neutral-secondary` | `#ede8db` | 선택적 Theme 팔레트: warm-neutral-secondary | Yes | Theme palette / adapter |
| `color.palette.warm-neutral-muted` | `--hm-sem-palette-warm-neutral-muted` | `#f4f1ea` | 선택적 Theme 팔레트: warm-neutral-muted | Yes | calendar, badge, table |
| `color.palette.green-tint` | `--hm-sem-palette-green-tint` | `#bddfd4` | 선택적 Theme 팔레트: green-tint | Yes | calendar, pagination, table, select, gnb, lnb |
| `color.palette.yellow-accent` | `--hm-sem-palette-yellow-accent` | `#ffc600` | 선택적 Theme 팔레트: yellow-accent | Yes | lnb |
| `color.palette.brown-deep` | `--hm-sem-palette-brown-deep` | `#221d13` | 선택적 Theme 팔레트: brown-deep | Yes | button |
| `color.palette.brown-highlight` | `--hm-sem-palette-brown-highlight` | `#a98d53` | 선택적 Theme 팔레트: brown-highlight | Yes | button |
| `color.palette.orange-light` | `--hm-sem-palette-orange-light` | `#d35a00` | 선택적 Theme 팔레트: orange-light | Yes | button |
| `color.palette.orange-mid` | `--hm-sem-palette-orange-mid` | `#ce5600` | 선택적 Theme 팔레트: orange-mid | Yes | button |
| `color.palette.orange-dark` | `--hm-sem-palette-orange-dark` | `#b44b00` | 선택적 Theme 팔레트: orange-dark | Yes | button |
| `color.palette.orange-deep` | `--hm-sem-palette-orange-deep` | `#783402` | 선택적 Theme 팔레트: orange-deep | Yes | button |
| `color.palette.orange-border` | `--hm-sem-palette-orange-border` | `#bc4c00` | 선택적 Theme 팔레트: orange-border | Yes | button |
| `color.palette.orange-border-deep` | `--hm-sem-palette-orange-border-deep` | `#5e2902` | 선택적 Theme 팔레트: orange-border-deep | Yes | button |
| `color.palette.gray` | `--hm-sem-palette-gray` | `#767676` | 선택적 Theme 팔레트: gray | Yes | button |
| `color.palette.gray-dark` | `--hm-sem-palette-gray-dark` | `#5a5a5a` | 선택적 Theme 팔레트: gray-dark | Yes | button |
| `color.palette.indicator` | `--hm-sem-palette-indicator` | `#2d2d2d` | 선택적 Theme 팔레트: indicator | Yes | select |
총 73개. 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 42개. 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
`color.action.cancel`은 Button의 `variant="cancel"` Outline ink를 위한 Public Runtime role로 사용한다. Solid 취소는 기존 gray Component mapping을 유지하고, Outline 취소는 이 semantic role을 소비한다. 외부 소비 가능성이 있으므로 deprecated로 간주하지 않는다. 적용·병합·fallback·scope 정책은 [Runtime Theme](runtime-theme.md)를 따른다.
# 디자인팀 Runtime 운영 추가 결정 — 2026-10-06
기존 v1 입력 73개와 CSS property/type 계약을 유지한다. 공개 API `isRuntimeTheme(unknown): value is RuntimeTheme`를 제공한다. 이 사전 검증은 DOM을 변경하지 않는다. 기존 `applyRuntimeTheme`의 invalid 입력 시 reset 정책은 유지한다. 편집기는 사전 검증으로 invalid Import를 차단해 마지막 유효 Theme를 보존한다.
Default 컬러의 운영 원본은 `themes/default.json`(71개 컬러)이다. `scripts/theme-defaults.mjs`가 JSON과 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `contract.ts`의 defaultValue와 `defaultRuntimeTheme`는 생성 데이터를 읽는다. 2개 radius/geometry는 template에서 유지한다. CSS fallback/현재 색을 유지하고 세 가지 최종 명령의 pre hook에서 stale 생성 데이터를 검사한다. Theme-only 산출은 `npm run build:themes`로 분리한다.
Storybook `Theme/Designer Playground/Editor`는 17개 상위 컬러 역할과 JSON URL/파일 Preview를 제공하는 소비자다. Global decorator에서 `resolveRuntimeTheme(theme, defaultJson)` 후 Production `applyRuntimeTheme(document.documentElement, theme)` → CSS Custom Properties → 기존 Mapping으로 적용한다. JSON Export/Import는 동일 v1 계약이다. 컬러 artifact/편집기만 radius를 거부한다. Production API의 기존 radius/gradient 계약은 유지한다.
추가 Production API는 `fetchRuntimeTheme(url, request?)`, `resolveRuntimeTheme(theme, defaults?)`, `loadRuntimeTheme(root,url,options?)`다. fetch는 DOM과 분리되며 validation은 기존 `isRuntimeTheme`을 재사용한다. Loader 실패는 HM package fallback, partial은 같은 계약으로 교체/병합하며 superseded 요청은 새 Theme를 덮거나 reset하지 않는다. 회사 CDN URL은 하드코딩하지 않는다.
사용법: [디자인팀 Theme 가이드](designer-theme-guide.md), [Theme 운영/배포](theme-delivery.md). 적용·fallback·scope 정책: [Runtime Theme](runtime-theme.md).
## Dashboard 상태 색상 범위
Dashboard의 `active / hold / done / issue` 업무 상태 계약은 `packages/ui/src/patterns/dashboard-status/contract.ts`에서 ID·label·tone을 제공한다. 상태 변수 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며 `@hm-design/ui/dashboard-status.css`로 배포한다. `src/patterns/dashboard-status-contract.css`는 기존 Story/HTML 경로의 호환 import다. Theme CSS 다음에 상태 CSS를 로드하고 `--hm-dashboard-status-active|hold|done|issue`를 화면 요소의 색상에 별도로 연결한다. 상태 CSS 자체는 Badge나 `data-status`를 자동으로 꾸미지 않는다. active/hold/issue는 기존 Story 색상을 보존하는 compatibility mapping이고 done은 `:root` 및 `[data-hm-theme]`에서 해당 scope의 기존 HM white token을 참조한다. 기존 HM Theme 및 Public Runtime Token/JSON 73개는 변경하지 않으며, 실제 Production 소비처가 반복되어 공통 색상 의미가 확정될 때만 Semantic Token 승격을 검토한다.
File diff suppressed because it is too large. Load diff
+193
View File
@@ -0,0 +1,193 @@
# External Runtime Theme 구조 점검·전환 결과
> HISTORICAL_RECORD: 작성·검증 시점(2026-10-06)의 기록이다. 아래 리터럴·실측·당시 예외를 보존한다. 현재 apply/merge/fallback/scope와 Slider Focus는 [Runtime Theme](runtime-theme.md), key/property/type은 [Runtime 계약표](runtime-theme-contract.md), CDN 배포는 [Theme delivery](theme-delivery.md)를 따른다.
2026-10-06 · HM_THEME / Theme infrastructure
## 1. 현재 Theme Architecture
Primitive(`packages/ui/src/theme/primitives.css`) → HM Default preset / Semantic / shadcn adapter(`default.css`) → Component Mapping(`theme/<name>.css`) → CSS selector / Tailwind → UI. `theme.css`가 Default부터 import한다. Tailwind namespace 매핑은 `tailwind.css`, Demo artwork는 `src/hm-theme.css`, Storybook 설정은 `.storybook/preview.tsx`다.
대표 체인을 실제 파일에서 확인했다. 모든 Component Token의 기존 이름을 유지했다.
| Component | 실제 대표 체인 |
|---|---|
| Checkbox | green-light-2 → sem-state-active → checkbox-checked-background → checked background |
| Radio | green-light-2 → sem-state-active → radio-checked-border-color / radio-dot-color |
| Switch | green-light-2 → sem-state-active → switch-on-background |
| Slider | green-light-2 → sem-state-active → slider-range-background / thumb-border-color |
| Progress | green-light-2 → sem-state-active → progress-indicator-background |
| Calendar | green-light-2 → sem-state-active → calendar-selected-background / range-edge-background |
| Pagination | green-default → sem-action-primary → pagination-active-background |
| Badge | green-default → sem-action-primary → badge-default-background |
| Table | sem-accent → sem-surface-highlight(45%) → table-row-hover/selected-background |
| Input | sem-stroke-input-focus → input-focus-border-color; sem-action-destructive → input-error-border-color |
| Select | sem-accent → sem-surface-highlight → select-option-state-background; sem-stroke-input-focus → focus border |
| Button | sem-action-primary → button-outline-green-ink; primary gradient stops → button-brand-green-background |
| GNB | primary surface stop group → gnb-background; sem-action-primary → gnb-current-text |
| LNB | Primary + palette shades → lnb-background; sem-action-primary → lnb-current-text |
## 2. Runtime Theme 기존 방식
기존 `src/theme/preview.ts`는 schemaVersion 1 / 25개 allowlist / 6자리 HEX / radius 2–12px / gradient 전체 세트 검증을 제공했다. 전체 문서를 검증한 뒤 HTMLElement inline 변수에 적용하고 이전 주입 키를 제거했다. **실제 호출처는 없었고 서비스 패키지에서 import할 수 없었다.** 새 엔진 없이 이 방식을 UI 패키지로 이동·보강했다.
## 3. 발견된 문제와 처리
- Active, on Active, Accent, Subtle Surface, Inverse Text 계약 누락 → 공개 계약에 추가.
- 일부 Button / Input / 패널 / navigation / palette 참조가 공개 Semantic을 우회 → 동일 Default 값을 유지하면서 Semantic/palette로 연결.
- Button·Select의 직접 HEX와 shadow 고정 RGB → Default preset palette / token 기반 derived mix로 이동.
- root에서 계산된 Component 별칭을 wrapper가 상속해 Scoped override가 전달되지 않음 → 명시적 `data-hm-theme` scope에서 별칭·Mapping 재계산.
- cleanup이 host 기존 inline 값과 우선순위를 잃을 수 있음 → 값/priority/기존 scope를 복원.
- Tailwind upstream root/dark preset과 HM preset 중복 → HM 색상 preset은 Default에 단일 소유, chart/sidebar fallback은 base layer 유지.
- Accent 45% 반복 → 동일 Soft Highlight 정책 사용처를 내부 Semantic으로 승격.
## 4. 확정 Runtime Theme Contract
전체 표: [runtime-theme-contract.md](runtime-theme-contract.md).
공개 입력 총 **73개**: 기존 25개, 핵심 역할 추가 6개, 선택적 Theme palette 42개. 모두 partial 입력 가능하며 Component Token 수백 개를 요구하지 않는다. CSS property 중복 매핑은 없다. 색은 불투명 6자리 HEX, radius는 기존 2–12px 정수다. Typography/spacing/layout/shadow geometry는 공개하지 않았다.
| Runtime Token | Default | 의미 | 외부 override | 주요 영향 |
|---|---|---|---|---|
| color.action.primary | #123328 | Strong/Primary | Yes | Badge, Pagination current, Button outline, navigation ink |
| color.state.active | #266651 | Checked/Selected/ON | Yes | Checkbox, Radio, Switch, Slider, Progress, Calendar |
| color.stroke.input.focus | #266651 | 일반 Focus | Yes | Input, Select, Checkbox, Calendar ring 등 |
| color.accent | #bddfd4 | Highlight 원색 | Yes | Select/Menu/Calendar/Table/Pagination soft state |
| color.bg.content | #f5f0e5 | 콘텐츠 면 | Yes | Dashboard, Card content, navigation current face |
| color.bg.surface | #ffffff | 기본 면 | Yes | Input, Select, Card, Floating/Modal |
| color.bg.subtle | #f4f1ea | 은은한 면 | Yes | Badge secondary, Table footer, Calendar today |
| color.fg.primary | #0a221d | 주요 텍스트 | Yes | 일반 UI 텍스트 |
| color.fg.inverse | #ffffff | 반전 텍스트 | Yes | Button, Badge, GNB/LNB, Tooltip |
| color.stroke.default | #9a9284 | 기본 border | Yes | Checkbox, Radio, Badge outline, Scroll Area |
| color.action.destructive | #d32f2f | 오류/위험 | Yes | Input error, destructive Badge/Button/Menu |
## 5. Default Theme 구조
Default CSS preset은 최초 렌더의 fallback이다. `defaultRuntimeTheme`는 같은 contract의 기본 Theme Set이다. 이를 주입한 실제 화면과 주입하지 않은 화면의 parity를 검사했다. 외부 JSON 값은 동일 CSS property를 override한다. Primitive 유지, Semantic 유지, Component Mapping 유지. Component는 외부 객체를 읽지 않는다.
Default → External 우선순위는 scope inline CSS로 보장한다. 기존 Component/Pattern import 순서는 유지하며 Filter Bar padding 순서도 바꾸지 않았다.
## 6. Active / Focus 독립성
Verification Theme는 Active `#007f73`, Focus `#c75b00`이다. Checkbox checked와 Calendar selected의 실제 배경은 `rgb(0,127,115)`, 실제 focus-visible shadow는 orange였다. 두 역할을 각각만 변경해 다른 core 역할의 Default 값이 유지되는지 검사했다. Default에서는 두 값 모두 Light 2여도 서로 alias하지 않는다.
## 7. Primary / Active 독립성
Primary `#6837a8`, Active `#007f73`. Badge default / Pagination current / Button outline / GNB·LNB current ink는 Primary, 선택 Control과 Calendar edge는 Active다. Checkbox/Radio unchecked hover border와 Slider halo, Accent foreground는 기존 Primary 정책을 유지하므로 Primary 변경의 실제 영향표에 포함했다.
## 8. Accent 및 영향표
결정: **PROMOTE_TO_SEMANTIC**. `--hm-sem-surface-highlight = color-mix(in srgb, var(--hm-sem-accent) 45%, transparent)`는 내부 derived다. 외부 사용자는 Accent만 지정한다. Highlight 비율은 바꾸지 않았다. Table header와 Tab의 과거 팔레트 장식, Toggle on의 full Accent는 각각 기존 정책을 유지한다.
| Component | Primary | Active | Focus | Accent |
|---|---|---|---|---|
| Checkbox | unchecked hover border | checked | ring | — |
| Radio | unchecked hover border | selected border/dot | ring | — |
| Switch | — | ON | ring | — |
| Slider | hover/focus halo(기존) | range/thumb border | Primary halo(기존) | — |
| Progress | — | fill | — | — |
| Calendar | highlight text | selected/edge | ring | hover/middle |
| Pagination | current/highlight text | — | ring | hover |
| Badge | default | — | — | — |
| Table | — | — | — | row hover/selected |
| Input | — | — | border/ring | — |
| Select | highlight text | — | border/ring | selected/highlight |
| Button | outline/ghost/link | — | ring | — |
| GNB | current/open ink | — | mobile ring; desktop inverse outline | mobile highlight |
| LNB | current/gradient first stop | — | inverse outline(기존) | — |
Accent를 단독 변경해 Primary/Active/Focus/Destructive가 변하지 않음을 검사했다. Global Select Portal의 선택 옵션은 yellow Accent 45%(`color(srgb 1 0.827451 0.427451 / 0.45)`)로 실제 렌더됐다. DatePicker Portal의 selected day도 Global Active를 받았다.
## 9. Hardcode Audit
전체 파일·줄·값·분류·조치는 [CSV](runtime-theme-hardcode-audit.csv)에 있다. 변경 전 UI Theme CSS의 하드코딩 후보 25개 발생 지점을 처리했고 변경 후 Component CSS의 후보는 0이다. 주석은 실행 리터럴에서 제외했다. Primitive, Default preset, 문서 예제, Story, Demo, 독립 CENTER를 구분했다.
| 파일 | 값 | 분류 | 조치 |
|---|---|---|---|
| theme/primitives.css | #266651 / #123328 등 | VALID | 기존 Primitive 유지 |
| theme/button.css | #221d13 / #a98d53 / orange / gray 등 | HARDCODE_CANDIDATE | 공개 Theme palette 기본값으로 이동 |
| theme/select.css | #2d2d2d | HARDCODE_CANDIDATE | palette-indicator 연결 |
| theme/dialog.css / sheet.css 등 | rgb(0 0 0 / 0.2) | HARDCODE_CANDIDATE | palette-black 기반 mix, shadow geometry 유지 |
| theme/dashboard-layout.css | rgb(7 30 24 / 0.18) | HARDCODE_CANDIDATE | palette-green-dark-2 기반 mix |
| theme/lnb.css | rgb black 20%/30% | HARDCODE_CANDIDATE | palette-black 기반 depth gradient |
| theme/checkbox.css 등 | #0000 / transparent | INTENTIONAL | ring sentinel / 투명면 유지 |
| Select/Menu/Table 등 | Accent 45% color-mix | DERIVED | 같은 의미의 내부 surface-highlight로 승격 |
| components/ui/* overlay | bg-black/10 | INTENTIONAL | upstream class 유지; 실제 색은 overlay.css의 derived Semantic |
| src/*stories.tsx / .storybook | specimen HEX | STORY_ONLY | 검수 specimen 유지 |
| src/hm-theme.css | artwork HEX | INTENTIONAL | Demo 고정 artwork 유지 |
| packages/center-graphic | 그래픽 palette | INTENTIONAL | 독립 CENTER 도메인 유지 |
## 10. Component Mapping 변경
기존 Mapping 이름/size/props를 삭제하거나 rename하지 않았다. Primary/Destructive/Surface/Text/Border 참조를 공개 Semantic으로 연결하고, 별도 브랜드 shade는 선택적 Theme palette로 연결했다. 모든 preset Mapping 선언에 명시적 Theme scope를 추가했다. Accent mix와 shadow chroma를 derived로 정리했다. UI 컴포넌트·Pattern `.tsx`는 수정하지 않았다.
## 11. Storybook Verification Theme
`src/theme/runtime-theme.stories.tsx`의 숨김 verification Story: Comparison, GlobalExternal, Verification, Dashboard. 요청한 15종과 Command를 함께 렌더링한다. Production 기본값에 검증색을 넣지 않았다.
Verification은 브라우저에서 **136개 검사, 63개 렌더된 data-slot 부품**을 확인한다. 1280 / 768 / 390px에서 모두 PASS. Default는 변경 전 토큰 그래프와 색상/geometry parity, full Default preset 주입 parity를 검사한다. 공개 core/palette는 개별 주입, gradient는 complete-group 주입을 검사한다. 포커스 ring은 실제 focus-visible 및 색 공간을 고려해 확인한다. Theme switching 중간 transition 프레임을 제외하기 위해 이 검사 화면만 transition-duration을 0으로 고정한다. Production transition은 유지한다.
추가로 `_local/check-theme-geometry.mjs`는 변경 전 snapshot과 최종 CSS 구문을 비교한다. **50개 파일, 1,633개 geometry 선언 변경 0**. Default/External 비교만으로 검사 로직을 대체하지 않았다.
## 12. Partial Override / Scope
Partial은 미지정 Default 유지. 빈 tokens는 Default. invalid 문서는 이전 override를 원자적으로 해제해 host/Default 복귀. 교체 때 stale key 제거. 기존 host inline 값·priority·scope 복원.
Global A 아래 Scoped B는 독립 HM preset에서 시작한다. B가 지정하지 않은 값은 HM Default이며 A를 상속하는 합성 정책은 아니다. Portal은 DOM inheritance를 따른다. 현재 HM SelectContent/PopoverContent/DatePicker는 container API를 노출하지 않으므로 wrapper Theme만으로 body popup을 scoped하게 만들 수 없다. Global popup 전달은 검증했으며 이 제한을 숨기거나 새 Portal API로 우회하지 않았다.
## 13. 수정 파일
- `packages/ui/package.json`: `./theme` 진입점 추가.
- `packages/ui/src/theme/{contract,runtime,index}.ts`: 단일 계약, 기존 검증/주입 확장, Default preset.
- `packages/ui/src/theme/default.css`, `theme.css`, `tailwind.css`: preset/derived/scope/import ownership.
- `packages/ui/src/theme/*.css`: 기존 50개 Component/Pattern Mapping의 scope / 색상 의존 관계. `overlay.css` 추가.
- `src/theme/preview.ts`: 기존 이름 호환 어댑터.
- `src/theme/runtime-theme.stories.tsx`, `verification-baseline.json`: Story 전용 검증.
- `docs/guidelines/{runtime-theme,runtime-theme-contract,runtime-theme-report}.md`, audit CSV, 테마 토큰 가이드, HM inventory, `CLAUDE.md`: 계약/결과 문서.
변경 전 CSS/Preview는 `_local/theme-before/`에 보존했다. Git 저장소가 없으므로 git diff 대신 snapshot 비교를 사용했다. Dependency 추가 없음.
## 14. API 변화
**Component API 변화 없음.** Theme infrastructure에 필요한 최소 공개 경로 `@hm-design/ui/theme`와 `applyRuntimeTheme`, `resetRuntimeTheme`, `runtimeThemeContract`, `defaultRuntimeTheme`, 타입 2개를 제공한다. 기존 applyPreviewTheme/resetPreviewTheme 이름은 같은 구현의 어댑터다. 네트워크 fetch·캐시·auth·hydration·Publish는 추가하지 않았다.
## 15. 문서
[Runtime 사용·정책](runtime-theme.md), [전체 계약표](runtime-theme-contract.md), [감사 CSV](runtime-theme-hardcode-audit.csv)를 추가하고 기존 테마 가이드/CLAUDE/HM inventory에 구현 상태를 연결했다. Default/External, 계층, partial/invalid, 적용 예제, scope/Portal, 초기 렌더 책임을 실제 API 기준으로 설명했다.
## 16. Build / 통합 검증
- `npm run typecheck`: PASS.
- `npm run build`: PASS.
- `npm run build-storybook`: PASS.
- Storybook browser: 1280/768/390px 각 136 checks PASS.
- CSS geometry snapshot 비교: PASS (1,633 declarations).
- 기존 Dashboard: 1280px GNB 70px / LNB 272px / main padding 34px, 390px GNB 56px / LNB display none / main padding 20px. 문서 가로 overflow 없음.
- LNB Standalone Mobile: 390px 서비스 메뉴 Sheet 열기/닫기, 중첩 메뉴 표시, Sheet width 342px, 문서 가로 overflow 없음.
- Global Select/DatePicker Portal: 실제 Accent/Active 전달 확인.
- 기존 CSS 예제 `bg-[var(...)]` warning, Storybook 500kB chunk warning 유지. Sandbox에서 Vite/esbuild EPERM이 발생해 동일 명령을 허용된 정상 실행 권한으로 재실행했다.
## 17. FOLLOW_UP
- 서비스 네트워크 Theme Loader/API/CDN/Publish는 별도 단계다. Bootstrap 전에 동기 Theme 적용은 가능하지만 네트워크 이후 flash의 실제 운영 측정은 미수행.
- Scoped popup이 필요하면 기존 HM Portal 경계의 API 설계를 별도 판단한다. 이번에 Component API/Portal을 바꾸지 않았다.
- Slider halo 및 GNB/LNB inverse focus outline을 일반 Focus 정책으로 통합하려면 Default 디자인 변경을 포함해 별도 결정한다.
- Light/Dark와 Theme selector UI, browser별 렌더 차이, 새 accessibility 대비 승인 절차는 이번 구조 작업 범위 밖이다.
## 검증 화면
Global External에서 실제 Select Portal을 연 화면: [runtime-theme-global-select.jpg](../../_local/runtime-theme-global-select.jpg). Primary/Active/Focus/Accent/Destructive는 각각 다른 값이다.
## 작업 시간 (Asia/Seoul)
- Started At: 2026-10-06 14:40:37 KST
- Completed At: 2026-10-06 15:01:50 KST
- Actual Duration: 21분 13초
- Estimated Duration: 최초 45–60분
- Variance: 예상보다 23분 47초–38분 47초 단축
# 후속: 디자인팀 실시간 Theme 운영 — 2026-10-06
이 문서 앞부분은 이전 Runtime 전환 기록이다. Git baseline `d1015309ab43fba69c8959a3dda660358e0e8fa1` 이후 JSON을 컬러 원본으로 연결하고 독립 Theme artifact/URL Loader/Storybook Preview를 추가한 결과는 [Designer Theme Report](designer-theme-report.md)에 기록했다. 사용 방법은 [디자인팀 Theme 가이드](designer-theme-guide.md), 운영/배포는 [Theme delivery](theme-delivery.md)다. 과거 수치를 새 검증 수치로 대체하지 않았다.
+117
View File
@@ -0,0 +1,117 @@
# Runtime Theme Contract v1
2026-10-06 · 분류: HM_THEME / Theme infrastructure. Component props, DOM, Radix 상태, Portal, 키보드, ARIA, breakpoint는 변경하지 않았다.
## 현재 파일과 적용 흐름
`themes/default.json` → 생성 `packages/ui/src/theme/primitives.css` / `default.css` HM fallback / Semantic → `<component>.css` Component Mapping → `[data-slot]` CSS → UI. 운영 External JSON은 같은 Contract로 Runtime 적용한다.
`packages/ui/src/theme/contract.ts`는 외부 JSON key, CSS property, 타입과 생성 Default 데이터를 연결하는 공개 계약이다. `runtime.ts`가 검증 후 DOM scope에 inline CSS 변수를 넣는다. 서비스는 `@hm-design/ui/theme`를 사용한다. `src/theme/preview.ts`는 같은 함수의 호환 adapter다. Production URL Loader는 `loader.ts`의 fetch/resolve/load API이며 회사 CDN 주소는 없다. `.storybook/preview.tsx`는 JSON Default + partial을 Global로 적용하고 Designer Playground가 JSON URL/파일 Preview를 제공한다. React Production Provider/Publish API는 없다.
운영/분리 배포: [Theme delivery](theme-delivery.md). 디자인팀 Preview/편집: [사용법](designer-theme-guide.md). 전체 입력/API 계약: [Runtime 계약표](runtime-theme-contract.md).
React 외 실행 형식으로 변환할 때도 이 문서의 Token·CSS Custom Properties·Runtime 적용 계약을 유지한다. Target Runtime, standalone asset, `file://` 검증 및 AI/도구 독립성 기준은 [Runtime Portability Policy](runtime-portability.md)를 따른다.
소비 CSS는 `tailwindcss` → `@hm-design/ui/tailwind.css` → `@hm-design/ui/theme.css` 순서로 앱 entry에서 로드한다. Dashboard 상태 표시가 있으면 그 뒤에 `@hm-design/ui/dashboard-status.css`와 소비 Badge mapping을 연결한다([상태 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)). Status CSS는 변수만 정의하며 73개 Runtime Theme 계약에 새 상태 Token을 추가하지 않는다. [Tailwind 검색 범위](runtime-portability.md#21-tailwind-v4-소비자-검색-범위)와 [Noto Sans KR·Asset 경로](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)는 실행 환경별 소비 가이드를 따른다.
Default는 특별한 컴포넌트 내부 상수가 아니라 이 계약을 만족하는 기본 제공 Theme Set이다. 주입이 없으면 패키지 CSS의 Default preset을 사용한다. `defaultRuntimeTheme`는 동일한 값을 가진 JSON 형태 preset이다. Story의 CSS fallback / Default preset 주입 비교와 계약 값 검사로 두 표현의 일치를 검증한다.
## 공개 경계
- **공개 Runtime 계약**: Semantic 색상 역할, 기존 그라데이션 stop 세트, 선택적 Theme 팔레트, 기존 radius 2개. 모든 Token은 선택적이다.
- **내부 Derived**: `--hm-sem-surface-highlight`(Accent 45%), gradient 조합, shadow/overlay 조합, shadcn adapter(`--primary`, `--ring` 등). 외부 JSON은 이 변수를 직접 쓰지 않는다.
- **내부 Component Mapping**: `--hm-checkbox-*`, `--hm-calendar-*`, `--hm-pagination-*` 등. 기존 이름을 유지한다. Runtime JSON으로 받지 않는다.
- Typography, height, spacing, gradient geometry, shadow geometry, responsive layout은 코드 계약이다. 이번 컬러 구조 변경으로 Runtime 범위를 확대하지 않았다.
- Typography 현황(2026-10-08): 기존 크기와 ui_viz 승인 변경값을 유지한다. `packages/ui/src/theme/*.css`의 font-size는 px이고 shadcn 유래 Tailwind class(`text-sm` 등)는 rem 기반이라 단위가 혼재한다. 소비자가 Tailwind class나 `className`으로 크기를 덮어쓰면 Theme 값보다 우선할 수 있다. Typography Role(heading/body/label 등)은 아직 공통 Token으로 도입하지 않았고 Runtime JSON 73개 계약에도 포함하지 않는다.
기존 25개 allowlist의 JSON 이름과 validation을 유지하고 Active / on Active / Inverse Text / Subtle Surface / Accent / on Accent를 추가했다. 같은 CSS property로 향하는 중복 공개 key는 없다. Component 수백 개의 Token을 요구하는 대신 Theme 팔레트는 선택적 확장이다. 핵심 역할만 지정해도 사용할 수 있다. 팔레트 이름은 색상 계열을 유지하며, 역할 Token은 각각 독립적으로 override할 수 있다.
전체 입력 목록과 영향은 [Runtime 계약표](runtime-theme-contract.md), 구조와 적용 절차는 이 문서와 [Theme delivery](theme-delivery.md)를 따른다. 과거 감사 CSV·작업 보고서는 테스트 ZIP에 포함하지 않는다. 새 공개 Token 추가는 패키지/계약 변경이다. `schemaVersion: 1` 입력 구조를 유지하며 별도 발행/versioning 시스템은 만들지 않았다.
## 기본 적용
앱 초기 CSS에서 순서를 유지한다.
```css
@import "tailwindcss";
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
```
Theme 주입을 하지 않으면 HM Default가 자동 적용된다. `tailwind.css`의 HM과 중복되던 upstream root/dark 색상 preset은 제거했고 chart/sidebar fallback만 base layer에 남겼다. `theme.css`는 Default를 먼저 import하고 기존 Component/Pattern import 순서를 유지한다. Filter Bar state padding 순서도 바꾸지 않았다. Runtime inline 값은 이후 기본 CSS import보다 우선한다. `!important`로 별도 override한 소비 CSS는 이 우선순위 계약 밖이다.
## External Theme 적용
```ts
import { applyRuntimeTheme, resetRuntimeTheme } from "@hm-design/ui/theme"
const accepted = applyRuntimeTheme(document.documentElement, {
schemaVersion: 1,
tokens: {
"color.action.primary": "#6837a8",
"color.state.active": "#007f73",
"color.stroke.input.focus": "#c75b00",
"color.accent": "#ffd36d",
"color.action.destructive": "#d41466",
},
})
// 선택적 교체 및 해제. 실제 Theme 값은 서비스의 검증/승인 과정을 거친다.
resetRuntimeTheme(document.documentElement)
```
기존 Preview 함수는 입력이 `unknown`이고 반환은 `boolean`이다. 새 공개 함수도 같은 형태다. 외부 Primary는 Badge default, Pagination current, Button outline, GNB/LNB current ink를 바꾼다. Brand Button/GNB 면은 기존 stop 계약, Solid Button 면은 팔레트 계약을 사용하므로 Primary 한 개로 gradient나 Solid 면을 임의 재설계하지 않는다. 기존 stop 세트 전체를 제공하면 해당 gradient도 바뀐다.
## Partial / Empty / Invalid / Switching
- `{ schemaVersion: 1, tokens: {} }`: 유효한 빈 Theme이며 Default를 사용한다.
- 부분 Token만 제공: 같은 scope의 미지정 값은 HM Default를 사용한다. Theme A에서 B로 교체할 때 A의 키가 남지 않는다.
- `null`, 단순 `{}`, 잘못된 schemaVersion, 알 수 없는 key, Component Token key, 잘못된 값: false를 반환하고 이 helper의 이전 override를 해제한다. 정상적인 host에서는 Default로 돌아간다. host가 원래 소유한 inline override는 복원한다.
- 불투명 6자리 HEX만 허용한다. radius는 2–12px 정수만 허용한다. `var()`, `url()`, CSS 함수, `!important`, 임의 CSS rule은 입력할 수 없다.
- 기존 3개의 gradient stop 그룹은 각각 전체 또는 미지정만 허용한다. 한 stop만 보내면 문서 전체를 거부한다.
- 입력 전체를 검증한 뒤 DOM에 쓴다. 하나의 잘못된 값 때문에 CSS가 오염되는 대신 마지막 override를 해제한다. 이 atomic fallback은 기존 정책을 유지했다.
- cleanup은 helper가 덮기 전 inline 값과 priority 및 `data-hm-theme` 속성을 복원한다. 같은 scope는 한 owner가 적용/해제를 관리한다. 서로 다른 helper 호출자가 같은 scope를 동시에 소유하는 다중 owner 시스템은 제공하지 않는다.
## Global / Scoped Theme
`document.documentElement` 주입은 Global이다. container를 전달하면 helper가 `data-hm-theme="runtime"`을 설정한다. CSS의 `:root, [data-hm-theme]` 규칙이 그 scope에서 Semantic/Derived/Component Mapping을 다시 계산한다. 단순히 후손 변수만 바꾸고 root에서 이미 계산된 Component 값을 상속하는 문제를 피한다.
```ts
const panel = document.querySelector<HTMLElement>("#service-panel")!
applyRuntimeTheme(panel, {
schemaVersion: 1,
tokens: { "color.state.active": "#345678" },
})
```
명시적 scope는 **독립 HM preset + 자신의 partial override**다. Global A 아래 Scoped B의 미지정 값은 A가 아니라 HM Default다. 이 정책은 밝은/어두운 모드나 Brand selector를 새로 만들지 않고 서로 독립된 scope를 검증하기 위한 것이다. 소비 CSS만으로 적용하려면 `[data-hm-theme="service-a"] { --hm-sem-state-active: ...; }`를 기본 CSS 뒤에 선언한다. 기존 host inline 값이 있는 경우에는 그 값이 우선한다.
**Portal 경계**: CSS inheritance는 DOM을 따른다. Select, Date Picker, Sheet 등 body Portal은 wrapper scope의 자손이 아니다. Global Theme에서는 그대로 같은 값을 받는다. 현재 HM SelectContent / PopoverContent / DatePicker는 Portal container API를 공개하지 않는다. 따라서 wrapper만으로 popup을 scoped Theme로 바꿀 수 없으며, popup은 Global Theme를 따른다. raw Radix Portal을 직접 조합하는 사용처에서 이미 container를 설정할 수 있다면 해당 Theme scope를 쓰거나 포털 호스트에 같은 계약을 적용한다. 기존 HM popup에 scope 전달을 추가하는 일은 별도 API/구조 작업이다.
## 의미별 독립성 및 기존 표현
- Active: Checkbox checked, Radio selected, Switch on, Slider range/thumb border, Progress fill, Calendar single / range edge, Date Picker의 Calendar selected.
- Focus: `color.stroke.input.focus` → `--hm-sem-stroke-input-focus` → `--ring`. Checkbox/Calendar/Input/Select 등의 Focus와 Active는 독립적이다. Default에서는 둘 다 Light 2지만 Verification에서는 다른 값이다.
- Primary: Badge default, Pagination current, Button outline, GNB/LNB current/strong ink. Active와 alias하지 않는다.
- Accent 45%: Select/Menu Item/Calendar hover·range middle/Pagination hover/Table row hover·selected 등의 Soft Highlight. 내부 `--hm-sem-surface-highlight`를 사용한다. Accent 100% + On Accent: Toggle/Toggle Group의 Persistent Selected Surface, Menubar 상위 Highlighted/Open, Navigation Menu Trigger/Open. Navigation Current는 Primary + contextual surface로 Control Active와 구분한다.
- On Accent: Primary와 sibling Semantic이다. `color.on.accent`와 CSS-only package fallback은 독립 기본값을 가지며 Primary를 override해도 On Accent는 함께 바뀌지 않는다.
- Destructive: Input 오류, Badge/Button/Menu 오류 표현. Primary/Active/Focus와 독립적이다.
- Slider Hover/Drag halo는 Primary 20%, Keyboard Focus halo는 Focus Semantic 20%다. Focus와 Hover/Drag가 겹치면 Focus 색이 우선한다. GNB/LNB는 contextual navigation focus 예외(초록 면의 inverse outline, Current/Open 면의 contextual outline)를 유지한다. 팔레트 기반 Table header/Tab/navigation 장식은 Control Active 의미가 아니다. Data Table Sort Indicator는 별도 Green Light 3 palette 기반이며 [Pattern 문서](../patterns/data-table.md)를 따른다.
## 초기 렌더
Default CSS는 첫 paint 전에 로드한다. Story scope는 `useLayoutEffect`로 적용해 React paint 전에 값을 준비한다. 서비스 Global 적용은 앱 bootstrap에서 React render 전에 호출할 수 있다. 네트워크 Theme를 나중에 받으면 Default → External 변화가 생길 수 있으며 bootstrap/캐시 정책의 책임이다. 네트워크 flash 측정이나 hydration 시스템은 이번에 만들지 않았다.
## 검증 Story
`src/theme/runtime-theme.stories.tsx`는 숨김 `verification` Story다. 기존 컴포넌트 API로 Button/Input/Checkbox/Radio/Switch/Select/Slider/Progress/Calendar/DatePicker/Pagination/Badge/Table과 Command를 렌더링하며 Navigation Preview는 GNB / LNB 중 하나만 선택한다. Dashboard Theme 예제의 콘텐츠에는 외부 GNB/LNB를 넣지 않는다.
- `/iframe.html?id=theme-runtime-contract--comparison&viewMode=story`: HM Default / External 비교.
- `/iframe.html?id=theme-runtime-contract--global-external&viewMode=story`: Global External 및 실제 Select/DatePicker Portal 검증.
- `/iframe.html?id=theme-runtime-contract--verification&viewMode=story`: 자동 DOM/계약/독립성/cleanup 검사 결과.
- `/iframe.html?id=theme-runtime-contract--dashboard&viewMode=story`: 기존 Dashboard composition에 Scoped Theme 적용.
`verification-baseline.json`은 Runtime 전환 전 CSS root 변수 그래프의 **Story 전용 고정 snapshot**이다. Production import가 없다. 새 Default와 같은 컴포넌트를 렌더링해 색상·geometry를 비교한다. 기준값을 새 CSS에서 재생성하지 않는다.
자동 play와 같은 검사를 Run verification 버튼으로 다시 실행할 수 있다. Verification 화면의 transition-duration만 0으로 고정해 중간 애니메이션 프레임이 최종값 검사에 섞이지 않도록 했다. Production transition/동작은 바꾸지 않았다.
@@ -0,0 +1,182 @@
# shadcn 기반 구현 원칙
> 적용 범위: HMDesignSystem의 새로운 Component / Pattern / Story 구현과 기존 테스트 디자인 재현
>
> 이 문서는 `docs/guidelines/프로젝트-지침.md`의 컴포넌트 구현 규칙을 보완하는 공통 지침이다. 두 문서가 충돌하면 더 구체적인 컴포넌트 문서(`docs/components/`, `docs/patterns/`)의 사용자 확정 결정과 이 문서의 재사용·구조 원칙을 함께 적용한다.
> **2026-10-07 사용자 확정 — AI 기본 구현 정책의 상세 Source of Truth.** 사용자가 별도로 기존 컴포넌트/디자인 시스템 사용, Theme 적용, Semantic Token 사용, Navigation 정책 준수를 요청하지 않아도 AI는 모든 신규·수정 UI에 자동 적용한다. 기본 전제는 **Existing Component First / Theme Applied By Default / Single Navigation Mode / Minimum Custom**이다. 과거 문서·예제와 충돌하면 이 정책과 [Navigation Single Mode Policy](navigation-single-mode.md)를 우선한다.
## 1. 구현 전 필수 판단
작업을 시작하기 전에 반드시 다음 질문에 답한다.
먼저 변경 범위를 `STORYBOOK_ONLY | PRODUCTION_UI | BOTH` 중 하나로 정하고, 아래 기존 구현과 사용례를 검색한다. 디렉터리 존재 여부와 관련 후보를 확인한 뒤 필요한 파일만 읽는다. CENTER 규격은 CENTER 작업에만 적용하며 일반 UI의 근거로 섞지 않는다.
| 탐색 대상 | 확인 내용 |
|---|---|
| `packages/ui` | 공개 exports, `src/components/ui`, `src/patterns`, Theme / Component Mapping |
| `src/components` | 대표 Component Story, 기존 Variant / Prop, `story-board.tsx` 등 shared helpers |
| `src/patterns` | Layout / Pattern Story, `dashboard-work-status.ts` 등 공통 상태 mapping |
| `src/center` | CENTER 관련 Story 및 `center-stories.css`의 Common Card 구현 |
| 기존 Storybook stories / shared helpers | 유사 대표/PRIMARY Story, composition, 상태·선택·Card mapping과 공통 helper |
| `docs/components/`, `docs/patterns/` | 후보의 현재 API, 적용 Token, 사용자 확정 결정 |
Storybook은 QA 도구이자 **구현 Reference Catalog**다. Button은 Button 대표 Story, Dashboard는 Dashboard 대표 Story, CENTER 카드는 CENTER Common Card Story부터 확인한다. 현재 Story의 시각 언어를 무시해 새 패턴을 만들지 않는다. `verification` Story는 계약/상태 검증용이며 운영 화면의 기본 Reference가 아니다. 특히 혼합 Navigation의 **VERIFICATION ONLY** Story는 신규 구현 근거로 사용하지 않는다. 일반 업무 화면은 Storybook Story·사이드바에 등록하지 않고 별도 React 화면과 독립 미리보기로 제공한다.
Component와 Theme은 함께 설계한다. Component 탐색과 동시에 기존 Theme / Token을 확인하고, 조합과 Semantic 연결을 포함해 화면을 구현한다. 구현 후 Theme을 별도 후처리로 추가하는 흐름은 사용하지 않는다. Theme 우선순위와 Source 수정 조건은 [테마 토큰 가이드](테마-토큰-가이드.md)의 AI 기본 적용 정책을 따른다.
> 이 화면을 현재 `@hm-design/ui` + shadcn 조합으로 만들 수 있는가?
답이 YES이면 그 구조와 동작을 유지하고 HM 디자인 값만 CSS와 Token으로 적용한다. 기본 방향은 **shadcn을 HM 디자인으로 스타일링**하는 것이며, HM 디자인을 이유로 shadcn을 대체하는 별도 시스템을 만들지 않는다.
## 2. 구현 우선순위
다음 순서를 건너뛰지 않는다.
1. 기존 `@hm-design/ui` Component / Pattern 재사용
2. 기존 Variant / Prop으로 요구사항 표현
3. 기존 Composition으로 해결
4. shadcn 기본 Component / Composition 활용
5. Radix 등 shadcn이 사용하는 Primitive 활용
6. 위 방법으로 해결할 수 없을 때만 최소 Custom 구현
기반 기술의 운영 우선순위는 `@hm-design/ui existing → shadcn → Radix Primitive → Composition → CSS / Token Visual → Minimum Custom`이다. 이는 새 기반 기능이 필요한 경우의 선택 기준이며, 이미 있는 HM Variant / Composition을 먼저 확인하는 위 재사용 순서를 건너뛰는 이유가 아니다. 모든 단계에서 Visual은 HM Theme / Token으로 연결하며 직접 primitive 동작을 만드는 것은 마지막 수단이다.
판단 예시는 다음과 같다.
| 필요 | 우선 선택 |
|---|---|
| Button | 기존 HM Button |
| Popup | Popover / Dropdown Menu |
| Disclosure | Collapsible / Accordion |
| Mobile navigation | Sheet |
| 검색 조합 | Input / Select / Command / Combobox |
| Empty / Result 화면 | 기존 Pattern / Component composition |
| Search Form | Input + Select + Button |
| Dashboard Empty | Dashboard Layout + 기존 Empty / Data Table composition |
| Filter 화면 | Filter Bar |
| GNB User Menu | Avatar + Dropdown Menu |
## 3. 구조·동작과 시각의 소유권
shadcn 기반으로 구현 가능한 형태라면 shadcn/Radix의 구조, API, 상태 모델, 접근성, keyboard interaction을 최대한 유지한다.
### shadcn/Radix 구조를 유지하는 영역
- DOM / Component 구조
- Public API
- controlled / uncontrolled 방식
- `data-state`
- ARIA
- keyboard navigation
- focus management
- Portal
- Dismiss / Escape 동작
- `asChild`
- composition 방식
### HM이 소유하고 조정할 수 있는 영역
- CSS와 CSS Custom Properties
- Component Token과 Semantic Token mapping
- height, padding, gap
- border, radius, color, shadow
- typography
- responsive layout
즉, **Behavior / Structure는 shadcn/Radix를 유지하고 Visual은 HM Theme로 조정**한다.
시각 차이는 HM CSS / Token으로, spacing·radius·color 차이는 Component Token으로 해결한다. CSS로 해결 가능한 차이를 위해 새 prop이나 별도 Component API를 추가하지 않는다.
## 4. Component와 Pattern
이미 `@hm-design/ui`에 구현된 Component가 있으면 shadcn Component를 다시 복사하거나 비슷한 별도 버전을 만들지 않는다.
Pattern도 새로운 primitive를 만들기 전에 기존 Component composition을 우선한다. 반복 사용성과 고유한 계약이 실제 제품 사용에서 확인될 때만 별도 Pattern으로 승격한다. 시각 결과가 기존 Component 조합으로 표현 가능하다면 Pattern은 조합과 상태 연결만 소유하고 primitive의 동작이나 스타일을 복제하지 않는다.
shadcn/Radix에 이미 존재하는 interaction은 새로 구현하지 않는다. 기존 Component 여러 개로 표현할 수 있는 화면은 composition으로 해결한다.
Status / Badge / Card / Navigation에 공통 mapping이 있으면 함께 재사용한다. 상태 Badge는 기존 Badge → status mapping → 기존 variant → 필요한 최소 확장 순으로 판단한다. Dashboard 상태는 `src/patterns/dashboard-work-status.ts`, CENTER 카드는 `src/center/center-stories.css`를 확인한다. 이 파일들은 현재 Story 범위의 구현이므로 재사용 시 Production export 여부와 파일 책임을 확인하고 Story helper를 Production에 직접 import하지 않는다. 색상 값·Card CSS를 복사하거나 Dashboard 전용 Badge를 중복 생성하지 않는다.
package exports, public props, public types, package entry, Story ID, `meta.title`을 불필요하게 변경하지 않는다. 요구사항상 변경이 필요하면 소비처, 호환성 및 Story 링크 영향을 먼저 확인하고 사유를 기록한다.
## 5. Storybook과 Production 구현 분리
### 5.1 Storybook Reference와 신규 화면 생성의 분리
일반적인 "화면 만들어줘", 대시보드 또는 업무 페이지 제작 요청에서 Storybook은 기존 디자인 시스템을 확인하는 Reference로만 사용한다. 신규 업무 화면을 Story로 등록하거나 Storybook 사이드바에 추가하지 않으며, 기존 대표 Story를 생성 화면으로 교체하지 않는다. 화면 확인을 위해 Storybook 실행을 필수로 요구하지 않고, 별도의 React 화면을 빌드한 뒤 독립적인 미리보기를 제공한다. H-2 또는 유사 자동화가 사용 가능한 경우 `preview.html`·desktop/mobile 이미지 등 독립 미리보기 산출을 지원할 수 있지만, 이는 Storybook Story 등록과 별개의 역할이다. Storybook 전용 helper/CSS는 운영 화면에 사용하지 않는다.
다음처럼 사용자가 Storybook 수정을 명시한 경우에는 예외로 처리한다: 새로운 Component 또는 Pattern 추가/수정, 대표 또는 Verification Story 작성, Storybook Reference 자체 개선. 이 예외에서도 화면 생성 결과와 Storybook Reference 변경을 서로 다른 범위로 기록한다.
Storybook에서 디자인 상태를 비교하기 위한 표현 때문에 Production API를 추가하지 않는다.
`STORYBOOK_ONLY`의 배치·specimen 표현 문제는 Story 범위에서 해결한다. 이를 이유로 Production Component를 불필요하게 수정하지 않는다. 실제 Production 동작 문제가 있으면 필요한 Production 변경과 관련 Story를 함께 다루며 Story CSS override로만 숨기지 않는다.
다음은 Story 전용 표현으로 처리할 수 있다.
- Hover 고정 specimen
- Selected + Hover 비교
- Spacing guide
- Token reference
Story 안의 실제 화면과 상호작용에는 가능한 한 production `@hm-design/ui` Component를 그대로 사용한다. 특정 상태를 보여줄 때도 production Component의 실제 API와 shadcn/Radix 상태 모델을 우선한다. Story 전용 helper나 selector는 검수 표현에만 한정하며 production bundle이나 Public API에 포함하지 않는다.
## 6. Custom 구현 허용 조건
별도 Custom 구현은 다음을 모두 확인한 뒤에만 허용한다.
- 기존 HM Component로 해결할 수 없다.
- 기존 Component composition으로 해결할 수 없다.
- 기존 Variant / Prop 확장이 요구사항·API 의미에 부적절하다.
- shadcn 기본 Component 및 Radix Primitive 조합으로 해결하기 어렵다.
- CSS / Token 조정만으로 해결할 수 없다.
- 실제 제품에서 반복적으로 필요한 독립적인 동작 또는 API가 있다.
Custom 구현은 필요한 동작과 API에 한해 최소 범위로 제한한다. 결과 보고와 관련 컴포넌트 또는 패턴 문서에는 다음을 기록한다.
- 왜 shadcn으로 해결할 수 없었는지
- 왜 기존 HM 조합으로 해결할 수 없었는지
- 왜 기존 Variant 확장이 부적절하고 Radix 조합도 어려운지
- 추가한 Custom 범위
- Public API 영향
## 7. 금지 사항
- 디자인을 맞추기 위해 shadcn을 통째로 다시 작성한다.
- Radix state logic을 재구현한다.
- 기존 HM Component와 유사한 Custom Component를 추가한다.
- Storybook 검증만을 위한 Production prop을 추가한다.
- CSS로 해결 가능한 차이에 새 API를 추가한다.
- 이전 ui-viz JSX, DOM, class 또는 상태 로직을 그대로 복사한다.
- 기존 Component와 Story를 확인하지 않고 Button / Input / Badge / Card / Dialog / Select / Table / Navigation 등을 생성한다.
- 기존 Badge가 있는데 Dashboard 전용 Badge를 새로 만들거나 기존 Common Card CSS를 복사한다.
- 기존 Theme Token이 있는데 색상·spacing·값을 하드코딩하거나 같은 Token을 여러 파일에 중복 정의한다.
- UI 구현 후 Theme을 선택적 후처리로 적용한다.
- Storybook 표현 문제만을 위해 Production 코드를 바꾸거나 Production 동작 문제를 Story CSS override로 숨긴다.
- GNB / LNB / Dashboard 혼합은 운영 페이지에 사용하지 않는다. 혼합 Navigation은 현재 단일 모드 정책의 조건을 충족하는 기술 검증 Story에만 둔다.
## 8. 구현·검토 체크리스트
- [ ] 현재 `@hm-design/ui`에 재사용할 Component 또는 Pattern이 있는가?
- [ ] 기존 Variant / Prop / Composition과 유사 대표 Story, 공통 mapping을 먼저 확인했는가?
- [ ] shadcn 기본 Component / Composition으로 해결할 수 있는가?
- [ ] Radix 또는 기존 기반 라이브러리가 동작과 접근성을 이미 제공하는가?
- [ ] Component composition으로 별도 primitive 없이 표현할 수 있는가?
- [ ] 남은 차이는 HM CSS / Token만으로 맞출 수 있는가?
- [ ] Story 전용 요구가 Production API로 유입되지 않았는가?
- [ ] Custom이 필요하다면 모든 허용 조건과 기록 요건을 충족했는가?
- [ ] Theme / Semantic 연결, 독립 fallback, 변경 범위와 public API 보호를 지켰는가?
- [ ] 단일 Navigation Mode와 같은 모드의 반응형 표현을 지켰는가?
## 9. AI 신규 화면 구현 순서
AI 화면 구현의 단일 실행 순서는 저장소 루트의 [AGENTS.md Quick Start](../../AGENTS.md#ai-화면-구현-quick-start)를 따른다. 후보 Component·Pattern의 실제 export, 사용 문서, 대표 Story는 [통합 Reference Index](component-pattern-reference.md)에서 찾는다. 일반 업무 화면은 Storybook에 등록하지 않고 별도 React 화면으로 생성하며, 화면별 빌드·필요한 검증·독립 미리보기를 결과로 제공한다. 상세 재사용·Theme·Navigation 조건은 이 문서와 연결된 Source of Truth를 유지한다.
구현 중에는 다음 단계에 필요한 최소 확인만 하고, 완료 후 diff / 요구사항 / 관련 Story / 필요한 프로젝트 검증을 한 번에 수행한다. 같은 조건으로 성공한 검증을 형식적으로 반복하지 않는다.
## 10. 명시적 예외
사용자가 새로운 Component 실험, Theme 미적용 prototype, 외부 디자인 완전 재현, 기존 디자인 시스템과 독립된 POC를 명시적으로 요청한 경우에만 기본 정책의 예외를 검토한다. 예외의 목적·범위·사유와 Theme 미적용 여부를 기록하고 기존 시스템에 영향을 주지 않는 별도 Story / 실험 범위로 격리한다. Production exports / Theme Source / 공통 mapping을 임의로 변경하지 않는다.
이 예외는 혼합 Navigation 운영을 자동 허용하지 않는다. Navigation 조합을 명시적으로 요청받아도 정책 변경인지 기술 검증인지 구분하며 [Navigation Single Mode Policy](navigation-single-mode.md)의 `verification` 태그와 **VERIFICATION ONLY** 조건을 유지한다.
+107
View File
@@ -0,0 +1,107 @@
# Theme JSON 운영과 독립 배포
## Repository와 배포 단위
하나의 Git Repository에서 소스·Theme·Storybook·문서를 관리한다. UI, CENTER Graphic, Theme JSON은 각각 별도 배포 단위다. 현재 두 패키지는 `private: false`이며 compiled dist(JS·타입·CSS)를 export한다. Library build는 `npm run build:packages`로 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 테스트 ZIP 실행에는 Registry 접근·publish가 필요하지 않다. Theme JSON의 독립 산출 경로는 구현했다. 실제 CDN URL·인프라·인증·CI publish는 미정이다.
운영 배포 경계는 `packages/ui` → UI package, `packages/center-graphic` → Graphic package, `themes/*.json` → Theme CDN이다. **Git push ≠ Theme CDN publish**다. 운영 CI/CD는 JSON 검증·독립 artifact 산출 후 CDN에 배포하는 별도 단계로 구성하며, 현재 저장소에는 실제 CDN publish pipeline이 구현되어 있지 않다.
```text
HMDesignSystem/
├─ packages/
│ ├─ ui/ # @hm-design/ui: logic, contract, mapping, runtime
│ └─ center-graphic/ # 독립 Graphic package
├─ themes/
│ ├─ default.json # 71개 public color 값의 공식 Default 원본
│ ├─ service-example.json # 5개 역할만 바꾸는 명시적 예제
│ └─ drafts/ # 과거 초안; Theme 산출 대상 아님
├─ scripts/
│ ├─ theme-templates/ # 컬러 literal 없이 alias/geometry만 소유
│ ├─ theme-defaults.mjs # JSON -> package CSS/default 데이터 생성
│ └─ theme-artifacts.mjs # JSON validation -> 독립 CDN artifact
├─ src/theme/ # Storybook Preview/Editor/verification 소비자
└─ docs/guidelines/
```
`themes/default.json`은 기존 v1 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. Component Token은 없다. 73개 Runtime 입력 중 71개 컬러를 포함한다. 나머지 두 radius와 layout은 패키지 template에서 관리한다. 서비스 Theme는 같은 계약의 부분 컬러 override다.
## 단일 Default 원본과 package fallback
사람이 관리하는 컬러 값은 `themes/*.json`이다. Default JSON과 `scripts/theme-templates`에서 `packages/ui/src/theme/primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. Template는 기존 alias/geometry 정책을 유지하고 HEX literal을 따로 관리하지 않는다. JSON의 역할값이 팔레트 alias의 값과 같으면 기존 alias를 유지하고, 달라지면 JSON의 역할값을 우선한다.
패키지는 생성된 CSS fallback snapshot을 포함하므로 네트워크 Theme가 없거나 실패해도 기본 화면을 렌더링한다. 이미 배포한 UI package의 fallback은 **그 패키지 버전에서 생성한 HM Default snapshot**이다. 최신 운영 Default JSON을 CDN에서 로드하면 UI 재배포 없이 새 값이 적용된다. 네트워크 장애에서 최신 CDN 값까지 보장하는 시스템은 아니며, 그 경우 패키지 snapshot으로 복원한다.
Theme-only CDN 배포는 `npm run build:themes`만 실행한다. UI 재빌드·version up·registry publish가 필요 없다. 이후 UI 패키지를 새로 유지보수/검증할 때는 `npm run generate:theme-defaults`로 repo의 생성 snapshot을 갱신한다. `typecheck/build/build-storybook`의 pre hook은 JSON/template/생성 snapshot의 불일치를 검사한다. 이 검사는 Theme artifact 산출의 선행 조건이 아니다.
## 개발팀 Runtime 적용
설치/초기 서비스 연결 시 UI Component와 CSS를 가져온다. 실제 `themeUrl`은 서비스 설정에서 결정하며 package 내부에는 회사 CDN 주소가 없다.
```ts
import "@hm-design/ui/theme.css"
import {
fetchRuntimeTheme, resolveRuntimeTheme, loadRuntimeTheme,
applyRuntimeTheme, resetRuntimeTheme,
} from "@hm-design/ui/theme"
// serviceConfig.themeUrl은 CDN/API 설정값이다.
const root = document.documentElement
const success = await loadRuntimeTheme(root, serviceConfig.themeUrl)
// success=false: 이 root의 override를 해제하고 host/package HM fallback 유지.
// 별도의 Default JSON + 서비스 partial을 사용할 때:
try {
const defaults = await fetchRuntimeTheme(serviceConfig.defaultThemeUrl)
const serviceTheme = await fetchRuntimeTheme(serviceConfig.themeUrl)
applyRuntimeTheme(root, resolveRuntimeTheme(serviceTheme, defaults))
} catch {
resetRuntimeTheme(root)
}
// 이미 받은 설정/API/로컬 JSON에도 같은 Apply 계약을 사용한다.
// Apply에는 unknown 전체 validation이 있다.
applyRuntimeTheme(root, configurationTheme)
// 설정 URL의 버전/hash가 바뀌거나 운영 refresh 이벤트가 오면 다시 호출한다.
// 라이브러리/Component 재빌드와 브라우저 reload는 필요 없다.
await loadRuntimeTheme(root, serviceConfig.updatedThemeUrl)
```
- `fetchRuntimeTheme(url, RequestInit?)`: HTTP status/JSON/기존 allowlist 검증 후 partial 문서 반환. DOM을 건드리지 않고 실패 시 throw한다. URL/캐시/credentials/signal은 소비 서비스에서 지정한다.
- `resolveRuntimeTheme(theme, defaults?)`: 패키지 Default + 선택적 supplied Default + partial override를 검증 후 합친다. Component Token을 만들지 않는다. 그룹의 일부 stop만 제공하는 입력은 거부한다.
- `loadRuntimeTheme(root,url,{request?,defaults?}?)`: fetch/검증/선택적 merge/Apply의 편의 함수. 오류/invalid/HTTP 실패 시 false와 package fallback. 더 늦게 도착한 이전 요청은 새 Theme를 덮거나 reset하지 않는다. 직접 Apply/Reset도 pending load를 무효화한다.
- `isRuntimeTheme`: DOM 변경 없는 사전 검증. `applyRuntimeTheme`/`resetRuntimeTheme`: 기존 public Apply/Reset 경로와 host inline 값/priority 복원 정책 유지.
앱 시작 전/후 어디서든 호출할 수 있다. CSS는 최초 paint 전에 로드한다. 네트워크 응답 전에는 package fallback, 응답 후에는 External Theme다. 로딩 timeout이 필요하면 서비스에서 `AbortSignal.timeout(...)` 또는 AbortController를 RequestInit에 제공한다. 로더는 polling/push subscription을 자동 시작하지 않는다. 이미 열린 서비스가 최신 JSON을 쓰려면 소비 서비스의 refresh 이벤트/설정 갱신/호출 정책이 필요하다.
## 디자인팀 workflow
1. `themes/service-example.json`을 서비스 파일로 복사하거나 `themes/default.json`의 상위 컬러 역할을 수정한다. 공통 팔레트/gradient보다 Primary/Active/Focus/Accent 같은 역할을 우선 편집한다.
2. Storybook의 `Theme/Designer Playground/Editor`에서 URL/파일 Import로 JSON을 불러온다. 기본값은 `themes/default.json`을 Runtime merge하여 표시한다. 개발 서버에서는 Default JSON import의 HMR Preview도 가능하다. 다른 서비스 파일을 변경한 뒤 같은 URL을 Load URL로 다시 불러오면 CSS/Component 수정 없이 즉시 반영한다.
3. 실제 Input/Control/Calendar/Date Picker/메뉴/표/내비게이션과 1280/768/390 레이아웃을 확인한다. Editor의 값 변경도 같은 공식 Contract를 사용한다.
4. Editor를 이용했다면 Export한 JSON을 해당 `themes/*.json`에 저장해 Git에 포함한다. Editor는 repository 파일을 자동으로 덮어쓰지 않는다.
5. Git diff/review 후 디자인팀의 승인된 운영 절차로 commit/push한다.
6. 아래 Theme-only 산출물을 CDN에 배포한다. Git은 이력/검토, CDN은 Runtime delivery를 담당한다. 서비스가 Git 파일을 직접 읽지 않는다.
## Theme-only 배포
```powershell
npm run build:themes
# 결과: theme-dist/themes/default.json, service-example.json 및 추가한 top-level JSON
```
전체 문서 validation을 통과한 뒤에만 산출한다. `themes/drafts`는 제외한다. 삭제된 source의 옛 top-level JSON artifact도 출력 디렉터리에서 제거한다. 산출물은 `theme-dist/themes/`이며 UI/Graphic bundle과 별개다. 이 디렉터리의 파일만 선택한 CDN origin에 업로드하고 서비스 설정의 Theme URL을 갱신한다. CDN infrastructure/publish credentials/자동 pipeline은 구현하지 않았다. CDN은 cross-origin 사용을 위한 CORS 응답과 JSON Content-Type을 설정해야 한다.
## Cache/버전/롤백 권장안
인프라 미정 상태의 **추천안**은 `/themes/service-a/<content-hash>.json` 또는 `/themes/service-a/v12.json`처럼 변경마다 별도 파일을 발행하고 서비스 설정을 새 URL로 바꾸는 방식이다. 이전 파일을 유지하므로 rollback은 이전 URL로 돌린 후 Loader를 다시 호출한다. 버전 파일에는 장기 `Cache-Control: public, max-age=31536000, immutable`을 사용할 수 있다. AWS도 versioned 파일/디렉터리 이름으로 갱신하는 방식을 안내한다. [CloudFront 파일 versioning](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html), [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control).
대안은 `/themes/service-a.json?v=<content-hash>`다. CDN cache key가 query를 포함하도록 설정해야 효과가 있다. 안정 URL을 덮어쓰려면 짧은 TTL/`no-cache`와 ETag 재검증 또는 CDN purge 정책을 함께 정한다. 브라우저의 `fetch(...,{cache:"no-store"})`만으로 CDN edge 캐시를 항상 갱신한다고 보장하지 않는다. Preview는 no-store로 요청하지만 Production 캐시 정책은 소비 서비스/CDN 설정의 책임이다. [MDN HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching), [MDN Request.cache](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache).
실제 운영 URL/versioning 방식은 서비스 설정에서 확정한다. 자동 버전 관리/manifest/publish/rollback 서비스는 미구현이다.
## 보안/제한
공개 allowlist의 HEX6/기존 radius 값만 Production Runtime이 허용한다. Theme artifact와 Designer UI는 컬러 전용이며 Component Token/임의 CSS/HTML/JS/URL 값은 거부한다. invalid Import는 마지막 유효 Preview를 유지하고 URL Loader 실패는 package HM Default로 돌아간다. Partial은 교체이며 이전 Theme key가 남지 않는다.
Global root Theme는 body Portal에도 적용된다. wrapper 밖 Portal은 Global을 사용하며 Scoped 전달은 기존 FOLLOW_UP이다. 동일 root는 한 owner가 관리한다. 새 상위 역할/Mapping/behavior가 필요한 변경은 UI package 작업이며 단순 Theme 컬러 변경과 구분한다.
+119
View File
@@ -0,0 +1,119 @@
# 개발 적용 가이드 (검토용 개정안)
페이지 구현 전에 [Navigation Single Mode Policy](navigation-single-mode.md)를 적용한다. AI 코드 생성·제안, 신규/기존 화면, 프로토타입, Production 모두 GNB / LNB / DASHBOARD 중 정확히 하나만 선택한다. 모바일도 같은 체계의 Responsive Variant를 사용하며 별도 요청 없이 Navigation 조합을 생성하거나 제안하지 않는다.
> 대상: 디자인팀과 서비스 개발팀. 현재 UI/CENTER compiled dist build/export, JSON Default/서비스 partial, 공개 Runtime 검증·Global/Scoped Apply와 URL Loader, Storybook JSON Preview, Theme-only artifact 산출이 구현되었다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 실제 CDN infrastructure/publish pipeline은 다음 단계다. 테스트 ZIP은 로컬 workspace와 포함 dist를 사용하므로 Registry 접근·publish가 필요하지 않다. 아래 장기 운영 예시의 `@company/ui`는 목표명이며 현재 import는 `@hm-design/ui`다. 현재 실제 API/독립 배포 절차는 [Theme delivery](theme-delivery.md), 디자인팀 workflow는 [Theme guide](designer-theme-guide.md)를 따른다.
## 1. 착수 순서
AI 신규·수정 UI 작업은 사용자의 별도 요청 없이 기존 Component / Storybook Reference 우선, Theme / Semantic 기본 적용, 단일 Navigation, 최소 Custom을 적용한다. 변경 범위(`STORYBOOK_ONLY | PRODUCTION_UI | BOTH`)를 정한 뒤 [AI 신규 화면 구현 순서](shadcn-기반-구현-원칙.md#9-ai-신규-화면-구현-순서)를 따른다. Theme 사용과 Theme Source 수정은 [테마 토큰 가이드](테마-토큰-가이드.md)의 별도 조건을 적용한다.
### 1.1 패키지·운영 기반 도입 순서
아래는 서비스 도입 계획이며 이미 구현된 HM Component를 새로 생성하라는 순서가 아니다.
1. 서비스별 사용 현황을 모아 공통화할 컴포넌트 3~5개와 필요한 Semantic Token을 선정한다. 예: Button, Input, Card, Dialog, Select.
2. `@company/ui`의 공개 API, 기본 Theme CSS, Semantic Token 계약을 버전 관리한다. 이름·타입·기본값·허용 범위를 한곳에서 정의한다.
3. shadcn 컴포넌트는 Design System 저장소에서만 도입한다. upstream 버전/도입일과 변경 이유를 기록하고 회사 API·토큰·접근성 기준에 맞게 수정한다. 이후 upstream 변경도 diff를 검토해 선택적으로 반영한다.
4. Storybook이 패키지의 **공개 진입점**을 사용하게 하고 대표 상태 및 Theme Preview를 추가한다.
5. 한 앱에서 패키지와 기본 CSS를 설치한다. 앱 번들에 import하지 않은 별도 로컬 서버의 Theme JSON을 Loader로 읽어 값만 바꾸고, 앱 재빌드 없이 다음 진입·새로고침에서 반영되는지 확인한다. 운영 단계에서는 이를 API/CDN Publish 경로로 바꾼다.
서비스 앱에서 `shadcn add`로 컴포넌트를 별도 생성하거나 공통 컴포넌트 코드를 복사해 고치지 않는다. 서비스 전용 조합은 공개 컴포넌트를 감싸서 만든다. 공통 API 변경이 필요하면 Design System 변경으로 제안한다.
## 2. 단일 저장소의 파일 책임과 패키지 경계
현재 파일 배치와 목표 배치를 구분한다. **한 Git 저장소**에서 코드, Storybook, Theme 초안을 함께 버전 관리할 수 있으며, 별도 Git 저장소나 monorepo 구성은 필수가 아니다.
| 위치 | 현재/목표 | 책임과 배포 경계 |
|---|---|---|
| `packages/ui/src/components/ui/`, `src/hm-theme.css` | 현재 구현 | Production Component와 Demo 스타일 분리. `@hm-design/ui` compiled dist export (JS·타입·CSS) |
| `.storybook/`, `src/components/*.stories.tsx` | 현재 시제품 → 계속 사용 | 실제 컴포넌트 검수. 패키지 산출물에는 Story를 넣지 않음 |
| `packages/ui/src/patterns/`, `packages/ui/src/theme.css` | 현재 구현 | 업무/Layout composition과 기본 Theme CSS. Library 산출물은 `npm run build:packages`로 생성 |
| `packages/center-graphic/` | 현재 구현 | `@hm-design/center-graphic` 독립 package 배포 경계. UI package와 필수 의존 없음 |
| `packages/ui/src/theme/contract.ts`, `runtime.ts` | 현재 구현 | 공개 73개 입력, 형식/범위 검증, atomic fallback·cleanup, Global/Scoped 적용. `@hm-design/ui/theme` export |
| `themes/drafts/` | Runtime MVP 제안 | 디자인팀의 Theme 값 초안·검수 이력. Git 커밋만으로 운영 Theme이 바뀌지 않으며 패키지에도 넣지 않음 |
| `themes/*.json` | 현재 구현 | `themes/default.json`은 공식 Default Color Source of Truth. 검증된 top-level JSON은 Theme CDN 배포 단위이며 Git push와 CDN publish는 별도 단계 |
| 서비스 앱의 bootstrap | 개발팀 앱 | 패키지 버전 설치, themeId/mode 결정, API/CDN fetch·캐시·실패 처리, 검증된 값 적용 |
패키지 공개 파일은 `package.json`의 export·포함 목록과 빌드 결과로 확인한다. 기본 CSS와 컴포넌트 CSS 규칙은 패키지 버전에 묶인다. 외부 발행 대상은 계약에 등록된 **기존 Semantic Token 값**이며, 변경된 JSON/CSS 초안을 자동으로 운영 앱에 노출하지 않는다. 필요해지면 `packages/ui`와 `apps/storybook`으로 재배치해도 Git 저장소는 하나로 유지한다.
검증 계약은 Storybook·서비스 앱·Theme Publish가 공유해야 한다. 현재 검증·적용 도우미는 `@hm-design/ui/theme`에서 공개한다. 네트워크 URL, tenant 식별, 인증, 갱신 시점은 개발팀이 서비스 앱에서 관리한다.
## 3. Token 연결과 기본값
현재 연결은 Primitive → Semantic → Component Mapping이다. 다음은 실제 HM selector와 Token 이름을 사용하는 예다. 컬러 원본은 `themes/default.json`이며 생성 CSS를 별도 수동 원본으로 관리하지 않는다. 전체 입력은 [Runtime 계약표](runtime-theme-contract.md)를 따른다.
```css
/* 앱 초기 렌더 전에 로드 */
@import "tailwindcss";
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
/* package의 Input Component Mapping이 Semantic을 소비하는 예 */
:root, [data-hm-theme] {
--hm-input-border-color: var(--hm-sem-stroke-input-default);
}
[data-slot="input"] {
border-color: var(--hm-input-border-color);
}
```
Tailwind `bg-primary`를 사용한다면 package의 `tailwind.css`에서 `--color-primary` → `--primary` → `--hm-sem-action-primary` 연결을 확인한다. 클래스 이름이 같다는 이유만으로 Theme 연동이 성립하지 않는다. 컴포넌트 코드의 임의 hex/px 값, Primitive 직접 참조, 서비스별 `!important` override는 허용하지 않는다. 구조적 고정값은 Runtime 계약 밖의 코드 값으로 별도 관리할 수 있다.
## 4. Theme 문서와 버전 호환성
MVP의 Theme 문서 예시는 다음과 같다. `tokens`는 **선택적 override**이므로 없는 키는 기본 CSS를 사용한다. 현재 Light/Dark 구분이 없어 모드별 구조는 두지 않는다.
```json
{
"schemaVersion": 1,
"themeId": "default",
"themeVersion": "2026-10-02.1",
"tokens": {
"color.action.primary": "#7c3aed",
"radius.input.default": "6px"
}
}
```
- `schemaVersion`은 필드 구조와 Token 계약의 호환성을 판단한다. 앱이 지원하지 않는 버전이면 Theme 전체를 적용하지 않는다.
- `themeVersion`은 Publish할 때마다 새로 발행하는 불변 값이다. `@company/ui` 패키지 버전과 독립적이다.
- 구버전 앱이 남아 있는 동안에는 해당 앱의 계약에서 지원하지 않는 Token을 운영 Theme에 발행하지 않는다. 계약 확장은 패키지 릴리스와 앱 업데이트가 먼저다.
현재 HM Storybook 시제품은 브랜드 배경과 주요 버튼의 면·테두리 그라데이션을 역할별 stop 색상으로 조합한다. 초안 JSON에는 allowlist에 등록한 stop 색만 6자리 HEX로 적고, 배경·버튼 면·버튼 테두리 각각의 stop 세트는 부분 적용하지 않는다. 각도·stop 위치·그림자는 기본 CSS 규칙으로 유지한다. `radius.input.default`와 `radius.container.default`는 별도 Semantic 역할로 연결하고 2–12px 정수를 공통 Runtime API에서 허용한다. Placeholder는 확인된 `color.fg.placeholder` 기본값을 사용한다. `ui_viz`의 Button D-087 색을 HM Runtime Semantic stop에 연결한 것은 현재 저장소의 **구현된 Runtime 계약**이다. 운영 allowlist로 승격하기 전에 디자인팀이 역할 이름·범위·호환성을 승인해야 한다.
초기 검증 정책은 색상은 승인된 형식(예: 불투명 hex), radius는 허용 범위의 `px` 값으로 제한한다. shadow, font, spacing을 추가할 때에는 **각각 별도 타입·범위·호환성 검사**를 설계한다. 임의 CSS 함수, `url()`, `var()`, `!important`, selector, JavaScript는 Theme 값으로 허용하지 않는다. 접근성 검토는 개별 값의 형식 검사와 별도로 수행한다.
## 5. 앱 Theme Loader의 책임과 실패 처리
1. 앱이 기본 Theme CSS를 먼저 로드한다.
2. API/CDN에서 활성 버전 포인터와 불변 Theme 문서를 가져온다. timeout과 캐시 정책을 명시한다.
3. 문서 전체의 스키마 버전, ID, allowlist, 값 타입·범위를 검증한다. **하나라도 실패하면 일부 키만 적용하지 않는다.**
4. 검증이 끝난 뒤 이전 Runtime override 키를 제거하고 허용된 키만 적용한다. Theme을 교체할 때 남은 오래된 키가 기본값을 가리지 않게 한다.
5. fetch·검증 실패 시 Runtime override를 제거하고 기본 CSS를 사용한다. 실패 원인과 적용한 버전을 기록한다.
6. 운영 초기에는 새 페이지 진입·새로고침 때 최신 활성 버전을 읽는다. 열린 화면의 실시간 교체는 별도 요구가 생길 때 도입한다.
현재 적용 함수는 unknown 입력을 자체 검증하며 문서 전체 실패 시 atomic fallback한다. Global은 document root, Scoped는 명시적 wrapper에 적용하고 cleanup으로 host 기존 inline 값/priority를 복원한다. Theme ID 확인·URL/캐시 정책은 서비스 책임이며 공개 URL Loader는 HTTP/JSON/Runtime 계약을 검증한다. 렌더 중 깜빡임이 문제라면 초기 Theme 로딩 순서와 CSS 적용 시점을 앱 bootstrap에서 조정한다. 캐시는 성능용이며 검증과 기본값 fallback을 대체하지 않는다.
**서비스 전역 Theme는 Global Runtime Theme를 권장한다.** `applyRuntimeTheme(document.documentElement, theme)` 또는 `loadRuntimeTheme(document.documentElement, themeUrl)`로 적용하면 body Portal도 같은 Theme를 받는다.
**Scoped wrapper Theme는 wrapper 밖 body Portal에 자동 전달되지 않는다.** SelectContent, PopoverContent(Date Picker·Combobox 포함), DropdownMenuContent/ContextMenuContent 등 내부 body Portal popup은 Global Theme를 따른다. 이 Content wrapper에는 Theme scope를 전달하는 `container` API가 없다. 별도 export된 Radix Portal wrapper의 props를 Content의 container 지원으로 간주하지 않는다. Scoped는 DOM 자손에만 적용되며 세부 apply/merge/fallback/scope 계약은 [Runtime Theme](runtime-theme.md)를 따른다.
## 6. Storybook 작성·검수 기준
- Story import는 `@company/ui`의 공개 진입점과 기본 CSS를 사용한다. 로컬 내부 파일을 직접 가리켜 패키지 배포 결과와 달라지는 구성을 피한다.
- 컴포넌트별 공개 props, variant/state, 오류·비활성·로딩, 키보드/포커스 동작을 지원 범위에 맞게 기록한다.
- 현재 Theme verification Story에서 Default/External과 Runtime 계약을 비교한다. Theme selector toolbar는 없다. Preview는 운영 Publish를 수행하지 않고 패키지 Runtime API를 사용한다.
- 값 검수는 실제 컴포넌트의 기본/hover/focus/disabled/error 상태, 대비·가독성, 잘림·넘침을 포함한다. Theme 변경이 닿는 Story에 집중한다.
- 검수 기록에는 `@company/ui` 버전, Theme 버전 또는 Draft ID, 결과를 남긴다. Storybook 검수 후 실제 앱에서도 같은 계약이 적용되는지 통합 확인한다.
## 7. 개발 완료 기준
- 단일 저장소에서 만든 `@company/ui`의 공개 컴포넌트·기본 CSS를 서비스 앱이 버전으로 설치하고, Theme 장애에도 기본값으로 동작한다.
- 별도 제공되는 승인된 Semantic Token 값만 변경해 Button 등 대표 컴포넌트가 앱 재빌드 없이 새 진입·새로고침에서 바뀐다. 패키지 CSS 규칙 변경에는 새 패키지 버전이 필요하다.
- 패키지 Runtime API는 잘못된 키·값·스키마 버전을 거부한다. 서비스별 themeId 일치 검사는 향후 Loader가 담당한다(현재 API가 ID를 검증한다고 간주하지 않는다).
- Theme 교체·해제 시 이전 override가 남지 않는다.
- Storybook Preview와 앱에서 동일한 Theme 문서를 같은 계약으로 검증한다.
- 컴포넌트 API 변경은 Theme Publish만으로 처리하지 않고 패키지 릴리스와 앱 업데이트로 전달한다.
운영 절차는 [운영/배포 가이드](운영-배포-가이드.md)를 따른다.
+104
View File
@@ -0,0 +1,104 @@
# 운영·배포 가이드 (검토용 개정안)
> 이 문서는 장기 운영 목표도 포함한다. 현재 UI/CENTER compiled dist build/export, JSON Default/partial과 공개 URL Loader/Apply, Designer JSON Preview, `npm run build:themes` 독립 artifact 산출이 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 Theme Admin, 실제 CDN/pipeline은 미구현이다. 테스트 ZIP은 포함 dist와 로컬 workspace를 사용하며 Registry 접근·publish는 필수가 아니다. 아래 `@company/ui`는 목표명이며 현재 패키지는 `@hm-design/ui`다. 이번 단계의 실제 배포/캐시/롤백은 [Theme delivery](theme-delivery.md)를 따른다. MVP 수동 운영과 향후 자동화를 구분한다.
단일 `HMDesignSystem` Git 저장소에서 컴포넌트 소스·기본 CSS, Storybook, Theme 초안을 함께 관리한다. **Git 커밋, 패키지 릴리스, Theme Publish는 서로 다른 단계**다. 소스에 CSS/JSON을 수정하거나 Storybook을 빌드하는 것만으로 사용 중인 앱에 변경이 전달되지 않는다.
| 같은 저장소의 변경 | 실제 배포 산출물 | 앱 반영 |
|---|---|---|
| 컴포넌트 구현·selector·기본 CSS·Token 계약 | 새 `@company/ui` 버전 | 개발팀이 의존성 업데이트·통합 검증·앱 배포 |
| 이미 지원하는 Semantic Token 값 | 검증·승인된 불변 Theme 버전과 API/CDN 활성 포인터 | 연동된 Loader가 다음 진입·새로고침에서 가져와 적용; 앱 재배포 없음 |
| Storybook Story·내부 문서·Manager 스타일 | 내부 Storybook 빌드 | 검수 환경 갱신; 서비스 앱 반영 경로가 아님 |
패키지와 Theme은 같은 Git 변경 이력을 추적하되 각자 버전·검증·승인·롤백을 기록한다. CI를 도입하면 변경 파일별로 관련 검증을 선택하고, Publish는 별도 승인 단계에서 실행한다. Theme 초안을 저장소에 두더라도 승인되지 않은 값을 운영 URL에 자동 공개하지 않는다.
## 1. 두 배포 경로
### 컴포넌트·계약 변경
```text
요구사항·영향 분석 → Design System 코드/Story 변경 → 검수·PR
→ @company/ui 버전 릴리스 → 서비스 앱 의존성 업데이트
→ 앱 통합 확인 → DEV → STG → PROD
```
DOM, 공개 props/variant/state, interaction, 접근성, 기본 CSS의 selector·상태·레이아웃 규칙, 새 Semantic Token·스키마는 이 경로를 따른다. shadcn upstream 업데이트도 Design System 저장소에서 검토한 뒤 이 경로로 반영한다. 패키지 버전과 앱별 적용 버전을 기록한다. 서비스는 패키지 새 버전을 자동 수용하지 않고 앱 테스트·배포를 거쳐 사용한다.
### 기존 Theme 값 변경
```text
Draft 값 수정 → 실제 @company/ui Storybook Preview → 자동 검증
→ 디자인/운영 승인 → 불변 Theme 버전 생성 → STG 확인
→ 운영 활성 포인터 전환 → API/CDN 제공 → 앱 새 진입·새로고침 시 로드
```
이 경로는 **이미 배포된 앱이 지원하는 Semantic Token의 값**에 한정한다. Theme Loader가 API/CDN을 읽도록 연동된 앱에서는 앱/패키지 재배포 없이 다음 진입·새로고침에 반영한다. Theme을 발행하기 위해 Storybook이나 앱을 다시 빌드하지 않는다. Storybook은 Preview 환경이며 Publish 권한과 분리한다.
## 2. Publish 전 검증 게이트
| 게이트 | 확인 사항 | 실패 시 |
|---|---|---|
| 계약 검증 | `schemaVersion`, themeId, Token allowlist, 필드/값 타입, 단위·범위 | Publish 중단 |
| 호환성 | 운영 중인 앱의 `@company/ui`/계약 버전이 모든 발행 키를 지원 | 앱 업데이트 선행 또는 값 범위 축소 |
| UI Preview | 실제 컴포넌트의 대표 상태와 대비·가독성, 잘림·넘침 | Draft 수정 후 재검수 |
| 환경 검증 | STG에서 앱이 문서를 받아 적용하고 장애 시 기본값 사용 | PROD 활성화 보류 |
| 승인·기록 | 변경 요청, 변경 전후 값, 검수 결과, 승인자, 예정 시각·영향 범위 | 기록 완료 전 보류 |
값 검증은 클라이언트와 Publish 서버 양쪽에서 수행한다. 클라이언트 검증은 오래된 앱과 잘못 배포된 문서로부터 UI를 보호하고, 서버 검증은 잘못된 Theme 발행을 막는다. 자동 검증을 통과해도 색 대비나 UI 안전성이 자동으로 보장되는 것은 아니다.
## 3. 버전, 활성화, 캐시
- Theme 문서는 `themeId`와 `themeVersion`으로 식별하는 **불변 산출물**로 저장한다. 발행 후 내용을 덮어쓰지 않는다.
- 환경별 활성 포인터(예: STG/PROD)가 버전을 가리킨다. Publish와 rollback은 이 포인터 변경으로 수행한다.
- API/CDN은 활성 포인터와 버전 문서를 제공한다. 버전 URL은 오래 캐시할 수 있고, 활성 포인터는 짧은 TTL 또는 재검증 가능한 ETag 정책을 사용한다. 구체 TTL은 운영 요구와 CDN 특성에 맞춰 확정한다.
- 앱은 새 페이지 진입·새로고침 시 활성 버전을 확인한다. 포인터 전환과 CDN 전파 사이의 지연을 운영 반영 시간에 포함해 기록한다.
- `@company/ui` 버전과 Theme 버전은 별개다. Publish 기록에는 검수한 패키지 버전 및 운영 앱의 최소 지원 계약 버전을 함께 남긴다.
## 4. Rollback 절차
1. 장애 범위, 현재 활성 Theme 버전, 직전 정상 버전, 영향 앱을 확인한다.
2. 직전 정상 버전의 계약 호환성을 확인하고 운영 활성 포인터를 해당 **기존 불변 버전**으로 되돌린다.
3. API/CDN에서 활성 포인터와 문서가 일치하는지 확인한다. 필요한 경우 포인터 캐시를 갱신하고 전파 시간을 관찰한다.
4. 새 페이지 진입·새로고침에서 실제 앱의 시각 결과와 적용 버전을 확인한다. fetch/검증 오류에는 기본 Theme fallback이 동작하는지 확인한다.
5. 사고 기록에 원인, 시작·복구 시각, 영향 범위, 후속 조치를 남긴다. 문제가 있는 버전을 덮어써서 복구하지 않는다.
이미 열려 있는 탭은 초기 운영 정책상 즉시 교체되지 않는다. 즉시 전환이 필요한 서비스가 확인되면 갱신 신호와 탭 동기화를 별도 설계한다. 활성 포인터나 CDN이 일시적으로 실패하면 앱은 검증되지 않은 값을 적용하지 않고 기본 Theme을 유지한다.
## 5. 단계별 운영 도입
| 단계 | 운영 방식 | 필수 증거 |
|---|---|---|
| 현재 구현 | compiled dist build/export 패키지·JSON Theme/URL Loader/Apply·Designer Preview·Theme-only artifact. Registry publish 이력은 ZIP 검증 범위 밖; 운영 CDN Publish 미구현 | JSON/Runtime/Default parity/레이아웃 및 URL 실패 fallback 검증 |
| MVP 서비스 도입 | 구현된 패키지 빌드·로컬 Theme JSON·Storybook Preview를 서비스 앱에 연결하고 수동 검증. Production Publish 없음 | 동일 패키지 컴포넌트의 override/fallback, 새로고침 확인 |
| 운영 기반 | Registry, 불변 버전, 검증 API, 승인 기록, 환경별 활성 포인터, API/CDN, 롤백 절차 | STG→PROD 발행과 이전 버전 복귀의 실제 앱 검증 |
| 운영 확장 | Theme Admin, 역할별 권한, 감사 로그, 자동 시각·접근성 검사, 관측 | 승인·배포·복구 이력과 서비스별 적용 버전 추적 |
Admin 없이 시작하더라도 Publish를 단순 파일 덮어쓰기나 디자인 툴 자동 직결로 만들지 않는다. 운영 기반 단계에서는 최소한 검증, 불변 버전, 승인, 활성화, 롤백을 갖춘다.
## 6. 단계별 책임과 산출물
| 단계 | 담당과 산출물 |
|---|---|
| Design System 구현 | **디자인팀**이 Token 의도·값, 컴포넌트와 계약 변경, 기본 Theme, Storybook·Preview 검수, `@company/ui` 패키지 버전을 담당 |
| Theme 운영·발행 | **디자인팀**이 서버 검증, 불변 버전 저장, 승인 기록, 환경별 활성화, API/CDN·롤백을 담당 |
| 서비스 적용 | **개발팀**이 패키지 업데이트, Theme ID 선택, Loader와 fallback, 앱 통합 검증을 담당 |
디자인팀은 Design System 구현과 Theme 운영을 함께 수행한다. 표는 조직 분리가 아니라 작업 단계를 구분한다. 같은 사람이 여러 단계를 수행할 수 있으며, 단계마다 변경 내용·검수 결과·승인·반영 버전을 남긴다. 운영 시작 전 각 단계의 실제 담당자와 승인자를 정하고, 디자인 승인만으로 운영 활성화가 자동 실행되는지는 디자인팀의 권한 정책에서 결정한다.
## 7. 운영 체크리스트
### 컴포넌트 릴리스
- [ ] 변경이 컴포넌트/API/기본 CSS 규칙/계약 경로인지 분류했다.
- [ ] 관련 Story와 패키지 공개 API를 갱신하고 접근성·주요 상태를 확인했다.
- [ ] 패키지 버전을 발행하고 앱별 업데이트·테스트·배포를 기록했다.
- [ ] 영향 Theme 문서의 스키마/Token 호환성을 확인했다.
### Theme Publish
- [ ] Git의 Theme 초안 변경과 운영 Publish를 구분하고, 기존 Semantic allowlist 값만 발행 대상으로 검증했다.
- [ ] 실제 컴포넌트 Storybook Preview와 STG 앱에서 검수했다.
- [ ] 승인자·변경 내용·호환 패키지 버전·이전 활성 버전을 기록했다.
- [ ] 새 불변 버전을 저장하고 운영 활성 포인터를 전환했다.
- [ ] API/CDN 응답과 새로고침한 운영 앱의 적용 버전·fallback을 확인했다.
- [ ] 필요 시 이전 버전으로 되돌릴 수 있는 포인터와 절차를 확인했다.
+310
View File
@@ -0,0 +1,310 @@
# Theme & Token Architecture
## AI Theme 기본 적용 정책 (2026-10-07 사용자 확정)
사용자가 별도로 "테마를 적용해줘", "Semantic Token을 사용해줘"라고 요청하지 않아도 모든 AI 신규·수정 UI는 Theme을 기본 적용한다. Theme은 Component 탐색·설계 단계부터 포함되는 구현 조건이다. 전체 구현 순서는 [AI/Component 구현 원칙](shadcn-기반-구현-원칙.md)을 따른다.
- 설계 계층은 **Primitive → Semantic → Component Mapping**을 유지한다.
- 값 선택 순서는 **Existing Semantic Token → 해당 Existing Component Token → Existing CSS Variable → 필요한 경우 최소 Semantic 확장**이다. 이는 탐색 순서이며 Component Mapping 계층을 제거하거나 다른 Component의 내부 Token을 직접 공유하는 규칙이 아니다.
- 기존 Token이 있는 색상·spacing·radius·typography·값을 먼저 고정값으로 작성하지 않는다. 같은 Token을 여러 파일에 중복 정의하지 않는다. 정의되지 않은 속성은 기존 shadcn 기본 구현과 구조적 값의 계약을 유지하고 임의 Token을 양산하지 않는다.
- 특정 Reference 색상을 그대로 재현하라는 요청에도 먼저 의미가 맞는 기존 Theme / Semantic Token에 연결한다. 정확한 값이 기존 Theme과 다르면 Theme 값 변경의 필요성과 영향을 확인한다. 독립 Reference 검증 표현은 Story 범위와 Production Theme 대상 여부를 먼저 확인하고 예외 사유를 기록한다.
```css
/* 금지: UI에서 Theme 색상을 고정 */
.status { color: #00C896; }
/* 기존 의미에 맞는 Token 연결 예: 실제 진행 control 색 역할일 때 */
.status { color: var(--hm-sem-state-active); }
```
위 예는 상태 역할이 실제로 일치할 때만 적용한다. Dashboard 업무 Status는 기존 mapping을 먼저 확인하며 모든 업무 상태를 Control Active에 합치지 않는다. 문서의 Token·색상 예시는 현재 코드/계약과 대조하고 존재하지 않는 Token을 실제 값처럼 사용하지 않는다.
### Theme 사용과 Theme Source 수정
| 작업 | 기본 처리 |
|---|---|
| 기존 Token / CSS Variable을 UI에서 사용 | 별도 승인 없이 기본 적용 |
| `themes/default.json`, Primitive / Semantic 값 자체 변경 | 요청 충족에 필요한 명시적 사유와 영향을 확인한 뒤 진행 |
| 새 Semantic / 공개 Runtime Token 추가 | 기존 계약으로 해결 불가능한 경우 최소 확장; 소비처·fallback·호환성 및 package 변경 범위 확인 |
Theme 적용 요청을 Theme Source 수정 허가로 해석하지 않는다. UI를 연결하기 위해 Default/Primitive 값을 임의 변경하지 않는다. Color Source of Truth는 `themes/*.json`이며 생성된 fallback CSS/데이터를 별도 원본처럼 수동 수정하지 않는다.
### Runtime과 독립 fallback
```text
themes/*.json
→ Runtime Theme Loader
→ CSS Custom Properties
→ Semantic
→ Component Mapping
→ UI
```
앱 초기 CSS에서 기존 `@hm-design/ui/theme.css` 적용을 확인하고, Theme 주입이 없으면 HM Default를 사용한다. 이미 적용된 Theme 환경과 공개 Runtime API를 재사용하며 별도 Theme 엔진을 만들지 않는다. Global / Scoped / Portal 경계와 partial / invalid / cleanup 계약은 [Runtime Theme](runtime-theme.md)를 따른다.
기존 공개 계약이 지원하는 color-only 변경은 Theme JSON만 별도 배포해 반영할 수 있어야 하며 UI package 재배포를 요구하지 않도록 Component에 값을 고정하지 않는다. 새 Token·CSS 규칙·Runtime 계약 변경은 package 변경이며 Theme JSON만으로 처리할 수 없다. 실제 배포 경로는 [Theme delivery](theme-delivery.md)를 따른다.
Primary와 On Accent는 독립 fallback을 가진 sibling Semantic이다. Primary override가 On Accent를 변경하게 만들거나 둘을 alias하지 않는다. Active / Focus / Destructive 및 Global / Scoped의 기존 독립성도 유지한다. 같은 Default 값이라는 이유로 fallback을 합치거나 새 상호 의존성을 만들지 않는다.
Theme 미적용 prototype / 독립 POC 등의 예외는 사용자의 명시적 요청과 기존 시스템에 영향 없는 별도 범위가 있을 때만 [AI 구현 예외 기준](shadcn-기반-구현-원칙.md#10-명시적-예외)에 따라 적용한다. 이 확정 정책은 아래 과거 예시의 상충 문구보다 우선한다.
## 목적
HMDesignSystem은 shadcn/ui를 기반으로 하며 하나의 HM 기본 테마를 운영한다.
HM Default는 기본 제공 Theme preset이다. 외부 서비스 Theme는 같은 공개 Runtime 계약을 사용해 값만 교체한다. Theme selector UI, Light/Dark 엔진은 만들지 않는다. 실제 계약/API와 scope 정책은 [Runtime Theme](runtime-theme.md)를 따른다.
디자인 변경 시 가능하면 React 컴포넌트 코드를 수정하지 않고 Theme / Token CSS 수정만으로 반영할 수 있는 구조를 유지한다.
---
## 기본 구조
HMDesignSystem의 스타일 값은 기본적으로 다음 구조를 따른다.
```text
Primitive
→ Semantic (공통 Theme 역할)
→ Component Mapping Token
→ Component
```
컬러 Token은 Primitive → Semantic → Component Mapping의 3계층으로 구분한다. 공통 Theme에는 typography/radius/geometry도 포함하며, 선택적 공개 palette 사용과 컴포넌트 고유 geometry는 현재 구현 계약을 따른다.
---
## Global Theme Token
여러 컴포넌트에서 공통으로 사용하는 디자인 정책을 관리한다.
주요 관리 대상:
- Color
- Typography
- Radius
- Shadow
- Control Height
- 공통 Spacing
- 공통 상태 표현
현재 HM 공통 Token 예(컬러 원본은 `themes/default.json`, 아래 CSS는 생성 결과의 일부):
```css
:root, [data-hm-theme] {
--hm-sem-action-primary: var(--hm-color-primary);
--hm-sem-bg-surface: var(--hm-color-surface-default);
--hm-sem-stroke-input-default: var(--hm-color-border-control);
--hm-radius-input: var(--hm-radius-control-md);
}
```
공통 디자인 정책이 변경되는 경우 Global Theme Token을 수정하여 관련된 여러 컴포넌트에 함께 반영할 수 있어야 한다.
---
## Component Mapping Token
각 컴포넌트는 공통 Theme 역할을 Component Mapping Token을 통해 사용한다.
예:
```css
--hm-button-radius: var(--hm-radius-button);
--hm-input-radius: var(--hm-radius-input);
--hm-select-radius: var(--hm-radius-input);
--hm-input-border-color: var(--hm-sem-stroke-input-default);
--hm-select-border-color: var(--hm-sem-stroke-input-default);
--hm-card-border-color: var(--hm-sem-stroke-input-default);
```
이 구조를 통해 공통 변경과 개별 변경을 모두 지원한다.
예를 들어:
```css
--hm-radius-input: 6px;
```
로 변경하면 이를 사용하는 Input, Select 등이 함께 변경된다. Button radius는 별도 Mapping을 따른다.
반대로 특정 컴포넌트만 변경해야 할 경우:
```css
--hm-input-radius: 8px;
```
처럼 Component Mapping Token만 변경할 수 있다.
---
## Component 적용 원칙
컴포넌트 내부에는 실제 색상값이나 공통 디자인 값을 가능한 직접 작성하지 않는다.
피해야 하는 예:
```tsx
className="bg-[#2563eb] border-[#d1d5db] rounded-[8px]"
```
대신 Theme / Component Mapping Token을 CSS selector에서 사용한다.
예:
```css
[data-slot="input"] {
border-color: var(--hm-input-border-color);
border-radius: var(--hm-input-radius);
}
```
shadcn의 기존 구조와 CVA variant 구조는 가능한 유지한다.
HMDesignSystem을 위해 shadcn 컴포넌트를 완전히 별도 구조로 재작성하지 않는다.
---
## 공통화 기준
현재 값이 같다는 이유만으로 무조건 하나의 Component Token으로 합치지 않는다.
반대로 모든 값을 컴포넌트별 Token으로 만들지도 않는다.
판단 기준은 다음과 같다.
### Global Theme Token
여러 컴포넌트에서 동일한 디자인 정책으로 함께 변경되어야 하는 값
예:
- 기본 Border Color
- 기본 Radius
- Primary Color
- Control Height
- 기본 Typography
### Component Mapping Token
특정 컴포넌트에서 향후 독립적으로 변경될 가능성이 있는 값
예:
- Button Primary Background
- Input Border
- Dialog Background
- Checkbox Checked Color
---
## State 관리
실제로 존재하는 상태에 대해서만 Token을 구성한다.
Control Active, Persistent Selected Surface, Navigation Current/Open은 서로 구분한다. Focus는 Active와 sibling Semantic이고 GNB/LNB는 contextual navigation focus 예외를 유지한다. Invalid는 Destructive border, Invalid + Focus는 Destructive border + Focus indication이다. 컴포넌트별 적용 범위는 해당 Component/Pattern 문서와 아래 HM Token 연결 절을 따른다.
예:
- default
- hover
- active / pressed
- focus
- selected / checked
- disabled
- error / invalid
size × variant × state × property 형태로 무조건 세분화하여 Token 수를 늘리지 않는다.
실제 디자인 차이가 있는 경우에만 분리한다.
---
## 단일 Theme 원칙
HM Default preset은 하나이며 Light/Dark preset이나 사용자 Theme selector는 없다. 공개 Runtime API는 동일 계약의 외부 값과 독립 Scoped override를 지원한다. 아래 제한은 전환 UI/제품별 Theme catalog를 새로 설계하지 않는다는 의미이며 구현된 Global/Scoped 적용을 금지하지 않는다.
현재 필요하지 않은 구조:
- ThemeProvider 기반 테마 전환
- Light / Dark Theme Switching
- 별도 Multi Theme 전환 UI/catalog
- 별도 Multi Brand catalog
- 사용자 Theme Selector
- Runtime Theme Selector
향후 확장은 가능하게 유지하되, 미래 가능성을 위해 현재 구조를 복잡하게 만들지 않는다.
---
## 작업 원칙
새로운 컴포넌트를 추가할 때 다음 순서로 판단한다.
1. 기존 Global Theme Token으로 표현 가능한지 확인
2. 공통 디자인 정책이라면 기존 Global Theme Token 재사용
3. 독립 변경 가능성이 있다면 Component Mapping Token 추가
4. 컴포넌트에는 실제 디자인 값을 직접 작성하지 않음
5. Storybook에서도 실제 소비 프로젝트와 동일한 Component API 사용
HMDesignSystem의 핵심 원칙은 다음과 같다.
**공통 디자인 정책은 Theme Token 하나를 수정하여 관련 컴포넌트에 함께 반영할 수 있어야 한다.**
동시에:
**특정 컴포넌트만 변경해야 할 경우 Component Mapping Token을 통해 독립적으로 override할 수 있어야 한다.**
---
## 현재 구현 기준
### Token 이름과 위치
위 예시의 Token 이름은 개념 설명용이다. 외부 ui_viz Reference (ZIP 미포함, 읽기 전용)는 초기 디자인 참고 자료다. 현재 Production 코드와 실제 Storybook을 1차 기준으로 Component/Pattern 문서의 HM 결정과 대조한다. 실제 저장소 기준은 다음과 같다.
| 계층 | 위치 | 이름 |
|---|---|---|
| Primitive | `packages/ui/src/theme/primitives.css` | `--hm-<family>-<step>` — ui_viz Primitive 이름 그대로 |
| 공통 Theme (Global/Semantic) | `packages/ui/src/theme/default.css` | `--hm-color-*`, `--hm-radius-*`, `--hm-control-height-*`, `--hm-font-*`, `--hm-sem-*` |
| shadcn/Tailwind adapter | `default.css` 하단 | `--primary`, `--border`, `--radius` 등을 HM Token에 연결 |
| Component Mapping | `packages/ui/src/theme/<component>.css` | `--hm-<component>-<속성>` |
- **Primitive**: ui_viz와 다른 이름을 만들지 않고 변환 대응표도 두지 않는다. ui_viz에 Primitive로 등록되지 않은 값(예: Button 범위 Component 값)은 Primitive로 올리지 않는다.
- **Component Mapping**: ui_viz에 등록된 Component Token(예: `button.radius`, `button.height.md`)은 그 속성 이름을 그대로 쓴다(`--hm-button-radius`, `--hm-button-height-md`). ui_viz의 CSS 변수는 `--button-radius`처럼 접두어가 없지만, HM은 모든 Token에 `--hm-`을 붙인다. Tailwind v4의 `--color-*`·`--radius-*` 같은 theme namespace와 소비 앱 변수와의 충돌을 피하기 위해서다. ui_viz에 없는 값은 같은 형식으로 정의한다.
- **파일**: 컴포넌트 전용 Mapping Token과 `[data-slot]` selector 스타일은 `packages/ui/src/theme/<component>.css`에 두고 `packages/ui/src/theme.css`가 import한다. 공통 값은 `default.css`에 둔다. Demo/App 페이지 스타일은 넣지 않는다. 컴포넌트 값은 Tailwind arbitrary value(`bg-[var(...)]`)가 아니라 이 CSS의 selector가 Mapping Token을 참조해 적용한다.
- Mapping Token의 기본값은 공통 Theme Token을 참조하고(`--hm-button-radius: var(--hm-radius-button)`), 해당 컴포넌트만 따로 바꿀 수 있다.
- **Focus Color**(`default.css`): `--hm-color-border-focus` → `--hm-sem-stroke-input-focus` → `--ring` 한 줄 체인이 일반 Focus 색(Input·Textarea·Select·Button·Checkbox·Tabs·Slider·Menubar 등)의 공통 소스다. 현재 **시험 적용값**은 Light 2(`--hm-green-light-2`, `#266651`)이며 이전 값은 `--hm-color-primary`(`#123328`)였다. 사용자 최종 확정 전이다. Semantic 값·이름, Runtime 계약과 Theme JSON 구조는 유지한다.
- 2026-10-06 Focus 정합: Slider Hover/Drag halo는 기존 Primary 20% / 4·6px를 유지하고, 키보드 Focus halo만 Focus Semantic 20%로 분리한다. Focus + Hover는 4px Focus, Focus + Drag는 6px Focus가 우선하며 Disabled는 후광이 없다. Range·Thumb border는 Active Semantic을 유지한다.
- Menubar 상위 Trigger Highlighted/Open 면은 Accent 100%를 유지하고 키보드 Focus에 Navigation Menu와 같은 3px `--ring` 50% 독립 ring을 더한다. Open + Focus는 Accent 면과 Focus ring을 함께 표시한다. Item Highlighted는 Accent 45%, Disabled는 기존 규칙을 유지한다. Navigation Menu는 변경하지 않는다. 일반 Story의 computed style로 검증하고 Designer 전용 button outline을 근거로 사용하지 않는다.
- **Active State Semantic**(`default.css`, 2026-10-06): `--hm-sem-state-active`(→ `--hm-sem-palette-green-light-2` → `--hm-green-light-2`, `#266651`)와 `--hm-sem-on-state-active`(→ `--hm-color-text-inverse`, 흰색, 대비 약 6.8:1). Checked·Selected control·ON·Active fill을 포괄하며 Navigation/Pagination Current는 포함하지 않는다. Checkbox checked, Radio selected(border·dot), Switch on, Slider range·thumb border, Progress indicator, Calendar single selected·range start/end의 `--hm-<component>-*`가 이를 참조한다. Badge default와 Pagination current는 대상이 아니며 Primary Green(`--hm-sem-action-primary`)을 쓴다. 이 한 줄을 바꾸면 위 컴포넌트가 함께 바뀌고 Focus는 영향을 받지 않는다.
- Focus chain(`--hm-color-border-focus`)과 **sibling**이다. 현재 값은 둘 다 Light 2지만 의미가 다르므로 어느 쪽도 다른 쪽을 참조하지 않는다. 같은 색일 때 fill(Active)과 바깥 ring(Focus)의 형태로 구분한다.
- Progress는 현재 Active와 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리한다.
- 대상 아님: Toggle / Toggle Group(on = `--accent` 면, 별도 정책), GNB·LNB·Navigation Menu current(기존 Strong Brand 언어), Error/Destructive, Button.
- **Persistent Selected Surface**: Toggle / Toggle Group의 Selected 및 Selected + Hover = `--accent` 100% / `--accent-foreground`(Git baseline `d101530` 매핑, 2026-10-06 원복). 아이콘은 전경을 따르고 border는 기존 default 투명 / outline input stroke를 유지한다. Active override에 영향받지 않고 Accent override를 따른다. Focus override는 별도 ring만 바꾼다.
- Color 역할 정리: Hover / Soft surface = `--accent` 45% (Select·Command·Combobox·Menu Item highlighted, Calendar hover·range middle, Pagination hover, Table row hover·selected) / Persistent Selected Surface = `--accent` 100%(Toggle·Toggle Group) / Menubar Top Trigger Highlighted·Open 및 Navigation Menu Trigger·Open = `--accent` 100% + On Accent / Active Control fill = `--hm-sem-state-active`(위 적용 대상) / Focus = Focus chain / Strong Brand·Navigation·Badge default·Pagination current = Primary Green(`--hm-sem-action-primary`, `#123328`) / Error = Destructive.
- Runtime 공개 계약(`packages/ui/src/theme/contract.ts`)에 Active/on Active를 포함했다. `src/theme/preview.ts`는 패키지 Runtime API의 호환 어댑터다.
- **Floating Surface Semantic**(`default.css`): `--hm-sem-floating-background|border-width|border-color|radius|shadow`. Dropdown Menu·Context Menu·Popover·Hover Card·Menubar content·Navigation Menu panel의 `--hm-<component>-*`가 이를 참조한다. Toast(Sonner)는 notification surface라 이를 쓰지 않고 `--hm-toast-*`를 따로 둔다(Component Token은 유지하며 컴포넌트별 override 가능). Date Picker·Combobox는 Popover를 조합하므로 별도 mapping이 없다. Tooltip(어두운 면)·Dialog/Alert Dialog/Sheet/Drawer(modal)·Select·Card·Command는 대상이 아니다.
> **[임시 — 정리되면 삭제] 현재 코드와 위 기준의 차이**
> - Primitive 이름: `--hm-warm-neutral-{light,mid,dark}`는 ui_viz `hm-neutral-warm-*`, `--hm-beige-light`는 `hm-neutral-beige-light`, `--hm-green-tint`(#BDDFD4)는 `hm-fg-on-green-secondary`이다. ui_viz Primitive에 없는 이름: `--hm-neutral-gray-500`(#999999, ui_viz는 `color.placeholder`), `--hm-warm-neutral-pale`(#DEDBD2, ui_viz는 Primitive 없이 `color.stroke.input.default`), `--hm-green-tone-1~4`(Button 범위 gradient 값), `--hm-warm-neutral-secondary`(#EDE8DB), `--hm-warm-neutral-muted`(#F4F1EA, 두 값은 ui_viz 문서에서 찾지 못함). Button 작업으로 ui_viz 이름 그대로 추가한 Primitive: `--hm-brown-light-2/-1`, `--hm-brown-dark-1`, `--hm-orange-light-4`, `--hm-orange-default`.
> - Button Mapping Token은 ui_viz 이름(`--hm-button-radius`, `--hm-button-height-sm/md/lg`, `--hm-button-brand-{green,brown,orange}-{background,border}`, `--hm-button-solid-*` 등)을 따른다. Gray(`#767676`/`#5A5A5A`), Brown·Orange gradient의 일부 stop은 ui_viz가 Button 범위 Component 값으로 정의한 값이라 초기에는 Primitive 없이 `button.css`에 두었으나 현재는 `default.css`의 공개 Theme palette로 이동했다(아래 Runtime 절).
> - `--hm-radius-button`과 `--hm-radius-input`(Input의 `--hm-input-radius`, Select의 `--hm-select-radius`가 참조)은 공통 `--hm-radius-control-md`(4px)를 참조하며 컨테이너도 같은 값을 쓴다. `--hm-radius-select`는 Select 작업 후 참조하는 곳이 없다. ui_viz는 `radius.input.default` 4px와 `radius.container.default` 4px를 값이 같아도 alias하지 않는다. ui_viz는 Button radius를 Button 범위 Component 값(`button.radius` 4px)으로 두고 다른 컴포넌트로 확대 해석하지 않는다고 적는다.
### HM 값과 shadcn 기본값
- ui_viz에 `Confirmed` 규격이 있는 속성 → Theme Token 적용
- 정의가 없거나 `TBD`·`HOLD` 등 미확정인 속성 → 새 Token을 만들지 않고 shadcn 기본 구현 유지
### Light / Dark
현재 HM Theme Token에는 Light/Dark 구분이 없다. 하나의 HM Theme 값만 정의하며, Dark 전용 값이나 Light/Dark 분기를 추가하지 않는다. 실제 디자인 요구로 Light/Dark가 정의되면 그 시점에 Token 구조를 검토한다.
## 2026-10-06 Runtime 구조 적용
이 절과 [Runtime Theme](runtime-theme.md)가 위 초기 시제품 상태 설명보다 우선한다. 모든 Component Mapping은 기존 이름을 유지하고 Semantic / 선택적 Theme palette를 소비한다. Button의 이전 로컬 HEX는 Default preset의 공개 팔레트로 옮겼다. 외부 주입은 공개 Semantic/palette만 허용하고 Component별 Token은 입력 계약에 포함하지 않는다. Default 값과 외부 값은 같은 계약을 사용한다. Accent 45%는 내부 surface-highlight Semantic으로 공통화했다. 전체 계약과 Default 값은 [계약표](runtime-theme-contract.md), 실제 적용·제한은 [Runtime Theme](runtime-theme.md)를 참고한다.
# 후속: 디자인팀 컬러 Runtime 편집 — 2026-10-06
현재 Storybook의 `Theme/Designer Playground/Editor`에서 JSON URL/파일을 Preview하고 상위 컬러 17개 역할을 편집·Reset·Export/Import할 수 있다. Component Token은 직접 수정하지 않는다. 운영 컬러 원본은 `themes/default.json`/서비스 JSON이며 package fallback CSS/데이터는 생성한다. Production은 `@hm-design/ui/theme`의 URL Loader/Apply를 사용한다. Theme artifact는 UI package와 별도 배포한다. 상세 절차/제한은 [디자인팀 Theme 가이드](designer-theme-guide.md), [Theme delivery](theme-delivery.md), Runtime 적용·제한은 [Runtime Theme](runtime-theme.md)를 참고한다. 기존 디자인 정책/과거 기록은 유지한다.
+318
View File
@@ -0,0 +1,318 @@
# Runtime Theme 기반 Design System 프로젝트 지침 (개정안)
> 상태: 현재 하나의 Git Repository에서 UI/CENTER npm workspaces의 compiled dist build/export, JSON 컬러 원본, Global/Scoped Runtime Apply·URL Loader, 통합 Storybook/Designer JSON Preview, Theme-only artifact를 관리한다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 실제 CDN/API/Publish pipeline은 미구현이다. 현재 구현의 운영·배포 절차는 [Theme delivery](theme-delivery.md)를 따른다.
> **2026-10-07 사용자 확정 정책은 개정안 상태와 관계없이 필수 적용한다.** AI의 모든 신규·수정 UI는 사용자의 별도 지시 없이 기존 Component / Storybook Reference를 먼저 확인하고 Theme / Semantic Token과 단일 Navigation Mode를 기본 적용하며 Custom은 최소화한다. 필수 요약은 [AGENTS.md](../../AGENTS.md), 상세 구현 정책은 [shadcn 기반 구현 원칙](shadcn-기반-구현-원칙.md), Theme 정책은 [테마 토큰 가이드](테마-토큰-가이드.md), Navigation 정책은 [Navigation Single Mode Policy](navigation-single-mode.md)가 각각 담당한다.
## 1. 목적과 결정
사내 공통 UI는 shadcn을 **구현 기반이자 upstream 참고원**으로 사용한다. Design System 저장소에서 가져온 코드를 회사 규칙에 맞게 관리하고 `@company/ui`로 배포한다. 서비스는 shadcn 코드를 직접 가져오거나 수정하지 않고 공통 패키지를 사용한다.
컴포넌트의 구조·동작·기본 스타일 규칙은 패키지 버전으로, 운영 중 바꿀 수 있는 디자인 값은 CSS Custom Properties 기반 Runtime Theme으로 관리한다. **소스와 검수 자료는 하나의 Git 저장소에서 관리하되, 배포 산출물은 `@company/ui` 패키지와 Theme 버전으로 나눈다.** Storybook은 같은 저장소의 실제 컴포넌트로 내부 검수, Theme Preview, 문서화를 수행한다. 서비스가 소비하는 배포물은 패키지와 Theme 데이터이며, Storybook 자체가 배포 API는 아니다.
| 변경 경계 | 소유 대상 | 반영 경로 |
|---|---|---|
| 컴포넌트 코드/API·기본 CSS 규칙 | DOM, variant, state, interaction, 접근성, 구조적 layout, selector와 기본 스타일 규칙 | Design System 변경 → `@company/ui` 릴리스 → 앱 의존성 업데이트 및 배포 |
| Runtime Theme 값 | 사전에 허용한 Semantic Token의 값 | Preview → 검증/승인 → 불변 Theme 버전 Publish → API/CDN → 앱 재방문·새로고침 시 적용 |
| Theme 계약 | Token 이름·타입·의미, 스키마, 허용 범위 | Design System 코드 변경·릴리스, 호환성 확인 후 Theme 발행 |
**이미 지원하는 Semantic Token 값만 변경할 때** Theme Publish 후 앱 재배포 없이 반영할 수 있다. Git에 CSS/JSON을 수정·커밋하는 행위만으로 운영 앱이 자동 갱신되지는 않는다. Theme Loader와 API/CDN 연결, 검증·승인·Publish가 먼저 구축되어야 한다. 새로운 Token, selector, 컴포넌트 API가 필요한 변경은 값 Publish로 처리할 수 없다.
## 2. 아키텍처와 소유권
```text
shadcn upstream → HM Design System 단일 Git 저장소
├─ 컴포넌트·기본 CSS ── 패키지 릴리스(@company/ui) ──→ 개발팀 앱 업데이트·배포
├─ Semantic Theme 초안 ── 검증·승인·Publish ──────→ Theme API/CDN
└─ Storybook ── 실제 컴포넌트·Theme Preview │
↓
앱 Theme Loader → CSS 변수 적용
```
- **디자인팀 — Design System**: Token 의미와 값, 컴포넌트 디자인, shadcn 도입·업스트림 변경 검토, 컴포넌트 구현/API와 접근성, Semantic Token 계약, 기본 Theme, `@company/ui` 패키지 릴리스, Storybook 및 Preview 검수를 담당한다.
- **디자인팀 — Theme 운영**: Theme 값 검증, 불변 버전 저장, 승인·Publish·롤백, API/CDN 제공, 캐시 및 관측을 담당한다. MVP에서는 수동 절차로 시작할 수 있다.
- **개발팀**: `@company/ui`와 기본 CSS 사용, 서비스 앱의 Theme 식별·로딩·적용·실패 대응, 패키지 업데이트 및 앱 릴리스를 담당한다.
위 항목은 조직 분리가 아니라 작업 단계와 책임의 구분이다. 같은 담당자가 여러 단계를 수행해도 각 산출물과 승인·반영 기록은 남긴다.
## 3. Token 계약
```text
Primitive (설계·빌드 시 관리) → Semantic (컴포넌트 참조) → Component
```
컴포넌트는 `blue-600` 같은 Primitive나 `#2563eb`, `8px` 같은 실제 값을 직접 참조하지 않는다. Tailwind를 쓰는 경우에도 `bg-primary`의 `--color-primary` → `--primary` → `--hm-sem-action-primary` adapter 연결을 확인한다. **Runtime override는 공개 allowlist의 Semantic/Theme palette 값만** 허용한다. Primitive, 컴포넌트 전용 내부 변수, 임의 CSS selector/rule/JavaScript는 Publish 대상이 아니다.
현재 allowlist는 `packages/ui/src/theme/contract.ts`의 73개 입력이다(색상 Semantic/Theme palette와 input/container radius). 정확한 이름·타입·기본값·허용 범위는 이 계약과 `runtime-theme-contract.md`를 따른다. Shadow geometry는 현재 공개 입력이 아니다. Typography와 spacing은 실제 사용처와 레이아웃 영향 검토 후 개별 등록한다. 문서에 예시가 있다는 이유로 곧바로 Runtime 허용 Token이 되지 않는다.
기본 Theme은 `@company/ui`가 배포하는 CSS에 포함하고 앱이 초기 렌더 전에 로드한다. Runtime 값이 없거나 가져오기·검증에 실패해도 기본값으로 UI가 렌더링되어야 한다. 현재 Light/Dark 구분은 없으며 단일 HM Theme 값을 기준으로 한다([테마 토큰 가이드](테마-토큰-가이드.md) 참조).
## 4. 변경 분류
| 요청 예 | 처리 | 판단 기준 |
|---|---|---|
| 기존 `color.action.primary` 값 변경 | Theme Publish | 계약에 등록된 Semantic Token의 값만 변경 |
| 기존 공개 radius 값 변경 | Theme Publish(운영 도입 후) | 허용 범위·레이아웃·접근성 검증 통과; shadow geometry는 현재 계약 밖 |
| 새 색상 Token 이름 또는 의미 추가 | 패키지/계약 릴리스 후 Theme Publish | 구버전 앱의 호환성 확인 필요 |
| Button DOM, icon slot, variant, loading state 변경 | 패키지 릴리스 + 앱 업데이트 | API·구조·동작 변경 |
| Dialog 상호작용, 포커스, 접근성 변경 | 패키지 릴리스 + 앱 업데이트 | 실행 코드 변경 |
| grid/flex 배치, display/position, animation logic 변경 | 패키지 또는 앱 코드 배포 | 디자인 값만으로 안전하게 표현할 수 없음 |
| 앱 화면의 콘텐츠·업무 로직 변경 | 앱 배포 | Design System 바깥의 책임 |
애매한 요청은 디자인팀과 개발팀이 영향 범위를 먼저 분류한다. Semantic Token이라는 이름만 붙여 레이아웃 구조나 동작을 Runtime으로 우회하지 않는다.
## 5. 단일 Git Repository와 산출물 구조
`HMDesignSystem`을 **하나의 Git 저장소**로 관리한다. 저장소 수와 배포물 수는 다르다. 동일 저장소의 변경이라도 컴포넌트·기본 CSS는 패키지 릴리스, 기존 Semantic Token 값은 Theme Publish, Storybook은 내부 검수용 빌드로 각각 처리한다. 개발팀의 서비스 앱은 이 저장소의 소스 파일을 직접 복사하지 않고 배포된 패키지와 Theme API/CDN을 소비한다.
현재 파일 배치는 다음과 같다. Library dist 산출물과 Runtime URL Loader는 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 운영 CDN/API/Publish pipeline은 별도 도입 대상이다.
```text
HMDesignSystem/
├─ packages/ui/src/components/ui/ # Production UI
├─ packages/ui/src/patterns/ # 업무/Layout composition
├─ packages/ui/src/theme/ # CSS Mapping, contract.ts, runtime.ts
├─ packages/center-graphic/src/ # 독립 CENTER 구현
├─ src/ # Demo, Component/Pattern/Theme/CENTER Story
├─ packages/*/dist/ # compiled ESM JS, 타입 선언, CSS
├─ themes/*.json # 공식 Default / 서비스 partial
├─ themes/drafts/ # 검토용 Theme 자료
├─ .storybook/ # 통합 검수 환경
├─ docs/ # Component/Pattern/Theme 문서
└─ package.json # npm workspaces 및 Demo/Storybook build
```
패키지 산출물에는 공개 컴포넌트·기본 CSS·필요한 계약만 포함하고 Story, Draft Theme, 내부 문서는 제외한다. Theme Publish는 승인된 초안을 검증해 불변 버전으로 저장하고 활성 버전을 API/CDN에서 제공하는 별도 절차다. 패키지 릴리스와 Theme Publish는 같은 저장소의 변경 이력을 참조하지만 버전·승인·롤백을 각각 관리한다. 폴더를 `packages/ui`·`apps/storybook`으로 나눌 필요가 확인되면 **동일 Git 저장소 안에서** 재배치할 수 있다.
### 5.1 패키지 구성과 Storybook 통합 (구현됨 — 2026-10-02 패키지 분리)
**하나의 Git 저장소 + 두 개의 독립 배포 패키지 + 하나의 통합 Storybook**을 기본으로 한다. 2026-10-02에 npm workspaces(`packages/*`)로 분리했다. `packages/ui`(`@hm-design/ui`)에 Button·Input·Select·Card·Dialog와 Theme(`src/theme/*.css`, `theme.css`, `tailwind.css`)이 있고, `packages/center-graphic`(`@hm-design/center-graphic`)은 ui-viz CENTER Web Reference(제목·정보·노선/공간·시설물)를 이식해 구현했다(기준 문서 `docs/center-graphic/README.md`). 둘은 서로 의존하지 않는다. 루트는 Storybook(`.storybook/`, `src/**/*.stories.tsx`; UI는 `Components`, CENTER는 `Center Graphic` 최상위 그룹)과 Demo(`src/App.tsx`, `src/hm-theme.css`), 컴포넌트 문서(`docs/components/`)를 유지하며 Story와 Demo는 `@hm-design/ui`를 workspace 패키지로 import한다. 두 패키지는 `private: false`이며 ESM JS·타입 선언·CSS를 compiled `dist`에서 export한다. `npm run build:packages`가 library 산출물을 생성하며 Registry publish 이력은 테스트 ZIP 검증 범위 밖이다. 소비 앱은 `@import "tailwindcss"; @import "@hm-design/ui/tailwind.css"; @import "@hm-design/ui/theme.css"`를 쓴다. UI dist에는 `fonts/`의 글꼴과 OFL이 포함되며 compiled `theme.css`는 상대 `./fonts/` 경로를 사용한다. Source `packages/ui/src/theme.css`는 `/fonts/NotoSansKR-variable.ttf`를 사용하므로 App/Storybook 개발 서버는 `public/fonts/`를 서빙한다.
아래는 목표 구조다.
| 패키지 | 내용 |
|---|---|
| `@hm-design/ui` | shadcn 기반 공통 UI 컴포넌트와 공통 Theme(디자인 Token, CSS Custom Properties, 런타임 Theme 주입) |
| `@hm-design/center-graphic` | 서비스 공통 그래픽 리소스와 그래픽 컴포넌트(Icon, Illustration, Empty State·Loading·Status Graphic 등) |
```text
HMDesignSystem/
├─ packages/
│ ├─ ui/ components/ styles/ tokens/ theme/
│ └─ center-graphic/ icons/ illustrations/ graphics/
├─ storybook/
└─ package.json workspace 설정
```
- 두 패키지는 따로 설치하고 따로 버전을 올릴 수 있어야 한다. 그래픽만 필요한 프로젝트가 UI 패키지 전체를 설치하지 않도록 한다.
- 의존성 방향: UI → Design Token / Theme, center-graphic → 자체 그래픽 리소스. `center-graphic → ui` 필수 의존은 만들지 않고, 공유가 필요하면 최소 수준의 Token만 공유한다.
- `tokens` 또는 `theme` 패키지의 별도 분리는 규모가 커지거나 여러 제품이 테마를 독립적으로 공유해야 할 때 검토한다. 그 전에는 두 패키지로 운영한다.
- Storybook은 하나로 통합해 UI와 Center Graphic을 함께 검수한다. 메뉴 구조는 패키지 폴더 구조와 같을 필요가 없고 검토 편의를 기준으로 한다. 컴포넌트는 §6.3의 규칙대로 대표 Story가 바로 열리는 1뎁스 항목으로 두고, Playground·Compatibility·verification Story는 기본 메뉴에서 숨긴다. Center Graphic도 같은 원칙을 따른다.
- 다른 문서의 `@company/ui`는 `@hm-design/ui`의 자리표시자로 읽고, 이름 치환은 패키징 작업 때 한꺼번에 한다. Storybook 최상위 그룹은 CENTER 구현 시점에 `Center Graphic`만 추가했고 기존 `Components`는 이름을 바꾸지 않았다(`UI Components`로의 개명은 필요할 때 정한다).
#### CENTER 규격의 적용 범위
`center-graphic`은 독립 배포 패키지의 경계다. ui_viz의 CENTER 도메인 문서가 정의하는 노선, 시설물, 지도 및 관련 시각화 규격은 `center-graphic`의 구현·검증 작업에서 참조할 수 있다. 반대로 `ui`의 Button, Input, Select 같은 일반 UI 컴포넌트는 CENTER 문서를 디자인 근거로 쓰지 않는다. 따라서 CENTER 문서 참조 제한은 저장소 전체의 금지가 아니라 패키지와 작업 도메인에 따른 적용 범위 규칙이다.
- `ui` 작업 → 현재 HM UI 문서·구현·대표 Story를 먼저 참조한다. 외부 ui_viz UI 자료는 제공된 경우에만 참고한다.
- `center-graphic` 작업 → CENTER 관련 규격을 참조할 수 있다.
- 두 패키지의 규격을 임의로 섞지 않고, 한쪽의 값을 다른 패키지의 근거로 쓰지 않는다.
- **CENTER 작업 규칙**: 현재 Source of Truth는 `docs/center-graphic/README.md`, `packages/center-graphic/src/`의 구현·승인 asset과 `src/center/`의 대표 Story다. 외부 ui_viz는 ZIP 미포함 선택적 Reference이며 제공된 경우에만 owner 문서의 명시 상태(`CONFIRMED_CANONICAL`/`CONFIRMED_BY_DESIGN` → `PLATFORM_MAPPING` → `Candidate`/`TBD`/`HOLD`) → 승인 asset → Web Reference 구현 순서로 참고한다. 승인되지 않은 후보 SVG와 미확정값은 구현 근거로 쓰지 않는다. 개별 그래픽 수치는 `docs/center-graphic/`에 기록한다.
## 6. 컴포넌트·Storybook·CSS 작업 규칙
Theme Token의 계층, 매핑 방식, Runtime Theme 적용 방식은 [테마 토큰 가이드](테마-토큰-가이드.md)를 따른다. 이 절에서는 중복해서 정의하지 않는다.
React 외 Runtime으로 구현·변환하는 작업은 [Runtime Portability Policy](runtime-portability.md)의 Target Runtime, 동일 Design/Theme Contract, 상대 asset 경로와 실제 실행 검증 기준을 추가로 따른다. 기존 컴포넌트·Token·Runtime Theme을 복제해 별도 Source of Truth를 만들지 않는다.
Component / Pattern / Story를 구현하거나 기존 테스트 디자인을 재현할 때는 [shadcn 기반 구현 원칙](shadcn-기반-구현-원칙.md)을 먼저 적용한다. 핵심 우선순위는 **기존 `@hm-design/ui` 재사용 → 기존 Variant / Prop → 기존 Composition → shadcn → Radix Primitive → 최소 Custom 구현**이다. Behavior / Structure는 shadcn/Radix를 유지하고 Visual은 구현 시작부터 HM Theme / Token에 연결한다. Custom 구현은 해당 문서의 허용 조건을 모두 충족하고 사유·범위·Public API 영향을 기록한 경우에만 허용한다.
> 패키지 초기화용 base 규칙은 `packages/ui/src/tailwind.css`, Demo 레이아웃 규칙은 `src/hm-theme.css`가 소유한다. Component CSS에는 전역 규칙을 넣지 않는다. 공개 API와 상태·규격은 각 `docs/components/` 문서와 현재 대표 Story를 따른다. Brand label의 Text Border는 두께가 `EXPERIMENTAL`이라 적용하지 않는다.
### 6.1 스타일 값의 출처
1. 현재 HM Component/Pattern 문서와 대표 Story에서 해당 속성의 확정 규격 및 기존 Semantic/Component Token을 확인한다.
2. 기존 Token으로 연결한다. 외부 ui_viz는 제공된 경우에만 HM 정책과 충돌하지 않는 참고 자료로 사용한다.
3. HM 지정값이 없으면 shadcn 기본 구현을 유지한다. 미확정 Reference 값을 확정값으로 쓰지 않는다.
HM 지정값이 있는 색상, radius, control height, typography 등은 Theme Token을 사용한다. HM에서 별도 값을 지정하지 않은 hover 동작, focus 구조, 애니메이션, 아이콘 크기, 접근성 속성 등은 shadcn 기본 구현을 유지하며, 임의로 새로 정의하거나 취향에 따라 바꾸지 않는다.
### 6.2 컴포넌트 구현
- 구현 전에 현재 `@hm-design/ui` + shadcn 조합으로 해결 가능한지 확인하고, 가능하면 새 primitive나 유사 Component를 만들지 않는다.
- shadcn의 기본 컴포넌트 구조를 최대한 유지한다. CVA `variant`/`size`, `data-slot`, `cn(..., className)`, Radix 구조를 특별한 이유 없이 바꾸지 않는다.
- HM 디자인은 기존 구조 안에서 Token과 스타일 값을 연결해 구현한다. HM 스타일만을 위한 wrapper 컴포넌트를 추가하지 않는다.
- 기존 export, props 이름, variant 구조 등 공개 API를 불필요하게 바꾸지 않는다. API를 깨는 변경은 별도 합의 후 진행한다.
- `<Button />`, `<Input />`, `<Select />`처럼 기본 API만 사용해도 HM 기본 디자인이 적용되는 구조를 지향한다. 소비자가 `hm-button`, `hm-input` 같은 HM 전용 class를 붙여야 하는 구조는 만들지 않는다.
- `className`은 소비 프로젝트의 스타일 확장용 API로 유지한다. 기본 디자인을 주입하는 필수 통로로 쓰지 않는다.
- shadcn/Radix가 제공하는 controlled/uncontrolled 상태, `data-state`, ARIA, keyboard navigation, focus management, Portal, Dismiss/Escape, `asChild`와 composition 방식을 새 로직으로 대체하지 않는다.
### 6.3 Storybook
- Story는 실제 소비 프로젝트와 같은 Component import와 props API를 사용한다. Story 전용 class, wrapper, CSS로 컴포넌트 외형을 보정하지 않으며, Storybook에서만 정상으로 보이는 구조를 만들지 않는다.
- hover 고정 specimen, Selected + Hover 비교, spacing guide, Token reference 같은 검수 표현을 위해 Production prop이나 Public API를 추가하지 않는다. 필요한 표현은 Story 범위에만 두고 실제 화면에는 production `@hm-design/ui` Component를 사용한다.
- Story 배치를 위한 gap, flex, grid 스타일은 사용할 수 있지만 컴포넌트 자체의 외형을 바꿔서는 안 된다.
- 컴포넌트가 실제 지원하는 default, variant, size, disabled 및 주요 상태를 확인한다. invalid, open 등 해당 컴포넌트에 실제로 있는 상태만 다루고, 없는 상태를 문서화를 위해 만들지 않는다.
- Storybook은 구현 Reference Catalog이자 컴포넌트 단위의 검수와 사용법 문서화 도구다. 신규 UI 구현 전 유사 대표/PRIMARY Story를 먼저 확인한다. `Demo/App` 화면은 여러 컴포넌트를 조합한 적용 결과 확인용이며, Demo의 페이지·타일 UI를 Story의 기준으로 쓰지 않는다.
- 컴포넌트 Story는 현재 HM 대표 Story의 공통 구조를 따른다. 컴포넌트 이름의 대표 Story 하나에 종류·크기·상태를 한 화면으로 모으고, 같은 구역을 `verification` 태그 Story(예: Roles, Sizes, Brand States)로도 둔다. `verification` Story는 메뉴와 검색에서 기본으로 숨기고(`.storybook/main.ts`의 `tags`) 직접 URL과 사이드바 태그 필터로 연다. HM 전용 Story(Playground, Compatibility)도 같은 태그로 숨긴다. 기본 사이드바에는 컴포넌트가 하위 메뉴 없는 1뎁스 항목 하나로만 보이고(대표 Story 하나만 노출되면 Storybook이 컴포넌트 항목에 합친다), 클릭하면 한 페이지에서 전체 검수 내용이 열린다. Story 이름과 순서, 행·열 구성은 현재 HM 유사 대표 Story의 공통 구성을 먼저 따른다. 외부 ui_viz Story는 제공된 경우에만 충돌하지 않는 참고로 사용한다.
- 대표 Story의 Focus 구역은 pseudo-state 복제 대신 `play` 함수로 실제 요소에 focus를 준다(ui_viz도 Focus는 정적 specimen을 만들지 않는다). hover 같은 상태가 ui_viz에 없으면 `.storybook/pseudo-states.ts`를 쓰지 않는다. 구역 배치 helper는 `src/components/story-board.tsx`의 `StateBoard`를 쓴다.
- hover, pressed 같은 pseudo state는 Storybook이 `:hover`/`:active` 규칙을 `data-sb-hover`/`data-sb-active` 규칙으로 복제해 고정한다(`.storybook/pseudo-states.ts`). 복제만 하고 값을 새로 정의하지 않으므로 실제 디자인을 보정하는 용도로 쓰지 않는다. Story에서는 `data-sb-*` 속성만 붙인다.
- ui_viz에서 값이 `TBD`인 상태(예: Button Focus)는 별도 specimen을 만들지 않고 키보드로 확인하는 안내만 둔다. ui_viz에 없는 상태(예: Button `aria-invalid`)와 `REFERENCE_ONLY` 항목은 검수 Story에 넣지 않는다. shadcn 호환 API는 `Compatibility` Story로 분리한다.
### 6.4 CSS 범위
- 컴포넌트 스타일(배포 대상)과 Demo/App 전용 스타일을 파일 단위로 분리한다.
- 배포 대상 CSS에는 소비 프로젝트 전체에 영향을 줄 수 있는 전역 스타일을 가능한 한 넣지 않는다. `html`, `body`, `*`, 일반 태그 selector를 이용한 margin, font-family, reset, scroll behavior 등은 Demo/App 영역에서 관리한다.
- 컴포넌트 스타일은 해당 컴포넌트 범위(`data-slot` 등) 안에서 동작하게 한다.
- 컴포넌트 CSS에 container, hero, page grid, 화면 바깥 margin 같은 페이지 레이아웃 스타일을 넣지 않는다. 배치는 소비 프로젝트 또는 Demo/App이 맡는다.
### 6.5 Light / Dark 기준
현재 HM Theme에는 Light/Dark별 디자인 값이 정의되어 있지 않다. HM Token이나 HM 전용 스타일을 추가할 때 새로운 `dark:` 분기나 Dark 전용 Token을 만들지 않는다.
기존 shadcn 코드의 컴포넌트 `dark:` 클래스와 `tailwind.css`의 dark variant는 HM 요구사항이 없는 한 정리 목적으로 제거하지 않는다. 중복 root/dark 색상 preset은 Runtime 정합성 작업에서 이미 제거했으며 HM Dark preset을 새로 추가하지 않는다.
### 6.6 작업 방식
- 요청하지 않은 전체 리팩토링을 하지 않는다. 변경 범위는 요청된 컴포넌트와 직접 필요한 범위로 제한한다.
- 새 패턴은 전체에 적용하기 전에 기준 컴포넌트에서 먼저 검증하고, 검증된 방식만 다른 컴포넌트로 확장한다.
- 기존 구조가 정상 동작하면 취향이나 코드 스타일 선호만으로 바꾸지 않는다.
- 새 abstraction, wrapper, helper, 스타일 구조를 만들기 전에 기존 프로젝트 패턴을 재사용할 수 있는지 먼저 확인한다.
- Multi Theme, Multi Brand, Dark Mode 등 요구되지 않은 미래 요구사항을 가정해 현재 구조를 과도하게 설계하지 않는다.
### 6.7 ui_viz 규격의 구현 (Button에서 검증된 방식)
- 이 절의 ui_viz 설명은 초기 Button/Input/Select 도입 근거다. 현재 정합성 감사는 Production 코드와 실제 Storybook을 1차 기준으로 문서와 대조한다. ui_viz `Confirmed`와 과거 보고서는 참고 근거이며 이후 HM 결정은 §6.9 및 Component/Pattern 문서를 따른다.
- 구현 근거는 `Confirmed`뿐이다. `Candidate`, `Temporary`, `TBD`, `HOLD`, `REVIEW VALUE`, `EXPERIMENTAL`, `Known Deviation`은 확정값으로 쓰지 않는다. 컴포넌트마다 `docs/components/<name>.md`의 최신 Decision 절을 먼저 확인하고(Button은 §1.1 D-093), 본문에서 "대체"로 표시된 이전 값과 `governance/current-rules.md` 같은 요약은 근거로 쓰지 않는다. `_local/`, `generated/`, `assets/`(대부분 CENTER)는 규격 근거로 읽지 않는다. `ui` 작업에서는 CENTER 문서의 값을 쓰지 않는다(`center-graphic` 작업의 범위는 §5.1 참고).
- ui_viz의 Web 구현 요소(`.button-*` 같은 class, DOM 구조, `is-hover` 같은 강제 상태 class, Source Class Mapping)는 복사하지 않는다. 규격의 의미(역할, treatment, 값, 상태)만 가져와 `ui_viz 규격 → shadcn Component API → HMDesignSystem 구현` 순서로 대응시킨다.
- 이름이 같다는 이유로 대응시키지 않는다. 소비자가 고르는 디자인 축인지, 내부 state인지, Token으로 풀 값인지, selector 수준의 상태인지로 판단한다. 예: ui_viz Button의 `secondary`는 Brown 브랜드 역할이며 shadcn `secondary`와 의미가 다르다. ui_viz가 역할과 treatment 두 축을 두는 컴포넌트는 두 축을 각각 prop으로 나눈다. Button은 `variant`가 역할(`primary`/`secondary`/`modify`/`destructive`/`cancel`), `appearance`가 treatment(`brand`/`solid`/`outline`), `size`가 `sm`/`default`/`lg`(= ui_viz Small/Medium/Large; ui_viz가 `default`를 Medium 별칭으로 정의)이며 기본값 `<Button />`은 primary + brand다. `data-variant`는 역할, `data-appearance`는 treatment이다.
- ui_viz에 정의된 역할·treatment가 shadcn에 없으면 variant를 추가할 수 있다. 이는 기존 API를 깨는 것이 아니다. 기존 shadcn 이름은 별칭으로 유지한다(Button: `default` = primary, `outline` = primary + outline; ui_viz도 `outline`을 별칭으로 유지한다). ui_viz에 없는 shadcn variant(`ghost`, `link`)와 size(`xs`, `icon*`)는 삭제하지 않고 shadcn 기본 스타일 그대로 둔다. 이 호환 variant에는 `data-appearance`를 붙이지 않아 HM 스타일이 적용되지 않는다.
- 스타일 적용 구조는 다음과 같다.
- **CVA(`ui/<component>.tsx`)**: shadcn의 구조·레이아웃·접근성 class(focus, invalid ring 포함)를 유지한다. HM 규격이 정의한 동작(예: Button의 transition 없음, 눌림 이동 없음, disabled 커서)의 shadcn class는 HM variant에서 빼고 shadcn 호환 variant에만 남긴다. size처럼 호환 variant와 공유하는 축은 CVA 값을 그대로 두고 theme CSS가 HM 요소에만 덮어쓴다.
- **`packages/ui/src/theme/<component>.css`**: 해당 컴포넌트의 Mapping Token(`--hm-<component>-*`)과 HM 규격 스타일(색, radius, height, padding, typography, 상태 표면)을 둔다. `[data-slot="<component>"]`와 그 컴포넌트에 실제 있는 `data-variant`/`data-size` 등 data attribute, pseudo state로 선택한다. 모든 컴포넌트가 `data-variant`를 가져야 하는 것은 아니다. Demo/App 페이지 스타일은 넣지 않는다. 이 파일은 `packages/ui/src/theme.css`가 import하고, Demo 스타일은 `src/hm-theme.css`에 둔다.
- theme CSS가 `box-shadow`를 선언할 때(bevel, 상태 표면) 끝에 `var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000)`를 포함한다. shadcn의 focus·invalid ring이 `box-shadow`라서, 포함하지 않으면 theme CSS가 ring을 가린다. 이 변수는 `:root` Token이 아니라 요소의 선언에서 직접 참조해야 한다.
- 소비자는 `variant`/`size` 같은 API만 고르고 HM 전용 class를 붙이지 않는다. `className`은 확장용이다.
- ui_viz가 `Confirmed`로 정의한 상태 표현은 그 범위에서 shadcn 기본 시각 스타일을 대체할 수 있다. ui_viz에 정의가 없거나 미확정인 동작과 상태(예: Button의 Focus는 전역 `TBD`, `aria-invalid`는 정의 없음)는 shadcn 기본 구현을 유지하며 HM이 임의로 만들지 않는다.
- ui_viz가 컴포넌트 안에 Field(Label, Required 별표, Helper/Error 문구)를 포함해도 shadcn 컴포넌트에 wrapper를 추가하지 않는다. Input은 입력 컨트롤만 담당하고 Label·문구는 소비 조합이다. Story와 Demo는 일반 요소에 shadcn Token 유틸(`text-muted-foreground`, `text-destructive`)과 ui_viz 수치(Label 14/20/500, 문구 12/18 또는 Error 11/18, field gap 8)를 맞춰 조합한다. Field/Label 컴포넌트 도입은 별도 결정이다.
- ui_viz prop 이름이 native HTML 속성과 겹치면(Input `size`) ui_viz 이름을 쓰고 native 속성을 대체한다. 단 native 속성이 CSS 폭과 무관해 쓰임이 없을 때만 그렇게 한다. Input은 `size="small" | "medium" | "large"`이고 생략하면 ui_viz 기본값 Large(42px)다. 기본값은 컴포넌트마다 ui_viz 문서를 따르므로 Button의 `default`(= Medium)와 같다고 가정하지 않는다.
- 상태는 컴포넌트에 이미 있는 native selector(Input: `:focus-visible`, `[aria-invalid="true"]`, `:disabled`, `[readonly]`)로 적용하고 새 `data-state`를 만들지 않는다. ui_viz가 "shadow·glow 없음"을 Confirmed로 정의한 상태(Input Focus·Error)는 TSX에서 shadcn ring class를 빼고 CSS에서 `box-shadow: none`을 선언한다. ui_viz에 정의가 없어 shadcn ring을 유지하는 Button과 구분한다.
- ui_viz에서 `Temporary`인 값은 구현하지 않고 shadcn 기본값을 유지한다. 같은 상태에서 Confirmed인 값(Input Disabled 면 White)만 덮어쓴다. 단 Input Disabled opacity `.6`은 ui_viz에서는 `Temporary`이지만, **2026-10-02 사용자 결정**(Select 작업에서 확정된 Disabled 시각 intent와의 정합)으로 Input과 Select에 한해 `0.6`으로 확정되었다. 이전의 "`.6` Temporary → 미적용" 판단은 이 결정으로 superseded 되었다. 전역 Disabled opacity Token으로 승격하지 않으며, Button 등 다른 컴포넌트는 각자의 Confirmed 규격 또는 별도 사용자 결정을 따른다.
- Radix 기반 컴포넌트(Select)는 Radix가 제공하는 상태 속성(`data-state`, `data-placeholder`, `data-highlighted`, `data-side`)과 `:disabled`로 상태를 적용하고, `data-size` 외에 HM 전용 상태 속성이나 상태 prop을 만들지 않는다. size는 DOM 요소가 있는 Trigger에 두며, ui_viz 값(Select: `medium` | `large`, 기본 `large`)이 shadcn 값(`sm` | `default`)을 대체한다.
- Overlay(Content)는 Radix positioning을 유지하고 ui_viz가 정한 시각 관계만 prop 기본값과 CSS로 맞춘다(Select: `sideOffset` -1, 폭은 `--radix-select-trigger-width`, 연결된 윤곽). ui_viz 윤곽과 충돌하는 shadcn 장식(ring, 4px 간격 translate)은 제거하고, ui_viz에 값이 없는 그림자는 shadcn 기본(`shadow-md`)을 유지한다.
- 상태 색이 `Temporary`여도 상태를 어떻게 표현하는지(Select: Selected는 Hover와 같은 배경 면, check icon·border 없음)가 Confirmed이면 표현은 구현하고 색은 shadcn Token(`accent`)을 그대로 쓴다. ui_viz에 없는 shadcn 장식(Select의 check indicator)은 제거한다. ui_viz에 정의가 없는 상태(Select Error)는 shadcn `aria-invalid` 스타일을 유지하고 theme CSS가 그 색을 덮지 않게 한다.
- ui_viz가 glyph를 Source로 제공하면(Select indicator 18×18) shadcn의 lucide 아이콘을 그 glyph로 바꾸고 색은 Mapping Token으로 둔다. ui_viz가 사용자 결정으로 확정한 값은 그 컴포넌트에 한해 구현한다(Select·Input Disabled opacity .6, 위 항목 참조). 같은 값이 다른 컴포넌트에서 `Temporary`여도 그쪽으로 확대하지 않는다.
- ui_viz에서 가져온 값에는 출처 주석을 한 줄 남긴다. 문서 경로, 절 또는 Decision, 상태만 적고 원문을 복사하지 않는다.
```css
/* ui_viz: docs/components/button.md §1.1 D-087 — Confirmed */
```
### 6.8 컴포넌트 문서 (`docs/components/`)
각 UI 컴포넌트의 현재 최종 규격, API, Token, 상태 및 사용자 결정은 이 저장소의 `docs/components/<component>.md`를 기준으로 한다(현재 Component 문서와 Field 소비 가이드 포함). 이 지침은 프로젝트 공통 원칙만 두고, 컴포넌트별 수치와 API 목록은 누적하지 않는다. §6.7의 Button·Input·Select 예시는 원칙을 설명하기 위한 것이며 값의 기준은 컴포넌트 문서다.
| 문서 | 담는 내용 |
|---|---|
| `프로젝트-지침.md` | 프로젝트 공통 원칙, 작업 순서 |
| `테마-토큰-가이드.md` | Token 구조 공통 원칙(Primitive → Semantic → Component Mapping) |
| `docs/components/<component>.md` | 컴포넌트 고유 규격, API, 상태, 실제 Token 목록, 사용자 확정 결정, 보류 항목 |
| 외부 ui_viz Reference (ZIP 미포함) | 제공된 경우에만 참고하는 원본 규격과 근거. 현재 HM 구현·정책을 대체하지 않는다 |
- HM이 따로 확정한 값 때문에 구현이 ui_viz `Temporary`와 달라지면 그 결정을 컴포넌트 문서의 "사용자 확정 결정"에 날짜와 함께 기록한다. 컴포넌트 문서의 사용자 결정은 ui_viz의 Temporary·TBD보다 우선한다.
- overlay 컴포넌트(Dialog)는 StateBoard를 쓰지 않고 트리거로 실제 Radix를 여는 대표 Story와 `defaultOpen`으로 열린 verification Story를 둔다. Radix의 Portal·focus·ARIA·Escape 동작은 시각 규격을 위해 바꾸지 않는다. `asChild` 래퍼(`DialogClose` 등)가 `data-slot`을 자식 컴포넌트(Button) 위에 덮어쓰면 `[data-slot]` 기반 HM 스타일이 사라지므로, 래퍼는 asChild일 때 `data-slot` prop을 아예 넘기지 않는다(`undefined`로 넘겨도 덮어쓴다).
- Floating/Overlay 계열(Dropdown Menu, Context Menu, Popover, Tooltip, Hover Card)은 Dialog와 같은 방식으로 다룬다: 트리거로 실제 Radix를 여는 대표 Story + `defaultOpen` verification Story(`Open`), Radix의 Portal·positioning·focus 이동/복귀·Escape·ARIA는 시각 규격을 위해 바꾸지 않고, 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다. surface·윤곽·그림자가 같아도 컴포넌트마다 `--hm-<component>-*`를 두고 서로 참조하지 않는다(공통 Semantic 승격은 반복 근거가 확정된 뒤 검토; 후보는 `docs/components/popover.md` §14).
- Modal/Panel 계열(Sheet, Drawer, Alert Dialog)도 Dialog와 같은 방식으로 다룬다: 대표 Story는 트리거로 실제 Radix(Drawer는 vaul)를 열고 `defaultOpen` verification Story를 둔다. Radix의 Portal·Overlay·focus trap·initial focus·Escape·focus 복귀·ARIA는 바꾸지 않는다. Dialog 수치는 복사하지 않고 `--hm-<component>-*`를 따로 둔다(공통 Semantic 후보: modal surface·border·shadow·radius). Action/Cancel처럼 안에 Button이 들어가는 Radix 부품은 `<Button asChild><Primitive/></Button>`로 합성하고 Primitive에는 `data-slot`을 붙이지 않는다. vaul 같은 래퍼가 Radix 기본 동작을 끄면(Drawer의 `autoFocus` 기본 false) 모달에서는 되살린다. 모달 확인은 좁은 viewport(390px) 한 번을 포함한다. Floating(Dropdown Menu·Popover·Tooltip·Hover Card)과 Modal은 역할이 달라 값을 서로 참조하지 않는다.
- Navigation 계열은 둘로 나눈다. 페이지 안 primitive(Breadcrumb, Pagination)는 링크를 `a`로 유지하고 router에 의존하지 않으며(`asChild`), 현재 위치는 `aria-current="page"`로 표시한다. anchor에는 native disabled가 없으므로 `aria-disabled="true"`이면 `tabIndex=-1`과 클릭 취소를 컴포넌트가 보장하고, Button의 `buttonVariants`/token을 가져다 쓰지 않는다. Menu Navigation(Navigation Menu, Menubar)은 Radix 구조와 키보드 모델을 유지하고 Dropdown Menu의 token을 참조하지 않는다. 역할은 Dropdown Menu = 명령, Navigation Menu = 목적지 이동(link), Menubar = 메뉴 바다. 모바일 navigation 패턴은 이 컴포넌트들이 아니라 GNB/Layout 단계에서 정한다.
- Story 파일의 Tailwind 임의값 클래스(`w-[360px]` 등)가 새 파일에서 개발 서버에 생성되지 않는 일이 있었다. Story 배치용 크기·grid는 필요하면 `style`로 지정한다(컴포넌트 외형 보정이 아니라 배치).
- Data / Feedback 계열(Table, Progress, Skeleton, Avatar, Scroll Area)의 원칙: Table은 정적 표시 primitive로 정렬·필터·선택 state·페이지 로직을 넣지 않고(이후 Data Table Pattern), 좁은 화면은 카드로 바꾸지 않고 container 가로 scroll만 쓴다. Story 안에서 Table을 감싸는 grid item에는 `minWidth: 0`을 준다(Table 최소 폭이 부모를 넓힘). Radix가 소유하는 동작(Progress의 ARIA, Avatar 이미지/fallback, Scroll Area의 scroll)은 바꾸지 않는다. 상태색(success/warning/error)이 확정되기 전에는 상태 variant를 만들지 않는다.
- theme CSS는 `@layer` 밖이라 Tailwind utility(`className`)보다 항상 우선한다. 소비자가 `className`으로 모양을 정하는 것이 역할인 primitive(Skeleton)는 기본 모양을 `@layer components`에 둔다. 그 외 컴포넌트에서 크기·색을 바꾸려면 prop(`size`) 또는 `--hm-<component>-*` Token을 쓴다.
- Selection Controls(Slider, Toggle, Toggle Group)의 원칙: Slider는 값 입력, Toggle은 pressed 상태, Toggle Group은 선택 그룹이며 역할 비교는 `docs/components/toggle.md` §14에 둔다. Toggle/Toggle Group은 Button과 모양이 가까워도 Button의 role/appearance 체계와 `--hm-button-*`를 상속하지 않고 독립 Token을 쓴다. `className`으로 크기·radius·레이아웃을 확장하는 것이 자연스러운 컨트롤(Toggle, Toggle Group)은 기본 모양을 `@layer components`에 둔다(Skeleton과 같은 규칙). Radix가 `role="slider"`를 Root가 아닌 Thumb에 두는 것처럼 접근 가능한 이름이 내부 요소에 필요한 경우 컴포넌트가 `aria-label`을 전달한다. Radix가 조건부로만 내는 속성(ToggleGroup의 `data-orientation`)에 기대지 말고 `:not()`으로 선택한다.
- Search / Date 계열(Calendar, Command, Date Picker, Combobox)의 원칙: Calendar(react-day-picker)와 Command(cmdk)는 기반 라이브러리가 날짜 계산·필터링·키보드·ARIA를 소유하는 primitive이고, 라이브러리 기본 class(`rdp-*`)와 속성(`data-selected`, `data-selected="true"` 등)에 시각만 연결한다. Date Picker는 Popover + Calendar, Combobox는 Popover + Command를 public API로 조합한 Component이며 Popover를 복제하지 않는다. 조합 컴포넌트가 `PopoverContent`에 `data-slot`을 넘기면 Popover의 `data-slot`을 덮어써 Popover 스타일이 사라지므로 넘기지 않는다. Popover의 padding·폭은 Popover 코드를 바꾸지 않고 `style`의 `--hm-popover-padding`/`--hm-popover-width`로 인스턴스마다 덮는다. 새 dependency는 shadcn 구현이 쓰는 최소(`react-day-picker`, `cmdk`)만 `packages/ui`에 추가하고, 날짜 표시는 `Intl.DateTimeFormat`과 native `Date`로 해결한다. field형 trigger는 grid/flex 부모를 넓히지 않도록 `min-width: 0`을 둔다. Select = 짧은 목록(Radix Select), Combobox = 검색 가능한 선택(Popover + Command)으로 구분한다.
- Layout / Media 계열(Carousel, Aspect Ratio, Resizable)의 원칙: 소비자 레이아웃 확장성이 우선이라 기본 모양은 `@layer components`에 두고 크기·폭·gap·control 위치는 소비자 utility가 이기게 한다. 시각 규격이 없는 Aspect Ratio는 CSS·Token 파일을 만들지 않는다. Carousel(Embla)과 Resizable(react-resizable-panels v4: `Group`/`Panel`/`Separator`, 숫자=픽셀, 단위 없는 문자열="%")은 라이브러리가 interaction을 소유하며 persistence·Dashboard state·autoplay는 넣지 않는다. Embla/패널 Root는 grid·flex 부모의 `min-width:auto`로 페이지를 밀 수 있으므로 Root에 `min-w-0`을 두고, 한 단계 감싼 wrapper에는 소비자가 `min-w-0`을 준다. 컨트롤(Carousel 이전/다음)은 좁은 부모에서 잘리지 않도록 가장자리 안쪽에 둔다. Resizable은 모든 panel에 `minSize`를 준다.
- Floating surface Semantic 후보(2026-10-02 C묶음 후 비교): Dropdown Menu, Popover, Hover Card, Menubar, Navigation Menu(viewport)는 흰 면(`--hm-sem-bg-surface`), 1px `--hm-warm-neutral-pale`, radius `--hm-radius-container`(4px), 그림자 `0 0 12px rgb(0 0 0 / .2)`이 같다. Date Picker와 Combobox의 popup은 Popover를 그대로 써서 같은 값이다. Command는 면과 radius만 같고 border·그림자가 없다(컨테이너 안에서 쓰는 전제). Tooltip은 어두운 면이라 제외, Dialog/Sheet/Drawer/Alert Dialog는 modal이라 별도(radius 8px 등). 승격 후보 이름은 `--hm-sem-floating-background|border|radius|shadow`이며, 2026-10-03에 `default.css`로 승격했고 5개 컴포넌트 token이 이를 참조한다(Component Token 유지, 화면 변화 없음).
- ui_viz에 `docs/components/<name>.md`가 없는 컴포넌트(Card)는 해당 역할을 정의한 pattern 문서(Card: `docs/patterns/content-container.md` Type B)를 근거로 삼는다. 문서가 `Confirmed`가 아닌 owner 채택(`SOURCE_ADOPTED`, `LOCAL_EXCEPTION`) 값을 쓰면 컴포넌트 문서에 그 상태를 구분해 적고, 정의되지 않은 요소(Card의 Footer 등)는 shadcn 기본값으로 둔다.
- 새 컴포넌트를 완료하면 같은 템플릿(상태, 적용 근거, 기본 규격, 상태 규격, API, Token, 유지 항목, 변경 항목, Storybook, 사용자 결정, 미확정, 관련 파일, 검증 상태)으로 문서를 추가한다. 없는 상태나 API는 채우지 않는다.
- CENTER 문서는 UI 컴포넌트 문서의 근거로 쓰지 않는다.
### 6.9 ui-viz의 위치와 새 컴포넌트 기준 (2026-10-02 결정)
외부 ui_viz Reference (ZIP 미포함)는 구현 방향이 바뀌기 전에 쓰던 테스트/검증 프로젝트이며 HMDesignSystem의 최종 기준 구현체가 아니다. §6.1, §6.7의 "Confirmed만 구현 근거" 방식은 Button·Input·Select·Card·Dialog·Textarea를 만든 방식으로 남기되, 앞으로의 기준은 다음과 같다.
- ui-viz의 값을 그대로 복제하는 것을 목표로 하지 않는다. 기존에 검토·확정된 디자인 의도와 수치는 참고 자료로 쓰고, 현재 HM 방향에 맞는지 확인해 가져온다.
- HOLD / TBD / Temporary / 미정의 값은 그대로 적용하지 않고 현재 HM Token 체계와 공통 디자인 원칙(기존 Primitive/Semantic, 구조가 가까운 기존 컴포넌트 패턴)으로 새로 정한다. ui-viz에 규격이 없는 컴포넌트(Checkbox, Radio Group, Switch, Badge, Alert, Separator)도 같은 방식으로 정식 컴포넌트로 구현한다.
- shadcn/Radix의 접근성, 상태 관리, 키보드 동작, API 구조를 우선 유지한다. wrapper는 필요한 경우에 한해 추가한다(예: Tabs의 frame과 이동 버튼).
- 다른 컴포넌트의 component token을 값이 같다는 이유로 직접 재사용하지 않는다. 컴포넌트마다 `--hm-<component>-*`를 두고 공통 Semantic을 참조한다.
- 최종 기준은 실제 구현과 `docs/components/<component>.md`다. 문서에는 값마다 ui-viz 상태명을 분류하지 않고 참고 근거, 현재 HM 규격, 구현 방식, 보류 항목만 기록한다. 기존 문서의 Confirmed/Temporary 표기는 당시 이력이다.
- 개별 수치는 이 지침에 넣지 않는다.
### 6.10 Pattern 계층 (2026-10-03 도입)
- `packages/ui/src/components/ui`는 독립 primitive, `packages/ui/src/patterns`는 여러 primitive를 조합한 반복 업무 UI(Data Table, Filter Bar)다. Pattern 문서는 `docs/patterns/`에 둔다(Story는 `Patterns/Data/<이름>` 또는 `Patterns/Layout/<이름>`).
- Pattern은 primitive의 시각 스타일을 복제하거나 다시 정의하지 않고 composition과 상태 연결만 맡는다. Pattern 전용 스타일은 toolbar·footer·loading/empty wrapper·레이아웃 정도로 `--hm-<pattern>-*` token에 둔다. primitive(`Table` 등)에는 업무 로직을 넣지 않는다.
- Pattern은 서로 종속시키지 않고(Filter Bar는 Data Table 없이도 쓴다), 외부 라이브러리 state(TanStack)는 그대로 노출한다. Story는 로컬 sample data로 실제 조합을 검증한다.
### 6.11 Layout 단계의 기준 (2026-10-03 결정)
개별 UI 컴포넌트와 Layout/Pattern(GNB, LNB, Dashboard, Page)의 기준을 구분한다.
```text
UI primitive / component → 현재 @hm-design/ui 구현, docs/components, shadcn·Radix 구조. ui-viz는 참고 (§6.9)
Layout / Pattern → 현재 HM 대표 Story / Pattern 문서와 단일 Navigation 정책 우선
외부 ui_viz Reference (ZIP 미포함)는 충돌하지 않는 범위의 참고 자료
구현은 현재 HM primitive / pattern으로 재구성
```
- Layout은 현재 HM 대표 Story와 사용자 확정 Pattern 규격을 먼저 재사용한다. ui-viz의 구조·시각 결과(높이, 배경, 선택·현재 표현, 간격, 영역 비율, 폭, 콘텐츠와의 연결)는 현재 기준 및 단일 Navigation 정책과 충돌하지 않는 범위에서 참고한다. 구형 Navigation 조합이나 색상 하드코딩을 복원하지 않는다.
- 구현 기술은 현재 HM을 따른다: Radix 접근성·keyboard·focus·Portal, shadcn API, 기존 primitive 조합(Navigation Menu, Dropdown Menu, Sheet, Collapsible 등). ui-viz의 class·DOM·state 코드는 복사하지 않고, ui-viz가 정의하지 않은 반응형은 현재 HM 접근성·사용성을 우선한다.
- ui-viz 값을 primitive에 반영하지 않는다. Layout 전용 재색칠은 `[data-slot="<pattern>"]` 범위 안에서만 하고, 하위 primitive의 기본 Story는 바뀌지 않아야 한다.
- Pattern 문서에는 ui-viz 비교표를 두고 차이를 MATCHED / IMPLEMENTATION_CHANGE / INTENTIONAL_CHANGE / MISSING / FOLLOW_UP으로 분류한다. 개별 px 값은 지침이 아니라 Pattern 문서에 둔다.
### 6.12 Navigation Single Mode (2026-10-07 사용자 확정)
**ONE PAGE = ONE NAVIGATION MODE.** GNB ONLY / LNB ONLY / DASHBOARD ONLY 중 정확히 하나를 선택한다. 독립 모드를 결합하는 GNB + LNB, GNB + Dashboard, LNB + Dashboard, 세 가지 중첩은 금지한다. GNB ONLY 내부 Sidebar에서 기존 LNB 메뉴 부품을 재사용하는 것은 독립 LNB Mode 추가가 아니며 허용한다. Sidebar 외곽 배경만 Green Light 2로 적용하고 메뉴 디자인·구조·동작은 유지한다.
- AI 기반 코드 생성·제안, 신규 페이지 구현, 기존 화면 수정, 프로토타입, Storybook, 실제 Production 구현 전체에 동일하게 적용한다. AI는 별도 요청이 없는 한 Navigation 조합을 생성하거나 제안하지 않는다.
- 모바일에서도 선택한 체계를 유지한다. LNB Desktop(≥1024px) → LNB Mobile Header(≤1023px)는 같은 LNB Mode다.
- Dashboard 운영 화면은 전용 Header(48px)·업무 상태 필터 + Content로 구성하며 모든 Sidebar 사용을 금지한다. 외부 GNB나 일반 LNB를 추가하지 않는다. Sidebar 없는 최소 폭 960px과 Dashboard 내부 가로 스크롤을 유지한다. 기존 `DashboardSidebar`와 `sidebar` slot 및 1232px 구성은 호환성·검증용이며 신규 운영 Reference가 아니다.
- Navigation 간 CSS 변수·height/width 계산·breakpoint·spacing 교차 의존성을 만들지 않는다.
- 대표/PRIMARY Story는 단일 모드의 운영 구성만 사용한다. 기존 혼합 Story와 Dashboard Sidebar 기술 검증은 verification 태그와 **VERIFICATION ONLY — 운영 사용 금지** 표시로 유지할 수 있으며 신규 화면 구현 기준으로 사용하지 않는다. 상세는 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다.
상세 기준은 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다. 이전 조합 기준과 검증 이력은 운영·AI 생성의 근거가 아니다.
## 7. Storybook 운영 규칙
- Story는 패키지의 공개 컴포넌트를 import한다. 내부용 복제 컴포넌트나 가짜 Preview UI로 검수를 대체하지 않는다.
- 각 주요 컴포넌트에 기본/variant/state, disabled·error·loading, 키보드 및 포커스 상태를 해당 컴포넌트의 실제 지원 범위에 맞게 문서화한다.
- 현재 Default/External/URL Loader 및 계약 검증은 Theme verification Story로 비교한다. `Theme/Designer Playground/Editor`는 JSON URL/파일 Preview와 최소 컬러 편집을 제공한다. Preview는 패키지 Runtime 계약을 사용하며 운영 Publish는 수행하지 않는다.
- 컴포넌트 PR에서는 관련 Story와 기본 Theme·대표 Runtime Theme을 확인한다. Theme Publish에서는 변경한 Token이 닿는 Story, 대비·가독성, 화면 넘침을 확인한다.
- Storybook 빌드/배포 버전과 `@company/ui` 버전을 표시해 검수한 결과와 서비스 패키지 버전을 연결한다. Storybook에서 보인다고 해서 앱의 실제 반영을 확인한 것으로 간주하지 않는다.
## 8. 도입 순서와 완료 조건
| 단계 | 구축 범위 | 다음 단계 진입 조건 |
|---|---|---|
| 현재 구현 | UI/CENTER compiled dist build/export, Component·Pattern, 공개 Runtime 계약/API·URL Loader, Default/External verification Story, Theme-only artifact. Registry publish 이력은 ZIP 검증 범위 밖; 운영 CDN/Publish pipeline은 미구현 | 코드/문서/실제 Storybook 정합성 검증 |
| MVP | 단일 저장소에서 `@company/ui` 패키징, 기본 CSS, Semantic 계약, Storybook, 로컬 JSON Preview/Loader | 같은 패키지 컴포넌트에서 값 override와 fallback을 실제로 확인 |
| 운영 기반 | 불변 Theme Registry, 스키마 검증, 버전·호환성, 승인 Publish, API/CDN, 롤백·로그 | 앱 재배포 없이 Theme 값 반영, 실패 시 기본값, 이전 버전 복귀를 확인 |
| 운영 확장 | 디자인팀 Admin, 권한·감사, 자동 검수·모니터링 | 운영 절차와 담당자가 정해지고 실제 서비스 적용 기록이 남음 |
### 8.1 초기 도입 순서 (구현 이력)
```text
Button / Input / Select 완료 + 컴포넌트 문서 정리 (docs/components)
↓
Card 완료
↓
Dialog 완료
↓
패키지 구조 분리 (packages/ui, packages/center-graphic)
↓
packages/ui 구조에서 나머지 UI 컴포넌트 추가
```
- **Card와 Dialog 완료를 패키지 분리 작업의 시작 기준점으로 한다.** 그 기준을 충족해 2026-10-02에 분리했다(§5.1).
- 현재 Storybook은 `Components`, `Patterns`, `Center Graphic`으로 통합 운영한다. `Theme`의 verification Story는 기본 sidebar에서 숨긴다.
- 패키지 구성은 §5.1을 따른다(하나의 Git 저장소, `@hm-design/ui`와 `@hm-design/center-graphic`, 독립 설치·버전 관리).
- `center-graphic` 실제 작업을 시작할 때 CENTER 문서와 assets의 읽기 범위·우선순위를 별도로 확정한다. 그 전까지 §5.1의 CENTER 적용 범위를 따른다.
상세 적용 절차는 [개발 적용 가이드](개발-적용-가이드.md), 발행과 복구 절차는 [운영/배포 가이드](운영-배포-가이드.md)를 따른다.