diff --git a/AGENTS.md b/AGENTS.md index 6bb0cb0..69fbdaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,8 @@ ## AI 조직도 독립 페이지 생성 — 공통 정책 - 자연어 또는 JSON·CSV·Excel에서 **새 독립 조직도 페이지를 생성하는 작업**은 먼저 [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md)을 읽고 적용한다. 회사·조직 유형·Depth·인원수를 특정 사례의 기본값으로 고정하지 않는다. -- 기본 화면에는 요청한 조직도와 기존 탐색 기능만 표시한다. AI 채팅·생성 입력·JSON·테스트·별도 통계·개발 안내와 Navigation은 요청 없이 추가하지 않는다. 이 범위에서는 아래 일반 업무 화면의 Navigation 의무 대신 해당 정책의 최소 화면 규칙을 따른다. 공개 API·Theme·React·접근성·검증 규칙은 계속 적용한다. -- 기존 Full Composition·Expanded OrgChart·P9.5·P-AI1에는 소급 적용하지 않는다. 일반 업무 화면과 명시적인 기존 화면 수정에는 기존 규칙 및 해당 요청 범위를 유지한다. +- Fixed·Auto/Expanded의 기본 출력은 **Chart Only: White Canvas + OrgChart Nodes + Connectors**다. 흰색 Root 캔버스·실제 Bounds 기준 가로/세로 중앙 정렬·외부 UI 기본 제외는 위 정책을 따른다. 제목·기준일·인원 요약·외부 Zoom/Fit 버튼·상태 문구·설명·회색 바깥 배경은 명시적 요청이 있을 때만 추가하며 기존 Core 내부 탐색 기능은 유지한다. AI 채팅·생성 입력·JSON·테스트·별도 통계·개발 안내와 Navigation은 요청 없이 추가하지 않는다. 이 범위에서는 아래 일반 업무 화면의 Navigation 의무 대신 해당 정책의 최소 화면 규칙을 따른다. 공개 API·Theme·React·접근성·검증 규칙은 계속 적용한다. +- 기존 Full Composition·Expanded OrgChart·Department Colors·Overview·P9.5·P-AI1에는 소급 적용하지 않는다. 일반 업무 화면과 명시적인 기존 화면 수정에는 기존 규칙 및 해당 요청 범위를 유지한다. 1. 범위 `STORYBOOK_ONLY | PRODUCTION_UI | BOTH`, Runtime, 요구사항·필수 동작·완료 조건을 확인한다. 신규 AI 업무 화면은 실제 HM React 컴포넌트를 사용한다. 명시적 다른 Runtime adapter는 [상세 계약](docs/guidelines/runtime-portability.md)을 따른다. 2. [기능별 선택·완료 계약](docs/guidelines/ai-screen-contract.md) → [Component / Pattern 인덱스](docs/guidelines/component-pattern-reference.md) → 선택한 문서·대표 All Story·필요한 Props 순으로 읽는다. Import는 `@hm-design/ui` 공개 API가 기본이다. 의존성은 package.json의 exports/types와 실제 파일 목록을 확인하며 내부 src/dist 경로를 추측하지 않는다. verification Story는 운영 기준이 아니다. diff --git a/CLAUDE.md b/CLAUDE.md index b3344d4..3f79954 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ shadcn/ui(Radix, Tailwind v4, CVA) 기반 HM 디자인 시스템 시제품. Reac 규칙의 기준은 아래 문서이며, 이 파일에는 규칙 본문을 복사하지 않는다. 작업 전에 해당 문서를 읽는다. - `AGENTS.md` — AI Implementation Defaults와 Navigation 필수 요약. 기존 Component / Storybook Reference 우선, Theme / Semantic 기본 적용, 단일 Navigation, 최소 Custom은 사용자의 별도 지시 없이 모든 신규·수정 UI에 적용한다. -- [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md) — 자연어 또는 JSON·CSV·Excel에서 새 독립 조직도 페이지를 생성할 때 먼저 읽는다. 가변 계층·정확한 인원·제공 데이터 보존·기존 Core/React 재사용을 따른다. 화면은 요청한 조직도와 탐색 기능만 표시하며, 이 범위의 Navigation 예외는 `AGENTS.md`의 조직도 전용 규칙을 따른다. 기존 Full Composition·Expanded OrgChart·P9.5·P-AI1에는 소급 적용하지 않는다. +- [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md) — 자연어 또는 JSON·CSV·Excel에서 새 독립 조직도 페이지를 생성할 때 먼저 읽는다. 가변 계층·정확한 인원·제공 데이터 보존·기존 Core/React 재사용을 따른다. Fixed·Auto/Expanded 모두 **Chart Only: White Canvas + OrgChart Nodes + Connectors**를 기본으로 하며, 흰색 Root·중앙 배치·외부 UI 기본 제외와 Core 내부 탐색 기능 유지는 위 정책을 따른다. 이 범위의 Navigation 예외는 `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/guidelines/shadcn-기반-구현-원칙.md` — Component / Pattern / Story의 재사용 우선순위와 Custom 구현 허용 조건 (관련 작업 전 필독) diff --git a/docs/org-chart/ai-page-generation-rules.md b/docs/org-chart/ai-page-generation-rules.md index a96f967..46c166a 100644 --- a/docs/org-chart/ai-page-generation-rules.md +++ b/docs/org-chart/ai-page-generation-rules.md @@ -1,12 +1,12 @@ # HMDesign — AI 조직도 페이지 자동 생성 공통 정책 -적용 대상은 Codex·Claude 등 AI 코딩 에이전트가 자연어 또는 제공 데이터에서 **새로 생성하는 독립 조직도 페이지**다. 회사, 조직 유형, 계층, Depth, 인원수에 종속되지 않는다. **고정형(Fixed)과 확장형(Auto/Expanded) 모두 동일한 화면 생성 원칙을 적용한다.** 기존 Full Composition 및 Expanded OrgChart의 승인된 화면은 변경하지 않으며, 기존 P9.5·P-AI1 결과에도 소급 적용하지 않는다. +적용 대상은 Codex·Claude 등 AI 코딩 에이전트가 자연어 또는 제공 데이터에서 **새로 생성하는 독립 조직도 페이지**다. 회사, 조직 유형, 계층, Depth, 인원수에 종속되지 않는다. **고정형(Fixed)과 확장형(Auto/Expanded) 모두 동일한 화면 생성 원칙을 적용한다.** 기존 Full Composition·Expanded OrgChart·Department Colors·Overview의 승인된 화면은 변경하지 않으며, 기존 P9.5·P-AI1 결과에도 소급 적용하지 않는다. ## 1. 실행 방식과 적용 우선순위 - 사용자가 코딩 에이전트에 요청을 전달하면 에이전트가 조직 JSON과 실제 페이지 코드를 생성한다. 웹페이지에 AI 채팅창이나 생성 입력 UI를 추가하지 않는다. 브라우저 AI Provider 연결은 이 실행 방식의 전제가 아니다. -- 현재 사용자의 명시적 요청 → 이 정책의 AI 조직도 전용 범위 → 충돌하지 않는 기존 AGENTS.md·CLAUDE.md 및 HM 공개 계약 순으로 적용한다. 관련 문서는 필요한 부분만 읽는다. -- 이 정책의 기본 화면은 조직도만 표시한다. 일반 업무 화면의 GNB/LNB/DASHBOARD Navigation 의무를 가져와 메뉴나 Header를 추가하지 않는다. 사용자가 Navigation을 요청한 경우에만 기존 단일 Navigation 계약을 적용한다. +- 사용자 요청에서 명시한 UI 요구사항 → 이 정책의 AI 조직도 전용 범위 → 기존 OrgChart Core의 디자인·동작 규칙 순으로 적용한다. 충돌하지 않는 AGENTS.md·CLAUDE.md와 HM 공개 계약·데이터 검증·접근성 규칙은 계속 준수한다. 관련 문서는 필요한 부분만 읽는다. +- 이 정책의 기본 출력은 **White Canvas + OrgChart Nodes + Connectors**인 Chart Only 화면이다. 일반 업무 화면의 GNB/LNB/DASHBOARD Navigation 의무를 가져와 메뉴나 Header를 추가하지 않는다. 사용자가 Navigation을 요청한 경우에만 기존 단일 Navigation 계약을 적용한다. - 기존 Core·React Wrapper·Theme·카드·Storybook을 요청 없이 변경하지 않는다. 필요한 공통 변경은 사유와 영향 범위를 먼저 보고한다. 새 조직도 엔진이나 디자인 시스템은 만들지 않는다. ## 2. 요청을 가변 구조로 정리 @@ -65,32 +65,69 @@ Depth에 회사나 직원 카드를 포함하는지, 조직별 인원이 직접 - 레이아웃·Depth·대표이사 표현·다중 회사·TB/LR/Hybrid·Zoom/Pan·접기·팝업은 실제 공개 API가 지원하는 범위만 사용한다. 직원 연결 방향을 전체 트리 방향 지원으로 해석하지 않는다. - 요청 구조나 최소 화면을 공개 API로 정확히 표현할 수 없으면 지원 한계와 대안을 보고한다. 임의 구현·합성 화면을 기존 Core 결과라고 주장하지 않는다. 공통 기능 확장이 필요하면 별도 승인 범위로 분리한다. -## 6. 조직도 중심의 독립 페이지 +## 6. Chart Only 독립 페이지 -- AI가 생성하는 Fixed 페이지와 Auto/Expanded 페이지 모두 요청한 조직도만 표시하는 원칙을 따른다. 레이아웃 모드가 다르다는 이유로 추가 UI를 허용하지 않는다. 해당 모드에서 지원되는 기존 Zoom/Pan·접기·펼치기·직원 정보 보기 등 탐색 기능은 유지한다. 요청 데이터가 달라지면 계층과 렌더링도 그 데이터에서 달라져야 한다. -- 사용자가 별도로 요청하지 않는 한 조직도 외의 설명 문구, 안내 패널, 테스트 정보, JSON 미리보기 또는 불필요한 추가 UI를 표시하지 않는다. AI 입력창, 사용 설명, 생성 조건 설명, 별도 통계, 개발 문구, Navigation과 추가 메뉴도 동일하게 제외한다. 저장·게시·다운로드·운영 API는 요청 범위 밖에서 추가하지 않는다. -- 기존 카드·회사색·연결선·직원 팝업 디자인과 Theme를 유지한다. 지원되는 공개 옵션으로 불필요한 내장 설명·요약을 제외하되 탐색 기능은 유지한다. 공개 옵션이 없으면 제약을 보고하며 Shadow DOM을 임의 조작하거나 공통 CSS를 고쳐 감추지 않는다. -- 실제 콘텐츠 크기와 컨테이너를 기준으로 지원되는 초기 자동 배치·Fit을 적용한다. 임의 고정 좌표·배율을 회사별 템플릿으로 만들지 않는다. 전체 개요와 확대 시 읽기, 내부 이동, 외부 가로 넘침을 함께 확인한다. -- 최소 배율, 많은 직원, 긴 명칭, 깊은 계층, 여러 회사에서 전체 Fit이 불가능하면 잘림·읽기·탐색 한계를 보고한다. 이를 해결하려고 이름·인원·Depth를 줄이지 않는다. -- 새 페이지는 독립 React entry로 제공한다. 기존 Full Composition·Expanded OrgChart·Storybook 메뉴·P9.5·P-AI1 결과를 교체하지 않는다. +### 6.1 기본 출력과 제외 요소 + +기본 결과물은 **White Canvas + OrgChart Nodes + Connectors**다. 기존 흰색 조직도 영역을 최상위 화면으로 사용한다. + +- 회사·조직 그룹, 요청에 포함된 대표자·본부장·팀장·직원 카드, 연결선과 필요한 화살표, 회사별 컬러를 표시한다. 조직명·직위·직원 이름 등 조직도를 구성하는 필수 텍스트는 유지한다. +- 해당 모드에서 지원되는 기존 Core의 그룹 접기·펼치기, Zoom/Pan·직원 정보 보기 등 조직도 내부 탐색 기능은 유지한다. 외부 조작 UI의 기본 제외를 Core 기능 삭제로 해석하지 않는다. +- 사용자가 명시적으로 요청한 요소만 아래 기본 제외 목록의 예외로 추가한다. 한 요소의 요청을 다른 부가 UI의 승인으로 확대하지 않는다. + +기본 제외 요소: + +- 페이지 제목(예: 가상 조직도), 기준일, 회사 수·전체 인원 요약 문구 +- 확대·축소·100% 복원·Fit to View 버튼 바 등 외부 컨트롤 +- Zoom 배율 안내, Drag & Drop ON/OFF 상태 문구, 조작 방법 안내 +- 설명 문단, 생성 조건 설명, 사용 설명·안내 패널 +- JSON 미리보기, 테스트 결과·디버그 패널, 별도 통계 대시보드, 개발 안내 +- 외부 회색 배경, 불필요한 Page Header·Wrapper·바깥 여백 +- AI 채팅·생성 입력 UI, Navigation·추가 메뉴 + +저장·게시·다운로드·운영 API는 요청 범위 밖에서 추가하지 않는다. 테스트 결과·가정·제약은 조직도 화면 대신 작업 완료 보고서에 기록한다. + +### 6.2 흰색 Root 캔버스와 초기 배치 + +- 캔버스가 화면의 기본 Root 영역을 차지하며 배경은 흰색이다. 외부 회색 영역이나 불필요한 바깥 여백을 생성하지 않는다. 사용자가 지정한 캔버스 크기가 있으면 이를 우선한다. +- 초기 배치는 조직도 전체의 실제 Bounds와 캔버스 크기를 기준으로 가로·세로 중앙 정렬한다. 노드·그룹·연결선을 포함한 전체 범위와 위치를 확인하며 임의 고정 좌표·배율을 회사별 템플릿으로 만들지 않는다. +- 기본 배율은 조직 구조와 캔버스 크기를 고려해 지원되는 초기 자동 배치·Fit으로 결정한다. 대규모 조직도에서 전체 Fit만을 위해 텍스트를 지나치게 작게 만들지 않는다. 전체 개요와 확대 시 읽기, 내부 이동, 외부 가로 넘침을 함께 확인한다. +- 기존 Zoom/Pan을 재사용하고 이를 위한 내부 이벤트 기능을 유지한다. 초기 Fit은 배치 동작이며 Fit to View 버튼 바를 렌더링해야 한다는 뜻이 아니다. 외부 컨트롤 버튼은 명시적 요청이 있을 때만 렌더링한다. +- 기존 카드·회사색·연결선·직원 팝업 디자인과 Theme를 유지한다. 캔버스 배경은 실제 선언된 공개 Theme·스타일 경로를 확인해 적용하며 없는 Token을 추측하지 않는다. +- 지원되는 공개 옵션으로 불필요한 내장 설명·요약·컨트롤을 제외한다. 옵션이 없거나 공개 API로 흰색 Root·중앙 배치를 구현할 수 없으면 제약을 보고한다. Shadow DOM 임의 조작, 공통 CSS 변경, Core·React Wrapper의 불필요한 공개 API 변경으로 해결하지 않는다. +- 최소 배율, 많은 직원, 긴 명칭, 깊은 계층, 여러 회사에서 전체 Fit과 가독성을 함께 만족할 수 없으면 잘림·읽기·탐색 한계를 보고한다. 이름·인원·Depth를 줄이거나 단순히 내용을 숨겨 잘림·레이아웃 문제를 감추지 않는다. + +기본 구성은 다음과 같다. + +- 기존 구성: Page Background → Header → Controls → Description → White Canvas → OrgChart +- 앞으로 생성할 기본 구성: **White Canvas → OrgChart** + +### 6.3 Fixed / Auto 공통 적용과 기존 화면 보호 + +- **Fixed:** 승인된 기존 카드·연결선 디자인과 해당 모드의 기존 배치를 재사용하되, 새 AI 생성 독립 페이지에는 Chart Only 구성을 적용한다. +- **Auto / Expanded:** 기존 Core 자동 배치를 재사용하되, 조직도 외부의 설명·테스트·조작 패널을 제외한다. +- 두 모드 모두 가변 계층·정확한 인원·제공 데이터 보존 규칙을 유지한다. 레이아웃 모드가 다르다는 이유로 추가 UI를 허용하지 않는다. 요청 데이터가 달라지면 계층과 렌더링도 그 데이터에서 달라져야 한다. +- 새 페이지는 독립 React entry로 제공한다. 기존 Full Composition·Expanded OrgChart·Department Colors·Overview·Storybook 메뉴·P9.5·P-AI1 결과를 교체하지 않으며 기존 Storybook 페이지의 제목·컨트롤·설명 UI를 삭제하지 않는다. - Guard나 문서 분석기의 제약 때문에 요청하지 않은 Navigation·통계·설명 UI를 추가하지 않는다. 정책과 검사 도구가 충돌하면 원본 검사 결과와 원인을 보고하며 도구나 공통 계약을 이번 범위 밖에서 임의 수정하지 않는다. ## 7. 생성 절차와 최종 통합 검증 +> P10.2 구현 한계 확인: 이 정책은 목표이며 현재 Core의 모든 내장 UI를 끄는 공개 옵션을 의미하지 않는다. 실제 Auto/Fixed 독립 검사는 Chart Only PARTIAL이다. Auto의 기본 초기 배치는 가로 중앙·상단 정렬이고, Vanilla의 공개 `fitToView()`는 전체 Bounds 기준 가로·세로 중앙 배치를 지원한다. React Wrapper에는 이를 호스트가 직접 호출할 공개 handle/ref가 없다. 지원 범위·후속 개선 후보는 [공통 사용 가이드](org-chart-usage-guide.md#chart-only-정책과-현재-기능의-관계)를 확인한다. 기존 화면·Core·공개 API를 변경하거나 내장 DOM을 숨겨 정책 완료로 보고하지 않는다. + 1. 요청을 분석하고 현재 작업 경로·Git 상태·기준 커밋·공개 API를 확인한다. 기존 미커밋 변경과 병렬 작업, 서버 포트를 보호한다. 2. 회사별 계층·유형·Depth·조직 수·직원 수·조직장 집계 조건을 정리한다. 충돌하는 필수 조건만 확인한다. 3. 요청에서 가상 데이터를 생성하거나 제공 데이터를 보존해 변환한다. 계획과 변환 대응은 화면 밖에 기록한다. 4. 기존 Contract와 추가 정합성 검사를 통과시킨다. 이는 잘못된 데이터를 렌더러에 넘기지 않기 위한 필요한 사전 확인이다. -5. 공개 Core·React Wrapper로 조직도만 있는 독립 페이지를 생성한다. 새로운 구조를 고정된 사례로 바꾸지 않는다. +5. 공개 Core·React Wrapper로 흰색 Root 캔버스에 조직도만 있는 Chart Only 독립 페이지를 생성한다. 실제 Bounds 기준 가로·세로 중앙 배치와 연결선을 확인한다. 새로운 구조를 고정된 사례로 바꾸지 않는다. 6. 구현 후 변경 범위의 코드·diff·계층·인원·중복 집계·보존·핵심 동작을 한 번에 검증한다. 필요한 Typecheck·Build·Guard를 수행하고 실제 브라우저에서 확인한다. 이미 성공한 동일 조건 검사를 반복하지 않고 수정·실패 범위만 다시 확인한다. -7. 390/768/1280/1440px에서 초기 Fit, 콘텐츠/내부 스크롤, 줄바꿈, 카드·연결선·회사색, Zoom/Pan·접기·팝업, 키보드·포커스를 관련 사용 요소에 맞게 확인한다. 실제 실행 URL과 화면 캡처를 보고한다. +7. 390/768/1280/1440px와 사용자 지정 캔버스 크기에서 흰색 Root, 실제 Bounds 기준 초기 중앙 정렬·배율·가독성, 외부 Header·Controls·Description 및 상태 문구의 기본 제외, 콘텐츠/내부 스크롤, 줄바꿈, 카드·연결선·회사색, Zoom/Pan 내부 이벤트·접기·팝업, 키보드·포커스를 관련 사용 요소에 맞게 확인한다. 실제 실행 URL과 화면 캡처를 보고한다. 8. 요청 대비 회사·조직·Depth·직원·조직장 집계, 생성 파일, 재사용 API, 가정, 제약, PASS/PARTIAL/FAIL와 NOT VERIFIED를 간결하게 보고한다. Guard 원본·정적·Runtime·STYLE 디자인 의도/최종 상태를 구분하고 미검증을 덮어쓰지 않는다. -정책 작성·연결만 수행하는 작업에는 화면 구현·빌드·브라우저 실행·전체 회귀를 추가하지 않는다. 데이터 생성·렌더링 기능을 실행하지 않았다면 정책 작성 완료를 범용 런타임 지원 PASS로 보고하지 않는다. +정책 작성·연결만 수행하는 작업에는 화면 구현·빌드·브라우저 실행·전체 회귀를 필수로 추가하지 않는다. 독립 실험 환경에서 선택적으로 화면을 검증할 수 있으나 기존 Storybook·AI Playground 테스트 페이지는 수정하지 않는다. 화면 검증을 수행하지 않았다면 **VISUAL NOT VERIFIED**로 보고한다. 데이터 생성·렌더링 기능을 실행하지 않았다면 정책 작성 완료를 범용 런타임 지원 PASS로 보고하지 않는다. ## 8. 범위 보호와 완료 기준 -- 기존 P9.5·P-AI1 산출물과 검증 기록을 보존한다. 이번 정책 작성·연결 범위는 이 파일과 AGENTS.md·CLAUDE.md의 참조 안내이며 화면·데이터·Core 변경을 포함하지 않는다. +- 기존 P9.5·P-AI1 산출물, P10 PARTIAL과 기존 Guard FAIL 10건의 검증 기록을 보존한다. 기존 P10.1 작업 폴더와 AI Playground Worktree를 수정하지 않으며 삼안 500명 테스트 페이지를 이관하지 않는다. 이번 정책 작성·연결 범위는 이 파일과 AGENTS.md·CLAUDE.md의 참조 안내이며 화면·데이터·Core 변경을 포함하지 않는다. - 이 정책을 참조한 생성 요청만으로 Stage·Commit·Push·Merge를 승인받은 것으로 간주하지 않는다. 현재 사용자의 작업별 Git 지시를 우선하며 이번 정책 작성에서는 해당 작업을 진행하지 않는다. - 서로 다른 회사·대표이사 유무·조직 유형·Depth·분기 수·직원 수가 같은 요청 분석 → 데이터 변환 → Contract 검증 → Core 소비 절차로 처리돼야 한다. 특정 테스트 수치나 전용 템플릿에 의존하면 완료가 아니다. - 생성 작업의 완료는 명시 조건의 일치, 제공 데이터 보존, 기존 Core의 실제 렌더링, 요청 범위의 최소 UI, 실행 증거로 판단한다. 미지원 구조를 지원한 것처럼 보고하거나 분석·JSON만으로 화면 생성 성공을 선언하지 않는다. diff --git a/docs/org-chart/expanded-usage.md b/docs/org-chart/expanded-usage.md index eac5e91..cee1cbc 100644 --- a/docs/org-chart/expanded-usage.md +++ b/docs/org-chart/expanded-usage.md @@ -228,14 +228,21 @@ Fixed의 CSS·엔진 옵션·기존 native 팝업은 보존한다. 노출을 제공한다. Fit/100% 복원은 Zoom/Pan 사용 여부와 독립적으로 가능하다. 2,100명 전체 Fit은 개요용으로 작게 축소되므로 카드 읽기에는 Zoom 또는 초기 Depth를 사용한다. -React에서 `employeePopup="hm-dialog"`는 실제 공개 HM Dialog·Avatar·Button을 사용한다. +React에서 명시적인 `@hm-design/org-chart-react/hm-dialog` Entry와 +`employeePopup="hm-dialog"`는 실제 공개 HM Dialog·Avatar·Button을 사용한다. `'core'`가 기본, `'none'`은 카드의 클릭·button 의미를 제거하되 키보드 이동용 초점은 유지한다. `onEmployeeClick`을 지정하면 선택 모드와 관계없이 callback이 우선한다. HM 모드는 `@hm-design/ui@0.1.0` optional peer와 호스트 `index.css → hm-theme.css` 연결이 필요하다. -Core/Vanilla에는 React/HM 의존성을 추가하지 않았다. peer 없는 호스트의 모든 bundler -조합 및 fresh install은 별도 미검증이다. +기본 Entry `@hm-design/org-chart-react`는 HM runtime/type Import 없이 Core 팝업을 +사용한다. UI 설치만으로 전환하지 않는다. 다른 디자인 시스템은 기본 Entry에서 +`employeePopup="adapter"`와 `employeePopupAdapter`를 명시하여 연결한다. +Core/Vanilla와 기존 데이터에는 React/HM 의존성이나 변경을 추가하지 않았다. +기존 HM 소비자는 Import만 `/hm-dialog`로 변경하고 기존 Props를 유지한다. +모든 bundler 조합은 별도 미검증이다. ```tsx +import { OrgChart } from '@hm-design/org-chart-react/hm-dialog'; + --scope PARTIAL --profile STORYBOOK --files department-colors.ts,expanded-content.ts,style-presets.ts --json` → [guard-repro.json](../../test-results/org-chart-p10-3/guard-repro.json) | +| 재현 결과 | exit 1, FAIL **10**, P10과 code/file/line/message **10/10 동일**, 새 FAIL 0 | + +P10 원래 실행의 정확한 15개 경로 `--files` 명령은 보고서·로그에 기록이 없다. 그래서 FAIL이 나온 3개 파일만 선택해 재현했다. 출력 파일은 Worktree의 gitignore 대상 경로와 scratchpad에만 썼다. Guard는 stdout 외에 파일을 쓰지 않는다. + +## 2. Guard의 판정 방식 (근거) + +`guard-theme-analysis.mjs` 기준: + +- **정의(definitions) 수집 출처**는 세 가지뿐이다. ① graph에 포함된 소비 CSS 파일의 선언, ② 공개 CSS 매핑(`@hm-design/ui/theme.css`, `@hm-design/ui/dashboard-status.css`)과 그 상대 `@import`(L85-89), ③ JS object literal의 `'--x'` key(L102). +- `element.style.setProperty('--hm-…', …)`는 **정의로 수집하지 않는다**. +- `@hm-design/org-chart/style.css`는 공개 CSS 매핑에 없다. Storybook에서는 이 CSS를 `?url`로 import해 Shadow DOM에 주입하므로 graph에서도 보이지 않는다(L83의 `r.css`가 아님). +- `--hm-` 참조가 미정의면 fallback이 있어도 **FAIL**이다(L128, [Guard 정책 TOKEN-001](../guidelines/ai-generation-guard.md): "fallback 색이 있어도 없는 HM token은 FAIL"). +- 같은 동작은 P6 최소 재현 스크립트 `examples/org-chart/verify-guard-p1.mjs`에도 assertion으로 고정되어 있다. Shadow CSS 제외 → FAIL, setProperty 정의 미수집 → FAIL. + +## 3. 필수 요약표 + +| ID | 검사명 | 실제 원인 | 재현 | 영향 | 차단 등급 | 수정 방안 | 예상 시간 | +|---|---|---|---|---|---|---|---| +| F1 | TOKEN-001 `department-colors.ts:47` `--hm-orgchart-current-gradient` | **B** 런타임 선언(`applyOrganizationStyle(row)` setProperty, L28)을 Guard가 수집하지 못함 | 재현 | 없음. Storybook Department Colors 전용, dist 미포함 | LOW | Guard에 literal setProperty 정의 수집 추가(승인 필요) 또는 예외 승인 | F1·F2·F8~F10 공통 60~120분 | +| F2 | TOKEN-001 `department-colors.ts:47` `--hm-orgchart-current-card-background` | **B** F1과 동일 | 재현 | 없음 | LOW | F1과 동일 | (F1에 포함) | +| F3 | TOKEN-001 `department-colors.ts:75` `--hm-orgchart-contrast-candidate-color` | **D** 저장소 어디에도 선언 없는 HM 토큰. 미승인 대비 후보 미리보기(`UNAPPROVED_NOT_APPLIED`)의 fallback `#10243b`로만 렌더 | 재현 | 외부 패키지 영향 없음(dist 미포함, `createDepartmentColors` 0회). **Guard 규칙상 실제 위반** | MEDIUM | 같은 값의 literal로 교체(색 변화 없음). 또는 미리보기 CSS에 선언(단 `?url` CSS는 Guard graph 밖이라 해소 안 됨) | F3~F5 합계 15~25분 | +| F4 | TOKEN-001 `department-colors.ts:78` `--hm-orgchart-contrast-candidate-color` | **D** F3과 동일 (chip label) | 재현 | F3과 동일 | MEDIUM | F3과 동일 | (F3에 포함) | +| F5 | TOKEN-001 `department-colors.ts:78` `--hm-orgchart-contrast-candidate-chip-background` | **D** 선언 없음, fallback `#ffffff` | 재현 | F3과 동일 | MEDIUM | F3과 동일 | (F3에 포함) | +| F6 | TOKEN-001 `expanded-content.ts:30` `--hm-orgchart-resolved-fg-primary` | **B+A** 변수는 `styles/design-tokens.css:12` → `dist/style.css`에 선언됨(Guard graph 밖). 그리고 이 줄의 `--hm-orgchart-expanded-title-ink`는 **소비처가 없는 dead code**(d0828a7에서 CSS `color: var(--…title-ink)` 삭제, 제목은 `#ffffff` 고정 L75) | 재현 | 렌더 영향 없음. 삼안·장헌산업만 어두운 글자를 쓰는 것처럼 오해할 수 있는 유지보수 위험 | LOW | dead `setProperty` 1줄 삭제(시각 변화 없음) | 5~10분 | +| F7 | TOKEN-001 `expanded-content.ts:30` `--hm-orgchart-resolved-on-action-primary` | **B+A** F6과 동일(`design-tokens.css:20`) | 재현 | F6과 동일 | LOW | F6과 동일 | (F6에 포함) | +| F8 | TOKEN-001 `style-presets.ts:9` `--hm-orgchart-resolved-bg-surface` | **B** `design-tokens.css:8` → `dist/style.css`에 선언. 공개 CSS 매핑에 OrgChart CSS 없음 / `?url` Shadow 주입 | 재현 | 없음(Fixed/Expanded 실행 PASS 이력) | LOW | Guard 공개 CSS 매핑에 `@hm-design/org-chart/style.css` 추가(승인 필요) 또는 예외 승인 | (F1에 포함) | +| F9 | TOKEN-001 `style-presets.ts:74` `--hm-orgchart-current-border` | **B** 같은 함수 L69에서 같은 element에 setProperty로 선언 | 재현 | 없음. 배포 JS 경로(render.ts L84) | LOW | F1과 동일 | (F1에 포함) | +| F10 | TOKEN-001 `style-presets.ts:75` `--hm-orgchart-current-gradient` | **B** 같은 함수 L71에서 setProperty로 선언 | 재현 | 없음 | LOW | F1과 동일 | (F1에 포함) | + +분류: A 0(단독) · **B 7** (F1, F2, F6~F10. F6·F7은 A 성격의 dead code 동반) · C 0 · **D 3** (F3~F5) · E 0 · UNKNOWN 0. + +## 4. 개별 근거 (12개 필수 항목 요약) + +공통 사항: 10건 모두 **기존 문제**다(P9.4와 동일, P10 신규 0). 모두 **OrgChart 도입으로 발생**했다(`61c6f8d`/`a92913d`/`064517e`, 2026-10-12. main에는 OrgChart가 없음. C 레거시 아님). 모두 재현됐다. + +- **F1·F2** — 조건: Storybook graph에 직접 import된 `department-colors.ts`가 선택 파일일 때. swatch는 `applyOrganizationStyle(row, index)`가 실행된 `row`의 자손이므로 실제 cascade로 값이 정의된다. 외부 사용 영향 없음. 배포 영향 없음. +- **F3~F5** — 조건: 같은 파일. 세 변수 모두 저장소 전체에서 선언 0건이다(rg 확인). override hook을 소비하는 곳도 없다. 렌더 결과는 fallback과 같다. Guard 정책 문구상 정당한 FAIL이며 오탐이 아니다. 다만 실행 영향은 없고 Storybook 전용이다. 접근성 대비 후보의 승인 여부와는 별개다. +- **F6·F7** — 조건: Expanded Storybook graph. 참조 변수 자체는 공개 패키지 CSS에 선언되어 있다(dist 확인). 남은 문제는 dead setProperty다. 제거하면 두 FAIL이 사라지고 화면은 바뀌지 않는다. +- **F8~F10** — 조건: `style-presets.ts`가 선택 파일일 때. F8은 패키지 CSS에 선언되어 있지만 graph에서 보이지 않는다. F9·F10은 같은 함수 안의 setProperty 직후 참조라서 런타임에서 항상 정의된다. + +## 5. 배포 차단 판정 + +| 구분 | 판정 | 근거 | +|---|---|---| +| 기술적 배포 차단 | **없음 (BLOCKER 0)** | 빌드 실패·공개 API 파손·데이터 손상 없음. F1·F2·F6~F10은 실제 선언이 존재한다(design-tokens.css / dist/style.css / setProperty). F3~F5는 fallback으로 렌더되며 배포 산출물에 포함되지 않는다 | +| CI상 차단 | **CI 근거 없음** | 저장소에 `.github`·`.gitea`·`.husky`·CI yaml이 없다. `package.json`에 Guard를 실행하는 build/test 훅도 없다(`test:guard`는 Guard 자체 단위 테스트). 원격 Gitea 서버 측 설정·브랜치 보호는 **NOT VERIFIED** | +| 조직 정책상 차단 | **UNKNOWN — 사용자 결정 필요** | Guard 정책상 exit 1=FAIL이다. P10 보고서는 "Guard 잔여 수용 여부를 정리한 뒤 명시 승인"을 Commit/Push 조건으로 두었다. 이 근거만으로 배포 가능하다고 판정하지 않는다 | + +건수: **BLOCKER 0 / HIGH 0 / MEDIUM 3 (F3~F5) / LOW 7 / UNKNOWN 0**. + +## 6. 수정 우선순위 (실행하지 않음) + +| 순서 | 구분 | 대상 | 최소 수정 | 예상 | 제품 영향 | 병렬 | 수정 후 검증 | +|---|---|---|---|---|---|---|---| +| 1 | 배포 전 권장 | `packages/org-chart/src/core/expanded-content.ts:30` | dead `--hm-orgchart-expanded-title-ink` setProperty 삭제 → F6·F7 해소 | 5~10분 | 없음(제목 `#ffffff` 유지, 회사색 불변) | 가능(P10.2 미수정 파일) | Core typecheck/test, Guard PARTIAL 해당 파일, Expanded 제목 computed color 1회 | +| 2 | 배포 전 권장 | `packages/org-chart/src/core/department-colors.ts:75,78` | 미선언 HM var를 동일 값 literal로 교체 → F3~F5 해소. 후보 색은 미승인·미적용 상태 유지 | 15~25분 | 없음(Storybook 미리보기, 동일 색) | 가능 | Guard PARTIAL, Department Colors Story 1회(다른 Guard 규칙이 hex literal을 지적하는지 확인 필요) | +| 3 | 별도 의사결정 | `scripts/guard-theme-analysis.mjs` + `ai-generation-guard.test.mjs` | ⓐ `@hm-design/org-chart/style.css`(`?url` 포함)를 공개 CSS로 인식 ⓑ literal `style.setProperty('--hm-…')`를 정의로 수집 → F1·F2·F8~F10 해소. 규칙 완화가 아니라 탐지 정확도 개선이지만 Guard 계약 변경이라 승인 필요 | 60~120분 | 제품 영향 없음. 다른 소비 화면 판정이 바뀔 수 있어 회귀 필요 | 가능 | `node --test scripts/ai-generation-guard.test.mjs` 전체, 기존 Baseline 수량 대조, OrgChart PARTIAL 재실행 | +| 3' | 대안(승인 필요) | 문서 | 3을 하지 않으면 F1·F2·F8~F10을 "Guard 탐지 범위 밖 · 런타임 선언 확인됨" 예외로 기록 | 15분 | — | — | 원본 FAIL 보존 | + +- **즉시 수정 대상(BLOCKER):** 없음. +- **최소 수정 대상 파일:** `expanded-content.ts`, `department-colors.ts`(+ 선택 시 Guard 2개 파일). +- **예상 총 수정 시간:** 코드 1·2: 20~35분 + 검증 20~30분. Guard 확장 3 포함 시 **약 1.5~3시간**. +- 1·2만 적용하면 FAIL은 10 → 5건이 되고 Guard status는 여전히 FAIL이다. Guard PASS에는 3 또는 3'의 결정이 필요하다. +- OrgChart를 검사 대상에서 빼거나 규칙을 완화하는 방안은 제시하지 않는다. + +## 7. 디자인·접근성 구분 (삼안·장헌산업) + +1. 흰 제목(`#ffffff`) on 회사 primary의 일반 텍스트 WCAG 미달 진단은 **미해결로 유지**한다(이번에 재측정하지 않음 → NOT VERIFIED). +2. **Guard FAIL 10건에 포함되지 않는다.** F6·F7은 이 회사들의 제목 색을 바꾸던 이전 코드의 잔재일 뿐이고 현재 렌더에 관여하지 않는다. +3. 사용자 승인 디자인 예외다. 접근성 PASS가 아니다. +4. 배포 정책상 WCAG 준수가 필수인지는 근거 문서가 없다 → 별도 승인 필요. +5. 색 변경 없는 대안(텍스트 그림자, 굵기 증가, 제목 영역 내부 흰 label 등)은 P11 또는 별도 디자인 결정 사항이며 이번에 적용하지 않는다. 회사색·카드 디자인은 변경하지 않았다. + +## 8. 외부 패키지 구조와의 관계 + +FAIL 10건은 모두 `@hm-design/org-chart` Core 소스의 CSS 변수 참조에서 나온다. React Wrapper, `/hm-dialog` Entry, `employeePopupAdapter`, Optional Peer, HMDesign UI 정적 import, exports, 타입과는 **관련이 없다**. 기준 Commit에는 P10.2 Wrapper 개선이 없으며, 이번 결과를 개선 코드의 실패로 보지 않는다. Package Graph와 외부 소비자 기준 Guard는 별도 실행하지 않았다(NOT VERIFIED). + +## 9. P10.2와의 충돌 가능성 + +P10.2 미커밋 변경 파일(AGENTS/CLAUDE, docs, `packages/org-chart-react/*`, `src/org-chart/org-chart-expanded.stories.tsx`)과 수정 후보 파일은 **겹치지 않는다**. 다만 P10.2가 바꾸는 Expanded Story가 Guard graph에 포함되므로, 통합 후 PARTIAL Guard를 1회 다시 실행해야 한다. Guard 확장(3)은 AGENTS·Guard 정책 문서 갱신과 겹칠 수 있다. + +## 10. NOT VERIFIED + +- P10 원래 15개 경로 Guard 명령(기록 없음. 3개 파일 subset으로 동일 FAIL 재현) +- dist 선언 확인은 P10 작업본의 기존 `packages/org-chart/dist`를 읽은 것이다. 이 dist가 5acaf8a에서 빌드되었는지는 확인하지 않았다(Core 소스는 P10.2 미수정) +- 원격 Gitea 서버 CI·브랜치 보호, 조직 배포 정책상 Guard 필수 여부 +- Runtime / Design Intent / 소비자(React·Vanilla) 기준 Guard graph +- 삼안·장헌산업 대비 재측정 +- 수정안 적용 시 다른 Guard 규칙의 반응 + +## 11. 작업 제한 준수 + +제품 코드·Core·React Wrapper·Storybook·Guard 규칙·회사색 수정 없음. P10.2 작업본 파일 수정 없음(읽기만 함). 새 Worktree·브랜치를 만들고 이 보고서와 gitignore 대상 재현 JSON만 추가했다. Commit·Push·Merge·Rebase·Cherry-pick·PR·main Checkout은 진행하지 않았다. diff --git a/docs/org-chart/integration-report-p10-2.md b/docs/org-chart/integration-report-p10-2.md new file mode 100644 index 0000000..04fe0a4 --- /dev/null +++ b/docs/org-chart/integration-report-p10-2.md @@ -0,0 +1,103 @@ +# HMDesign OrgChart P10.2 — 병렬 결과 통합 및 Chart Only 검증 + +RESULT: **PARTIAL**. 문서 통합·React Entry 정합성은 PASS다. 실제 공개 API의 Chart Only 출력은 PARTIAL이며, 기존 P10 PARTIAL 및 Guard FAIL 10건을 유지한다. 신규 제품 코드·디자인·공개 API를 변경하지 않았다. + +## 1. 작업 상태와 보호 + +- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`. +- 브랜치: `feature/orgchart-package`. +- 통합 전·후 HEAD: `5acaf8a29596317133e54e8ad437ee00beba1cf0`. +- 공식 origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 읽기 전용 `ls-remote`로 Feature HEAD 일치 확인. +- 시작 Working Tree: tracked 수정 9개 + untracked 4개 = 기존 React Wrapper 개선 13개. Stage 0. +- 기존 13개 파일을 [사본·SHA-256](../../test-results/org-chart-p10-2/baseline.json)과 binary diff로 보존했다. 통합 후에도 13개 모두 byte 동일하다. +- ChartOnly: `feature/orgchart-ai-chart-only-policy`, HEAD `5acaf8a…`, AGENTS/CLAUDE/정책 3개 수정. UsageGuide: `feature/orgchart-usage-guide`, HEAD `5acaf8a…`, 가이드 1개 신규. 공통 수정 파일 0. 각 Worktree 원본을 변경하지 않았다. +- AI Playground·삼안 500명 데이터/페이지는 읽기·복사·수정·통합하지 않았다. 서버 존재만 보호 목적으로 확인했다. + +기존 서버는 Storybook 6024, Vanilla 예제 4178, 기존 검증 6064/6074, AI Playground preview 6067, 외부 소비자 preview 4187이었다. 모두 재시작·종료·수정하지 않았다. 이번 브라우저 검사는 별도 임시 포트에서 수행한 뒤 해당 검사 서버만 종료했다. + +## 2. 통합 결과 + +- **PASS — React Wrapper 보존:** 기본 Entry는 HM UI runtime/type 의존성을 요구하지 않는다. 선택 `/hm-dialog` Entry만 HM UI를 사용한다. React 19 peer·Core 독립성과 기존 Props를 유지한다. +- **PASS — 기존 소비자 Import:** Expanded Story는 이미 `/hm-dialog`로 전환되어 있으며 `employeePopup="hm-dialog"`를 유지한다. Full Composition은 기본 Entry/Core popup을 유지한다. 예제의 기본 Entry는 Core popup이므로 변경이 필요 없다. +- **PASS — Popup Adapter:** 기본 Entry의 `employeePopup="adapter"` + `employeePopupAdapter` 계약을 문서화했다. Adapter 전달이나 HM UI 설치만으로 전환하지 않는다. `onEmployeeClick`이 우선하며 Fixed는 기존 Core/호스트 callback 경로를 유지한다. +- **PASS — Chart Only 정책:** ChartOnly diff를 검토하고 관련 추가 사항만 통합했다. 목표는 White Canvas + OrgChart Nodes + Connectors다. 회사·Depth·인원·특정 사례를 기본 템플릿으로 고정하지 않는다. 실제 API 한계 문단을 추가해 정책 목표와 지원 기능을 구분했다. +- **PASS — AGENTS/CLAUDE:** 기존 다른 지침을 보존하고 같은 정책을 참조한다. 기존 Full Composition·Expanded·Department Colors·Overview에 소급 적용하지 않는다. +- **PASS — 공통 가이드:** [사용 가이드](org-chart-usage-guide.md)를 통합하고 최소 tarball 설치·기본 Entry·`/hm-dialog`·`HMEmployeeDialog` Export·선택 popup·커스텀 Adapter 예제·React/Vanilla 차이·실제 API 범위를 보완했다. 과거 lazy chunk 실패 제약은 현재 Entry 분리 결과로 갱신했다. 원본 예제/데이터 검증 기록은 과거 범위로 보존했다. + +## 3. 최소 설치·Entry·문서 검증 + +기존 [최소 설치 보고서](minimal-install-report-2026-10-13.md)의 실제 npm pack A/B/C 설치·strict 타입·Production Build·브라우저 결과를 재사용했다. 성공한 동일 조건 검사를 반복하지 않았다. 현재 Core/Wrapper/UI의 모든 최상위 배포 JS·`.d.ts`·CSS를 실제 tarball로 설치했던 각 소비자의 파일과 byte 비교했고 동일했다. [재사용 근거](../../test-results/org-chart-p10-2/reused-pack-evidence.json). + +- **A Core + Vanilla: PASS 재사용.** React/HM UI 없는 설치·타입·Build·Fixed/Auto·Core popup. +- **B Core + React Wrapper: PASS 재사용.** HM UI/Theme/Tailwind 없는 기본 Entry·React 19.3.0·타입·Build·Core popup·커스텀 Adapter. +- **C Core + React Wrapper + HM UI: PASS 재사용.** 명시 `/hm-dialog` 선택·HM Dialog·닫기/Escape/focus 복귀·Core 전환. +- 기본 Entry 전이 그래프에서 HM UI 참조 없음 PASS 재사용. exports와 실제 설치 JS/타입 경로 일치 확인. +- App/Storybook Typecheck와 Storybook Production Build는 기존 최소 설치 작업의 PASS를 재사용했다. 제품 코드가 이번 통합에서 변경되지 않아 반복하지 않았다. +- **새 가이드 TS/TSX 10개 strict 타입 검사: PASS.** 현재 public package declarations, `skipLibCheck=false`. 이전 원본 가이드 8개 검사와 구분한다. [예제 목록](../../test-results/org-chart-p10-2/guide-examples.json). +- **새 독립 packed React/Vanilla Production Build: PASS.** 이미 설치된 실제 packed dependency를 사용했으며 신규 패키지 설치·lockfile 변경·alias를 제품에 추가하지 않았다. 테스트 전용 resolve 설정만 사용했다. +- 변경 문서 5개 내부 Markdown 링크 85개 경로/anchor 및 `git diff --check`: PASS. [최종 감사](../../test-results/org-chart-p10-2/final-audit.json). + +기존 빌드 경고인 `:host-context`, `use client` bundling, 큰 chunk 경고는 유지된다. 새 테스트 첫 브라우저 접속은 sandbox localhost 차단으로 중단되어 승인된 실행에서 완료했다. 연결선(TB CSS / LR·Hybrid SVG)과 Storybook 전용 Preview DOM을 실제 소스에 맞춰 검사했다. 테스트 harness 문제를 제품 FAIL로 계산하지 않는다. + +## 4. 실제 Chart Only — PARTIAL + +테스트 데이터는 가상 한맥기술 1회사·2본부·본부별 2팀·7조직·20명이다. 대표 1 + 본부장 2 + 팀장 4 + 직원 13을 고유 ID로 한 번씩 집계했다. 정책의 고정 기본값이 아니다. + +[브라우저 측정](../../test-results/org-chart-p10-2/browser.json), [React 1440px 캡처](../../test-results/org-chart-p10-2/react-1440.png), [Vanilla 390px 캡처](../../test-results/org-chart-p10-2/vanilla-390.png), [Fixed 캡처](../../test-results/org-chart-p10-2/fixed-1440.png). + +- **PASS:** 실제 React/Vanilla, 390/768/1280/1440px. 20개 고유 직원 카드·7개 조직·TB 조직 연결선 6개 및 직원 연결선 표시. 한맥 기존 컬러 `#CB2E28`, 기존 카드/엔진/CSS 재사용. 외부 페이지·설명·JSON·테스트 패널·Navigation을 생성하지 않았다. +- **PASS:** document/body와 내부 viewport는 흰색. document 가로 overflow 없음. 기본 키보드 Zoom/Pan. Console Error·pageerror·HTTP 실패 0. +- **제한 확인:** Core가 내부 제목·기준일·회사/인원 요약·Zoom/Fit toolbar·배율/조작 안내·Drag & Drop OFF 안내를 생성한다. 조직도 바깥 Auto 영역은 `rgb(244, 247, 251)`이다. 공개 API로 모두 제거하는 옵션이 없다. 따라서 최상위 흰색 캔버스만 있는 목표는 만족하지 못한다. +- **초기 중앙 배치 PARTIAL:** 실제 전체 Bounds의 가로 중앙 오차 0px, 세로 오차 +182.25px. 기본 배치가 상단 16px 정렬이고 전체 Fit/세로 중앙 정렬이 아니다. 모든 콘텐츠를 축소 없이 한 번에 볼 수 있다는 뜻이 아니다. +- **Vanilla 공개 Fit PASS:** `fitToView()` 호출 뒤 가로·세로 중앙 오차 절댓값 0.001px 미만. 실제 Bounds를 사용하고 콘텐츠를 맞춘다. +- **React 한계:** 외부 handle/ref·초기 Fit prop이 없어 호스트가 버튼 바 없이 초기 Fit을 직접 요청할 경로가 없다. 테스트의 React 캡처는 기존 내장 Fit 버튼을 사용한 상태이며 초기 기본 출력 PASS로 해석하지 않는다. +- **Fixed PARTIAL:** 동일 20명 readOnly 렌더링에서도 제목·기준일·기존 요약과 회색 영역이 남는다. 승인된 Workdesk 표시 경로는 원본 직원 이름 대신 `홍 길 동`을 표시한다(사용 가이드 §6의 기존 제약, 데이터 원본/ID는 유지). 제공 인명을 그대로 표시하는 일반 Chart Only 결과로 사용할 수 있다고 주장하지 않는다. Fixed 전체 화면의 Zoom/Pan·Fit은 Auto API와 같지 않다. + +`display:none`, DOM 후처리 삭제, 전역 `!important`, Core 내부 수정 없이 관찰했다. 공개 옵션으로 조직 데이터·기본 white viewport·탐색은 가능하지만, 완전한 부가 UI 제거와 Root 캔버스-only 출력은 불가능하다. + +**후속 개선 후보(미구현):** 기본값을 보존하는 선택형 `presentationMode` 또는 heading/summary/toolbar/feedback 표시 계약, white root/full-canvas 레이아웃, React/Vanilla 공통 초기 Fit 정책. 표시 상태와 Zoom/Pan 이벤트를 분리하고 접근성 live status는 화면 문구와 구분해야 한다. 영향 후보는 Core options/types, render heading/composition, expanded-content/viewport/CSS와 Wrapper Props 전달, 사용 가이드 및 소비자 검증이다. 타입/API 이름은 제안이며 확정하지 않았다. 기존 Fixed/Expanded 기본값·디자인 보존 검사가 필요한 별도 작업이며 이번 범위에서 자동 시작하지 않는다. + +## 5. 기존 Storybook·Guard·미검증 + +- **PASS:** 기존 4개 공개 메뉴 유지 — Overview(`orgchart--logo-personnel-summary`), Department Colors, Full Composition, Expanded OrgChart. 현재 Storybook index를 확인하고 Department Colors/Overview 실제 렌더링·콘솔 오류 0을 확인했다. [Storybook 증거](../../test-results/org-chart-p10-2/storybook.json). +- **PASS 재사용:** Full Composition Core popup 및 Expanded HM Dialog/Escape/focus 복귀. 관련 원본/수정 13개 파일의 byte 동일성을 확인했다. 신규 제품 회귀는 이번 검사 범위에서 발견되지 않았다. 전체 회귀 PASS를 선언하지 않는다. +- Core/UI/Theme·Fixed SVG/PNG·회사 컬러·Department Colors/Overview·기존 JSON Contract·Drag & Drop·Guard 설정을 변경하지 않았다. 외부 CSS 테마 표준화(P11)는 진행하지 않았다. +- **기존 Guard FAIL 10건 유지:** P10.1 원본 결과를 보존했다. Wrapper 기존 PARTIAL Guard의 FAIL 0 / NOT VERIFIED 5도 보존했다. +- **새 임시 화면 PARTIAL Guard 1회:** FAIL 0, RENDER-002 NOT VERIFIED 1. staticStatus NOT APPLICABLE, 원본 status/runtimeStatus NOT VERIFIED. [원본 검사](../../test-results/org-chart-p10-2/guard-fixture.json)는 덮어쓰지 않고 별도 브라우저 증거를 연결한다. styleEvidence STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS(적용 검사 0) / FINAL NOT VERIFIED다. 디자인 승인으로 해석하지 않는다. +- **NOT VERIFIED:** 2,100명 성능·전체 P10 회귀, SSR/hydration·다른 bundler/React major/OS·실제 touch·전체 스크린리더·시각 디자인 승인, Fixed의 가변 데이터 모든 배치/반응형, 가이드 HMChart/CustomChart 예제 자체의 브라우저 시연. 기존 검사 결과나 타입 PASS를 이 항목의 Runtime PASS로 확대하지 않는다. + +## 6. 변경 파일·Git·시간 + +이번 P10.2 변경은 아래 5개 문서다. + +- `AGENTS.md` +- `CLAUDE.md` +- `docs/org-chart/ai-page-generation-rules.md` +- `docs/org-chart/org-chart-usage-guide.md` (신규, 원본 가이드 통합·보완) +- `docs/org-chart/integration-report-p10-2.md` (신규) + +기존 React Wrapper 13개 변경은 그대로 유지한다. + +- `docs/org-chart/expanded-usage.md` +- `packages/org-chart-react/README.md` +- `packages/org-chart-react/build.mjs` +- `packages/org-chart-react/package.json` +- `packages/org-chart-react/src/HMEmployeeDialog.tsx` +- `packages/org-chart-react/src/OrgChart.tsx` +- `packages/org-chart-react/src/index.ts` +- `packages/org-chart-react/src/types.ts` +- `src/org-chart/org-chart-expanded.stories.tsx` +- `docs/org-chart/minimal-install-report-2026-10-13.md` (신규) +- `packages/org-chart-react/src/hm-dialog.tsx` (신규) +- `scripts/verify-orgchart-hm-story.mjs` (신규) +- `scripts/verify-orgchart-minimal-install.mjs` (신규) + +최종 Working Tree: tracked 수정 12개 + untracked 6개 = 18개. Stage 0. 테스트 fixture·빌드·스크립트·캡처·증거는 Git ignored `test-results/org-chart-p10-2`에만 있으며 패키지/Feature Commit에 포함하지 않았다. + +**Stage·Commit·Push·Merge·main Checkout·Rebase·Cherry-pick·Pull·Reset·Clean·Stash pop·Force Push 모두 미진행.** 기존 Feature HEAD와 다른 Worktree를 보존했다. + +- Started At: 2026-10-13 15:37:03 KST +- Completed At: 2026-10-13 15:55:33 KST +- Actual Duration: 18분 30초 +- Estimated Duration: 약 35–50분 +- Variance: 예상보다 16분 30초–31분 30초 짧음 diff --git a/docs/org-chart/minimal-install-report-2026-10-13.md b/docs/org-chart/minimal-install-report-2026-10-13.md new file mode 100644 index 0000000..540f7a5 --- /dev/null +++ b/docs/org-chart/minimal-install-report-2026-10-13.md @@ -0,0 +1,125 @@ +# OrgChart 선택형 UI 의존성 및 최소 설치 검증 + +RESULT: **PASS — 이번 최소 설치·선택형 Adapter 개선 범위.** 기존 P10 전체 판정 PARTIAL과 기존 Guard FAIL 10건은 유지한다. 전체 회귀·디자인 승인 결과로 확장하지 않는다. + +## 1. 실제 실패 원인 + +원본 외부 로그: `D:\디자인시스템\조직도 테스트\evidence\consumer-build-without-ui.log`. +기준 Feature/HEAD: `feature/orgchart-package`, `5acaf8a29596317133e54e8ad437ee00beba1cf0`. + +기본 React Entry → `OrgChart.tsx` → `lazy(() => import('./HMEmployeeDialog.js'))` → Dialog 청크 → `@hm-design/ui` 순으로 연결됐다. UI가 없는 소비자도 Vite 8.3.0이 dynamic import 청크를 Production Build에서 해석했다. optional peer stub에는 named export가 없어 다음 오류를 포함한 **12건의 MISSING_EXPORT**로 실패했다. + +```text +[MISSING_EXPORT] "Avatar" is not exported by "__vite-optional-peer-dep:@hm-design/ui:@hm-design/org-chart-react". +node_modules/@hm-design/org-chart-react/dist/HMEmployeeDialog-gdk9Uddu.js:3:10 +``` + +React.lazy는 실행 시 로딩을 늦추지만 소비자의 빌드 의존성 그래프에서 해당 Import를 제거하지 않는다. 원인을 확인한 후에만 Entry를 분리했다. + +## 2. 의존성·Import·공개 API + +- Core dependencies는 기존 `orgchart: 6.0.0` 그대로다. React/HM UI dependency, peer, Import를 추가하지 않았다. +- Wrapper dependencies는 `@hm-design/org-chart: 0.1.0` 그대로다. `react: ^19.0.0` peer 및 `@hm-design/ui: 0.1.0` optional peer 선언도 그대로다. 신규 런타임 라이브러리·lockfile 변경이 없다. +- 기본 `@hm-design/org-chart-react`는 UI-independent 공유 Wrapper만 Import한다. runtime JS와 `.d.ts`의 전이 그래프에 UI Import/참조가 없다. +- 신규 공개 `@hm-design/org-chart-react/hm-dialog`는 같은 Props의 `OrgChart`와 `HMEmployeeDialog`를 제공한다. 이 Entry만 HM UI 공개 API를 Import한다. package exports의 JS/타입 경로와 실제 tarball 산출물이 일치한다. +- 기존 `OrgChart`, 기존 Props, Core type-only re-export, `'core' | 'hm-dialog' | 'none'` 값을 유지했다. `'adapter'`, `employeePopupAdapter`, `OrgChartEmployeeSelection`, `OrgChartEmployeePopupProps`는 추가 API다. +- 기본값은 항상 Core다. HM UI 설치, HM Entry Import 또는 Adapter 전달만으로 팝업을 전환하지 않는다. `employeePopup` 설정으로 사용자가 명시적으로 선택한다. +- 기본 Entry에서 `'hm-dialog'`/`'adapter'`를 선택하고 Adapter를 제공하지 않으면 Core 팝업으로 유지하며 `onLifecycleError`에 구성 오류를 전달한다. `onEmployeeClick`은 기존대로 우선한다. +- HM Adapter의 직원 필드·Avatar·Dialog·닫기·Escape·focus 복귀 구현은 유지했다. 타입 계약만 공용 UI-independent 타입으로 이동했다. +- Auto에서 팝업 소유권 전환은 기존 `setEmployeeClickHandler`를 사용한다. Core 인스턴스와 데이터는 재생성·변경하지 않는다. Fixed의 기존 Core 팝업 정책은 유지하며, 다른 UI는 기존 `onEmployeeClick`으로 연결할 수 있다. +- Department Colors와 Overview의 별도 페이지 Export를 추가하지 않았다. AI 실험 페이지를 이관하지 않았다. + +**기존 HM 소비자의 설치/API 전환은 Import 한 줄이 필요하다.** 기존 기본 Entry와 `employeePopup="hm-dialog"`만 사용한 코드는 다음 Entry를 선택하거나 `HMEmployeeDialog`를 명시적으로 주입해야 한다. 자동 HM 로딩은 더 이상 제공하지 않는다. + +```tsx +import { OrgChart } from '@hm-design/org-chart-react/hm-dialog'; +; +``` + +다른 디자인 시스템은 기본 Entry에서 `employeePopup="adapter" employeePopupAdapter={MyPopup}`를 사용한다. Adapter는 `{ selection, onClose }`를 받으며 직원/context 원본을 수정하지 않고 접근성·닫기·focus 복귀를 소유한다. Core와 조직도 JSON을 수정하지 않는다. + +## 3. CSS·자산·설치 경계 + +Core의 공개 `engine.css`와 `style.css` 실제 배포 URL을 기존 `stylesheetUrls`로 Shadow DOM에 연결했다. Core CSS는 기존 빌드가 source-relative CSS를 inline한 자체 산출물이며 HM Theme/CSS를 Import하지 않는다. React Wrapper에는 자동 CSS Import나 새로운 자산 참조가 없다. HM Entry도 Theme를 자동 Import하지 않고 호스트의 실제 HM Theme/Tailwind 연결을 사용한다. + +A/B에서는 HM Theme나 Tailwind를 설치하지 않았다. A에는 React도 없다. C는 선택 HM UI와 해당 peer/Theme를 소비 프로젝트에서 제공했다. 일반 콘텐츠 CSS나 Fixed/Expanded 디자인을 수정하지 않았다. + +## 4. 실제 외부 프로젝트 A/B/C + +세 프로젝트는 저장소 및 상위 `node_modules` 밖의 새 임시 폴더에 만들었다. 소비자 소스에는 저장소 내부 Import/alias/symlink가 없다. `npm pack` 실제 `.tgz`를 dependency로 설치했고 `npm ls --all --json`으로 설치 그래프를 기록했다. 프로젝트 절대경로는 [projects.json](../../test-results/org-chart-minimal-install/projects.json)에 보존했다. + +- **A Core + Vanilla: PASS.** React·Wrapper·HM UI가 없는 실제 설치. strict Typecheck, Production Build, Fixed/Auto 렌더링 및 native Core 팝업·Enter·Escape·닫기·focus 복귀 PASS. +- **B Core + React Wrapper: PASS.** React/React DOM 19.3.0, HM UI·HM Theme·Tailwind 없음. 기본 선언 strict Typecheck, Production Build, Fixed/Auto 렌더링·Core 팝업 PASS. 다른 디자인 시스템의 팝업 Adapter 주입·callback 우선권·Adapter 미제공 시 Core 유지도 PASS. +- **C Core + React Wrapper + HM UI: PASS.** React 19.3.0, HM UI optional peer·Theme 연결. strict Typecheck 및 Production Build PASS. 기본 Entry와 HM Entry 모두 명시적 HM 선택 이전에는 Core를 유지한다. 기존 HM Dialog의 직원 정보·Enter·Escape·닫기·focus 복귀, HM→Core 전환 시 동일 Core 인스턴스 유지 PASS. + +[외부 브라우저 검사](../../test-results/org-chart-minimal-install/browser.json): **17개 그룹 PASS**, 브라우저 Chromium 155.0.8059.39, 콘솔 오류·브라우저 예외·HTTP 실패 0. StrictMode의 최종 host 1개와 입력 데이터 byte 보존도 검사했다. 이것은 전체 회귀 테스트 건수가 아니다. + +[기본 Entry 전이 그래프](../../test-results/org-chart-minimal-install/default-entry-graph.json): `index.js`, 공유 Wrapper JS, `index.d.ts`, `OrgChart.d.ts`, `types.d.ts`에 HM UI 참조가 없음 PASS. B의 `skipLibCheck: false` 타입 검사가 HM UI 없이 통과했다. + +실제 로그: [A 빌드](../../test-results/org-chart-minimal-install/build-A.log), [B 빌드](../../test-results/org-chart-minimal-install/build-B.log), [C 빌드](../../test-results/org-chart-minimal-install/build-C.log), [최종 외부 실행](../../test-results/org-chart-minimal-install/verification-online.log). + +첫 offline 설치 시도는 샌드박스 npm 캐시에 TypeScript가 없어 ENOTCACHED로 중단됐다. 네트워크 설치로 해결했고 패키지 FAIL로 계산하지 않았다. 반복 검증은 실패한 환경·미완료 검사만 보완했다. + +## 5. 기존 Storybook·최소 통합 검증 + +- `npm pack`: Core/UI/Wrapper의 prepack Build와 선언 생성 PASS. 공개 Entry가 실제 tarball에 포함됨 PASS. +- `tsc -p tsconfig.app.json --noEmit`: 기존 앱·Storybook TypeScript graph PASS. +- Storybook 10.6.1 Production Build: PASS. 기존 전체 회귀 suite는 실행하지 않았다. +- [Storybook 브라우저](../../test-results/org-chart-minimal-install/storybook-browser.json): Expanded의 기본 HM 팝업, Escape/focus 복귀, 기존 Select를 통한 Core 팝업 전환 및 Core 모달 focus PASS. Full Composition Fixed의 렌더링·Core 팝업·Escape PASS. 콘솔 오류·브라우저 예외 0. +- 외부 HM Dialog 캡처와 Storybook Expanded 캡처를 직접 확인했다. 레이아웃·직원 정보·닫기 버튼이 정상 표시됐다. 디자인 최종 승인을 의미하지 않는다. +- [Git/산출물 보존](../../test-results/org-chart-minimal-install/preservation.json): Core/UI/Center Graphic/Theme/기존 데이터/정책/P10 보호 파일의 tracked 수정 0. Expanded Story는 Import 한 줄만 변경했다. 최종 Wrapper tarball은 README의 잔여 lazy 설명만 보완하여 `--ignore-scripts`로 다시 pack했고, 모든 JS·`.d.ts`가 실제 설치·검증된 B/C와 byte 동일함을 확인했다. 정상 Build를 반복하지 않았다. + +빌드 경고는 기존 `:host-context` CSS minify 경고, vendor `use client` bundling 경고 및 큰 chunk 경고다. 빌드 실패는 없다. 이 작업에서는 관련 디자인·엔진 최적화 범위를 확대하지 않았다. + +## 6. Guard와 NOT VERIFIED + +기존 [P10.1 Guard](../../test-results/org-chart-p10-1/guard.json)의 FAIL 10건은 Core Token 참조에 대한 기존 기록이며 해당 파일을 수정하지 않았다. 원본 보고서를 보존했다. 전체 Guard를 반복하거나 전체 판정을 PASS로 바꾸지 않았다. + +이번 변경만 한 번 검사한 [PARTIAL Guard](../../test-results/org-chart-minimal-install/guard-partial.json)는 **FAIL 0 / NOT VERIFIED 5**다. 원본 `status`와 `staticStatus`는 NOT VERIFIED, `runtimeStatus`도 NOT VERIFIED로 그대로 둔다. + +- SCOPE-001 3건: 패키지 구현이 Guard의 증명된 Storybook graph 범위 밖임. 별도 npm pack·외부 build·브라우저 증거를 연결하며 원본 상태를 덮어쓰지 않는다. +- TOKEN-001: 기존 Expanded CSS의 Radix runtime 제공 변수. +- TOKEN-002: 기존 StoryPage의 동적 className. +- CSS styleEvidence: STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED. 기존 CSS를 수정하지 않았으며 기술적 팝업 성공을 CSS 디자인 승인으로 사용하지 않았다. + +SSR/hydration, 다른 React major·bundler·OS, 전체 스크린리더, 실제 touch, 장기 메모리/성능, 390/768/1280px의 재검증, 전체 회귀 및 디자인·배포 승인은 **NOT VERIFIED / 이번 범위에서 미실행**이다. 이번 브라우저 검증은 1440px 핵심 팝업 동작에 한정했다. 기존 유효한 검사 결과는 과거 증거로 보존한다. + +## 7. 변경 파일과 사용 가이드 반영 + +기존 수정 파일: + +- `packages/org-chart-react/package.json`: 선택 HM Entry Export 추가. dependency 선언 유지. +- `packages/org-chart-react/build.mjs`: UI-independent/HM Entry를 함께 빌드. +- `packages/org-chart-react/src/OrgChart.tsx`: UI Import 제거, 명시 Adapter 주입·Core fallback. +- `packages/org-chart-react/src/types.ts`: UI-independent popup Adapter 계약 및 추가 Props. +- `packages/org-chart-react/src/index.ts`: 공용 Adapter 타입 Export. +- `packages/org-chart-react/src/HMEmployeeDialog.tsx`: 팝업 타입을 공용 계약으로 변경. +- `src/org-chart/org-chart-expanded.stories.tsx`: HM 선택 Entry로 Import 한 줄 변경. +- `packages/org-chart-react/README.md`: 최소 설치·HM Import 전환·커스텀 Adapter·빌드 경계 안내. +- `docs/org-chart/expanded-usage.md`: HM 선택 Entry·Adapter 사용법 보완. + +신규 파일: + +- `packages/org-chart-react/src/hm-dialog.tsx`: 선택 HM Adapter Entry. +- `scripts/verify-orgchart-minimal-install.mjs`: 외부 실제 설치·타입·빌드·팝업 검증. +- `scripts/verify-orgchart-hm-story.mjs`: 기존 변경 Story/Fixed 핵심 팝업 검증. +- 이 보고서. + +병렬 사용 가이드 담당자가 반영할 내용: 최소 설치에서 UI/Theme 제외, 기존 HM 소비자의 `/hm-dialog` Import 전환, `employeePopupAdapter` 및 `'adapter'` 계약, package 설치만으로 전환하지 않는 정책, Fixed의 기존 Core/`onEmployeeClick` 정책. Chart Only·UsageGuide Worktree 파일은 수정하지 않았다. 같은 파일의 동시 변경을 발견하지 않아 현재 Feature에서 최소 수정했다. + +## 8. Git 및 시간 + +공식 origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. +Local/Remote Feature HEAD: `5acaf8a29596317133e54e8ad437ee00beba1cf0`. +읽기 전용 확인한 Remote main: `c45d63a03b84eaac43f01bd400a16488a80e4229`. main을 checkout·수정·Merge하지 않았다. + +Stage 0. **Commit·Push·main Merge 미진행.** 이 작업의 수정은 현재 Feature Working Tree에 남긴다. 기존 P10.1과 병렬 정책·사용 가이드 작업을 보존한다. + +- Started At: 2026-10-13 15:17:35 KST +- Completed At: 2026-10-13 15:32:28 KST +- Actual Duration: 14분 53초 +- Estimated Duration: 약 35–50분 +- Variance: 예상보다 20분 7초–35분 7초 짧음 + +검증 재현: 저장소의 일반 개발 의존성이 준비된 상태에서 `ORGCHART_PLAYWRIGHT_MODULE`을 기존 Playwright entry로 설정하고 `node scripts/verify-orgchart-minimal-install.mjs`를 실행한다. 기존 Chrome의 다른 설치 경로는 `ORGCHART_CHROME`으로 지정한다. 각 실행은 새 외부 임시 소비자를 만들고 기존 폴더를 삭제하지 않는다. Storybook 빌드 후 `node scripts/verify-orgchart-hm-story.mjs`로 선택된 기존 Story만 검사한다. 생성물은 `test-results/org-chart-minimal-install`에 보존하며 Stage하지 않았다. diff --git a/docs/org-chart/org-chart-usage-guide.md b/docs/org-chart/org-chart-usage-guide.md new file mode 100644 index 0000000..b198d98 --- /dev/null +++ b/docs/org-chart/org-chart-usage-guide.md @@ -0,0 +1,660 @@ +# HMDesign OrgChart 공통 사용 가이드 + +개발자·Codex·Claude가 현재 공개 API로 조직 데이터를 연결하기 위한 가이드다. 구현 기준은 `feature/orgchart-package`의 **`5acaf8a29596317133e54e8ad437ee00beba1cf0`**(2026-10-13)과 P10.2에서 통합한 React Wrapper Entry 개선이다. P10.3의 Git 공유 대상은 이 Feature 브랜치이며 실제 받은 버전은 `git rev-parse HEAD`로 확인한다. 과거 P-AI1의 구버전 제한을 현재 지원 여부로 사용하지 않는다. + +빠른 시작은 아래 데이터 파일과 React 또는 Vanilla 진입점을 함께 복사한다. 예제는 Vite의 TypeScript 환경을 전제로 한다. **이 가이드의 기본 예제는 현재 Auto 컴포넌트를 사용하는 예제이며, 외부 UI가 없는 Chart Only 구현 완료를 뜻하지 않는다.** + +## 1. 설치·패키지·실행 환경 + +- `@hm-design/org-chart@0.1.0`: 데이터 타입·Validator·JSON DataSource·집계·이동 함수·DOM 렌더러. React 없이 사용한다. 내부 엔진 의존성은 `orgchart@6.0.0`이며 소비자가 엔진을 직접 조작하지 않는다. +- `@hm-design/org-chart-react@0.1.0`: 위 Core에 의존하는 React lifecycle adapter. 조직 배치·카드·데이터 검증을 다시 구현하지 않는다. React peer는 `^19.0.0`, HM Dialog 선택에 사용하는 `@hm-design/ui@0.1.0` peer는 optional이다. +- 저장소는 npm workspaces다. Git Clone 후 소스에서 설치·빌드한다. 외부 Registry에 두 패키지가 게시됐다는 증거는 이번 범위에 없다. +- Core 렌더링은 DOM·Shadow DOM·ResizeObserver·native dialog를 사용하는 브라우저 작업이다. SSR 중에는 mount하지 않는다. React 호스트도 client 경계에서 렌더링한다. + +### Feature 브랜치 Git Clone + +Git, Node.js와 npm, 공식 Gitea 및 npm 레지스트리 접근이 필요하다. Node.js 조건은 `^20.19.0 || >=22.12.0`이며 Node 21은 제외한다. P10.3 실행 환경은 Node 22.23.2 / npm 10.9.8이다. 기존 폴더를 덮어쓰지 않고 새 경로에서 실행한다. ZIP·별도 설치 파일·다른 작업 폴더의 node_modules/dist 복사는 필요하지 않다. + +```powershell +git clone --branch feature/orgchart-package --single-branch https://gitea.hmac.kr/b25027/HMDesign.git HMDesign-OrgChart +cd HMDesign-OrgChart +git branch --show-current +git rev-parse HEAD +node --version +npm.cmd --version +npm.cmd ci +npm.cmd run build:packages +npm.cmd run typecheck --ignore-scripts +npm.cmd run test:org-chart +npm.cmd run build --ignore-scripts +``` + +`--ignore-scripts`는 위에서 완료한 package build를 pretypecheck/prebuild에서 반복하지 않기 위한 옵션이다. 이 위치에서 Typecheck/Build 본문은 실제로 실행된다. 루트 설치는 Storybook/HM UI를 포함하는 개발 workspace 설치이며 Core 소비자의 최소 의존성을 의미하지 않는다. + +Vanilla 예제는 아래 build 명령 후 `dev:org-chart-example`로 실행한다. React 예제는 package build 후 `build:org-chart-react-example`, `dev:org-chart-react-example`을 사용한다. HM Dialog Reference는 `npm.cmd run storybook`의 Expanded OrgChart에서 확인한다. 포트가 사용 중이면 기존 서버를 재사용한 결과를 독립 Clone 성공으로 보고하지 않는다. + +저장소의 설치 절차를 완료한 환경에서 사용하는 명령이다. Windows에서는 `npm.cmd`를 사용한다. + +```powershell +npm.cmd run build:org-chart +npm.cmd run build:org-chart-react +npm.cmd run build:org-chart-example +npm.cmd run dev:org-chart-example +# 또는 React 예제 서버 +npm.cmd run dev:org-chart-react-example +``` + +위 명령을 실행하면 기존 예제/산출물을 갱신할 수 있다. 병렬 문서 작업의 검증 명령으로 실행하지 않는다. 실행 위치·포트는 [Vanilla 예제 서버](../../examples/org-chart/serve.mjs), [React Vite 설정](../../examples/org-chart-react/vite.config.ts)을 확인하고 기존 서버와 충돌시키지 않는다. + +### 외부 프로젝트 최소 설치 + +외부 호스트의 최소 의존성을 검증할 때는 **Clone한 소스에서 직접 생성한** `npm pack` 산출물을 로컬 설치한다. tarball을 별도 전달하거나 Registry publish를 요구하지 않는다. 위 package build가 완료된 Clone 루트에서 다음을 실행한 뒤 아래 설치 경로를 생성된 파일 위치로 바꾼다. + +```powershell +New-Item -ItemType Directory -Path test-results/local-pack -Force +npm.cmd pack -w @hm-design/org-chart --ignore-scripts --pack-destination test-results/local-pack +npm.cmd pack -w @hm-design/org-chart-react --ignore-scripts --pack-destination test-results/local-pack +npm.cmd pack -w @hm-design/ui --ignore-scripts --pack-destination test-results/local-pack +``` + +생성물은 Git ignored 로컬 검증 자료다. A/B/C는 별도 호스트 폴더에서 아래처럼 설치한다. Registry publish 여부는 확인되지 않았다. + +```powershell +# A: Core + Vanilla. React/HMDesign UI 없이 설치한다. +npm.cmd install ./hm-design-org-chart-0.1.0.tgz +# B: Core + React Wrapper. HMDesign UI/Theme/Tailwind 없이 설치한다. +npm.cmd install ./hm-design-org-chart-0.1.0.tgz ./hm-design-org-chart-react-0.1.0.tgz react@19.3.0 react-dom@19.3.0 +# C: HM Dialog를 명시적으로 사용할 때만 HMDesign UI와 해당 peer를 추가한다. +npm.cmd install ./hm-design-ui-0.1.0.tgz +``` + +Core와 Wrapper의 `package.json`에서 실제 dependency/peer를 확인한다. C의 HM Theme/Tailwind 연결은 호스트가 제공하며 `/hm-dialog`가 CSS를 자동 import하지 않는다. 기본 Entry는 HM UI runtime/type 의존 그래프를 포함하지 않는다. 설치 여부로 popup을 자동 전환하지 않는다. + +### 공개 Export와 CSS + +Core package의 exports는 `.`, `./style.css`, `./engine.css`, `./browser`다. + +- 주 진입점: `createOrgChart`, `JsonOrgChartDataSource`, `OrgChartSourceError`, `validateOrgChartDataset`, `isSafeImageUrl`, `normalizeOrgChartDataset`, `calculateSummary`, `snapshotOrgChartDataset`. +- 색상: `companyColorPresets`, `getCompanyColorPreset`; 타입: `CompanyId`, `CompanyColorPreset`. +- 가상 예제: `createOrgChartExampleDataset` 및 관련 타입. +- 이동: `createOrgChartMoveValidator`, `validateOrgChartMove`, `applyOrgChartMove` 및 관련 타입. +- 독립 모듈: `createOrgChartTree`, `createPositionSummary`, `createAffiliationSummary`, `createStatusSummary`. 이들은 Full Composition의 Auto 기능 전체를 제공하는 별도 React 컴포넌트가 아니다. +- 데이터·옵션·Handle 타입은 같은 주 진입점에서 가져온다. 내부 `src`/`dist` 경로를 소비 코드에서 import하지 않는다. + +React 기본 `@hm-design/org-chart-react`의 runtime export는 **`OrgChart` 하나**이며 HMDesign UI가 필요 없다. 선택 Entry `@hm-design/org-chart-react/hm-dialog`는 `OrgChart`와 `HMEmployeeDialog`를 export한다. 두 Entry 모두 `OrgChartProps`, `OrgChartEmployeeSelection`, `OrgChartEmployeePopupProps`와 Core 공개 타입을 type-only로 export한다. `JsonOrgChartDataSource` 같은 runtime helper는 Core에서 import한다. 내부 `src`/`dist` Import는 사용하지 않는다. + +`stylesheetUrls: { engine, core }`는 필수다. Core가 자기 Shadow DOM에 engine CSS → core CSS 순으로 link를 넣는다. document에 CSS를 import하는 것만으로 내부 카드가 스타일링되지는 않는다. 아래 `?url`은 Vite 전용이며 다른 bundler에서는 공개 CSS를 실제 배포 URL로 해석해 전달한다. + +React 없는 일반 HTML도 공개 browser bundle을 HTTP 서버로 배포해 사용할 수 있다. `./browser`는 ESM named import용이 아니라 build가 생성하는 IIFE `HMOrgChart` 전역 bundle이다. 이 가이드의 Vanilla TypeScript 예제는 주 ESM 진입점을 사용한다. URL JSON은 HTTP(S)/상대 경로를 사용하며 `file://` fetch는 지원하지 않는다. + +근거: [Core manifest](../../packages/org-chart/package.json), [Core exports](../../packages/org-chart/src/index.ts), [Data exports](../../packages/org-chart/src/data/index.ts), [React manifest](../../packages/org-chart-react/package.json), [React exports](../../packages/org-chart-react/src/index.ts), [Core build](../../packages/org-chart/build.mjs). + +## 2. 빠른 시작용 데이터 — 예제 A / B / E + +다음은 **가상 데이터 예제**다. `data.ts`로 저장한다. 예제의 회사·분기 수를 제품 기본값으로 사용하지 않는다. A는 1회사·2본부·본부별 2팀·총 7명(대표 1명 + 본부장 2명 + 팀 직원 4명)이다. B는 회사별 독립 Root 두 개와 회사별 컬러를 보여준다. E는 동일한 `singleCompany`를 React와 Vanilla에서 사용한다. + +업무 명칭인 본부를 이 예제에서는 `office`로 대응한다. Contract에 `headquarters` enum을 새로 추가하지 않는다. + +```ts +// data.ts +import { + getCompanyColorPreset, validateOrgChartDataset, + type CompanyId, type Employee, type Organization, type OrgChartDataset, +} from '@hm-design/org-chart'; + +export function makeCompanyDataset( + companyId: CompanyId, teamCounts: readonly number[], +): OrgChartDataset { + if (teamCounts.some(count => !Number.isSafeInteger(count) || count < 0)) { + throw new Error('팀 수는 0 이상의 정수여야 합니다.'); + } + const name = getCompanyColorPreset(companyId).companyName; + const rootId = companyId + '-root'; + const organizations: Organization[] = []; + const employees: Employee[] = []; + const addEmployee = (id: string, organizationId: string, position: string) => { + employees.push({ + id, organizationId, affiliationId: companyId, name: '가상 ' + id, + position, jobTitle: null, profileImage: null, workStatus: 'active', + }); + }; + addEmployee(companyId + '-ceo', rootId, '대표이사'); + organizations.push({ + id: rootId, companyId, parentId: null, name, type: 'root', + leaderId: companyId + '-ceo', displayOrder: 0, + }); + teamCounts.forEach((count, index) => { + const officeId = companyId + '-office-' + (index + 1); + const leaderId = officeId + '-leader'; + addEmployee(leaderId, officeId, '본부장'); + organizations.push({ + id: officeId, parentId: rootId, name: (index + 1) + '본부', + type: 'office', leaderId, displayOrder: index, + }); + for (let team = 0; team < count; team++) { + const teamId = officeId + '-team-' + (team + 1); + organizations.push({ + id: teamId, parentId: officeId, name: (team + 1) + '팀', + type: 'team', leaderId: null, displayOrder: team, + }); + addEmployee(teamId + '-employee', teamId, '사원'); + } + }); + const data: OrgChartDataset = { + schemaVersion: '0.1-draft', organizations, employees, + affiliations: [{ id: companyId, name }], + metadata: { asOfDate: '2026-10-13', dataVersion: 'example-1' }, + }; + const checked = validateOrgChartDataset(data); + if (!checked.valid) throw new Error(JSON.stringify(checked.issues)); + return checked.data; +} + +export const singleCompany = makeCompanyDataset('hanmac', [2, 2]); +const secondCompany = makeCompanyDataset('saman', [1]); +export const multiCompany: OrgChartDataset = { + ...singleCompany, + organizations: [...singleCompany.organizations, ...secondCompany.organizations], + employees: [...singleCompany.employees, ...secondCompany.employees], + affiliations: [...singleCompany.affiliations, ...secondCompany.affiliations], + metadata: { ...singleCompany.metadata, dataVersion: 'example-multi-1' }, +}; +const checkedMulti = validateOrgChartDataset(multiCompany); +if (!checkedMulti.valid) throw new Error(JSON.stringify(checkedMulti.issues)); +``` + +Root의 `companyId`는 하위 조직이 상속한다.복수 회사는 `parentId: null`인 별도 Root를 만든다. 회사들을 잇는 가짜 상위 조직을 추가하지 않는다. B는 위 `multiCompany`를 같은 렌더러에 전달하면 된다. + +## 3. React 최소 사용 — 예제 D / E + +Vite React 프로젝트에서 아래 `App.tsx`, `main.tsx`, `index.html`과 앞의 `data.ts`를 사용한다. 기본 조회용 설정은 `dragDropEnabled={false}`다. Zoom/Pan/팝업은 별도 설정으로 계속 사용한다. + +```tsx +// App.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'; +import coreUrl from '@hm-design/org-chart/style.css?url'; +import { singleCompany } from './data'; + +const source = new JsonOrgChartDataSource(singleCompany); +const stylesheetUrls = { engine: engineUrl, core: coreUrl }; +const autoLayout = { type: 'tb' as const, initialVisibleDepth: 'all' as const }; + +export default function App() { + return { + if (state.status === 'error') console.error(state.issues); + }} + onLifecycleError={error => console.error(error)} + />; +} +``` + +```tsx +// main.tsx +import { createRoot } from 'react-dom/client'; +import App from './App'; + +const container = document.getElementById('root'); +if (!container) throw new Error('root 요소가 없습니다.'); +const root = createRoot(container); +root.render(); +window.addEventListener('pagehide', () => root.unmount(), { once: true }); +``` + +```html + + + + OrgChart + +
+ + + +``` + +React가 전용 div와 Core handle의 mount·subscribe·destroy를 소유한다. 소비자가 같은 div에 다른 자식이나 별도 Core 인스턴스를 넣지 않는다. StrictMode도 wrapper가 lifecycle을 관리한다. 공개 `ref`/imperative handle prop이나 `data`, `title`, `children` prop은 없다. + +기본 Entry의 `core` 모드는 Core CSS로 동작하며 HMDesign UI/Theme/Tailwind를 설치하지 않아도 된다. 실제 tarball을 설치한 React 19.3.0/Vite 8.3.0 환경의 타입·Production Build 검증은 [최소 설치 보고서](minimal-install-report-2026-10-13.md)를 참조한다. 다른 bundler·React major의 지원을 이 결과로 확대하지 않는다. + +### HMDesign Dialog 선택형 연동 + +기존 HM Dialog 소비자는 Import를 `/hm-dialog`로 변경하고 `employeePopup="hm-dialog"`를 유지한다. 기본 Entry에 HM UI가 설치되어 있어도 자동 전환하지 않으며, HM Entry만 Import해도 기본 popup은 Core다. 실제 `@hm-design/ui`와 호스트의 `index.css → hm-theme.css`/Tailwind 연결이 필요하다. + +```tsx +// HMChart.tsx +import { OrgChart } from '@hm-design/org-chart-react/hm-dialog'; +import { JsonOrgChartDataSource } from '@hm-design/org-chart'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import coreUrl from '@hm-design/org-chart/style.css?url'; +import { singleCompany } from './data'; + +const source = new JsonOrgChartDataSource(singleCompany); +export function HMChart() { + return ; +} +``` + +### 외부 Popup Adapter + +기본 Entry에 `employeePopup="adapter"`와 `employeePopupAdapter`를 함께 전달한다. Adapter는 `{ selection, onClose }`를 받고 표시·닫기·접근성·focus 복귀를 소유한다. selection에는 직원 원본과 context가 있다. 아래 native dialog는 호스트가 소유하는 예시이며 조직도 카드·Core·데이터를 수정하지 않는다. + +```tsx +// CustomChart.tsx +import { useEffect, useRef } from 'react'; +import { OrgChart, type OrgChartEmployeePopupProps } from '@hm-design/org-chart-react'; +import { JsonOrgChartDataSource } from '@hm-design/org-chart'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import coreUrl from '@hm-design/org-chart/style.css?url'; +import { singleCompany } from './data'; + +function EmployeePopup({ selection, onClose }: OrgChartEmployeePopupProps) { + const dialog = useRef(null); + useEffect(() => { + if (!selection) return; + const element = dialog.current!; + element.showModal(); + return () => { + element.close(); + if (selection.context.trigger.isConnected) selection.context.trigger.focus(); + }; + }, [selection]); + return selection ? { event.preventDefault(); onClose(); }}> +

