Files
HMDesign/docs/guidelines/프로젝트-지침.md
T

322 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`의 94개 입력이다(색상 92개: Semantic/Theme palette/Gradient stop, input/container radius 2개). 정확한 이름·타입·기본값·허용 범위는 이 계약과 `runtime-theme-contract.md`를 따른다. Shadow geometry는 현재 공개 입력이 아니다. Typography와 spacing은 실제 사용처와 레이아웃 영향 검토 후 개별 등록한다. 문서에 예시가 있다는 이유로 곧바로 Runtime 허용 Token이 되지 않는다.
집계 기준 Commit은 `3ebc84e32cf91298a2a4d8046753244c499fd719`다. `scripts/theme-contract.mjs`가 `contract.ts`의 공개 Key·Property·type 항목을 추출한 `definitions.length`는 **94개(색상 92개 + radius 2개)**다. `Object.keys(themes/default.json의 tokens).length`는 색상 직접 설정값 **92개**이며 radius는 JSON에 없고 CSS Template에서 기본값을 제공한다. `scripts/theme-defaults.mjs --check`는 JSON + `scripts/theme-templates/{primitives,default}.css` → `packages/ui/src/theme/{primitives,default}.css` 및 `default-values.generated.ts`의 **94개 계약 기본값** 연결을 확인한다. 공개 CSS의 계산·Component 매핑 변수 선언 수는 이 Runtime 입력 수와 별도 집계다.
기본 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 컴포넌트 도입은 별도 결정이다.
- **2026-10-11 최종 결정**: 기존 Input Story 소비 조합의 Disabled Label만 원본 `#6E6759` / opacity `0.6`을 적용한다. Input 컨트롤 `0.6`, 공통 Label Component Disabled `0.5`, Input 기본·readOnly·Error Label `1`(Disabled가 아닐 때)을 유지한다. 부모 Field에 opacity를 적용하거나 Message / Helper Text·전역 Token으로 확대하지 않는다. 이전 “Label 100% 유지” 검토 및 2026-10-02/06 기록은 당시 근거로 보존한다. 상세 기준은 [Input 사용자 결정](../components/input.md#10-사용자-확정-결정)을 따른다. 신규 업무 화면의 공개 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, 세 가지 중첩은 금지한다. 신규 AI Production 화면의 GNB ONLY·DASHBOARD ONLY에는 Sidebar를 생성하지 않는다. LNB ONLY의 공식 좌측 Navigation은 허용한다. GNB 내부 LNB 부품 Sidebar와 Green Light 2 배경 계약은 기존 Component·Pattern·Reference의 호환성 보존용이며 신규 운영 화면 허용 기준이 아니다.
- 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)를 따른다.