Merge branch 'feature/sw-package' into main
Deploy Storybook / deploy (push) Successful in 1m9s

This commit is contained in:
shkim4 committed 2026-10-20 21:53:55 +09:00
commit 553e4df4a8
1152 files changed
+370481 -16

No files matched your search

+2
View File
@@ -46,6 +46,8 @@ const preview: Preview = {
"OrgChart",
["Overview", "Department Colors", "Full Composition", "Expanded OrgChart"],
"Center Graphic",
"SW",
["Header", "Footer", "Popup", "Viewer", "Theme"],
"*",
],
},
+12 -5
View File
@@ -4,6 +4,12 @@
이 파일을 먼저 읽고 필요한 항목만 탐색한다. 전체 문서·전체 소스·전체 Storybook을 반복해서 읽지 않는다.
## 패키지 소비용 AI 지침 — 2026-10-19
- 이 파일은 HMDesign 저장소를 개발하는 AI용이다. 패키지를 설치한 **다른 프로젝트**의 AI가 읽는 지침은 각 패키지의 `ai/`(`packages/ui/ai`, `packages/sw/ai`, `packages/org-chart/ai`, `packages/org-chart-react/ai`)에 있고 공통 진입점 원본은 [docs/ai/AI_GUIDE.md](docs/ai/AI_GUIDE.md)다.
- 사용 유형은 Web / SW(**SW 일반 모드**·**SW 뷰어 모드**) / OrgChart / 혼합이다. SW 작업은 모드를 먼저 정하고(불명확하면 질문, 요청·설정으로 명확하면 묻지 않음) 선택 모드의 컴포넌트·토큰만 쓴다 — 일반 `SWHeader`·`SWPopup`, 뷰어 `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`. 기준은 [packages/sw/ai/AI_USAGE.md](packages/sw/ai/AI_USAGE.md) §0과 공통 가이드 1·4·5단계이며 이 파일에 본문을 복사하지 않는다.
- 공개 Export·Props·토큰을 바꾸면 같은 변경에서 해당 `ai/` 문서를 갱신하고 `npm run check:ai-docs`를 실행한다. 공통 가이드는 원본만 고친 뒤 `npm run sync:ai-docs`로 사본을 갱신한다. 절차는 [docs/ai/MAINTAINING.md](docs/ai/MAINTAINING.md).
## Typography — 공통 정책
- HMDesign의 Font Weight는 400(Regular), 500(Medium), 700(Bold)의 3단계만 사용한다. Font Weight 600은 사용하지 않으며, 카드 제목은 700을 기본으로 한다.
@@ -32,10 +38,11 @@
필요할 때만 [보존된 상세 지침](docs/guidelines/ai-implementation-details.md)을 읽는다. 신규 화면에는 위 필수 규칙이 우선한다.
## 공식 작업 경로와 Git 정책 — 2026-10-11 사용자 확정
## 작업 위치와 Git 정책 — 2026-10-20 개정 (2026-10-11 정책 대체)
- HMDesign 실제 수정은 `D:\디자인시스템\HMDesign-Gitea-Transfer`에서 진행한다. 시작 전 절대경로·`main` 브랜치·공식 origin `https://gitea.hmac.kr/b25027/HMDesign.git`·최신 원격 HEAD·Working Tree·기존 미커밋 변경을 확인한다.
- 퍼블리셔 요청에 필요한 부분만 수정하고 기존 Component·Pattern·Theme·Storybook을 보존한다. 변경 영향과 관련 MD/Storybook 정합성을 검증하고 유효한 기존 정상 검증은 불필요하게 반복하지 않는다.
- 필수 검증을 통과하면 관련 파일만 Stage하고 Staged Diff를 검토한 뒤 Commit·공식 Gitea `main` 일반 Push를 수행한다. 작업별 사용자의 명시적 Commit/Push 보류 지시가 있으면 그 지시를 우선한다.
- Commit/Push 직전에 원격 `main`을 다시 확인한다. 시작 기준 이후 원격이 변경됐거나 중요한 충돌이 있으면 자동으로 덮어쓰지 않고 중단한다. 완료 후 Local/Remote SHA와 Working Tree를 확인한다.
- 작업 위치는 고정 폴더가 아니라 **사용자가 지정한 브랜치의 작업 트리**다(별도 Clone·worktree 포함). 시작 전 절대경로·현재 브랜치·HEAD·공식 origin `https://gitea.hmac.kr/b25027/HMDesign.git`·최신 원격 HEAD·Working Tree·기존 미커밋 변경을 확인한다. 같은 폴더를 다른 작업자나 AI가 수정 중일 수 있으므로 수정 전 파일을 다시 읽는다.
- 기능 작업은 기능 브랜치(예: `feature/sw-package`, `feature/orgchart-package`)에서 진행하고 `main`에는 사용자가 지시한 병합(PR 등)으로만 반영한다. `main` 직접 Commit/Push와 병합은 사용자의 명시적 지시가 있을 때만 수행한다.
- 요청에 필요한 부분만 수정하고 기존 Component·Pattern·Theme·Storybook을 보존한다. 변경 영향과 관련 MD/Storybook 정합성을 검증하고 유효한 기존 정상 검증은 불필요하게 반복하지 않는다.
- Commit·Push는 사용자 승인 후 수행한다. 관련 파일만 Stage하고 Staged Diff를 검토한다. 직전에 대상 원격 브랜치를 다시 확인하고, 시작 기준 이후 원격이 변경됐거나 중요한 충돌이 있으면 자동으로 덮어쓰지 않고 중단한다. 완료 후 Local/Remote SHA와 Working Tree를 확인한다.
- `main` push는 웹 Storybook(`https://design.hmac.kr/`) 배포 Workflow를 실행한다(`.gitea/workflows/deploy-storybook.yml`). 병합 전에 운영 웹을 변경하지 않는다.
- GitHub 이전 원본 폴더·remote에는 수정/Push하지 않는다. Force Push·Reset·Clean·기존 이력 삭제·백업 브랜치 변경·무관한 파일 Stage를 금지한다.
+2 -1
View File
@@ -14,6 +14,7 @@ shadcn/ui(Radix, Tailwind v4, CVA) 기반 HM 디자인 시스템 시제품. Reac
- [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md) — 자연어 또는 JSON·CSV·Excel에서 새 조직도 컴포넌트 또는 전체 페이지를 생성할 때 먼저 읽는다. 생성 방식 미지정 시 코드 작성 전에 위 정책의 사전 선택 질문으로 Chart Only / Full Page를 확인하고 답변 후 구현한다. 이미 명시한 방식은 다시 묻지 않는다. Chart Only는 흰색 캔버스·노드·연결선의 삽입용 컴포넌트, 최초 Fit·휠 Zoom·Pan과 외부 UI 제외를 따르고, Full Page는 기존 HM UI로 요청 UI·전체 배치·반응형을 구성한다. 두 방식 모두 가변 계층·정확한 인원·제공 데이터 보존·기존 Core/React 재사용을 따른다. Fixed·Auto/Expanded는 생성 방식 선택을 대신하지 않는다. Chart Only의 Navigation 예외와 Full Page의 HM 화면 계약은 `AGENTS.md`를 따른다. 기존 Full Composition·Expanded OrgChart·Department Colors·Overview·P9.5·P-AI1에는 소급 적용하지 않는다.
- 일반 업무 화면은 Storybook에 등록하지 않고 별도 React 화면·독립 미리보기로 제공한다. Storybook 수정은 Component/Pattern 또는 명시적 Storybook Reference 작업에 한정한다. 상세한 Storybook / 화면 생성 분리는 `AGENTS.md`와 `docs/guidelines/shadcn-기반-구현-원칙.md`를 따른다.
- `docs/guidelines/프로젝트-지침.md` — §6 컴포넌트·Storybook·CSS 작업 규칙 (모든 컴포넌트 작업 전 필독)
- `docs/ai/MAINTAINING.md` — 패키지에 포함되는 소비 프로젝트용 AI 지침(`packages/*/ai/`, 공통 원본 `docs/ai/AI_GUIDE.md`). 공개 API·토큰 변경 시 같은 변경에서 갱신하고 `npm run check:ai-docs`를 실행한다.
- `docs/guidelines/shadcn-기반-구현-원칙.md` — Component / Pattern / Story의 재사용 우선순위와 Custom 구현 허용 조건 (관련 작업 전 필독)
- `docs/guidelines/테마-토큰-가이드.md` — Token 구조, 이름 규칙, HM Default/External Runtime 계약, Light/Dark 없음
- `docs/guidelines/개발-적용-가이드.md`, `운영-배포-가이드.md`, `theme-delivery.md` — JSON 원본/Runtime URL Loader/독립 Theme 배포와 향후 실제 CDN·Publish의 경계
@@ -62,7 +63,7 @@ shadcn/ui(Radix, Tailwind v4, CVA) 기반 HM 디자인 시스템 시제품. Reac
- Button(`variant`=역할, `appearance`=treatment, `size`), Input(`size`, native 상태 selector), Select(Trigger `size`, Radix 상태 속성)는 ui_viz Confirmed 규격으로 구현했다. Card도 `<Card />`만으로 HM 스타일이 적용된다(ui_viz Content Card, `packages/ui/src/theme/card.css`). Dialog도 `<DialogContent />`만으로 HM 스타일이 적용된다(`packages/ui/src/theme/dialog.css`).
- 구조(2026-10-02 패키지 분리, npm workspaces): UI 컴포넌트·Theme 구현은 `packages/ui`(`@hm-design/ui`, 컴포넌트 `src/components/ui/`, Theme `src/theme/*.css`, 진입점 `src/theme.css`·`src/tailwind.css`), 컴포넌트 문서는 `docs/components/`, Storybook·Demo는 루트(`.storybook/`, `src/`; Demo 스타일은 `src/hm-theme.css`), `packages/center-graphic`(`@hm-design/center-graphic`)은 CENTER 구현(제목·정보·노선/공간·시설물), 기준 문서는 `docs/center-graphic/`다.
- 작업 순서: 컴포넌트 문서 정리, Card, Dialog, 패키지 분리, CENTER 구현, 1단계 UI 컴포넌트(Textarea, Tabs, Label, Checkbox, Radio Group, Switch, Badge, Alert, Separator)와 2단계 A묶음 Floating(Dropdown Menu, Popover, Tooltip, Hover Card)과 B묶음 Modal(Sheet, Drawer(vaul), Alert Dialog)과 C묶음 Expand(Accordion, Collapsible)과 D묶음 Navigation(Breadcrumb, Pagination, Navigation Menu, Menubar)까지 완료로 2단계 UI가 끝났다. 3단계 A묶음 Data/Feedback(Table, Progress, Skeleton, Avatar, Scroll Area)과 B묶음 Selection Controls(Slider, Toggle, Toggle Group)과 C묶음 Search/Date(Calendar, Command, Date Picker, Combobox)까지 완료(새 dependency: `react-day-picker`, `cmdk`)과 D묶음 Layout/Media(Carousel, Aspect Ratio, Resizable)까지 완료로 3단계 UI가 끝났다(새 dependency: `embla-carousel-react`, `react-resizable-panels`). floating surface Semantic 정합성 작업도 완료했다. 4단계 A묶음 독립 UI(Context Menu, Toast/Sonner)까지 완료(새 dependency: `sonner`). 4단계 B묶음 업무 Pattern(Data Table, Filter Bar)까지 완료(새 dependency: `@tanstack/react-table` v9)로 4단계 핵심이 끝났다. Layout Pattern은 GNB·LNB·Page Header·Dashboard Layout까지 완료(`docs/patterns/gnb.md`, `lnb.md`, `page-header.md`, `dashboard-layout.md`, ui-viz 시각 기준)했다. 각 Navigation Mode는 독립 레이아웃을 소유한다. GNB ONLY는 기존 LNB 메뉴 부품을 재사용하는 내부 Sidebar를 허용하며 외곽 배경만 Green Light 2로 적용한다. DASHBOARD ONLY 운영 화면은 전용 Header(48px)·업무 상태 필터 + Content로 구성하고 Sidebar를 금지한다. 운영 최소 폭 960px·내부 가로 스크롤을 유지한다. 기존 DashboardSidebar Export·sidebar slot·1232px 구성은 호환성·검증용이며 신규 운영 Reference가 아니다. LNB 3뎁스(`LNBGroup` 중첩, 2026-10-06)와 모바일 Sheet 중첩 그룹까지 완료(`docs/patterns/lnb.md` §3a). 현재 LNB 모바일은 독립 LNB Mobile Header와 Sheet를 사용한다(`lnb.md` §3b). 과거 GNB+LNB 조합·Brand ownership·gradient 접점 기준은 Navigation Single Mode Policy로 대체되었다. 한 페이지에는 GNB / LNB / DASHBOARD 중 하나만 사용하며 기존 혼합 Story는 verification / VERIFICATION ONLY 조건의 기술 검증에만 한정한다. 경로: UI primitive `packages/ui/src/components/ui`, Pattern `packages/ui/src/patterns`, Pattern 문서 `docs/patterns`. Field는 코드 없이 `docs/components/field.md` 소비 가이드만 둔다.
- 두 패키지는 서로 의존하지 않으며 `private: false`, compiled dist export(JS·타입·CSS)와 library build가 구현되어 있다. Registry publish 이력은 ZIP 검증 범위 밖이다. Runtime Theme URL Loader와 Theme JSON 독립 산출은 구현되었고 실제 CDN infrastructure/publish pipeline은 미구현이다. CENTER 기준은 `docs/center-graphic/README.md`, `packages/center-graphic/src/`, `src/center/`의 현재 HM 구현과 대표 Story다. 외부 ui_viz owner 자료는 제공된 경우에만 참고하며 필수 실행 경로가 아니다. UI와 CENTER 규격을 섞지 않는다.
- 패키지는 5개다: `@hm-design/ui`(Web), `@hm-design/sw`(SW 일반·뷰어 모드, `packages/sw`, Story `src/sw/`, 문서 `docs/sw/`), `@hm-design/org-chart`·`@hm-design/org-chart-react`(OrgChart, Story `src/org-chart/`, 문서 `docs/org-chart/`), `@hm-design/center-graphic`(CENTER). 역할·진입점 표는 `README.md`의 패키지 구성, 외부 설치는 `docs/guidelines/external-install-submodule.md`를 따른다. `org-chart-react`가 Core에 의존하고 `/hm-dialog`만 `@hm-design/ui`를 optional peer로 쓰는 것 외에는 서로 의존하지 않는다. 모두 private이 아니며 compiled dist export(JS·타입·CSS)와 library build가 구현되어 있다. Registry publish 이력은 ZIP 검증 범위 밖이다. Runtime Theme URL Loader와 Theme JSON 독립 산출은 구현되었고 실제 CDN infrastructure/publish pipeline은 미구현이다. CENTER 기준은 `docs/center-graphic/README.md`, `packages/center-graphic/src/`, `src/center/`의 현재 HM 구현과 대표 Story다. 외부 ui_viz owner 자료는 제공된 경우에만 참고하며 필수 실행 경로가 아니다. UI와 CENTER 규격을 섞지 않는다.
> 위 작업 순서는 구현 이력이다. 현재 API·제약은 코드와 Component/Pattern 문서를 대조한다.
+33 -5
View File
@@ -1,6 +1,20 @@
# HM Design System
React + TypeScript + Vite와 shadcn/Radix 기반 HM 디자인 시스템입니다. UI Component·업무/Layout Pattern·CENTER Graphic을 독립 workspace 패키지와 통합 Storybook으로 확인합니다.
React + TypeScript + Vite와 shadcn/Radix 기반 HM 디자인 시스템입니다. Web UI·SW UI·조직도·CENTER Graphic을 독립 workspace 패키지와 통합 Storybook으로 확인합니다.
## 패키지 구성
npm workspace(`packages/*`)의 패키지 5개입니다. 서로 의존하지 않으며(아래 표의 예외 제외) 각자 `dist/`의 ESM JS·타입·CSS를 `exports`로 공개합니다.
| 패키지 | 역할 | 공개 진입점 | Storybook | AI 지침 |
|---|---|---|---|---|
| `@hm-design/ui` | **Web** 일반 웹 UI Component(shadcn/Radix)·업무/Layout Pattern·HM Theme Runtime | `.`, `/theme`, `/theme.css`, `/tailwind.css`, `/dashboard-status.css` | Foundation · Components · Patterns · Theme | `packages/ui/ai/` |
| `@hm-design/sw` | **SW** 설치형·도면 SW UI. SW 일반 모드(`SWHeader`·`SWPopup`·`SWFooter`)와 SW 뷰어 모드(`SWViewer*`), 공통 범위 `SWTheme`. `@hm-design/ui`와 독립 | `.`, `/styles.css` | SW | `packages/sw/ai/` |
| `@hm-design/org-chart` | **OrgChart** Core(Vanilla, Shadow DOM, 데이터·배치·상호작용) | `.`, `/style.css`, `/engine.css`, `/browser` | OrgChart | `packages/org-chart/ai/` |
| `@hm-design/org-chart-react` | OrgChart React Wrapper. Core `@hm-design/org-chart`에 의존하고, 선택 Entry `/hm-dialog`만 `@hm-design/ui`(optional peer)를 사용 | `.`, `/hm-dialog` | OrgChart | `packages/org-chart-react/ai/` |
| `@hm-design/center-graphic` | CENTER 그래픽(제목·정보·노선/공간·시설물). 내부 검토 범위, License 미확정 | `.`, `/reference`, `/style.css` | Center Graphic | 없음(README) |
다른 프로젝트에서의 설치는 [외부 설치 가이드(Git Submodule + npm Workspace)](docs/guidelines/external-install-submodule.md)를 따릅니다.
## Navigation 정책
@@ -28,7 +42,7 @@ Storybook은 기존 HM Component·Pattern·Theme·Variant·State·Interaction을
최신 [Generic Status Error](docs/reports/generic-status-error-token-2026-10-11.md)는 `#B3261E` / Provisional이며 기존 Destructive `#D32F2F`를 유지합니다. [Disabled Input Label](docs/reports/disabled-input-label-2026-10-11.md)은 승인된 Input Story 소비 범위 60%, 공통 Label은 50%입니다. [Background Gradient 기록](docs/reports/brown-orange-background-gradients-2026-10-11.md)과 [현재 계약](docs/guidelines/runtime-theme-contract.md)은 Green·Brown·Orange 공통 배경을 구분하며, Foundation Color의 Button 전용 미리보기는 제거하고 실제 Button·Dashboard Gradient는 유지합니다. Focus `#266651`은 시험 적용 상태입니다.
Node.js는 lockfile의 `^20.19.0 || >=22.12.0` 조건을 충족해야 합니다(Node 21 제외). 이번 재현 환경은 **Node 20.20.1 / npm 10.8.2**입니다. Node 설치 시 제공되는 npm과 PATH를 확인하고, 재현 시에는 검증한 버전을 사용합니다. 최초 설치에는 공식 Gitea와 npm 레지스트리 접근이 필요합니다.
Node.js는 lockfile의 `^20.19.0 || >=22.12.0` 조건을 충족해야 합니다(Node 21 제외). 검증 기록은 **Node 20.20.1 / npm 10.8.2**(2026-10-11)와 **Node 22.23.2 / npm 10.9.8**(2026-10-20, SW 포함 전체 build·Storybook build)입니다. Docker 이미지는 `node:24-alpine`에서 빌드하며 Docker 경로는 아직 실제 빌드로 검증하지 않았습니다. Node 설치 시 제공되는 npm과 PATH를 확인하고, 재현 시에는 검증한 버전을 사용합니다. 최초 설치에는 공식 Gitea와 npm 레지스트리 접근이 필요합니다.
```powershell
git clone https://gitea.hmac.kr/b25027/HMDesign.git
@@ -71,7 +85,7 @@ docker compose up -d --build
### Gitea Actions 배포
`.gitea/workflows/deploy-storybook.yml`은 `main`에 push되거나 수동 실행될 때 해당 실행 커밋의 소스 archive를 SSH로 배포 서버에 보내고, 서버에서 Docker build와 Compose 갱신을 수행합니다. 기존 Git 작업 트리는 건드리지 않고 빌드는 서버에서 이뤄지며, 빌드 산출물은 Actions Runner에서 전송하지 않습니다. 실행 커밋별 소스는 `.storybook-deploy/releases/`에 보관하고 서비스는 배포 서버의 **6024 포트**를 사용합니다. 새 이미지 빌드가 끝난 뒤 Compose가 컨테이너를 교체하므로 빌드 중에는 현재 서비스를 유지합니다.
`.gitea/workflows/deploy-storybook.yml`은 `main`에 push되거나 수동 실행될 때 해당 실행 커밋의 소스 archive를 SSH로 배포 서버에 보내고, 서버에서 Docker build와 Compose 갱신을 수행합니다. 기존 Git 작업 트리는 건드리지 않고 빌드는 서버에서 이뤄지며, 빌드 산출물은 Actions Runner에서 전송하지 않습니다. 실행 커밋별 소스는 `.storybook-deploy/releases/`에 보관하고 서비스는 배포 서버의 **6024 포트**를 사용합니다. 새 이미지 빌드가 끝난 뒤 Compose가 컨테이너를 교체하므로 빌드 중에는 현재 서비스를 유지합니다. 따라서 웹 Storybook(`https://design.hmac.kr/`)은 `main` 배포본이며, `main`에 병합되지 않은 브랜치(예: SW 패키지 작업 브랜치)의 Story는 표시되지 않습니다.
저장소 Actions Variables에 `DEPLOY_HOST` (`10.13.10.4`), `DEPLOY_USER` (`user`), `DEPLOY_PATH` (`/home/user/services/HMDesign`), `DEPLOY_PORT` (`22`), `DEPLOY_KNOWN_HOSTS` (배포 서버의 검증된 SSH host key)를 설정하고, Actions Secret에 `DEPLOY_SSH_KEY`를 등록합니다. 배포 서버 계정은 지정한 경로에 `.storybook-deploy/releases/`를 만들고 Docker Engine/Compose plugin을 실행할 권한이 필요합니다. Actions Runner는 SSH와 SCP로 배포 서버에 접속할 수 있어야 합니다.
@@ -95,6 +109,9 @@ npm run dev:publisher-example
import { Button } from "@hm-design/ui"
import { CenterTitle, TunnelFacility } from "@hm-design/center-graphic"
import { DataTable, FilterBar, useDataTable } from "@hm-design/ui" // Patterns: docs/patterns/
import { SWTheme, SWHeader, SWFooter } from "@hm-design/sw" // SW 일반 모드
import { SWViewerHeader, SWViewerPanel } from "@hm-design/sw" // SW 뷰어 모드
import { OrgChart } from "@hm-design/org-chart-react" // Core: @hm-design/org-chart
```
```css
@@ -107,9 +124,11 @@ import { DataTable, FilterBar, useDataTable } from "@hm-design/ui" // Patterns:
별도 React 앱은 [Tailwind v4 검색 범위 가이드](docs/guidelines/runtime-portability.md#21-tailwind-v4-소비자-검색-범위)를 확인합니다. `source("./")`는 CSS 위치에 따라 범위가 달라지며 HM `tailwind.css`의 명시 source를 유지해야 합니다. 공개 package Theme는 동봉된 `dist/fonts/`를 상대 URL로 참조합니다. 개발 저장소·포터블 ZIP·React 소비자·단일 HTML의 차이는 [폰트·Asset 경로](docs/guidelines/runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)를 따릅니다.
SW는 Tailwind 없이 앱 진입점에서 `import "@hm-design/sw/styles.css"`를 한 번 추가합니다. 한 화면에서는 SW 일반 모드와 뷰어 모드 중 하나를 고르고, 글꼴 등 토큰은 가장 바깥 `<SWTheme style>`에서 바꿉니다([SW 사용 안내](packages/sw/ai/AI_USAGE.md)). SW와 Web UI의 기본 글꼴은 모두 Noto Sans KR입니다(SW는 2026-10-20 Dotum 기본값을 철회하고 확정). 글꼴 토큰은 패키지마다 따로 쓰며 서로의 글꼴 설정을 복사하지 않습니다. OrgChart는 Core CSS 두 개(`@hm-design/org-chart/engine.css`, `/style.css`)의 URL을 `stylesheetUrls`로 전달합니다(Shadow DOM, [OrgChart React README](packages/org-chart-react/README.md)).
화면 생성 Reference: [Navigation 선택 근거](docs/guidelines/navigation-single-mode.md#선택-근거), [GNB Desktop·Mobile 구성](docs/patterns/gnb.md#8-desktop--mobile)과 [내부 Sidebar](docs/patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08), [FilterBar 조회·초기화 소비 예제](docs/patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제). 일반 업무 목록의 모드를 임의 확정하거나 FilterBar에 조회 API가 있다고 가정하지 않습니다.
루트 `package.json`의 `workspaces`(`packages/*`)로 연결됩니다. 확인: `npm run typecheck`, `npm run build`. 두 패키지는 `private: false`이며 ESM JS·타입 선언·CSS를 `dist`에서 export합니다. `npm run build:packages`로 재빌드할 수 있고, Registry publish 이력은 Git 기반 테스트 공유의 검증 범위에 포함하지 않습니다.
루트 `package.json`의 `workspaces`(`packages/*`)로 연결됩니다. 확인: `npm run typecheck`, `npm run build`. 위 [패키지 구성](#패키지-구성)의 5개 패키지는 모두 private이 아니며 ESM JS·타입 선언·CSS를 `dist`에서 export합니다. `npm run build:packages`로 재빌드할 수 있고, Registry publish 이력은 Git 기반 테스트 공유의 검증 범위에 포함하지 않습니다.
## 파일 위치
@@ -120,6 +139,11 @@ import { DataTable, FilterBar, useDataTable } from "@hm-design/ui" // Patterns:
- `packages/ui/src/theme/primitives.css`, `packages/ui/src/theme/default.css`: Default JSON에서 생성한 package fallback과 Semantic/shadcn 연결
- `packages/ui/src/theme/<component>.css`: 컴포넌트별 Theme 매핑과 상태 스타일
- `packages/ui/src/theme.css`: Theme CSS 진입점, `packages/ui/src/tailwind.css`: Tailwind 쪽 설정
- `packages/sw/` (`@hm-design/sw`): SW 일반 모드·뷰어 모드 컴포넌트(`src/components/`), SW Theme CSS(`src/theme/`), 패키지 테스트(`tests/`), 소비 AI 지침(`ai/`)
- `src/sw/`: SW Storybook(`SW` 그룹), `docs/sw/`: SW 설계·Figma 매핑·검증 기록
- `packages/org-chart/` (`@hm-design/org-chart`), `packages/org-chart-react/` (`@hm-design/org-chart-react`): OrgChart Core와 React Wrapper
- `src/org-chart/`: OrgChart Storybook(`OrgChart` 그룹), `docs/org-chart/`: 디자인 승인·AI 생성 정책·구현 보고
- `docs/ai/`: 패키지 `ai/` 공통 지침 원본(`npm run sync:ai-docs`, `npm run check:ai-docs`)
- `packages/center-graphic/` (`@hm-design/center-graphic`): CENTER 그래픽(제목·정보·노선/공간·시설물), `@hm-design/ui`에 의존하지 않음
- `src/center/`: CENTER Storybook (`Center Graphic` 그룹)
- `docs/center-graphic/`: CENTER 구현 기준 문서
@@ -134,7 +158,8 @@ import { DataTable, FilterBar, useDataTable } from "@hm-design/ui" // Patterns:
- `docs/components/`: 컴포넌트별 현재 구현 기준 문서
- `public/fonts/`: 기존 자료의 Noto Sans KR 글꼴과 라이선스
- `docs/guidelines/`: 프로젝트 지침·구현 원칙·Theme Token·적용/배포 가이드
- 로컬 검증·감사·진단 자료는 Git에서 제외합니다. 과거 증거 경로는 로컬 보관 기록이며 Clone에서 열 수 있는 링크가 아닙니다.
- 로컬 검증·감사·진단 자료는 Git에서 제외하는 것이 원칙입니다(`test-results/`, `_local/`, `*.log` 등 `.gitignore`). 과거 증거 경로는 로컬 보관 기록이며 Clone에서 열 수 있는 링크가 아닙니다.
- **현재 상태와의 불일치(2026-10-20 기준, 정리 정책 미결정):** `docs/sw/*-qa*/` 등에는 SW 작업의 QA 증거(스크린샷·JSON, 약 600개 파일·40MB)가 Git에 포함되어 있습니다. 반대로 `docs/org-chart/` 보고서 일부는 Git에서 제외된 `test-results/` 증거를 링크합니다(`node scripts/check-doc-links.mjs`의 missing path). 두 경우 모두 기록 보존을 위해 삭제·링크 제외 없이 유지하며, 보관 위치는 별도로 결정합니다.
## 참고 범위
@@ -155,6 +180,9 @@ Component Mapping은 Semantic/Theme palette를 참조하며 기본 API로 HM 스
- Navigation: [단일 모드 정책](docs/guidelines/navigation-single-mode.md)
- Dashboard: [Dashboard Pattern](docs/patterns/dashboard-layout.md)
- CENTER: [CENTER 구현·사용 범위](docs/center-graphic/README.md)
- SW: [SW 패키지 README](packages/sw/README.md), [SW AI 사용 안내](packages/sw/ai/AI_USAGE.md)
- OrgChart: [최종 디자인 승인 기록](docs/org-chart/design-approval.md), [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md)
- 외부 프로젝트 설치: [Git Submodule + npm Workspace](docs/guidelines/external-install-submodule.md)
- 프로젝트·서비스 적용: [프로젝트 지침](docs/guidelines/프로젝트-지침.md), [개발 적용](docs/guidelines/개발-적용-가이드.md)
## Package Registry / Publish (별도 내부 운영 정보)
+16
View File
@@ -0,0 +1,16 @@
## HMDesign 디자인 시스템
<!-- HMDesign 패키지의 ai/AGENTS_SNIPPET.md에서 복사. 아래 "사용 유형"을 하나로 고치거나(SW는 모드까지: SW 일반 / SW 뷰어), 정하지 않았으면 그 줄을 지운다. -->
HMDesign 사용 유형: Web | SW 일반 | SW 뷰어 | OrgChart | 혼합(영역별로 적기, SW 영역은 모드까지)
화면·컴포넌트를 구현하거나 수정하기 전에 다음을 지킨다.
1. 설치된 HMDesign 패키지의 공통 가이드를 먼저 읽는다: `node_modules/@hm-design/<설치된 패키지>/ai/AI_GUIDE.md` (`ui`, `sw`, `org-chart`, `org-chart-react` 중 아무 사본이나 내용이 같다).
2. 위 "사용 유형"이 없고 요청으로도 정할 수 없으면 코드 작성 전에 "어떤 HMDesign 디자인 시스템을 사용하시겠습니까? 1. Web 2. SW (일반 모드 / 뷰어 모드) 3. OrgChart 4. 혼합 사용"을 묻는다. SW인데 모드(일반/뷰어)가 불명확하면 "SW 화면을 어떤 모드로 만들까요? 1. SW 일반 모드 2. SW 뷰어 모드"를 묻는다(근거는 `@hm-design/sw/ai/AI_USAGE.md` §0). 정해져 있으면 다시 묻지 않는다.
3. 선택한 패키지의 `ai/AI_USAGE.md` → `COMPONENT_CATALOG.md` → `DESIGN_RULES.md` → `COMPOSITION_PATTERNS.md` 순으로 필요한 절을 읽는다.
- Web: `node_modules/@hm-design/ui/ai/`
- SW: `node_modules/@hm-design/sw/ai/` (뷰어 모드는 `AI_USAGE.md` §0 다음 `VIEWER.md`)
- OrgChart: `node_modules/@hm-design/org-chart/ai/` (React는 `node_modules/@hm-design/org-chart-react/ai/AI_USAGE.md`도)
4. 선택한 유형·모드 안에서 기존 컴포넌트 → Props → 조합 → (없는 부분만) 그 모드의 토큰·스타일로 신규 → (참고 값도 없으면) HMDesign 공통 규칙과 조화되게 구성하고 "AI 구성 값"으로 보고하는 순서를 지킨다. 패키지 파일 수정·내부 경로 import·구현 복사는 하지 않는다.
5. Web / SW 일반 / SW 뷰어 / OrgChart 스타일을 영역별로 분리하고(한 SW 화면에 일반·뷰어 컴포넌트를 섞지 않음), 완료 후 AI_GUIDE의 결과 보고 형식으로 유형·모드, 재사용·신규·토큰·AI 구성 값·검증·NOT VERIFIED를 보고한다.
+275
View File
@@ -0,0 +1,275 @@
# HMDesign AI 작업 가이드 — 공통 진입점
> 이 파일은 HMDesign 패키지(`@hm-design/ui`, `@hm-design/sw`, `@hm-design/org-chart`, `@hm-design/org-chart-react`)의 `ai/AI_GUIDE.md`에 **같은 내용으로** 들어 있습니다. 원본은 HMDesign 저장소 `docs/ai/AI_GUIDE.md`이며 패키지 사본은 `npm run sync:ai-docs`로 갱신합니다. 사본을 직접 고치지 않습니다.
사용자가 "HMDesign 패키지를 기준으로 화면을 구현해 주세요", "HMDesign SW로 화면을 만들어 주세요"처럼 요청하면 AI 코딩 도구는 이 파일의 1~7단계를 순서대로 따릅니다. 이 문서와 각 패키지의 `ai/` 문서는 실제 공개 API·CSS에서 확인한 내용만 적습니다. 여기에 없는 컴포넌트·Props·토큰은 없는 것으로 취급합니다.
## 0. 읽는 순서
1. **이 파일**(어느 패키지의 사본이든 같음)
2. 선택한 패키지의 `ai/AI_USAGE.md` → `ai/COMPONENT_CATALOG.md` → `ai/DESIGN_RULES.md` → `ai/COMPOSITION_PATTERNS.md` (SW 뷰어 모드는 `AI_USAGE.md` §0 다음에 `ai/VIEWER.md`를 먼저 읽고 카탈로그의 뷰어 절을 봅니다)
3. API가 의심스러우면 그 패키지의 `dist/*.d.ts`(공개 진입점에서 export되는 것만)
| 유형 | 패키지 | 지침 위치(설치 후) | JS 진입점 | CSS |
|---|---|---|---|---|
| ① Web UI | `@hm-design/ui` | `node_modules/@hm-design/ui/ai/` | `@hm-design/ui`, `@hm-design/ui/theme` | `@hm-design/ui/tailwind.css` + `@hm-design/ui/theme.css`(Tailwind CSS 4 필요), 선택 `@hm-design/ui/dashboard-status.css` |
| ② SW UI — 일반 모드 | `@hm-design/sw` | `node_modules/@hm-design/sw/ai/` (`AI_USAGE.md` §0 모드 선택 → 일반 모드 문서) | `@hm-design/sw` | `@hm-design/sw/styles.css` |
| ② SW UI — 뷰어 모드 | `@hm-design/sw`(같은 패키지) | `node_modules/@hm-design/sw/ai/` (`AI_USAGE.md` §0 → `VIEWER.md`) | `@hm-design/sw` | `@hm-design/sw/styles.css`(같은 파일) |
| ③ OrgChart | `@hm-design/org-chart`(Core) + React면 `@hm-design/org-chart-react` | `node_modules/@hm-design/org-chart/ai/` (React 어댑터 안내는 `node_modules/@hm-design/org-chart-react/ai/AI_USAGE.md`) | `@hm-design/org-chart`, `@hm-design/org-chart-react`, 선택 `@hm-design/org-chart-react/hm-dialog` | `@hm-design/org-chart/engine.css`, `@hm-design/org-chart/style.css`(URL로 전달 — Shadow DOM 안에 로드) |
| 기타 | `@hm-design/center-graphic` | AI 지침 없음. 패키지 `README.md`만 | `@hm-design/center-graphic` | 자동 import |
모든 패키지는 서로 독립입니다(`@hm-design/org-chart-react`만 Core에 의존, `@hm-design/ui`는 `/hm-dialog`를 쓸 때만 선택 peer). React·React DOM `^19`는 peer입니다.
## 1단계. 사용 목적 확인
화면 구현 요청을 받으면 코드 작성 전에 **사용 유형**을 정합니다. 유형은 다음 넷이고, SW는 다시 두 모드로 나뉩니다.
1. Web
2. SW — **SW 일반 모드**(편집용: `SWHeader`, `SWPopup` …) / **SW 뷰어 모드**(보기 전용: `SWViewerHeader`, `SWViewerMenu`, `SWViewerPanel` …)
3. OrgChart
4. 혼합 사용(영역마다 위 유형 하나)
다음 순서로 판단하고, **판단되면 다시 묻지 않습니다.**
1. 소비 프로젝트의 `AGENTS.md`·`CLAUDE.md` 등에 `HMDesign 사용 유형:` 설정이 있으면 그대로 따릅니다(§9). 값은 `Web` / `SW 일반` / `SW 뷰어` / `OrgChart` / `혼합(영역별: …)`입니다. 값이 `SW`뿐이면 유형은 정해진 것이고 모드만 아래 4번으로 정합니다.
2. 사용자 요청이 유형을 분명히 말하면 그대로 따릅니다.
- Web: 웹 화면, 관리자·업무 화면, 목록·검색·Form, 대시보드, GNB/LNB, Dialog, Table …
- SW: 데스크톱 SW, 도면·CAD, SW Header, 툴바, 레이어/색상/선스타일, 속성창·SW Popup, 뷰어 …
- OrgChart: 조직도, 조직 계층·직원 카드, 조직 JSON/CSV/Excel 시각화 …
- 둘 이상이 함께 나오면 ④ 혼합(예: "업무 화면 안에 조직도" = Web + OrgChart).
3. 위로 정할 수 없으면 코드 작성 전에 **그대로** 묻습니다.
> 어떤 HMDesign 디자인 시스템을 사용하시겠습니까?
>
> 1. Web
> 2. SW (일반 모드 / 뷰어 모드)
> 3. OrgChart
> 4. 혼합 사용
4. **SW가 정해졌거나 포함됐는데 모드가 불명확하면** 코드 작성 전에 묻습니다. 설정 값(`SW 일반`/`SW 뷰어`)이나 요청이 모드를 분명히 말하면 묻지 않습니다. 판단 근거와 질문 문구는 `@hm-design/sw/ai/AI_USAGE.md` §0을 따릅니다. 유형 질문이 필요한 경우에는 같은 메시지에서 2번 선택 시의 모드도 함께 묻습니다(한 번에 끝냄).
> SW 화면을 어떤 모드로 만들까요?
>
> 1. SW 일반 모드 — 편집용 Header(`SWHeader`)·속성창·SW Popup
> 2. SW 뷰어 모드 — 보기 전용 Header(`SWViewerHeader`)·뷰어 메뉴·뷰어 패널
- "HMDesign SW로 화면을 만들어 주세요"처럼 SW만 말하고 모드 근거가 없으면 유형은 SW로 확정하고 4번의 모드 질문만 합니다.
- `package.json`에 설치된 패키지가 하나뿐이어도 그것만으로 유형을 확정하지 않습니다(설치가 덜 된 상태일 수 있음). 요청 내용과 맞을 때만 근거로 씁니다. SW 모드도 같은 패키지이므로 설치 상태로는 판단하지 않습니다.
- 정해진 유형·모드는 결과 보고(7단계)에 적고, 작업 중에 다른 유형·모드의 컴포넌트·스타일을 임의로 섞지 않습니다(5단계).
- 혼합을 고르면 영역별 패키지(예: "페이지 = Web, 가운데 영역 = OrgChart")를 확인합니다. SW 영역이 있으면 그 영역의 모드(일반/뷰어)까지 정합니다.
- **OrgChart가 포함되고 생성 방식이 없으면** 같은 메시지에서 다음도 묻습니다(이미 말했으면 묻지 않음): "조직도만 기존 페이지에 삽입할까요(Chart Only), 아니면 레이아웃까지 구성한 새 페이지 전체를 생성할까요(Full Page)?" 상세는 `@hm-design/org-chart/ai/AI_USAGE.md` §1.
- 선택한 패키지가 설치되어 있지 않으면 설치가 필요하다고 보고합니다. 다른 HMDesign 패키지나 임의 라이브러리로 대신하지 않습니다.
## 2단계. 패키지 분석
1. `node_modules/@hm-design/<패키지>/package.json`의 `version`·`exports`·`peerDependencies`를 확인합니다. `exports`에 없는 경로(`src/...`, `dist/components/...`)는 import하지 않습니다.
2. 그 패키지의 `ai/` 문서를 §0 순서로 읽습니다. 필요한 절만 읽고 전체 `dist`를 훑지 않습니다.
3. 카탈로그와 실제 `dist/index.d.ts`가 다르면 **`.d.ts`가 기준**입니다. 차이를 결과 보고에 적습니다.
4. 소비 프로젝트의 기존 CSS 진입점·폰트·빌드 도구(Vite 등)·이미 쓰는 디자인 시스템을 확인합니다. 기존 프로젝트의 디자인 시스템을 바꾸지 않습니다.
## 3단계. 화면 분석
요청 화면을 항목으로 나누고 항목마다 판단을 기록합니다.
| 요청 항목 | 판단 | 근거(카탈로그의 실제 이름) |
|---|---|---|
| 예: 상단 메뉴 | 재사용 | Web `GNB` + `GNBMobileMenu` |
| 예: 레이어 선택(SW 일반) | 재사용 | SW `SWHeaderLayerMenu` |
| 예: 상단 메뉴(SW 뷰어) | Props 설정 | SW `SWViewerHeader`의 `groups` |
| 예: 검색 상자(SW 일반) | 신규 | SW 카탈로그에 없음 → `SWHeaderInput` 재사용 + 일반 모드 토큰 |
판단 값은 **재사용**(그대로) / **Props 설정**(기존 Variant·Props) / **조합**(기존 컴포넌트 배치) / **신규**(소비 프로젝트에 최소 구현) 중 하나입니다.
## 4단계. UI 구현
우선순위(위가 먼저):
1. 선택 패키지의 기존 컴포넌트·Pattern을 그대로 사용
2. 기존 Variant / Props / Callback으로 해결
3. 기존 컴포넌트 조합(배치용 CSS만 소비 프로젝트에 작성)
4. 조합으로 안 되는 부분만 **소비 프로젝트에** 새로 구현 — 아래 "없는 컴포넌트 처리 3단계"
모든 단계는 **1단계에서 정한 유형·모드 안에서만** 찾습니다. SW 뷰어 모드는 뷰어 컴포넌트(`SWViewer*`)를, SW 일반 모드는 일반 컴포넌트(`SWHeader`·`SWPopup` …)를 먼저 씁니다. 다른 모드에 같은 역할의 컴포넌트가 있어도 가져오지 않습니다.
### 없는 컴포넌트 처리 3단계
| 단계 | 조건 | 할 일 | 결과 보고 |
|---|---|---|---|
| ① 재사용 | 선택 유형·모드에 컴포넌트·Variant·Props·조합이 있음 | 그대로 사용(위 1~3) | 재사용 / Props 설정 / 조합 |
| ② 같은 모드 스타일로 신규 | 컴포넌트는 없지만 그 모드의 토큰·확인값·상태 표현이 있음 | 소비 프로젝트에 최소 구현. 그 유형·모드의 토큰과 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 크기·색·상태 표현만 읽고, 안쪽에는 그 모드의 공개 컴포넌트를 재사용 | 신규 · 읽은 토큰 목록 |
| ③ HMDesign과 조화되는 새 스타일 | ②로 참고할 토큰·확인값이 없음 | HMDesign 공통 규칙(아래)과 선택 유형·모드의 가장 가까운 토큰을 기준으로 새 스타일을 구성 | 신규 · **"AI 구성 값 — 디자인 확인 필요"** 목록(속성·값·이유) |
- 토큰·확인값이 있으면 ②에서 반드시 그것을 씁니다. ③은 참고할 값이 정말 없을 때만이며, ③에서 만든 값은 확정 디자인이 아니라 확인 대상입니다. 화면 인상을 크게 바꾸는 값(주요 색·Header 높이 등)이 필요하면 구현 전에 사용자에게 먼저 확인합니다.
- 유형·모드별 구체 토큰과 예는 각 패키지 `ai/AI_USAGE.md`에 있습니다.
규칙:
- `node_modules` 안 패키지 파일을 수정하지 않습니다. 패키지 `dist` 구현이나 내부 CSS 블록을 복사해 같은 컴포넌트를 다시 만들지 않습니다.
- 이미 있는 컴포넌트와 같은 역할의 컴포넌트를 새로 만들지 않습니다(예: Web에 `Dialog`가 있는데 자체 Modal 작성 금지).
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
신규 항목마다 아래를 선택 유형·모드의 문서에서 찾아 그대로 적용합니다(②). 찾을 수 없는 항목은 ③으로 구성하고 "AI 구성 값 — 디자인 확인 필요"로 보고합니다.
| 항목 | Web(`@hm-design/ui`) | SW 일반 모드(`@hm-design/sw`) | SW 뷰어 모드(`@hm-design/sw`) | OrgChart |
|---|---|---|---|---|
| 토큰·CSS 변수 | `--hm-sem-*`, `--hm-color-*`, `--hm-radius-*`(`:root`) | `--hm-sw-header-*`, `--hm-sw-property-*`(`SWTheme` 범위) | `--hm-sw-viewer-*` + 두 Header가 공유하는 `--hm-sw-header-document-*`·`object-*`·`property-*`(SW 범위) | `--hm-orgchart-*` 공개 입력(Shadow DOM 상속) |
| 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 |
| 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) |
| Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 |
| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 |
| 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan |
| Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 |
| Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 |
| 동작·키보드 | Radix 동작 유지 | 카탈로그 "공통 키보드" | VIEWER.md `SWViewerMenu`·`SWViewerPanel` 키보드 | Core 키보드(Pan, Alt+M 이동 등) |
## 5단계. 패키지별 디자인 분리 (혼합 사용 포함)
각 패키지의 스타일 범위는 다음과 같습니다(실제 CSS 기준).
| 패키지 | 스타일이 적용되는 범위 | 주의 |
|---|---|---|
| Web | `theme.css`의 토큰은 `:root, [data-hm-theme]`. `tailwind.css`는 Tailwind base에서 `*`(border·outline), `body`(배경·글자), `html`(글꼴)을 **문서 전체에** 적용. 앱이 import하는 `tailwindcss`의 Preflight도 전역 | Web CSS를 넣은 문서에서는 다른 영역에도 전역 초기화가 적용됨 |
| SW(일반·뷰어 공통) | 토큰은 `.hm-sw-header`와 `.hm-sw-theme`에만 정의(뷰어 토큰 `--hm-sw-viewer-*`도 같은 범위). 전역 선택자 없음. `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`은 자기 root에 `hm-sw-theme`를 가짐. 토큰 기본값은 가장 바깥 SW 범위에서만 선언되고 안쪽 SW 범위는 상속(변경은 가장 바깥 범위의 `style`, SW `AI_USAGE.md` §3.1) | 일반 모드의 Header 밖 SW 부품은 반드시 `<SWTheme>` 안 |
| OrgChart | Core가 자기 open Shadow DOM에만 CSS 로드. 호스트 CSS selector는 안쪽에 닿지 않고 **Custom Property 상속만** 통과 | 호스트 class로 카드 모양을 바꾸려 하지 않음 |
혼합 규칙:
1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 Noto Sans KR(`--hm-sw-header-font-family`, 2026-10-20 확정, Dotum 기본값 철회), Web 기본 글꼴도 Noto Sans KR(`--hm-font-family-base`)이지만 토큰은 각자 쓰고, OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 토큰·값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
- OrgChart Full Page의 바깥 레이아웃 = Web(또는 요청된 패키지)
- OrgChart React의 직원 팝업 = 선택 `@hm-design/org-chart-react/hm-dialog`(Web `Dialog`, Web Theme CSS 필요)
3. 패키지 class(`hm-sw-*`, `hm-orgchart-*`, Web `data-slot`)를 소비 코드에서 덮어쓰지 않습니다. 배치는 `className`·`style`과 소비 프로젝트 class로 합니다.
4. **Web + SW를 한 문서에 함께 쓰면** Web의 전역 base(위 표)가 SW 화면에도 적용됩니다. SW는 자기 범위에서 버튼 등을 초기화하지만 이 조합의 시각 결과는 패키지에서 검증된 적이 없습니다(NOT VERIFIED). 이 조합이면 브라우저에서 SW 부품의 글꼴·테두리·배경을 직접 확인하고 차이를 보고합니다. 가능하면 Web CSS가 없는 SW 전용 화면(별도 entry)으로 분리합니다.
5. Web Scoped Theme(`[data-hm-theme]`)는 body Portal(Select·Popover·DatePicker 팝업)에 전달되지 않습니다. Web 영역만 다른 테마로 바꾸려면 Global Theme를 쓰거나 차이를 보고합니다.
### SW 일반 모드 ↔ 뷰어 모드 분리
SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지만 **서로 다른 디자인**입니다. 한 화면(한 Header 영역)에는 한 모드만 씁니다.
1. 일반 모드 화면: `SWHeader`·개별 컨트롤·`SWPopup`·`SWHeaderPropertyPanel`·`SWToolButton`/`SWToolGroup`. `SWViewer*` 컴포넌트와 `--hm-sw-viewer-*` 토큰을 쓰지 않습니다.
2. 뷰어 모드 화면: `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`과 뷰어 패널 컨트롤(`SWViewerSection`·`SWViewerField` …). `SWHeader`에 뷰어 메뉴를 억지로 넣거나 `SWPopup`을 뷰어처럼 재스타일하지 않습니다.
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
1. Typecheck(`tsc --noEmit` 등)와 Production Build
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭과 항목 `collapseBelow`·`iconOnlyBelow` 경계 전후(More 이동·아이콘만 표시), OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
```md
## HMDesign 적용 결과
- 사용 유형: Web | SW 일반 | SW 뷰어 | OrgChart(Chart Only|Full Page) | 혼합(영역별: …)
- 유형·모드 판단 근거: 프로젝트 설정 | 요청 문구 "…" | 사용자 답변
- 패키지·버전: @hm-design/ui 0.1.0, …
- 재사용한 컴포넌트: …(Props 설정 포함)
- 조합한 부분: …
- AI가 새로 구현한 부분: 파일 경로 · 이유(카탈로그에 없음) · 안쪽에서 재사용한 컴포넌트
- 적용한 디자인 토큰: --hm-… (신규 UI가 읽은 것)
- AI 구성 값 — 디자인 확인 필요: 없음 | 속성 · 값 · 이유(4단계 ③)
- 카탈로그와 .d.ts 차이: 없음 | …
- 검증: Typecheck PASS · Build PASS · 브라우저(폭/상태) … · NOT VERIFIED: …
- 제안(구현하지 않음): …
```
## 8. 상황별 예시
### 예시 A — Web 화면 (목록 + 검색)
- 읽기: `node_modules/@hm-design/ui/ai/` → COMPOSITION_PATTERNS "목록 화면"
- 재사용: `LNB`(또는 `GNB`) 중 **하나**, `PageHeader`, `Card`, `FilterBar` + `FilterBarSearch` + `FilterBarActions`, `DataTable` + `useDataTable`, `Button`
- 신규(필요 시만): 카탈로그에 없는 콘텐츠 블록 → 소비 프로젝트 컴포넌트, `--hm-sem-*` 토큰과 `Card` 재사용
```css
/* src/index.css */
@import "tailwindcss";
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
```
```tsx
import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch, PageHeader, PageHeaderContent, PageHeaderTitle } from "@hm-design/ui"
```
### 예시 B0 — "HMDesign SW로 화면을 만들어 주세요" (모드 미지정)
- 유형은 SW로 확정, 모드 근거가 없으므로 코드 작성 전에 1단계 4번의 모드 질문만 합니다. 답에 따라 B1 또는 B2로 진행합니다.
- 소비 프로젝트 설정이 `HMDesign 사용 유형: SW 일반` 또는 `SW 뷰어`이면 묻지 않고 바로 B1/B2입니다.
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
```tsx
import { SWHeader, SWPopup, SWTheme, SWToolButton, SWToolGroup } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
```
### 예시 B2 — SW 뷰어 모드 (뷰어 Header + 메뉴 + 패널)
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
```
### 예시 C — 조직도만 (Chart Only)
- 읽기: `node_modules/@hm-design/org-chart/ai/` → COMPOSITION_PATTERNS "1. Chart Only"
- 재사용: React `OrgChart`(`@hm-design/org-chart-react`) 또는 Vanilla `createOrgChart`, 데이터 `JsonOrgChartDataSource` + `validateOrgChartDataset`
- 필수 옵션: `layoutMode="auto"` + `presentationMode="chart-only"`(초기 Fit·직접 휠 Zoom이 기본)
- **추가하지 않음:** 페이지 제목·통계·설명·Zoom 버튼 바·외부 회색 배경·Navigation. Fixed Chart Only는 미지원(`TypeError`)
```tsx
import { OrgChart } from "@hm-design/org-chart-react"
import { JsonOrgChartDataSource } from "@hm-design/org-chart"
import engineUrl from "@hm-design/org-chart/engine.css?url" // Vite. 다른 도구는 실제 배포 URL
import coreUrl from "@hm-design/org-chart/style.css?url"
```
### 예시 D — Web + OrgChart (Full Page)
- 영역: 페이지 레이아웃·제목·필터 = Web, 조직도 영역 = OrgChart
- 읽기: Web `ai/` + OrgChart `ai/COMPOSITION_PATTERNS.md` "2. Full Page"
- 재사용: Web `LNB`/`GNB` 중 하나, `PageHeader`, `Card`, `Select`·`Label`(회사 선택 등 요청된 컨트롤만) + OrgChart `OrgChart`(`selectedCompanyIds` 등 Props로 연결)
- 직원 팝업을 Web Dialog로: `import { OrgChart } from "@hm-design/org-chart-react/hm-dialog"` + `employeePopup="hm-dialog"`(Web CSS 진입점 필요)
- 분리: 조직도 카드·색은 OrgChart 기본 유지. Web 토큰을 조직도에 넣지 않음(바꿔야 하면 `--hm-orgchart-*` 공개 입력만)
## 9. 소비 프로젝트 설정 — AI가 이 지침을 찾게 하기
AI 코딩 도구는 보통 **프로젝트 루트의** `AGENTS.md`(Codex 등)·`CLAUDE.md`(Claude Code)·도구별 규칙 파일만 자동으로 읽고 `node_modules`는 읽지 않습니다. 패키지 설치만으로 지침이 자동 적용되지는 않으므로 소비 프로젝트에서 한 번 연결합니다.
1. 패키지에 들어 있는 `ai/AGENTS_SNIPPET.md`를 루트 `AGENTS.md`(없으면 새로 만듦)에 붙여 넣고 `HMDesign 사용 유형:` 줄을 프로젝트에 맞게 고칩니다. 값은 `Web` / `SW 일반` / `SW 뷰어` / `OrgChart` / `혼합(영역별: 예 "페이지 = Web, 중앙 = OrgChart")` 중 하나입니다. SW 프로젝트면 모드까지 적어야 AI가 모드를 다시 묻지 않습니다(`SW`만 적으면 모드를 묻습니다). 정하지 않았으면 그 줄을 지워 AI가 1단계에서 묻게 합니다.
```bash
cat node_modules/@hm-design/ui/ai/AGENTS_SNIPPET.md >> AGENTS.md
```
(PowerShell: `Get-Content node_modules/@hm-design/ui/ai/AGENTS_SNIPPET.md | Add-Content AGENTS.md`. 설치한 아무 HMDesign 패키지의 사본이나 같습니다.)
2. Claude Code는 `CLAUDE.md`에 `@AGENTS.md` 한 줄을 두면 같은 내용을 읽습니다. 다른 도구(Cursor 규칙 등)는 그 도구의 규칙 파일에 같은 내용을 넣습니다.
3. 패키지를 업데이트한 뒤 스니펫이 바뀌었는지 확인합니다(스니펫은 경로 안내만 하므로 보통 그대로 둡니다).
자동으로 되지 않는 것: 패키지 설치 시 루트 파일 생성(`postinstall` 없음), 도구가 `node_modules`를 스스로 탐색하는 것. 위 연결이 없으면 사용자가 "node_modules/@hm-design/…/ai/AI_GUIDE.md를 먼저 읽어"라고 요청해야 합니다.
**Git Submodule + npm Workspace로 연결한 경우**(Registry 없이 HMDesign 저장소를 예: `HMDesign/` 폴더에 Submodule로 고정하고 `workspaces`에 `HMDesign/packages/<패키지>`를 등록한 구성): `node_modules/@hm-design/<패키지>`는 `HMDesign/packages/<패키지>`로 연결되므로 지침 위치는 위 표와 같습니다(`HMDesign/packages/<패키지>/ai/`와 같은 파일). 공개 진입점은 `dist/`를 가리키므로 HMDesign 폴더 안에서 `npm ci`와 `npm run build:packages`를 먼저 실행해야 하며, `dist/`가 없으면 Import 오류를 컴포넌트 결함으로 보고하지 말고 빌드 필요로 보고합니다. HMDesign 저장소 루트의 `AGENTS.md`·`CLAUDE.md`는 HMDesign 개발용이므로 소비 프로젝트 지침으로 쓰지 않습니다.
## 10. 문서 갱신 (HMDesign 저장소 유지보수자)
컴포넌트를 추가·수정하면 같은 변경에서 해당 패키지 `ai/COMPONENT_CATALOG.md`와 필요한 `DESIGN_RULES.md`·`COMPOSITION_PATTERNS.md`를 갱신하고 `npm run check:ai-docs`를 통과시킵니다. 절차는 저장소 `docs/ai/MAINTAINING.md`(패키지 미포함)에 있습니다.
+52
View File
@@ -0,0 +1,52 @@
# HMDesign 패키지 AI 지침 유지보수
이 문서는 HMDesign 저장소 유지보수자용이며 패키지에 포함되지 않습니다. 패키지에 포함되는 AI 지침은 소비 프로젝트의 AI 코딩 도구가 읽는 **실행 규칙**이므로, 코드와 다른 내용이 남지 않게 아래 절차로 함께 갱신합니다.
## 1. 구조
| 위치 | 역할 | 패키지 포함 |
|---|---|---|
| `docs/ai/AI_GUIDE.md` | 공통 진입점 원본(사용 유형 Web / SW 일반·뷰어 / OrgChart / 혼합 선택, 없는 컴포넌트 처리 3단계, 1~7단계, 패키지·SW 모드 분리, 예시 A~D, 소비 프로젝트 설정) | 사본으로 포함 |
| `docs/ai/AGENTS_SNIPPET.md` | 소비 프로젝트 루트 `AGENTS.md`에 붙일 연결 문구 원본 | 사본으로 포함 |
| `docs/ai/MAINTAINING.md` | 이 문서 | 미포함 |
| `packages/ui/ai/` | Web: `AI_USAGE`, `COMPONENT_CATALOG`, `DESIGN_RULES`, `COMPOSITION_PATTERNS` | 포함(`files`) |
| `packages/sw/ai/` | SW: 위 4종(`AI_USAGE.md` §0 = SW 일반/뷰어 모드 선택) + 뷰어 모드 전용 `VIEWER.md` + `examples/`(A~F 일반 모드, H 뷰어 모드, J 테마 설정) | 포함 |
| `packages/org-chart/ai/` | OrgChart(Core + React 어댑터 API): 위 4종 | 포함 |
| `packages/org-chart-react/ai/` | `AI_USAGE`(Core `ai/` 안내) | 포함 |
| `scripts/ai-docs.mjs` | 동기화(`--sync`)·검사(`--check`) | — |
`@hm-design/center-graphic`에는 AI 지침이 없습니다(공통 가이드에 "README만"으로 안내). 추가하려면 같은 4종 구조로 만들고 `scripts/ai-docs.mjs`의 `packages`에 등록합니다.
저장소 루트 `AGENTS.md`·`CLAUDE.md`는 **이 저장소를 개발하는 AI**용 규칙이고, `packages/*/ai/`는 **패키지를 설치해 쓰는 소비 프로젝트의 AI**용 규칙입니다. 내용이 겹쳐도 서로를 대신하지 않습니다. 패키지 `ai/` 문서는 패키지 밖(`../../docs/…`)을 링크하지 않습니다(설치 후 존재하지 않음).
## 2. 컴포넌트를 추가·수정할 때
같은 변경(같은 Commit)에서 처리합니다.
1. **공개 Export가 바뀌면** 해당 패키지 `ai/COMPONENT_CATALOG.md`에 이름·용도·HM Props(기본값)를 추가·수정·삭제합니다. 내부 전용 이름은 "공개하지 않은 것" 절에만 적습니다.
2. **Props·Variant 기본값·허용 조합이 바뀌면** 카탈로그 표와 `COMPOSITION_PATTERNS.md`의 예시 코드를 함께 고칩니다.
3. **토큰·CSS 값이 바뀌면** `DESIGN_RULES.md`의 표를 실제 CSS 선언과 맞춥니다(값은 CSS에서 복사, 추측 금지).
4. **새 화면 유형·조합 규칙이 생기면** `COMPOSITION_PATTERNS.md` §0 표에 행을 추가합니다. SW 뷰어 모드 컴포넌트(`SWViewer*`)를 바꾸면 `VIEWER.md`와 카탈로그의 뷰어 절을 함께 고치고, 일반/뷰어 모드 구분이 바뀌면 `packages/sw/ai/AI_USAGE.md` §0과 공통 `AI_GUIDE.md` 1단계·5단계를 같이 맞춥니다.
5. 공통 규칙(유형·SW 모드 선택·혼합·보고 형식)이 바뀌면 `docs/ai/AI_GUIDE.md`만 고치고 `npm run sync:ai-docs`로 사본을 갱신합니다. 패키지 사본을 직접 고치지 않습니다.
6. `npm run check:ai-docs`를 실행해 PASS를 확인합니다.
7. 버전을 올리면 카탈로그 첫 줄의 기준 버전을 같이 바꿉니다.
## 3. `npm run check:ai-docs`가 검사하는 것
1. `docs/ai`의 공유 파일과 각 패키지 사본이 같은지(줄바꿈 CRLF/LF 차이는 무시 — `core.autocrlf=true`에서 `packages/sw`만 `.gitattributes`로 LF checkout)
2. 각 패키지 `package.json`의 `files`에 `ai`가 있는지
3. 공개 진입점(`src/index.ts` 등, `export *` 재귀 포함)의 **모든 Export 이름이 카탈로그에 나오는지**
4. 카탈로그에 Export처럼 보이지만(기존 Export 이름으로 시작) 실제로는 없는 이름이 없는지 — 예: `DialogFancyPanel`. "공개하지 않은 것"·"없는 것"·"지원하지 않는" 절은 제외
5. `ai/` 문서의 상대 링크가 존재하고 패키지 밖(저장소 전용 문서)을 가리키지 않는지
검사하지 않는 것: Props 이름·기본값·토큰 값의 정확성, 예시 코드의 컴파일. 이 부분은 리뷰와 소비 프로젝트 검증(§4)으로 확인합니다.
## 4. 소비 프로젝트 검증 (배포 전 권장)
1. `npm run build:packages` 후 `npm pack -w <패키지> --ignore-scripts --pack-destination <임시 폴더>`
2. 임시 소비 프로젝트(React 19 + Vite + Tailwind CSS 4)에 tarball 설치
3. `node_modules/@hm-design/*/ai/AI_GUIDE.md`가 있는지, 링크가 설치 위치에서 열리는지 확인
4. `COMPOSITION_PATTERNS.md`의 예시를 옮겨 `tsc --noEmit`·`vite build`·브라우저 렌더링 확인
5. 결과와 NOT VERIFIED를 기록
`npm pack`의 `prepack`은 build를 다시 실행합니다. 공유 `dist`를 쓰는 build를 동시에 돌리지 않습니다.
+81
View File
@@ -0,0 +1,81 @@
# 패키지 AI 지침 구축 검증 기록 (2026-10-19)
범위: 공통 진입점(`docs/ai/AI_GUIDE.md` → 각 패키지 `ai/AI_GUIDE.md`), Web(`packages/ui/ai/`)·OrgChart(`packages/org-chart/ai/`, `packages/org-chart-react/ai/`) 신규 문서, SW 기존 `ai/` 연결, 패키지 `files`에 `ai` 추가, `scripts/ai-docs.mjs`. 런타임 코드·CSS·Storybook·공개 API 변경 없음.
## 실행한 검증
| 항목 | 결과 |
|---|---|
| `npm run check:ai-docs` (사본 일치, `files`, 카탈로그 ↔ 공개 Export 양방향, `ai/` 링크) | PASS |
| 검사기 음성 테스트(Export 누락, 가짜 이름 `DialogFancyPanel`, 사본 불일치, 패키지 밖 링크를 임시 주입) | 4건 모두 FAIL로 검출 후 원복, 재검사 PASS |
| `npm run build:packages` | PASS |
| `npm pack --ignore-scripts` 4종(ui 0.1.0, sw 0.2.0, org-chart 0.1.0, org-chart-react 0.1.0) | 모든 tarball에 `package/ai/AI_GUIDE.md`·`AGENTS_SNIPPET.md`와 패키지별 문서 포함 |
| 독립 소비 프로젝트(React 19.3.0, Vite 8.3.0, Tailwind CSS 4.3.3, TypeScript 7.0.2)에 tarball 설치 | `node_modules/@hm-design/*/ai/` 4곳 존재, 설치 위치 기준 상대 링크 42개 모두 해석, `AI_GUIDE.md` 4사본 동일 |
| `AI_GUIDE.md` §9의 스니펫 연결 명령(`cat … >> AGENTS.md`) | 동작 |
| 문서만 근거로 구현: Web+OrgChart 페이지(LNB ONLY, PageHeader, 신규 StatCard = Card + 토큰, FilterBar + DataTable, AlertDialog, OrgChart Full Page + `/hm-dialog`), Chart Only 미리보기, SW 뷰어(SWHeader + SWToolGroup + SWPopup, 별도 entry) | `tsc --noEmit`(strict, skipLibCheck false) PASS, `vite build` PASS |
| 브라우저(1440px) | HM 외형 렌더링(글꼴 Noto Sans KR, Card·Button 토큰 값), 조회 1건 → 초기화 3건, 삭제 확인창 열림·Escape 닫힘·Trigger Focus 복귀, 조직도 직원 카드 → Web Dialog 표시·Escape 닫힘, 회사 Select → `selectedCompanyIds` 반영, 콘솔 오류 0, 가로 넘침 없음 |
| 브라우저(390px) | LNB Desktop 숨김·LNB Mobile Header 표시, 문서 가로 넘침 없음 |
| Chart Only(1280px) | 흰 캔버스 + 노드 + 연결선만, 외부 제목·버튼·요약 없음, 중앙 Fit |
| SW 뷰어 | Popup 열림·내부 Focus·Escape 닫힘·Trigger 복귀, `--hm-sw-header-*`가 `body`에는 없고 `.hm-sw-theme` 범위에만 존재 |
검증 프로젝트는 세션 임시 폴더에서 수행했고 저장소에 추가하지 않았다.
## NOT VERIFIED
- 다른 AI 코딩 도구(Codex·Cursor 등)가 소비 프로젝트의 `AGENTS.md`만 보고 스스로 `ai/` 문서를 찾아가는지. 이번 시나리오는 이 문서를 작성한 같은 AI가 수행했으므로 독립 Cold-start 검증이 아니다.
- Web CSS와 SW를 **한 문서**에서 함께 쓰는 경우의 시각 결과(문서에 NOT VERIFIED로 명시, 별도 entry 권장).
- 768px 화면, 실제 터치·스크린리더, Registry publish 후 설치.
- 문서의 Props 기본값·토큰 값 전부를 기계적으로 대조하지는 않았다(검사기는 Export 이름·링크·사본만 검사). 이번 작성은 소스를 직접 확인했다.
## 추가 검증 — SW 일반 / 뷰어 모드 구분 (2026-10-19)
범위: `feature/sw-viewer-header`(68fbcf6)를 merge(be7a04a)한 뒤, 사용 유형을 Web / SW(일반·뷰어) / OrgChart / 혼합으로 나누고 SW 모드 선택·혼합 금지·없는 컴포넌트 처리 3단계를 공통 가이드와 각 패키지 `ai/` 문서에 반영. 런타임 코드·CSS·Storybook·공개 API 변경 없음. 위 기록의 "SW 뷰어(SWHeader + SWToolGroup + SWPopup)"는 당시 명칭이며, 현재 기준으로는 **일반 모드 Header + 도구 막대** 구성이다(뷰어 모드 = `SWViewer*`).
| 항목 | 결과 |
|---|---|
| `npm run sync:ai-docs` → `npm run check:ai-docs` (뷰어 Export 33개 포함 카탈로그 ↔ 공개 Export) | PASS |
| `npm run build -w packages/sw` 후 `npm pack --ignore-scripts` 4종을 임시 소비 폴더 `node_modules/@hm-design/*`에 풀어 확인 | sw tarball에 `ai/VIEWER.md`·`examples/ExampleH-ViewerMode.tsx` 포함, `dist/index.d.ts`가 `sw-viewer`를 `export *` |
| 뷰어 문서(VIEWER.md, 카탈로그 §8, DESIGN_RULES §11, PATTERNS 7, AI_USAGE §0)의 Props·토큰 이름 ↔ `sw-viewer` 소스·`sw-viewer.css` | 일치(남은 차이는 예제 변수명과 "없는 Props" 목록뿐) |
| 설치 위치 기준 시나리오 정적 검사 124건: 공통(사본 동일·상대 링크), Web(설정 → `ui/ai`, 다른 영역 이관, 3단계), SW 미지정("HMDesign SW로 화면을 만들어 주세요" → 유형 확정 + 모드 질문만, 애매하면 추측 금지), SW 일반(설정 → 묻지 않음, `SWViewer*` 금지, 예제 B·F 명칭), SW 뷰어(설정·요청 근거 → `VIEWER.md`, `SWHeader`/`SWPopup` 재스타일 금지, 주요 8개 컴포넌트가 카탈로그와 설치 `.d.ts`에 존재), OrgChart(Chart Only / Full Page 질문 1회, 승인 디자인 유지), 혼합(SW 영역 모드, 보고 형식) | 124 PASS / 0 FAIL |
### Cold-start 검증(`be7a04a` 기준) 지적 사항 반영
| 지적 | 현재 작업본 |
|---|---|
| `check:ai-docs` FAIL — sw 사본 `AI_GUIDE.md`·`AGENTS_SNIPPET.md` 불일치 | 원인은 내용이 아니라 줄바꿈: `core.autocrlf=true`에서 `docs/ai`·ui·org-chart는 CRLF, `packages/sw`(`.gitattributes` `eol=lf`)는 LF로 checkout. `scripts/ai-docs.mjs` 사본 비교를 줄바꿈 무시로 변경. 작업본 스냅샷을 `git archive`(CRLF checkout 재현)로 풀어 검사 PASS, 실제 내용 차이를 넣으면 FAIL 검출 |
| `check:ai-docs` FAIL — SW 카탈로그에 `SWViewer*` 33개 누락 | 카탈로그 §8에 33개 모두 등재, 검사 PASS |
| 카탈로그 "표에 없는 이름은 공개 API 아님"과 VIEWER.md 충돌 | §8 등재로 해소, 머리말 "무엇을 쓸까"에 일반/뷰어 분기 |
| "도면 뷰어" 요청 시 예시 B(`SWHeader`+`SWPopup`)와 VIEWER.md가 다른 답 | 공통 1단계 4번·예시 B0/B1/B2, SW `AI_USAGE.md` §0(판단 순서·질문 문구·혼합 금지), 예제 B·F는 일반 모드로 명시 |
| `SWPopup` `anchorRef` 사용 시 열릴 때 Focus 미이동 | **이번 AI 지침 작업 범위 밖 — 별도 수정 대상.** 코드 수정하지 않음 |
### 추가 검증 — 코드 예제 (변경분만, 기존 PASS 재사용)
| 항목 | 결과 |
|---|---|
| 임시 소비 프로젝트(설치 tarball, React 19.3.0, TypeScript 7.0.2, Vite 8.3.0)에서 `COMPOSITION_PATTERNS.md` 7 코드 조각과 `VIEWER.md` §5 코드 조각을 **본문 그대로** 감싸고 자유 변수만 선언, 예제 H 포함 | `tsc --noEmit`(strict, skipLibCheck false) PASS, `vite build` PASS |
| 브라우저 1440px — PATTERNS 7 | 샘플 메뉴 열림·선택 → 파일명 표시, 도면 공간 탭 전환(`space=layout`), 설정 패널 열림 시 패널 안 Focus·Escape 닫힘·토글로 Focus 복귀, 정보 `modal` Focus·확인 닫힘, 가로 넘침 없음, 글꼴 Noto Sans KR, 콘솔 오류 0 |
| 브라우저 1440px — VIEWER.md §5 | 파일명 표시, 전체 보기 명령, 설정 패널 Focus, Switch 토글·Label 연결 |
| 800px 폭 | 항목이 More(⋯)로 이동(반응형 동작 확인, 세부 경계값 미측정) |
### 최종 독립 Cold-start 검증 — PASS (`7621934` 기준)
**독립 세션의 보고 결과**를 옮겨 적은 것이다(이 문서를 작성한 세션이 다시 실행한 결과가 아님). 기준 Commit `7621934f69a0324041ea59137b764117841d44b0`, 독립 세션 scratchpad의 격리 사본(`git archive 7621934`)·tarball 4종·소비 프로젝트에서 수행, 원본 폴더는 읽기만 했다. `be7a04a`→`7621934` 변경은 AI 문서와 `scripts/ai-docs.mjs` 25개 파일뿐이라 패키지 빌드·Web·OrgChart 코드 PASS는 `be7a04a` 결과를 재사용했다(두 tarball은 `ai/*.md`만 다름).
| 항목 | 판정 | 근거(독립 세션 보고) |
|---|---|---|
| AI 문서 동기화 | PASS | `check:ai-docs` 통과(`be7a04a`의 FAIL 3건 해소). 4개 사본 `AI_GUIDE.md`는 줄끝을 LF로 맞추면 SHA-256 `1a1e0f98727f206191eadcea524ef2aa81d92e30c0a5d4fdbae635d0fbb46420`로 동일(원본 바이트는 ui·org-chart·org-chart-react CRLF, sw LF) |
| Viewer Export 33개 | PASS | 설치 `.d.ts`에서 추출해 카탈로그와 대조: 누락 0, 33개 모두 §8 |
| SW 일반/뷰어 모드 선택 | PASS | "도면 뷰어" 요청은 SW `AI_USAGE.md` §0.1 근거로 질문 없이 뷰어 모드. 모드 근거가 없는 요청은 모드만, 애매한 요청은 묻도록 되어 있음(문서 판단) |
| 컴포넌트 검색 | PASS | 뷰어 문서 순서만으로 필요한 API를 모두 찾음, 추측 API 없음, Typecheck 오류 0 |
| 신규 UI 구현 규칙(3단계) | PASS | 간격은 `--hm-sw-viewer-gap` 토큰, 토큰 없는 결과 행 정렬만 "AI 구성 값 — 디자인 확인 필요"로 보고, 뷰어 화면에 일반 모드 컴포넌트 혼입 없음(브라우저 확인) |
실행: Typecheck·Build(4개 화면 entry) PASS. 뷰어 검색 패널 열림 Focus·검색·결과 선택·Escape 닫힘·Focus 복귀, 폭 1181 / 1179 / 901 / 899px에서 새로고침 기준 More 이동과 More에서 패널 열기 확인.
### NOT VERIFIED
- 실시간 `ResizeObserver` 동작: 창 폭을 바꾸는 도중의 More 재배치. 검증용 내장 브라우저의 크기 조정에서 `ResizeObserver`가 호출되지 않아 판정 불가, 새로고침 기준 동작만 확인(독립 세션 보고).
- 질문 규칙의 실제 대화 흐름(질문 → 답변 → 구현). 독립 검증도 문서 판단으로만 확인했다.
- 독립 검증에서 쓰지 않은 뷰어 기능: 단독·우클릭 `SWViewerMenu`, `iconOnlyBelow`(Switch·Slider·modal 패널은 위 코드 예제 검증에서 확인).
- SW 일반/뷰어 모드 전환을 소비 프로젝트에서 실제로 구성한 화면, SW 화면 + 조직도 조합.
- Web·OrgChart의 기존 NOT VERIFIED(Web 1280px, Hover 세부, OrgChart 키보드 Pan)는 그대로.
- `SWPopup` + `anchorRef` 열림 Focus 결함: 별도 세션에서 수정 중(이 기록의 범위 밖).
@@ -0,0 +1,101 @@
# 외부 프로젝트 설치 — Git Submodule + npm Workspace
> 2026-10-20 작성. 현재 HMDesign을 다른 React 프로젝트에서 쓰는 공식 방식입니다. npm Registry publish 없이 Git 저장소를 Submodule로 연결하고, HMDesign 안에서 패키지를 빌드한 뒤 소비 프로젝트의 npm Workspace로 연결합니다.
> 실제 적용 근거: 외부 DWG/DXF Viewer(React 19, `@hm-design/ui` + `@hm-design/sw`)가 이 절차로 연결되었습니다. 패키지 tarball 소비 검증은 `packages/sw/tests/integrated-consumer.mjs`가 별도로 수행합니다.
## 1. 전제
| 항목 | 기준 |
|---|---|
| Node.js / npm | `package-lock.json`의 엔진 조건 `^20.19.0 \|\| >=22.12.0`(Node 21 제외). 검증 기록: Node 20.20.1 / npm 10.8.2(2026-10-11), Node 22.23.2 / npm 10.9.8(2026-10-20) |
| React | 소비 프로젝트 `react`·`react-dom` `^19.0.0`(모든 패키지의 peer) |
| 네트워크 | 최초 설치에 Gitea(`https://gitea.hmac.kr/b25027/HMDesign.git`)와 npm Registry 접근 필요 |
| Git | 패키지 `dist/`는 Git에 포함되지 않습니다(`.gitignore`). Clone·Commit 변경 후에는 항상 HMDesign 안에서 다시 빌드합니다. |
## 2. Submodule 연결과 검증 Commit 고정
소비 프로젝트 루트에서 실행합니다. `<검증 Commit>`은 사용할 HMDesign Commit의 전체 SHA입니다(브랜치 이름을 그대로 쓰지 않습니다).
```bash
git submodule add https://gitea.hmac.kr/b25027/HMDesign.git HMDesign
git -C HMDesign fetch origin
git -C HMDesign checkout <검증 Commit>
git add .gitmodules HMDesign
git commit -m "chore: pin HMDesign <검증 Commit>"
```
- 소비 프로젝트에 기록되는 것은 Submodule Commit 포인터입니다. 다른 사람·CI는 `git submodule update --init --recursive`로 같은 Commit을 받습니다.
- Submodule 안에서 HMDesign 파일을 직접 수정하지 않습니다. `git status`에 `HMDesign (modified content)` / `-dirty`가 보이면 고정 Commit과 다른 코드로 빌드된 상태입니다. 그 상태의 검증 결과는 해당 Commit의 결과로 보고하지 않습니다.
## 3. HMDesign 안에서 설치·빌드
```bash
git submodule update --init --recursive
cd HMDesign
npm ci
npm run build:packages
cd ..
```
- `npm ci`는 HMDesign 자체 빌드 도구(Vite·TypeScript·Tailwind 등)를 `HMDesign/node_modules`에 설치합니다. 패키지 빌드 스크립트가 이 위치의 도구를 사용하므로 생략할 수 없습니다.
- `build:packages`는 `org-chart → ui → center-graphic → org-chart-react → sw` 순서로 5개 패키지의 `dist/`를 만듭니다. 필요한 패키지만 빌드할 때는 `npm run build:ui`, `npm run build:sw`를 쓸 수 있고, `org-chart-react`는 `org-chart`를 먼저 빌드해야 합니다.
## 4. 소비 프로젝트 Workspace 연결
소비 프로젝트 `package.json`에 사용할 패키지만 Workspace와 dependency로 추가합니다.
```json
{
"workspaces": ["HMDesign/packages/ui", "HMDesign/packages/sw"],
"dependencies": {
"@hm-design/ui": "*",
"@hm-design/sw": "*",
"react": "19.3.0",
"react-dom": "19.3.0"
}
}
```
```bash
npm install
```
- `npm install` 후 `node_modules/@hm-design/<패키지>`는 `HMDesign/packages/<패키지>`를 가리키는 링크입니다. Import는 패키지 `exports`의 공개 진입점만 사용합니다(`dist`·`src` 내부 경로 금지).
- OrgChart를 쓰면 `HMDesign/packages/org-chart`(와 React에서는 `HMDesign/packages/org-chart-react`)를 추가합니다.
- Vite 소비 프로젝트는 HMDesign 저장소 안의 예제·Export용 HTML을 의존성 스캔에서 제외하도록 `optimizeDeps.entries: ['index.html']`를 지정합니다(외부 Viewer에서 확인된 설정).
## 5. CSS Import
| 패키지 | 앱 진입점 |
|---|---|
| Web `@hm-design/ui` | CSS entry에서 `@import "tailwindcss";` → `@import "@hm-design/ui/tailwind.css";` → `@import "@hm-design/ui/theme.css";`. Tailwind 검색 범위는 [Runtime portability](./runtime-portability.md#21-tailwind-v4-소비자-검색-범위)를 따릅니다. |
| SW `@hm-design/sw` | `import "@hm-design/sw/styles.css"`를 한 번. JS import만으로 CSS가 들어오지 않습니다. 글꼴 파일은 포함하지 않습니다(기본 Noto Sans KR). |
| OrgChart | Core CSS 두 개(`@hm-design/org-chart/engine.css`, `@hm-design/org-chart/style.css`)를 URL로 해석해 `stylesheetUrls`에 전달합니다(Shadow DOM). 문서에 import하는 것만으로는 적용되지 않습니다. |
Web과 SW를 함께 쓰면 두 CSS의 순서는 서로의 계산 스타일을 바꾸지 않습니다(`integrated-consumer` 검증 항목).
## 6. AI 지침 탐색
- 소비 프로젝트의 AI 코딩 도구는 `node_modules/@hm-design/<패키지>/ai/`(= `HMDesign/packages/<패키지>/ai/`)를 읽습니다. 진입점은 각 폴더의 `AI_GUIDE.md`이며 사용 유형(Web / SW 일반·뷰어 / OrgChart / 혼합)을 먼저 정합니다.
- 루트 `AGENTS.md`·`CLAUDE.md`는 자동으로 연결되지 않습니다. `ai/AGENTS_SNIPPET.md`의 문구를 소비 프로젝트 루트 지침에 붙입니다(방법은 `AI_GUIDE.md`의 "소비 프로젝트 설정").
- HMDesign 저장소 루트의 `AGENTS.md`·`CLAUDE.md`는 HMDesign을 개발하는 AI용이며 소비 프로젝트 지침이 아닙니다.
## 7. HMDesign 버전 갱신
```bash
git -C HMDesign fetch origin
git -C HMDesign checkout <새 검증 Commit>
cd HMDesign && npm ci && npm run build:packages && cd ..
npm install
git add HMDesign
git commit -m "chore: update HMDesign to <새 검증 Commit>"
```
갱신 후 소비 프로젝트의 Typecheck·Build·화면 확인을 다시 실행합니다.
## 8. Registry 없이 사용할 때의 제약
- `dist/`가 Git에 없으므로 Clone·Commit 변경·CI마다 HMDesign 빌드(§3)가 필요합니다. CI에서도 Submodule 초기화와 `npm ci && npm run build:packages`를 실행합니다.
- HMDesign 빌드 도구와 소비 프로젝트 도구가 따로 설치됩니다(예: 외부 Viewer는 TypeScript 5.9·Vite 5, HMDesign은 TypeScript 7·Vite 8). 패키지는 HMDesign 도구로 빌드되고 소비 프로젝트는 결과물만 사용합니다.
- 버전 식별은 Submodule Commit SHA입니다. SW 패키지는 `dist/build-info.json`에 빌드한 Commit과 dirty 여부를 기록합니다.
- npm Registry publish는 현재 필수가 아닙니다. Registry 배포를 도입하면 이 문서와 별도로 [운영/배포 가이드](./운영-배포-가이드.md)에 절차를 둡니다.
@@ -0,0 +1,276 @@
{
"checks": [
{
"name": "1 all SVG/PNG assets decode (data URI, no external path)",
"status": "PASS",
"data": {
"count": 33,
"broken": []
}
},
{
"name": "1 styles.css loaded; SWTheme provides tokens",
"status": "PASS",
"data": {
"height": "49px",
"font": "\"Noto Sans KR\", \"Malgun Gothic\", \"Apple SD Gothic Neo\", sans-serif"
}
},
{
"name": "2 A default header 1920 geometry = Figma (49 / 0·80 / 65·310 / 377·1301 / 1680·240)",
"status": "PASS",
"data": {
"header": {
"x": 0,
"y": 0,
"w": 1920,
"h": 49
},
"home": {
"x": 0,
"y": 0,
"w": 80,
"h": 48
},
"document": {
"x": 65,
"y": 0,
"w": 310,
"h": 48
},
"object": {
"x": 377,
"y": 0,
"w": 1301,
"h": 48
},
"window": {
"x": 1680,
"y": 0,
"w": 240,
"h": 48
}
}
},
{
"name": "2 A layer dropdown select callback",
"status": "PASS",
"data": "layer:center"
},
{
"name": "2 A close callback",
"status": "PASS",
"data": "close"
},
{
"name": "3 B order Logo → File → Layer → Color → K (DOM)",
"status": "PASS",
"data": [
"뷰어 홈",
"hm-sw-header__document",
"레이어",
"색상",
"팝업 가이드"
]
},
{
"name": "3 B sizes keep SW rules (bar 49, home 80 overlapping file 15px, fields 234/192 x 38)",
"status": "PASS",
"data": {
"bar": {
"x": 0,
"y": 409,
"w": 1920,
"h": 49
},
"home": {
"x": 0,
"y": 409,
"w": 80,
"h": 48
},
"document": {
"x": 65,
"y": 409,
"w": 310,
"h": 48
},
"layer": {
"x": 382,
"y": 414,
"w": 234,
"h": 38
},
"color": {
"x": 620,
"y": 414,
"w": 192,
"h": 38
}
}
},
{
"name": "3 B document dropdown New callback",
"status": "PASS",
"data": "document:new"
},
{
"name": "3 B layer dropdown opens under the field",
"status": "PASS",
"data": "true"
},
{
"name": "3 B Escape closes and returns focus to the field",
"status": "PASS",
"data": null
},
{
"name": "3 B color chip callback (dropdown stays open)",
"status": "PASS",
"data": "color:red"
},
{
"name": "3 B K guide preview on hover (animation frame advances)",
"status": "PASS",
"data": {
"guide": true
}
},
{
"name": "3 B K click callback",
"status": "PASS",
"data": "popupGuide"
},
{
"name": "4 C order File → 선 → 문자 → 치수 → 인쇄",
"status": "PASS",
"data": [
"file",
"선스타일",
"문자스타일",
"치수스타일",
"인쇄"
]
},
{
"name": "4 C first file well has a left edge (consumer wrapper)",
"status": "PASS",
"data": "1px"
},
{
"name": "4 C line type callback + field value",
"status": "PASS",
"data": "lineType:center"
},
{
"name": "4 C text style callback + field value",
"status": "PASS",
"data": "textStyle:dotum"
},
{
"name": "4 C dimension style callback",
"status": "PASS",
"data": "dimensionStyle:iso"
},
{
"name": "4 C print callback",
"status": "PASS",
"data": "print"
},
{
"name": "5 D new search field matches the SW field surface (height/top/border/radius/background/shadow/label)",
"status": "PASS",
"data": {
"layer": {
"h": 38,
"top": 1232,
"border": "1px rgb(8, 37, 28)",
"radius": "3px",
"bg": "linear-gradient(0deg, rgba(0, 0, 0, 0.45",
"shadow": "rgba(9, 39, 30, 0.7) 0px 1px 4px 0px"
},
"search": {
"h": 38,
"top": 1232,
"border": "1px rgb(8, 37, 28)",
"radius": "3px",
"bg": "linear-gradient(0deg, rgba(0, 0, 0, 0.45",
"shadow": "rgba(9, 39, 30, 0.7) 0px 1px 4px 0px"
},
"label": "12px",
"fieldLabel": "12px"
}
},
{
"name": "5 D hover ring like existing fields (edge turns yellow, inside unchanged)",
"status": "PASS",
"data": {
"ring": "1",
"before": {
"inside": [
18,
50,
39
],
"edge": [
8,
37,
28
]
},
"after": {
"inside": [
18,
50,
39
],
"edge": [
172,
131,
3
]
}
}
},
{
"name": "5 D Enter commits through SWHeaderInput → search callback",
"status": "PASS",
"data": "search:WALL-01"
},
{
"name": "5 D search button callback uses committed value",
"status": "PASS",
"data": "search:WALL-01"
},
{
"name": "5 D Escape restores the committed value",
"status": "PASS",
"data": "WALL-01"
},
{
"name": "5 D existing color dropdown in the same bar",
"status": "PASS",
"data": "color:blue"
},
{
"name": "6 B Tab order = display order",
"status": "PASS",
"data": [
"문서: C0020101-006-평면및종단면도(06)",
"레이어",
"색상",
"팝업 가이드"
]
},
{
"name": "7 no page/console errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"warnings": [
"Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently",
"Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently"
]
}
@@ -0,0 +1,104 @@
{
"baseCommit": "50bf5bda8e956a4973b923d64131f2f2285bd38c",
"checks": [
{
"name": "all 28 Story IDs, names, order and tags retained",
"status": "PASS"
},
{
"name": "other 27 Story declarations byte-identical",
"status": "PASS"
},
{
"name": "package production source untouched",
"status": "PASS"
},
{
"name": "document first row is above the property Bar and clickable",
"status": "PASS"
},
{
"name": "existing 레이어 상세 reports Callback",
"status": "PASS"
},
{
"name": "existing 색상 상세 reports Callback",
"status": "PASS"
},
{
"name": "existing 선스타일 상세 reports Callback",
"status": "PASS"
},
{
"name": "existing 문자스타일 상세 reports Callback",
"status": "PASS"
},
{
"name": "existing 치수스타일 상세 reports Callback",
"status": "PASS"
},
{
"name": "ByBlock mode immediately appears in Header without changing numeric RGB property",
"status": "PASS"
},
{
"name": "direct color selection clears Header mode label",
"status": "PASS"
},
{
"name": "small Inner layer dropdown stays in browser width",
"status": "PASS",
"data": {
"x": 141,
"y": 345.0625,
"width": 234,
"height": 250
}
},
{
"name": "small Inner lower layer row hit-test is unobstructed",
"status": "PASS"
},
{
"name": "small Inner layer selection or Escape closes only nested dropdown",
"status": "PASS"
},
{
"name": "small Inner line dropdown stays in browser width",
"status": "PASS",
"data": {
"x": 186,
"y": 39.0625,
"width": 189.1875,
"height": 342
}
},
{
"name": "small Inner line selection or Escape closes only nested dropdown",
"status": "PASS"
},
{
"name": "small Inner color dropdown stays in browser width",
"status": "PASS",
"data": {
"x": 175,
"y": 377.0625,
"width": 192,
"height": 182
}
},
{
"name": "small Inner color selection or Escape closes only nested dropdown",
"status": "PASS"
},
{
"name": "no overlay browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"sizes": [],
"browser": "155.0.8059.80",
"status": "PASS"
}
@@ -0,0 +1,670 @@
{
"baseCommit": "50bf5bda8e956a4973b923d64131f2f2285bd38c",
"checks": [
{
"name": "all 28 Story IDs, names, order and tags retained",
"status": "PASS"
},
{
"name": "other 27 Story declarations byte-identical",
"status": "PASS"
},
{
"name": "package production source untouched",
"status": "PASS"
},
{
"name": "16:9 fitted to both Inner dimensions at 1920x1080",
"status": "PASS",
"data": {
"stage": {
"x": 360,
"y": 264.25,
"w": 1200,
"h": 675,
"right": 1560,
"bottom": 939.25
},
"slot": {
"x": 360,
"y": 169.5,
"w": 1200,
"h": 864.5,
"right": 1560,
"bottom": 1034
},
"header": {
"x": 360,
"y": 264.25,
"w": 1200,
"h": 49,
"right": 1560,
"bottom": 313.25
},
"viewport": {
"w": 1920,
"h": 1080
},
"overflow": 0
}
},
{
"name": "16:9 fitted to both Inner dimensions at 1440x900",
"status": "PASS",
"data": {
"stage": {
"x": 120,
"y": 174.25,
"w": 1200,
"h": 675,
"right": 1320,
"bottom": 849.25
},
"slot": {
"x": 120,
"y": 169.5,
"w": 1200,
"h": 684.5,
"right": 1320,
"bottom": 854
},
"header": {
"x": 120,
"y": 174.25,
"w": 1200,
"h": 49,
"right": 1320,
"bottom": 223.25
},
"viewport": {
"w": 1440,
"h": 900
},
"overflow": 0
}
},
{
"name": "16:9 fitted to both Inner dimensions at 1280x720",
"status": "PASS",
"data": {
"stage": {
"x": 191.5625,
"y": 169.5,
"w": 896.875,
"h": 504.5,
"right": 1088.4375,
"bottom": 674
},
"slot": {
"x": 38.40625,
"y": 169.5,
"w": 1203.1875,
"h": 504.5,
"right": 1241.59375,
"bottom": 674
},
"header": {
"x": 191.5625,
"y": 169.5,
"w": 896.875,
"h": 49,
"right": 1088.4375,
"bottom": 218.5
},
"viewport": {
"w": 1280,
"h": 720
},
"overflow": 0
}
},
{
"name": "16:9 fitted to both Inner dimensions at 768x600",
"status": "PASS",
"data": {
"stage": {
"x": 42.21875,
"y": 169.5,
"w": 683.546875,
"h": 384.5,
"right": 725.765625,
"bottom": 554
},
"slot": {
"x": 23.046875,
"y": 169.5,
"w": 721.90625,
"h": 384.5,
"right": 744.953125,
"bottom": 554
},
"header": {
"x": 42.21875,
"y": 169.5,
"w": 683.546875,
"h": 49,
"right": 725.765625,
"bottom": 218.5
},
"viewport": {
"w": 768,
"h": 600
},
"overflow": 0
}
},
{
"name": "16:9 fitted to both Inner dimensions at 390x844",
"status": "PASS",
"data": {
"stage": {
"x": 16,
"y": 383.0625,
"w": 358,
"h": 201.375,
"right": 374,
"bottom": 584.4375
},
"slot": {
"x": 16,
"y": 169.5,
"w": 358,
"h": 628.5,
"right": 374,
"bottom": 798
},
"header": {
"x": 16,
"y": 383.0625,
"w": 390,
"h": 49,
"right": 406,
"bottom": 432.0625
},
"viewport": {
"w": 390,
"h": 844
},
"overflow": 0
}
},
{
"name": "16:9 fitted to both Inner dimensions at 1000x500",
"status": "PASS",
"data": {
"stage": {
"x": 247.109375,
"y": 169.5,
"w": 505.765625,
"h": 284.5,
"right": 752.875,
"bottom": 454
},
"slot": {
"x": 30,
"y": 169.5,
"w": 940,
"h": 284.5,
"right": 970,
"bottom": 454
},
"header": {
"x": 247.109375,
"y": 169.5,
"w": 505.765625,
"h": 49,
"right": 752.875,
"bottom": 218.5
},
"viewport": {
"w": 1000,
"h": 500
},
"overflow": 0
}
},
{
"name": "one Header and no duplicate fixed Figma specimen",
"status": "PASS"
},
{
"name": "document selection updates Header and Callback",
"status": "PASS",
"data": "onDocumentClick → documentOpen(true) → onDocumentSelect(doc-2) → documentOpen(false)"
},
{
"name": "layer selection updates shared data",
"status": "PASS",
"data": "onDocumentClick → documentOpen(true) → onDocumentSelect(doc-2) → documentOpen(false) → onLayerOpen → onLayerSelect(layer, CC-D GNL)"
},
{
"name": "existing layer Detail callback closes list",
"status": "PASS"
},
{
"name": "color selection invokes host callback",
"status": "PASS",
"data": "onLayerOpen → onLayerSelect(layer, CC-D GNL) → onLayerOpen → onLayerDetail → onColorOpen → onColorSelect(color, 255,255,255)"
},
{
"name": "line selection updates Header",
"status": "PASS",
"data": "onLayerOpen → onLayerDetail → onColorOpen → onColorSelect(color, 255,255,255) → onLineStyleOpen → onLineTypeSelect(line, CENTER)"
},
{
"name": "ByLayer weight mode does not turn numeric property into a mode string",
"status": "PASS"
},
{
"name": "text style selection updates Header",
"status": "PASS",
"data": "onColorSelect(color, 255,255,255) → onLineStyleOpen → onLineTypeSelect(line, CENTER) → onLineWeightSelect(w-bylayer) → onTextStyleOpen → onTextStyleSelect(dotum)"
},
{
"name": "dimension style selection updates Header",
"status": "PASS",
"data": "onLineTypeSelect(line, CENTER) → onLineWeightSelect(w-bylayer) → onTextStyleOpen → onTextStyleSelect(dotum) → onDimensionStyleOpen → onDimensionStyleSelect(iso-25)"
},
{
"name": "opening another dropdown closes the previous one",
"status": "PASS"
},
{
"name": "Escape closes Header dropdown",
"status": "PASS"
},
{
"name": "outside click closes Header dropdown",
"status": "PASS"
},
{
"name": "Bar Enter commits state",
"status": "PASS"
},
{
"name": "Bar validates min=0",
"status": "PASS"
},
{
"name": "Bar Escape cancels edit",
"status": "PASS"
},
{
"name": "Header opens panel with same Bar value",
"status": "PASS"
},
{
"name": "numeric weight property remains numeric after ByLayer mode",
"status": "PASS"
},
{
"name": "Header layer value shared with panel",
"status": "PASS"
},
{
"name": "panel edit updates the Bar",
"status": "PASS"
},
{
"name": "panel number validation rejects negative radius",
"status": "PASS"
},
{
"name": "panel Layer dropdown updates Header shared value",
"status": "PASS"
},
{
"name": "panel line dropdown updates Header shared value",
"status": "PASS"
},
{
"name": "panel color dropdown updates Header shared value",
"status": "PASS"
},
{
"name": "panel RGB validation rejects out-of-range value",
"status": "PASS"
},
{
"name": "gauge keyboard editing commits host state",
"status": "PASS"
},
{
"name": "first Escape cancels draft and retains panel",
"status": "PASS"
},
{
"name": "next Escape closes panel and restores Header focus",
"status": "PASS"
},
{
"name": "panel Close button restores focus",
"status": "PASS"
},
{
"name": "external Props update Bar",
"status": "PASS"
},
{
"name": "Bar ReadOnly",
"status": "PASS"
},
{
"name": "Bar Disabled",
"status": "PASS"
},
{
"name": "dynamic Bar field added",
"status": "PASS"
},
{
"name": "dynamic field shared with panel",
"status": "PASS"
},
{
"name": "dynamic field removed from both",
"status": "PASS"
},
{
"name": "shape Slot can be empty without changing panel structure",
"status": "PASS"
},
{
"name": "original panel size none:314x256",
"status": "PASS",
"data": {
"x": 1000,
"y": 301.25,
"width": 314,
"height": 256
}
},
{
"name": "original panel size all:314x361",
"status": "PASS",
"data": {
"x": 1000,
"y": 301.25,
"width": 314,
"height": 361
}
},
{
"name": "original panel size circle:314x658",
"status": "PASS",
"data": {
"x": 1000,
"y": 301.25,
"width": 314,
"height": 658
}
},
{
"name": "original panel size expanded:314x754",
"status": "PASS",
"data": {
"x": 1000,
"y": 301.25,
"width": 314,
"height": 754
}
},
{
"name": "expanded popup bottom accessible through internal scroll",
"status": "PASS"
},
{
"name": "small Inner reaches full popup bottom with native Header and popup dimensions",
"status": "PASS",
"data": {
"top": 630,
"left": 16,
"height": 201,
"scrollHeight": 831,
"width": 358,
"scrollWidth": 390
}
},
{
"name": "narrow popup Escape restores More focus",
"status": "PASS"
},
{
"name": "actual Storybook manager iframe fits its own Inner",
"status": "PASS",
"data": {
"width": 1071.59375,
"height": 602.765625,
"slotWidth": 1071.59375,
"slotHeight": 744.5,
"viewportWidth": 1140,
"viewportHeight": 960
}
},
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"sizes": [
{
"stage": {
"x": 360,
"y": 264.25,
"w": 1200,
"h": 675,
"right": 1560,
"bottom": 939.25
},
"slot": {
"x": 360,
"y": 169.5,
"w": 1200,
"h": 864.5,
"right": 1560,
"bottom": 1034
},
"header": {
"x": 360,
"y": 264.25,
"w": 1200,
"h": 49,
"right": 1560,
"bottom": 313.25
},
"viewport": {
"w": 1920,
"h": 1080
},
"overflow": 0
},
{
"stage": {
"x": 120,
"y": 174.25,
"w": 1200,
"h": 675,
"right": 1320,
"bottom": 849.25
},
"slot": {
"x": 120,
"y": 169.5,
"w": 1200,
"h": 684.5,
"right": 1320,
"bottom": 854
},
"header": {
"x": 120,
"y": 174.25,
"w": 1200,
"h": 49,
"right": 1320,
"bottom": 223.25
},
"viewport": {
"w": 1440,
"h": 900
},
"overflow": 0
},
{
"stage": {
"x": 191.5625,
"y": 169.5,
"w": 896.875,
"h": 504.5,
"right": 1088.4375,
"bottom": 674
},
"slot": {
"x": 38.40625,
"y": 169.5,
"w": 1203.1875,
"h": 504.5,
"right": 1241.59375,
"bottom": 674
},
"header": {
"x": 191.5625,
"y": 169.5,
"w": 896.875,
"h": 49,
"right": 1088.4375,
"bottom": 218.5
},
"viewport": {
"w": 1280,
"h": 720
},
"overflow": 0
},
{
"stage": {
"x": 42.21875,
"y": 169.5,
"w": 683.546875,
"h": 384.5,
"right": 725.765625,
"bottom": 554
},
"slot": {
"x": 23.046875,
"y": 169.5,
"w": 721.90625,
"h": 384.5,
"right": 744.953125,
"bottom": 554
},
"header": {
"x": 42.21875,
"y": 169.5,
"w": 683.546875,
"h": 49,
"right": 725.765625,
"bottom": 218.5
},
"viewport": {
"w": 768,
"h": 600
},
"overflow": 0
},
{
"stage": {
"x": 16,
"y": 383.0625,
"w": 358,
"h": 201.375,
"right": 374,
"bottom": 584.4375
},
"slot": {
"x": 16,
"y": 169.5,
"w": 358,
"h": 628.5,
"right": 374,
"bottom": 798
},
"header": {
"x": 16,
"y": 383.0625,
"w": 390,
"h": 49,
"right": 406,
"bottom": 432.0625
},
"viewport": {
"w": 390,
"h": 844
},
"overflow": 0
},
{
"stage": {
"x": 247.109375,
"y": 169.5,
"w": 505.765625,
"h": 284.5,
"right": 752.875,
"bottom": 454
},
"slot": {
"x": 30,
"y": 169.5,
"w": 940,
"h": 284.5,
"right": 970,
"bottom": 454
},
"header": {
"x": 247.109375,
"y": 169.5,
"w": 505.765625,
"h": 49,
"right": 752.875,
"bottom": 218.5
},
"viewport": {
"w": 1000,
"h": 500
},
"overflow": 0
}
],
"browser": "155.0.8059.80",
"managerOrder": [
"sw-header--overview",
"sw-header--interactive-states",
"sw-header--window-move-motion",
"sw-header--composed-controls",
"sw-header--logo-slot",
"sw-header--document-dropdown",
"sw-header--layer-dropdown",
"sw-header--color-dropdown",
"sw-header--line-style-dropdown",
"sw-header--text-dimension-style-dropdown",
"sw-header--responsive-more",
"sw-header--layer-detail-input",
"sw-header--property-panel",
"sw-header--property-panel-variants",
"sw-header--k-guide",
"sw-header--additional-hover",
"sw-header--property-bar",
"sw-header--dynamic-controls",
"sw-header--colorize",
"sw-header--composition",
"sw-header--property-panel-none",
"sw-header--property-panel-all",
"sw-header--property-panel-circle",
"sw-header--property-panel-expanded",
"sw-header--property-panel-compare",
"sw-header--property-shape-slot",
"sw-header--property-fields"
],
"status": "PASS"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+116
View File
@@ -0,0 +1,116 @@
# SW AI 재사용 템플릿 — 조사·구현·검증 기록 (2026-10-19)
> 범위: `@hm-design/sw`만. `@hm-design/ui`·조직도·Center Graphic·DWG/DXF Viewer는 변경하지 않았습니다. 작업 Worktree `D:\디자인시스템\HMDesign-SW-AI-Template`, 브랜치 `feature/sw-ai-template`, 시작 HEAD `50bf5bda8e956a4973b923d64131f2f2285bd38c`(`feature/sw-package`).
>
> AI용 사용 규칙·카탈로그·디자인 규칙·조합 패턴의 **원본은 패키지 안 [`packages/sw/ai/`](../../../packages/sw/ai/AI_USAGE.md)** 이며 npm 패키지에 포함됩니다. 이 문서는 조사 결과와 검증 기록만 담고 원본 내용을 반복하지 않습니다.
## 1. 결론
- 기존 공개 Export만으로 재사용(예제 A)·조합(예제 B·C)·확장(예제 D)이 모두 가능했습니다. **런타임 소스(`packages/sw/src`)와 공개 API는 변경하지 않았습니다.** 그래서 기존 SW Header의 디자인·기능·Props·Callback은 그대로입니다.
- 변경은 패키지 배포 범위(`files`에 `ai` 추가), AI 가이드 문서 4종, 공개 API만 쓰는 예제, README 안내 1절, SW 전용 검증 스크립트 3개입니다.
- 공개 API가 부족한 부분(More, 범용 필드 면, Header 면 레이아웃 등)은 내부 Import로 우회하지 않았습니다. §3 EXPORT CANDIDATE / MISSING으로 보고합니다.
## 2. 기존 구성 분석 요약
- 진입점: `src/index.ts` → `components/sw-header/index.ts`, `SWTheme`. 배포는 `exports` `"."`(ESM `dist/index.js` + `dist/index.d.ts`)와 `"./styles.css"` 두 개입니다. `main`·`types`가 있고 `module` 필드는 없습니다(ESM 전용 `"type": "module"`, `exports`가 우선). Peer는 `react`/`react-dom` `^19.0.0`, 런타임 dependency는 없습니다.
- 자산: SVG·PNG 48개는 Vite library build에서 data URI로 JS에 포함됩니다(소비 프로젝트 33개 이미지 디코드 확인). 글꼴은 포함하지 않습니다.
- CSS: 토큰은 `.hm-sw-header`와 `.hm-sw-theme`에만 정의됩니다. 컴포넌트 규칙은 대부분 `hm-sw-header__*` class 단위라서 `SWTheme` 안에서 단독 렌더링됩니다. 반응형은 `@container hm-sw-header`(12개 규칙)라 `SWHeader` 안에서만 적용됩니다. 전역 선택자는 없습니다.
- Overlay: 필드 Dropdown은 absolute(필드 아래, `z-index` 30)이고 `closest(".hm-sw-header") ?? documentElement` 기준으로 좌우를 보정하므로 Header 밖에서도 동작합니다. 속성창·K 가이드는 `position: fixed`(`useSWHeaderOverlay`)이며 Portal·Dim은 없습니다.
- `dist`에는 내부 모듈 `.d.ts`도 생성되지만 `exports`가 deep import를 막습니다.
## 3. 분류
### PUBLIC READY (현재 외부 재사용 가능)
| 컴포넌트 | 근거 |
|---|---|
| `SWHeader` | 공식 기준 템플릿. 예제 A에서 1920 좌표가 Figma와 일치(0·80 / 65·310 / 377·1301 / 1680·240, 49px) |
| `SWTheme` | 단독 사용 토큰 범위. 소비 프로젝트에서 토큰·글꼴 확인 |
| `SWHeaderHome`, `SWHeaderDocumentTitle` | 예제 B·C 렌더링, 문서 Dropdown `+ New` Callback |
| `SWHeaderLayerMenu`, `SWHeaderColorMenu`, `SWHeaderLineStyleMenu`, `SWHeaderTextStyleMenu`, `SWHeaderDimensionStyleMenu` | 예제 B·C·D에서 열기·선택 Callback·Escape Focus 복귀 |
| `SWHeaderActions` | `showPrint` / `showPopupGuide`로 K만·인쇄만. K 가이드 Hover·Click |
| `SWHeaderInput`, `parseSWHeaderColor`, `parseSWHeaderNumber` | Input은 예제 D에서 Enter 확정·Escape 복원 확인. 파서는 이번 미검증 |
| `SWHeaderLayerControl` 외 필드 버튼 4종, `SWHeaderWindowControls`, `SWHeaderProperties`, `SWHeaderPropertyBar`, `SWHeaderPropertyPanel`, `SWHeaderLinePreview`, `swHeaderLinePatterns`, 공개 타입 | 공개·문서화 상태(기존 사용 가이드 §4·§8). 이번 소비 검증 범위 밖 |
### EXPORT CANDIDATE (내부 구현 있음, 공개 API 정비 필요)
| 내부 구현 | 재사용 가치 | 공개 전 필요한 정비 |
|---|---|---|
| `SWHeaderMore` | 조합 Header의 Overflow | tier·`data-visible`·container 규칙이 `SWHeader` 구조에 결합. 항목 visibility API 설계 필요 |
| `SWHeaderField` / `SWHeaderFieldText` | 새 필드형 컨트롤이 같은 면을 재사용(예제 D 표면 CSS 대체) | `variant`가 5개 고정 폭. 범용 폭·`children` 계약 필요 |
| `SWHeaderKGuide` | pill 없는 K 단독 버튼 | 현재 `SWHeaderActions showPrint={false}`로 대체 가능. 수요 확인 후 |
| `useSWHeaderDropdown`, `SWHeaderMenuDetail`, `SWHeaderColorChip` | 새 Dropdown 동작·`… 상세` pill·색상 칩 | 반환값 계약(ref·style·shifted)과 CSS 의존 정리 |
| Header 면(객체 수정 / 창 영역 / 막대) | 조합 Header 배치 | 현재 `SWHeader` 내부 DOM. 예제는 소비 CSS + 공개 토큰으로 구성 |
### INTERNAL ONLY
`SWHeaderLayerRows`, `SWHeaderColorPanelBody`, `SWHeaderLineStyleColumns`(헤더 Dropdown과 속성창 Picker의 공유 본문), `SWHeaderPropertyPickerField`, `propertyPicker`, `propertyLineTypes`, `SWHeaderLayerIconArt`, `SWHeaderLayerToggle`, `layerIconOn`, `SWHeaderCommitPresence`, `useSWHeaderOverlay`, `useSWHeaderColorMode`, `SWHeaderKGuidePreview`. 상위 공개 컴포넌트의 상태·Focus 계약에 묶여 있어 단독 공개 시 중복 상태 위험이 있습니다.
### MISSING (SW 패키지에 없음)
검색 컨트롤(예제 D에서 소비 프로젝트 확장으로 작성), 범용 SW Button / IconButton, 범용 Tooltip 컴포넌트(창 이동 Tooltip은 CSS 전용), 조합 Header용 반응형 More, Header 면 레이아웃 컴포넌트.
### 발견한 재사용 제약 (코드 미변경, 문서화)
1. `SWHeaderDocumentTitle`은 앞의 로고와 겹치는 전제로 왼쪽 테두리가 없습니다 → 첫 칸이면 소비 wrapper에 테두리(예제 C).
2. `SWHeaderDocumentTitle` 폭은 `100cqw` 기반이라 Header 밖에서는 뷰포트 폭을 기준으로 계산합니다.
3. 창 영역 배경·테두리는 `SWHeader` DOM에 있어 `SWHeaderActions`/`SWHeaderWindowControls` 단독 사용 시 소비 면이 필요합니다.
4. `body` margin 등 페이지 기본값은 소비 앱 책임입니다(첫 소비 실행에서 8px 오프셋으로 확인 → 테스트 앱에서 지정).
## 4. 변경 파일
| 파일 | 내용 |
|---|---|
| `packages/sw/package.json` | `files`: `["dist"]` → `["dist", "ai"]` |
| `packages/sw/README.md` | 끝에 "AI 코딩 도구용 가이드" 절 추가(기존 본문 무변경) |
| `packages/sw/ai/AI_USAGE.md`, `COMPONENT_CATALOG.md`, `DESIGN_RULES.md`, `COMPOSITION_PATTERNS.md` | AI 가이드(배포 포함) |
| `packages/sw/ai/examples/*` | 예제 A~D, `SWSearchControl`(소비 확장), 소비 배치 CSS, 샘플 데이터 |
| `packages/sw/tests/ai-docs.check.mjs` | 문서·예제와 공개 Export·링크 일치 검사 |
| `packages/sw/tests/ai-template-consumer.mjs` | `npm pack` → 저장소 밖 독립 React 19 + TS 7 + Vite 8 프로젝트 생성, Tarball 설치, 설치된 `ai/examples` 복사, `tsc`·`vite build` |
| `packages/sw/tests/ai-template-consumer.browser.mjs` | 소비 빌드 브라우저 검증(Chrome, Playwright) |
| `docs/sw/ai-template/` | 이 기록과 `evidence/`(스크린샷 7장, `report.json`) |
## 5. 이번에 실행한 검증 (2026-10-19)
| 항목 | 결과 |
|---|---|
| `npm run typecheck -w @hm-design/sw` | PASS |
| `npm run build -w @hm-design/sw` | PASS(`dist/index.js` 712.25 kB, `styles.css` 102.25 kB) |
| `npm pack`(실제 tarball, Publish 없음) | PASS — 40개 파일, `ai/` 문서 4 + 예제 7, `dist`, `README.md`, `package.json` |
| `node packages/sw/tests/ai-docs.check.mjs` | 19/19 PASS — 문서에 쓴 SW 이름 전부 공개 Export, "공개하지 않은 것" 목록에 Export 없음, 예제 Import 전부 공개·deep import 없음, 패키지 상대 링크 정상 |
| 독립 소비 프로젝트 설치(`react@19.3.0`, `typescript@7.0.2`, `vite@8.3.0`, `@vitejs/plugin-react@6.1.1`) | PASS |
| 소비 프로젝트 `tsc`(strict, `noUnusedLocals`, `skipLibCheck: false`) | PASS — 설치된 패키지의 예제를 그대로 컴파일 |
| 소비 프로젝트 Production Build | PASS(번들 500 kB 경고만, 오류 없음) |
| 소비 빌드 브라우저(Chrome 1920 × 1080) | 27/27 PASS([report.json](./evidence/report.json)) |
브라우저 27개 항목: 자산 33개 data URI 디코드 · CSS/토큰 로딩 · A 기본 Header 좌표 = Figma · A 레이어 선택 / 종료 Callback · B 순서(DOM) · B 크기(49 / 80 / 파일 x65 / 234·192 × 38) · B 문서 `+ New` · B 레이어 열림 · Escape 닫힘 + Focus 복귀 · B 색상 선택(열림 유지) · B K 가이드 Hover · B K Click · C 순서 · C 파일 왼쪽 테두리 · C 선·문자·치수 선택과 필드 값 반영 · C 인쇄 · D 검색 필드 표면 = 레이어 필드(높이·위치·테두리·radius·배경·그림자·라벨 12px) · D Hover 링(테두리만 노랑, 내부 픽셀 불변) · D Enter → 검색 · D 버튼 검색 · D Escape 복원 · D 색상 선택 · B Tab 순서 = 표시 순서 · 페이지/콘솔 오류 0.
중간 실패와 수정: 첫 실행 24/27(소비 앱 `body` 8px margin, 예제 막대 `box-sizing`, favicon 404) → 소비 앱·예제 CSS 수정. 육안 확인 중 D의 Hover 링이 필드 전체를 덮는 문제(표준 `mask` 뒤에 `-webkit-mask` 선언) 발견 → 선언 순서 수정, 테스트에 픽셀 검사 추가 후 재실행 PASS.
증거: [A](./evidence/A-default-1920.png) · [B](./evidence/B-viewer-1920.png) · [B 레이어](./evidence/B-layer-open.png) · [B K](./evidence/B-k-guide.png) · [C](./evidence/C-feature-1920.png) · [D](./evidence/D-search-1920.png) · [D Hover](./evidence/D-search-hover.png).
재현:
```bash
npm run build -w @hm-design/sw
node packages/sw/tests/ai-docs.check.mjs
node packages/sw/tests/ai-template-consumer.mjs <저장소 밖 빈 폴더>
node packages/sw/tests/ai-template-consumer.browser.mjs <같은 폴더> <증거 폴더>
```
## 6. NOT VERIFIED
- Firefox·Safari에서 소비 빌드(이번에는 Chrome만). 예제 D의 mask는 표준 + prefix 둘 다 선언했으나 Firefox 미실행.
- 이번 소비 검증에서 직접 렌더링하지 않은 공개 컴포넌트: 필드 버튼 5종 단독, `SWHeaderWindowControls` 단독(창 이동 Motion), `SWHeaderProperties`, `SWHeaderPropertyBar`, `SWHeaderPropertyPanel`(13개 필드·4상태), `SWHeaderLinePreview`, 파서 함수. 기존 Storybook 검증 기록만 있습니다.
- `SWHeader`의 반응형 More·10개 폭, Colorize, 게이지바, 속성창 연결: 런타임 코드를 바꾸지 않았으므로 이번에 다시 실행하지 않았습니다(과거 PASS 기록은 기존 문서).
- Noto Sans KR 실제 로드(소비 테스트는 시스템 대체 글꼴).
- 저장소 루트 전체 `npm run typecheck`·Storybook build(SW 패키지 단위만 실행).
- `README.md`의 기존 `../../docs/...` 링크는 Tarball 안에서 열리지 않습니다(기존 상태, `ai/` 문서는 패키지 내부 링크만 사용).
## 7. 추가 개선 후보
1. `SWHeaderField` 범용 폭 공개 → 새 필드형 컨트롤이 표면 CSS를 다시 쓰지 않도록.
2. 조합 Header용 More(또는 `SWHeaderMore` 공개 API) 설계.
3. `SWHeaderDocumentTitle`에 단독 첫 칸용 왼쪽 테두리 옵션 검토(기본 Header 픽셀 영향 확인 필요).
4. Header 면(막대·객체 수정·창 영역) 레이아웃 컴포넌트 검토.
5. 소비 검증을 Firefox와 속성창 단독 사용까지 확장.
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+276
View File
@@ -0,0 +1,276 @@
{
"checks": [
{
"name": "1 all SVG/PNG assets decode (data URI, no external path)",
"status": "PASS",
"data": {
"count": 33,
"broken": []
}
},
{
"name": "1 styles.css loaded; SWTheme provides tokens",
"status": "PASS",
"data": {
"height": "49px",
"font": "\"Noto Sans KR\", \"Malgun Gothic\", \"Apple SD Gothic Neo\", sans-serif"
}
},
{
"name": "2 A default header 1920 geometry = Figma (49 / 0·80 / 65·310 / 377·1301 / 1680·240)",
"status": "PASS",
"data": {
"header": {
"x": 0,
"y": 0,
"w": 1920,
"h": 49
},
"home": {
"x": 0,
"y": 0,
"w": 80,
"h": 48
},
"document": {
"x": 65,
"y": 0,
"w": 310,
"h": 48
},
"object": {
"x": 377,
"y": 0,
"w": 1301,
"h": 48
},
"window": {
"x": 1680,
"y": 0,
"w": 240,
"h": 48
}
}
},
{
"name": "2 A layer dropdown select callback",
"status": "PASS",
"data": "layer:center"
},
{
"name": "2 A close callback",
"status": "PASS",
"data": "close"
},
{
"name": "3 B order Logo → File → Layer → Color → K (DOM)",
"status": "PASS",
"data": [
"뷰어 홈",
"hm-sw-header__document",
"레이어",
"색상",
"팝업 가이드"
]
},
{
"name": "3 B sizes keep SW rules (bar 49, home 80 overlapping file 15px, fields 234/192 x 38)",
"status": "PASS",
"data": {
"bar": {
"x": 0,
"y": 409,
"w": 1920,
"h": 49
},
"home": {
"x": 0,
"y": 409,
"w": 80,
"h": 48
},
"document": {
"x": 65,
"y": 409,
"w": 310,
"h": 48
},
"layer": {
"x": 382,
"y": 414,
"w": 234,
"h": 38
},
"color": {
"x": 620,
"y": 414,
"w": 192,
"h": 38
}
}
},
{
"name": "3 B document dropdown New callback",
"status": "PASS",
"data": "document:new"
},
{
"name": "3 B layer dropdown opens under the field",
"status": "PASS",
"data": "true"
},
{
"name": "3 B Escape closes and returns focus to the field",
"status": "PASS",
"data": null
},
{
"name": "3 B color chip callback (dropdown stays open)",
"status": "PASS",
"data": "color:red"
},
{
"name": "3 B K guide preview on hover (animation frame advances)",
"status": "PASS",
"data": {
"guide": true
}
},
{
"name": "3 B K click callback",
"status": "PASS",
"data": "popupGuide"
},
{
"name": "4 C order File → 선 → 문자 → 치수 → 인쇄",
"status": "PASS",
"data": [
"file",
"선스타일",
"문자스타일",
"치수스타일",
"인쇄"
]
},
{
"name": "4 C first file well has a left edge (consumer wrapper)",
"status": "PASS",
"data": "1px"
},
{
"name": "4 C line type callback + field value",
"status": "PASS",
"data": "lineType:center"
},
{
"name": "4 C text style callback + field value",
"status": "PASS",
"data": "textStyle:dotum"
},
{
"name": "4 C dimension style callback",
"status": "PASS",
"data": "dimensionStyle:iso"
},
{
"name": "4 C print callback",
"status": "PASS",
"data": "print"
},
{
"name": "5 D new search field matches the SW field surface (height/top/border/radius/background/shadow/label)",
"status": "PASS",
"data": {
"layer": {
"h": 38,
"top": 1232,
"border": "1px rgb(8, 37, 28)",
"radius": "3px",
"bg": "linear-gradient(0deg, rgba(0, 0, 0, 0.45",
"shadow": "rgba(9, 39, 30, 0.7) 0px 1px 4px 0px"
},
"search": {
"h": 38,
"top": 1232,
"border": "1px rgb(8, 37, 28)",
"radius": "3px",
"bg": "linear-gradient(0deg, rgba(0, 0, 0, 0.45",
"shadow": "rgba(9, 39, 30, 0.7) 0px 1px 4px 0px"
},
"label": "12px",
"fieldLabel": "12px"
}
},
{
"name": "5 D hover ring like existing fields (edge turns yellow, inside unchanged)",
"status": "PASS",
"data": {
"ring": "1",
"before": {
"inside": [
18,
50,
39
],
"edge": [
8,
37,
28
]
},
"after": {
"inside": [
18,
50,
39
],
"edge": [
172,
131,
3
]
}
}
},
{
"name": "5 D Enter commits through SWHeaderInput → search callback",
"status": "PASS",
"data": "search:WALL-01"
},
{
"name": "5 D search button callback uses committed value",
"status": "PASS",
"data": "search:WALL-01"
},
{
"name": "5 D Escape restores the committed value",
"status": "PASS",
"data": "WALL-01"
},
{
"name": "5 D existing color dropdown in the same bar",
"status": "PASS",
"data": "color:blue"
},
{
"name": "6 B Tab order = display order",
"status": "PASS",
"data": [
"문서: C0020101-006-평면및종단면도(06)",
"레이어",
"색상",
"팝업 가이드"
]
},
{
"name": "7 no page/console errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"warnings": [
"Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently",
"Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently"
]
}
@@ -0,0 +1,47 @@
{
"mode": "baseline",
"browser": "155.0.8059.79",
"checks": [
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"storyIds": [
"sw-header--overview",
"sw-header--default",
"sw-header--controls",
"sw-header--interactive-states",
"sw-header--responsive",
"sw-header--window-move-motion",
"sw-header--composed-controls",
"sw-header--logo-slot",
"sw-header--document-dropdown",
"sw-header--layer-dropdown",
"sw-header--color-dropdown",
"sw-header--line-style-dropdown",
"sw-header--text-dimension-style-dropdown",
"sw-header--responsive-more",
"sw-header--text-stroke",
"sw-header--layer-detail-input",
"sw-header--dynamic-data",
"sw-header--property-panel",
"sw-header--property-panel-variants",
"sw-header--k-guide",
"sw-header--additional-hover",
"sw-header--property-bar",
"sw-header--dynamic-controls",
"sw-header--colorize",
"sw-header--composition",
"sw-header--property-panel-none",
"sw-header--property-panel-all",
"sw-header--property-panel-circle",
"sw-header--property-panel-expanded",
"sw-header--property-panel-compare",
"sw-header--property-shape-slot"
],
"status": "PASS"
}
@@ -0,0 +1 @@
["sw-header--overview","sw-header--default","sw-header--controls","sw-header--interactive-states","sw-header--responsive","sw-header--window-move-motion","sw-header--composed-controls","sw-header--logo-slot","sw-header--document-dropdown","sw-header--layer-dropdown","sw-header--color-dropdown","sw-header--line-style-dropdown","sw-header--text-dimension-style-dropdown","sw-header--responsive-more","sw-header--text-stroke","sw-header--layer-detail-input","sw-header--dynamic-data","sw-header--property-panel","sw-header--property-panel-variants","sw-header--k-guide","sw-header--additional-hover","sw-header--property-bar","sw-header--dynamic-controls","sw-header--colorize","sw-header--composition","sw-header--property-panel-none","sw-header--property-panel-all","sw-header--property-panel-circle","sw-header--property-panel-expanded","sw-header--property-panel-compare","sw-header--property-shape-slot"]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,14 @@
{
"mode": "capture",
"browser": "155.0.8059.79",
"checks": [
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"status": "PASS"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,114 @@
{
"mode": "preservation",
"browser": "155.0.8059.79",
"checks": [
{
"name": "required responsive width 390",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 768",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1280",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1440",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "preserve property panel none",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 256
}
},
{
"name": "preserve property panel all",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 361
}
},
{
"name": "preserve property panel circle",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 658
}
},
{
"name": "preserve property panel expanded",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 754
}
},
{
"name": "preserve window motion 1400ms",
"status": "PASS"
},
{
"name": "window motion leave clears animations",
"status": "PASS"
},
{
"name": "preserve K guide animation",
"status": "PASS"
},
{
"name": "K guide leave closes",
"status": "PASS"
},
{
"name": "preserve Colorize size",
"status": "PASS"
},
{
"name": "preserve editable Input",
"status": "PASS"
},
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"status": "PASS"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,73 @@
# AI 가이드 초안 — SW 로고 클릭 메뉴 (`SWHomeMenu`)
> 상태: **통합 전 초안**입니다(Task B 2단계, 2026-10-19). 통합 승인 후 `packages/sw/ai/COMPONENT_CATALOG.md`·`COMPOSITION_PATTERNS.md`·`DESIGN_RULES.md`로 옮깁니다. 지금은 `@hm-design/sw` 진입점에서 export되지 않으므로 소스 경로 `packages/sw/src/components/sw-home-menu`를 사용합니다.
> 기준: Figma `rj6wpsfk24lOWvWiVRAANP` 26:10591(메뉴), 26:11355…26:11631(타일 15개), 26:12604…(섹션 제목), 662:67267·662:67331·662:67471(최근파일 하위 패널). 구현: `SWHomeMenu.tsx`, `SWRecentFiles.tsx`, `theme/sw-home-menu.css`.
## 1. 무엇인가
- SW Header의 **로고(홈) 버튼을 누르면 Header 위에 겹쳐 나타나는 2단 메뉴**입니다. 기본은 숨김이고, 로고를 다시 누르면 닫힙니다.
- 구성: 섹션(제목 badge + 타일 그룹) 여러 개 → 섹션마다 80 × 46 타일 여러 개. 타일 하나가 하위 패널(예: 최근파일 목록)을 열 수 있습니다.
- 운영체제 파일 선택창이 아닙니다. "찾아열기" 같은 항목의 실제 동작(파일 대화상자 등)은 host가 `onSelect`에서 수행합니다.
## 2. 최소 사용법
```tsx
import { SWHeader, SWTheme } from "@hm-design/sw"
import { SWHomeMenu, type SWHomeMenuSectionData } from "@hm-design/sw" // 통합 후 경로
const sections: SWHomeMenuSectionData[] = [
{ id: "file", title: "작업중인 파일", items: [
{ id: "save", label: "저장", icon: <SaveIcon />, toggle: false, onSelect: () => host.save() },
{ id: "export", label: "내보내기", icon: <ExportIcon /> },
] },
]
function App() {
const [open, setOpen] = useState(false)
return (
<div style={{ position: "relative" }}>
<SWHeader onHomeClick={() => setOpen(v => !v)} /* 기존 props */ />
{/* 통합 전: host가 위치를 지정합니다(Header 객체 수정 영역 왼쪽 - 2px, 창 영역 왼쪽까지). */}
<div style={{ position: "absolute", top: 0, left: objectAreaLeft - 2, maxWidth: windowLeft - objectAreaLeft + 2, zIndex: 3 }}>
<SWTheme><SWHomeMenu sections={sections} open={open} onOpenChange={setOpen} /></SWTheme>
</div>
</div>
)
}
```
## 3. 데이터 규칙 (새 메뉴를 만들 때)
| 하고 싶은 것 | 방법 |
|---|---|
| 항목 추가 | 섹션 `items`에 `{ id, label, icon }`을 추가합니다. 폭·구분선·모서리·섹션 폭은 자동입니다 |
| 섹션 추가 | `{ id, title, items }`를 추가합니다. 제목 badge 폭은 글자 + 46px로 자동입니다 |
| 숨김 | `hidden: true`. 자리를 남기지 않습니다 |
| 비활성 | `disabled: true`. 기존 SW 규칙대로 opacity .4이며 Hover가 없습니다 |
| 실행만 하는 버튼 | `toggle: false`. Active(Variant3)가 되지 않습니다 |
| 켜고 끄는 버튼 | 기본값(`toggle` 생략)입니다. 클릭하면 Active, 다시 클릭하면 Default입니다 |
| Active 제어 | `activeIds` + `onActiveChange`(controlled) 또는 `defaultActiveIds`. 하나만 켜려면 `selection="single"` |
| 하위 패널 | `panel: <SWRecentFiles files={…} onFileOpen={…} />`. 패널은 한 번에 하나만 열리고 열린 동안 하단 강조가 전체 외곽선이 됩니다 |
| 아이콘 | `currentColor` SVG 하나를 권장합니다(Default `#B6B4AF`, Hover·Active `#FF5C00` 자동). 색이 고정된 이미지 쌍이면 `hoverIcon`을 함께 줍니다 |
| 이벤트 | 항목 `onSelect(event)` → 메뉴 `onItemSelect(event)`. `event.active`는 클릭 후 상태입니다 |
## 4. 상태와 닫기 (Figma 기준 + 접근성 보완)
| 동작 | 결과 | 근거 |
|---|---|---|
| 로고 클릭 | 열기 / 다시 클릭하면 닫기 | Figma btn_home ON_CLICK → OVERLAY, 재클릭 닫기는 사용자 확정 |
| 타일 Hover | Variant2: 주황 gradient 1px 테두리, 아이콘·글자 `#FF5C00`. 아이콘·글자 위치·크기는 Default와 같음(2026-10-20 사용자 확정: Figma의 1px 위·왼쪽 이동은 적용하지 않음) | Figma Default ON_HOVER → V2 |
| 타일 클릭 | Variant3(Active, V2와 같은 모양) → 다시 클릭하면 Default | Figma V2 → V3 → Default |
| 메뉴 빈 곳 클릭 | 닫기 | Figma Group ON_CLICK → CLOSE (`closeOnBackgroundClick`) |
| Escape | 열린 패널을 먼저 닫고, 다시 누르면 메뉴를 닫은 뒤 로고로 Focus 복귀 | Figma 없음 · 접근성 (`closeOnEscape`) |
| 메뉴 밖 클릭 | 기본은 닫지 않음 | Figma overlay에 바깥 동작 없음 (`closeOnOutsideClick`) |
| ←/→/Home/End | 타일 사이 이동. 열린 패널 타일에서 ↓를 누르면 패널로 이동, ↑/↓로 행 이동, Enter로 파일 열기 | Figma 없음 · 접근성 |
| 최근파일 행 선택 | `onFileOpen(file)` 후 메뉴 닫기(`closeOnOpen`) | Figma 없음 · 검토 대상 |
## 5. 하지 말 것
- 타일·섹션·제목을 Figma 화면별로 따로 만들지 않습니다. 모든 메뉴는 `sections` 데이터 하나로 표현합니다.
- `hm-sw-home-menu*` class를 소비 코드에서 붙여 외형을 만들지 않습니다(`className`은 확장용입니다).
- 새 색·크기를 임의로 정하지 않습니다. 필요한 값은 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*` 토큰을 `style`로 덮어씁니다.
- Font Weight는 400/500/700만 씁니다(타일 라벨·파일명 700, 섹션 제목 400).
- 홈 버튼이나 창 영역을 메뉴 안에 다시 그리지 않습니다. 그것은 `SWHeader`가 담당합니다.
+259
View File
@@ -0,0 +1,259 @@
# 작업 B 1단계 — Figma 추가 디자인 분석·설계 보고 (2026-10-19)
> **2단계 이후 정정(우선 적용):** [2단계 보고](./stage2-report.md)를 기준으로 봅니다. 아래 1단계 본문은 당시 기록으로 남겨 둡니다.
> - 로고 클릭 = 2단 Header 메뉴, 기본 숨김, 재클릭 닫힘(사용자 최종 정정).
> - 662:67267은 Figma 주석 "최근파일" 상태 화면입니다. 최근파일 목록 662:67331과 2뎁스 미리보기 662:67471이 함께 붙으며, 두 노드는 `SWRecentFiles`로 구현했습니다.
> - V3 = 클릭으로 켜지고 다시 클릭하면 꺼지는 Active 상태입니다(사용자 지시).
> - 아이콘 prop은 `iconHover`가 아니라 `hoverIcon`입니다(Task A `SWToolButton`과 이름을 맞춤).
> - `accent`의 기본값은 `"auto"`입니다(패널이 열린 동안 outline).
> 상태: **분석·설계 보고만 작성했습니다(코드 수정 없음)**. 구현, Commit, Push는 하지 않았고 2단계는 사용자 승인 후 시작합니다.
> 대상: Figma `rj6wpsfk24lOWvWiVRAANP`, 속성Bar `26:10492`와 로고 클릭 노드 21개. 기준 브랜치는 `claude/sw-figma-logo-menu-36832b`(HEAD `5faa1f1`, `feature/sw-package`와 동일)입니다.
> 수집 자료: [figma-nodes.json](./figma-nodes.json)(노드별 메타데이터·디자인 컨텍스트 정규화·Prototype 연결), [screenshots/](./screenshots/)(노드 22개 PNG, 1x).
## 0. 수집 방법과 한계
| 항목 | 내용 | 상태 |
|---|---|---|
| Figma 접근 | 공식 Figma MCP(`get_metadata`, `get_design_context`, `get_variable_defs`, `get_screenshot`)를 사용했습니다. `figma-design-to-code` 스킬을 먼저 불러왔습니다. 인증이 필요한 `plugin:figma:figma` 서버는 사용하지 않았습니다. | 완료 |
| 세부 속성 | 읽기 전용 `use_figma`(Plugin API)로 reactions, gradient stop, stroke, effect, font, 변형 간 diff를 확인했습니다. 캔버스는 수정하지 않았습니다. | 완료 |
| 디자인 컨텍스트 저장 | React/Tailwind 원문 대신 구현에 필요한 값만 `figma-nodes.json`에 정규화했습니다. 원문은 같은 node id로 다시 받을 수 있습니다. | 정규화본만 저장 |
| Asset | `get_design_context`의 asset URL은 2026-10-26에 만료됩니다. 이번 단계에서는 asset을 받지 않았고, 2단계에서 다시 받습니다. | 미수집 |
| Storybook 비교 | `localhost:6024`에서 이미 실행 중인 Storybook의 `SW/Header/Overview` 속성Bar를 측정했습니다. 그 서버가 어느 checkout에서 실행되는지는 확인하지 못했습니다. Pane 폭은 390px, DPR은 1.5였습니다. | 실측 1회, 출처 미확인 |
| 형제 노트 | 공유 노트 폴더에 다른 세션의 노트가 없었습니다. 작업 A의 계획은 사용자 메모리의 확정 기록 기준으로 대조했습니다. | 확인 |
## 1. 노드별 표 (22개)
"로고 클릭 화면 21개"는 실제로 **화면 상태 2개**와 **구성 컴포넌트 19개**(섹션 제목 4개, 버튼 세트 15개 × 상태 3개)입니다. 다섯 번째 섹션 제목 `tit_마이세팅`(26:12628, "My Setting")은 요청 목록에 없지만 화면에서 사용되므로 참고로 함께 정리했습니다.
| # | Node | 이름 / 유형 | 크기 | 역할 | 상태·Prototype |
|---|---|---|---|---|---|
| 1 | 26:10492 | 속성Bar / FRAME (`header_green` → `선택한것` 26:10491 하위) | 1222×28 | 객체 속성 편집 strip | input ON_HOVER → hover(26:11659) |
| 2 | 26:10591 | 헤더/꾸러미 ver2/home on / COMPONENT | 1920×77 | **로고 클릭 overlay 본체** | btn_home ON_CLICK → OVERLAY(TOP_LEFT, 배경 없음), 메뉴 그룹 ON_CLICK → CLOSE |
| 3 | 662:67267 | 같은 이름 / FRAME | 1920×77(렌더 1935×193) | 2의 정적 사본. 팝업가이드 Variant5(K 미리보기)와 다른 하단 강조 | hover만 있음(open/close 없음) |
| 4 | 26:12604 | tit_작업중인파일 | 124×19 | 섹션 제목 "작업중인 파일" | 없음 |
| 5 | 26:12610 | tit_다른파일열기 | 123×19 | "다른파일 열기" | 없음 |
| 6 | 26:12622 | tit_파일손상시 | 111×19 | "파일 손상시" | 없음 |
| 7 | 26:12616 | tit_궁금하면 | 95×19 | "궁금하면" | 없음 |
| 8 | 26:11355 | btn_저장 / COMPONENT_SET | 3×(80×46) | 저장 · save · 그룹 첫 버튼 | D→hover V2, V2 click→V3, V3 click→D |
| 9 | 26:11371 | 복사저장 | 〃 | save_as · 중간 | 〃 |
| 10 | 26:11387 | 내보내기 | 〃 | file_open · 중간 | 〃 |
| 11 | 26:11403 | 도면묶기 | 〃 | inventory_2 · 마지막(구분선 없음) | 〃 |
| 12 | 26:11416 | 찾아열기 | 〃 | folder_open · 첫 버튼. 아이콘 top 5 | 〃. 구분선 gradient가 다름 |
| 13 | 26:11450 | 최근파일 | 〃 | 시계 붙은 파일 아이콘 · 중간 | **V2→V3 클릭 reaction 없음**. 구분선 gradient가 다름 |
| 14 | 26:11485 | 새파일 | 〃 | insert_drive_file · 마지막 | 〃 |
| 15 | 26:11507 | 복구하기 | 〃 | rotate_left · 첫 버튼 | 〃 |
| 16 | 26:11529 | 임시파일 | 〃 | file_copy · 마지막 | 〃 |
| 17 | 26:11542 | 디스플레이 | 〃 | monitor 24×22 · 첫 버튼. 라벨 "Display..."(원문에서 이미 말줄임) | 〃 |
| 18 | 26:11558 | 저장하기 | 〃 | 설정 저장 · 중간. gap 2 | 〃 |
| 19 | 26:11586 | 불러오기 | 〃 | save_alt · 마지막 | 〃 |
| 20 | 26:11599 | 설명보기 | 〃 | content_paste · 첫 버튼 | 〃 |
| 21 | 26:11615 | 문의하기 | 〃 | question_answer · 중간 | 〃 |
| 22 | 26:11631 | 소프트웨어 정보 | 〃 | 라벨 "S/W정보" · 마지막 | **V3가 button이 아니고 Default로 돌아가는 reaction 없음** |
## 2. 속성Bar 차이표 (Figma 26:10492 ↔ 현재 구현·Storybook)
비교 기준은 `packages/sw/src/theme/sw.css`(`.hm-sw-property-bar*`, `.hm-sw-header__input`), `SWHeaderPropertyBar.tsx`, Storybook `SW/Header/Overview` 실측입니다. **실제 차이가 있는 항목만** 적었습니다. 수정 위치는 모두 기존 공유 파일인 `sw.css`이므로 **통합 단계 항목**입니다.
| # | 항목 | Figma | 현재 구현 / Storybook | 영향 | 수정 위치 |
|---|---|---|---|---|---|
| P1 | 테두리 색 | 좌·우·하 1px **세로 gradient**(`--gra-border-green`): `#08251C` 0% → `#208769` 37.1% → `#08241C` 80.5% → `#051612` 100%(위→아래) | 단색 `1px solid #08251C`. Storybook computed도 `rgb(8,37,28)` 단색 | 중간 높이의 초록 하이라이트(`#208769`)가 없음 | **통합 단계** `sw.css` `.hm-sw-property-bar`(gradient border는 기존 hover border 기법 재사용 검토) |
| P2 | 객체명 글자 외곽선 | 검정 1.3px Stroke + `#FFC600` 0.3px. 기존 정책상 검정 외곽선만 적용 | `text-shadow: none`. 외곽선 목록(`sw.css` 약 1472–1477행)에 `.hm-sw-property-bar__object`가 없음 | 노란 글자 가장자리가 Figma보다 흐림 | **통합 단계** `sw.css`, `--hm-sw-header-text-outline` 적용 |
| P3 | 라벨 글자 외곽선 | 14px 라벨에 검정 1px Stroke | `text-shadow: none` | 라벨 대비가 약함 | **통합 단계** `sw.css`, 1px 외곽선(`--hm-sw-header-label-outline`) 적용 검토 |
| P4 | 라벨 박스 폭 | 고정 텍스트 박스(반지름 46, 지름·원주·면적 30), 가운데 정렬. 박스 안 좌 1.4–2.1px, 우 1.8–2.8px 여백 | 자연 폭(예: "원주" 26.9px). 라벨→입력 간격이 Figma보다 약 2–3px 좁음. 콘텐츠 폭은 527.6px로 Figma 538px보다 짧음 | 글꼴 대체(Dotum → Noto Sans KR) 영향과 섞여 있음 | **검토 대상**(§5 R-P1). 고친다면 통합 단계 `sw.css` |
| P5 | 세로 위치 | 내용 Frame top 4px(20px 행) | 실측 top 3.5px(bar 875.2 → field 878.7, 27px 영역 가운데 정렬) | 0.5px | **검토 대상**(§5 R-P2) |
**의도된 차이(수정하지 않음):** 바 폭은 Figma 1222px 고정 대신 콘텐츠 폭입니다(2026-10-19 사용자 확정). 글꼴은 Dotum Regular + Stroke 대신 Noto Sans KR이며 객체명·값은 700입니다(기존 Typography 정책). 입력 폭은 Figma 고정 58/66px 대신 값 길이를 따릅니다. 필드 순서와 객체명 문구는 Story fixture 데이터입니다.
**일치(차이 없음):** 높이 28, 배경 `#271F13`, 아래 radius 6, inset shadow `0 4 4 rgba(0,0,0,.6)`, 객체명 90px·`#FFC600`, 필드 간격 16·라벨-입력 6, 라벨 `#F6F4F2`·자간 4%, 입력 `#E60403`·자간 2%·padding 2/4/2/6·radius 2. 입력 상태 Default(면 없음) / hover(24px, 1px .25) / on(`#0F0D0A`, 1px .3)과 hover reaction도 같습니다. 아이콘은 Figma와 구현 모두 없습니다.
**미검증:** 1920px 폭 실측, Hover·편집 상태 Storybook 실측, P1–P3를 고친 뒤의 렌더 비교.
## 3. 로고 클릭 화면 분석
### 3.1 화면 구성 (26:10591 기준, 1920 디자인)
```
y 0 ─ border-top 2px #5F4B0F ───────────────────────────────────────────────────
[btn_home 80] (기존 Header가 비쳐 보임: 문서 제목 등) [섹션 5개, x377–1678] [창 영역 240: 인쇄·K pill | 창이동 | – | ×]
y 46 ─ border-bottom 2px #5F4B0F ───────────────
y 47 ─ 하단 강조 strip 30px(섹션 뒤, 아래로 보이는 주황 gradient)
y 77 ─ 메뉴 끝(Header 48px보다 29px 아래로 내려옴)
```
- **Overlay**: 로고 클릭 시 `home on` 전체가 기존 Header 위에 TOP_LEFT로 겹칩니다(배경 없음). `bg black`과 `document_tit`가 숨겨져 있어 그 부분은 기존 Header가 그대로 보입니다. 즉 **새로 그려지는 것은 섹션 메뉴(x375–1680)이고**, 홈·창 영역은 기존 Header와 같은 컴포넌트 사본입니다.
- **섹션**(5개): 제목 badge(19px, Union 도형) + 버튼 그룹 box(46px) + 섹션 면(75px). 섹션 폭 = 버튼 수 × 80 + 좌우 padding(10, 첫·마지막 섹션은 한쪽 11)입니다.
- 작업중인 파일: 저장 · 복사저장 · 내보내기 · 도면묶기
- 다른파일 열기: 찾아열기 · 최근파일 · 새파일
- 파일 손상시: 복구하기 · 임시파일
- My Setting: Display... · 저장하기 · 불러오기
- 궁금하면: 설명보기 · 문의하기 · S/W정보
- **버튼**: 80×46, 아이콘 18px(31,6), 라벨 14px(top 25). 마지막 버튼을 뺀 버튼 오른쪽에 1×44 gradient 구분선이 있습니다.
### 3.2 공통 영역과 화면별 차이
| 구분 | 내용 |
|---|---|
| 공통(두 화면 동일) | 섹션 5개·버튼 15개·제목·구분선·면 gradient, 홈 버튼, 창 영역(인쇄·K·창이동·최소·종료), border-top/bottom `#5F4B0F` |
| 26:10591만 | 하단 강조 = 섹션별 사각형 5개(`#FEC501` + `#5F4B0F → #FF5100` gradient), btn_home 클릭 → open, 메뉴 클릭 → close |
| 662:67267만 | 하단 강조 = 메뉴 전체 1305×30에 2px gradient stroke(`#FF5100 → #5F4B0F`, fill 숨김), 팝업가이드 Variant5(K 미리보기 209×186, "다중인쇄/도면관리") 표시, open/close reaction 없음, btn_home 배경 asset이 다름 |
| 버튼 세트 공통 | Default / Variant2 / Variant3. **V2와 V3는 시각 속성이 같습니다**(Plugin API로 하위 노드 전체를 비교했고 위치만 다름) |
| 버튼별 차이 | 아이콘 asset과 위치(찾아열기 top 5, Display 24×22), 라벨 박스 폭(33–70), 그룹 내 위치에 따른 hover radius(첫 3 0 0 3 / 중간 0 / 마지막 0 3 3 0), 구분선 유무(마지막 없음), 찾아열기·최근파일만 구분선 gradient가 다름, 저장하기·S/W정보만 flex gap 2 |
### 3.3 상태
| 상태 | Figma 근거 | 시각 |
|---|---|---|
| Default | 각 세트 Default | 면 없음, 아이콘 회색, 라벨 `#F6F4F2` + 검정 외곽선 |
| Hover | Default ON_HOVER → V2 | 1px INSIDE 주황 세로 gradient 테두리(`#DA6813` 8.5% / `#FFBB89` 37.9% / `#E5690D` 71.9% / `#AE3607` 92.8%), 아이콘·라벨 `#FF5C00`, 아이콘·라벨 1px 위·왼쪽 이동 |
| Pressed / 선택(?) | V2 ON_CLICK → V3, V3 ON_CLICK → Default | V2와 동일. 의미는 미정(§5 R-3) |
| Open / Close | btn_home ON_CLICK → OVERLAY, 메뉴 영역 ON_CLICK → CLOSE | — |
| Focus / Disabled / 키보드 | **Figma에 없음** | §5 검토 대상 |
### 3.4 전환 관계 (Prototype)
```
[기존 Header] --btn_home click--> [home on overlay 26:10591]
[home on] --메뉴 영역(Group 2882) click--> CLOSE → [기존 Header]
버튼: Default --hover--> V2 --click--> V3 --click--> Default (최근파일: V2에서 끝, S/W정보: V3에서 끝)
팝업가이드: Default --hover--> Variant5(K 미리보기) ※ 기존 SWHeaderKGuide와 같은 대상
```
Figma에는 메뉴 밖 클릭, Escape, 로고 재클릭으로 닫기, 각 버튼의 실제 동작(파일 대화상자, 최근 파일 목록 등)이 정의되어 있지 않습니다(§5).
## 4. 공통 컴포넌트 구조·공개 API 초안
### 4.1 재사용 우선순위 판단
| 단계 | 후보 | 판단 |
|---|---|---|
| 기존 SW | `SWHeaderHome`(`onHomeClick`), `SWHeaderKGuide`, 창 제어, `useSWHeaderDropdown`의 닫기 계약(Escape·바깥 클릭·Tab 이탈·Focus 복귀), 텍스트 외곽선·focus 토큰, `SWTheme` | **재사용.** Header 쪽은 그대로 두고 메뉴만 새로 만듭니다. 홈·창 영역을 다시 그리지 않습니다 |
| 작업 A 계획 | `SWToolButton`(pill/text/document), `SWToolGroup`(selection), `SWPopup*` | §4.4 참고. 그대로 쓸 수 있는 appearance가 없습니다 |
| shadcn / Radix | Popover, NavigationMenu | `@hm-design/sw`는 `@hm-design/ui`·Radix에 의존하지 않습니다(독립 패키지). 새 의존성은 **별도 승인 대상**이므로 기존 SW hook 방식을 따릅니다 |
| 최소 Custom | 섹션 badge, 그룹 box, 80×46 타일 버튼 | 시각 요소만 새로 만듭니다(Figma 근거 있음) |
### 4.2 구조
```
<SWHomeMenu> overlay 패널(region/dialog 역할은 §5 R-6), open 상태·닫기 계약
<SWHomeMenuSection title> 섹션 면 + 제목 badge(폭은 글자에 맞춤) + 그룹 box
<SWHomeMenuItem icon label /> 80×46 타일 button. 마지막 항목은 구분선 자동 생략, 첫/마지막 hover radius 자동
```
데이터형(`sections`)과 조합형(`children`) 중 하나만 써도 21개를 모두 표현할 수 있습니다. 위치·첫/마지막 여부·구분선·섹션 폭은 **항목 수에서 자동 계산**합니다. 따라서 AI가 항목만 추가해도 같은 스타일이 적용됩니다.
### 4.3 공개 API 초안
```ts
export interface SWHomeMenuItemData {
id: string
label: string // 한 줄, 넘치면 말줄임 + title (기존 규칙)
icon?: ReactNode // currentColor SVG 권장: Default 회색 → Hover #FF5C00을 CSS가 처리
iconHover?: ReactNode // 색 고정 이미지 쌍일 때만(기존 close/close-hover 방식)
disabled?: boolean // 시각은 검토 대상(R-4)
hidden?: boolean // 미렌더, 자리 없음(작업 A 숨김 정책과 맞춤)
title?: string // tooltip/전체 이름
"aria-label"?: string
closeOnSelect?: boolean // 기본 true (Figma: 메뉴 클릭 → CLOSE)
onSelect?: (item: SWHomeMenuItemData) => void
}
export interface SWHomeMenuSectionData {
id: string
title: string
items: readonly SWHomeMenuItemData[]
hidden?: boolean
}
export interface SWHomeMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
sections?: readonly SWHomeMenuSectionData[] // 또는 children(<SWHomeMenuSection>)
open?: boolean // controlled
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
onItemSelect?: (item: SWHomeMenuItemData, section: SWHomeMenuSectionData) => void
/** 닫힐 때 Focus를 돌려줄 대상(기본: 연결된 홈 버튼). 통합 단계에서 SWHeader가 자동 연결 */
returnFocusRef?: RefObject<HTMLElement>
}
export function SWHomeMenu(props: SWHomeMenuProps): JSX.Element
export function SWHomeMenuSection(props: { title: string; children: ReactNode; className?: string }): JSX.Element
export function SWHomeMenuItem(props: ButtonHTMLAttributes<HTMLButtonElement> & { label: string; icon?: ReactNode; iconHover?: ReactNode }): JSX.Element
```
- **텍스트·아이콘·메뉴·이벤트·상태는 모두 외부에서 지정합니다.** 패키지에는 CAD 동작이 없습니다. Figma의 15개 메뉴는 Storybook fixture(또는 선택형 기본 아이콘 세트, §6 A-2)로만 제공합니다.
- **독립 단계 연결 방식(SWHeader 무수정):** `<SWHeader onHomeClick={() => setOpen(v => !v)} />`와 `<SWHomeMenu open onOpenChange />`를 같은 상대 위치 컨테이너 안에 둡니다. 위치는 host가 `style`로 지정합니다.
- **통합 단계 연결 방식(안):** `SWHeader`에 `homeMenu?: SWHomeMenuProps | ReactNode`를 추가합니다. 홈 버튼에 `aria-expanded`/`aria-controls`를 붙이고, 객체 영역 시작 x에 앵커하며, 열림 중 홈 버튼 상태를 표시합니다. 작업 A의 `leading` 슬롯·파일 열기 배치와 같은 파일을 수정하므로 통합 단계에서만 합니다.
- **CSS:** 새 파일 `packages/sw/src/theme/sw-home-menu.css`에 `.hm-sw-home-menu*`만 둡니다. 새 토큰은 `--hm-sw-home-menu-*`이고, 기존 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`·`--hm-sw-header-focus-color`·font 토큰은 그대로 읽습니다. 전역 선택자와 Dark 분기는 두지 않습니다. 단독 사용 시 `<SWTheme>` 안에서 렌더합니다.
- **Font Weight:** 버튼 라벨 700(기존 "값·제목·토글 700"과 Figma 'Dotum/bold' 명칭), 제목 badge 12px 400(Stroke 없는 검정 글자)을 제안합니다. 600은 쓰지 않습니다. 최종값은 §5 R-5에서 확인합니다.
### 4.4 작업 A와 겹치는 부분
| 작업 A 항목 | 겹침 | 제안 |
|---|---|---|
| `SWToolButton` appearance pill/text/document | 80×46 세로 아이콘+라벨 타일은 승인된 appearance에 없습니다 | B는 `SWHomeMenuItem`을 독립으로 만듭니다. 통합 단계에서 `SWToolButton appearance="tile"`로 합칠지 정합니다(**별도 승인 대상** A-1) |
| `SWToolGroup` selection="single" | 메뉴에는 선택 상태가 없습니다(V3 의미 미정) | 사용하지 않습니다. 키보드 이동 규칙만 맞춥니다 |
| `SWPopup`/`Section`/`Row` | 밝은 속성창 Popup과 시각·역할이 다릅니다 | 재사용하지 않습니다. 이름이 비슷한 `Section`은 namespace(`SWHomeMenuSection`)로 구분합니다 |
| `hiddenControls`·`show*` | 항목 숨김 개념이 같습니다 | 항목·섹션 `hidden`(미렌더, 자리 없음)으로 A와 같은 의미를 씁니다 |
| `documentOpenPlacement`, 문서 New/Open | "찾아열기·새파일"이 `onDocumentOpen`/`onDocumentNew`와 중복될 수 있습니다 | **검토 대상**(R-7). 컴포넌트는 콜백만 받습니다 |
| `leading` 슬롯·파일 열기 배치·반응형 | 메뉴 시작 x(Figma 375 = 문서 제목 끝)가 바뀝니다 | 통합 단계에서 실제 DOM 위치로 앵커합니다(x 하드코딩 금지) |
| 속성Bar(`.hm-sw-property-bar`) | 같은 49px 아래 overlay 영역을 씁니다(메뉴는 77px까지) | z-index·동시 표시는 R-8 |
## 5. 검토 대상 (Figma에 없는 동작 — 확정하지 않음)
| ID | 항목 | 제안(미확정) |
|---|---|---|
| R-1 | 메뉴 닫기: 바깥 클릭·Escape·로고 재클릭·Tab 이탈 | 기존 SW Dropdown 계약과 같게 처리하고 Focus는 홈 버튼으로 복귀 |
| R-2 | 항목 클릭 후 닫힘 | Figma CLOSE를 따라 기본 닫힘, `closeOnSelect`로 변경 |
| R-3 | V3(클릭 후)의 의미: 누름(pressed) 순간 표시인지, 유지되는 선택 상태인지. 최근파일·S/W정보의 reaction 누락 | 누름(`:active`) 표시로만 구현하고 유지 상태는 만들지 않음 |
| R-4 | Disabled·Focus-visible 시각 | 기존 SW 규칙(opacity .4, focus 2px `--hm-sw-header-focus-color`) |
| R-5 | 라벨·제목 Font Weight(700/400), Stroke 적용 범위 | §4.3 제안 |
| R-6 | 접근성 역할: `role="menu"` + 화살표 이동 vs region + button + Tab | button 목록 + 섹션 `aria-labelledby`, 화살표 이동은 작업 A 규칙과 맞춤 |
| R-7 | 찾아열기·새파일과 기존 `onDocumentOpen`/`onDocumentNew` 관계, 최근파일 하위 목록 여부 | 컴포넌트는 콜백만 받고, 연결은 host 또는 통합 단계에서 결정 |
| R-8 | 속성Bar·Dropdown·K 미리보기와의 z-index와 동시 표시 | 메뉴가 열리면 다른 Dropdown을 닫고 속성Bar보다 위에 표시 |
| R-9 | 1920 미만 반응형(메뉴 1305px) | 가용 폭 안 내부 가로 스크롤(속성Bar 정책과 동일). 섹션 접기·줄바꿈은 새 디자인이 필요해 A-4로 분리 |
| R-10 | 하단 강조 strip 두 가지(섹션별 vs 전체 2px) 중 기준 | 기본 디자인 컴포넌트인 26:10591(섹션별) |
| R-11 | home on의 border-top/bottom `#5F4B0F`와 홈 버튼 배경 asset이 기존 Header와 다른지 | 2단계에서 기존 `home-bg*.svg`·Header 선과 대조한 뒤 보고 |
| R-P1 | 속성Bar 라벨 고정 박스 여백(P4) | 글꼴 대체 영향이라 유지 권장 |
| R-P2 | 속성Bar 세로 0.5px(P5) | 유지 권장 |
## 6. 별도 승인 대상 (새 디자인 판단 또는 승인 범위 초과)
| ID | 항목 | 이유 |
|---|---|---|
| A-1 | `SWToolButton`에 `tile` appearance 추가 또는 `SWHomeMenuItem` 별도 유지 | 작업 A에서 확정된 공개 API 변경 |
| A-2 | Figma 메뉴 아이콘 15개(+hover 15개)를 패키지 기본 asset으로 제공할지, Story fixture로만 둘지 | 공개 asset·번들 크기 증가 |
| A-3 | `SWHeader`에 `homeMenu` prop·홈 버튼 열림 상태 추가 | 기존 Header API·스타일 변경(통합 단계) |
| A-4 | 좁은 폭에서 섹션 접기·More 이동 등 새 반응형 디자인 | Figma에 없는 디자인 |
| A-5 | Radix 등 새 dependency 사용 | 현재 `@hm-design/sw`에는 런타임 의존성이 없음 |
| A-6 | 속성Bar P1–P3 수정 | 기존 공유 CSS 변경이며, 사용자 확정 디자인 범위 안인지 확인 필요 |
## 7. 파일 경계
| 구분 | 파일 | 내용 |
|---|---|---|
| **B 독립(신규만)** | `packages/sw/src/components/sw-home-menu/**` | `SWHomeMenu.tsx`, `SWHomeMenuSection.tsx`, `SWHomeMenuItem.tsx`, `types.ts`, `index.ts`, (A-2 승인 시) `assets/` |
| | `packages/sw/src/theme/sw-home-menu.css` | `.hm-sw-home-menu*` 스타일·토큰 |
| | `src/sw/sw-home-menu.stories.tsx` | Figma 21개 재현, 단독, 동적 데이터, Header 조합(host wiring) Story. Story CSS는 새 파일만 |
| | `docs/sw/figma-additional/**` | 이 보고서, 수집 자료, 2단계 검증 기록 |
| **통합 단계로 미룸** | `packages/sw/src/index.ts`, 테마 진입 CSS(`sw.css` import 체인) | export·CSS 연결(2단계 Story는 상대 경로 import로 확인) |
| | `packages/sw/src/components/sw-header/**`, `packages/sw/src/theme/sw.css` | `homeMenu` 연결(A-3), 속성Bar P1–P3(A-6) |
| | `build.mjs`, `package.json` | asset·CSS 번들 포함 |
| | `packages/sw/ai/**`, `packages/sw/README.md`, `docs/sw/sw-header-*.md` | AI 가이드·카탈로그·사용법. 2단계에서는 초안을 `docs/sw/figma-additional/`에 둠 |
| | 기존 stories(`src/sw/sw-header*.stories.tsx`, `sw-header-overview.tsx`, `sw-stories.css`) | 수정하지 않음 |
## 8. 2단계 구현·검증 계획 (승인 후)
**구현 순서**
1. Asset 재수집(만료 전) → A-2 결정에 따라 경로 확정 → 섹션 badge·그룹 box·구분선·타일 CSS
2. `SWHomeMenuItem` → `SWHomeMenuSection` → `SWHomeMenu`(open 계약, R-1·R-2 결정 반영)
3. Story `SW/Home Menu`: Figma 재현, 단독, 동적, Header 조합
4. AI 가이드 초안(`docs/sw/figma-additional/ai-home-menu-draft.md`) → 통합 단계에서 `packages/sw/ai/`로 이동
**검증 범위**(실행하지 않은 항목은 미검증으로 기록)
| 범위 | 방법 | 기준 |
|---|---|---|
| Figma 비교 | 1920 viewport, Figma 스크린샷과 같은 배율 비교 + 요소 실측 | 섹션 x·폭(340/260/180/260/261), 높이 75·46·19, 색·gradient·shadow |
| 21개 재현 | 제목 5개(참고 1개 포함), 버튼 15개 × Default/Hover/Pressed 상태 Story | 노드별 체크리스트 |
| 단독 사용 | `<SWTheme>` 안 `SWHomeMenu`만 렌더 | Header 없이 동작·스타일 유지, 전역 CSS 누수 없음 |
| 동적 콘텐츠 | 항목 추가·삭제·숨김, 긴 라벨, 아이콘 없음, 섹션 1개·6개, 비동기 갱신 | 폭·구분선·radius 자동 계산, 말줄임 |
| Header/Popup 조합 | `onHomeClick` host wiring, 속성Bar·Dropdown·K 가이드 동시 상태 | 닫기 계약, Focus 복귀, z-index(R-8) |
| 반응형 | 1920/1440/1280/1080/900/768/640/390 | 가로 넘침 없이 내부 스크롤(R-9) |
| Storybook | 기존 SW Story 회귀 없음 | 기존 Story 무수정 확인 |
| AI 가이드 | 문서만으로 새 메뉴 항목·섹션 생성 시나리오 1건 | 임의 값 없이 같은 스타일 |
| 독립 React 19 소비 프로젝트 | 통합 단계 build 후 packed tarball 설치 | 타입·CSS·asset 로드. **dist·export 연결이 통합 단계라 2단계에서는 미검증 예정** |
+115
View File
@@ -0,0 +1,115 @@
{
"source": {
"fileKey": "rj6wpsfk24lOWvWiVRAANP",
"collectedAt": "2026-10-19",
"tools": "공식 Figma MCP get_metadata / get_design_context / get_variable_defs / get_screenshot + 읽기 전용 use_figma(Plugin API: reactions, fills, strokes, effects, fonts)",
"page": "2. 헤더 (SECTION) — 속성Bar만 header_green 26:10332의 'Property 1=선택한것' 26:10491 하위",
"note": "get_design_context의 React/Tailwind 원문은 화면 재현용 참고 코드라 그대로 저장하지 않고, 구현에 필요한 값만 아래에 정규화했습니다. 원문은 같은 node id로 다시 받을 수 있습니다. 응답에 들어 있던 asset URL은 2026-10-26에 만료되므로 2단계에서 다시 받아야 합니다. 스크린샷은 screenshots/<node>.png (1x)입니다."
},
"nodes": [
{
"id": "26:10492", "name": "속성Bar", "type": "FRAME", "size": [1222, 28],
"screenshot": "screenshots/26-10492_property-bar.png",
"variables": { "--accent-yellow": "#FFC600", "--text-brown100": "#F6F4F2", "--text-red": "#E60403", "--gra-border-green": "(gradient, 값 없음)" },
"metadata": "Rectangle 7541 1222x28 + Frame(top 4, h 20) = 객체명 Dotum_14_bold 90x16 + Frame(gap 16) x4 [라벨 Dotum_regular_14 (46|30)x16 + gap 6 + input 58|66x20]",
"designContext": {
"background": "#271F13",
"border": "좌·우·하 1px (상 0) GRADIENT_LINEAR 위→아래 #08251C 0% / #208769 37.1% / #08241C 80.5% / #051612 100%",
"radius": "0 0 6 6",
"effect": "INNER_SHADOW 0 4 4 rgba(0,0,0,.6)",
"object": "90px 가운데 정렬, Dotum Regular 14 / 4%, #FFC600, 아래층 Stroke 검정 1.3px + 위층 #FFC600 0.3px",
"label": "Dotum Regular 14 / 4%, #F6F4F2, Stroke 검정 1px, 고정 폭 텍스트 박스(46 또는 30) 가운데 정렬",
"fieldGap": 16, "labelInputGap": 6,
"input": "component set 'input'(26:11656) Default 26:11657 58x20 pad 2/4/2/6 r2 면·테두리 없음 / hover 26:11659 58x24 1px rgba(212,203,189,.25) / on 26:11661 58x24 #0F0D0A + 1px .3. 값 Dotum Regular 14 / 2%, 검정 1.3 + #E60403 0.3"
},
"reactions": ["input 4개: ON_HOVER → CHANGE_TO input hover(26:11659)"]
},
{
"id": "26:10591", "name": "헤더/꾸러미 ver2/home on", "type": "COMPONENT", "size": [1920, 77],
"screenshot": "screenshots/26-10591_home-on.png",
"role": "로고(btn_home) 클릭 시 열리는 overlay 자체(TOP_LEFT, 배경 없음, 바깥 클릭 상호작용 없음)",
"variables": { "--text-brown100": "#F6F4F2", "--gra-border-brown1": "(gradient)", "--gra-border-brown2": "(gradient)", "--accent-border-yellow": "(gradient)" },
"hiddenLayers": ["bg balck 1920x48", "Rectangle 6671 310x48", "header/document_tit 310x48"],
"metadata": "창 영역 240x48(x1680) · 최소/종료 32x48 x2 · pill(인쇄 + 팝업가이드) 85x42 · 창이동 motion 88x48 · border-top/bottom 1920x2 #5F4B0F · btn_home 80x48 · Group 2882 x375 1305x77 = 하단 강조 strip(y47, 30px, 섹션별 사각형 5개) + 섹션 5개(x377 1301x75)",
"sections": [
{ "frame": "Frame 2408", "width": 340, "padding": "5 10 5 11", "title": "tit_작업중인파일 26:12604 124x19", "group": "g_작업중인파일 320x46", "items": ["btn_저장 26:11355", "복사저장 26:11371", "내보내기 26:11387", "도면묶기 26:11403"] },
{ "frame": "Frame 2409", "width": 260, "padding": "5 10", "title": "tit_다른파일열기 26:12610 123x19", "group": "g_다른파일열기 240x46", "items": ["찾아열기 26:11416", "최근파일 26:11450", "새파일 26:11485"] },
{ "frame": "Frame 2410", "width": 180, "padding": "5 10", "title": "tit_파일손상시 26:12622 111x19", "group": "g_파일손상시 160x46", "items": ["복구하기 26:11507", "임시파일 26:11529"] },
{ "frame": "Frame 2411", "width": 260, "padding": "5 10", "title": "tit_마이세팅 26:12628 107x19 (요청 목록 밖, 'My Setting')", "group": "Frame 2407 240x46", "items": ["디스플레이 26:11542", "저장하기 26:11558", "불러오기 26:11586"] },
{ "frame": "Frame 2412", "width": 261, "padding": "5 10 5 11", "title": "tit_궁금하면 26:12616 95x19", "group": "g_궁금하면 240x46", "items": ["설명보기 26:11599", "문의하기 26:11615", "소프트웨어 정보 26:11631"] }
],
"designContext": {
"section": "h75, 1px #081813, radius 0 0 3 3, fill 2중 gradient(세로 #18150E 0 / #262015 21.3% / #332B1C 47% / #38301F 75% / #18150E 100% + 가장자리 어둡게 #16120A 0 / 투명 54.5% / #16120A 100%), 안쪽 1px #20190E 사각형",
"group": "h46, 1px GRADIENT stroke(#886D35 0 / #423625 8.8% / #F3DBA8 26.2% / #0E0B06 83.8% / #574B30 100%), radius 3, fill gradient(#19160E 2% / #38301F 43.7% / #463C29 67.7% / #2F281A 100%), DROP_SHADOW 0 2 radius 4 rgba(0,0,0,.6)",
"title": "Union 도형(위아래 반전) 19px, fill gradient #887757 → #A8997C 75.6% → #C3B79E, stroke 1px #7B6332 → #9F8D68, DROP_SHADOW 0 -2 radius 4 rgba(0,0,0,.4), 글자 Dotum Regular 12 / 2% #000 가운데, group 위에 -1px 겹침",
"bottomStrip": "y47 h30, 섹션별 사각형(343/263/181/262/264, -2px 겹침, radius 0 0 3 3) fill #FEC501 + GRADIENT #5F4B0F → #FF5100",
"headerLines": "border-top 0-2px, border-bottom 46-48px, #5F4B0F 1920px"
},
"reactions": [
"btn_home 26:10652 ON_CLICK → OVERLAY 26:10591",
"Group 2882(메뉴 전체) ON_CLICK → CLOSE",
"메뉴 버튼 15개 ON_HOVER → 각 Variant2",
"header/btn_최소최대화·btn_종료 ON_HOVER → hover",
"Component 743(인쇄) ON_HOVER → hover 26:224",
"꾸러미ver2/팝업가이드 ON_HOVER → Variant5 26:426 (209x186, SMART_ANIMATE)",
"창이동 motion ON_HOVER → 26:10258 SMART_ANIMATE",
"header/document_tit(숨김) MOUSE_ENTER → OVERLAY 26:10667 hover"
]
},
{
"id": "662:67267", "name": "헤더/꾸러미 ver2/home on", "type": "FRAME", "size": [1920, 77], "renderBounds": [1935, 193],
"screenshot": "screenshots/662-67267_home-on-popup-guide.png",
"role": "26:10591과 같은 구성의 독립 Frame. 팝업가이드가 Variant5(K 미리보기 209x186) 상태이고 하단 강조가 다름",
"diffFrom_26:10591": [
"하단 strip: 섹션별 사각형 4개 숨김, Rectangle 6991만 1305x30, fill(#FF5100) 숨김 + 2px GRADIENT stroke #FF5100 → #5F4B0F",
"꾸러미ver2/팝업가이드 = Variant5 (검정 미리보기, '다중인쇄/도면관리')",
"btn_home 클릭·메뉴 CLOSE reaction 없음 (정적 상태 화면)",
"btn_home 배경 asset이 26:10591과 다른 파일(a80fd vs 9931a)"
],
"reactions": ["창 버튼·인쇄·창이동·메뉴 버튼 15개 ON_HOVER만 (26:10591과 같은 대상의 다른 사본 id)"]
},
{ "id": "26:12604", "name": "tit_작업중인파일", "type": "COMPONENT", "size": [124, 19], "text": "작업중인 파일", "textBox": [78, 14], "screenshot": "screenshots/26-12604_tit-working-file.png" },
{ "id": "26:12610", "name": "tit_다른파일열기", "type": "COMPONENT", "size": [123, 19], "text": "다른파일 열기", "textBox": [78, 14], "screenshot": "screenshots/26-12610_tit-open-other.png" },
{ "id": "26:12622", "name": "tit_파일손상시", "type": "COMPONENT", "size": [111, 19], "text": "파일 손상시", "textBox": [66, 14], "screenshot": "screenshots/26-12622_tit-file-damaged.png" },
{ "id": "26:12616", "name": "tit_궁금하면", "type": "COMPONENT", "size": [95, 19], "text": "궁금하면", "textBox": [49, 14], "screenshot": "screenshots/26-12616_tit-curious.png" }
],
"titleWidthRule": "제목 폭 = 글자 폭 + 약 46px(좌우 약 23px). 5개 모두 같은 Union 도형을 폭만 늘린 것",
"buttonSets": {
"common": {
"type": "COMPONENT_SET 120x218, variants 'Property 1' = Default / Variant2 / Variant3, 각 80x46",
"default": "면 없음, 아이콘 18x18 at (31,6) 회색 #B6B4AF 계열 SVG, 라벨 Dotum Regular 14 / 4% #F6F4F2 + 검정 1.3 / 자기 색 0.3 Stroke, top 25 가운데",
"variant2": "1px INSIDE GRADIENT stroke 위→아래 #DA6813 8.5% / #FFBB89 37.9% / #E5690D 71.9% / #AE3607 92.8%, 아이콘 hover SVG(#FF5C00), 라벨 #FF5C00, 아이콘·라벨 1px 위·왼쪽 이동(31,6→30,5 / top 25→24), radius = 그룹 첫 버튼 3 0 0 3, 마지막 0 3 3 0, 중간 0",
"variant3": "Variant2와 시각 속성 동일(Plugin API 전체 하위 노드 비교 결과 위치 y만 다름)",
"divider": "마지막 버튼을 뺀 버튼 오른쪽(-1px, hover -2px)에 1x44 세로선 GRADIENT(#886D35/#423625/#F3DBA8/#0E0B06/#574B30) + DROP_SHADOW -1 0 radius 2 #000",
"prototype": "Default ON_HOVER → Variant2, Variant2 ON_CLICK → Variant3, Variant3 ON_CLICK → Default"
},
"items": [
{ "set": "26:11355", "name": "btn_저장", "label": "저장", "icon": "save", "position": "first", "variants": ["26:11356", "26:11361", "26:11366"], "screenshot": "screenshots/26-11355_btn-save.png", "note": "Default에 숨김(visible=false) 노란 gradient stroke 존재" },
{ "set": "26:11371", "name": "복사저장", "label": "복사저장", "icon": "save_as", "position": "middle", "variants": ["26:11372", "26:11377", "26:11382"], "screenshot": "screenshots/26-11371_btn-save-copy.png" },
{ "set": "26:11387", "name": "내보내기", "label": "내보내기", "icon": "file_open", "position": "middle", "variants": ["26:11388", "26:11393", "26:11398"], "screenshot": "screenshots/26-11387_btn-export.png" },
{ "set": "26:11403", "name": "도면묶기", "label": "도면묶기", "icon": "inventory_2", "position": "last", "variants": ["26:11404", "26:11408", "26:11412"], "screenshot": "screenshots/26-11403_btn-bundle.png" },
{ "set": "26:11416", "name": "찾아열기", "label": "찾아열기", "icon": "folder_open (top 5)", "position": "first", "variants": ["26:11417", "26:11428", "26:11439"], "screenshot": "screenshots/26-11416_btn-find-open.png", "note": "구분선 gradient가 다른 버튼과 다름(0deg 갈색 #493927 계열)" },
{ "set": "26:11450", "name": "최근파일", "label": "최근파일", "icon": "insert_drive_file(시계 포함)", "position": "middle", "variants": ["26:11451", "26:11462", "26:11473"], "screenshot": "screenshots/26-11450_btn-recent.png", "note": "구분선 gradient 2중(갈색 + 초록). Variant2 → Variant3 클릭 reaction 없음" },
{ "set": "26:11485", "name": "새파일", "label": "새파일", "icon": "insert_drive_file", "position": "last", "variants": ["26:11486", "26:11493", "26:11500"], "screenshot": "screenshots/26-11485_btn-new-file.png" },
{ "set": "26:11507", "name": "복구하기", "label": "복구하기", "icon": "rotate_left", "position": "first", "variants": ["26:11508", "26:11515", "26:11522"], "screenshot": "screenshots/26-11507_btn-recover.png" },
{ "set": "26:11529", "name": "임시파일", "label": "임시파일", "icon": "file_copy", "position": "last", "variants": ["26:11530", "26:11534", "26:11538"], "screenshot": "screenshots/26-11529_btn-temp-file.png" },
{ "set": "26:11542", "name": "디스플레이", "label": "Display...", "icon": "monitor 24x22 at (29,6)", "position": "first", "variants": ["26:11543", "26:11548", "26:11553"], "screenshot": "screenshots/26-11542_btn-display.png", "note": "라벨 박스 70px, Figma 원문 자체가 '...' 말줄임" },
{ "set": "26:11558", "name": "저장하기", "label": "저장하기", "icon": "설정 저장(그룹 아이콘 16.3x16.5)", "position": "middle", "variants": ["26:11559", "26:11568", "26:11577"], "screenshot": "screenshots/26-11558_btn-setting-save.png", "note": "flex gap 2px" },
{ "set": "26:11586", "name": "불러오기", "label": "불러오기", "icon": "save_alt", "position": "last", "variants": ["26:11587", "26:11591", "26:11595"], "screenshot": "screenshots/26-11586_btn-setting-load.png" },
{ "set": "26:11599", "name": "설명보기", "label": "설명보기", "icon": "content_paste", "position": "first", "variants": ["26:11600", "26:11605", "26:11610"], "screenshot": "screenshots/26-11599_btn-help.png" },
{ "set": "26:11615", "name": "문의하기", "label": "문의하기", "icon": "question_answer", "position": "middle", "variants": ["26:11616", "26:11621", "26:11626"], "screenshot": "screenshots/26-11615_btn-inquiry.png" },
{ "set": "26:11631", "name": "소프트웨어 정보", "label": "S/W정보", "icon": "정보(Frame 2405)", "position": "last", "variants": ["26:11632", "26:11639", "26:11646"], "screenshot": "screenshots/26-11631_btn-sw-info.png", "note": "Variant3가 button 형태가 아니며 Default로 돌아가는 클릭 reaction 없음" }
]
},
"supplementary": {
"note": "요청 21개 밖, 2단계 재확인(2026-10-19)에서 찾은 노드. 같은 SECTION '2. 헤더'의 주석 '최근파일'(26:11784) 아래 662:67267과 함께 배치. 서로 연결된 prototype reaction 없음(drop ON_CLICK의 action 비어 있음).",
"nodes": [
{ "id": "662:67331", "name": "최근파일", "type": "FRAME", "size": [354, 750], "screenshot": "screenshots/662-67331_recent-files.png",
"position": "662:67267 기준 x 808, y 77 (최근파일 타일 왼쪽 + 1, 메뉴 바로 아래)",
"designContext": "frame 1px #DA6813 좌·우·하, radius 0 0 3 3 / drop 354x750 #27241D, 1px #27241D, radius 0 0 4 4, shadow 0 2 4 rgba(0,0,0,.25) + inset 0 4 8 rgba(0,0,0,.4) / row 'header/최근파일/list' 50px, px 8, gap 8 (배지 없는 행 px 45) / icon_dualmonitor 29x28 (84a46 왼쪽 노랑, 9967e 오른쪽 노랑) / 이름 Dotum 14 4% #F6F4F2 (Stroke 2층) / 경로 12 4% #D4CBBD 60% / Hover 행 #62533D + 이름 #FF5C00" },
{ "id": "662:67471", "name": "파일출력 2뎁스", "type": "FRAME", "size": [310, 270], "screenshot": "screenshots/662-67471_file-preview-2depth.png",
"position": "최근파일 오른쪽 +1, Hover 행 top +1",
"designContext": "#62533D, 1px #62533D 상·우·하, radius 0 3 4 0, drop-shadow 0 2 1 rgba(0,0,0,.5), inset 2 0 2.5 rgba(0,0,0,.5), py 4 gap 4 / 경로 14px #FF5C00 (아래층 #D7D2B0) / image 918 310x244 (crop 158.67% x 191.87%, -25.97% / -52.46%)" }
]
}
}
@@ -0,0 +1,40 @@
# Task A + B 통합 계획 — 로고 클릭 2단 Header 메뉴 (초안, 2026-10-19)
> 상태: **미승인 계획**입니다. Task B는 신규 파일만 작성했고 아래 공유 파일은 아직 수정하지 않았습니다. 통합은 사용자 별도 승인 후 진행합니다.
> 근거: [2단계 보고](./stage2-report.md), Task A 현황(읽기 전용 확인, 미커밋): SWHeader `leading` 슬롯, `documentOpenPlacement`, CSS 변수 `--hm-sw-header-leading-width`, `components/sw-tool/`.
## 1. 순서
1. Task A 변경을 먼저 확정한 뒤, 그 위에 Task B 신규 파일을 merge합니다. B 파일은 A 파일과 겹치지 않습니다.
2. export 연결 → 3. SWHeader 연결 → 4. 속성Bar 수정(선택) → 5. 문서·AI → 6. 검증.
## 2. 작업 목록과 적용 방법
| # | 파일 | 변경 | 적용 방법 |
|---|---|---|---|
| I-1 | `packages/sw/src/index.ts` | 공개 export | `export * from "./components/sw-home-menu"` 한 줄. CSS는 `SWHomeMenu.tsx`·`SWRecentFiles.tsx`에서 import하므로 index에 CSS import를 추가하지 않습니다(`sideEffects` 때문에 index에서만 import하면 빌드에서 빠지는 것을 확인함) |
| I-2 | `components/sw-header/types.ts` | Header prop | `homeMenu?: Omit<SWHomeMenuProps, "open" \| "onOpenChange" \| "returnFocusRef">`, `homeMenuOpen?`, `defaultHomeMenuOpen?`(false), `onHomeMenuOpenChange?(open, reason)`. 미전달이면 기존 동작과 같습니다(하위 호환) |
| I-3 | `components/sw-header/SWHeader.tsx` | 연결 | `homeMenu`가 있을 때만 홈 클릭으로 열림을 토글합니다. 기존 `onHomeClick`은 항상 먼저 호출합니다. 메뉴는 `<header>` 안의 absolute 레이어(`top: 0`)로 렌더링하고, `returnFocusRef`는 홈 버튼입니다. 메뉴가 열리면 기존 필드 Dropdown(`activeMenu`)을 닫습니다 |
| I-4 | 같은 파일 | 앵커 | 시작 x = 객체 수정 영역 `getBoundingClientRect().left − header.left − 2px`, 폭 = 창 영역 left − 그 값입니다. 고정 375px을 쓰지 않습니다. Task A `leading` 슬롯이 있으면 `--hm-sw-header-leading-width`만큼 객체 영역이 이동하므로 **DOM 실측을 기준으로 하고 변수는 보조로 씁니다**. 기존 속성Bar `barOffset` 측정 방식과 같은 패턴입니다 |
| I-5 | `components/sw-header/components/SWHeaderHome.tsx` | 접근성 | `aria-expanded`·`aria-controls`(메뉴 id)를 추가합니다. Figma에 열림 시각 변화가 없으므로 스타일은 바꾸지 않습니다 |
| I-6 | `theme/sw.css` | z-index | 메뉴 레이어를 속성Bar 앵커(z 2)보다 위, 기존 Dropdown 패널과 같은 층에 둡니다. 동시에 열리지 않게 I-3에서 닫습니다 |
| I-7 | `theme/sw.css` (선택, A-6 승인 필요) | 속성Bar | P1: 좌·우·하 테두리를 세로 gradient `#08251C 0% / #208769 37.1% / #08241C 80.5% / #051612 100%`로 바꿉니다(기존 hover-ring mask 기법). P2: `.hm-sw-property-bar__object`에 `--hm-sw-header-text-outline`. P3: 라벨에 `--hm-sw-header-label-outline` |
| I-8 | `theme/sw.css` (검토) | home on 상·하단 선 | Figma 열림 상태의 1920 전폭 `#5F4B0F` 2px 선(y 0–2, 46–48)을 `.hm-sw-header[data-home-menu-open]`에서만 적용할지 사용자가 확인해야 합니다 |
| I-9 | `build.mjs`, `package.json` | 번들 | 변경이 필요 없을 것으로 예상합니다. `dist/styles.css`에 `.hm-sw-home-menu`가 포함되는지, `title-badge.svg`가 inline 또는 asset으로 나오는지 build 후 확인합니다 |
| I-10 | `packages/sw/ai/**`, `README.md`, `docs/sw/sw-header-usage.md` | 문서 | [AI 가이드 초안](./ai-home-menu-guide.md)을 COMPONENT_CATALOG·COMPOSITION_PATTERNS·DESIGN_RULES로 옮기고, `homeMenu` prop 사용 예를 추가합니다 |
| I-11 | 기존 stories | 예시 | `src/sw/sw-home-menu.stories.tsx`의 소스 경로 import를 `@hm-design/sw`로 바꿉니다. host wiring 예시는 I-3 이후 `SWHeader homeMenu` 예시로 대체합니다 |
## 3. Task A와 맞출 결정 (사용자 확인)
- `SWToolButton`에 `tile` appearance를 둘지, `SWHomeMenuItem`을 별도로 유지할지(A-1).
- 토글 이름: A는 `pressed`, B는 `active`/`activeIds`(별칭 추가 여부).
- `documentOpenPlacement` 기본값 `"menu"`는 메뉴와 독립적입니다. 찾아열기·새파일은 host가 `onDocumentOpen`·`onDocumentNew`와 같은 handler를 연결합니다(내부 결합 없음).
- Figma 아이콘 15쌍을 패키지 asset으로 둘지 여부(A-2). 현재는 Story fixture에만 있습니다.
## 4. 통합 검증
1920·1440·1080·768·390 폭에서 확인합니다.
- 로고 열기·재클릭 닫기, 그리고 `leading`·문서 Dropdown(New/Open)과 겹치지 않는지.
- 기존 Header·속성Bar·Dropdown·K 가이드 회귀 없음.
- `tsc`, library build, packed tarball을 React 19 소비 프로젝트에 설치했을 때 동작하는지.
- Storybook 전체 Story(포트 충돌 주의).
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+129
View File
@@ -0,0 +1,129 @@
# 작업 B 2단계 — 로고 클릭 2단 Header 메뉴 구현·검증 보고 (2026-10-19)
> 기준: 사용자 최종 정정(로고 클릭 = 2단 Header 메뉴, 기본 숨김, 재클릭 닫힘), [1단계 분석](./analysis-report.md), Figma `rj6wpsfk24lOWvWiVRAANP`.
> 범위: Task B worktree `claude/sw-figma-logo-menu-36832b`(HEAD `5faa1f1`)의 **신규 파일만** 수정했습니다. Task A 공유 파일은 수정하지 않았고, Commit·Push·Merge·Publish도 하지 않았습니다.
## 0. 해석 변경 이력
| 시점 | 해석 | 최종 |
|---|---|---|
| 1단계 | 로고 클릭 → `home on` 2단 메뉴 overlay | **유지(최종 정정과 일치)** |
| 2단계 중간 요청 | 로고 클릭 → "파일 열기 창" | Figma 재확인 결과 앱 내부 UI이며 OS 파일 선택창 호출 구조가 아닙니다. 이후 사용자 최종 정정으로 2단 메뉴로 돌아왔습니다 |
| 재확인으로 바뀐 점 | 662:67267을 단순 "하단 강조 outline 변형"으로 본 것 | 실제로는 Figma 주석 **"최근파일" 상태 화면**입니다. 최근파일이 열려 메뉴 전체에 주황 외곽선이 생기고, 그 아래 최근파일 목록(662:67331)과 2뎁스 미리보기(662:67471)가 붙습니다. 두 노드는 요청한 21개 목록 밖이라 **선택 기능**(`panel` + `SWRecentFiles`)으로 구현했습니다 |
## 1. 구현 컴포넌트
| 파일(모두 신규) | 내용 |
|---|---|
| `packages/sw/src/components/sw-home-menu/SWHomeMenu.tsx` | `SWHomeMenu`(메뉴·열림·Active·닫기·키보드), `SWHomeMenuSection`(제목 badge + 그룹), `SWHomeMenuItem`(80×46 타일, Default / Variant2 / Variant3), `useSWHomeMenuClose` |
| `…/sw-home-menu/SWRecentFiles.tsx` | 선택 하위 패널: 최근파일 목록 354px, 행 50px, 2뎁스 미리보기 310×270 |
| `…/sw-home-menu/types.ts`, `index.ts` | 공개 타입·export |
| `…/sw-home-menu/assets/title-badge.svg` | Figma tit_* Union 도형(9-slice로 폭 가변) |
| `packages/sw/src/theme/sw-home-menu.css` | `.hm-sw-home-menu*`, `.hm-sw-recent-files*` 스타일과 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*` 토큰. 기존 SW 토큰(글꼴, 외곽선, focus, 행 Hover)을 그대로 읽습니다 |
| `src/sw/sw-home-menu.stories.tsx`, `sw-home-menu-fixture.tsx`, `sw-home-menu.css`, `src/sw/assets/home-menu/**` | Storybook `SW/Home Menu`(Figma 재현, Header 조합, 3 Variant, 데이터 변경, 단독, 최근파일, 속성Bar 비교, 비교 캔버스). Figma 아이콘 30개·미리보기 이미지는 **Story fixture 전용**입니다 |
| `docs/sw/figma-additional/ai-home-menu-guide.md`, `stage2-report.md`, `stage2-qa/**`, `screenshots/662-67331·662-67471` | AI 가이드 초안, 이 보고서, 검증 스크린샷 |
## 2. Figma 노드 대응 (22 + 참고 3)
| Node | 대응 | 상태 |
|---|---|---|
| 26:10492 속성Bar | 기존 `SWHeaderPropertyBar`(수정 없음). Story `속성Bar 비교`에서 Figma 값으로 비교 | 차이 3건은 통합 단계(§8) |
| 26:10591 home on | `SWHomeMenu` + `accent="auto"`(패널 없음 → 섹션별 강조) | 재현 |
| 662:67267 home on(최근파일 상태) | 최근파일 타일 Active + 패널 → `accent` 자동 외곽선. K 미리보기는 기존 `SWHeaderKGuide` Hover | 재현 |
| 26:12604 / 12610 / 12622 / 12616 (+26:12628 참고) | `SWHomeMenuSection.title` (badge 공통 1개) | 재현 |
| 26:11355 … 26:11631 버튼 세트 15개 | `SWHomeMenuItem` 1개 + Story 데이터 15건. Default / V2(Hover) / V3(Active) | 재현 |
| 662:67331 최근파일 / 662:67471 2뎁스 (참고) | `SWRecentFiles` (`panel`로 연결) | 재현(선택 기능) |
## 3. 공개 API
```ts
<SWHomeMenu
sections?: SWHomeMenuSectionData[] // 또는 children(<SWHomeMenuSection>/<SWHomeMenuItem>)
open? defaultOpen?=false onOpenChange?(open, reason: "escape"|"background"|"outside"|"select"|"file")
activeIds? defaultActiveIds? onActiveChange?(ids, event) selection?: "multiple"|"single"
onItemSelect?(event) closeOnBackgroundClick?=true closeOnSelect?=false closeOnEscape?=true closeOnOutsideClick?=false
autoFocus?=true returnFocusRef? accent?: "auto"|"sections"|"outline" />
SWHomeMenuItemData { id, label, icon?, hoverIcon?, toggle?=true, panel?, panelLabel?, disabled?, hidden?, title?, aria-label?, onSelect? }
SWHomeMenuSectionData { id, title, items, hidden? }
<SWRecentFiles files: SWFileEntry[] onFileOpen? onFileHover? renderPreview? closeOnOpen?=true emptyText? />
SWFileEntry { id, name, path?, badge?, preview?, previewTitle?, disabled?, title? }
```
- 텍스트·아이콘·항목·이벤트·Active·하위 패널은 모두 외부 데이터입니다. 패키지에는 메뉴 항목이나 CAD 동작이 들어 있지 않습니다.
- 단독 사용: `<SWTheme>` 안에 둡니다. 메뉴 밖에서 쓰는 `SWHomeMenuItem`도 `active`·`onActiveChange`로 동작합니다.
- Header 연동(통합 전): 기존 `SWHeader onHomeClick`으로 `open`을 토글합니다. 위치는 host가 Header DOM(객체 수정 영역 왼쪽 − 2px부터 창 영역 왼쪽까지)에서 측정합니다.
## 4. Figma 디자인 일치
1x 캡처는 headless Chromium의 1920px 화면에서 Figma 스크린샷과 2배 확대로 비교했습니다(`stage2-qa/compare-*.png`).
| 항목 | 결과 |
|---|---|
| 위치·크기 | 1920에서 메뉴 left 375, 폭 1305, 높이 77이며 섹션 폭은 340/260/180/260/261입니다. 그룹 46, 타일 80×46, 아이콘 18 슬롯(31,6), 라벨 top 25 **일치** |
| 색·gradient·그림자 | 섹션 면·안쪽 선, 그룹 테두리 gradient(위→아래)·면(아래→위)·그림자, 구분선, 주황 Hover 테두리, 하단 강조 2종 **일치**(Plugin API 값과 gradientTransform 방향 반영) |
| 아이콘 | Figma SVG 30개(Default·Hover)를 그대로 사용하고, 위치 보정 값은 fixture에 둠 **일치** |
| 최근파일·미리보기 | 354 폭, 행 50, 배경 `#27241D`, 주황 테두리, Hover 행 `#62533D` + `#FF5C00`, 미리보기 310×270 **일치**(DPR 1.5 화면에서는 테두리 반올림으로 269.3) |
| 의도된 차이 | 글꼴은 Dotum → Noto Sans KR이고, 타일 라벨·파일명은 700(기존 SW 정책)이라 Figma보다 굵습니다. 섹션 제목은 400이며 Figma보다 가늘고 badge가 7px 좁습니다(116.6 vs 124). 첫 섹션 면 gradient stop은 나머지 4개 기준으로 통일했습니다. 찾아열기·최근파일 구분선의 다른 gradient는 공통 구분선으로 통일했습니다 |
| Header 소유 차이 | home on의 1920 전폭 상·하단 `#5F4B0F` 2px 선은 Header 소유라 미구현(통합 검토) |
## 5. 상태 전환·Overlay 테스트 (Storybook 6031, 실제 실행)
| 테스트 | 결과 |
|---|---|
| 기본 숨김 | PASS (`hiddenByDefault: true`, consumer DOM에도 없음) |
| 로고 클릭 열기 / 재클릭 닫기 | PASS |
| Hover → V2 (실제 마우스) | PASS (테두리 opacity 1, `#FF5C00`, Hover 아이콘 표시, -1px 이동) |
| 클릭 → V3, 재클릭 → Default | PASS (`aria-pressed` true/false) |
| 메뉴 빈 곳 클릭 닫기 / Escape 닫기 + 로고 Focus 복귀 | PASS |
| ←/→/Home/End, Tab Focus 2px 윤곽선, Enter 실행 | PASS (실제 키 입력) |
| 최근파일: 열기 → 외곽선 강조, `aria-expanded`, 재클릭 닫기, ↓로 목록 진입, 행 Hover·Focus 미리보기, Enter/클릭 → `onFileOpen` + 메뉴 닫기, 다시 열면 Default | PASS |
| Escape 단계(패널 → 메뉴) | PASS |
| 데이터 변경(추가·삭제·숨김·disabled·긴 라벨·섹션 추가·single·accent) | PASS |
| 반응형 1920/1440/1080/768/390 | PASS: 로고·창 영역과 겹치지 않고 메뉴 안에서 가로 스크롤. 좁은 폭 접기 디자인은 없음(별도 승인) |
| 타입 | PASS: `tsc` SW 패키지, Story, 독립 consumer |
| 독립 React 19.3 consumer(Vite 빌드, 소스 import) | PASS: 렌더, 기본 숨김, CSS·SVG 번들 포함 |
참고: 브라우저 pane은 background page라 ResizeObserver 측정을 할 수 없어, 반응형은 headless Chromium으로 확인했습니다. 메뉴 CSS를 `index.ts`에서만 import하면 `sideEffects` 설정 때문에 빌드에서 빠지는 것을 발견했고, 사용하는 컴포넌트 파일에서 import하도록 고쳤습니다.
## 6. AI 재사용
[AI 가이드 초안](./ai-home-menu-guide.md) 기준입니다. 새 메뉴는 `sections` 데이터만으로 같은 스타일이 적용되고, 폭·구분선·모서리·badge 폭이 자동으로 계산됩니다. 소비 프로젝트에서 새 섹션 2개·항목 5개(currentColor 아이콘, `toggle:false`, 패널)를 만든 결과가 같은 스타일로 렌더링되는 것을 확인했습니다(`stage2-qa/react19-consumer.png`). AI 가이드를 `packages/sw/ai/`로 옮기는 작업은 통합 단계입니다.
## 7. Task A와의 중복·충돌
| 항목 | 내용 | 조치 |
|---|---|---|
| `SWToolButton` (pill/text/document) | 80×46 타일 appearance가 없어 중복 구현은 아닙니다 | 통합 때 `tile` appearance로 합칠지 결정(별도 승인) |
| 아이콘 prop 이름 | A `hoverIcon` | B도 `hoverIcon`으로 맞춤 |
| 토글 상태 이름 | A `pressed` / B `active`·`activeIds` | 메뉴는 여러 항목 상태라 `activeIds` 유지. 통합 때 별칭 검토 |
| CSS | A `hm-sw-tool__*` / B `hm-sw-home-menu__*`, `hm-sw-recent-files__*` | 충돌 없음 |
| `documentOpenPlacement` | 사용자 최종 지시: 기본 `"menu"`(Open은 문서 Dropdown 안), 로고 옆 독립 Open 없음 | B 메뉴는 이 값과 무관합니다. B Story의 Header(5faa1f1)에는 독립 Open이 없습니다 |
| `leading` 슬롯 | 객체 수정 영역 시작 x가 바뀝니다 | B는 DOM 측정이라 하드코딩 없음 |
| 찾아열기·새파일 ↔ `onDocumentOpen`·`onDocumentNew` | 동작이 중복될 수 있습니다 | 컴포넌트는 콜백만 받습니다. host가 같은 handler를 연결하는 방식을 권장합니다(통합 문서에 기재) |
## 8. 통합 시 수정할 공유 파일
| 파일 | 변경 |
|---|---|
| `packages/sw/src/index.ts` | `export * from "./components/sw-home-menu"` |
| `components/sw-header/SWHeader.tsx`, `types.ts` | (안) `homeMenu?: SWHomeMenuProps`, `homeMenuOpen`/`defaultHomeMenuOpen`/`onHomeMenuOpenChange`. 홈 버튼 `aria-expanded`·`aria-controls`, 객체 수정 영역 −2px ~ 창 영역 앵커, 열릴 때 다른 Dropdown 닫기, 기존 `onHomeClick` 유지(하위 호환) |
| `components/sw-header/components/SWHeaderHome.tsx` | 열림 상태 전달(시각 변화는 Figma에 없음) |
| `theme/sw.css` | 속성Bar P1 gradient 테두리, P2·P3 객체명·라벨 외곽선. home on 상·하단 `#5F4B0F` 선 적용 여부(검토) |
| `build.mjs`, `package.json` | 새 CSS가 `styles.css`에 포함되는지 확인(컴포넌트 import 방식이라 변경 불필요 예상, 미검증) |
| `packages/sw/ai/**`, `README.md`, `docs/sw/sw-header-usage.md` | AI 가이드 초안 반영 |
| 기존 stories | 필요하면 Header Overview에 메뉴 연결 |
## 9. 판정
**PARTIAL** — 독립 구현과 검증은 PASS입니다. 다만 공유 파일 연동(공개 export, `SWHeader` 내장 연결, 속성Bar 수정)은 승인 범위상 통합 단계로 남았습니다.
미검증 항목:
- 패키지 진입점(`@hm-design/sw`) export와 `build.mjs` dist의 `styles.css` 포함 여부. tarball 소비 검증도 하지 않았습니다(통합 단계).
- Task A 최신 Header(leading·documentOpenPlacement 반영본)와의 실제 조합. B는 5faa1f1 기준 Header로만 검증했습니다.
- Firefox·Safari 렌더링(Chromium만 확인).
- 속성Bar P1–P3를 수정한 뒤의 비교.
- 스크린리더 실제 낭독.
검토 대상: V3의 의미(유지 토글 vs 순간 누름; 현재는 사용자 지시대로 유지 토글), 파일 열기 후 닫기 기본값, 최근파일 패널 열기 방식(Figma 연결 없음 → 클릭), 빈 목록 문구, 좁은 폭 디자인, Header 상·하단 선.
로컬 검증 환경(커밋 대상 아님): worktree `node_modules` junction(→ HMDesign-SW), `.storybook-task-b/`(신규 Story만 로드하는 설정과 웹폰트 fs.allow), `.claude/launch.json`. 실행 중인 Storybook 포트는 6031입니다. 정리 여부는 사용자가 결정합니다.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Loaded 100 of 1152 files, more files were not shown because too many files have changed in this diff. Show more