{selection.employee.name} · {selection.employee.position}

+ +
: null; +} +const source = new JsonOrgChartDataSource(singleCompany); +export function CustomChart() { + return ; +} +``` + +Adapter 전달만으로는 활성화되지 않는다. 기본 Entry에서 `hm-dialog`/`adapter`를 선택하고 Adapter를 제공하지 않으면 Core popup을 유지하고 `onLifecycleError`로 구성 오류를 알린다. `onEmployeeClick`이 지정되면 이 선택보다 우선한다. Fixed는 기존 Core popup을 유지하고 외부 popup은 `onEmployeeClick`으로 연결한다. + +근거: [Props](../../packages/org-chart-react/src/types.ts), [Wrapper lifecycle](../../packages/org-chart-react/src/OrgChart.tsx), [HM Dialog](../../packages/org-chart-react/src/HMEmployeeDialog.tsx). + +## 4. Vanilla TypeScript 최소 사용 — 예제 D / E + +React가 없는 Vite TypeScript 프로젝트에서 `data.ts`, 아래 `main.ts`과 `index.html`을 사용한다. 반환 cleanup은 페이지/호스트 lifecycle 종료 시 호출한다. + +```ts +// main.ts +import { createOrgChart, JsonOrgChartDataSource } from '@hm-design/org-chart'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import coreUrl from '@hm-design/org-chart/style.css?url'; +import { singleCompany } from './data'; + +export async function mountChart(container: HTMLElement): Promise<() => void> { + const chart = createOrgChart({ + container, dataSource: new JsonOrgChartDataSource(singleCompany), + stylesheetUrls: { engine: engineUrl, core: coreUrl }, + layoutMode: 'auto', displayMode: 'readOnly', dragDropEnabled: false, + autoLayout: { type: 'tb', initialVisibleDepth: 'all' }, + onStateChange: state => { + if (state.status === 'error') console.error(state.issues); + }, + }); + try { + await chart.mount(); + const state = chart.getState(); + if (state.status !== 'ready') throw new Error('조직도를 표시할 수 없습니다.'); + // Auto에서 필요할 때 호출: 현재 전체 표시 영역을 맞춘다. + chart.fitToView(); + return () => chart.destroy(); + } catch (error) { + chart.destroy(); + throw error; + } +} + +const container = document.getElementById('chart'); +if (!container) throw new Error('chart 요소가 없습니다.'); +const dispose = await mountChart(container); +window.addEventListener('pagehide', dispose, { once: true }); +``` + +```html + + + + OrgChart + +
+ + + +``` + +`createOrgChart()` 자체는 DOM/요청 부작용 없이 handle을 만든다. `mount(container?)`는 연결된 HTMLElement 하나를 소유하고 첫 load까지 수행한다. 다른 handle이 소유한 컨테이너, 중복 mount, destroy 후 재사용은 거절한다. `destroy()`는 여러 번 호출해도 안전한 최종 종료이며 다시 사용할 때는 새 handle을 만든다. + +Core의 subscribe(listener)는 unsubscribe 함수를 반환하며 이후 상태 변경을 구독한다. getState()는 현재 상태를 읽는다. Auto의 getData()는 숨긴 회사와 로컬 이동을 포함한 현재 immutable 데이터, getSelectedCompanies()/getAutoLayout()/getInteractions()/getDragDropEnabled()는 현재 옵션을 조회한다. getData()는 Auto 데이터가 준비되지 않으면 undefined다. + +mount/update/reload의 Promise 완료만으로 성공을 판정하지 않는다. Contract·소스·CSS 오류는 reject 대신 `getState().status === 'error'`/`onStateChange`로 전달될 수 있다. + +## 5. JSON 데이터 계약 + +현재 구현 타입 이름은 **`OrgChartDataset`**다. 과거 [데이터 계약 문서](data-contract.md)의 초안 이름·선택 필드는 현재 타입보다 우선하지 않는다. JSON 문자열을 직접 `JsonOrgChartDataSource`에 전달하면 URL로 해석된다. 문자열 JSON은 먼저 `JSON.parse(text)`하고 Validator를 통과시킨다. + +### 필수·선택 필드 + +- Dataset 필수: `schemaVersion: '0.1-draft'`, `organizations: Organization[]`, `employees: Employee[]`, `affiliations: Affiliation[]`, `metadata: Metadata`. +- Dataset 선택: `relations?: OrganizationRelation[]`, `presentation?: PresentationConfig`. +- Organization 필수: `id: string`, `parentId: string | null`, `name: string`, `type: 'root' | 'division' | 'office' | 'div' | 'cell' | 'team'`, `leaderId: string | null`, `displayOrder: number`(0 이상 안전한 정수). 선택: `companyId?: string`. +- Employee는 모든 필드 필수: `id`, `organizationId`, `affiliationId`, `name`, `position`은 string; `jobTitle`, `profileImage`는 string 또는 null; `workStatus`는 `active | dispatched | onLeave`. +- Affiliation 필수: `id: string`, `name: string`. 별도 `Company[]`는 없다. Auto 회사 식별은 Organization.companyId와 등록된 회사 프리셋을 사용하며 직원의 관계사/법인 소속은 Employee.affiliationId로 구분한다. +- Metadata 필수: 실제 달력 날짜인 `asOfDate: 'YYYY-MM-DD'`, 비어 있지 않은 `dataVersion: string`. 선택: `schemaVersion?: '0.1-draft'`(최상위와 같아야 함), `updatedAt?: string | null`(timezone 포함 RFC3339), `updateNote?: string | null`. +- Relation은 `id`, `sourceOrganizationId`, `targetOrganizationId`, `type: 'cooperation' | 'support'`가 필수다. 추가 관계는 검증/보존되지만 기본 계층 연결선으로 렌더링되지 않는다. +- Presentation을 넣으면 `title: string`, `description: string | null`, `hubText: string | null`, `organizationOrder: string[]`, `groups`의 `showEmployees/showSummary/showDispatched/showOnLeave/showAffiliations: boolean` 모두를 넣는다. Presentation 생략은 현재 heading 제거 옵션이 아니다. + +nullable 필드는 생략하거나 빈 문자열로 대신하지 않는다. 필수 string은 공백뿐인 값이 거절된다. Validator는 trim으로 원본을 고치지 않는다. Contract에 없는 키도 `UNKNOWN_FIELD`다. 원본에 추가 정보가 있다면 별도 자료에 보존하고 Contract용 변환본을 만든다. + +### 최소 JSON: 회사 → 본부 → 팀 → 직원 + +```json +{ + "schemaVersion": "0.1-draft", + "organizations": [ + { "id": "company-root", "companyId": "saman", "parentId": null, "name": "삼안", "type": "root", "leaderId": null, "displayOrder": 0 }, + { "id": "hq-1", "parentId": "company-root", "name": "1본부", "type": "office", "leaderId": null, "displayOrder": 0 }, + { "id": "team-1", "parentId": "hq-1", "name": "1팀", "type": "team", "leaderId": "employee-1", "displayOrder": 0 } + ], + "employees": [ + { "id": "employee-1", "organizationId": "team-1", "affiliationId": "saman", "name": "가상직원", "position": "차장", "jobTitle": "팀장", "profileImage": null, "workStatus": "active" } + ], + "affiliations": [{ "id": "saman", "name": "삼안" }], + "metadata": { "asOfDate": "2026-10-13", "dataVersion": "minimal-1" } +} +``` + +`leaderId`는 해당 조직에 소속된 Employee.id를 참조한다. 위 팀장도 employees에 한 번만 있다. 조직장 별도 레코드를 추가해 총원을 두 번 세지 않는다. 직원은 한 `organizationId`로 직접 소속되며 이름으로 관계를 연결하지 않는다. ID 중복 검사는 collection 내부 기준이고 서로 다른 collection의 같은 문자열은 현재 Validator가 거절하지 않지만, 호스트 변환 단계에서는 엔티티 prefix로 구분하기를 권장한다. + +`profileImage`는 HTTP(S) 또는 안전한 상대 URL이다. null 또는 이미지 load 실패 시 기존 사람 SVG Placeholder로 전환한다. credentials·data URL·javascript·protocol-relative URL을 이미지 API로 우회하지 않는다. 직위(position)와 직책(jobTitle)은 별개이며 연락처 같은 미지원 정보를 합성하지 않는다. + +### 깊은 JSON: 회사 → 부문 → 본부 → 부서 → 팀 → 직원 + +현재 Auto의 트리는 enum 순서에 종속되지 않고 실제 parentId 관계를 따른다. 다음은 5개 조직 계층 + 직원 카드 예제다. 본부=`office`, 부서=`div` 대응은 이 예제의 변환 선택이며 일반 기업 의미를 강제하지 않는다. + +```json +{ + "schemaVersion": "0.1-draft", + "organizations": [ + { "id": "deep-root", "companyId": "hanmac", "parentId": null, "name": "한맥기술", "type": "root", "leaderId": null, "displayOrder": 0 }, + { "id": "division-1", "parentId": "deep-root", "name": "기술부문", "type": "division", "leaderId": null, "displayOrder": 0 }, + { "id": "hq-1", "parentId": "division-1", "name": "기술본부", "type": "office", "leaderId": null, "displayOrder": 0 }, + { "id": "department-1", "parentId": "hq-1", "name": "설계부서", "type": "div", "leaderId": null, "displayOrder": 0 }, + { "id": "team-1", "parentId": "department-1", "name": "설계팀", "type": "team", "leaderId": null, "displayOrder": 0 } + ], + "employees": [ + { "id": "deep-employee-1", "organizationId": "team-1", "affiliationId": "hanmac", "name": "가상직원", "position": "사원", "jobTitle": null, "profileImage": null, "workStatus": "active" } + ], + "affiliations": [{ "id": "hanmac", "name": "한맥기술" }], + "metadata": { "asOfDate": "2026-10-13", "dataVersion": "deep-1" } +} +``` + +임의의 최대 Depth를 보장하는 API는 없다. 소스는 가변 계층을 처리하지만 극단적으로 깊거나 큰 입력의 렌더링·성능은 실제 브라우저에서 별도로 확인한다. + +근거: [실제 타입](../../packages/org-chart/src/data/types.ts), [Validator](../../packages/org-chart/src/data/validate.ts), [DataSource](../../packages/org-chart/src/data/source.ts), [정규화](../../packages/org-chart/src/data/normalize.ts), [집계](../../packages/org-chart/src/data/summary.ts). + +## 6. Fixed / Auto 선택과 레이아웃 + +### Fixed — 기존 Workdesk 디자인 구성 + +`layoutMode: 'fixed'`가 생략 시 기본이다. `displayMode: 'interactive' | 'readOnly'`와 별도 설정이다. + +- Fixed readOnly는 기존 승인 SVG 카드·대표자·허브·요약과 Workdesk 구성/비례 축소를 사용한다. 기준 캔버스는 1920×914.5, 축 X=935다. +- 측정된 배치는 `business-management/business-planning/talent-growth/integrated-system/design-planning/solution-sales` ID의 기존 열 구성에 맞는다. 상위 열이 매핑되지 않으면 일반 중앙 flow로 돌아가며 임의 데이터에 승인된 좌표 배치를 보장하지 않는다. 데이터의 ID/parentId를 디자인 매핑에 맞추려고 바꾸지 않는다. +- readOnly는 전체 펼침이며 Fixed interactive는 기존 접기 동작을 제공한다. Auto용 TB/LR/Hybrid·회사 선택·Drag & Drop·Zoom/Pan Handle은 Fixed의 새로운 기능이 아니다. +- **표시 데이터 제약:** `employee-card.ts`는 Workdesk 표현 카드의 이름을 `홍 길 동`으로 표시하고 Fixed 대표자 카드는 `대표이사` 직위를 표시하는 경로가 있다. 원본 Employee는 유지되지만 화면 표시가 원본 인명과 일치하지 않을 수 있다. 실제 사용자 데이터 표시에는 이를 보고하고 Auto를 검토한다. 이를 이번 문서 작업에서 수정하지 않는다. +- readOnly가 제목·기준일·요약까지 제거하는 Chart Only prop은 아니다. + +### Auto / Expanded — 데이터 기반 가변 조직도 + +`layoutMode: 'auto'`는 회사별 독립 Root와 전체 관계를 사용한다. Auto의 회사별 컬러·접기·팝업·Zoom/Pan·이동은 Core 구현을 공유한다. + +- `autoLayout.type`: `tb`(기본, 위→아래), `lr`(왼쪽→오른쪽), `hybrid`(지정 Depth부터 세로 목록). 현재 기준 소스는 세 유형을 구현한다. +- `hybridStartDepth?: 2 | 3 | 4`: 기본 3, global Hybrid용. Root 조직=Depth 1이며 직원 카드를 조직 Depth로 세지 않는다. +- `initialVisibleDepth?: 1 | 2 | 3 | 'all'`: 기본 all. 해당 Depth부터 그룹을 접는 초기 정책이고 조직장은 남는다. 값을 변경하면 접힘 상태를 초기 정책으로 재설정한다. 다른 레이아웃 변경은 유효한 ID의 접힘 상태를 유지한다. +- `hybridOrganizationIds?: readonly string[]`: TB에서 지정 조직의 자손만 세로 배치. LR/global Hybrid와 동시 사용은 거절한다. 전환 전에 빈 배열로 해제한다. +- `interactions: { collapse?, zoom?, pan? }`: 각각 기본 true, 서로 독립이다. Compact 레이아웃, 임의 `layoutConfig`, 엔진 방향 옵션 전달, 공개 collapse setter는 없다. +- 초기 자동 배치는 배율 1을 기본으로 실제 Bounds의 가로 중앙을 맞추며 상단 여유를 둔다. 항상 가로·세로 중앙 Fit하는 동작과 다르다. 사용자가 Pan/Zoom한 뒤에는 viewport를 보존한다. +- 명시적 Fit은 표시된 Bounds를 맞추고 가로·세로 중앙에 둔다. 큰 입력은 읽기 어려울 만큼 축소될 수 있어 initial Depth 또는 확대를 함께 검증한다. Fit은 수동 zoom.min보다 작아질 수 있다. +- Auto unknown 회사 프리셋은 거절한다. 회사 선택 `undefined`=전체, `[]`=명시적 빈 표시. 회사 숨김/복귀는 원본 데이터나 완료된 이동을 삭제하지 않는다. +- `companyTopNodes`는 회사 ID별 `employeeId/title/name/profileImage/organizationName/display` 표시 옵션이다. 기본은 Root.leaderId이며 직원은 해당 회사 Root 소속이어야 한다. `employeeId: null`은 상단 카드 배정을 제거한다. `display`는 `card | titleOnly | hidden`. 표시 override는 원본 직원/팝업 identity를 바꾸지 않는다. + +Vanilla Auto에서 설정 변경은 기존 handle로 한다. + +```ts +import type { OrgChartHandle } from '@hm-design/org-chart'; + +export async function configureLayout(chart: OrgChartHandle) { + await chart.setAutoLayout({ hybridOrganizationIds: [] }); + await chart.setAutoLayout({ type: 'lr', initialVisibleDepth: 2 }); + await chart.setInteractions({ collapse: true, zoom: true, pan: true }); + await chart.setSelectedCompanies(['hanmac', 'saman']); + // chart.setSelectedCompanies()는 전체, []는 빈 선택. +} +``` + +setter는 mount 후 Auto에서 호출한다. Vanilla의 생성 옵션인 layoutMode/displayMode/companyTopNodes/employeeConnections/zoom/organizationLinks를 바꾸려면 destroy 후 새 handle을 만든다. + +React에서는 대응 prop으로 변경한다. dataSource 변경은 reload, autoLayout/interactions/dragDropEnabled/selectedCompanyIds와 Auto 팝업 변경은 기존 handle을 갱신한다. aggregationPolicy·CSS URL·layoutMode·displayMode·companyId·companyTopNodes·employeeConnections·zoom·organizationLinks 변경은 handle을 재생성하므로 객체를 안정적으로 유지한다. `setAutoLayout`는 patch merge이며 React에서 새 autoLayout 객체에 넣지 않은 필드가 매번 Core 기본으로 초기화된다고 가정하지 않는다. + +근거: [Core Handle/옵션](../../packages/org-chart/src/core/types.ts), [배치 정책](../../packages/org-chart/src/core/auto-layout-policy.ts), [회사 검증](../../packages/org-chart/src/core/expanded-company.ts), [Workdesk 좌표](../../packages/org-chart/src/core/workdesk-layout.ts), [Fixed 구성](../../packages/org-chart/src/core/org-chart-content.ts), [Viewport](../../packages/org-chart/src/core/expanded-viewport.ts). + +## 7. 디자인·회사별 컬러·카드 재사용 + +현재 Auto의 `getCompanyColorPreset(id).primary` 값: + +- `hanmac` 한맥기술: `#CB2E28` +- `saman` 삼안: `#F08300` +- `jangheon-industry` 장헌산업: `#4580A6` +- `jangheon` 장헌: `#1D418D` +- `ptc` 피티씨: `#84338E` +- `halla` 한라산업개발: `#0068B7` +- `baron` 바론컨설턴트: `#0C367F` + +색상은 조회 helper와 Core에 맡기고 새 카드 CSS에 복사하지 않는다. Root.companyId → 자손 상속 → legacy Root leader의 등록 affiliation → 생성 옵션 companyId(기본 hanmac)의 순으로 회사가 결정된다. 새 데이터는 Root.companyId를 명시한다. 조직 회사와 직원 affiliation은 다른 값일 수 있다. 카드 강조는 등록된 affiliation 프리셋을 우선하고 미등록 affiliation은 조직 회사 색을 사용한다. 자손의 companyId가 부모 회사와 다르면 Auto에서 `COMPANY_HIERARCHY`다. + +Fixed는 별도의 기존 부서 ID별 스타일·SVG를 사용한다. 회사 컬러 옵션을 바꿔 Fixed 전체를 재색칠하는 API는 없다. + +### 카드와 글자 + +- Fixed의 기존 대표자 SVG는 146×74, 1Depth 조직장 SVG는 146×68, 더 깊은 조직장/일반 직원의 small SVG는 108×52다. 등록 부서 자산이 없는 조직은 기존 fallback 카드다. 모든 Fixed 데이터에 SVG 매핑이 있는 것은 아니다. +- Fixed SVG 직위/이름 기본 크기는 small 15/18px, 대표자 20/24px, 부서장 SVG 직위 18px·이름 24px다. 이름 weight 600, 직위 400이며 전역 서체는 Noto Sans KR 등 기존 fallback을 따른다. 재설계 규격으로 사용하지 않는다. +- Auto는 SVG 배경 대신 기존 중립 카드와 회사 강조 Border를 쓴다. 일반 직원 폭 114px·최소 높이 54px, 조직장/상단 카드 폭 154px·최소 높이 72px, 사진 36×36, padding 8px, Border 1px, inset 강조 3px, 이름/직위 13/10px다. 긴 텍스트는 실측 높이가 늘 수 있다. +- Auto의 Root 대표자와 하위 조직장 카드는 같은 leader geometry를 사용한다. 1Depth/2Depth별 새 디자인 prop은 없다. 3글자 한글 이름은 표시 계층에서 띄어 쓰고 직위 공백을 표시상 제거하지만 원본과 callback 데이터는 유지한다. +- Auto 일반 카드에서 jobTitle·근무 상태를 별도 행으로 출력하지 않는 경로가 있다. 팝업은 원본 jobTitle/workStatus를 보여준다. Fixed의 파견/휴직 보조 카드·요약과 Auto의 직원 카드를 혼동하지 않는다. +- Fixed SVG의 외형 Border는 자산에 포함되고 Auto Border는 CSS/실제 연결면에 따라 강조한다. 회사 타이틀 텍스트는 현재 CSS에서 흰색이며 특정 색 조합의 대비가 승인됐다는 뜻은 아니다. + +### 연결선·스타일 적용 + +Auto 전체 트리 방향은 autoLayout.type, 직원 연결 방향은 조직 ID별 `employeeConnections: { [id]: 'top' | 'left' | 'right' }`다. LR/세로 경로는 실제 endpoint를 측정해 연결면을 정하고 LR 직원 카드의 연결/강조면은 왼쪽이다. TB/Hybrid 조직장은 상단 연결을 사용한다. 직원 방향 옵션을 전체 트리 방향 API로 해석하지 않는다. + +Auto 관계선/직원선은 기존 `--hm-orgchart-expanded-line-color`(기본 `#738399`)를 공유한다. Fixed 선·회사 Border와 별개다. 기본 선과 기존 SVG/회사 프리셋을 변경하지 않는다. + +Core는 Shadow DOM을 사용하므로 호스트의 일반 selector로 내부 카드가 바뀌지 않는다. 기존 공개 CSS 두 개와 Core를 재사용한다. 별도 승인된 스타일 변경이 있을 때만 [Expanded 스타일 안내](expanded-usage.md)의 실제 변수·스타일 로딩 경로를 확인한다. 변수 이름을 추측하거나 카드 교체 API를 만들지 않는다. + +근거: [회사 프리셋](../../packages/org-chart/src/company-color-presets.ts), [Fixed 부서 자산](../../packages/org-chart/src/core/department-card-assets.ts), [대표자 자산](../../packages/org-chart/src/core/representative-card-asset.ts), [직원 카드](../../packages/org-chart/src/core/employee-card.ts), [공통 카드 CSS](../../packages/org-chart/src/styles/employee-card.css), [Auto 카드 CSS](../../packages/org-chart/src/styles/org-chart-expanded.css), [Auto 연결선](../../packages/org-chart/src/core/expanded-layout.ts). + +## 8. 인터랙션·팝업·Drag & Drop + +### 탐색 + +- Auto toolbar의 확대/축소는 현재 배율×step 또는 ÷step, 기본 step=1.2, min=0.05, max=3이다. 공개 handle에는 zoomIn/zoomOut이 없으며 `zoomTo(scale)`를 사용한다. +- Ctrl/Command+휠 확대, viewport 초점에서 +/−, 빈 공간 드래그/방향키 Pan을 지원한다. `panBy(x,y)`, `getViewportState(): {scale,x,y} | undefined`를 제공한다. +- toolbar의 100% 복원은 배율 1과 위치를 함께 복원한다. `zoomTo(1)`는 배율 변경이므로 동일한 동작이 아니다. 공개 resetViewport 메서드는 없다. +- `fitToView()`는 Auto에서 전체 표시 범위를 맞춘다. Fixed의 위 handle viewport 호출은 Auto 동작을 제공하지 않는다. +- 그룹 타이틀은 click/Enter/Space로 접기·펼치고 조직장은 남긴다. 부모 재확장 시 유효한 자손 접힘 상태를 유지한다. `interactions.collapse=false`는 토글만 끈다. 하위 내용 없는 제목은 aria-disabled로 접기 불가이나 이동/키보드 대상일 수 있다. +- Zoom/Pan을 끄면 해당 조작/API를 막지만 Fit은 독립적으로 가능하다. manual Pan이 OFF여도 키보드로 화면 밖 카드에 도달하면 배율을 유지하고 초점을 화면에 드러낸다. + +### 팝업 + +Core/Vanilla 기본 직원 카드 click/Enter/Space는 native dialog를 연다. X·ESC로 닫고 Tab 초점을 가두며 원래 카드로 복귀한다. 카드가 사라졌거나 인스턴스가 정리되면 shell/연결 상태에 따라 복귀한다. **Core dialog에는 외부 클릭으로 닫는 별도 핸들러가 없다.** + +React Auto의 `employeePopup`은 `core`(기본), `hm-dialog`, `adapter`, `none`이다. HM 모드는 `/hm-dialog` Entry 또는 명시적으로 주입한 `HMEmployeeDialog`를 사용한다. HM Dialog·Avatar·Button으로 X·닫기 버튼·ESC·외부 클릭으로 닫으며 onCloseAutoFocus에서 원래/재생성된 카드 또는 shell에 복귀한다. `adapter`는 호스트의 `employeePopupAdapter`를 명시적으로 선택한다. none은 기본 팝업/button 의미를 제거하지만 키보드 탐색용 초점은 남긴다. Fixed에서는 employeePopup 선택이 적용되지 않고 기존 Core 팝업을 유지한다. + +`onEmployeeClick(employee, context)`를 지정하면 기본 팝업보다 우선한다. context는 `organization`, `affiliation`, `trigger: HTMLElement`, `originalEvent: MouseEvent | KeyboardEvent`다. 임의 popup API를 추가하지 않고 호스트가 자신이 만든 UI의 닫기·접근성·focus 복귀를 책임진다. + +### 이동 정책 + +- `dragDropEnabled`는 Core/React 공통으로 **기본 false**다. displayMode readOnly도 이 옵션을 대체하는 편집 잠금이 아니다. +- OFF는 포인터 이동·Alt+M·handle.move를 차단하고 `EDITING_DISABLED`를 반환한다. OFF에서도 독립 설정에 따른 Pan/Zoom/Fit·팝업·접기는 유지한다. +- ON은 Auto 직원 카드/조직 타이틀 드래그 이동, Alt+M → 방향키 대상 선택 → Enter 확정 / ESC 취소를 지원한다. 빈 공간은 Pan이다. +- Root, 자신/자손으로 조직 이동, 같은 소속, leaderId 참조를 손상시키는 조직장 이동 및 지정된 상단 직원 이동을 차단한다. 대상 회사 affiliation이 없으면 회사 간 이동도 거절한다. +- 동일 회사 직원 이동은 organizationId만 바꾸고 affiliationId를 보존한다. 회사 간 직원 이동은 affiliationId도 대상 회사로 변경한다. +- 그룹 이동은 parentId를 바꾸며 회사 간 이동은 하위 조직 전체 companyId와 소속 직원 affiliationId도 대상 회사로 바꾼다. ID·이름·직위·사진·자손 관계·조직장·정렬은 보존한다. +- 결과는 **현재 Core 인스턴스의 새 immutable snapshot**이다. 원본 JSON·DataSource·서버·파일은 수정하지 않고 영구 저장 API도 없다. 새 source/reload는 소스 데이터를 다시 적용한다. UI 회사 선택만으로 이동 snapshot을 초기화하지 않는다. +- OFF 전환은 진행 중 드래그/키보드 이동을 취소하고 이미 완료한 이동은 남긴다. 설정만 바꿀 때 reload하지 않는다. + +```ts +import type { OrgChartHandle, OrgChartMoveResult } from '@hm-design/org-chart'; + +export async function moveEmployee(chart: OrgChartHandle): Promise { + chart.setDragDropEnabled(true); + const result = await chart.move({ + kind: 'employee', + sourceId: 'hanmac-office-1-team-1-employee', + targetOrganizationId: 'hanmac-office-2-team-1', + }); + chart.setDragDropEnabled(false); + if (result.moved) console.log(result.data); // 새 snapshot, 저장은 호스트 별도 책임 + else console.error(result.code, result.message); + return result; +} +``` + +위 예제는 A를 mount한 handle 기준이다. `onMove(result)`도 성공/실패를 알리며 `result.moved`로 분기한다. 성공은 data/request/previousOrganizationId, 실패는 code/message다. React 성공 이동은 열린 HM Dialog를 닫는다. 데이터용 순수 `applyOrgChartMove`는 UI OFF 잠금과 별개인 변환 함수이며 handle.move 대신 호출해 편집 잠금을 우회하지 않는다. + +근거: [Viewport 이벤트](../../packages/org-chart/src/core/expanded-viewport.ts), [편집 이벤트](../../packages/org-chart/src/core/expanded-editing.ts), [이동 정책](../../packages/org-chart/src/data/moves.ts), [Core 팝업](../../packages/org-chart/src/core/employee-info-dialog.ts), [HM 팝업](../../packages/org-chart-react/src/HMEmployeeDialog.tsx), [HM Dialog primitive](../../packages/ui/src/components/ui/dialog.tsx). + +## 9. 데이터 업데이트 — 예제 C + +Vanilla `update(input)`는 전달 snapshot을 검증해 현재 인스턴스에 적용하고 DataSource 자체를 교체하지 않는다. `reload(nextSource?)`는 현재/새 source를 load한다. 이후 reload가 이전 source 값을 다시 적용할 수 있다는 차이를 구분한다. 인라인 JsonOrgChartDataSource는 생성 시 값을 캡처하므로 원본 객체를 나중에 바꾸는 방식은 업데이트가 아니다. + +```ts +import { + JsonOrgChartDataSource, validateOrgChartDataset, type OrgChartHandle, +} from '@hm-design/org-chart'; + +export async function updateChart(chart: OrgChartHandle, input: unknown) { + const checked = validateOrgChartDataset(input); + if (!checked.valid) throw new Error(JSON.stringify(checked.issues)); + await chart.update(checked.data); + if (chart.getState().status !== 'ready') throw new Error('업데이트 실패'); +} + +export async function replaceSource(chart: OrgChartHandle, input: unknown) { + const checked = validateOrgChartDataset(input); + if (!checked.valid) throw new Error(JSON.stringify(checked.issues)); + await chart.reload(new JsonOrgChartDataSource(checked.data)); + if (chart.getState().status !== 'ready') throw new Error('소스 변경 실패'); +} +``` + +React는 dataSource identity가 바뀌면 같은 handle에 reload한다. `useMemo`/module 상수로 source를 안정적으로 만들고 매 render에서 새 source를 만들지 않는다. 다음은 호스트가 받은 새 데이터 prop으로 갱신하는 별도 React 예제다. + +```tsx +// UpdatingChart.tsx +import { useMemo } from 'react'; +import { OrgChart } from '@hm-design/org-chart-react'; +import { JsonOrgChartDataSource, type OrgChartDataset } from '@hm-design/org-chart'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import coreUrl from '@hm-design/org-chart/style.css?url'; + +const stylesheetUrls = { engine: engineUrl, core: coreUrl }; +export function UpdatingChart({ dataset }: { dataset: OrgChartDataset }) { + const source = useMemo(() => new JsonOrgChartDataSource(dataset), [dataset]); + return { + if (state.status === 'error') console.error(state.issues); + }} + onLifecycleError={error => console.error(error)} />; +} +``` + +새 dataset 객체를 전달한다. 성공한 onMove의 data를 집계용 read model로 저장하는 것과 바로 새 source로 넣어 reload하는 것을 구분한다. Core는 최신 요청 revision을 사용해 오래된 load 결과를 무시/취소한다. 재생성 옵션 변경은 viewport/이동의 보존을 보장하지 않는다. + +## 10. AI 코딩 에이전트 적용 절차 + +1. 현재 사용자 요청·Git 기준·공개 API를 확인하고 회사/조직 유형·부모 관계·분기 수·직원 수를 정리한다. Root를 포함한 조직 Depth와 직원 수 집계 기준을 구분한다. +2. 자연어의 가상 데이터를 생성하거나 제공 JSON·CSV·Excel을 Contract로 변환한다. Core가 CSV/XLSX parser나 자연어 생성기를 제공하는 것은 아니다. 외부 변환 결과는 unknown으로 취급한다. +3. 원본 이름·ID·소속·직위·사진·부모 관계를 보존한다. 조직장과 대표자를 총원에 포함할지 확인하고 employees에 한 번만 생성한다. 미지원 필드와 원본을 별도 보존하며 오류 항목을 삭제해 Validator를 통과시키지 않는다. +4. validateOrgChartDataset와 요청별 회사·조직·Depth·인원 검사를 수행한다. Auto 회사 프리셋/회사 경계/상단 직원 제약도 확인한다. Validator 성공만으로 Auto 표시가 보장되지 않는다. +5. 기존 공개 Core/React·카드·컬러·연결선을 재사용해 실제 client 진입점을 만든다. 새로운 엔진·입력 UI·개발 패널을 만들지 않는다. +6. 구현 완료 후 한 번의 통합 검증으로 핵심 동작·데이터 보존·연결선·가독성·viewport·키보드·필요한 타입/build/Guard를 확인한다. 390/768/1280/1440px 및 요청 크기에서 확인한 실행 URL/캡처와 미검증을 보고한다. + +예: 삼안 6본부, 팀 5·2·4·1·3·2=17, 총 500명은 실제 생성 데이터의 organizations와 unique employee 수로 검증한다. 한맥기술 3부문×2본부×4팀과 300명도 같은 절차로 처리한다. 이 가이드의 7명 예제 분배를 재사용하지 않는다. + +공개 `createOrgChartExampleDataset`는 기존 template에서 `employeeCount: 20 | 100 | 300`, scope `total | per-company`로 가상 예시를 만드는 helper다. 임의 500명 생성·실제 Excel 변환 API가 아니다. 회사별 모드는 선택 회사마다 N명이며 total 모드는 원래 회사별 분배 후 선택 회사만 필터링한다. 실제 사용자 데이터를 이 helper로 재생성하지 않는다. + +### Chart Only 정책과 현재 기능의 관계 + +[AI 페이지 생성 정책](ai-page-generation-rules.md)을 먼저 읽고 AGENTS.md·CLAUDE.md의 연결을 따른다. **별도 요청이 없다면 AI 생성 화면에는 조직도 캔버스만 표시하고 추가 제목·안내·테스트·JSON 패널을 만들지 않는다.** 실제 적용 기준은 최종 통합된 정책이다. + +P10.2에서 ChartOnly Worktree의 정책 보완을 통합했다. 기본 목표는 **White Canvas + OrgChart Nodes + Connectors**이며 외부 제목·기준일·요약·Zoom/Fit 버튼 바·설명·회색 배경은 요청 없이 추가하지 않는다. 정책은 생성 지침이며 새 Core prop/기능이 아니다. 현재 Auto는 heading/기준일·회사/인원 요약·Zoom/Fit toolbar·배율/조작 안내·Drag & Drop feedback를 내부 생성한다. Fixed readOnly도 heading·기준일과 기존 구성 요소를 유지한다. + +현재 공개 옵션에는 `chartOnly`, `showToolbar`, `showHeader` 또는 모든 내장 부가 UI를 끄는 기능이 없다. displayMode readOnly나 presentation 생략으로 해결된다고 주장하지 않는다. `createOrgChartTree`는 기존 Fixed 계열 독립 Tree이며 Auto 기능 전체의 대체 API가 아니다. 요구한 최소 화면을 공개 API로 구현할 수 없으면 제약과 별도 승인 필요성을 보고하고 Core 복제·Shadow DOM 임의 조작·강제 숨김·병렬 정책 수정으로 해결하지 않는다. + +P10.2의 독립 실제 tarball 브라우저 검사에서는 한맥기술 2본부×2팀·전체 20명 데이터를 React/Vanilla로 렌더링했다. 390/768/1280/1440px에서 흰색 내부 캔버스·7개 조직·20개 고유 직원 카드·TB 계층 연결선·Zoom/Pan과 콘솔 오류 0을 확인했다. 그러나 위 내장 UI와 `rgb(244, 247, 251)` 바깥 영역이 남아 **Chart Only PARTIAL**이다. Fixed readOnly도 제목·기준일을 유지한다. DOM 숨김·삭제·Core 수정은 사용하지 않았다. + +Auto 초기 배치는 전체 Bounds의 **가로 중앙·상단 16px 정렬**이며 초기 자동 Fit과 다르다. Vanilla의 공개 `fitToView()`는 실제 전체 Bounds로 가로·세로 중앙 배치/축소를 수행한다. React Wrapper에는 공개 handle/ref나 초기 Fit prop이 없으므로 버튼 바 없이 호스트가 이 동작을 직접 요청하는 경로는 없다. 후속 개선 후보는 기본 동작을 유지하는 선택형 부가 UI 표시 옵션과 초기 Fit 정책이다. 이들은 현재 API가 아니며 이번 작업에서 추가하지 않았다. 자세한 근거와 미검증은 [P10.2 통합 보고서](integration-report-p10-2.md)를 참조한다. + +React 없이 생성해 달라는 명시적 요청은 이 가이드의 Vanilla Core 경로로 처리하고 [Runtime portability](../guidelines/runtime-portability.md)의 명시적 adapter 계약을 확인한다. HM React 화면을 HTML로 모방해 결과라고 제출하지 않는다. + +## 11. 오류·검증·지원 한계 + +### 검사와 오류 처리 + +- validateOrgChartDataset는 valid=true이면 깊게 freeze한 snapshot을 반환한다. false이면 `issues: {code,path,message}[]`를 반환한다. +- 기본 오류: SCHEMA_VERSION, UNKNOWN_FIELD, REQUIRED_VALUE, DUPLICATE_ID, EMPTY_ORGANIZATIONS, INVALID_TYPE/STATUS/RELATION, PARENT_REFERENCE, LEADER_REFERENCE/LEADER_APPROVAL_REQUIRED, ORGANIZATION_REFERENCE, AFFILIATION_REFERENCE, HIERARCHY_CYCLE, RELATION_REFERENCE, PRESENTATION_REFERENCE, UNSAFE_INPUT. +- organizations=[]는 오류이며 employees=[]는 유효할 수 있다. State 타입의 empty가 실제 empty UI 지원 증거는 아니다. +- Auto는 추가로 COMPANY_PRESET/COMPANY_HIERARCHY 및 TOP_NODE_CONFIG/TOP_NODE_REFERENCE를 검사한다. 기본 Validator는 회사 프리셋/회사 간 계층 경계를 검사하지 않으므로 두 층을 구분한다. +- source 오류는 SOURCE_URL/LOAD/PARSE/ABORT/SNAPSHOT, 스타일 오류는 CORE_STYLE 등으로 구별한다. Core state.error와 React onLifecycleError의 책임을 구분한다. +- calculateSummary(data).raw는 unique 직원·direct/inclusive·position·affiliation·status 집계다. 업무 승인 집계는 aggregationPolicy가 없으면 unavailable(policyRequired)이다. 현재 정책 구현은 includeLeaders=true와 membershipMode=primaryOnly 등 제한을 확인하며 임의 excludeLeaders/uniqueEmployee 지원을 선언하지 않는다. + +### 주요 미지원·별도 확인 대상 + +- 자연어→데이터 자동 생성 UI, CSV/XLSX 직접 import, 서버 저장·외부 API·CSV/PDF/이미지 export는 공개 사용 기능이 아니다. +- 임의 회사 프리셋 등록 setter·카드 교체·Compact·공개 collapse setter·React imperative ref·Chart Only 전용 옵션은 없다. +- 제공 데이터의 최대 인원/Depth·모든 브라우저·실제 모바일 터치/스크린리더·외부 fresh install을 이번 소스 분석으로 보장하지 않는다. +- 기존 P10 PARTIAL/Guard FAIL 10건은 그대로다. 새 가이드의 문서 검사와 제품 전체 PASS를 혼동하지 않는다. +- [P10.3 GuardReview](guard-review-p10-3.md)는 별도 Worktree에서 작성한 원본 분석이다. 기존 FAIL 10건을 재현했으며 제품·Guard 수정은 없다. P10.3 Git 공유 단계에서도 이 FAIL을 유지한다. 완전한 Chart Only 공개 API, 외부 CSS 테마 표준화, Fixed SVG/PNG 테마화, 디자인 승인, main Merge와 Docker 배포는 별도 후속 작업이다. + +### 이 가이드 예제 검증 범위 + +아래 검증 상태는 이 문서의 예제를 대상으로 한 별도 결과이며 기존 제품 회귀검사 결과가 아니다. + +- **TypeScript PASS:** 이 문서에서 추출한 TS/TSX 코드 블록 8개를 설치된 TypeScript 7.0.2의 strict·ES2022·Bundler·react-jsx 설정으로 검사했다. Core/React import는 기준 Worktree의 src 공개 진입점, CSS URL은 기존 Vite client 타입에 연결했다. 새 빌드 산출물을 지원 증거로 사용하지 않았다. +- **데이터 실행 PASS:** 기준 Worktree의 데이터 모듈을 임시 경로에 컴파일해 JSON 2개와 data.ts의 A/B 데이터 2개를 실제 Validator로 확인했다. A=7조직/7명, B=독립 Root 2개/10명, 조직장 중복 없는 집계, 직원 이동 후 원본 소속·총원 보존, 자손으로 그룹 이동 거절, 회사 프리셋 7개 값을 확인했다. +- **Runtime NOT VERIFIED:** App/UpdatingChart의 React 렌더링, mountChart의 Vanilla mount, configureLayout, handle 기반 moveEmployee/updateChart/replaceSource, HTML 진입점의 실제 실행은 확인하지 않았다. 타입 검사와 순수 applyOrgChartMove 실행을 이들 브라우저 예제의 실행 PASS로 해석하지 않는다. +- 실제 React/Vanilla 화면 렌더링·CSS load·인터랙션 시연·시각 QA: **NOT VERIFIED**. +- 설치/전체 build/Storybook 회귀·외부 소비 프로젝트 fresh install: **NOT VERIFIED**. + +위 항목은 UsageGuide 원본 문서 작업 당시의 범위다. **P10.2 추가 검증:** 통합 후 TS/TSX 예제 10개를 현재 package 공개 `.d.ts`에 연결해 strict·skipLibCheck=false로 확인했다(PASS). 기존 실제 npm pack A/B/C 설치·타입·Build·팝업 검사 PASS는 현재 배포 JS/타입/CSS의 byte 동일성을 확인해 재사용했다. 독립 Chart Only React/Vanilla Build와 위 브라우저 검사는 새로 수행했다. 이 가이드의 HMChart/CustomChart 예제 자체의 브라우저 시연, 전체 회귀·성능·SSR·스크린리더·모바일 touch는 NOT VERIFIED다. + +## 12. 관련 문서·실제 소스 + +필요한 부분만 확인한다. + +- [AGENTS.md](../../AGENTS.md), [CLAUDE.md](../../CLAUDE.md): 프로젝트 공통 제약. +- [AI 생성 정책](ai-page-generation-rules.md): 자연어/제공 데이터 생성·보존·최소 화면 정책. +- [Expanded 사용법](expanded-usage.md), [Framework 통합](framework-integration.md), [독립 컴포넌트](standalone-components-p1.md): 상세 소비 경로. +- [Core 구현](../../packages/org-chart/src/core/index.ts), [공개 옵션](../../packages/org-chart/src/core/types.ts): 초기화·업데이트·정리·Auto setter·이벤트. +- [React 구현](../../packages/org-chart-react/src/OrgChart.tsx), [Props](../../packages/org-chart-react/src/types.ts): lifecycle과 갱신 경계. +- [데이터 타입](../../packages/org-chart/src/data/types.ts), [검증](../../packages/org-chart/src/data/validate.ts), [이동](../../packages/org-chart/src/data/moves.ts): 실제 Contract와 정책. +- [P10 보고서](implementation-report-p10.md): 기존 PARTIAL/Guard FAIL·별도 미검증 기록. 현재 기능의 근거는 보고서보다 이 가이드 기준 소스다. + +원본 UsageGuide Worktree의 문서 예제 검증 기록은 위에 보존했다. P10.2에서는 이 가이드의 Entry/설치/Adapter 설명과 Chart Only 정책 연결을 보완한다. 제품 코드·기존 디자인·Core 공개 API를 추가 변경하지 않는다. diff --git a/packages/org-chart-react/README.md b/packages/org-chart-react/README.md index 52650a8..5a48681 100644 --- a/packages/org-chart-react/README.md +++ b/packages/org-chart-react/README.md @@ -33,11 +33,39 @@ export function OrganizationContent({ data }: { data: OrgChartDataset }) { } ``` -The `?url` imports above are Vite asset imports, not a universal browser feature. Other hosts resolve the two Core public CSS entries to actual deployment URLs. Importing CSS only into the document does not style the Core's Shadow DOM. Engine CSS is linked first, then standalone Core CSS. P1 owns its colors, typography and geometry without depending on HM Theme. Set public `--hm-orgchart-*` properties on the wrapper container to override only this instance. No HM common variables are redefined. Default Core rendering does not load `@hm-design/ui`; opting into `employeePopup="hm-dialog"` loads the optional peer and requires the consumer's HM Theme CSS. Bundlers may resolve that optional dynamic chunk during a build even for a default consumer. For independent Tree/Summary integration, use the [Core factories](https://gitea.hmac.kr/b25027/HMDesign/src/commit/d2b76e766466e1f156f6a0f8f63710e023af900e/docs/org-chart/standalone-components-p1.md) in a ref/effect without copying their UI. +The `?url` imports above are Vite asset imports, not a universal browser feature. Other hosts resolve the two Core public CSS entries to actual deployment URLs. Importing CSS only into the document does not style the Core's Shadow DOM. Engine CSS is linked first, then standalone Core CSS. P1 owns its colors, typography and geometry without depending on HM Theme. Set public `--hm-orgchart-*` properties on the wrapper container to override only this instance. No HM common variables are redefined. The default entry has no runtime or declaration dependency on HM UI. For independent Tree/Summary integration, use the [Core factories](https://gitea.hmac.kr/b25027/HMDesign/src/commit/d2b76e766466e1f156f6a0f8f63710e023af900e/docs/org-chart/standalone-components-p1.md) in a ref/effect without copying their UI. + +## Explicit popup adapters + +Minimum React installation: `@hm-design/org-chart`, `@hm-design/org-chart-react`, React 19 and the host renderer (usually `react-dom`). HM UI, HM Theme and Tailwind are unnecessary for the default entry and Core popup. Core + Vanilla requires only `@hm-design/org-chart` and its public CSS URLs. + +For the existing HM Dialog, install the optional `@hm-design/ui@0.1.0` peer and its required peers, provide the host's real HM Theme/Tailwind CSS, and explicitly select both the entry and popup mode: + +```tsx +import { OrgChart } from '@hm-design/org-chart-react/hm-dialog'; +// Existing dataSource, stylesheetUrls and all other Props are unchanged. +; +``` + +Migration from the previous HM popup setup: change the `OrgChart` import to `/hm-dialog`; keep `employeePopup="hm-dialog"`. Installing UI alone never switches the popup. Even the HM entry defaults to the Core popup. The default entry can also use the exported `HMEmployeeDialog` as `employeePopupAdapter` explicitly. + +For another design system, keep the default entry and implement a React component with the public `OrgChartEmployeePopupProps` contract: + +```tsx +import { OrgChart, type OrgChartEmployeePopupProps } from '@hm-design/org-chart-react'; +// MyEmployeePopup receives { selection, onClose }; selection is null when closed. +// Read selection.employee/context without mutating them. Own accessible modal +// semantics, Escape/close behavior and focus restoration to context.trigger. +; +``` + +Neither Core nor the dataset changes. Supplying an adapter alone does not enable it. An explicit `onEmployeeClick` still takes precedence. If an adapter mode is requested without an adapter, the wrapper retains the Core popup and reports a configuration error through `onLifecycleError`. Fixed continues to use its existing Core popup. Department Colors and Overview remain Storybook-only pages and have no new exports. ## Public API -Named runtime export: `OrgChart`. Type exports: `OrgChartProps` and the Core's public types, reexported as types only. Runtime helpers such as `JsonOrgChartDataSource` remain available from `@hm-design/org-chart`. +Default named runtime export: `OrgChart`. `/hm-dialog` exports the same `OrgChart` Props and `HMEmployeeDialog` adapter. Type exports: `OrgChartProps`, `OrgChartEmployeeSelection`, `OrgChartEmployeePopupProps` and the Core's public types, reexported as types only. Runtime helpers such as `JsonOrgChartDataSource` remain available from `@hm-design/org-chart`. `OrgChartProps` reuses `Omit`: @@ -54,7 +82,8 @@ feedback and drag/keyboard activation are implemented in the shared Core. - Optional `selectedCompanyIds`: undefined selects all, [] selects none. Changes call Core selection on the existing instance, preserving per-company collapse state and source counts. - Optional `autoLayout`: `{ type: 'tb' | 'lr' | 'hybrid', hybridStartDepth: 2 | 3 | 4, initialVisibleDepth: 1 | 2 | 3 | 'all', hybridOrganizationIds?: readonly string[] }` (each field optional). Changes use Core `setAutoLayout` on the existing instance; TB/LR/Hybrid changes retain valid collapse state. Per-group hybrid IDs apply only to TB; clear them when selecting LR/global Hybrid. Omission follows Core defaults (TB, Hybrid depth 3, all visible). - Optional `interactions`: `{ collapse?: boolean, zoom?: boolean, pan?: boolean }`, all enabled by default. Auto independently enables/disables group toggles, Zoom controls/wheel Zoom and drag Pan via Core `setInteractions` on the same instance. Omitted fields are enabled, and removing the prop restores all three defaults. Existing collapse and viewport state remain; TB/LR/Hybrid has no interaction prerequisite. Fixed ignores this option and keeps its existing behavior. -- Optional `employeePopup`: `'core'` (default), `'hm-dialog'`, or `'none'`. Applies only to Auto; Fixed retains its existing Core popup. HM Dialog requires the optional `@hm-design/ui@0.1.0` peer and the consumer's real HM Theme/Tailwind CSS entry. It is loaded lazily so default/Core-only consumers do not load HM UI. An explicit `onEmployeeClick` always takes precedence and suppresses either default popup. HM popup state closes on source/company changes and resolves the employee's replacement card in the original ShadowRoot when returning focus after a layout render. Auto uses Core `setEmployeeClickHandler` to change popup ownership on the same instance, retaining company selection, layout, collapse and viewport state. +- Optional `employeePopup`: `'core'` (default), `'hm-dialog'`, `'adapter'`, or `'none'`. Applies only to Auto; Fixed retains its existing Core popup. HM Dialog requires the explicit `/hm-dialog` entry or `employeePopupAdapter={HMEmployeeDialog}`, the optional `@hm-design/ui@0.1.0` peer and the consumer's real HM Theme/Tailwind CSS entry. An explicit `onEmployeeClick` always takes precedence and suppresses either default popup. Host popup state closes on source/company changes, adapter replacement and successful movement. HM focus restoration still resolves the employee's replacement card in the original ShadowRoot after a layout render. Auto uses Core `setEmployeeClickHandler` to change popup ownership on the same instance, retaining company selection, layout, collapse and viewport state. +- Optional `employeePopupAdapter`: React `ComponentType`. Receives the untouched employee/context selection and `onClose`; used only with an explicit adapter popup mode. No global registration or package-presence detection is performed. - Optional `companyTopNodes`, `employeeConnections`, `zoom`: immutable Core configuration maps/range. Identity changes recreate the handle; memoize these objects. Names, positions and profile overrides affect only Auto presentation, not callback data. - Optional `organizationLinks`: existing organization-ID-to-URL map, applied only to original 1Depth titles. Missing/blank values retain plain titles. Core validates HTTP(S)/relative URLs and rejects invalid URLs, other schemes and credentials. Treat the map as immutable and memoize it; a new map recreates the handle. - Optional `onEmployeeClick(employee, context)`: host-owned information UI, replacing the Core's built-in popup. `context` contains the existing `organization`, `affiliation`, focusable `trigger` card and `originalEvent`. It works in both modes and receives untouched Employee data. @@ -105,7 +134,7 @@ Committed callback publication, Core ownership and source updates use layout eff ## Package boundary and verification limits -The build emits ESM `dist/index.js`, a lazy HM Dialog chunk, and TypeScript declarations. It preserves the `"use client"` boundary and leaves React (including `react/jsx-runtime`), `@hm-design/org-chart`, and optional `@hm-design/ui` external. React is a `^19.0.0` peer, matching the repository's existing React package convention; Core `0.1.0` is a runtime dependency. React DOM is supplied by the consuming app and is not imported by this package. Core must be built before wrapper typecheck/build in this workspace because dependency resolution uses its public declarations. +The build emits separate ESM `dist/index.js` and `dist/hm-dialog.js` entries, their shared UI-independent wrapper chunk, and TypeScript declarations. It preserves the `"use client"` boundary and leaves React (including `react/jsx-runtime`), `@hm-design/org-chart`, and optional `@hm-design/ui` external. Only the HM entry imports UI. React is a `^19.0.0` peer, matching the repository's existing React package convention; Core `0.1.0` is a runtime dependency. React DOM is supplied by the consuming app and is not imported by this package. Core must be built before wrapper typecheck/build in this workspace because dependency resolution uses its public declarations. Building all entries, including the HM adapter, also needs HM UI declarations in the maintainer workspace; installing the packed default entry does not. From the repository root: @@ -114,4 +143,4 @@ npm run typecheck --workspace @hm-design/org-chart-react npm run build --workspace @hm-design/org-chart-react ``` -Storybook is one React host, not proof of every consumer deployment. The adapter delegates Auto Pan/Zoom to Core; it adds no alternate layout, additional relation overlays or API client. SSR/hydration, full screen reader behavior, physical touch, complete heap/GC recovery, Theme-less rendering, Workdesk reference visual approval and production consumers require their own evidence. Public ESM import without a DOM does not establish SSR/hydration support. Consult the final integration report for the actual typecheck/build/browser/consumer results. +Storybook is one React host, not proof of every consumer deployment. The adapter delegates Auto Pan/Zoom to Core; it adds no alternate layout, additional relation overlays or API client. SSR/hydration, full screen reader behavior, physical touch, complete heap/GC recovery, HM Dialog without its Theme, Workdesk reference visual approval and every deployment environment require their own evidence. Default/Core rendering requires no HM Theme. Public ESM import without a DOM does not establish SSR/hydration support. Consult the minimal-install verification report for actual packed-package consumer results. diff --git a/packages/org-chart-react/build.mjs b/packages/org-chart-react/build.mjs index 2f51571..4cbc123 100644 --- a/packages/org-chart-react/build.mjs +++ b/packages/org-chart-react/build.mjs @@ -18,7 +18,10 @@ await build({ target: 'es2022', outDir: 'dist', minify: false, - lib: { entry: path.join(root, 'src/index.ts'), formats: ['es'], fileName: () => 'index.js' }, + lib: { + entry: { index: path.join(root, 'src/index.ts'), 'hm-dialog': path.join(root, 'src/hm-dialog.tsx') }, + formats: ['es'], fileName: (_format, name) => `${name}.js`, + }, rolldownOptions: { external: id => externals.some(dependency => id === dependency || id.startsWith(`${dependency}/`)), onwarn(warning, warn) { @@ -37,5 +40,8 @@ execFileSync(process.execPath, [ '-p', path.join(root, 'tsconfig.build.json'), ], { stdio: 'inherit' }); -const entry = path.join(root, 'dist/index.js'); -await writeFile(entry, `"use client";\n${await readFile(entry, 'utf8')}`); +for (const name of ['index', 'hm-dialog']) { + const entry = path.join(root, `dist/${name}.js`); + const content = await readFile(entry, 'utf8'); + await writeFile(entry, content.startsWith('"use client";') ? content : `"use client";\n${content}`); +} diff --git a/packages/org-chart-react/package.json b/packages/org-chart-react/package.json index 4d61a88..8630125 100644 --- a/packages/org-chart-react/package.json +++ b/packages/org-chart-react/package.json @@ -10,6 +10,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./hm-dialog": { + "types": "./dist/hm-dialog.d.ts", + "import": "./dist/hm-dialog.js", + "default": "./dist/hm-dialog.js" } }, "files": ["dist", "README.md"], diff --git a/packages/org-chart-react/src/HMEmployeeDialog.tsx b/packages/org-chart-react/src/HMEmployeeDialog.tsx index c3855a3..1308654 100644 --- a/packages/org-chart-react/src/HMEmployeeDialog.tsx +++ b/packages/org-chart-react/src/HMEmployeeDialog.tsx @@ -5,21 +5,13 @@ import { Avatar, AvatarFallback, AvatarImage, Button, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@hm-design/ui'; -import type { Employee, OrgChartEmployeeClickContext } from '@hm-design/org-chart'; - -export interface EmployeeDialogSelection { - employee: Employee; - context: OrgChartEmployeeClickContext; -} +import type { OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js'; const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const; -/** Lazy React-only adapter. The Core and Vanilla remain independent of HM UI. */ -export default function HMEmployeeDialog({ selection, onClose }: { - selection: EmployeeDialogSelection | null; - onClose: () => void; -}) { - const returnTarget = useRef(null); +/** Explicit React-only adapter. The Core and default entry stay UI-independent. */ +export default function HMEmployeeDialog({ selection, onClose }: OrgChartEmployeePopupProps) { + const returnTarget = useRef(null); const returnRoot = useRef(null); const returnContainer = useRef(null); useLayoutEffect(() => { diff --git a/packages/org-chart-react/src/OrgChart.tsx b/packages/org-chart-react/src/OrgChart.tsx index 3a6a795..db00077 100644 --- a/packages/org-chart-react/src/OrgChart.tsx +++ b/packages/org-chart-react/src/OrgChart.tsx @@ -1,12 +1,9 @@ "use client"; -import { lazy, Suspense, useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; import { createOrgChart } from '@hm-design/org-chart'; import type { OrgChartAutoLayoutOptions, OrgChartDataSource, OrgChartHandle, OrgChartInteractionOptions, OrgChartOptions } from '@hm-design/org-chart'; -import type { OrgChartProps } from './types.js'; -import type { EmployeeDialogSelection } from './HMEmployeeDialog.js'; - -const HMEmployeeDialog = lazy(() => import('./HMEmployeeDialog.js')); +import type { OrgChartEmployeeSelection, OrgChartProps } from './types.js'; function annotateEmployeeCards(shadow: ShadowRoot, popup: NonNullable, customCallback: boolean) { const actionable = customCallback || popup !== 'none'; @@ -33,7 +30,7 @@ interface Instance { dragDropEnabled: boolean; readonly employeeClick: NonNullable; employeeCallbackEnabled: boolean; - popupMode: 'core' | 'hm-dialog' | 'none'; + popupMode: NonNullable; operation: number; } @@ -56,6 +53,7 @@ export function OrgChart({ onEmployeeClick, onMove, employeePopup = 'core', + employeePopupAdapter: EmployeePopupAdapter, className, style, onStateChange, @@ -64,19 +62,28 @@ export function OrgChart({ const containerRef = useRef(null); const instanceRef = useRef(null); const generationRef = useRef(0); - const [employeeSelection, setEmployeeSelection] = useState(null); - const committedRef = useRef({ dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup }); + const [employeeSelection, setEmployeeSelection] = useState(null); + const requestedAdapter = employeePopup === 'hm-dialog' || employeePopup === 'adapter'; + const effectivePopup = requestedAdapter && !EmployeePopupAdapter ? 'core' : employeePopup; + const committedRef = useRef({ dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup: effectivePopup }); // Publish committed values before Core effects run, without letting callback // identity changes or abandoned concurrent renders alter Core ownership. useLayoutEffect(() => { - committedRef.current = { dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup }; - }, [dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup]); + committedRef.current = { dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup: effectivePopup }; + }, [dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, effectivePopup]); + + useLayoutEffect(() => { + if (layoutMode !== 'auto' || !requestedAdapter || EmployeePopupAdapter || onEmployeeClick) return; + try { + committedRef.current.onLifecycleError?.(new Error('OrgChart popup adapter is missing. Use @hm-design/org-chart-react/hm-dialog or supply employeePopupAdapter. Falling back to the Core popup.')); + } catch { /* Consumer error reporting cannot prevent Core rendering. */ } + }, [layoutMode, requestedAdapter, EmployeePopupAdapter, Boolean(onEmployeeClick)]); const engineStylesheetUrl = stylesheetUrls.engine; const coreStylesheetUrl = stylesheetUrls.core; const hasEmployeeCallback = typeof onEmployeeClick === 'function'; - const popupMode = layoutMode === 'auto' ? employeePopup : 'core'; + const popupMode = layoutMode === 'auto' ? effectivePopup : 'core'; const adapterOwnsPopup = popupMode !== 'core'; const hasAdapterCallback = hasEmployeeCallback || popupMode !== 'core'; const fixedHasEmployeeCallback = layoutMode !== 'auto' && hasEmployeeCallback; @@ -103,7 +110,7 @@ export function OrgChart({ if (generationRef.current !== generation) return; const callback = committedRef.current.onEmployeeClick; if (callback) callback(employee, context); - else if (committedRef.current.employeePopup === 'hm-dialog') setEmployeeSelection({ employee, context }); + else if (committedRef.current.employeePopup === 'hm-dialog' || committedRef.current.employeePopup === 'adapter') setEmployeeSelection({ employee, context }); }; handle = createOrgChart({ dataSource: source, @@ -197,7 +204,7 @@ export function OrgChart({ if (shadow && layoutMode === 'auto') { annotateEmployeeCards(shadow, popupMode, hasEmployeeCallback); } - }, [dataSource, selectedCompanyIds, popupMode, hasEmployeeCallback, layoutMode, adapterOwnsPopup]); + }, [dataSource, selectedCompanyIds, popupMode, hasEmployeeCallback, layoutMode, adapterOwnsPopup, EmployeePopupAdapter]); useLayoutEffect(() => { const instance = instanceRef.current; @@ -259,8 +266,7 @@ export function OrgChart({ return <>
- {popupMode === 'hm-dialog' && !hasEmployeeCallback ? - setEmployeeSelection(null)} /> - : null} + {(popupMode === 'hm-dialog' || popupMode === 'adapter') && EmployeePopupAdapter && !hasEmployeeCallback + ? setEmployeeSelection(null)} /> : null} ; } diff --git a/packages/org-chart-react/src/hm-dialog.tsx b/packages/org-chart-react/src/hm-dialog.tsx new file mode 100644 index 0000000..d4b6cc1 --- /dev/null +++ b/packages/org-chart-react/src/hm-dialog.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { OrgChart as CoreOrgChart } from './OrgChart.js'; +import HMEmployeeDialog from './HMEmployeeDialog.js'; +import type { OrgChartProps } from './types.js'; + +/** HM integration is explicit at the import and popup setting boundaries. */ +export function OrgChart(props: OrgChartProps) { + return ; +} + +export { HMEmployeeDialog }; +export type { OrgChartProps, OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js'; +export type * from '@hm-design/org-chart'; diff --git a/packages/org-chart-react/src/index.ts b/packages/org-chart-react/src/index.ts index 6726cdc..f96de25 100644 --- a/packages/org-chart-react/src/index.ts +++ b/packages/org-chart-react/src/index.ts @@ -1,5 +1,5 @@ "use client"; export { OrgChart } from './OrgChart.js'; -export type { OrgChartProps } from './types.js'; +export type { OrgChartProps, OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js'; export type * from '@hm-design/org-chart'; diff --git a/packages/org-chart-react/src/types.ts b/packages/org-chart-react/src/types.ts index e1c0149..020fa64 100644 --- a/packages/org-chart-react/src/types.ts +++ b/packages/org-chart-react/src/types.ts @@ -1,15 +1,29 @@ -import type { CSSProperties } from 'react'; -import type { OrgChartOptions, OrgChartState } from '@hm-design/org-chart'; +import type { ComponentType, CSSProperties } from 'react'; +import type { Employee, OrgChartEmployeeClickContext, OrgChartOptions, OrgChartState } from '@hm-design/org-chart'; + +export interface OrgChartEmployeeSelection { + employee: Employee; + context: OrgChartEmployeeClickContext; +} + +/** Host-owned popup UI. Adapters own presentation and focus restoration. */ +export interface OrgChartEmployeePopupProps { + selection: OrgChartEmployeeSelection | null; + onClose: () => void; +} /** The Core owns data, organization rendering and interactions; React owns its - * dedicated container and the optional Portal-based HM employee information UI. */ + * dedicated container and optional host-owned employee information UI. */ export interface OrgChartProps extends Omit { className?: string; style?: CSSProperties; - /** Auto only. Opt into the public HM Dialog (requires @hm-design/ui + its - * Theme), retain the Vanilla popup, or suppress the default information UI. + /** Auto only. Explicitly select an HM/custom adapter, retain the Vanilla + * popup, or suppress the default information UI. * An explicit onEmployeeClick callback always owns the popup instead. */ - employeePopup?: 'core' | 'hm-dialog' | 'none'; + employeePopup?: 'core' | 'hm-dialog' | 'adapter' | 'none'; + /** Explicit UI integration. Supplying an adapter does not enable it: select + * employeePopup="adapter" (or "hm-dialog" for the HM entry) as well. */ + employeePopupAdapter?: ComponentType; /** Latest committed callback. Controlled data/style failures arrive as error states. */ onStateChange?: (state: OrgChartState) => void; /** Unexpected create/mount/reload/cleanup errors, rather than controlled error states. */ diff --git a/scripts/verify-orgchart-hm-story.mjs b/scripts/verify-orgchart-hm-story.mjs new file mode 100644 index 0000000..9e028f3 --- /dev/null +++ b/scripts/verify-orgchart-hm-story.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import http from 'node:http'; +import path from 'node:path'; +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +// Target only the affected existing Story and the unchanged Fixed composition. +const evidence = fileURLToPath(new URL('../test-results/org-chart-minimal-install/', import.meta.url)); +const root = path.join(evidence, 'storybook'); +const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' }; +const server = http.createServer(async (request, response) => { + const filename = path.resolve(root, '.' + decodeURIComponent(new URL(request.url, 'http://localhost').pathname)); + if (!filename.startsWith(root + path.sep)) { response.writeHead(403).end(); return; } + try { const bytes = await readFile(filename); response.writeHead(200, { 'Content-Type': mime[path.extname(filename)] || 'application/octet-stream' }).end(bytes); } + catch { response.writeHead(404).end(); } +}); +await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); +assert.ok(process.env.ORGCHART_PLAYWRIGHT_MODULE, 'Set ORGCHART_PLAYWRIGHT_MODULE'); +const { chromium } = await import(pathToFileURL(process.env.ORGCHART_PLAYWRIGHT_MODULE).href); +const browser = await chromium.launch({ headless: true, executablePath: process.env.ORGCHART_CHROME || 'C:/Program Files/Google/Chrome/Application/chrome.exe' }); +const errors = [], results = []; +try { + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); + for (const id of ['orgchart--expanded-org-chart', 'orgchart--full-composition']) { + await page.goto(`http://127.0.0.1:${server.address().port}/iframe.html?id=${id}&viewMode=story`); + const card = page.locator('.hm-orgchart-employee').first(); + await card.waitFor(); await card.focus(); await page.keyboard.press('Enter'); + const hm = page.locator('[data-component="HMOrgChartEmployeeDialog"]'); + const core = page.locator('dialog.hm-orgchart-employee-dialog'); + if (id.includes('expanded')) { + await hm.waitFor({ state: 'visible' }); + assert.equal(await core.evaluate(e => e.open), false); + await page.screenshot({ path: path.join(evidence, 'storybook-expanded-hm.png') }); + await page.keyboard.press('Escape'); await hm.waitFor({ state: 'hidden' }); + assert.equal(await card.evaluate(e => e.getRootNode().activeElement === e), true); + const trigger = page.getByRole('combobox', { name: '직원 정보 팝업' }); + await trigger.click(); await page.getByRole('option', { name: 'Core 기본 팝업', exact: true }).click(); + // Wait for Radix's exiting Select portal/focus restoration before opening + // another modal with the keyboard. External consumers have no Select here. + await page.getByRole('listbox').waitFor({ state: 'detached' }); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await card.focus(); await page.keyboard.press('Enter'); + await core.waitFor({ state: 'visible' }); + assert.equal(await hm.count(), 0); + assert.equal(await core.evaluate(e => e.contains(e.getRootNode().activeElement)), true, 'Core modal receives focus after Select closes'); + await page.keyboard.press('Escape'); await core.waitFor({ state: 'hidden' }); + results.push({ story: id, status: 'PASS', checks: ['Existing default HM Dialog', 'Escape and focus restoration', 'Existing selector switches to Core popup'] }); + } else { + await core.waitFor({ state: 'visible' }); assert.equal(await hm.count(), 0); + await page.screenshot({ path: path.join(evidence, 'storybook-fixed-core.png') }); + await page.keyboard.press('Escape'); await core.waitFor({ state: 'hidden' }); + results.push({ story: id, status: 'PASS', checks: ['Existing Fixed render and Core popup', 'Escape'] }); + } + } + assert.deepEqual(errors, []); + await writeFile(path.join(evidence, 'storybook-browser.json'), JSON.stringify({ status: 'PASS', results, errors }, null, 2)); + console.log(JSON.stringify({ status: 'PASS', results, errors })); +} finally { + await browser.close(); await new Promise(resolve => server.close(resolve)); +} diff --git a/scripts/verify-orgchart-minimal-install.mjs b/scripts/verify-orgchart-minimal-install.mjs new file mode 100644 index 0000000..8702990 --- /dev/null +++ b/scripts/verify-orgchart-minimal-install.mjs @@ -0,0 +1,225 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { mkdir, mkdtemp, readFile, writeFile, copyFile, access } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +// External consumers are outside the checkout and its node_modules ancestors. +// Preserve the temp projects, lockfiles and logs for inspection; never clean them. +const root = fileURLToPath(new URL('../', import.meta.url)); +const evidence = path.join(root, 'test-results/org-chart-minimal-install'); +await mkdir(evidence, { recursive: true }); +const npmCli = process.env.ORGCHART_NPM_CLI || path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'); +const externalRoot = process.env.ORGCHART_CONSUMER_ROOT || await mkdtemp(path.join(tmpdir(), 'hm-orgchart-minimal-')); +const projects = Object.fromEntries(['A', 'B', 'C'].map(name => [name, path.join(externalRoot, name)])); +await writeFile(path.join(evidence, 'projects.json'), JSON.stringify({ externalRoot, projects }, null, 2)); +console.log(`External projects: ${externalRoot}`); + +function run(args, cwd, log) { + const result = spawnSync(process.execPath, args, { cwd, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 }); + return { status: result.status, output: `${result.stdout ?? ''}${result.stderr ?? ''}`, error: result.error, log }; +} +async function npm(args, cwd, log) { + const result = run([npmCli, ...args], cwd, log); + await writeFile(path.join(evidence, `${log}.log`), result.output); + assert.equal(result.status, 0, `${log} failed: ${result.output.slice(-5000)}`); + console.log(`${log}: PASS`); + return result; +} + +if (process.env.ORGCHART_SKIP_PACK !== '1') for (const name of ['org-chart', 'ui', 'org-chart-react']) { + await npm(['pack', '--workspace', `@hm-design/${name}`, '--pack-destination', evidence], root, `pack-${name}`); +} +const tarball = name => `file:${path.join(evidence, `hm-design-${name}-0.1.0.tgz`).replaceAll('\\', '/')}`; +const versions = { react: '19.3.0', 'react-dom': '19.3.0' }; +for (const [name, directory] of Object.entries(projects)) { + await mkdir(directory, { recursive: true }); + const manifest = { + name: `hm-orgchart-minimal-${name.toLowerCase()}`, private: true, type: 'module', + scripts: { typecheck: 'tsc --noEmit', build: 'npm run typecheck && vite build' }, + dependencies: { '@hm-design/org-chart': tarball('org-chart'), ...(name === 'A' ? {} : { ...versions, '@hm-design/org-chart-react': tarball('org-chart-react') }), ...(name === 'C' ? { '@hm-design/ui': tarball('ui'), tailwindcss: '4.3.3' } : {}) }, + devDependencies: { typescript: '7.0.2', vite: '8.3.0', ...(name === 'A' ? {} : { '@types/react': '19.3.0', '@types/react-dom': '19.3.0' }), ...(name === 'C' ? { '@tailwindcss/vite': '4.3.3' } : {}) }, + }; + await writeFile(path.join(directory, 'package.json'), JSON.stringify(manifest, null, 2)); + await writeFile(path.join(directory, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', lib: ['ES2022', 'DOM', 'DOM.Iterable'], module: 'ESNext', moduleResolution: 'Bundler', jsx: 'react-jsx', strict: true, skipLibCheck: false, noEmit: true, types: ['vite/client'], resolveJsonModule: true }, include: ['main.ts', 'main.tsx'] }, null, 2)); + await copyFile(path.join(root, 'examples/org-chart/data-expanded-small.json'), path.join(directory, 'data.json')); + await writeFile(path.join(directory, 'index.html'), 'OrgChart package verification
'); + const shared = `import { createOrgChart, JsonOrgChartDataSource } from '@hm-design/org-chart'; +import engine from '@hm-design/org-chart/engine.css?url'; +import core from '@hm-design/org-chart/style.css?url'; +import data from './data.json'; +const source = new JsonOrgChartDataSource(data); +const stylesheetUrls = { engine, core }; +const params = new URLSearchParams(location.search); +const layoutMode = params.get('layout') === 'fixed' ? 'fixed' : 'auto'; +const events: unknown[] = [], errors: string[] = []; +const onStateChange = (state: import('@hm-design/org-chart').OrgChartState) => { events.push(state); document.documentElement.dataset.chartState = state.status; }; +const onLifecycleError = (error: unknown) => errors.push(String(error)); +Object.assign(window, { verification: { events, errors, dataBefore: JSON.stringify(data), data } }); +document.body.style.margin = '0'; +`; + if (name === 'A') { + await writeFile(path.join(directory, 'main.ts'), shared + `const handle = createOrgChart({dataSource: source, stylesheetUrls, layoutMode, displayMode: 'readOnly', onStateChange}); +Object.assign(window, { handle }); +await handle.mount(document.getElementById('app')!); +window.addEventListener('pagehide', () => handle.destroy()); +`); + } else { + await writeFile(path.join(directory, 'main.tsx'), shared.replace('createOrgChart, ', '') + `import { StrictMode, useLayoutEffect, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { OrgChart as BaseOrgChart, type OrgChartEmployeePopupProps, type OrgChartProps } from '@hm-design/org-chart-react'; +${name === 'C' ? "import { OrgChart as HMOrgChart } from '@hm-design/org-chart-react/hm-dialog';\nimport './hm.css';" : ''} +function CustomPopup({selection, onClose}: OrgChartEmployeePopupProps) { + useLayoutEffect(() => { if (!selection) return; const trigger = selection.context.trigger; return () => { if (trigger.isConnected) trigger.focus(); }; }, [selection]); + return selection ?

{selection.employee.name}

: null; +} +function App() { + const [mode, setMode] = useState>((params.get('popup') as OrgChartProps['employeePopup']) || 'core'); + const [callback, setCallback] = useState(false); + const Chart = ${name === 'C' ? "params.get('entry') === 'base' ? BaseOrgChart : HMOrgChart" : 'BaseOrgChart'}; + Object.assign(window, { setPopup: setMode, setCallback, source }); + return { Object.assign(window, { clicked: { employee, organization: context.organization } }); } : undefined} + onStateChange={onStateChange} onLifecycleError={onLifecycleError} />; +} +createRoot(document.getElementById('app')!).render(); +`); + if (name === 'C') { + await writeFile(path.join(directory, 'hm.css'), '@import "tailwindcss";\n@import "@hm-design/ui/theme.css";\n@source "./node_modules/@hm-design/ui/dist";\n'); + await writeFile(path.join(directory, 'vite.config.mjs'), "import tailwindcss from '@tailwindcss/vite'; export default { plugins: [tailwindcss()] };\n"); + } + } + await npm(['install', ...(process.env.ORGCHART_OFFLINE === '1' ? ['--offline'] : []), '--no-audit', '--no-fund'], directory, `install-${name}`); + const require = createRequire(path.join(directory, 'package.json')); + assert.ok(!require.resolve('@hm-design/org-chart').includes(root)); + if (name !== 'C') { + assert.throws(() => require.resolve('@hm-design/ui'), { code: 'MODULE_NOT_FOUND' }); + await assert.rejects(access(path.join(directory, 'node_modules/@hm-design/ui'))); + } + if (name === 'A') { + assert.throws(() => require.resolve('react'), { code: 'MODULE_NOT_FOUND' }); + assert.throws(() => require.resolve('@hm-design/org-chart-react'), { code: 'MODULE_NOT_FOUND' }); + } + await npm(['ls', '--all', '--json'], directory, `dependencies-${name}`); + await npm(['run', 'build'], directory, `build-${name}`); +} +// Traverse the shipped default runtime and declaration graph, including shared chunks. +const baseDir = path.join(projects.B, 'node_modules/@hm-design/org-chart-react/dist'); +const graph = []; +async function inspect(file) { + if (graph.includes(file)) return; + graph.push(file); + const text = await readFile(path.join(baseDir, file), 'utf8'); + assert.ok(!text.includes('@hm-design/ui') && !text.includes('HMEmployeeDialog') && !text.includes('hm-dialog.js'), `UI leaked into ${file}`); + for (const match of text.matchAll(/(?:from\s*|import\s*\()?["'](\.\/[^"']+\.(?:js|ts))["']/g)) { + let relative = match[1].slice(2); + if (file.endsWith('.d.ts') && relative.endsWith('.js')) relative = relative.slice(0, -3) + '.d.ts'; + await inspect(relative); + } +} +await inspect('index.js'); await inspect('index.d.ts'); +await writeFile(path.join(evidence, 'default-entry-graph.json'), JSON.stringify({ status: 'PASS', files: graph }, null, 2)); + +const playwrightPath = process.env.ORGCHART_PLAYWRIGHT_MODULE; +assert.ok(playwrightPath, 'Set ORGCHART_PLAYWRIGHT_MODULE to an existing Playwright entry (test-only).'); +const { chromium } = await import(pathToFileURL(playwrightPath).href); +const browser = await chromium.launch({ headless: true, executablePath: process.env.ORGCHART_CHROME || 'C:/Program Files/Google/Chrome/Application/chrome.exe' }); +const results = [], servers = [], runtimeErrors = []; +const check = (name, detail = null) => { results.push({ name, status: 'PASS', detail }); console.log(`browser: ${name}: PASS`); }; +try { + for (const [name, directory] of Object.entries(projects)) { + const vite = await import(pathToFileURL(createRequire(path.join(directory, 'package.json')).resolve('vite')).href); + const server = await vite.preview({ root: directory, preview: { host: '127.0.0.1', port: 0 }, logLevel: 'silent' }); + servers.push(server); + const url = server.resolvedUrls.local[0]; + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + page.on('pageerror', error => runtimeErrors.push({ project: name, message: error.message })); + page.on('console', message => { if (message.type() === 'error') runtimeErrors.push({ project: name, message: message.text() }); }); + page.on('response', response => { if (response.status() >= 400) runtimeErrors.push({ project: name, url: response.url(), status: response.status() }); }); + async function ready(query) { + await page.goto(url + query); + await page.waitForFunction(() => document.documentElement.dataset.chartState === 'ready'); + await page.locator('.hm-orgchart-employee').first().waitFor(); + assert.equal(await page.locator('.hm-orgchart-host').count(), 1); + assert.equal(await page.evaluate(() => JSON.stringify(window.verification.data) === window.verification.dataBefore), true); + } + const card = () => page.locator('.hm-orgchart-employee').first(); + const coreDialog = () => page.locator('dialog.hm-orgchart-employee-dialog'); + const hmDialog = () => page.locator('[data-component="HMOrgChartEmployeeDialog"]'); + async function corePopup(label) { + await card().focus(); await page.keyboard.press('Enter'); + await coreDialog().waitFor({ state: 'visible' }); + assert.equal(await coreDialog().evaluate(e => e.open), true); + assert.equal(await hmDialog().count(), 0); + await page.keyboard.press('Escape'); + await coreDialog().waitFor({ state: 'hidden' }); + assert.equal(await card().evaluate(e => e.getRootNode().activeElement === e), true); + await card().click(); await coreDialog().waitFor({ state: 'visible' }); + await page.getByRole('button', { name: '직원 정보 닫기', exact: true }).click(); + await coreDialog().waitFor({ state: 'hidden' }); + check(label); + } + for (const layout of ['auto', 'fixed']) { + await ready(`?layout=${layout}`); + await corePopup(`${name} ${layout}: render, Core popup, keyboard, close, focus`); + assert.deepEqual(await page.evaluate(() => window.verification.errors), []); + await page.screenshot({ path: path.join(evidence, `${name}-${layout}.png`) }); + } + if (name !== 'A') { + // Presence of a custom adapter or the HM entry never automatically opts in. + await ready('?layout=auto&adapter=supplied'); + await corePopup(`${name}: supplied adapter with default mode retains Core`); + await page.evaluate(() => window.setPopup('adapter')); + await card().click(); + await page.locator('[data-component="CustomPopup"]').waitFor({ state: 'visible' }); + assert.equal(await coreDialog().evaluate(e => e.open), false); + await page.getByRole('button', { name: 'Close custom' }).click(); + await page.locator('[data-component="CustomPopup"]').waitFor({ state: 'detached' }); + check(`${name}: custom adapter without Core/data changes`); + await page.evaluate(() => window.setCallback(true)); + await card().click(); + await page.waitForFunction(() => !!window.clicked); + assert.equal(await page.locator('[data-component="CustomPopup"]').count(), 0); + check(`${name}: onEmployeeClick takes precedence`); + } + if (name === 'B') { + await ready('?layout=auto&popup=hm-dialog'); + await corePopup('B: missing HM adapter safely retains Core popup'); + assert.ok((await page.evaluate(() => window.verification.errors)).every(e => e.includes('popup adapter is missing'))); + } + if (name === 'C') { + await ready('?layout=auto&entry=base'); + await corePopup('C: UI installed, base entry still uses Core'); + await ready('?layout=auto&popup=hm-dialog'); + await card().focus(); await page.keyboard.press('Enter'); + await hmDialog().waitFor({ state: 'visible' }); + assert.equal(await coreDialog().evaluate(e => e.open), false); + const employeeName = await card().locator('.hm-orgchart-person-name').textContent(); + assert.ok((await hmDialog().textContent()).includes(employeeName.trim())); + await page.screenshot({ path: path.join(evidence, 'C-hm-dialog.png') }); + await page.keyboard.press('Escape'); await hmDialog().waitFor({ state: 'hidden' }); + assert.equal(await card().evaluate(e => e.getRootNode().activeElement === e), true); + await card().click(); await hmDialog().waitFor({ state: 'visible' }); + await hmDialog().getByRole('button', { name: '닫기', exact: true }).click(); await hmDialog().waitFor({ state: 'hidden' }); + check('C: HM Dialog employee data, Enter, Escape, close, focus restoration'); + // Switch ownership without rebuilding Core, with an open HM popup. + await card().click(); await hmDialog().waitFor({ state: 'visible' }); + await page.evaluate(() => { window.savedHost = document.querySelector('.hm-orgchart-host'); window.setPopup('core'); }); + await hmDialog().waitFor({ state: 'hidden' }); + assert.equal(await page.evaluate(() => window.savedHost === document.querySelector('.hm-orgchart-host')), true); + await corePopup('C: HM to Core switch retains same Core instance'); + assert.deepEqual(await page.evaluate(() => window.verification.errors), []); + } + await page.close(); + } + assert.deepEqual(runtimeErrors, []); + check('No browser exceptions, console errors or HTTP failures', runtimeErrors); + await writeFile(path.join(evidence, 'browser.json'), JSON.stringify({ status: 'PASS', browser: browser.version(), results, runtimeErrors }, null, 2)); +} finally { + await browser.close(); + await Promise.all(servers.map(server => new Promise(resolve => server.httpServer.close(resolve)))); +} +console.log(`PASS: ${results.length} targeted browser checks; artifacts: ${evidence}`); diff --git a/src/org-chart/org-chart-expanded.stories.tsx b/src/org-chart/org-chart-expanded.stories.tsx index 3825cda..07cef0d 100644 --- a/src/org-chart/org-chart-expanded.stories.tsx +++ b/src/org-chart/org-chart-expanded.stories.tsx @@ -6,7 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger, Separator, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@hm-design/ui'; -import { OrgChart, type OrgChartProps } from '@hm-design/org-chart-react'; +import { OrgChart, type OrgChartProps } from '@hm-design/org-chart-react/hm-dialog'; import { companyColorPresets, createOrgChartExampleDataset, JsonOrgChartDataSource, type CompanyId, type OrgChartAutoLayoutOptions, type OrgChartDataset, type OrgChartExampleSelection, type OrgChartInteractionOptions,