Reviewed-on: b25027/HMDesign#2
This commit was merged in pull request #2.
This commit is contained in:
commit
a95ff7b810
283 files changed
+34695
-3
No files matched your search
@@ -43,6 +43,8 @@ const preview: Preview = {
|
||||
],
|
||||
"Patterns",
|
||||
["Data", ["Data Table", "Filter Bar"], "Layout", ["GNB", "LNB", "Dashboard Layout", "Loading Screen"]],
|
||||
"OrgChart",
|
||||
["Overview", "Department Colors", "Full Composition", "Expanded OrgChart"],
|
||||
"Center Graphic",
|
||||
"*",
|
||||
],
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
|
||||
이 파일을 먼저 읽고 필요한 항목만 탐색한다. 전체 문서·전체 소스·전체 Storybook을 반복해서 읽지 않는다.
|
||||
|
||||
## AI 조직도 생성 — 공통 정책
|
||||
|
||||
- OrgChart UI 수정 또는 AI 생성 작업은 [최종 디자인 승인 기록](docs/org-chart/design-approval.md)을 먼저 확인해 승인된 기본 디자인을 따른다. 기본값 변경·외부 테마 커스터마이징·데이터 생성의 구분과 접근성 미해결 상태는 해당 문서를 따른다.
|
||||
- 자연어 또는 JSON·CSV·Excel에서 **새 조직도 컴포넌트 또는 전체 페이지를 생성하는 작업**은 먼저 [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md)을 읽고 적용한다. 생성 방식 미지정 시 코드 작성 전에 위 정책의 사전 선택 질문으로 Chart Only / Full Page를 확인하고 답변 후 구현한다. 이미 명시한 방식은 다시 묻지 않는다. 회사·조직 유형·Depth·인원수를 특정 사례의 기본값으로 고정하지 않는다.
|
||||
- **Chart Only**는 기존 페이지에 삽입 가능한 독립 컴포넌트로 흰색 캔버스·조직도 노드·연결선, 최초 Fit to View·마우스 휠 확대/축소·Pan을 제공하며 외부 제목·통계·설명·조작 패널을 생성하지 않는다. **Full Page**는 기존 HM UI를 우선 재사용해 요청된 제목·정보·필터·컨트롤과 전체 배치·반응형을 구성한다. 두 방식 모두 기존 OrgChart Core·React Wrapper와 정확한 요청 구조·인원·데이터를 사용한다. Fixed·Auto/Expanded는 생성 방식 선택을 대신하지 않는다. Chart Only에는 아래 일반 업무 화면의 Navigation 의무 대신 해당 정책의 최소 화면 규칙을 적용하고, Full Page에는 기존 HM 화면 계약을 적용한다. 공개 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는 운영 기준이 아니다.
|
||||
3. 요구 기능에 맞는 기존 Pattern을 먼저 확인한 뒤 기존 Component → Variant/Prop → Composition 순으로 재사용한다. 목록 검색은 [FilterBar](docs/patterns/filter-bar.md)를 Table 유무와 관계없이 검토한다. 미사용 시 요구사항과 맞지 않는 API·구조 제약을 기록하며 강제하지 않는다. 기본 HTML·임의 CSS·`data-slot`으로 HM을 모방하지 않는다. 재사용은 import가 아닌 실제 렌더링으로 확인한다.
|
||||
|
||||
@@ -9,6 +9,8 @@ shadcn/ui(Radix, Tailwind v4, CVA) 기반 HM 디자인 시스템 시제품. Reac
|
||||
규칙의 기준은 아래 문서이며, 이 파일에는 규칙 본문을 복사하지 않는다. 작업 전에 해당 문서를 읽는다.
|
||||
|
||||
- `AGENTS.md` — AI Implementation Defaults와 Navigation 필수 요약. 기존 Component / Storybook Reference 우선, Theme / Semantic 기본 적용, 단일 Navigation, 최소 Custom은 사용자의 별도 지시 없이 모든 신규·수정 UI에 적용한다.
|
||||
- [최종 디자인 승인 기록](docs/org-chart/design-approval.md) — OrgChart UI 수정 또는 AI 생성 작업 전에 먼저 확인하고 승인된 기본 디자인을 따른다. 기본값 변경·외부 테마 커스터마이징·데이터 생성의 구분과 접근성 미해결 상태는 해당 문서를 따른다.
|
||||
- [AI 조직도 자동 생성 정책](docs/org-chart/ai-page-generation-rules.md) — 자연어 또는 JSON·CSV·Excel에서 새 조직도 컴포넌트 또는 전체 페이지를 생성할 때 먼저 읽는다. 생성 방식 미지정 시 코드 작성 전에 위 정책의 사전 선택 질문으로 Chart Only / Full Page를 확인하고 답변 후 구현한다. 이미 명시한 방식은 다시 묻지 않는다. Chart Only는 흰색 캔버스·노드·연결선의 삽입용 컴포넌트, 최초 Fit·휠 Zoom·Pan과 외부 UI 제외를 따르고, Full Page는 기존 HM UI로 요청 UI·전체 배치·반응형을 구성한다. 두 방식 모두 가변 계층·정확한 인원·제공 데이터 보존·기존 Core/React 재사용을 따른다. Fixed·Auto/Expanded는 생성 방식 선택을 대신하지 않는다. Chart Only의 Navigation 예외와 Full Page의 HM 화면 계약은 `AGENTS.md`를 따른다. 기존 Full Composition·Expanded OrgChart·Department Colors·Overview·P9.5·P-AI1에는 소급 적용하지 않는다.
|
||||
- 일반 업무 화면은 Storybook에 등록하지 않고 별도 React 화면·독립 미리보기로 제공한다. Storybook 수정은 Component/Pattern 또는 명시적 Storybook Reference 작업에 한정한다. 상세한 Storybook / 화면 생성 분리는 `AGENTS.md`와 `docs/guidelines/shadcn-기반-구현-원칙.md`를 따른다.
|
||||
- `docs/guidelines/프로젝트-지침.md` — §6 컴포넌트·Storybook·CSS 작업 규칙 (모든 컴포넌트 작업 전 필독)
|
||||
- `docs/guidelines/shadcn-기반-구현-원칙.md` — Component / Pattern / Story의 재사용 우선순위와 Custom 구현 허용 조건 (관련 작업 전 필독)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# AI Generation Guard — 경량 검증
|
||||
|
||||
## OrgChart 공개 CSS 선언 수집 (P11, 2026-10-13)
|
||||
|
||||
`guard-theme-analysis.mjs`는 연결된 `@hm-design/org-chart/style.css` 및 `style.css?url`의 공개 source CSS와 상대 import에서 선언명을 수집한다. OrgChart는 URL 값을 `stylesheetUrls.core`로 Shadow DOM에 넣으므로 일반 CSS entry와 구분한다. 이 수집은 **공개 변수 선언의 존재**를 확인하며, 실제 link 로딩·selector 우선순위·computed style을 증명하지 않는다. 실제 적용은 별도 브라우저 검증이 필요하다.
|
||||
|
||||
임의 `?url`/`?raw`/`?inline` CSS, 연결되지 않은 OrgChart 패키지, 다른 Story의 CSS를 선언 공급자로 사용하지 않는다. `setProperty`를 무조건 선언으로 인정하거나 미선언 HM 변수를 fallback으로 PASS 처리하지 않는다. OrgChart `current-*` 회사/부서 계산 변수는 실제 사용 범위의 CSS에 명시적 기본값을 둔다. 외부 입력 변수는 조상 상속을 막지 않도록 내부 요소에 기본 선언하지 않고 기존 `resolved-*` fallback 경로를 유지한다.
|
||||
|
||||
이 변경은 UI/Status CSS 매핑과 기존 검사 판정·exit 규칙을 유지한다. P11 검증은 기존 Guard 94개 + 별도 `guard-orgchart-theme.test.mjs` 5개 PASS이며 다른 UI/CENTER/Foundation/Pattern의 67개 기존 이슈가 동일함을 확인했다. 과거 검증 기록과 미검증 항목은 보존한다. [P11 결과](../org-chart/implementation-report-p11.md)에서 기존 TOKEN-001 5건의 원본 재현과 FAIL 0 결과를 구분한다.
|
||||
|
||||
## 별도 요구사항 액션 검사
|
||||
|
||||
`node scripts/guard-requirements.mjs <guard.json> <requirements-actions.json>`는 원본 Guard의 status/issues를 보존하고 별도 판정을 출력한다. 정책 입력 예시는 `{"schemaVersion":1,"actionPolicy":{"allowedLabels":["조회","초기화"]}}`다. 실제 요구사항에서 허용한 소비 화면의 Button/button/link 이름만 작성하며, Component 기본 동작을 검사하는 경우 그 이름도 명시한다. 허용하지 않은 정적 이름(예: 미요청 CSV 다운로드)은 REQ-003 FAIL, 동적/복합 이름·spread props·오래된 source/contract는 NOT VERIFIED다.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# HMDesign OrgChart — 개발 전 설계 기준
|
||||
|
||||
> 현재 P1 기준(2026-10-12): 공통 HM Theme와 분리된 독립 디자인을 사용한다. [개별 컴포넌트·스타일 API](standalone-components-p1.md)와 [구현·검증 결과](implementation-report-p1.md)를 따른다. 아래 초기 설계와 v0.1 보고서는 과거 기록이며 Theme 필수 연결·reserved hub/status는 최신 사용법이 아니다.
|
||||
|
||||
상태: **문서 초안 / 사용자 검토 대기 / 구현 미착수**. 기준일: 2026-10-12(KST).
|
||||
|
||||
확정 요구사항은 이번 사용자 요청에서 확정된 범위다. `제안`은 승인 전 설계 후보이며, `TBD`는 정책 또는 기술 검토가 필요한 항목이다. 문서 검증의 PASS는 실행 기능의 PASS를 의미하지 않는다.
|
||||
|
||||
## 1. 목적과 문서 읽기 순서
|
||||
|
||||
여러 업무 화면의 콘텐츠 컨테이너에서 재사용할 Framework-independent OrgChart를 설계한다. 1차 공식 개발 대상은 **Vanilla HTML/JavaScript와 React 두 환경**이다. 표준 DOM API와 JavaScript/TypeScript 기반 Core를 우선하고 React는 별도 Wrapper로 연결한다. Core는 framework runtime과 HM React Component를 직접 import하지 않는다. 특정 Dashboard, Header 높이 또는 Navigation에 종속시키지 않는다. JSON과 향후 REST API가 같은 계약을 제공하며 각 환경의 공개 사용 방식은 유지해야 한다.
|
||||
|
||||
1. 이 문서: 확정 범위와 기존 기준과의 차이.
|
||||
2. [Architecture](architecture.md): 계층별 책임, 의존성, 공개 사용 방식 초안.
|
||||
3. [Data Contract](data-contract.md): 정규화 계약, TypeScript·JSON Schema 초안, 검증·집계.
|
||||
4. [UI Composition](ui-composition.md): 콘텐츠 구성, HM 재사용, 컨테이너·접근성.
|
||||
5. [Integration](integration.md): 엔진 공식 근거, React·Theme 연동, JSON → API 교체.
|
||||
6. [Implementation Plan](implementation-plan.md): 단계별 완료 조건, Storybook, TBD·리스크, 이번 문서 검증 기록.
|
||||
7. [Framework Integration](framework-integration.md): Vanilla·React 사용 계약과 예시, 생명주기, CSS·Theme·배포 검증, 미지원 환경.
|
||||
|
||||
## 2. 작업 환경 확인
|
||||
|
||||
읽기 전용 확인 결과, 다음 환경이 존재한다.
|
||||
|
||||
- 실제 Git Root·작업 경로: `D:\디자인시스템\HMDesign-OrgChart`.
|
||||
- 현재 브랜치: `feature/orgchart-package`.
|
||||
- 시작 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`.
|
||||
- 공식 origin(fetch/push): `https://gitea.hmac.kr/b25027/HMDesign.git`.
|
||||
- 운영 Worktree: `D:\디자인시스템\HMDesign`의 `main`. 기존 Worktree를 재사용하며 새 Worktree는 생성하지 않았다.
|
||||
- 시작 시 Local `main`, Feature, 두 remote-tracking ref와 `git ls-remote`의 원격 두 브랜치 SHA가 모두 위 HEAD와 일치했다. `main...feature/orgchart-package`의 좌/우 독자 Commit 수는 `0 / 0`이다.
|
||||
- 시작 시 개발·운영 Working Tree는 모두 깨끗하고 staged 변경도 없었다.
|
||||
- 루트 [package.json](../../package.json)의 npm workspaces는 `packages/*`; 현재 패키지는 `@hm-design/ui`, `@hm-design/center-graphic`이다. OrgChart 패키지는 아직 없다.
|
||||
- 루트 React/React DOM은 `19.3.0`, Storybook은 `10.6.1`, Tailwind는 `4.3.3`이다. 이는 저장소 manifest 확인이며 설치 상태·호환성 검증 결과가 아니다.
|
||||
- 기존 문서는 `docs/components`, `docs/patterns`, `docs/guidelines`, `docs/reports`, `docs/center-graphic`으로 구분된다. 독립 도메인인 [Center Graphic](../center-graphic/README.md)의 선례에 맞춰 `docs/org-chart`를 신규 설계 문서 위치로 선택했다.
|
||||
|
||||
이번 작업은 이 디렉터리의 Markdown 신규 작성만 허용한다. 구현, 설치, package/lockfile·Theme·Component·Pattern·Storybook 변경, Stage·Commit·Push·Merge는 수행하지 않는다. 운영 프로젝트와 `main`도 변경하지 않는다.
|
||||
|
||||
## 3. 확정 요구사항
|
||||
|
||||
- 엔진은 `dabeng/OrgChart`를 사용한다. 채택할 정확한 버전·배포 모듈 방식은 기술 검증 후 확정한다. [공식 데모](https://dabeng.github.io/OrgChart/), [공식 저장소](https://github.com/dabeng/OrgChart).
|
||||
- 포함 콘텐츠: 제목·설명, 최상위 리더, 중앙 허브, 부문·실·Div, 셀·팀, 직원 프로필, 조직 계층·연결선, 조직 간 추가 관계, 팀별 인원, 파견·휴직 그룹, 관계사별 인원, 전체 인원 집계.
|
||||
- 제외: Header, GNB, LNB, Dashboard Header, 페이지 전체 Layout, Routing, 신규 페이지 등록, 전역 Navigation, 상위 앱 인증 처리.
|
||||
- 최신 추가 요청을 우선하여 Core 노드는 Framework-independent HTML/CSS로 표현하고 HM Theme를 CSS Custom Properties로 연결한다. 공개 React Component는 React 호스트/Adapter의 적합한 영역에서 재사용을 검토하고 Core에는 직접 import하지 않는다. 기존 Color/Semantic Token·Typography·Spacing·Radius 기준을 참조하되 기존 Theme·Component·Pattern이나 공통 디자인 규칙은 변경하지 않는다.
|
||||
- 조직·직원·관계사·추가 관계·표현 설정·Metadata를 ID 기반으로 분리한다. 엔진용 중첩 트리는 ViewModel에서 파생하며 원본 계약이 아니다.
|
||||
- Data Source → Data Validation → Normalized Data Contract → ViewModel / Adapter → OrgChart UI를 유지한다. JSON Mock, 외부 JSON, 향후 REST API 공급 방식은 같은 계약을 제공한다.
|
||||
- 조직·직원·관계·표현 설정 변경은 가능한 한 데이터만 바꿔 반영한다. 집계는 직원 원본에서 계산한다. 미확정 집계 정책을 코드 기본값으로 숨기지 않는다.
|
||||
- Validation 실패는 제어된 Error 상태로 전환하고 화면 전체 비정상 종료를 방지한다.
|
||||
- 검토 너비는 390/768/1280/1440px이다. 컨테이너 기준으로 검토하고 모바일 전략은 기술 검토 후 확정한다.
|
||||
- Storybook은 OrgChart 콘텐츠 단독·가상 데이터 검증용으로 설계한다. Header/Dashboard 조합 Story는 제외한다.
|
||||
- 실제 API 명세가 없으므로 Endpoint·인증 방식·권한·캐시·재시도 정책은 TBD다. 실제 인물·개인정보는 재사용하지 않는다.
|
||||
- 1차 공식 개발 대상은 Vanilla HTML/JavaScript와 React다. Vue·Angular·기타 framework는 이번 개발 범위에서 제외하고 Core의 향후 확장 가능성만 유지한다. 공식 개발 대상 지정과 실제 지원 완료는 구별하며 현재 실행 지원은 모두 NOT VERIFIED다.
|
||||
- Core는 엔진·데이터·Validation·Data Source Adapter·집계·Mount/Update/Reload/Destroy를 공통 관리한다. npm·ESM·브라우저 번들·별도 CSS·타입 선언·Framework Adapter 배포를 검증한다. Theme 미연결 정책은 TBD다.
|
||||
- 두 환경은 동일 JSON 계약·Data Source Interface·Validation·집계·노드 UI·HM Theme CSS Variables·차트 인터랙션을 사용한다. React Wrapper에 별도 엔진/노드 디자인/업무 로직을 중복 구현하지 않는다.
|
||||
|
||||
## 4. 제안과 자료 한계
|
||||
|
||||
- 패키지 경로 `packages/org-chart`, 이름 `@hm-design/org-chart`는 **제안 유지**다. `packages/*`와 양립 가능함을 확인했지만 이름·공개 Export·의존성 승인이나 생성은 하지 않았다.
|
||||
- 최신 확정 요청의 우선 검토안은 `@hm-design/org-chart` Core + `@hm-design/org-chart-react` Wrapper 두 패키지다. 실제 분리는 기존 build 연결과 의존성 검증 후 최종 결정한다.
|
||||
- 문서의 함수·Interface·Schema·폴더·Story 이름은 구현 초안이다. 현재 공개 API처럼 import하거나 사용하지 않는다.
|
||||
- Workdesk 조직도 캡처를 시각 레퍼런스로 사용하는 요구는 확정이다. 그러나 이번 첨부에는 작업 지시 텍스트만 있고 캡처 파일은 없다. 콘텐츠 8영역은 사용자 설명에서 도출했다. 실제 카드 크기·색상·곡선·배치·시각적 일치는 **NOT VERIFIED**이며 추정으로 확정하지 않는다.
|
||||
- 가상 직원은 `가상직원 가`처럼 예시임을 식별할 수 있는 명칭을 사용한다. 사진은 null/fallback 또는 승인된 가상 asset만 사용한다.
|
||||
|
||||
## 5. 기존 지침과 적용 차이
|
||||
|
||||
기존 기준은 [AGENTS.md](../../AGENTS.md), [화면 선택·완료 계약](../guidelines/ai-screen-contract.md), [공개 인덱스](../guidelines/component-pattern-reference.md), [현재 추가 수정 기준](../guidelines/publisher-additional-review-2026-10-10.md), [Runtime Theme](../guidelines/runtime-theme.md)를 참조한다. 공통 기준 전체를 복제하지 않는다.
|
||||
|
||||
- **경로·브랜치·Git 정책 충돌**: 기존 AGENTS는 `HMDesign-Gitea-Transfer/main` 수정과 검증 후 Commit/Push를 요구한다. 이번 요청은 `HMDesign-OrgChart/feature/orgchart-package`의 문서 전용, 운영 `main` 보존, Commit/Push 금지다. 이번 명시적 요청을 우선 적용하며 기존 AGENTS는 수정하지 않는다. 이 예외를 향후 구현·릴리스 권한으로 일반화하지 않는다.
|
||||
- **Storybook 범위 해석**: 기존 규칙은 신규 업무 전체 화면 등록과 대표 Story 교체를 금지한다. 이번 요청은 신규 패키지의 콘텐츠 단독 Story 설계다. 이를 컴포넌트/도메인 검증으로 분리하며 전체 업무 화면을 등록하지 않는다. 미래 코드 등록 전에 분류·경로를 승인받는다.
|
||||
- **Navigation·Dashboard 규칙 적용 범위**: 페이지 소비자는 기존 단일 Navigation 규칙을 따른다. 콘텐츠 패키지가 모드를 선택하거나 Dashboard 48px Header·960px 최소 폭을 내장하지 않는다. 상위 컨테이너 제약은 소비자가 제공한다.
|
||||
- **Guard 범위**: 이번 산출물은 Markdown만 있고 React target/bootstrap이 없다. 화면 생성 Guard를 억지로 실행하거나 PASS를 만들지 않는다. 미래 UI 검증의 적합한 target과 범위는 [Implementation Plan](implementation-plan.md)에서 별도로 다룬다.
|
||||
- **CENTER 선례의 차이**: OrgChart Core의 framework 독립성과 HM CSS Token 재사용을 함께 유지한다. CENTER의 전용 Token 정책을 복사하지 않는다.
|
||||
- **Framework 요구와 기존 React 원칙 충돌**: 기존 AGENTS의 신규 React 화면·HM Component 렌더링 원칙은 최신 추가 요청의 Core 독립성·HTML/CSS 노드 조건에 맞춰 제한한다. Core 표준 DOM 표현을 이번 OrgChart 전용 예외로 기록하며 기존 지침을 자동 수정하지 않는다. React 업무 화면 소비자는 적합한 HM 공개 Component를 계속 사용한다. Vanilla 배포는 기존 Runtime Portability 계약과 함께 검증한다.
|
||||
|
||||
## 6. 확정 요구사항 추적
|
||||
|
||||
- A 프로젝트 목적·패키지 제안 → 이 문서, Architecture §1.
|
||||
- B 콘텐츠 범위·페이지 제외 → 이 문서 §3, UI Composition §1.
|
||||
- C HM 재사용 → UI Composition §2, Integration §3.
|
||||
- D 공급 계층 → Architecture §2, Integration §4.
|
||||
- E 데이터 모델 → Data Contract §1–3.
|
||||
- F 데이터 변경·유지보수 → Architecture §4, Integration §4.
|
||||
- G Validation → Data Contract §4.
|
||||
- H 인원 집계 → Data Contract §5.
|
||||
- I 엔진 기술 검토 → Integration §1–2.
|
||||
- J 화면 8영역·가상 데이터 → UI Composition §1, Data Contract §3.
|
||||
- K 반응형·컨테이너 → UI Composition §3.
|
||||
- L 콘텐츠 Storybook → Implementation Plan §2.
|
||||
- M API 전환 점검 → Integration §4–5.
|
||||
- 추가 Framework-independent 요구 → Architecture 전체, Framework Integration 전체, Implementation Plan §1–3.
|
||||
|
||||
문서화 다음 단계는 사용자 검토·승인이다. 이 문서 작성만으로 개발, 설치 또는 Commit/Push가 승인되지는 않는다.
|
||||
@@ -0,0 +1,166 @@
|
||||
# HMDesign — AI 조직도 페이지 자동 생성 공통 정책
|
||||
|
||||
적용 대상은 Codex·Claude 등 AI 코딩 에이전트가 자연어 또는 제공 데이터에서 **새로 생성하는 삽입용 조직도 컴포넌트 또는 조직도 전체 페이지**다. 회사, 조직 유형, 계층, Depth, 인원수에 종속되지 않는다. **고정형(Fixed)과 확장형(Auto/Expanded) 모두 동일한 생성 방식 선택 원칙을 적용한다.** 기존 Full Composition·Expanded OrgChart·Department Colors·Overview의 승인된 화면은 변경하지 않으며, 기존 P9.5·P-AI1 결과에도 소급 적용하지 않는다.
|
||||
|
||||
## 1. 실행 방식과 적용 우선순위
|
||||
|
||||
- 사용자가 코딩 에이전트에 요청을 전달하면 에이전트가 조직 JSON과 실제 페이지 코드를 생성한다. 웹페이지에 AI 채팅창이나 생성 입력 UI를 추가하지 않는다. 브라우저 AI Provider 연결은 이 실행 방식의 전제가 아니다.
|
||||
- 사용자 요청에서 명시한 UI 요구사항 → 이 정책의 AI 조직도 전용 범위 → 기존 OrgChart Core의 디자인·동작 규칙 순으로 적용한다. 충돌하지 않는 AGENTS.md·CLAUDE.md와 HM 공개 계약·데이터 검증·접근성 규칙은 계속 준수한다. 관련 문서는 필요한 부분만 읽는다.
|
||||
- 생성 방식은 아래 사전 선택 절차로 확정한다. 미지정 요청을 Chart Only 또는 Full Page로 임의 결정하지 않는다. Chart Only에는 일반 업무 화면의 GNB/LNB/DASHBOARD Navigation 의무를 가져와 메뉴나 Header를 추가하지 않는다. Full Page의 요청 UI·Navigation은 기존 HM 화면 계약을 따른다.
|
||||
- 기존 Core·React Wrapper·Theme·카드·Storybook을 요청 없이 변경하지 않는다. 필요한 공통 변경은 사유와 영향 범위를 먼저 보고한다. 새 조직도 엔진이나 디자인 시스템은 만들지 않는다.
|
||||
|
||||
### 1.1 생성 방식 사전 선택
|
||||
|
||||
Codex·Claude가 조직도 생성 요청을 받았을 때 **생성 방식이 명시되지 않았다면 코드 작성 전에 다음 질문을 먼저 하고, 사용자의 답변을 받은 뒤 구현한다.** 이 질문은 에이전트 대화에서 하며 웹페이지에 생성 방식 선택 UI를 추가하지 않는다.
|
||||
|
||||
> 조직도만 기존 페이지에 삽입할까요, 아니면 레이아웃까지 구성한 새 페이지 전체를 생성할까요?
|
||||
|
||||
**① 조직도만 생성 (Chart Only)**
|
||||
|
||||
- 기존 페이지에 삽입 가능한 독립 조직도 컴포넌트를 생성한다.
|
||||
- 흰색 캔버스 및 조직도 노드·연결선만 표시한다.
|
||||
- 최초 Fit to View, 마우스 휠 확대·축소 및 Pan을 적용한다.
|
||||
- 외부 제목·통계·설명·조작 패널을 생성하지 않는다. 상세 구성은 §6의 Chart Only 규칙을 따른다.
|
||||
|
||||
**② 새 페이지 전체 생성 (Full Page)**
|
||||
|
||||
- 새로운 페이지의 레이아웃까지 설계하고 화면 전체의 배치 및 반응형 구성을 구현한다.
|
||||
- 기존 HMDesign UI 컴포넌트를 우선 재사용한다.
|
||||
- 사용자가 요청한 제목·정보·필터·컨트롤 등을 반영한다. 선택 자체를 요청하지 않은 부가 기능의 승인으로 확대하지 않는다.
|
||||
- 기존 OrgChart Core·React Wrapper로 조직도를 표시한다. 상세 구성은 §6.4를 따른다.
|
||||
|
||||
사용자가 이미 생성 방식을 명시했다면 같은 선택 질문을 다시 하지 않고 해당 방식으로 바로 구현한다. 두 방식 모두 사용자 요청의 조직 구조·인원수·데이터를 정확히 반영하고 기존 OrgChart 컴포넌트를 재사용한다. Fixed·Auto/Expanded는 조직도 배치 모드이며 Chart Only·Full Page 선택을 대신하지 않는다.
|
||||
|
||||
이미 Chart Only로 확정되어 진행 중인 구현에는 이 선택 절차를 다시 적용하거나 Full Page로 확대하지 않는다. 생성 방식 사전 선택 정책을 작성한 작업은 정책·참조 안내만 다뤘으며 진행 중인 구현·기존 Storybook 4개 화면은 수정하지 않았다. 별도로 승인된 2026-10-13 Feature 통합 범위와 결과는 [최종 Feature 통합 보고서](final-integration-2026-10-13.md)를 따른다.
|
||||
|
||||
## 2. 요청을 가변 구조로 정리
|
||||
|
||||
화면 구현 전에 다음 조건을 내부 생성 계획에 정리한다. 계획·가정·검증 결과는 작업 보고나 별도 파일에 기록하며 조직도 화면에 자동 삽입하지 않는다.
|
||||
|
||||
- 회사명·회사 수·회사 ID 및 회사별 독립 트리
|
||||
- 대표이사 유무와 소속, 조직장·직원·직위 조건
|
||||
- 부문·본부·실·부서·팀·사업부 등 요청된 조직 유형과 명칭
|
||||
- 부모·자식 관계, 조직별 서로 다른 하위 조직 수와 직원 수
|
||||
- 전체 직원 수, 조직 수, Depth 및 집계 범위
|
||||
- 사용자 지정 추가 UI와 필요한 기존 탐색 기능
|
||||
|
||||
특정 중간 단계나 공통 Depth를 강제하지 않는다. 조직 수와 직원 수는 요청에서 산출하며 전용 회사 템플릿·배열 길이·고정 분배값으로 대체하지 않는다. 삼안의 6본부·17팀·500명은 테스트 사례일 뿐 기본값이나 완료 기준이 아니다.
|
||||
|
||||
다음 요청 형태를 모두 같은 해석·변환 절차로 처리한다. 실제 렌더링 가능 여부는 사용 중인 Core의 공개 계약을 확인한다.
|
||||
|
||||
- 회사 → 본부 → 팀 → 직원
|
||||
- 회사 → 부문 → 본부 → 부서 → 팀 → 직원
|
||||
- 대표이사 → 실 → 부서 → 직원
|
||||
- 회사 → 사업부 → 직원
|
||||
- 여러 회사 → 회사마다 다른 조직 계층과 Depth → 직원
|
||||
|
||||
Depth에 회사나 직원 카드를 포함하는지, 조직별 인원이 직접 소속인지 하위 조직 포함인지 구분한다. 의미가 불분명해 구조나 총원이 달라지는 경우 해당 항목만 질문한다. 내부 렌더러의 0부터 시작하는 인덱스를 사용자 Depth와 혼동하지 않는다.
|
||||
|
||||
## 3. 자연어에서 가상 데이터 생성
|
||||
|
||||
- 명시된 회사·조직명·관계·유형·조직 수·직원 수·직위·대표이사 유무를 정확히 반영한다. 합계가 충돌하거나 관계를 결정할 수 없으면 생성 전에 필요한 조건만 질문한다.
|
||||
- 미지정 가상 명칭과 직원 분배는 요청 범위 안에서 합리적으로 결정할 수 있다. 선택한 값과 근거를 화면 밖에서 보고한다. 이전 테스트의 회사·Depth·팀 수·직원 수를 재사용하지 않는다.
|
||||
- 요청하지 않은 대표이사, 중간 조직, 조직장용 추가 직원을 템플릿에서 자동 삽입하지 않는다. 대표이사가 직원 카드인지 조직 노드인지 구분하며 직원과 조직을 중복 생성하지 않는다.
|
||||
- 전체 직원 수에는 별도 지시가 없는 한 요청된 대표이사·조직장을 포함한다. 직원 레코드는 한 번만 생성하고 조직장 참조를 연결한다. 조직별 직접 인원과 하위 조직 포함 인원을 분리해 집계한다.
|
||||
- 직원 ID는 전체 데이터에서 고유하게 생성한다. 회사·조직 ID도 트리 간 충돌하지 않아야 한다. 입력 순서가 달라져도 같은 원본 항목의 내부 ID가 불필요하게 바뀌지 않도록 한다.
|
||||
- 가상 데이터와 적용 가정을 사용자에게 명확히 보고한다. 화면 배너나 개발 안내는 기본적으로 추가하지 않고, 명시적으로 요청된 표시만 구현한다.
|
||||
|
||||
## 4. JSON·CSV·Excel 제공 데이터 변환
|
||||
|
||||
원본 구조와 값을 우선한다. 원본 파일·파싱 결과와 Contract 변환 결과를 분리해 관리한다.
|
||||
|
||||
- 직원·조직을 임의로 추가·삭제·합치지 않는다. 직원명·조직명·직위·원본 ID·상하위 관계를 변경하지 않는다. trim, 철자 교정, 누락 노드 합성, 임의 인원 분배를 적용하지 않는다.
|
||||
- 행 수와 실제 조직·직원 수를 구분한다. 반복된 조직 정보가 같은 조직인지 원본 ID와 관계로 확인한다. 동명이인을 같은 직원으로 취급하거나 같은 이름의 조직을 자동 합치지 않는다.
|
||||
- ID가 없는 항목에 변환용 내부 ID만 별도로 생성할 수 있다. 원본 필드와의 대응을 남긴다. 항목을 구별할 수 없으면 추가 식별 정보를 요청한다.
|
||||
- 필요한 필드·컬럼 매핑을 확인한다. 원본에 없는 근무 상태·관계사·조직 유형·메타데이터를 실제 정보처럼 채우지 않는다. Contract 필수값이 없으면 사용자에게 확인하고, 명시적으로 허용된 변환 기본값은 원본과 구분해 기록한다.
|
||||
- 중복 ID, 누락 조직·직원, 잘못된 부모·소속·조직장 참조, 순환 관계, 회사 간 계층 연결, 원본과 변환 결과의 조직 수·직원 수를 검사한다. 오류는 원본 행·셀 또는 변환 경로와 수정할 항목으로 표시한다.
|
||||
- Excel의 선택 시트와 컬럼 범위를 확인한다. 여러 시트를 임의로 합치거나 제외하지 않는다. 수식·날짜·서식 있는 숫자·앞자리 0·인코딩을 보존할 수 없으면 지원 한계를 알리고 필요한 입력 형태를 요청한다.
|
||||
- Contract에 없는 원본 정보는 별도 원본 자료에 보존한다. 미지원 필드를 Contract에 넣거나 원본을 조용히 버리지 않는다. 오류 데이터의 부분 렌더링은 사용자가 요청하고 안전하게 구분할 수 있을 때만 허용한다.
|
||||
|
||||
## 5. 현재 Core Contract와 공개 API 확인
|
||||
|
||||
관련 안내는 [데이터 계약](data-contract.md), [Expanded 사용 안내](expanded-usage.md)와 실제 작업 기준 커밋의 패키지 exports·타입·Validator다. 문서의 초안이나 과거 검증을 현재 지원 증거로 사용하지 않는다.
|
||||
|
||||
- `@hm-design/org-chart`의 Organization·Employee, Validator·DataSource와 `@hm-design/org-chart-react`의 공개 OrgChart를 우선 재사용한다. 공개 CSS export와 실제 React bootstrap을 연결한다. 내부 src/dist 경로를 추측하거나 기존 Core를 복제하지 않는다.
|
||||
- 업무상 조직 유형과 Contract의 기계적 type 값을 구분한다. 사용자 조직명은 유지하고 확인된 대응을 기록한다. 새 enum을 임의로 넣거나 맞지 않는 유형으로 조용히 치환하지 않는다.
|
||||
- 회사별 독립 트리와 회사 ID·컬러 매핑을 확인한다. 특정 회사나 회사 수를 기본값으로 고정하지 않는다. 미등록 회사를 기존 회사 ID에 임의 연결하지 않는다. 필요한 프리셋이나 공개 확장 경로가 없으면 제약과 필요한 결정을 보고한다.
|
||||
- 대표이사는 공개 조직장·상단 직원 표현 API로 연결한다. 기술적 루트가 필요한 경우 업무 조직과의 대응 및 집계 범위를 확인한다. 실제 제공 데이터에 가짜 회사·대표이사·조직을 추가하지 않는다.
|
||||
- Core의 Validator를 통과한 데이터만 DataSource와 렌더러에 연결한다. 별도 사전 검사는 가능하지만 기존 Validator를 우회하거나 실패 항목을 삭제해 통과시키지 않는다.
|
||||
- 레이아웃·Depth·대표이사 표현·다중 회사·TB/LR/Hybrid·Zoom/Pan·접기·팝업은 실제 공개 API가 지원하는 범위만 사용한다. 직원 연결 방향을 전체 트리 방향 지원으로 해석하지 않는다.
|
||||
- 요청 구조나 최소 화면을 공개 API로 정확히 표현할 수 없으면 지원 한계와 대안을 보고한다. 임의 구현·합성 화면을 기존 Core 결과라고 주장하지 않는다. 공통 기능 확장이 필요하면 별도 승인 범위로 분리한다.
|
||||
|
||||
## 6. 선택한 생성 방식의 화면 구성
|
||||
|
||||
### 6.1 기본 출력과 제외 요소
|
||||
|
||||
Chart Only 결과물은 **White Canvas + OrgChart Nodes + Connectors**인 독립 조직도 컴포넌트다. 기존 페이지에 삽입할 수 있으며, 독립 미리보기에서는 흰색 조직도 영역을 최상위 화면으로 사용한다.
|
||||
|
||||
- 회사·조직 그룹, 요청에 포함된 대표자·본부장·팀장·직원 카드, 연결선과 필요한 화살표, 회사별 컬러를 표시한다. 조직명·직위·직원 이름 등 조직도를 구성하는 필수 텍스트는 유지한다.
|
||||
- 해당 모드에서 지원되는 기존 Core의 그룹 접기·펼치기, Zoom/Pan·직원 정보 보기 등 조직도 내부 탐색 기능은 유지한다. 외부 조작 UI의 기본 제외를 Core 기능 삭제로 해석하지 않는다.
|
||||
- 외부 제목·통계·설명·조작 패널은 Chart Only 컴포넌트에 생성하지 않는다. 새 페이지 레이아웃과 외부 UI 생성은 Full Page 범위로 구분하며, 한 요소의 요청을 다른 부가 UI의 승인으로 확대하지 않는다. 삽입 대상 기존 페이지의 UI를 삭제하거나 변경하지 않는다.
|
||||
|
||||
기본 제외 요소:
|
||||
|
||||
- 페이지 제목(예: 가상 조직도), 기준일, 회사 수·전체 인원 요약 문구
|
||||
- 확대·축소·100% 복원·Fit to View 버튼 바 등 외부 컨트롤
|
||||
- Zoom 배율 안내, Drag & Drop ON/OFF 상태 문구, 조작 방법 안내
|
||||
- 설명 문단, 생성 조건 설명, 사용 설명·안내 패널
|
||||
- JSON 미리보기, 테스트 결과·디버그 패널, 별도 통계 대시보드, 개발 안내
|
||||
- 외부 회색 배경, 불필요한 Page Header·Wrapper·바깥 여백
|
||||
- AI 채팅·생성 입력 UI, Navigation·추가 메뉴
|
||||
|
||||
저장·게시·다운로드·운영 API는 요청 범위 밖에서 추가하지 않는다. 테스트 결과·가정·제약은 조직도 화면 대신 작업 완료 보고서에 기록한다.
|
||||
|
||||
### 6.2 흰색 Root 캔버스와 초기 배치
|
||||
|
||||
- 캔버스는 Chart Only 컴포넌트의 Root 영역을 차지하며 배경은 흰색이다. 독립 미리보기에서는 화면의 Root로 사용하고, 기존 페이지 삽입 시에는 호스트가 제공한 영역 안에 배치한다. 호스트 페이지의 배경·레이아웃을 덮어쓰지 않는다. 외부 회색 영역이나 불필요한 바깥 여백을 생성하지 않으며 사용자가 지정한 캔버스 크기가 있으면 이를 우선한다.
|
||||
- 초기 배치는 조직도 전체의 실제 Bounds와 캔버스 크기를 기준으로 가로·세로 중앙 정렬한다. 노드·그룹·연결선을 포함한 전체 범위와 위치를 확인하며 임의 고정 좌표·배율을 회사별 템플릿으로 만들지 않는다.
|
||||
- 최초 Fit to View로 조직도 전체의 실제 Bounds를 캔버스에 맞춘다. 대규모 조직도에서는 초기 전체 개요와 확대 시 텍스트 읽기, 내부 이동, 외부 가로 넘침을 함께 확인한다. 공개 API의 배율 한계로 전체 Fit이 불가능하면 제약을 보고한다.
|
||||
- 기존 Zoom/Pan을 재사용해 마우스 휠 확대·축소 및 Pan을 제공하고 내부 이벤트 기능을 유지한다. 초기 Fit은 배치 동작이며 Fit to View 버튼 바를 렌더링해야 한다는 뜻이 아니다. Chart Only에는 외부 컨트롤 버튼을 생성하지 않는다.
|
||||
- 기존 카드·회사색·연결선·직원 팝업 디자인과 Theme를 유지한다. 캔버스 배경은 실제 선언된 공개 Theme·스타일 경로를 확인해 적용하며 없는 Token을 추측하지 않는다.
|
||||
- 지원되는 공개 옵션으로 불필요한 내장 설명·요약·컨트롤을 제외한다. 옵션이 없거나 공개 API로 흰색 Root·중앙 배치를 구현할 수 없으면 제약을 보고한다. Shadow DOM 임의 조작, 공통 CSS 변경, Core·React Wrapper의 불필요한 공개 API 변경으로 해결하지 않는다.
|
||||
- 최소 배율, 많은 직원, 긴 명칭, 깊은 계층, 여러 회사에서 전체 Fit과 가독성을 함께 만족할 수 없으면 잘림·읽기·탐색 한계를 보고한다. 이름·인원·Depth를 줄이거나 단순히 내용을 숨겨 잘림·레이아웃 문제를 감추지 않는다.
|
||||
|
||||
기본 구성은 다음과 같다.
|
||||
|
||||
- 기존 구성: Page Background → Header → Controls → Description → White Canvas → OrgChart
|
||||
- Chart Only 구성: **White Canvas → OrgChart**
|
||||
|
||||
### 6.3 Fixed / Auto 공통 적용과 기존 화면 보호
|
||||
|
||||
- **Fixed:** 승인된 기존 카드·연결선 디자인과 해당 모드의 기존 배치를 재사용하고, 선택한 Chart Only 또는 Full Page 범위를 적용한다. 현재 공개 Chart Only 옵션은 Auto 전용이며 Fixed와 함께 쓰면 거부된다. Fixed Chart Only가 필요하면 제약으로 보고하고 별도 승인 범위로 분리한다.
|
||||
- **Auto / Expanded:** 기존 Core 자동 배치를 재사용하고, Chart Only에서는 조직도 외부의 설명·테스트·조작 패널을 제외한다. Full Page에서는 요청된 외부 UI만 구성한다.
|
||||
- 두 배치 모드 모두 가변 계층·정확한 인원·제공 데이터 보존 규칙을 유지한다. 배치 모드가 다르다는 이유로 추가 UI를 허용하지 않는다. 요청 데이터가 달라지면 계층과 렌더링도 그 데이터에서 달라져야 한다.
|
||||
- Chart Only는 삽입용 독립 컴포넌트로, Full Page와 독립 미리보기는 독립 React entry로 제공한다. 기존 Full Composition·Expanded OrgChart·Department Colors·Overview·Storybook 메뉴·P9.5·P-AI1 결과를 교체하지 않으며 기존 Storybook 페이지의 제목·컨트롤·설명 UI를 삭제하지 않는다.
|
||||
- Guard나 문서 분석기의 제약 때문에 요청하지 않은 Navigation·통계·설명 UI를 추가하지 않는다. 정책과 검사 도구가 충돌하면 원본 검사 결과와 원인을 보고하며 도구나 공통 계약을 이번 범위 밖에서 임의 수정하지 않는다.
|
||||
|
||||
### 6.4 Full Page 전체 페이지
|
||||
|
||||
- 기존 HMDesign Component·Pattern·Theme와 공개 API를 우선 재사용해 새 페이지의 전체 레이아웃을 구성한다. 기존 OrgChart Core·React Wrapper를 소비하며 새 조직도 엔진이나 카드 디자인을 만들지 않는다.
|
||||
- 사용자가 요청한 제목·정보·필터·컨트롤과 필수 동작을 반영한다. 요구사항에 없는 통계·설명·다운로드·외부 API·추가 메뉴·AI 채팅·생성 입력 UI는 자동으로 추가하지 않는다.
|
||||
- 조직도 캔버스와 요청 UI의 배치·크기·스크롤·반응형 동작을 함께 설계한다. Chart Only의 흰색 전체 Root·외부 UI 제외 규칙을 페이지 전체에 강제하지 않으며, 조직도 내부의 기존 디자인·탐색 기능은 유지한다.
|
||||
- 일반 업무 화면의 HM 컴포넌트 선택·Navigation·Theme·React bootstrap·접근성·Guard 계약은 AGENTS.md의 필수 규칙을 따른다. Full Page 선택만으로 기존 Storybook 화면 수정이나 공통 Component·Pattern·Theme 변경을 허용하지 않는다.
|
||||
|
||||
## 7. 생성 절차와 최종 통합 검증
|
||||
|
||||
> Chart Only 구현(2026-10-13): Chart Only를 선택한 Auto 조직도는 공개 옵션 `layoutMode: 'auto'` + `presentationMode: 'chart-only'`로 생성한다. 초기 Fit·가로/세로 중앙 배치(`initialView: 'fit'`)와 Ctrl/⌘ 없는 커서 기준 휠 Zoom(`wheelZoom: 'direct'`)이 기본 적용되며 React·Vanilla가 같다. 외부 제목·요약·버튼·안내를 따로 만들지 않고 Shadow DOM을 조작하지 않는다. Fixed는 이 옵션을 거부하므로(TypeError) Fixed 요청을 임의로 Auto로 바꾸지 않고 Chart Only 미지원 제약으로 보고한다. 생성 방식의 사전 선택과 Full Page 범위는 계속 §1.1을 따른다. 사용법은 [공통 사용 가이드](org-chart-usage-guide.md#chart-only-공개-api-auto)를 따른다. 아래 P10.2 문단은 구현 전 기록이다.
|
||||
>
|
||||
> 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. 코드 작성 전에 §1.1에 따라 생성 방식을 확인한다. 미지정이면 정해진 선택 질문에 대한 답변을 받은 뒤 진행하고, 이미 명시된 방식은 다시 묻지 않는다. 현재 작업 경로·Git 상태·기준 커밋·공개 API를 확인하고 기존 미커밋 변경과 병렬 작업, 서버 포트를 보호한다.
|
||||
2. 회사별 계층·유형·Depth·조직 수·직원 수·조직장 집계 조건을 정리한다. 충돌하는 필수 조건만 확인한다.
|
||||
3. 요청에서 가상 데이터를 생성하거나 제공 데이터를 보존해 변환한다. 계획과 변환 대응은 화면 밖에 기록한다.
|
||||
4. 기존 Contract와 추가 정합성 검사를 통과시킨다. 이는 잘못된 데이터를 렌더러에 넘기지 않기 위한 필요한 사전 확인이다.
|
||||
5. 선택한 방식으로 구현한다. Chart Only는 흰색 캔버스·노드·연결선의 삽입용 컴포넌트와 최초 Fit·휠 Zoom·Pan을 제공한다. Full Page는 기존 HM UI와 OrgChart Core·React Wrapper로 요청 UI·전체 배치·반응형 페이지를 구성한다. 새로운 구조를 고정된 사례로 바꾸지 않는다.
|
||||
6. 구현 후 변경 범위의 코드·diff·계층·인원·중복 집계·보존·핵심 동작을 한 번에 검증한다. 필요한 Typecheck·Build·Guard를 수행하고 실제 브라우저에서 확인한다. 이미 성공한 동일 조건 검사를 반복하지 않고 수정·실패 범위만 다시 확인한다.
|
||||
7. 390/768/1280/1440px와 사용자 지정 캔버스 크기에서 선택한 방식의 화면을 확인한다. Chart Only는 흰색 컴포넌트 Root·실제 Bounds 기준 최초 Fit과 중앙 배치·외부 UI 제외·호스트 삽입을, Full Page는 요청 UI·전체 배치·반응형·Navigation을 확인한다. 공통으로 배율·가독성·콘텐츠/내부 스크롤·줄바꿈·카드·연결선·회사색·Zoom/Pan 내부 이벤트·접기·팝업·키보드·포커스를 관련 사용 요소에 맞게 확인한다. 실제 실행 URL과 화면 캡처를 보고한다.
|
||||
8. 선택한 생성 방식, 요청 대비 회사·조직·Depth·직원·조직장 집계, 생성 파일, 재사용 API, 가정, 제약, PASS/PARTIAL/FAIL와 NOT VERIFIED를 간결하게 보고한다. Guard 원본·정적·Runtime·STYLE 디자인 의도/최종 상태를 구분하고 미검증을 덮어쓰지 않는다.
|
||||
|
||||
정책 작성·연결만 수행하는 작업에는 화면 구현·빌드·브라우저 실행·전체 회귀를 필수로 추가하지 않는다. 독립 실험 환경에서 선택적으로 화면을 검증할 수 있으나 기존 Storybook·AI Playground 테스트 페이지는 수정하지 않는다. 화면 검증을 수행하지 않았다면 **VISUAL NOT VERIFIED**로 보고한다. 데이터 생성·렌더링 기능을 실행하지 않았다면 정책 작성 완료를 범용 런타임 지원 PASS로 보고하지 않는다.
|
||||
|
||||
## 8. 범위 보호와 완료 기준
|
||||
|
||||
- 기존 P9.5·P-AI1 산출물, P10 PARTIAL과 당시 Guard FAIL 10건의 검증 기록을 보존한다. 기존 P10.1 작업 폴더와 AI Playground Worktree를 수정하지 않으며 삼안 500명 테스트 페이지를 이관하지 않는다. 사전 선택 정책의 작성·연결 작업은 이 파일과 AGENTS.md·CLAUDE.md의 참조 안내만 다뤘으며 화면·데이터·Core 변경을 포함하지 않았다. 이후 2026-10-13 Feature 통합에는 Chart Only·초기 Fit·직접 Wheel Zoom, GuardFix 해결 5건, 생성 방식 사전 선택 정책만 포함한다. GuardFix는 F3–F7을 해결하며 잔여 FAIL 5건·NOT VERIFIED 9건과 원본 상태는 [최종 Feature 통합 보고서](final-integration-2026-10-13.md)에 구분해 기록한다. Cell 디자인·미리보기는 통합 대상에서 제외한다.
|
||||
- 이 정책을 참조한 생성 요청만으로 Stage·Commit·Push·Merge를 승인받은 것으로 간주하지 않는다. 현재 사용자의 작업별 Git 지시를 우선한다. 사전 선택 정책의 작성 작업에서는 Stage·Commit·Push·Merge를 진행하지 않았으며, 이후 2026-10-13 통합 요청은 위 3개 작업의 변경 범위 검증 후 Feature Commit·Push를 명시적으로 승인했다. main Merge·Docker 작업·npm Registry Publish는 승인 범위에 포함되지 않는다.
|
||||
- 서로 다른 회사·대표이사 유무·조직 유형·Depth·분기 수·직원 수가 같은 요청 분석 → 데이터 변환 → Contract 검증 → Core 소비 절차로 처리돼야 한다. 특정 테스트 수치나 전용 템플릿에 의존하면 완료가 아니다.
|
||||
- 생성 작업의 완료는 명시 조건의 일치, 제공 데이터 보존, 기존 Core의 실제 렌더링, 요청 범위의 최소 UI, 실행 증거로 판단한다. 미지원 구조를 지원한 것처럼 보고하거나 분석·JSON만으로 화면 생성 성공을 선언하지 않는다.
|
||||
@@ -0,0 +1,117 @@
|
||||
# OrgChart Architecture
|
||||
|
||||
상태: 개발 전 설계. 확정 범위·환경·우선순위는 [README](README.md)를 따른다. 최신 추가 요청에 따라 Framework-independent Core를 기준으로 한다.
|
||||
|
||||
## 1. 독립 Core와 패키지 제안
|
||||
|
||||
확정: Core는 React·Vue runtime에 의존하지 않는다. 표준 DOM API와 JavaScript/TypeScript를 사용하고 dabeng/OrgChart 엔진을 Core에서 관리한다. 데이터 계약·Validation·Data Source Adapter·집계는 공통이다. Routing·로그인·Navigation·상위 Layout은 소유하지 않는다.
|
||||
|
||||
`packages/org-chart` / `@hm-design/org-chart`는 제안이다. npm workspaces `packages/*`와 양립할 수 있음을 확인했지만 생성·명칭 승인은 하지 않았다.
|
||||
|
||||
우선 검토안은 Core `@hm-design/org-chart`와 React Wrapper `@hm-design/org-chart-react` 두 패키지다. 경로 후보는 `packages/org-chart`와 `packages/org-chart-react`다. workspace·build 연결과 Core 소비에 React가 따라오지 않는 dependency·bundle 경계를 입증하고 최종 분리를 결정한다. 같은 패키지의 `/react` subpath는 우선안이 불가능할 때의 비교 항목만 남긴다. Vue·Angular·기타 framework는 이번 범위에서 제외한다. 실제 분리·경로·Export는 승인 전이다.
|
||||
|
||||
향후 책임별 구조 예시이며 이번에는 파일을 만들지 않는다.
|
||||
|
||||
```text
|
||||
packages/org-chart/ 제안: framework-independent Core
|
||||
src/
|
||||
index.ts 공개 Core facade와 type
|
||||
data/ 계약·Validation·집계(DOM도 비의존)
|
||||
sources/ JSON Mock / 외부 JSON 공급
|
||||
view-model/ ID index·파생 트리·표시 모델
|
||||
engine/ dabeng adapter·DOM·생명주기
|
||||
renderers/ 표준 DOM 노드·콘텐츠·안전한 텍스트
|
||||
styles/ 컨테이너 scope·Theme mapping·Overlay
|
||||
브라우저 entry·빌드 설정 승인 후 결정
|
||||
packages/org-chart-react/ 우선 검토안: React 연결만 담당
|
||||
src/org-chart/ 콘텐츠 단독 Story·가상 fixture 제안 위치
|
||||
```
|
||||
|
||||
Core는 `@hm-design/ui` React API·`@hm-design/center-graphic`을 import하지 않는다. HM Theme CSS는 호스트의 별도 제공 또는 승인된 standalone 배포 경로로 연결한다. Theme 때문에 React peer를 가진 UI 전체 패키지 설치를 Vanilla의 필수 조건으로 만들지 않는다. 엔진 dependency의 버전·포함 범위는 TBD다. React Wrapper만 React/React DOM peer를 검토한다.
|
||||
|
||||
루트 [build-package.mjs](../../scripts/build-package.mjs)는 현재 `ui`, `center-graphic`만 허용한다. workspace 자동 탐색만으로 신규 빌드가 완성되지 않는다. 미래 별도 build 또는 최소 기존 빌드 연결 승인이 필요하다. 배포·의존성 격리는 [Framework Integration](framework-integration.md)에서 검증한다.
|
||||
|
||||
## 2. 데이터 흐름과 계층 책임
|
||||
|
||||
```text
|
||||
JSON Mock / 외부 JSON / 미래 REST API
|
||||
↓ Data Source Adapter: 원본 필드 매핑
|
||||
동일 normalized 계약 후보(unknown)
|
||||
↓ Data Validation: 구조·참조·정책 검증
|
||||
검증 성공한 Normalized Data Contract(OrgChartData)
|
||||
↓ ViewModel / Engine Adapter
|
||||
조직별 표시 모델·집계·engine용 파생 트리·추가 relation 모델
|
||||
↓ Framework-independent Core
|
||||
DOM 콘텐츠·engine 계층 영역·Relation Overlay 후보
|
||||
↑ 동일 Core 생명주기 호출
|
||||
Vanilla 호스트 / React Wrapper (동일 Core 재사용)
|
||||
```
|
||||
|
||||
Source 매핑은 Validation 이전에 공통 계약 후보를 만든다. 검증 후 신뢰된 계약과 구별한다. TypeScript cast만으로 신뢰하지 않고 모든 Source가 같은 runtime validator를 거친다.
|
||||
|
||||
- Data Source: 데이터를 읽고 매핑한다. 네트워크·취소·향후 캐시를 담당하며 engine DOM이나 framework를 알지 못한다.
|
||||
- Validation: Schema Version·필수 값·ID·참조·순환·상태·관계·입력 안전성을 검사한다. 성공 데이터 또는 위치별 issue를 반환한다.
|
||||
- Normalized Contract: 조직·직원·관계사·관계·표현·Metadata를 분리한다. engine용 `children`·`relationship`·`className`은 저장하지 않는다.
|
||||
- ViewModel / 집계: index·하위 조직·정렬·leader·직원 그룹·집계를 순수 변환한다. 모든 환경이 같은 함수를 사용한다.
|
||||
- Engine Adapter: 복사한 파생 트리만 전달하며 DOM mount/update/dispose를 관리한다. 원본 수정·fetch·공통 UI 변경은 금지한다.
|
||||
- Core facade / DOM Renderer: 요청 경쟁, Validation, 상태, 콘텐츠 DOM, 인스턴스 ID, 생명주기·Overlay를 관리한다. 텍스트는 `textContent` 등 안전한 경로로 처리한다.
|
||||
- React Wrapper(Framework Adapter): React mount/effect/ref/cleanup과 Props를 Core에 연결한다. Validation·집계·engine·노드 디자인·인터랙션 재구현은 금지한다. 다른 framework Adapter는 이번에 설계 구체화/구현하지 않고 동일 facade의 향후 확장성만 보존한다.
|
||||
- 상위 앱: 컨테이너·Theme bootstrap·Routing·로그인/권한·API client를 소유한다. React 호스트는 적합한 HM 공개 Component 조합을 사용할 수 있다.
|
||||
|
||||
## 3. 공개 생명주기 초안
|
||||
|
||||
현재 Export가 아닌 제안이다. 데이터 type은 [Data Contract](data-contract.md)와 연결한다. `HTMLElement`는 DOM 경계에만 사용한다.
|
||||
|
||||
```ts
|
||||
interface OrgChartDataSource {
|
||||
load(context: { signal: AbortSignal }): Promise<unknown>;
|
||||
}
|
||||
type ValidationIssue = { code: string; path: string; message: string };
|
||||
type OrgChartIssue = ValidationIssue | {
|
||||
code: "SOURCE_LOAD" | "SOURCE_PARSE" | "SOURCE_MAP";
|
||||
path: "dataSource";
|
||||
message: string;
|
||||
};
|
||||
type OrgChartState =
|
||||
| { status: "loading" }
|
||||
| { status: "empty" }
|
||||
| { status: "ready"; dataVersion: string; aggregation: AggregationResult }
|
||||
| { status: "error"; issues: readonly OrgChartIssue[] };
|
||||
interface OrgChartOptions {
|
||||
dataSource: OrgChartDataSource;
|
||||
aggregationPolicy?: ApprovedAggregationPolicy;
|
||||
interaction?: { pan: boolean; zoom: boolean }; // 공개 옵션 후보, 기본값 TBD
|
||||
onStateChange?: (state: OrgChartState) => void;
|
||||
}
|
||||
interface OrgChartHandle {
|
||||
mount(container: HTMLElement): Promise<void>;
|
||||
update(input: unknown): Promise<void>;
|
||||
reload(source?: OrgChartDataSource): Promise<void>;
|
||||
destroy(): void;
|
||||
}
|
||||
declare function createOrgChart(options: OrgChartOptions): OrgChartHandle;
|
||||
```
|
||||
|
||||
`update`는 normalized 스냅샷 후보를 검증·반영하고 `reload`는 현재 또는 새 Source를 읽는다. 두 동작은 동일 Validation·집계·render 경로를 사용한다. 데이터 실패는 state callback으로 전달해 페이지 종료를 방지한다. 호출 순서·Promise·정리 규칙은 [Framework Integration](framework-integration.md)을 따른다.
|
||||
|
||||
구조·참조·cycle·보안의 fatal Validation issue는 차트 Error다. 집계 정책 미확정은 차트 Error와 구별하여 ready의 AggregationResult `unavailable`로 전달하는 제안이다. Source 로딩·JSON 구문·매핑 실패도 제어된 Error지만 code/path로 Validation 오류와 구별하고 서버 원문을 전달하지 않는다. warning·업무 승인 필요 issue의 공개 전달 형식과 치명도는 API 승인 전 확정한다.
|
||||
|
||||
API Source로 전환해도 facade·각 framework의 사용 구조는 유지하고 Source만 바꾼다. 인증은 호스트 client가 처리한다. 검색·다운로드·편집·추가 메뉴·상세 모달은 이번 범위가 아니다.
|
||||
|
||||
## 4. 데이터 변경과 갱신
|
||||
|
||||
조직 추가/삭제·명칭·parent 이동, 직원 추가/삭제·조직 이동·직위/직책·사진·상태·관계사 변경, relation 변경, 기준일·제목 변경은 스냅샷 → Validation → ViewModel 재계산으로 반영한다. 전체 재계산 방식을 우선 검토하고 정렬은 Data Contract의 초안을 따른다.
|
||||
|
||||
Source 전환·reload·update마다 이전 작업을 취소하고 revision을 비교해 늦은 응답을 무시한다. Metadata 버전이 같아도 새 스냅샷은 검증한다. engine에는 복사본만 전달한다. 확장 상태·focus·zoom 보존은 PoC 후 결정하며 복원 키는 안정된 도메인 ID다.
|
||||
|
||||
검증·집계·render는 Core가 소유한 동일 불변 스냅샷을 사용한다. JSON Source의 캡처 시점은 생성 시점으로 제안하고 update는 호출 시 입력을 확보한다. 최종 DOM/state 반영 직전에도 revision·destroy 상태를 검사한다. consumer가 원본 객체를 나중에 바꿔 이미 반영한 화면·숫자가 암묵적으로 바뀌면 계약 실패다.
|
||||
|
||||
데이터만 바꾸는 범위는 승인된 계약의 유형·상태·표현 옵션 안이다. 새 업무 정책·관계 의미·카드 기능은 계약/UI 버전 변경이 필요할 수 있다. 모든 미래 변경을 무조건 무코드로 약속하지 않는다.
|
||||
|
||||
## 5. 엔진과 콘텐츠 경계
|
||||
|
||||
엔진은 조직 parent-child와 기본 계층 연결선을 담당한다. 직원은 조직 ID로 조회해 노드/그룹에 표시하고 리더를 조직으로 중복 삽입하지 않는다.
|
||||
|
||||
중앙 허브가 실제 조직인지 설명 콘텐츠인지 TBD다. 임시 visual root는 내부에서만 생성하고 집계에서 제외한다. 다중 root 처리도 미확정이다. `OrganizationRelation`은 parent-child와 별도이며 engine `relationship` 플래그로 치환하지 않는다. 특수 곡선·교차 관계·고정 배치는 별도 Overlay/Layout 후보로 검토한다. 추가 relation은 집계 분모를 변경하지 않는다.
|
||||
|
||||
완료 조건과 미결정 의존성은 [Implementation Plan](implementation-plan.md)을 따른다.
|
||||
@@ -0,0 +1,25 @@
|
||||
# OrgChart 브라우저 검증 실행 환경
|
||||
|
||||
검증 시나리오·기대값과 과거 `test-results`는 보존한다. 과거 전체 suite는 이번 수정의 필수 재실행 대상이 아니다.
|
||||
|
||||
`examples/org-chart/verify*.mjs`와 React 브라우저 runner는 공통
|
||||
`scripts/orgchart-browser-runtime.mjs`를 사용한다.
|
||||
|
||||
- 기본: 프로젝트의 표준 Node 모듈 탐색으로 `playwright`를 불러오고 해당 설치의 관리 Chromium을 실행한다.
|
||||
- `ORGCHART_PLAYWRIGHT_MODULE`: 선택형. 기존 외부 Playwright 설치의 entry 파일 절대경로 또는 실행 디렉터리 기준 상대경로.
|
||||
- `ORGCHART_CHROME`: 선택형. 기존 Chromium/Chrome 실행 파일 절대경로 또는 실행 디렉터리 기준 상대경로.
|
||||
- 지정한 모듈을 불러올 수 없거나 브라우저가 없으면 실행을 실패시키고 해결 방법 및 원래 오류를 출력한다. 자동 설치·버전 업그레이드·특정 PC fallback은 없다.
|
||||
|
||||
저장소 루트에서 실행한다. 기존 프로젝트 의존성에 Playwright가 없다면 이미 설치된 외부 entry를 환경변수로 지정한다. 환경변수 없이 실행하려면 해당 프로젝트 환경에서 Playwright 및 그 관리 Chromium이 제공되어야 한다. 실제 새 PC의 의존성 설치는 별도 확인 사항이다.
|
||||
|
||||
```powershell
|
||||
# 기본 모듈을 사용할 수 있는 환경
|
||||
node examples/org-chart/verify-p2.mjs
|
||||
|
||||
# 기존 외부 도구 설치를 재사용하는 환경(실제 경로로 지정)
|
||||
$env:ORGCHART_PLAYWRIGHT_MODULE = '<existing-playwright-entry>'
|
||||
$env:ORGCHART_CHROME = '<existing-browser-executable>'
|
||||
node examples/org-chart/verify-p2.mjs
|
||||
```
|
||||
|
||||
각 suite의 준비된 example build, baseline dist, Storybook/서버 origin 등의 전제는 그대로 유지된다. 도구 경로가 이식 가능해졌다는 것이 과거 fixture/baseline을 자동 생성하거나 모든 suite가 새 PC에서 검증되었다는 뜻은 아니다.
|
||||
@@ -0,0 +1,87 @@
|
||||
# HMDesign OrgChart P1 — 카드 디자인 1단계 결과
|
||||
|
||||
RESULT: **PASS — 요청한 카드 디자인 범위**. 원본 Guard는 기존 Token 추적 문제로 **FAIL**이며 이를 전체 릴리스 PASS로 치환하지 않았다.
|
||||
|
||||
## 작업 정보
|
||||
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 작업 전/후 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`
|
||||
- Started At: 2026-10-12 12:34:53 KST
|
||||
- Completed At: 2026-10-12 12:56:20 KST
|
||||
- Actual Duration: 21분 27초
|
||||
- Variance: 최초 예상 하한 30분 대비 8분 33초 단축 / 상한 45분 대비 23분 33초 단축.
|
||||
- Estimated Duration: 최초 30–45분. 대표자·사업관리 추가 요청 후 40–60분으로 조정.
|
||||
- 기존 package.json/package-lock.json 수정과 OrgChart 미추적 구현을 보존했다. Commit/Push/Merge/브랜치 생성/main 수정은 하지 않았다.
|
||||
|
||||
## 구현 결과
|
||||
|
||||
1. **SVG 자산 방식**: 전달된 10개 SVG를 원본 그대로 복사했다. Vite `?inline`으로 ESM/IIFE에 포함해 HTML/React/file://에서 외부 경로 의존 없이 사용한다. 배경은 장식용 img(`alt=""`, `aria-hidden`, `object-fit:contain`)이며 SVG의 path·gradient를 유지한다. 기존 박스 테두리·배경·우측 가상 장식은 자산 variant에만 제거한다.
|
||||
2. **조직별 매핑**: `department-card-assets.ts`에서 사업관리·경영기획·디자인기획·솔루션영업·인재성장·통합시스템 6개 부서를 매핑한다. 하위 셀/팀은 실제 parentId를 따라 해당 부서 자산을 상속한다. 사용자 승인에 따라 솔루션통합팀 SVG를 기존 solution-sales에 연결하며 조직명/JSON은 바꾸지 않았다. 알려지지 않은 조직은 기존 카드 디자인을 유지한다.
|
||||
3. **대표자**: 본문으로 전달된 146×74 SVG를 `대표자.svg`로 저장했다. `representative-card-asset.ts`로 부서와 별도 관리하고 기존 topLeader 독립 슬롯에만 사용한다. Full Composition의 최상단 가상리더 00 카드 1개에 적용했다.
|
||||
4. **부서 조직장/직원**: 기존 organization.leaderId 및 kind 판별을 재사용한다. 조직장 146×68, 일반 직원 108×52이며 하위 IS/디자인 팀 리더도 146×68이다. 판별 규칙이나 데이터 계약을 변경하지 않았다. 파견·휴직 보조 영역의 카드·배치는 유지했다.
|
||||
5. **사업관리 신규 벡터**: PNG(`D:\디자인시스템\카드 이미지\사업관리`, 확장자 없음, 408×191)에서 외곽 1,048개 RGBA 표본과 내측 5,619개 표본을 사용했다. 외곽 색상 `#DEE6F0 / #A8BBD1 / #577BA5`, 마지막 alpha 0.6; 내측 `#FFFFFF / #E0E6F4`. 기존 같은 크기 카드의 path와 곡선을 재사용하고 PNG에서 내측 그라데이션 방향도 복원했다. 두 자산에 텍스트·로고·사진·래스터는 없다. 원본 SHA-256은 `a8353bbf3869e4935c78a6774741d304836deb427d290586b439277022b2695f`다.
|
||||
6. **데이터 레이어**: 이름·직위·직책·상태·프로필·실제 회사 로고 renderer를 유지했다. 새 작은 카드에만 데이터 폰트·간격을 맞추고 로고 영역을 14px 고정 높이로 두어 숨겨진 fallback의 줄바꿈이 이름 영역과 겹치지 않게 했다. 로고의 투명 배경·정상 이미지 시 fallback 숨김은 유지했다.
|
||||
7. **Storybook**: 사용자 메뉴 5개와 내부 Story 8개를 유지했다. Department Colors는 대표자 1종과 6개 부서의 2종, 총 13개 카드 specimen·실제 크기·자산 출처를 표시한다. 기존 대비 MODEL/미승인 후보 24개 출력도 유지했다. Full Composition은 같은 Core renderer를 사용한다. 새 메뉴를 만들거나 기존 Story를 대체하지 않았다.
|
||||
8. **독립 패키지**: 자산·스타일은 @hm-design/org-chart 내부에 한정했다. React Wrapper/공개 exports/JSON/공통 Theme/AI Guard/허브/Summary/관계선은 변경하지 않았다. Navigation은 기존 Header 없는 Core 콘텐츠를 유지했다. 이번 작업에는 목록 검색 요구가 없어 FilterBar를 추가하지 않았다.
|
||||
|
||||
## 변경 파일 — 이번 작업만
|
||||
|
||||
신규:
|
||||
|
||||
- `packages/org-chart/src/assets/cards/` SVG 13개(전달 10개, 대표자 1개, 사업관리 생성 2개) 및 README.md.
|
||||
- `packages/org-chart/src/core/department-card-assets.ts`
|
||||
- `packages/org-chart/src/core/representative-card-asset.ts`
|
||||
- `examples/org-chart/create-business-card-assets.py`
|
||||
- `examples/org-chart/verify-card-design.mjs`
|
||||
- `docs/org-chart/card-design-stage1.md`
|
||||
|
||||
수정:
|
||||
|
||||
- `packages/org-chart/src/core/employee-card.ts` — 장식 SVG 레이어/내부 자산 옵션.
|
||||
- `packages/org-chart/src/core/render.ts` — 조직장·직원 부서 자산 선택.
|
||||
- `packages/org-chart/src/core/org-chart-content.ts` — 대표자 전용 자산.
|
||||
- `packages/org-chart/src/core/department-colors.ts` — 카드 specimen/크기/출처/대표자 표시.
|
||||
- `packages/org-chart/src/core/vendor.d.ts` — svg?inline 타입 선언.
|
||||
- `packages/org-chart/src/styles/employee-card.css` — 자산 variant 크기/장식 제거/데이터 레이어 조정.
|
||||
- `packages/org-chart/src/styles/org-chart-content.css` — specimen caption 및 대표자 미리보기 정렬.
|
||||
|
||||
생성 산출물: packages/org-chart/dist, examples/org-chart/dist, examples/org-chart-react/dist, storybook-static. 검증 근거는 기존 기록과 별도 `test-results/org-chart-card-design/`에 보존했다.
|
||||
|
||||
## 최종 통합 검증
|
||||
|
||||
- Core Typecheck **PASS**: `npm.cmd run typecheck:org-chart`.
|
||||
- Story/preview consumer Typecheck **PASS**: `npm.cmd exec -- tsc -p tsconfig.app.json --noEmit`.
|
||||
- Core ESM/IIFE/types/CSS Build **PASS**: `npm.cmd run build:org-chart`.
|
||||
- HTML prepare / React demo Build **PASS**. React Wrapper 소스 변경이 없어 기존 Wrapper 검증을 반복하지 않았다.
|
||||
- Storybook production Build **PASS**. 기존 500kB chunk 경고는 남아 있으며 빌드 실패는 없다.
|
||||
- 최종 Chrome **231/231 PASS**, console/runtime error **0**.
|
||||
- Department Colors / Full Composition / HTML / React / file:// classic 각각 390/768/1280/1440px에서 로딩·원본 크기·배경 장식 제거·실제 DOM Range 텍스트와 프로필/로고의 카드 경계·겹침·페이지 overflow를 확인했다.
|
||||
- 6개 부서 전체 매핑, 대표자 단독 top slot, 사업관리 조직장/직원 구분, 기존 status 카드 보존 **PASS**. 대표자/조직장/직원 프로필에 로컬 이미지 fixture를 넣어 경계와 로딩을 확인했으며 원본 JSON은 수정하지 않았다.
|
||||
- HTML/React/file:// 카드의 computed geometry/장식/이미지 표시/위치 판정 일치 **PASS**. 전체 화면 픽셀 동일성이나 모든 사용자 데이터 조합의 적합성을 뜻하지 않는다.
|
||||
- 원본 SVG 10개의 SHA-256 동일 **PASS**. Logo/JSON/Wrapper/Story 원본 보존은 preservation.json으로 확인한다.
|
||||
- 사업관리 PNG와 신규 큰 SVG를 Chrome canvas로 원본 408×191 크기에 렌더링해 흰 배경 합성 RGB를 비교했다. 전체 픽셀 채널 평균 오차 **1.113/255**, 95백분위 **1.094/255**. geometry/안티앨리어싱 차이를 포함하며 임의의 지각적 동일성 인증은 아니다. 색상 유사성 검사 **PASS**, 직접 시각 확인 완료.
|
||||
- 기존 Core 데이터·Summary·전체 Guard 회귀는 재실행하지 않았다. 중간 실패는 작은 카드 로고 높이와 PNG 내측 그라데이션 방향에 한정해 수정했으며, 수정된 산출물의 최종 표시를 재검증했다.
|
||||
|
||||
### Guard 원본 결과
|
||||
|
||||
수정된 파일만 PARTIAL/STORYBOOK으로 실행했다. 대표자·사업관리 추가 요청으로 코드 graph가 변경된 후 해당 최종 범위를 한 번 더 실행했고, 기존 결과는 guard-five-departments.json으로 보존했다. 원본 status/staticStatus **FAIL**: TOKEN-001 8건, TOKEN-002 NOT VERIFIED 6건, SCOPE-001 NOT VERIFIED 4건. 기존 품질 보고와 같은 Shadow CSS/setProperty/선택적 입력 추적 문제이며 이 작업에서 공통 검사기를 변경하지 않았다.
|
||||
|
||||
`styleEvidence`: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **PASS**, FINAL **NOT VERIFIED**. 이 값은 Guard 증거 연결 상태다. 별도 브라우저 PASS가 raw FAIL을 덮어쓰지 않는다. 결합 styleVerification은 생성/승격하지 않았다. 카드 전용 하드코드 geometry는 공통 HM Component override가 아니라 승인된 OrgChart SVG의 원본 크기다.
|
||||
|
||||
## 남은 이슈
|
||||
|
||||
- 검사한 가상 데이터의 텍스트·이미지·로고는 잘림/겹침 없이 표시된다. 매우 긴 소비자 이름·직책·임의 폰트 override는 **NOT VERIFIED**이며 고정 카드 크기에서 추가 정책이 필요할 수 있다.
|
||||
- 작은 실제 로고의 판독성 한계는 남는다. 원본 로고는 crop/recolor하지 않았다.
|
||||
- PNG로부터 SVG 그라데이션을 복원하므로 래스터 안티앨리어싱까지 정확히 같다는 보장은 없다. 투명도 포함 채널 비교와 원본 SHA를 보존했다.
|
||||
- 허브·관계선·부서 헤더 대비의 기존 REVIEW와 Guard 추적 FAIL은 다음 별도 작업 대상이다. 전체 Workdesk 레이아웃 비교/통계 재설계/운영 API/전체 접근성 검증은 이번 범위에서 **NOT VERIFIED**.
|
||||
|
||||
미리보기: `http://127.0.0.1:6028/?path=/story/orgchart--department-colors`, `http://127.0.0.1:6028/?path=/story/orgchart--full-composition`. 직접 열기 결과: `examples/org-chart/dist/p1-classic.html`.
|
||||
|
||||
## 완료 기록
|
||||
|
||||
Started At: **2026-10-12 12:34:53 KST**. Completed At: **2026-10-12 12:56:20 KST**. Actual Duration: **21분 27초**. Estimated Duration: **최초 30–45분**, 추가 요청 후 **40–60분**. Variance: 최초 예상 대비 **하한 −8분 33초 / 상한 −23분 33초**. 시간은 실제 clock 도구로 확인했다.
|
||||
|
||||
최종 SHA 보존 확인: baseline 111개 중 승인 범위 수정 7개, 나머지 104개 유지, 삭제 0개. 신규 소스/자산/문서 19개. root manifests/기존 Story/JSON/로고/React Wrapper/Guard/기존 보고서의 시작값은 유지했다. staged file 0개, branch/HEAD 동일. 근거: `test-results/org-chart-card-design/preservation.json`.
|
||||
|
||||
Guard snapshot 후 자산 출처를 설명하는 주석과 문서만 정리했다. 실행 동작과 Guard 분석 대상의 Token/CSS 구현은 최종 검사 이후 변경하지 않았다. 정상 Typecheck/Build/browser 검사를 문서 편집 때문에 반복하지 않았다.
|
||||
@@ -0,0 +1,122 @@
|
||||
# OrgChart 카드 디자인 2단계 — Typography 및 내부 콘텐츠
|
||||
|
||||
> P3.2 후속 확정: 대표자 카드에는 모든 직책·역할·상태 뱃지와 빈 뱃지 행을 생성하지 않는다. `대표이사`·`홍 길 동`, 승인 카드 146×74 및 기존 Typography를 유지한다. `대표이사`는 표시 계층에만 적용하며 직원 데이터·팝업 원문과 다른 1Depth/2Depth 뱃지는 보존한다. [현재 표시 계약](ui-composition.md#p32-후속--대표자-뱃지-미사용-2026-10-12)이 아래 과거 기록보다 우선한다.
|
||||
|
||||
> P3.2 최신 계약은 [P3.2 보고서](implementation-report-p3.2.md)를 따른다. 대표자는 그대로, 대형 이름 24px·직위 18px, 소형 이름 18px·직위 15px이다. 기존 가중치·line-height·컬러는 보존한다. readOnly 파견/휴직은 일반 소형의 108×52 카드·24×44 사진·텍스트 레이아웃을 공유하고 전용 회색 색상을 유지한다. 상태 타이틀은 일반 2Depth 기본 20px/700/19.7px, 카드 로고만 기존 실제 표시의 1.5배다. 아래 수치·검증은 역사 기록이다.
|
||||
|
||||
> P3.1 최신 표시 계약은 [P3.1 보고서](implementation-report-p3.1.md)를 따른다. Noto Sans KR을 OrgChart scope에 적용하며 직위 공백은 표시에서만 제거한다. 조회 화면의 `경영진` 직책 뱃지만 제거하고 실제 직위·다른 뱃지·데이터는 보존한다. 사진 없음/실패에는 공통 사각 `person-placeholder.svg`를 표시한다. 아래 P3 및 P2의 수치와 검증은 역사 기록이다.
|
||||
|
||||
> P3 최신 표시 계약: 카드/SVG 크기와 기존 Typography는 유지한다. 표시 이름만 `홍 길 동`, 직위·직책 뱃지는 원문이다. 대표자를 포함하여 직위와 뱃지는 같은 행(Gap 4px), 이름은 아래 행(Gap 0px)이다. 모든 조직 타이틀↔첫 카드 Gap은 8px이다. Workdesk 태그 Border/색상/폰트와 사각형 사진 슬롯을 적용하며, 사진 없음에는 새 프로필 이미지가 없다. 실제 결과는 [P3 보고서](implementation-report-p3.md)에 기록한다. 아래 P2 및 이전 수치·검증은 역사 기록으로 보존한다.
|
||||
|
||||
RESULT: PARTIAL
|
||||
|
||||
지정 Typography·Gap·말줄임·전체 이름 제공 구현과 해당 범위 검증은 PASS입니다. 제공한 Workdesk 캡처 경로의 파일과 폴더가 현재 없어 사진·텍스트·로고의 실제 운영 화면 정렬 일치는 NOT VERIFIED입니다. Guard 원본 FAIL도 유지합니다.
|
||||
|
||||
## 작업 정보
|
||||
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 작업 전/후 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c` (동일)
|
||||
- Started At: 2026-10-12 12:58:39 KST
|
||||
- Completed At: 2026-10-12 13:17:42 KST
|
||||
- Actual Duration: 19분 3초
|
||||
- Estimated Duration: 30–45분
|
||||
- Variance: 최소 예상 30분보다 10분 57초 짧음
|
||||
- 기존 Working Tree 보존, Stage·Commit·Push·Merge·브랜치 생성 없음.
|
||||
|
||||
## 구현 결과
|
||||
|
||||
1. 대표자: 직위 `#224387 / 20px / 400 / 22px / -0.8px`, 이름 `#071D3A / 24px / 600 / 30px / -0.8px` 적용.
|
||||
2. 그룹장: 직위 `#797979 / 20px / 400 / 22px / -0.8px`, 이름 `#071D3A / 24px / 600 / 30px / -0.96px` 적용.
|
||||
3. 일반 직원: 직위 `#797979 / 15px / 400 / 18px / -0.6px`, 이름 `#071D3A / 20px / 600 / 24px / -0.8px` 적용. 위 값의 순서는 color / font-size / font-weight / line-height / letter-spacing입니다.
|
||||
4. 직위와 이름을 별도 세로 Flex 컨테이너에 두고 gap·margin·padding을 모두 0으로 적용했습니다. 카드 Padding과 사진·텍스트 사이 간격은 보존했습니다.
|
||||
5. 사진은 왼쪽, 직위·이름은 오른쪽, 실제 회사 로고는 우측 상단에 유지했습니다. 기존 직책 배지는 사진 아래쪽 독립 영역으로 배치했습니다. SVG 카드의 상태 텍스트는 접근성 읽기에 유지하고 별도 세 번째 시각 행을 만들지 않았습니다. 파견·휴직 별도 영역의 카드 스타일은 보존했습니다.
|
||||
6. 카드 크기는 대표자 146×74, 그룹장 146×68, 일반 직원 108×52입니다. 프로필 영역은 각각 36×36, 32×32, 24×24로 이전 단계와 동일합니다. 폰트·사진·카드 크기를 축소하지 않았습니다.
|
||||
7. 긴 이름에만 말줄임을 적용합니다. 전체 문자열은 DOM에 유지하고, 실제 Overflow 카드만 Tab 순서에 포함합니다. Hover와 Focus로 같은 Shadow Root의 전체 이름 툴팁을 열고 `aria-describedby`로 연결합니다. 툴팁 위로 포인터를 이동할 수 있고 Escape로 닫을 수 있습니다. ResizeObserver·글꼴 로딩으로 판정을 갱신하고 기존 AbortSignal에서 관찰자·이벤트·툴팁을 정리합니다.
|
||||
8. 기본 가상 이름 41개를 자연스러운 2–3음절 한국어 이름으로 바꿨습니다. 직원 ID·직위·소속·상태·프로필·인원수·JSON 계약은 그대로입니다. 실제 직원 개인정보를 복사하지 않았습니다.
|
||||
|
||||
## Storybook 및 공통 적용
|
||||
|
||||
- Department Colors: 대표자와 6개 조직의 그룹장·직원 카드에 같은 Core Typography 적용. 기존 메뉴 내 “긴 이름 검증 시작/종료”로 대표자·그룹장·직원 각 1개의 긴 이름을 확인할 수 있습니다. 기본값은 짧은 이름입니다.
|
||||
- Full Composition: 최상단 대표자, 6개 조직 그룹장, 일반 직원 카드의 기본 이름·Typography·로고 배치 확인.
|
||||
- 사용자용 메뉴 Logo / Department Colors / Position Summary / Affiliation Summary / Full Composition 5개 유지.
|
||||
- 내부 OrgChart/Content Story 8개 보존. 별도 신규 Story 메뉴 없음.
|
||||
- HTML·React는 같은 Core 렌더러와 전용 CSS를 사용하며, 390/768/1280/1440px의 카드 Computed Style·크기·콘텐츠 측정값이 일치했습니다.
|
||||
- 로컬 미리보기: [Department Colors](http://127.0.0.1:6030/?path=/story/orgchart--department-colors), [Full Composition](http://127.0.0.1:6030/?path=/story/orgchart--full-composition).
|
||||
|
||||
## 변경 파일
|
||||
|
||||
신규 파일:
|
||||
|
||||
- `packages/org-chart/src/core/person-name-overflow.ts`: Overflow 판정·Hover/Focus 툴팁·접근성·정리.
|
||||
- `examples/org-chart/verify-card-content.mjs`: 이번 단계의 브라우저 검증.
|
||||
- `docs/org-chart/card-design-stage2.md`: 이 보고서.
|
||||
|
||||
수정 파일:
|
||||
|
||||
- `packages/org-chart/src/core/employee-card.ts`: SVG 카드의 직위/이름 전용 컨테이너 및 배지 영역.
|
||||
- `packages/org-chart/src/styles/employee-card.css`: 종류별 Typography·Gap·말줄임·Focus·툴팁.
|
||||
- `src/org-chart/org-chart-p1.stories.tsx`: 기존 Department Colors 내 긴 이름 검증 토글.
|
||||
- `examples/org-chart/create-p1-fixture.mjs`: 자연스러운 가상 이름 생성.
|
||||
- `examples/org-chart/data-p1.json`: 이름 필드만 갱신.
|
||||
|
||||
생성 결과: `packages/org-chart/dist/`, HTML/React demo dist, `storybook-static/`, `test-results/org-chart-card-content/`. 공통 Theme·Guard·공개 API·React Wrapper·차트 엔진·허브·Summary 구현은 변경하지 않았습니다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- 변경 패키지 Typecheck: PASS.
|
||||
- Story 소비 코드 Typecheck (`tsconfig.app.json`): PASS.
|
||||
- 변경 패키지 Build (ESM / browser IIFE / types / CSS): PASS.
|
||||
- HTML 준비 및 React demo Build: PASS.
|
||||
- Storybook production Build: PASS. 기존 큰 chunk 경고는 남아 있습니다.
|
||||
- 실제 Chrome 브라우저: **368/368 PASS**, console/runtime 오류 0.
|
||||
- 대표자/그룹장/직원 Computed Style, weight·line-height·letter-spacing·색상·Gap·margin·padding: PASS.
|
||||
- Department Colors 및 Full Composition 렌더링: PASS.
|
||||
- 짧은 이름: 줄바꿈·잘림 없이 표시, 불필요한 Tab 진입 없음.
|
||||
- 긴 이름: Overflow 카드에만 ellipsis·Tab Focus·전체 이름 툴팁, Hover·툴팁 Hover·실제 키보드 Tab·Escape·Focus 유지·접근성 트리 전체 문자열: PASS.
|
||||
- 긴 이름을 짧은 이름으로 복구할 때 툴팁 제거: PASS.
|
||||
- 카드 크기와 프로필 크기 보존, 사진/텍스트/로고/배지 겹침, 가시 텍스트 범위, 페이지 Overflow: PASS. 실제 이미지 로딩 사례도 검증했습니다.
|
||||
- HTML·React 4개 뷰포트 Computed Style/콘텐츠 일치: PASS.
|
||||
- 데이터 비교: 이름 필드 외 값 동일, JSON 계약·직원 수·소속·프로필 보존 PASS.
|
||||
- 시작 파일 130개 중 125개 해시 동일, 누락 없음. 변경은 위 5개 기존 파일뿐입니다.
|
||||
- SVG 13개와 실제 로고 PNG 7개 해시 모두 동일. 이전 품질 보완 파일 및 1단계 보고서 보존.
|
||||
- `git diff --check`: PASS. 기존 Stage 파일 없음, HEAD·브랜치 동일.
|
||||
- 기존 1단계 231개 검사는 재실행하지 않았습니다. 통계·데이터 Core 전체·AI Guard 전체 회귀도 재실행하지 않았습니다.
|
||||
|
||||
증거:
|
||||
|
||||
- `test-results/org-chart-card-content/browser.json`: 브라우저 버전, 368개 결과, 카드별 측정값.
|
||||
- `test-results/org-chart-card-content/preservation.json`: 해시 보존, Story 수, Git 상태.
|
||||
- `test-results/org-chart-card-content/baseline.json`: 시작 파일 해시.
|
||||
- `test-results/org-chart-card-content/data-p1-before.json`: 이름 변경 전 비교 기준.
|
||||
- `test-results/org-chart-card-content/*-normal.png`, `composition-1440.png`, `representative-tooltip-visible.png`: 실제 렌더링 화면.
|
||||
|
||||
### Guard 원본 상태
|
||||
|
||||
요청 파일을 대상으로 PARTIAL / STORYBOOK Guard를 1회 실행했습니다. 원본 `status=FAIL / staticStatus=FAIL`을 보존합니다.
|
||||
|
||||
- TOKEN-001 FAIL 8개: 변경하지 않은 `department-colors.ts` 및 `style-presets.ts`의 OrgChart 전용 동적 CSS 변수 추적.
|
||||
- 이전 1단계 FAIL 항목과 code·file·line·message가 모두 동일합니다.
|
||||
- TOKEN-002 NOT VERIFIED 6개, 패키지 CSS 추적 SCOPE-001 NOT VERIFIED 1개.
|
||||
- styleEvidence: STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED. 기존 Story 레이아웃 CSS의 디자인 의도 항목을 포함한 원본 결과이며, 이번 브라우저 증거로 원본을 덮어쓰지 않았습니다.
|
||||
- `test-results/org-chart-card-content/guard-storybook.json`에 전체 원본 저장.
|
||||
- 이번 카드 동작의 PASS와 Guard 전체 상태를 구분합니다. Guard 수정은 범위에서 제외합니다.
|
||||
|
||||
## 남은 차이 및 NOT VERIFIED
|
||||
|
||||
- `D:\디자인시스템\HMDesign-OrgChart-References\workdesk-orgchart.png`와 상위 References 폴더가 현재 없습니다. 따라서 실제 Workdesk 사진·텍스트·로고 위치, 글꼴 및 미세 정렬 일치는 NOT VERIFIED입니다. 지정 확정 CSS 수치는 우선 적용했습니다.
|
||||
- 기존 OrgChart 글꼴 설정을 유지했습니다. 실제 Workdesk의 글꼴 차이에 따른 시각적 차이는 캡처를 확인한 후 판단해야 합니다.
|
||||
- 브라우저 접근성 트리와 키보드 동작은 PASS이나, 별도의 실제 스크린리더 음성 낭독은 NOT VERIFIED입니다.
|
||||
- Guard의 기존 추적 FAIL/NOT VERIFIED는 미해소입니다.
|
||||
- 긴 이름을 완전히 한 줄로 표시하려면 고정 카드 폭을 초과하므로, 사용자 확정대로 크기·폰트를 유지하고 말줄임과 전체 이름 툴팁을 적용했습니다.
|
||||
- 전체 레이아웃·관계선·허브·통계 재설계는 다음 단계 범위이며 이번에는 변경하지 않았습니다.
|
||||
|
||||
## P2 최신 확정 표시 규칙 — 2026-10-12
|
||||
|
||||
위 2단계 기록과 검증 결과는 보존한다. 조회 전용 Full Composition의 현재 적용 기준은 [P2 보고서](implementation-report-p2.md)다.
|
||||
|
||||
- Depth 1 조직장만 146×68, Depth 2 이하 조직장은 108×52 소형 SVG. 독립 카드와 interactive 모드의 기존 조직장 선택은 유지한다.
|
||||
- 가상 직원의 이름은 표시 계층에서 `홍 길 동`으로 통일하고 직위·직책에도 연속 한글 사이 공백을 적용한다. 원본 데이터 문자열에는 공백을 삽입하지 않는다.
|
||||
- 기존 font-size, font-weight, line-height, 컬러와 내부 직위/뱃지 행 ↔ 이름 행 gap 0px는 유지한다. 글자 간격에 관한 이전 표시보다 이번 확정 지시를 우선한다.
|
||||
- 외부 1Depth 헤더 ↔ 대형 카드 gap은 최종 8px이며, 이전 P2 초안의 10px는 적용하지 않는다. 아래 Depth 헤더 ↔ 소형 카드는 10px다.
|
||||
- 긴 직책의 공백 표시가 회사 로고와 겹치지 않도록 조회 전용 대형 카드의 뱃지를 같은 행 안에서 아래로 정렬하고 로고 위치를 3px 올렸다. SVG·로고 원본, 글자/로고 크기 및 내부 행 높이는 변경하지 않았다.
|
||||
@@ -0,0 +1,407 @@
|
||||
HMDesign OrgChart P5-1A Commit Preparation
|
||||
Branch: feature/orgchart-package
|
||||
HEAD / live remote main + feature: d18e3c33801e165e17b6d971e2f6198eed483c3c
|
||||
Origin: https://gitea.hmac.kr/b25027/HMDesign.git
|
||||
Stage / Commit / Push / Merge: NOT EXECUTED
|
||||
|
||||
BASELINE P0-P5-0: 163 files (2 modified tracked + 161 untracked)
|
||||
All stage labels are historical accumulated work, not Git commits. Individual P0/P1/P2 phase file ownership cannot be reconstructed from HEAD alone.
|
||||
|
||||
docs/org-chart/README.md
|
||||
docs/org-chart/architecture.md
|
||||
docs/org-chart/card-design-stage1.md
|
||||
docs/org-chart/card-design-stage2.md
|
||||
docs/org-chart/data-contract.md
|
||||
docs/org-chart/framework-integration.md
|
||||
docs/org-chart/implementation-plan.md
|
||||
docs/org-chart/implementation-report-p1.md
|
||||
docs/org-chart/implementation-report-p2.md
|
||||
docs/org-chart/implementation-report-p3.1.md
|
||||
docs/org-chart/implementation-report-p3.2.md
|
||||
docs/org-chart/implementation-report-p3.md
|
||||
docs/org-chart/implementation-report-v0.1.md
|
||||
docs/org-chart/integration.md
|
||||
docs/org-chart/quality-review-p1.md
|
||||
docs/org-chart/react-integration-report-v0.1.md
|
||||
docs/org-chart/standalone-components-p1.md
|
||||
docs/org-chart/ui-composition.md
|
||||
examples/org-chart-react/README.md
|
||||
examples/org-chart-react/app.tsx
|
||||
examples/org-chart-react/index.css
|
||||
examples/org-chart-react/index.html
|
||||
examples/org-chart-react/p1.css
|
||||
examples/org-chart-react/p1.html
|
||||
examples/org-chart-react/p1.tsx
|
||||
examples/org-chart-react/verify-boundaries.mjs
|
||||
examples/org-chart-react/verify-package.mjs
|
||||
examples/org-chart-react/verify.mjs
|
||||
examples/org-chart-react/vite.config.ts
|
||||
examples/org-chart/app.js
|
||||
examples/org-chart/audit-quality-pixels.py
|
||||
examples/org-chart/classic.html
|
||||
examples/org-chart/create-business-card-assets.py
|
||||
examples/org-chart/create-fixtures.mjs
|
||||
examples/org-chart/create-p1-fixture.mjs
|
||||
examples/org-chart/data-p1.json
|
||||
examples/org-chart/data-v1.json
|
||||
examples/org-chart/data-v2.json
|
||||
examples/org-chart/demo.css
|
||||
examples/org-chart/index.html
|
||||
examples/org-chart/p1-app.js
|
||||
examples/org-chart/p1-classic.html
|
||||
examples/org-chart/p1.css
|
||||
examples/org-chart/p1.html
|
||||
examples/org-chart/prepare.mjs
|
||||
examples/org-chart/serve.mjs
|
||||
examples/org-chart/verify-card-content.mjs
|
||||
examples/org-chart/verify-card-design.mjs
|
||||
examples/org-chart/verify-final-card-check.mjs
|
||||
examples/org-chart/verify-final-logo-check.mjs
|
||||
examples/org-chart/verify-final-status-check.mjs
|
||||
examples/org-chart/verify-guard-p1.mjs
|
||||
examples/org-chart/verify-p1.mjs
|
||||
examples/org-chart/verify-p2.mjs
|
||||
examples/org-chart/verify-p3.1.mjs
|
||||
examples/org-chart/verify-p3.2.mjs
|
||||
examples/org-chart/verify-p3.mjs
|
||||
examples/org-chart/verify-package.mjs
|
||||
examples/org-chart/verify-quality-boundaries.mjs
|
||||
examples/org-chart/verify-quality-p1.mjs
|
||||
examples/org-chart/verify.mjs
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/build.mjs
|
||||
packages/org-chart-react/package.json
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
packages/org-chart-react/src/index.ts
|
||||
packages/org-chart-react/src/types.ts
|
||||
packages/org-chart-react/tsconfig.build.json
|
||||
packages/org-chart-react/tsconfig.json
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/build.mjs
|
||||
packages/org-chart/package.json
|
||||
packages/org-chart/src/assets/affiliations/README.md
|
||||
packages/org-chart/src/assets/affiliations/baron.png
|
||||
packages/org-chart/src/assets/affiliations/halla.png
|
||||
packages/org-chart/src/assets/affiliations/hanmac.png
|
||||
packages/org-chart/src/assets/affiliations/jangheon-industry.png
|
||||
packages/org-chart/src/assets/affiliations/jangheon.png
|
||||
packages/org-chart/src/assets/affiliations/ptc.png
|
||||
packages/org-chart/src/assets/affiliations/saman.png
|
||||
packages/org-chart/src/assets/cards/README.md
|
||||
packages/org-chart/src/assets/cards/경영기획팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/경영기획팀.svg
|
||||
packages/org-chart/src/assets/cards/대표자.svg
|
||||
packages/org-chart/src/assets/cards/디자인기획팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/디자인기획팀.svg
|
||||
packages/org-chart/src/assets/cards/사업관리팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/사업관리팀.svg
|
||||
packages/org-chart/src/assets/cards/솔루션통합팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/솔루션통합팀.svg
|
||||
packages/org-chart/src/assets/cards/인재성장팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/인재성장팀.svg
|
||||
packages/org-chart/src/assets/cards/통합시스템 디비전-sm.svg
|
||||
packages/org-chart/src/assets/cards/통합시스템 디비전.svg
|
||||
packages/org-chart/src/assets/person-placeholder.svg
|
||||
packages/org-chart/src/assets/workdesk/1-8-4x-1@2x.png
|
||||
packages/org-chart/src/assets/workdesk/Frame2@3x.png
|
||||
packages/org-chart/src/assets/workdesk/Group.svg
|
||||
packages/org-chart/src/assets/workdesk/README.md
|
||||
packages/org-chart/src/assets/workdesk/bg_main.png
|
||||
packages/org-chart/src/assets/workdesk/bg_sky.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-blue-purple.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-blue-short.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-purple-teal.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-teal-green.png
|
||||
packages/org-chart/src/company-color-presets.test.ts
|
||||
packages/org-chart/src/company-color-presets.ts
|
||||
packages/org-chart/src/components.ts
|
||||
packages/org-chart/src/core/affiliation-bar.ts
|
||||
packages/org-chart/src/core/affiliation-mark.ts
|
||||
packages/org-chart/src/core/affiliation-summary.ts
|
||||
packages/org-chart/src/core/contrast.test.ts
|
||||
packages/org-chart/src/core/contrast.ts
|
||||
packages/org-chart/src/core/department-card-assets.ts
|
||||
packages/org-chart/src/core/department-colors.ts
|
||||
packages/org-chart/src/core/employee-card.ts
|
||||
packages/org-chart/src/core/employee-info-dialog.ts
|
||||
packages/org-chart/src/core/engine.ts
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/logo-legend.ts
|
||||
packages/org-chart/src/core/org-chart-content.ts
|
||||
packages/org-chart/src/core/org-chart-tree.ts
|
||||
packages/org-chart/src/core/organization-totals.ts
|
||||
packages/org-chart/src/core/person-name-overflow.ts
|
||||
packages/org-chart/src/core/position-summary.ts
|
||||
packages/org-chart/src/core/render.ts
|
||||
packages/org-chart/src/core/representative-card-asset.ts
|
||||
packages/org-chart/src/core/status-summary.ts
|
||||
packages/org-chart/src/core/style-presets.ts
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/core/vendor.d.ts
|
||||
packages/org-chart/src/core/workdesk-artwork.ts
|
||||
packages/org-chart/src/core/workdesk-layout.ts
|
||||
packages/org-chart/src/data/content-selectors.test.ts
|
||||
packages/org-chart/src/data/content-selectors.ts
|
||||
packages/org-chart/src/data/data.test.ts
|
||||
packages/org-chart/src/data/index.ts
|
||||
packages/org-chart/src/data/normalize.ts
|
||||
packages/org-chart/src/data/snapshot.ts
|
||||
packages/org-chart/src/data/source.ts
|
||||
packages/org-chart/src/data/summary.ts
|
||||
packages/org-chart/src/data/types.ts
|
||||
packages/org-chart/src/data/validate.ts
|
||||
packages/org-chart/src/index.ts
|
||||
packages/org-chart/src/styles/design-tokens.css
|
||||
packages/org-chart/src/styles/employee-card.css
|
||||
packages/org-chart/src/styles/employee-info-dialog.css
|
||||
packages/org-chart/src/styles/org-chart-content.css
|
||||
packages/org-chart/src/styles/org-chart-p3-summary.css
|
||||
packages/org-chart/src/styles/org-chart-readonly.css
|
||||
packages/org-chart/src/styles/org-chart.css
|
||||
packages/org-chart/src/styles/summary-sections.css
|
||||
packages/org-chart/test.mjs
|
||||
packages/org-chart/tsconfig.build.json
|
||||
packages/org-chart/tsconfig.json
|
||||
src/org-chart/core-component-preview.tsx
|
||||
src/org-chart/org-chart-cards.stories.tsx
|
||||
src/org-chart/org-chart-p1.stories.tsx
|
||||
src/org-chart/org-chart-stories.css
|
||||
src/org-chart/org-chart-summary.stories.tsx
|
||||
src/org-chart/org-chart.stories.tsx
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
P5-1A CHANGES TO EXISTING BASELINE: 9
|
||||
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/build.mjs
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/render.ts
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/index.ts
|
||||
packages/org-chart/src/styles/org-chart.css
|
||||
|
||||
P5-1A NEW FILES: 9
|
||||
|
||||
examples/org-chart/data-expanded.json
|
||||
examples/org-chart/verify-p5-1a.mjs
|
||||
packages/org-chart/src/core/expanded-content.ts
|
||||
packages/org-chart/src/core/expanded-layout.test.ts
|
||||
packages/org-chart/src/core/expanded-layout.ts
|
||||
packages/org-chart/src/styles/org-chart-expanded.css
|
||||
src/org-chart/org-chart-expanded.stories.tsx
|
||||
docs/org-chart/implementation-report-p5-1a.md
|
||||
docs/org-chart/commit-preparation-p5-1a.txt
|
||||
|
||||
CURRENT ENTIRE UNCOMMITTED FILE LIST: 172
|
||||
|
||||
M package-lock.json
|
||||
M package.json
|
||||
?? docs/org-chart/README.md
|
||||
?? docs/org-chart/architecture.md
|
||||
?? docs/org-chart/card-design-stage1.md
|
||||
?? docs/org-chart/card-design-stage2.md
|
||||
?? docs/org-chart/data-contract.md
|
||||
?? docs/org-chart/framework-integration.md
|
||||
?? docs/org-chart/implementation-plan.md
|
||||
?? docs/org-chart/implementation-report-p1.md
|
||||
?? docs/org-chart/implementation-report-p2.md
|
||||
?? docs/org-chart/implementation-report-p3.1.md
|
||||
?? docs/org-chart/implementation-report-p3.2.md
|
||||
?? docs/org-chart/implementation-report-p3.md
|
||||
?? docs/org-chart/implementation-report-v0.1.md
|
||||
?? docs/org-chart/integration.md
|
||||
?? docs/org-chart/quality-review-p1.md
|
||||
?? docs/org-chart/react-integration-report-v0.1.md
|
||||
?? docs/org-chart/standalone-components-p1.md
|
||||
?? docs/org-chart/ui-composition.md
|
||||
?? examples/org-chart-react/README.md
|
||||
?? examples/org-chart-react/app.tsx
|
||||
?? examples/org-chart-react/index.css
|
||||
?? examples/org-chart-react/index.html
|
||||
?? examples/org-chart-react/p1.css
|
||||
?? examples/org-chart-react/p1.html
|
||||
?? examples/org-chart-react/p1.tsx
|
||||
?? examples/org-chart-react/verify-boundaries.mjs
|
||||
?? examples/org-chart-react/verify-package.mjs
|
||||
?? examples/org-chart-react/verify.mjs
|
||||
?? examples/org-chart-react/vite.config.ts
|
||||
?? examples/org-chart/app.js
|
||||
?? examples/org-chart/audit-quality-pixels.py
|
||||
?? examples/org-chart/classic.html
|
||||
?? examples/org-chart/create-business-card-assets.py
|
||||
?? examples/org-chart/create-fixtures.mjs
|
||||
?? examples/org-chart/create-p1-fixture.mjs
|
||||
?? examples/org-chart/data-expanded.json
|
||||
?? examples/org-chart/data-p1.json
|
||||
?? examples/org-chart/data-v1.json
|
||||
?? examples/org-chart/data-v2.json
|
||||
?? examples/org-chart/demo.css
|
||||
?? examples/org-chart/index.html
|
||||
?? examples/org-chart/p1-app.js
|
||||
?? examples/org-chart/p1-classic.html
|
||||
?? examples/org-chart/p1.css
|
||||
?? examples/org-chart/p1.html
|
||||
?? examples/org-chart/prepare.mjs
|
||||
?? examples/org-chart/serve.mjs
|
||||
?? examples/org-chart/verify-card-content.mjs
|
||||
?? examples/org-chart/verify-card-design.mjs
|
||||
?? examples/org-chart/verify-final-card-check.mjs
|
||||
?? examples/org-chart/verify-final-logo-check.mjs
|
||||
?? examples/org-chart/verify-final-status-check.mjs
|
||||
?? examples/org-chart/verify-guard-p1.mjs
|
||||
?? examples/org-chart/verify-p1.mjs
|
||||
?? examples/org-chart/verify-p2.mjs
|
||||
?? examples/org-chart/verify-p3.1.mjs
|
||||
?? examples/org-chart/verify-p3.2.mjs
|
||||
?? examples/org-chart/verify-p3.mjs
|
||||
?? examples/org-chart/verify-p5-1a.mjs
|
||||
?? examples/org-chart/verify-package.mjs
|
||||
?? examples/org-chart/verify-quality-boundaries.mjs
|
||||
?? examples/org-chart/verify-quality-p1.mjs
|
||||
?? examples/org-chart/verify.mjs
|
||||
?? packages/org-chart-react/README.md
|
||||
?? packages/org-chart-react/build.mjs
|
||||
?? packages/org-chart-react/package.json
|
||||
?? packages/org-chart-react/src/OrgChart.tsx
|
||||
?? packages/org-chart-react/src/index.ts
|
||||
?? packages/org-chart-react/src/types.ts
|
||||
?? packages/org-chart-react/tsconfig.build.json
|
||||
?? packages/org-chart-react/tsconfig.json
|
||||
?? packages/org-chart/README.md
|
||||
?? packages/org-chart/build.mjs
|
||||
?? packages/org-chart/package.json
|
||||
?? packages/org-chart/src/assets/affiliations/README.md
|
||||
?? packages/org-chart/src/assets/affiliations/baron.png
|
||||
?? packages/org-chart/src/assets/affiliations/halla.png
|
||||
?? packages/org-chart/src/assets/affiliations/hanmac.png
|
||||
?? packages/org-chart/src/assets/affiliations/jangheon-industry.png
|
||||
?? packages/org-chart/src/assets/affiliations/jangheon.png
|
||||
?? packages/org-chart/src/assets/affiliations/ptc.png
|
||||
?? packages/org-chart/src/assets/affiliations/saman.png
|
||||
?? packages/org-chart/src/assets/cards/README.md
|
||||
?? packages/org-chart/src/assets/cards/경영기획팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/경영기획팀.svg
|
||||
?? packages/org-chart/src/assets/cards/대표자.svg
|
||||
?? packages/org-chart/src/assets/cards/디자인기획팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/디자인기획팀.svg
|
||||
?? packages/org-chart/src/assets/cards/사업관리팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/사업관리팀.svg
|
||||
?? packages/org-chart/src/assets/cards/솔루션통합팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/솔루션통합팀.svg
|
||||
?? packages/org-chart/src/assets/cards/인재성장팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/인재성장팀.svg
|
||||
?? packages/org-chart/src/assets/cards/통합시스템 디비전-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/통합시스템 디비전.svg
|
||||
?? packages/org-chart/src/assets/person-placeholder.svg
|
||||
?? packages/org-chart/src/assets/workdesk/1-8-4x-1@2x.png
|
||||
?? packages/org-chart/src/assets/workdesk/Frame2@3x.png
|
||||
?? packages/org-chart/src/assets/workdesk/Group.svg
|
||||
?? packages/org-chart/src/assets/workdesk/README.md
|
||||
?? packages/org-chart/src/assets/workdesk/bg_main.png
|
||||
?? packages/org-chart/src/assets/workdesk/bg_sky.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-blue-purple.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-blue-short.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-purple-teal.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-teal-green.png
|
||||
?? packages/org-chart/src/company-color-presets.test.ts
|
||||
?? packages/org-chart/src/company-color-presets.ts
|
||||
?? packages/org-chart/src/components.ts
|
||||
?? packages/org-chart/src/core/affiliation-bar.ts
|
||||
?? packages/org-chart/src/core/affiliation-mark.ts
|
||||
?? packages/org-chart/src/core/affiliation-summary.ts
|
||||
?? packages/org-chart/src/core/contrast.test.ts
|
||||
?? packages/org-chart/src/core/contrast.ts
|
||||
?? packages/org-chart/src/core/department-card-assets.ts
|
||||
?? packages/org-chart/src/core/department-colors.ts
|
||||
?? packages/org-chart/src/core/employee-card.ts
|
||||
?? packages/org-chart/src/core/employee-info-dialog.ts
|
||||
?? packages/org-chart/src/core/engine.ts
|
||||
?? packages/org-chart/src/core/expanded-content.ts
|
||||
?? packages/org-chart/src/core/expanded-layout.test.ts
|
||||
?? packages/org-chart/src/core/expanded-layout.ts
|
||||
?? packages/org-chart/src/core/index.ts
|
||||
?? packages/org-chart/src/core/logo-legend.ts
|
||||
?? packages/org-chart/src/core/org-chart-content.ts
|
||||
?? packages/org-chart/src/core/org-chart-tree.ts
|
||||
?? packages/org-chart/src/core/organization-totals.ts
|
||||
?? packages/org-chart/src/core/person-name-overflow.ts
|
||||
?? packages/org-chart/src/core/position-summary.ts
|
||||
?? packages/org-chart/src/core/render.ts
|
||||
?? packages/org-chart/src/core/representative-card-asset.ts
|
||||
?? packages/org-chart/src/core/status-summary.ts
|
||||
?? packages/org-chart/src/core/style-presets.ts
|
||||
?? packages/org-chart/src/core/types.ts
|
||||
?? packages/org-chart/src/core/vendor.d.ts
|
||||
?? packages/org-chart/src/core/workdesk-artwork.ts
|
||||
?? packages/org-chart/src/core/workdesk-layout.ts
|
||||
?? packages/org-chart/src/data/content-selectors.test.ts
|
||||
?? packages/org-chart/src/data/content-selectors.ts
|
||||
?? packages/org-chart/src/data/data.test.ts
|
||||
?? packages/org-chart/src/data/index.ts
|
||||
?? packages/org-chart/src/data/normalize.ts
|
||||
?? packages/org-chart/src/data/snapshot.ts
|
||||
?? packages/org-chart/src/data/source.ts
|
||||
?? packages/org-chart/src/data/summary.ts
|
||||
?? packages/org-chart/src/data/types.ts
|
||||
?? packages/org-chart/src/data/validate.ts
|
||||
?? packages/org-chart/src/index.ts
|
||||
?? packages/org-chart/src/styles/design-tokens.css
|
||||
?? packages/org-chart/src/styles/employee-card.css
|
||||
?? packages/org-chart/src/styles/employee-info-dialog.css
|
||||
?? packages/org-chart/src/styles/org-chart-content.css
|
||||
?? packages/org-chart/src/styles/org-chart-expanded.css
|
||||
?? packages/org-chart/src/styles/org-chart-p3-summary.css
|
||||
?? packages/org-chart/src/styles/org-chart-readonly.css
|
||||
?? packages/org-chart/src/styles/org-chart.css
|
||||
?? packages/org-chart/src/styles/summary-sections.css
|
||||
?? packages/org-chart/test.mjs
|
||||
?? packages/org-chart/tsconfig.build.json
|
||||
?? packages/org-chart/tsconfig.json
|
||||
?? src/org-chart/core-component-preview.tsx
|
||||
?? src/org-chart/org-chart-cards.stories.tsx
|
||||
?? src/org-chart/org-chart-expanded.stories.tsx
|
||||
?? src/org-chart/org-chart-p1.stories.tsx
|
||||
?? src/org-chart/org-chart-stories.css
|
||||
?? src/org-chart/org-chart-summary.stories.tsx
|
||||
?? src/org-chart/org-chart.stories.tsx
|
||||
?? docs/org-chart/implementation-report-p5-1a.md
|
||||
?? docs/org-chart/commit-preparation-p5-1a.txt
|
||||
|
||||
REQUIRED SOURCE DEPENDENCIES
|
||||
- package.json + package-lock.json: workspace package registrations and orgchart6.0.0 pinned dependency tree.
|
||||
- packages/org-chart/: manifest, build.mjs, tsconfigs, all runtime src/data/core/styles/assets; public exports refer to generated dist.
|
||||
- packages/org-chart-react/: manifest, build.mjs, tsconfigs, src; requires Core public dist declarations first.
|
||||
- src/org-chart/ and referenced examples/org-chart/data-p1.json + data-expanded.json; original CoreComponentPreview depends on Core internal baseline source.
|
||||
- Vanilla examples need prepare/serve + app/HTML/CSS/JSON; React examples need TSX/HTML/CSS/vite.config.
|
||||
- verify*.mjs, *.test.ts, audit/asset-generation scripts are authored verification/reproduction source, not generated caches.
|
||||
- Existing HM UI exports and Storybook shared preview require their existing built public dist; no source changes or package installation in this task.
|
||||
|
||||
DO NOT INCLUDE GENERATED OUTPUTS / CACHES
|
||||
packages/org-chart/dist/
|
||||
packages/org-chart-react/dist/
|
||||
examples/org-chart/dist/
|
||||
examples/org-chart-react/dist/
|
||||
storybook-static/
|
||||
test-results/ (preserve local snapshots, JSON, captures; no forced add)
|
||||
node_modules/
|
||||
_local/
|
||||
playwright-report/
|
||||
coverage/
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
*.zip
|
||||
.env and private local settings
|
||||
|
||||
RECOMMENDED COMMIT GROUPS AFTER USER APPROVAL
|
||||
1. P0-P5-0 baseline: the 163-file starting source snapshot, manifests/lock, packages/assets/styles, examples/fixtures/Storybook/docs/authored verification.
|
||||
2. P5-1A: the 9 modified baseline files + 9 new files listed above.
|
||||
Several baseline files are still untracked: staging current file paths cannot separate their old/new contents.
|
||||
Starting source snapshot: test-results/org-chart-p5-1a/baseline-source/
|
||||
Starting SHA256 inventory: test-results/org-chart-p5-1a/baseline-files.json
|
||||
Use that captured baseline and an exact reviewed P5-1A patch if two commits are required.
|
||||
If snapshot data is unavailable later, prefer one reviewed accumulated OrgChart commit over inventing historical phase boundaries.
|
||||
A P5-1A-only commit directly against current HEAD omits Core/cards/JSON/React/workspace registrations and is incomplete.
|
||||
Use explicit file selection and staged diff review. Do not use git add -A. Recheck live remote before later commit/push.
|
||||
@@ -0,0 +1,270 @@
|
||||
# HMDesign OrgChart P11.1 — 최종 정합성 검증
|
||||
|
||||
**RESULT: PASS — 요청 범위의 문서·공개 API·CSS·Storybook 정합성 및 Feature Commit 조건 충족.** 이 보고서는 Commit 전 검증 기록이며 최종 Commit/Push SHA·원격 일치·Working Tree 결과는 작업 최종 응답에 기록한다. main 병합·배포·최종 디자인 승인 완료를 뜻하지 않는다.
|
||||
|
||||
## 1. 검사 기준과 범위
|
||||
|
||||
| 항목 | 확인값 |
|
||||
|---|---|
|
||||
| 작업 경로 | `D:\디자인시스템\HMDesign-OrgChart-Recovered` |
|
||||
| 브랜치 | `feature/orgchart-package` |
|
||||
| 검사 기준 HEAD / 시작 원격 Feature | `dcfa4e0895eecc97ce516cbf3d4424bbaadfddeb` |
|
||||
| 시작 원격 main | `c45d63a03b84eaac43f01bd400a16488a80e4229` |
|
||||
| origin | `https://gitea.hmac.kr/b25027/HMDesign.git` |
|
||||
| 작성자 | `b25027 <shkim4@samaneng.com>` |
|
||||
| 시작 변경 | P11 정상 산출물 26개, staged 0. [P11 보고서](implementation-report-p11.md)의 목록과 일치 |
|
||||
| P11.1 추가 변경 | 사용 가이드의 문서 오류 보완 및 이 보고서 1개. 제품·공개 타입·Story 소스 변경 0 |
|
||||
| 최종 Stage 후보 | P11 26개 + 이 보고서 = 27개. 임시 로그·캡처·dist·node_modules는 Git ignored이며 제외 |
|
||||
|
||||
이번 작업의 명시적 경로·Feature Commit/Push 지시를 공통 지침의 공식 경로/main 지시보다 우선했다. main Checkout/Merge/Push, PR, Registry Publish, Docker, 배포, 다른 Worktree 정리, Reset/Rebase/Force Push를 수행하지 않는다.
|
||||
|
||||
## 2. 정합성 결과표
|
||||
|
||||
| 검사 대상 | 문서 | 실제 코드 | Storybook | 결과 |
|
||||
|---|---|---|---|---|
|
||||
| Core 공개 API | 가이드 import·옵션·Handle 대조 | manifest/공개 export/구현/`.d.ts` 확인 | Full/Expanded 렌더·대표 동작 | PASS |
|
||||
| React Wrapper | 기본/HM Entry·Props·Adapter 대조 | optional peer, 타입 상속·설정 전달 확인 | Full/Expanded React 19 렌더 | PASS |
|
||||
| Chart Only | 기본 fit/direct·Fixed 거부·UI 제외 대조 | Core 기본 분기·viewport·wrapper 전달 확인 | 별도 React/Vanilla 화면 | PASS |
|
||||
| 외부 CSS 테마 | 이름·적용 범위·우선순위 수정 | CSS 선언/사용 추출, 예제 입력 39개 연결 | React/Vanilla 테마 대표 화면 | PASS |
|
||||
| HMDesign Dialog | 별도 HM Theme/Portal 범위 대조 | `/hm-dialog` 및 HMEmployeeDialog 확인 | Expanded Dialog·ESC·focus 복귀 | PASS |
|
||||
| 회사별 컬러 | 7개 회사색/secondary 입력 대조 | TS 프리셋과 CSS fallback 일치 | Expanded 실제 computed 7색 | PASS |
|
||||
| Fixed 디자인 | 6개 부서색·SVG/PNG 한계 대조 | 기존 자산·구성 보존 | Full/Department/Overview | PASS |
|
||||
| AI 생성 정책 | 사전 선택·가변 구조·원본 보존 | enum/parentId 트리·회사 경계 대조 | 기존 Story 비소급 범위 확인 | PASS(정적 정책) |
|
||||
| Guard 토큰 | P10 원본/P11 현재 결과 구분 | 실제 재검사 FAIL 0, 규칙 비활성화 없음 | 실제 theme 렌더는 별도 증거 | PASS(정적), Runtime NOT VERIFIED |
|
||||
|
||||
Core 공개 export는 `createOrgChart`, DataSource/Validator/집계/이동 함수와 독립 presenter이며 새 API를 만들지 않았다. `layoutMode`는 Fixed/Auto, `autoLayout.type`은 TB/LR/Hybrid다. Auto의 Zoom/Pan/Fit·접기·편집·직원 팝업은 기존 구현을 사용하며 CSV/XLSX parser, 공개 collapse setter, React imperative ref, 서버 저장 API는 없다. JSON 최소/깊은 예제 및 필수·nullable 필드는 현재 `data/types.ts`·Validator와 일치한다.
|
||||
|
||||
React 기본 Entry는 Core lifecycle·Props만 사용하고 HM UI를 import하지 않는다. `@hm-design/ui` peer는 optional, React peer는 `^19.0.0`이며 현재 19.3.0에서 렌더링했다. 선택 Entry `/hm-dialog`는 기존 HM UI Dialog를 제공하며 `employeePopup="hm-dialog"`를 명시해야 한다. `employeePopupAdapter`도 모드를 함께 지정한다. Fixed는 기존 Core 팝업/`onEmployeeClick` 경계를 유지한다. HM UI 없는 fresh install은 [기존 최소 설치 증거](minimal-install-report-2026-10-13.md)를 재사용했고 이번에 다시 설치하지 않았다.
|
||||
|
||||
## 3. 공개 화면과 Chart Only
|
||||
|
||||
공개 index에는 `OrgChart/Overview`, `Department Colors`, `Full Composition`, `Expanded OrgChart` 4개가 그대로 있다. ID는 각각 `orgchart--logo-personnel-summary`, `orgchart--department-colors`, `orgchart--full-composition`, `orgchart--expanded-org-chart`다. OrgChart는 최상위 그룹이며 Components/Patterns와 같은 Depth다. 신규 Story 등록·기존 ID/URL 교체 없음.
|
||||
|
||||
| 화면 | 실제 확인 |
|
||||
|---|---|
|
||||
| Overview | 로고 legend 7개, 중복 내부 h2 없음, 동일 `data-p1.json` Core 집계/표·하단 관계사 총계 41명. 하단 집계에 반복된 회사 로고를 legend 수에 더하지 않음 |
|
||||
| Department Colors | 대표자·그룹장·직원, 6개 서로 다른 기존 gradient, 모든 직원 이름 `홍 길 동`, 파견/휴직 preview. 기존 카드·Typography 캡처 확인, Cell Border Only 미적용 |
|
||||
| Full Composition | 대표자·화살촉 1개·협업 PNG 화살표 5개·중앙 구성·상단 제목/인원표·하단 파견/휴직·관계사 현황·팝업 X/카드 focus 복귀 확인. 390px 반응형/내부 스크롤 판정은 [P10.10](visual-review-p10-10.md) 재사용, 새 수정 사유 없음 |
|
||||
| Expanded OrgChart | 최초 7회사×20=140명, 회사 선택, 한맥기술 단독 20/100/300명 실제 카드 수, 300명 TB/LR/Hybrid 유지, 7회사 기본 computed 색, 기본 D&D OFF, ON의 직원/그룹 소속 이동(키보드 편집 경로)·300명 보존, OFF Pan, HM Dialog ESC/focus 복귀, 기존 Zoom/Fit 컨트롤 유지 |
|
||||
| React/Vanilla Chart Only | 기본 흰 캔버스, 외부 heading/selection/toolbar 없음, 전체 실제 Bounds 초기 Fit/가로·세로 중앙, 직접 wheel·커서 기준 anchor, 직원 Core 팝업, 접기/재확장 확인. Fit은 빈 engine wrapper 여백 대신 실제 조직/조직 연결 path bounds로 측정 |
|
||||
|
||||
Chart Only는 `layoutMode='auto'` + `presentationMode='chart-only'`, 기본 `initialView='fit'` / `wheelZoom='direct'`다. 기본 Expanded에는 이 모드를 전달하지 않아 기존 제목·요약·툴바 및 modifier wheel 정책을 유지한다. Fixed Chart Only는 Core `TypeError` 경로로 명시적 미지원이다. React와 Vanilla는 같은 Core 설정을 사용하며 호스트가 캔버스 크기를 제공한다. P11 React fixture의 host 높이 미지정 fallback 320px와 Vanilla의 850px 컨테이너를 동일 크기라고 보고하지 않는다.
|
||||
|
||||
신규 대표 검사: 최초 16개 중 10 PASS/6 FAIL. 소스 대조 후 실패 6개만 보정해 6/6 PASS + 재검사 오류 0. 최초 결과를 보존한다. 원인은 logo legend와 하단 반복 로고 합산, 3종류 composition region의 6개 slot을 3개 element로 예상, Fit의 engine wrapper 여백 포함, 상태 gradient 카드에 일반 active 카드 backgroundColor 기대였다. 제품 버그나 신규 제품 변경은 없었다.
|
||||
|
||||
현재 캡처를 확인한 공개 Story 4개는 렌더 PASS다. 기존 Story 전체의 변경 전후 픽셀 동일성이나 사용자의 최종 디자인 승인을 뜻하지 않는다. 순수 pointer D&D의 모든 조합은 이번에 재실행하지 않았으며 키보드 편집은 실제 ON의 공통 validation/engine drop/move 경로를 사용했다.
|
||||
|
||||
## 4. CSS 변수·연결·기본값
|
||||
|
||||
실제 PostCSS AST에서 532개 변수 선언/사용 위치를 추출했다. P11 tracked CSS 변경에 나타난 66개 이름과 신규 `color-presets.css`를 합치면 116개이며, 관련 기존 공통 컬러 입력 12개를 더해 부록에 128개 이름을 기록했다. **128개가 모두 신규 공개 변수라는 뜻은 아니다.** 공개 입력, 기존 입력, 내부 resolved/current alias·geometry를 구분했다. 예제 CSS 3개의 서로 다른 공개 입력 39개는 모두 실제 `var(...)` 사용 경로가 있으며 가상의 이름은 없다.
|
||||
|
||||
- 공개 입력은 상속을 위해 내부에 고정 선언하지 않는다. 기본값은 해당 scope의 소비 표현식 fallback에 있다. 내부 선언·default·selector/적용 속성은 부록에 기록한다. React/Vanilla는 같은 Core CSS를 사용하므로 모든 행의 CSS 적용 경로가 공통이다.
|
||||
- 회사 ID primary 7개 기본값은 변경하지 않았다. 보조 입력은 6회사 secondary-1과 ptc secondary-2만 연결되고 baron 보조 입력은 없다. 보조 alias를 주요 카드/제목의 추가 컬러 역할로 오인하지 않는다.
|
||||
- Fixed 6부서의 gradient/Border/default는 기존 TS 프리셋과 일치한다. 공통 그룹 배경은 회사/부서 제목 배경보다 우선한다. 부서별 배경·fallback 텍스트와 공통 Border·compact HTML 이름의 우선순위 차이를 문서에 보완했다.
|
||||
- 파견/휴직 Fixed 요약 gradient·Border·텍스트와 Auto 상태 카드 gradient·Border·텍스트를 구분한다. Fixed `*-gradient`는 `background-image`에 연결하므로 CSS image 표현식이 필요하다. 단색 CSS color를 그 입력에 넣는다고 일반 배경값과 동일하게 적용되지 않는다.
|
||||
- Core dialog 배경/글자/Border, 닫기 Hover/Focus는 Core 내부 입력이다. `/hm-dialog`는 Shadow 밖 HM Theme/Portal이며 자동 동기화하지 않는다.
|
||||
|
||||
`stylesheetUrls.core`는 기본 CSS 전체를 포함한 URL 하나다. 기본 CSS `@import` 뒤에 `:host` override를 두며 engine→core 순서로 Shadow link를 로드한다. Vite는 공개 CSS/외부 CSS의 `?url`, HTTP ESM 소비자는 실제 서빙 URL을 사용한다. 변수 전용 host CSS는 document에 연결하고 class로 상속한다. 일반 host selector가 Shadow 내부를 통과하지 않는다.
|
||||
|
||||
Shadow `:host`의 직접 지정값이 조상 상속보다 우선하며 소비자가 같은 입력을 inline 지정하면 해당 inline이 우선한다. P11 Core의 회사·부서 기본색 inline `setProperty`는 제거됐고 dataset에 따른 CSS 해석으로 바뀌었다. `setProperty` 자체를 새 테마 API로 문서화하거나 Guard 자동 승인에 사용하지 않았다. 호스트 class 제거의 기본값 복원과 배율 보존, React/Vanilla 외부 테마·팝업·Hover/Focus·4폭은 P11 104/104 PASS 증거를 재사용했다. 이번 대표 테마 검사는 두 runtime의 회사 제목·active 카드·연결선·상태 gradient·Core 팝업을 추가 확인했다.
|
||||
|
||||
Fixed SVG/PNG 내부 색상은 CSS 변수로 바뀌지 않는다. 실제 inline SVG overlay 연결선은 CSS로 변경되지만 이미지 화살표·로고·배경·카드 SVG 불투명 영역은 그대로다. 외부 Theme 기능은 삼안/장헌산업 흰 제목 대비나 보안 Advisory를 자동 해결하지 않는다.
|
||||
|
||||
## 5. 문서 보완과 예제 검증
|
||||
|
||||
| 발견된 문서 불일치 | 최소 수정 |
|
||||
|---|---|
|
||||
| 하단 제한에 잔여 Guard FAIL 5건이 현재 상태처럼 기재 | P10 당시 기록과 P11 실제 FAIL 0건을 분리, 원본 보고서 링크 보존 |
|
||||
| 외부 CSS 테마 표준화가 현재 미포함처럼 기재 | P11 지원과 Fixed 이미지/Chart Only 미지원을 분리 |
|
||||
| Fixed 카드 변수 우선순위 전체 일반화 | 부서 배경/fallback 텍스트, 공통 Border/accent, SVG/compact HTML 이름을 구분 |
|
||||
| 모든 gradient 입력에 background 표현식을 허용한 설명 | Fixed `background-image`용 CSS image 표현식 필요 명시 |
|
||||
| company secondary의 임의 n처럼 읽히는 표 | 실제 ID/n 조합 명시 |
|
||||
| HTTP Vanilla snippet의 Element/null 컨테이너 | `getElementById` + 존재 확인으로 HTMLElement 계약 명시 |
|
||||
|
||||
가이드의 TS/TSX 13개와 HTTP Vanilla JS 1개를 추출해 strict·ES2022·Bundler·react-jsx·skipLibCheck=false로 **14/14 타입 검사 PASS**. 공개 package `.d.ts`와 Vite client CSS URL 타입을 사용했다. 짧은 theme 예제의 앞 문맥 `dataSource`만 harness 선언으로 제공하고, HTTP ESM 경로는 실제 공개 Core 타입으로 해석했다. API·Props를 임의 생성하거나 잘못된 옵션을 mock으로 감추지 않았다. P11 TSX fixture도 함께 검사했다.
|
||||
|
||||
전체 문서 예제의 개별 browser 실행은 NOT VERIFIED다. HMChart/CustomChart 자체의 browser 시연 대신 실제 HM Entry Story 및 Core/React fixture를 확인했다. P10.7·UsageGuide 당시 검증 원본을 지우거나 후속 결과로 덮어쓰지 않았다.
|
||||
|
||||
AI 정책의 사전 질문, 이미 명시된 방식 재질문 금지, Chart Only 최소 UI, Full Page HM UI 우선 재사용, 정확한 계층/인원/제공 데이터 보존, 기존 화면 비소급은 현재 API와 일치한다. parentId 기반 가변 계층과 division/office/div/team/cell enum을 소스에서 확인했으며 유형별 새 디자인 자동 선택으로 설명하지 않는다. Cell Border Only 제외 및 Fixed Chart Only 제약을 보존했다. 새 조직도 생성 요청이 아니므로 이번 감사에 생성 방식 질문을 추가하지 않았다.
|
||||
|
||||
## 6. 검증·Guard·보존 증거
|
||||
|
||||
| 확인 | 결과 / 근거 |
|
||||
|---|---|
|
||||
| Core/React 변경 범위 Typecheck | 신규 실행 PASS (`typecheck:org-chart`, `typecheck:org-chart-react`) |
|
||||
| Core/React/소비 예제 Build | 신규 실행 PASS, CSS URL production assets 생성. 기존 host-context/minifier·큰 chunk 경고 유지 |
|
||||
| 문서 예제/fixture Typecheck | 신규 실행 PASS, `test-results/org-chart-p11-1/tsconfig.json` |
|
||||
| Storybook 4개 화면 렌더 | 신규 4/4 PASS, 오류 0. 공개 index/데이터/대표 동작은 신규 browser/recheck 결과 |
|
||||
| React/Vanilla Chart Only·Theme 대표 | 신규 browser + 실패 한정 recheck PASS, console/page 오류 0 |
|
||||
| 실제 Guard | 신규 재검사 `status PASS`, `staticStatus PASS`, issues 0 / FAIL 0 |
|
||||
| Guard CSS phases | STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED. 결합 styleVerification 미생성 유지 |
|
||||
| Guard 회귀 | **기존 P11 94/94 및 CSS 회귀 5/5 PASS 재사용**. 검사기 P11.1 변경 0이라 반복하지 않음. 실제 Guard 결과와 별개 |
|
||||
| 다른 HM 패키지 Guard 영향 | 기존 P11 실제 비교: 이슈 67개 동일, FAIL 0, 원본 NOT VERIFIED 보존. 변경 없는 동일 분석기/제품 소스 범위 재사용 |
|
||||
| 기본 디자인 6화면 | P11 before/after PNG 12파일을 읽어 기록 SHA-256을 재확인, 6/6 동일. 신규 baseline 캡처·승인 아님 |
|
||||
| Full 390px/4폭/P11 HTTP Vanilla | P10.10 Full 390px·P11 4폭 104개·순수 HTTP Core 8개 PASS 기존 유효 결과 재사용 |
|
||||
| Markdown 링크/경로·diff | 변경 문서 4개 로컬 링크/앵커 236개 PASS, `git diff --check` PASS. Staged 검사는 Commit 직전 수행 |
|
||||
|
||||
Guard는 다음 한 번의 실제 검사로 확인했다. 규칙 우회·예외 목록·미선언 token 자동 승인·Runtime PASS 승격 없음.
|
||||
|
||||
```powershell
|
||||
node scripts/ai-generation-guard.mjs --target . --scope PARTIAL --profile STORYBOOK --files packages/org-chart/src/core/department-colors.ts,packages/org-chart/src/core/style-presets.ts,packages/org-chart/src/core/expanded-content.ts --json
|
||||
```
|
||||
|
||||
증거는 ignored 로컬 폴더에 보존하며 새 Clone에 포함되지 않는다. `verify-p11.mjs`의 기본값 비교는 변경 전 산출물에서 `--baseline`으로 생성한 기록을 필요로 한다. 새 Clone에서 baseline이 없는 실행을 역사적 기본 디자인 보존 검사로 해석하지 않는다:
|
||||
|
||||
- [현재 검사 원본](../../test-results/org-chart-p11-1/browser-result.json), [실패 한정 재검사](../../test-results/org-chart-p11-1/recheck-result.json), [실제 Guard](../../test-results/org-chart-p11-1/guard-actual.json), [CSS 전체 추출](../../test-results/org-chart-p11-1/css-inventory.json), [예제 manifest](../../test-results/org-chart-p11-1/snippet-manifest.json).
|
||||
- [Chart Only React 최초 화면](../../test-results/org-chart-p11-1/initial-react.png), [Vanilla 최초 화면](../../test-results/org-chart-p11-1/initial-vanilla.png), [HM Dialog](../../test-results/org-chart-p11-1/hm-dialog.png).
|
||||
- [기본 6화면 해시](../../test-results/org-chart-p11/default-image-comparison.json), [P11 공용 비교](../../test-results/org-chart-p11/guard-other-comparison.json), [P11 구현·검증](implementation-report-p11.md).
|
||||
|
||||
실행 URL: [Storybook](http://127.0.0.1:6034/), [React Chart Only](http://127.0.0.1:6026/p11.html?runtime=react&mode=auto), [Vanilla Chart Only](http://127.0.0.1:6026/p11.html?runtime=vanilla&mode=auto), [React 외부 테마](http://127.0.0.1:6026/p11.html?runtime=react&mode=auto&theme=1&statuses=1). 서버는 이 작업에서 시작했고 검토용으로 유지했다. sandbox 로컬 연결 제한을 해소하기 위해 서버/Chrome을 같은 실행 환경에서 실행했다. 연결 실패를 제품 오류로 판정하지 않았다.
|
||||
|
||||
## 7. 미검증과 Commit 판정
|
||||
|
||||
**Commit 가능: PASS.** 공개 API 일치·테마/Chart Only/4 Story 유지·필수 타입/빌드·실제 Guard FAIL 0·기본 6화면 보존·정상 변경 27개·작성자 일치를 확인했다. 관련 파일만 Stage하고 Staged Diff·원격 Feature/main SHA를 재확인한 뒤 일반 Feature Fast-forward Push한다. 원격이 기준에서 바뀌면 중단하며 main은 건드리지 않는다.
|
||||
|
||||
NOT VERIFIED: 모든 pointer D&D 조합·회사×100/300명×모든 layout의 전체 회귀, HM Dialog 별도 팔레트 변경 비교, HMChart/CustomChart 자체 browser 시연, 전체 Story before/after 픽셀 비교, Firefox/Safari/실제 모바일/touch/스크린리더, 전체 성능·SSR·새 fresh install·신규 npm audit·배포. 사용자 최종 디자인 승인 PENDING. Core 86개·Chart Only 51개 및 직접 영향 없는 P7~P10 전체 회귀는 반복하지 않았다.
|
||||
|
||||
기존 개발 High Advisory 7건, 삼안 약 2.64:1·장헌산업 약 4.29:1 제목 대비 미해결 기록을 보존한다. P11 Guard 개선/테마 기능을 보안 해결·WCAG 자동 충족으로 표현하지 않는다. Fixed Chart Only와 Fixed 이미지 내부 테마화는 미검증 후보가 아닌 미지원이다.
|
||||
|
||||
## 8. 작업 시간
|
||||
|
||||
- Started At: 2026-10-13 22:26:04 KST
|
||||
- Estimated Duration: 최초 약 60분 (분석 20 / 검증 30 / 보고·Git 10)
|
||||
- 검증 완료·최종 Git 완료의 Completed At / Actual Duration / Variance는 작업 최종 응답에 기록한다. 이 파일은 Commit 전 기록이므로 미래 완료 시간을 추정하지 않는다.
|
||||
|
||||
## 9. P11 변수 소스 대조 부록
|
||||
|
||||
각 이름의 실제 선언 또는 첫 사용 표현식·selector/속성을 기록한다. 공개 입력은 내부 선언 없음(상속용)이며 표현식의 fallback이 기본값이다. 공개 행은 호스트 상속 또는 Shadow 외부 CSS override 가능, 내부 행은 계산 구현이며 직접 override를 권장하지 않는다. 모든 행은 같은 Core CSS를 쓰는 React/Vanilla 공통 경로다. 여러 scope에서 fallback이 다르면 해당 소스 전체가 최종 기준이며 첫 표현식만 유일한 기본값으로 해석하지 않는다. 카드 SVG/PNG 영역·HM Dialog는 앞의 지원 경계를 따른다.
|
||||
|
||||
<!-- P11-CSS-INVENTORY -->
|
||||
|
||||
| 실제 이름 | 구분 | 선언/사용 위치 | scope / 적용 속성 | 선언값 또는 소비 표현식(default fallback 포함) |
|
||||
|---|---|---|---|---|
|
||||
| `--hm-orgchart-accent` | 내부/기존 선언 | [color-presets.css:11](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-accent` | `var(--hm-orgchart-current-border)` |
|
||||
| `--hm-orgchart-accent-color` | 공개 입력(상속) | [design-tokens.css:17](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-accent` | `var(--hm-orgchart-accent-color, #eef2fa)` |
|
||||
| `--hm-orgchart-action-primary` | 공개 입력(상속) | [design-tokens.css:19](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-action-primary` | `var(--hm-orgchart-action-primary, #45639f)` |
|
||||
| `--hm-orgchart-bg-brand` | 공개 입력(상속) | [design-tokens.css:11](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-brand` | `var(--hm-orgchart-bg-brand, linear-gradient(180deg, #45639f 0%, #809dd7 100%))` |
|
||||
| `--hm-orgchart-bg-content` | 공개 입력(상속) | [design-tokens.css:9](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-content` | `var(--hm-orgchart-bg-content, #f4f7fb)` |
|
||||
| `--hm-orgchart-bg-subtle` | 공개 입력(상속) | [design-tokens.css:10](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-subtle` | `var(--hm-orgchart-bg-subtle, #e9edf3)` |
|
||||
| `--hm-orgchart-bg-surface` | 공개 입력(상속) | [design-tokens.css:8](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-surface` | `var(--hm-orgchart-bg-surface, #ffffff)` |
|
||||
| `--hm-orgchart-border` | 공개 입력(상속) | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` |
|
||||
| `--hm-orgchart-business-management-border` | 공개 입력(상속) | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` |
|
||||
| `--hm-orgchart-business-management-card-background` | 공개 입력(상속) | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-business-management-card-color` | 공개 입력(상속) | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-business-management-department-color` | 공개 입력(상속) | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-business-management-gradient` | 공개 입력(상속) | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` |
|
||||
| `--hm-orgchart-business-planning-border` | 공개 입력(상속) | [color-presets.css:19](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-planning-border, var(--hm-orgchart-border, #2164a8))` |
|
||||
| `--hm-orgchart-business-planning-card-background` | 공개 입력(상속) | [color-presets.css:16](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-business-planning-card-color` | 공개 입력(상속) | [color-presets.css:17](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-business-planning-department-color` | 공개 입력(상속) | [color-presets.css:18](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-planning-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-business-planning-gradient` | 공개 입력(상속) | [color-presets.css:20](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #2164a8 0%, #254975 100%)))` |
|
||||
| `--hm-orgchart-card-accent` | 공개 입력(상속) | [employee-card.css:40](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee[data-card-kind="leader"],␍ .hm-orgchart-employee[data-card-kind="topLeader"]` → `border-top` | `var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-card-accent, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-action-primary)))` |
|
||||
| `--hm-orgchart-card-background` | 공개 입력(상속) | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-card-border` | 공개 입력(상속) | [employee-card.css:14](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee[data-card-kind]` → `border` | `var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-card-border, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-stroke-default)))` |
|
||||
| `--hm-orgchart-card-color` | 공개 입력(상속) | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-card-focus-color` | 공개 입력(상속) | [employee-info-dialog.css:10](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-organization-link:focus-visible,␍ .hm-orgchart-employee[role="button"]:focus-visible` → `outline` | `2px solid var(--hm-orgchart-card-focus-color, var(--hm-orgchart-resolved-stroke-input-focus))` |
|
||||
| `--hm-orgchart-card-hover-background` | 공개 입력(상속) | [org-chart-expanded.css:117](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"]:hover` → `background` | `var(--hm-orgchart-card-hover-background, var(--hm-orgchart-resolved-status-card-background, var(--hm-orgchart-resolved-role-card-background)))` |
|
||||
| `--hm-orgchart-card-position-color` | 공개 입력(상속) | [employee-card.css:242](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-role` → `color` | `var(--hm-orgchart-card-position-color, var(--hm-orgchart-person-position-color))` |
|
||||
| `--hm-orgchart-company-baron` | 공개 입력(상속) | [color-presets.css:103](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="baron"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-baron, #0C367F)` |
|
||||
| `--hm-orgchart-company-halla` | 공개 입력(상속) | [color-presets.css:97](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="halla"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-halla, #0068B7)` |
|
||||
| `--hm-orgchart-company-halla-secondary-1` | 공개 입력(상속) | [color-presets.css:99](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="halla"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-halla-secondary-1, #231815)` |
|
||||
| `--hm-orgchart-company-hanmac` | 공개 입력(상속) | [color-presets.css:66](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` |
|
||||
| `--hm-orgchart-company-hanmac-secondary-1` | 공개 입력(상속) | [color-presets.css:68](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-hanmac-secondary-1, #1D4289)` |
|
||||
| `--hm-orgchart-company-jangheon` | 공개 입력(상속) | [color-presets.css:84](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-jangheon, #1D418D)` |
|
||||
| `--hm-orgchart-company-jangheon-industry` | 공개 입력(상속) | [color-presets.css:78](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon-industry"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-jangheon-industry, #4580A6)` |
|
||||
| `--hm-orgchart-company-jangheon-industry-secondary-1` | 공개 입력(상속) | [color-presets.css:80](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon-industry"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-jangheon-industry-secondary-1, #92BED4)` |
|
||||
| `--hm-orgchart-company-jangheon-secondary-1` | 공개 입력(상속) | [color-presets.css:86](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-jangheon-secondary-1, #AE232A)` |
|
||||
| `--hm-orgchart-company-ptc` | 공개 입력(상속) | [color-presets.css:90](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-ptc, #84338E)` |
|
||||
| `--hm-orgchart-company-ptc-secondary-1` | 공개 입력(상속) | [color-presets.css:92](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-ptc-secondary-1, #F7AF00)` |
|
||||
| `--hm-orgchart-company-ptc-secondary-2` | 공개 입력(상속) | [color-presets.css:93](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-2` | `var(--hm-orgchart-company-ptc-secondary-2, #0066B5)` |
|
||||
| `--hm-orgchart-company-saman` | 공개 입력(상속) | [color-presets.css:72](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="saman"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-saman, #F08300)` |
|
||||
| `--hm-orgchart-company-saman-secondary-1` | 공개 입력(상속) | [color-presets.css:74](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="saman"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-saman-secondary-1, #6D6C6C)` |
|
||||
| `--hm-orgchart-current-border` | 내부/기존 선언 | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` |
|
||||
| `--hm-orgchart-current-card-background` | 내부/기존 선언 | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-current-card-color` | 내부/기존 선언 | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-current-department-color` | 내부/기존 선언 | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-current-gradient` | 내부/기존 선언 | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` |
|
||||
| `--hm-orgchart-department-color` | 공개 입력(상속) | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-design-planning-border` | 공개 입력(상속) | [color-presets.css:49](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-design-planning-border, var(--hm-orgchart-border, #09a7a2))` |
|
||||
| `--hm-orgchart-design-planning-card-background` | 공개 입력(상속) | [color-presets.css:46](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-design-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-design-planning-card-color` | 공개 입력(상속) | [color-presets.css:47](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-design-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-design-planning-department-color` | 공개 입력(상속) | [color-presets.css:48](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-design-planning-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-design-planning-gradient` | 공개 입력(상속) | [color-presets.css:50](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-design-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #09a7a2 0%, #00847f 100%)))` |
|
||||
| `--hm-orgchart-dialog-background` | 공개 입력(상속) | [employee-info-dialog.css:21](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `background` | `var(--hm-orgchart-dialog-background, var(--hm-orgchart-resolved-bg-surface))` |
|
||||
| `--hm-orgchart-dialog-border` | 공개 입력(상속) | [employee-info-dialog.css:19](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `border` | `var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-dialog-border, var(--hm-orgchart-resolved-stroke-default))` |
|
||||
| `--hm-orgchart-dialog-close-color` | 공개 입력(상속) | [employee-info-dialog.css:38](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ink` | `var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d)))` |
|
||||
| `--hm-orgchart-dialog-close-hover` | 내부/기존 선언 | [employee-info-dialog.css:39](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-hover` | `var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)))` |
|
||||
| `--hm-orgchart-dialog-close-hover-background` | 공개 입력(상속) | [employee-info-dialog.css:39](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-hover` | `var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)))` |
|
||||
| `--hm-orgchart-dialog-close-ink` | 내부/기존 선언 | [employee-info-dialog.css:38](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ink` | `var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d)))` |
|
||||
| `--hm-orgchart-dialog-close-ring` | 내부/기존 선언 | [employee-info-dialog.css:40](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ring` | `var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651)))` |
|
||||
| `--hm-orgchart-dialog-color` | 공개 입력(상속) | [employee-info-dialog.css:22](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `color` | `var(--hm-orgchart-dialog-color, var(--hm-orgchart-resolved-fg-primary))` |
|
||||
| `--hm-orgchart-dialog-focus-color` | 공개 입력(상속) | [employee-info-dialog.css:40](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ring` | `var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651)))` |
|
||||
| `--hm-orgchart-dispatched-card-border` | 공개 입력(상속) | [org-chart-expanded.css:107](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `border-color` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default)))` |
|
||||
| `--hm-orgchart-dispatched-card-color` | 공개 입력(상속) | [org-chart-expanded.css:108](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-dispatched-card-gradient` | 공개 입력(상속) | [org-chart-expanded.css:105](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-resolved-role-card-background))` |
|
||||
| `--hm-orgchart-dispatched-title-background` | 공개 입력(상속) | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` |
|
||||
| `--hm-orgchart-expanded-card-accent-width` | 내부/기존 선언 | [org-chart-expanded.css:154](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-card-accent-width` | `3px` |
|
||||
| `--hm-orgchart-expanded-card-border-width` | 내부/기존 선언 | [org-chart-expanded.css:153](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-card-border-width` | `1px` |
|
||||
| `--hm-orgchart-expanded-card-primary` | 내부/기존 선언 | [color-presets.css:67](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-card-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` |
|
||||
| `--hm-orgchart-expanded-line-color` | 공개 입력(상속) | [org-chart-expanded.css:10](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-resolved-expanded-line-color` | `var(--hm-orgchart-expanded-line-color, #738399)` |
|
||||
| `--hm-orgchart-expanded-line-width` | 내부/기존 선언 | [org-chart-expanded.css:8](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-line-width` | `var(--hm-orgchart-resolved-border-width)` |
|
||||
| `--hm-orgchart-expanded-primary` | 내부/기존 선언 | [color-presets.css:66](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` |
|
||||
| `--hm-orgchart-expanded-secondary-1` | 내부/기존 선언 | [color-presets.css:68](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-hanmac-secondary-1, #1D4289)` |
|
||||
| `--hm-orgchart-expanded-secondary-2` | 내부/기존 선언 | [color-presets.css:93](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-2` | `var(--hm-orgchart-company-ptc-secondary-2, #0066B5)` |
|
||||
| `--hm-orgchart-fg-primary` | 공개 입력(상속) | [design-tokens.css:12](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-primary` | `var(--hm-orgchart-fg-primary, #10243b)` |
|
||||
| `--hm-orgchart-fg-secondary` | 공개 입력(상속) | [design-tokens.css:13](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-secondary` | `var(--hm-orgchart-fg-secondary, #596575)` |
|
||||
| `--hm-orgchart-fixed-line-color` | 공개 입력(상속) | [org-chart-readonly.css:204](../../packages/org-chart/src/styles/org-chart-readonly.css) | `.hm-orgchart-workdesk-overlay path[data-connector]` → `stroke` | `var(--hm-orgchart-fixed-line-color, #45639F)` |
|
||||
| `--hm-orgchart-gradient` | 공개 입력(상속) | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` |
|
||||
| `--hm-orgchart-group-background` | 내부/기존 선언 | [color-presets.css:12](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-group-background` | `var(--hm-orgchart-current-gradient)` |
|
||||
| `--hm-orgchart-group-border` | 공개 입력(상속) | [org-chart-content.css:73](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization` → `border-top` | `var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-group-border, var(--hm-orgchart-current-border))` |
|
||||
| `--hm-orgchart-group-title-background` | 공개 입력(상속) | [org-chart-content.css:100](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-header-role="organization"] .hm-orgchart-organization-name` → `background` | `var(--hm-orgchart-group-title-background, var(--hm-orgchart-group-background))` |
|
||||
| `--hm-orgchart-group-title-color` | 공개 입력(상속) | [org-chart-content.css:95](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization-name` → `color` | `var(--hm-orgchart-group-title-color, var(--hm-orgchart-current-department-color))` |
|
||||
| `--hm-orgchart-integrated-system-border` | 공개 입력(상속) | [color-presets.css:39](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-integrated-system-border, var(--hm-orgchart-border, #8c67b6))` |
|
||||
| `--hm-orgchart-integrated-system-card-background` | 공개 입력(상속) | [color-presets.css:36](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-integrated-system-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-integrated-system-card-color` | 공개 입력(상속) | [color-presets.css:37](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-integrated-system-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-integrated-system-department-color` | 공개 입력(상속) | [color-presets.css:38](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-integrated-system-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-integrated-system-gradient` | 공개 입력(상속) | [color-presets.css:40](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-integrated-system-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #8c67b6 0%, #5d3494 100%)))` |
|
||||
| `--hm-orgchart-leader-card-background` | 공개 입력(상속) | [org-chart-expanded.css:99](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="leader"]` → `background` | `var(--hm-orgchart-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-on-accent` | 공개 입력(상속) | [design-tokens.css:18](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-accent` | `var(--hm-orgchart-on-accent, #224387)` |
|
||||
| `--hm-orgchart-on-action-primary` | 공개 입력(상속) | [design-tokens.css:20](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-action-primary` | `var(--hm-orgchart-on-action-primary, #ffffff)` |
|
||||
| `--hm-orgchart-on-leave-card-border` | 공개 입력(상속) | [org-chart-expanded.css:113](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `border-color` | `var(--hm-orgchart-on-leave-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default)))` |
|
||||
| `--hm-orgchart-on-leave-card-color` | 공개 입력(상속) | [org-chart-expanded.css:114](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `color` | `var(--hm-orgchart-on-leave-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-on-leave-card-gradient` | 공개 입력(상속) | [org-chart-expanded.css:111](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-on-leave-card-gradient, var(--hm-orgchart-resolved-role-card-background))` |
|
||||
| `--hm-orgchart-on-leave-title-background` | 공개 입력(상속) | [summary-sections.css:148](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="onLeave"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-on-leave-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` |
|
||||
| `--hm-orgchart-person-position-color` | 내부/기존 선언 | [employee-card.css:206](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"])` → `--hm-orgchart-person-position-color` | `#797979` |
|
||||
| `--hm-orgchart-resolved-action-primary` | 내부/기존 선언 | [design-tokens.css:19](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-action-primary` | `var(--hm-orgchart-action-primary, #45639f)` |
|
||||
| `--hm-orgchart-resolved-bg-surface` | 내부/기존 선언 | [design-tokens.css:8](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-surface` | `var(--hm-orgchart-bg-surface, #ffffff)` |
|
||||
| `--hm-orgchart-resolved-border-top-width` | 내부/기존 선언 | [design-tokens.css:35](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-border-top-width` | `var(--hm-orgchart-border-top-width, 3px)` |
|
||||
| `--hm-orgchart-resolved-border-width` | 내부/기존 선언 | [design-tokens.css:34](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-border-width` | `var(--hm-orgchart-border-width, 1px)` |
|
||||
| `--hm-orgchart-resolved-expanded-line-color` | 내부/기존 선언 | [org-chart-expanded.css:10](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-resolved-expanded-line-color` | `var(--hm-orgchart-expanded-line-color, #738399)` |
|
||||
| `--hm-orgchart-resolved-fg-primary` | 내부/기존 선언 | [design-tokens.css:12](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-primary` | `var(--hm-orgchart-fg-primary, #10243b)` |
|
||||
| `--hm-orgchart-resolved-on-action-primary` | 내부/기존 선언 | [design-tokens.css:20](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-action-primary` | `var(--hm-orgchart-on-action-primary, #ffffff)` |
|
||||
| `--hm-orgchart-resolved-role-card-background` | 내부/기존 선언 | [org-chart-expanded.css:119](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-employee[data-card-kind="employee"]` → `--hm-orgchart-resolved-role-card-background` | `var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))` |
|
||||
| `--hm-orgchart-resolved-status-card-background` | 내부/기존 선언 | [org-chart-expanded.css:105](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-resolved-role-card-background))` |
|
||||
| `--hm-orgchart-resolved-status-card-border` | 내부/기존 선언 | [summary-sections.css:144](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-border` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35)))` |
|
||||
| `--hm-orgchart-resolved-status-card-color` | 내부/기존 선언 | [summary-sections.css:145](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-status-card-color, #3e4650))` |
|
||||
| `--hm-orgchart-resolved-status-card-gradient` | 내부/기존 선언 | [summary-sections.css:143](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-gradient` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%)))` |
|
||||
| `--hm-orgchart-resolved-status-title-background` | 내부/기존 선언 | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` |
|
||||
| `--hm-orgchart-resolved-stroke-default` | 내부/기존 선언 | [design-tokens.css:14](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-default` | `var(--hm-orgchart-stroke-default, #bac7d7)` |
|
||||
| `--hm-orgchart-resolved-stroke-input-focus` | 내부/기존 선언 | [design-tokens.css:16](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-focus` | `var(--hm-orgchart-stroke-input-focus, #45639f)` |
|
||||
| `--hm-orgchart-solution-sales-border` | 공개 입력(상속) | [color-presets.css:59](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-solution-sales-border, var(--hm-orgchart-border, #5a963c))` |
|
||||
| `--hm-orgchart-solution-sales-card-background` | 공개 입력(상속) | [color-presets.css:56](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-solution-sales-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-solution-sales-card-color` | 공개 입력(상속) | [color-presets.css:57](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-solution-sales-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-solution-sales-department-color` | 공개 입력(상속) | [color-presets.css:58](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-solution-sales-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-solution-sales-gradient` | 공개 입력(상속) | [color-presets.css:60](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-solution-sales-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #5a963c 0%, #3f7629 100%)))` |
|
||||
| `--hm-orgchart-status-card-border` | 공개 입력(상속) | [summary-sections.css:144](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-border` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35)))` |
|
||||
| `--hm-orgchart-status-card-color` | 공개 입력(상속) | [summary-sections.css:145](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-status-card-color, #3e4650))` |
|
||||
| `--hm-orgchart-status-card-gradient` | 공개 입력(상속) | [summary-sections.css:143](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-gradient` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%)))` |
|
||||
| `--hm-orgchart-status-position-color` | 공개 입력(상속) | [summary-sections.css:139](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-role` → `color` | `var(--hm-orgchart-status-position-color, #555f6a)` |
|
||||
| `--hm-orgchart-status-title-background` | 공개 입력(상속) | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` |
|
||||
| `--hm-orgchart-status-title-border` | 공개 입력(상속) | [summary-sections.css:129](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group > .hm-orgchart-summary-title` → `border` | `1px solid var(--hm-orgchart-status-title-border, rgba(92, 101, 112, 0.52))` |
|
||||
| `--hm-orgchart-status-title-color` | 공개 입력(상속) | [org-chart-readonly.css:275](../../packages/org-chart/src/styles/org-chart-readonly.css) | `.hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-status-group > .hm-orgchart-summary-title` → `color` | `var(--hm-orgchart-status-title-color, var(--hm-orgchart-resolved-on-action-primary))` |
|
||||
| `--hm-orgchart-stroke-default` | 공개 입력(상속) | [design-tokens.css:14](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-default` | `var(--hm-orgchart-stroke-default, #bac7d7)` |
|
||||
| `--hm-orgchart-stroke-input-default` | 공개 입력(상속) | [design-tokens.css:15](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-default` | `var(--hm-orgchart-stroke-input-default, #d3dce8)` |
|
||||
| `--hm-orgchart-stroke-input-focus` | 공개 입력(상속) | [design-tokens.css:16](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-focus` | `var(--hm-orgchart-stroke-input-focus, #45639f)` |
|
||||
| `--hm-orgchart-talent-growth-border` | 공개 입력(상속) | [color-presets.css:29](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-talent-growth-border, var(--hm-orgchart-border, #0f6ace))` |
|
||||
| `--hm-orgchart-talent-growth-card-background` | 공개 입력(상속) | [color-presets.css:26](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-talent-growth-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
| `--hm-orgchart-talent-growth-card-color` | 공개 입력(상속) | [color-presets.css:27](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-talent-growth-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` |
|
||||
| `--hm-orgchart-talent-growth-department-color` | 공개 입력(상속) | [color-presets.css:28](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-talent-growth-department-color, var(--hm-orgchart-department-color, #ffffff))` |
|
||||
| `--hm-orgchart-talent-growth-gradient` | 공개 입력(상속) | [color-presets.css:30](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-talent-growth-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #0f6ace 0%, #044a94 100%)))` |
|
||||
| `--hm-orgchart-top-leader-card-background` | 공개 입력(상속) | [org-chart-expanded.css:102](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="topLeader"]` → `background` | `var(--hm-orgchart-top-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` |
|
||||
@@ -0,0 +1,109 @@
|
||||
# HMDesign OrgChart P11.6 — 최종 정합성·병합 준비 검증
|
||||
|
||||
RESULT: **PARTIAL — 검증한 기능·정합성 PASS, 배포 플랫폼 일부 NOT VERIFIED.**
|
||||
기술 판정: **CONDITIONALLY READY.** 아래 Feature Git 완료 확인을 전제로 하며 실제 main Merge·Push·배포 승인을 뜻하지 않는다.
|
||||
|
||||
## 1. 기준과 범위
|
||||
|
||||
- 작업 경로: `D:\디자인시스템\HMDesign-OrgChart`.
|
||||
- 작업 시작: 2026-10-14 08:45:22 KST. 증거 생성·검토: 같은 날 08:45–08:51 KST.
|
||||
- Feature: `feature/orgchart-package`, 검사 전 HEAD `cd7a0db916c3eed75e5ebf4155828bf3727d21d5`.
|
||||
- 공식 origin: `https://gitea.hmac.kr/b25027/HMDesign.git`.
|
||||
- 읽기 전용 원격 조회로 Feature HEAD 일치 및 main `c45d63a03b84eaac43f01bd400a16488a80e4229` 확인.
|
||||
- 초기 Working Tree: P11.5 관련 5개 수정, Untracked 0. 기존 변경을 모두 보존했다.
|
||||
- 초기 Git user.name은 `김수현`, email은 `shkim4@samaneng.com`. 이번 Commit에만 요청된 `b25027` / `shkim4@samaneng.com` 작성자를 지정한다.
|
||||
- P11.4 `deployment-rollback-review-p11-4.md`는 없음. 재작성·Stage하지 않는다. 외부 보관 위치 NOT VERIFIED, 기존 롤백 PARTIAL 유지.
|
||||
|
||||
P11.6은 정합성 검사 및 같은 작업 범위의 Feature Commit·일반 Push 준비다. 신규 화면·Story·Props·기본 회사색·승인 디자인·Workflow·Docker 정의를 수정하지 않았다.
|
||||
|
||||
## 2. 변경 파일
|
||||
|
||||
이번 Commit 대상은 다음 6개로 한정한다.
|
||||
|
||||
1. `packages/org-chart/src/styles/org-chart-content.css` — Fixed interactive composition/stage 전용 입력, 투명 fallback.
|
||||
2. `packages/org-chart/src/styles/org-chart-readonly.css` — Fixed readOnly composition/stage 전용 입력 및 기존 bg-content fallback.
|
||||
3. `packages/org-chart/src/styles/org-chart-expanded.css` — Auto 내부 viewport 전용 입력 및 기존 bg-surface fallback.
|
||||
4. `docs/org-chart/org-chart-usage-guide.md` — P11.5 배경 설정과 P11.6 한정 검증 근거.
|
||||
5. `docs/org-chart/design-approval.md` — 사용자 확정 Expanded 내부 흰색 변경 기록.
|
||||
6. `docs/org-chart/consistency-review-p11-6.md` — 이 보고서.
|
||||
|
||||
P11.6에서 추가 제품 오류를 발견하지 않아 제품 코드 추가 수정은 없다. `test-results`, `_local`, node_modules/dist, 캡처·로그·개인 설정·Cell 실험은 제외한다.
|
||||
|
||||
## 3. Core·React·데이터·사용 가이드 정합성
|
||||
|
||||
**PASS — 실제 소스·공개 exports·타입과 가이드의 관련 예제 대조.**
|
||||
|
||||
- Core의 주 Entry·CSS 2개·IIFE browser Entry 및 DataSource/Validator/집계/이동/모듈 export가 manifest와 일치한다. React/Vanilla 모두 같은 Core를 사용한다.
|
||||
- Fixed 기본/interactive/readOnly, Auto TB/LR/Hybrid, 독립 interaction flags, Fit/Zoom/Pan, 그룹 접기, 직원 팝업, 기본 OFF Drag & Drop, 회사 선택과 데이터 갱신의 지원 범위·제약이 타입·구현과 일치한다.
|
||||
- Chart Only는 Auto 전용 `presentationMode: 'chart-only'`, 초기 Fit·direct wheel 기본값. Fixed 조합은 TypeError로 거부하며 신규 Fixed Chart Only를 만들지 않았다.
|
||||
- 데이터의 schemaVersion, Organization/Employee/affiliation 및 metadata, nullable 필드, 계층·조직장 참조 제약을 실제 타입/Validator와 대조했다. 문서 JSON 2개를 현재 빌드의 Validator로 실행해 통과했다.
|
||||
- React 기본 Entry는 `OrgChart`와 UI-independent 타입을 제공한다. `/hm-dialog`만 HMEmployeeDialog를 연결하며 employeePopup의 명시적 선택이 필요하다. 과거 lazy HM UI MISSING_EXPORT 문제를 현재 미해결 상태로 해석하지 않는다.
|
||||
- Core 옵션 전달 및 CSS URL 연결은 기존 Props 그대로다. 배경 입력은 class/style/외부 CSS 상속 경로이며 배경용 Props를 추가하지 않았다.
|
||||
- optionKey 기반 값 비교와 stylesheet URL primitive 의존성, 데이터 identity reload, Auto setter를 실제 Wrapper에서 확인했다. 동일 값의 옵션 객체만 바뀌면 create/destroy를 반복하지 않는다. 이 생명주기 전체를 새 브라우저 검사했다고 확대하지 않는다.
|
||||
- React peer `^19.0.0`, 실제 설치 React 19.3.0. 현재 소스·전체 타입/빌드와 기존 P11.5 React 브라우저 증거를 확인했다. 다른 React major/SSR 지원은 NOT VERIFIED.
|
||||
|
||||
가이드의 TS/TSX 블록 **14개**를 공개 빌드 `.d.ts` 기준 strict·skipLibCheck=false로 통과시켰다. 문서에서 앞 절의 문맥을 요구하는 블록 2개에만 Import/데이터 문맥을 보충했다. JS 부분 예제는 실제 API와 수동 대조했으며 모든 예제를 별도로 브라우저 실행하지 않았다.
|
||||
|
||||
## 4. 배경·외부 테마·Shadow DOM
|
||||
|
||||
**PASS — CSS 대조, P11.5 증거 재사용 및 추가 Computed Style 검사.**
|
||||
|
||||
- `--hm-orgchart-fixed-canvas-background`는 Fixed composition/stage에 적용한다. readOnly는 기존 `--hm-orgchart-bg-content` → `#F5F8FB`, interactive는 기존 transparent를 유지한다.
|
||||
- `--hm-orgchart-expanded-canvas-background`는 Auto 내부 viewport에 적용한다. 기존 `--hm-orgchart-resolved-bg-surface` 경로를 보존하고 기본은 `#FFFFFF`다. 외부 카드/프레임/Storybook 페이지와 직원 카드에는 전용 입력을 전파하지 않는다.
|
||||
- 두 입력은 var 참조로 소비하며 내부 요소에 기본 custom property를 선언해 호스트 상속을 차단하지 않는다. bg-surface/bg-content 기존 외부 테마, host 상속·Shadow :host·소비자 inline의 우선순위를 유지한다.
|
||||
- React/Vanilla 각각 기본/Fixed만 custom/Expanded만 custom/서로 다른 custom/제거 후 기본값 복구를 추가 검사했다. 같은 페이지의 Chart Only sibling과 다른 runtime에 영향이 없었다.
|
||||
- Chart Only의 전용 Expanded custom 배경 및 제거 후 흰색 복구를 실제 viewport computed style로 확인했다.
|
||||
- P11.5의 진짜 외부 CSS `@import + :host`, 반대 유형 입력 무영향, 같은 composition/stage 입력 결과를 재사용했다.
|
||||
- 깨끗한 Core CSS와 P11.5 산출물은 줄바꿈 정규화 후 동일하다. Core/React 기본 JS 산출물은 byte 동일하다. 원본의 정상 dist를 수정하지 않았다.
|
||||
|
||||
## 5. Storybook·승인 디자인·Chart Only
|
||||
|
||||
**PASS — P11.5 한정 회귀 증거 재사용, 새 빌드의 4개 화면 렌더 확인.**
|
||||
|
||||
- Overview: 회사 로고·현황·카드·텍스트·외부 Container의 P11.5 주요 computed style/geometry 동일성 재사용.
|
||||
- Department Colors: 6종 Gradient·그룹/직원·파견/휴직·`홍 길 동` 및 기존 프레임의 P11.5 동일성 재사용. Expanded 7개 회사색과 분리 유지.
|
||||
- Full Composition: Fixed 기존 BG·배경 이미지/자산·카드·연결선/화살표·통계·중앙/반응형 구조에 추가 변경 없음. P11.5 before/after Full Composition PNG byte 동일성과 주요 스타일 동일성 재사용.
|
||||
- Expanded OrgChart: 내부 viewport `rgb(255, 255, 255)`를 새 Storybook 빌드에서도 측정했다. P11.5 외부 배경·geometry·PNG 동일성 재사용. 회사별 20/100/300 선택, TB/LR/Hybrid, HM popup·Drag & Drop 컨트롤의 소스·구성은 변경 없음. 모든 조합을 새로 실행했다고 주장하지 않는다.
|
||||
- 승인 Story ID 유지: `orgchart--logo-personnel-summary`, `orgchart--department-colors`, `orgchart--full-composition`, `orgchart--expanded-org-chart`. 새 Story 추가 없음.
|
||||
- Chart Only: P11.5 React/Vanilla 초기 Fit·wheel Zoom·Pan·popup/focus 복귀·stable-ID 접기/펼치기 PASS 재사용. 외부 제목/요약/툴바 미생성·Fixed 미지원은 현재 코드 확인. 이번에는 custom BG와 제거·인스턴스 격리를 추가 실행했다.
|
||||
- 승인 문서의 5개 대상 및 P11.5 변경 한정 범위가 구현과 일치한다. 7개 회사색 `#CB2E28`, `#F08300`, `#4580A6`, `#1D418D`, `#84338E`, `#0068B7`, `#0C367F` 및 흰색 그룹 제목은 그대로다.
|
||||
- AGENTS.md·CLAUDE.md·AI 생성 정책의 Chart Only/Full Page 구분, 기본 승인 디자인·외부 CSS custom·정확한 사용자 데이터·Fixed Chart Only 미지원 정책에 충돌 없음.
|
||||
|
||||
P11.5 최초 검사 **33 PASS/선택자 harness FAIL 2** 및 실패 한정 보완 **15/15 PASS** 원본을 보존한다. 전체 48개 제품 PASS로 단순 합산하지 않는다. 별도 역사적 승인 PNG 비교와 모든 viewport/동작 조합의 신규 회귀는 NOT VERIFIED다.
|
||||
|
||||
## 6. 통합 검증과 증거
|
||||
|
||||
로컬 ignored 증거는 `test-results/org-chart-p11-6/`에 보존하며 Commit에서 제외한다. P11.5 증거는 `test-results/org-chart-p11-5/final-result.json`, `browser-result.json`, `recheck-result.json`, `baseline.json`, `after.json`, before/after PNG를 검토·재사용했다.
|
||||
|
||||
- `npm.cmd ci --offline --no-audit --no-fund`: 격리 복사본에서 PASS, 552 packages 설치. 첫 sandbox EPERM은 권한 문제이며 정상 허용 실행에서 해결했다. 신규 audit는 수행하지 않았다.
|
||||
- main Docker 순서 그대로 `npm.cmd run build` → `npm.cmd run build-storybook`: 모두 PASS. prebuild의 패키지 빌드 반복도 Docker 실제 명령대로 수행했다. 로그 `build.log`, `build-storybook.log`.
|
||||
- `npm.cmd run typecheck --ignore-scripts`: PASS, 전체 workspace. 이미 완료한 prebuild를 다시 실행하지 않았다. 로그 `typecheck.log`.
|
||||
- `node test-results/org-chart-p11-6/audit.mjs` 및 격리 TypeScript의 `-p .../test-results/snippets`: PASS, 공개 선언 예제 14개·JSON 2개·JS/CSS/lockfile 정합성. `audit.json`, `snippet-typecheck.log`.
|
||||
- `node test-results/org-chart-p11-6/browser.mjs`: 추가 **20/20 PASS**, 브라우저 오류 0. `browser.json`, `browser.log`. 실제 URL은 `http://127.0.0.1:6046/storybook-static/iframe.html?id=orgchart--<기존ID>&viewMode=story`, fixture는 `/_local/p11-6/fixture.html`이다.
|
||||
- Guard: `node scripts/ai-generation-guard.mjs --target . --profile STORYBOOK --scope FULL_SCREEN --json`. `guard-full.json`: **FAIL 0, NOT VERIFIED 98**, status/staticStatus/runtimeStatus **NOT VERIFIED**. CSS phases **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**, 결합 styleVerification 미생성. 전체 Storybook import graph 범위이며 모든 package source CSS의 실행 검증을 뜻하지 않는다. 변경 CSS의 P11.5 scoped Guard FAIL 0도 재사용했다.
|
||||
- 기존 `bg-[var(...)]` CSS 최적화, `host-context` minifier, 큰 bundle 경고는 보존했다. Build exit 0이며 이번 신규 제품 오류와 구분한다. 검사 규칙/예외를 변경하지 않았다.
|
||||
|
||||
전체 Core/browser 회귀·접근성·성능·SSR/hydration·다른 브라우저·전 화면 디바이스 재검증은 수행하지 않았다. 기존 정상 검사를 무조건 반복하지 않는다.
|
||||
|
||||
## 7. main 병합·배포 빌드 호환성
|
||||
|
||||
- Merge Base: `d18e3c33801e165e17b6d971e2f6198eed483c3c`.
|
||||
- 검사 전 main-only 3 / Feature-only 16 Commit. **Fast-forward 불가, Merge Commit 필요**.
|
||||
- main-only 변경은 `.dockerignore`, `.gitea/workflows/deploy-storybook.yml`, `.gitignore`, `Dockerfile`, `README.md`, `compose.yaml` 6개다. Feature 변경 경로와 겹치지 않는다.
|
||||
- 읽기 전용 `git merge-tree <base> origin/main HEAD` 결과에 changed-in-both/충돌 marker가 없다. 원본 `merge-tree.txt` 보존. 새 P11.6 6개 경로도 main 변경과 겹치지 않는다. 검토한 SHA 기준 **예상 텍스트 충돌 없음**.
|
||||
- `.storybook/main.ts` 변경 없음, preview는 기존 승인 OrgChart 4개 순서 추가. main 쪽 설정 경쟁 변경 없음.
|
||||
- Feature의 manifest/lockfile은 OrgChart workspace·엔진을 포함한다. **Core → UI → Center → React** 빌드 순서라 UI 타입 생성 전에 React가 빌드되는 과거 문제가 해결되어 있다.
|
||||
- 격리 디렉터리는 `git archive HEAD`에 P11.5 현재 5개 파일과 main 전용 6개 파일을 복사한 예상 병합 source다. 입력 경로가 서로 겹치지 않음을 먼저 확인했고 Git 브랜치를 실제 병합하지 않았다.
|
||||
- 설치 전에 4개 패키지에 dist/node_modules가 없음을 확인했다. 격리 `npm ci`로 별도 node_modules를 설치했다. root lockfile SHA-256 `fae6ace49f0cfb2c9fe606e2fdd2f1fdf9ef9f19139f9a8628a7719f0a75c9b9` 동일 유지. 원본 node_modules·dist 보존.
|
||||
- main Docker 정의는 node:24-alpine에서 npm ci → npm run build → npm run build-storybook, nginx:1.29-alpine으로 Storybook 산출물 제공이다. main Workflow는 main Push/수동 실행 시 immutable source archive 전송 후 compose build/up 한다.
|
||||
- **동일 명령의 clean source 빌드 호환성 PASS**. 실행 환경은 Windows Node 22.23.2/npm 10.9.8이다. Docker 실행 파일이 없으므로 Linux/Node 24 Alpine 컨테이너 build, nginx serving, 실제 Gitea runner/서버/SSH/복구는 **NOT VERIFIED**. 이를 Docker 배포 Runtime PASS로 올리지 않는다.
|
||||
|
||||
## 8. Git 완료 조건과 최종 판정
|
||||
|
||||
기능·문서·승인 디자인의 변경 범위 PASS, 추가 브라우저 20/20, clean build/typecheck PASS, 실행한 Guard FAIL 0, 예상 충돌 없음으로 Feature Commit·일반 Push 조건을 충족했다. 이번 Commit 메시지는 `fix(org-chart): separate fixed and expanded canvas backgrounds`다. Commit 직전 원격 main/Feature가 검사 기준에서 바뀌지 않았는지 다시 확인하며 위 6개 파일만 Stage·staged diff 검토한다.
|
||||
|
||||
이 문서를 포함하는 최종 Commit SHA·원격 일치·Working Tree·main 유지 여부는 작업 완료 응답 및 로컬 `test-results/org-chart-p11-6/git-final.json`에 기록한다. Git 실행 실패나 원격 변경이 있으면 완료로 간주하지 않는다. main 실제 Merge·Push·자동 배포는 이번 범위가 아니다.
|
||||
|
||||
**CONDITIONALLY READY**: 확인한 소스의 배포 명령 빌드와 정합성은 PASS다. Docker/운영 플랫폼 검증 제한을 남기고 별도의 사용자 병합·배포 지시를 기다린다. 실제 병합 직전에 최신 원격 SHA를 다시 조회하여 새 변경·충돌 여부를 확인해야 한다. 이번 검증으로 운영 위험 수용이 승인된 것은 아니다.
|
||||
|
||||
기존 위험은 유지한다: 자동 롤백 없음, P11.4 롤백 검토 PARTIAL, 서버 복구 Runtime NOT VERIFIED, 개발 의존성 High Advisory 7건 처리 방침 PENDING(이번 신규 audit 없음), 삼안 약 2.64:1·장헌산업 약 4.29:1 흰색 제목 대비 미달, Guard Runtime NOT VERIFIED. 디자인 승인은 WCAG AA 준수/조직 위험 수용 승인이 아니며 롤백 검증을 병합 필수 조건으로 두지 않는다는 방향도 복구 성공 보장이나 포괄 위험 수용을 의미하지 않는다.
|
||||
@@ -0,0 +1,262 @@
|
||||
# OrgChart Data Contract
|
||||
|
||||
상태: **JSON Schema / TypeScript 초안, 승인 전**. ID 기반 분리·동일 계약 사용·Validation·원본 집계는 확정 요구다. 아래 필드명·enum·null·버전 값은 제안이다. 모든 환경이 이 계약과 같은 검증·집계를 공유한다. [Architecture](architecture.md), [API 전환](integration.md)을 함께 읽는다.
|
||||
|
||||
## 1. 정규화 모델과 TypeScript 초안
|
||||
|
||||
ID는 엔티티별 배열 안에서 유일한 불투명 문자열이다. 이름·배열 위치로 연결하지 않는다. 조직은 `parentId`, 직원은 `organizationId`·`affiliationId`, 조직장은 `leaderId`, 추가 관계는 두 조직 ID를 참조한다. 서로 다른 엔티티의 같은 ID 문자열은 허용할지 TBD이며, DOM에는 엔티티·인스턴스 prefix를 별도로 붙인다.
|
||||
|
||||
```ts
|
||||
type ID = string;
|
||||
type OrganizationType = "root" | "division" | "office" | "div" | "cell" | "team";
|
||||
type WorkStatus = "active" | "dispatched" | "onLeave";
|
||||
type RelationType = "cooperation" | "support";
|
||||
|
||||
interface Organization {
|
||||
id: ID;
|
||||
companyId?: ID; // P5-2/P5-3 Auto 회사 Root 식별. 하위 조직은 상속, 기존 JSON 생략 허용.
|
||||
parentId: ID | null;
|
||||
name: string;
|
||||
type: OrganizationType;
|
||||
leaderId: ID | null;
|
||||
displayOrder: number;
|
||||
}
|
||||
interface Employee {
|
||||
id: ID;
|
||||
organizationId: ID;
|
||||
affiliationId: ID;
|
||||
name: string;
|
||||
position: string; // 직위. jobTitle(직책)과 구별
|
||||
jobTitle: string | null;
|
||||
profileImage: string | null;
|
||||
workStatus: WorkStatus;
|
||||
}
|
||||
interface Affiliation { id: ID; name: string }
|
||||
interface OrganizationRelation {
|
||||
id: ID;
|
||||
sourceOrganizationId: ID;
|
||||
targetOrganizationId: ID;
|
||||
type: RelationType;
|
||||
}
|
||||
interface PresentationConfig {
|
||||
title: string;
|
||||
description: string | null;
|
||||
hubText: string | null;
|
||||
groups: {
|
||||
showEmployees: boolean;
|
||||
showSummary: boolean;
|
||||
showDispatched: boolean;
|
||||
showOnLeave: boolean;
|
||||
showAffiliations: boolean;
|
||||
};
|
||||
organizationOrder: ID[];
|
||||
}
|
||||
interface Metadata {
|
||||
schemaVersion: "0.1-draft";
|
||||
asOfDate: string; // YYYY-MM-DD, 유효한 실제 날짜
|
||||
dataVersion: string;
|
||||
updatedAt: string | null; // timezone을 포함한 RFC 3339 갱신 시각
|
||||
updateNote: string | null;
|
||||
}
|
||||
interface OrgChartData {
|
||||
organizations: Organization[];
|
||||
employees: Employee[];
|
||||
affiliations: Affiliation[];
|
||||
relations: OrganizationRelation[];
|
||||
presentation: PresentationConfig;
|
||||
metadata: Metadata;
|
||||
}
|
||||
```
|
||||
|
||||
제안된 `root/division/office/div/cell/team`은 최상위/부문/실/Div/셀/팀 표현이다. 실제 조직 유형·업무 상태·관계 유형 vocabulary, 직위 필수 여부, 무소속·관계사 미지정 허용은 **TBD**다. 새로운 enum은 schema/API 버전과 함께 확장하고 알 수 없는 값을 임의 재직 상태로 치환하지 않는다.
|
||||
|
||||
`schemaVersion`은 계약 호환성, `dataVersion`은 스냅샷 식별이다. 서로 대신 쓰지 않는다. 부모 null은 root 후보, leader null은 공석 후보, 이미지 null은 가상 fallback을 뜻하는 초안이다. API에서 필드 생략이 가능한지와 null 허용은 승인 후 확정한다.
|
||||
|
||||
표현 설정은 표시만 바꾸고 원본 소속·집계 분모를 변경하지 않는다. 순서는 제안상 `organizationOrder`에 있는 ID 우선 → 나머지 형제 `displayOrder` → 동률 ID 순서로 결정한다. 전역 목록이 계층 parent를 바꾸지는 않는다. 실제 정렬 우선순위는 TBD다. PresentationConfig에 CSS·HTML·DOM 좌표·API URL을 받지 않는다.
|
||||
|
||||
## 2. JSON Schema 초안
|
||||
|
||||
JSON Schema Draft 2020-12 제안이다. `format`을 실제 검사하는 validator 설정이 필요하다. 아래 구조는 JSON 구문 검증 대상이며 runtime validator 구현 또는 Schema 승인 결과가 아니다. 중복 ID·참조·순환·업무 규칙은 §4의 별도 검증이 필요하다. 모든 필드를 required로 하고 nullable을 명시하는 엄격한 초안이며 API 매핑에서 같은 형태로 변환한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "HMDesign OrgChart draft",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["organizations", "employees", "affiliations", "relations", "presentation", "metadata"],
|
||||
"properties": {
|
||||
"organizations": {"type": "array", "items": {"$ref": "#/$defs/Organization"}},
|
||||
"employees": {"type": "array", "items": {"$ref": "#/$defs/Employee"}},
|
||||
"affiliations": {"type": "array", "items": {"$ref": "#/$defs/Affiliation"}},
|
||||
"relations": {"type": "array", "items": {"$ref": "#/$defs/OrganizationRelation"}},
|
||||
"presentation": {"$ref": "#/$defs/PresentationConfig"},
|
||||
"metadata": {"$ref": "#/$defs/Metadata"}
|
||||
},
|
||||
"$defs": {
|
||||
"ID": {"type": "string", "minLength": 1},
|
||||
"Text": {"type": "string", "minLength": 1},
|
||||
"NullableText": {"anyOf": [{"$ref": "#/$defs/Text"}, {"type": "null"}]},
|
||||
"NullableID": {"anyOf": [{"$ref": "#/$defs/ID"}, {"type": "null"}]},
|
||||
"Organization": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["id", "parentId", "name", "type", "leaderId", "displayOrder"],
|
||||
"properties": {
|
||||
"id": {"$ref": "#/$defs/ID"},
|
||||
"parentId": {"$ref": "#/$defs/NullableID"},
|
||||
"name": {"$ref": "#/$defs/Text"},
|
||||
"type": {"enum": ["root", "division", "office", "div", "cell", "team"]},
|
||||
"leaderId": {"$ref": "#/$defs/NullableID"},
|
||||
"displayOrder": {"type": "integer", "minimum": 0}
|
||||
}
|
||||
},
|
||||
"Employee": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["id", "organizationId", "affiliationId", "name", "position", "jobTitle", "profileImage", "workStatus"],
|
||||
"properties": {
|
||||
"id": {"$ref": "#/$defs/ID"},
|
||||
"organizationId": {"$ref": "#/$defs/ID"},
|
||||
"affiliationId": {"$ref": "#/$defs/ID"},
|
||||
"name": {"$ref": "#/$defs/Text"},
|
||||
"position": {"$ref": "#/$defs/Text"},
|
||||
"jobTitle": {"$ref": "#/$defs/NullableText"},
|
||||
"profileImage": {"anyOf": [{"type": "string", "format": "uri-reference", "minLength": 1}, {"type": "null"}]},
|
||||
"workStatus": {"enum": ["active", "dispatched", "onLeave"]}
|
||||
}
|
||||
},
|
||||
"Affiliation": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["id", "name"],
|
||||
"properties": {"id": {"$ref": "#/$defs/ID"}, "name": {"$ref": "#/$defs/Text"}}
|
||||
},
|
||||
"OrganizationRelation": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["id", "sourceOrganizationId", "targetOrganizationId", "type"],
|
||||
"properties": {
|
||||
"id": {"$ref": "#/$defs/ID"},
|
||||
"sourceOrganizationId": {"$ref": "#/$defs/ID"},
|
||||
"targetOrganizationId": {"$ref": "#/$defs/ID"},
|
||||
"type": {"enum": ["cooperation", "support"]}
|
||||
}
|
||||
},
|
||||
"PresentationConfig": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["title", "description", "hubText", "groups", "organizationOrder"],
|
||||
"properties": {
|
||||
"title": {"$ref": "#/$defs/Text"},
|
||||
"description": {"$ref": "#/$defs/NullableText"},
|
||||
"hubText": {"$ref": "#/$defs/NullableText"},
|
||||
"organizationOrder": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/ID"}},
|
||||
"groups": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["showEmployees", "showSummary", "showDispatched", "showOnLeave", "showAffiliations"],
|
||||
"properties": {
|
||||
"showEmployees": {"type": "boolean"}, "showSummary": {"type": "boolean"},
|
||||
"showDispatched": {"type": "boolean"}, "showOnLeave": {"type": "boolean"},
|
||||
"showAffiliations": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["schemaVersion", "asOfDate", "dataVersion", "updatedAt", "updateNote"],
|
||||
"properties": {
|
||||
"schemaVersion": {"const": "0.1-draft"},
|
||||
"asOfDate": {"type": "string", "format": "date"},
|
||||
"dataVersion": {"$ref": "#/$defs/Text"},
|
||||
"updatedAt": {"anyOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]},
|
||||
"updateNote": {"$ref": "#/$defs/NullableText"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
빈 문자열·공백만 있는 필수 값은 runtime Validation에서 거부하도록 제안한다. `minLength`만으로 공백 검사가 끝나지 않는다. 입력 길이·배열 크기·깊이 상한은 실제 데이터 규모 검토 후 TBD로 확정한다.
|
||||
|
||||
## 3. 가상 JSON 스냅샷 예시
|
||||
|
||||
실제 인사 정보와 사진이 없는 예시다. Schema 값은 초안이며 아래 데이터가 운영 계약으로 승인된 것은 아니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"organizations": [
|
||||
{"id": "org-root", "parentId": null, "name": "가상 사업본부", "type": "root", "leaderId": "emp-a", "displayOrder": 0},
|
||||
{"id": "team-a", "parentId": "org-root", "name": "가상 기획팀", "type": "team", "leaderId": "emp-b", "displayOrder": 1},
|
||||
{"id": "team-b", "parentId": "org-root", "name": "가상 운영팀", "type": "team", "leaderId": null, "displayOrder": 2}
|
||||
],
|
||||
"employees": [
|
||||
{"id": "emp-a", "organizationId": "org-root", "affiliationId": "aff-a", "name": "가상직원 가", "position": "대표", "jobTitle": "본부장", "profileImage": null, "workStatus": "active"},
|
||||
{"id": "emp-b", "organizationId": "team-a", "affiliationId": "aff-a", "name": "가상직원 나", "position": "책임", "jobTitle": "팀장", "profileImage": null, "workStatus": "active"},
|
||||
{"id": "emp-c", "organizationId": "team-a", "affiliationId": "aff-b", "name": "가상직원 다", "position": "선임", "jobTitle": null, "profileImage": null, "workStatus": "dispatched"},
|
||||
{"id": "emp-d", "organizationId": "team-b", "affiliationId": "aff-b", "name": "가상직원 라", "position": "선임", "jobTitle": null, "profileImage": null, "workStatus": "onLeave"}
|
||||
],
|
||||
"affiliations": [{"id": "aff-a", "name": "가상 관계사 A"}, {"id": "aff-b", "name": "가상 관계사 B"}],
|
||||
"relations": [{"id": "rel-a", "sourceOrganizationId": "team-a", "targetOrganizationId": "team-b", "type": "cooperation"}],
|
||||
"presentation": {
|
||||
"title": "가상 조직도", "description": "설계 검토용 가상 데이터", "hubText": "협업 중심",
|
||||
"groups": {"showEmployees": true, "showSummary": true, "showDispatched": true, "showOnLeave": true, "showAffiliations": true},
|
||||
"organizationOrder": ["org-root", "team-a", "team-b"]
|
||||
},
|
||||
"metadata": {"schemaVersion": "0.1-draft", "asOfDate": "2026-10-12", "dataVersion": "mock-1", "updatedAt": null, "updateNote": "초안 예시"}
|
||||
}
|
||||
```
|
||||
|
||||
직원 고유 ID 수 4, 상태별 raw 수 2/1/1은 fixture 산술 기준이다. 업무상 재직/전체 인원과 파견·휴직 포함 여부는 §5의 정책 승인이 필요하다. 조직장 카드와 상태 그룹에 같은 직원이 보이더라도 raw ID 수가 늘어나지 않아야 한다.
|
||||
|
||||
## 4. Validation과 오류 상태
|
||||
|
||||
제안 순서: 구조·버전 → 각 collection ID index → parent/직원/관계사/leader 참조 → cycle/root → 업무 규칙·입력 안전성. invalid 데이터를 engine로 전달하지 않는다.
|
||||
|
||||
- `SCHEMA_VERSION`: 승인된 버전과 불일치. 자동 추정·silent migration 금지.
|
||||
- `DUPLICATE_ID`: 같은 collection 내 ID 중복. 임의 last-write-wins 금지.
|
||||
- `REQUIRED_VALUE`: 누락/null 위반/공백뿐인 필수 값/잘못된 타입.
|
||||
- `ORGANIZATION_REFERENCE`: 직원 소속이 존재하지 않음. 삭제 시 parent/직원 이동을 일관된 스냅샷으로 제공해야 한다.
|
||||
- `PARENT_REFERENCE` / `HIERARCHY_CYCLE`: 없는 parent, 자기 parent, 다단계 순환. 정렬로 cycle을 은폐하지 않는다.
|
||||
- `LEADER_REFERENCE`: leader 직원이 없음. leader가 동일 조직/하위 조직/타 조직 소속이어도 되는지는 TBD이며 정책 없는 불일치는 승인 요구 issue로 보고한다.
|
||||
- `AFFILIATION_REFERENCE`: 직원 관계사 없음. 미지정 소속 허용 정책은 TBD.
|
||||
- `INVALID_STATUS` / `INVALID_TYPE`: 허용 vocabulary 외 값. 제안 enum이 실제 승인 목록을 대신하지 않는다.
|
||||
- `RELATION_REFERENCE` / `INVALID_RELATION`: 없는 양 끝 조직, 자기 관계, 중복 양 끝+유형, 허용되지 않은 유형을 검사한다. 관계 방향·대칭·역방향 중복 허용은 TBD. 일반 관계의 cycle은 조직 parent cycle과 구별한다.
|
||||
- `PRESENTATION_REFERENCE`: organizationOrder의 없는 ID·중복·정렬 정책 위반.
|
||||
- `UNSAFE_INPUT`: 실행 가능한 HTML/스크립트 payload·위험한 이미지 URL·제어문자/식별 교란 입력을 검사한다. 표시 문자열은 plain text, external `className/style/nodeTemplate`는 받지 않는다. image scheme/origin·상대 경로 정책·길이 상한은 승인 후 확정한다. JSON Schema `uri-reference`만으로 URL 안전성을 보장하지 않는다.
|
||||
|
||||
입력 검사와 출력 안전성은 함께 적용한다. 정상 문자열을 무조건 삭제하지 않고 거부/경고 기준을 문서화한다. DOM 텍스트는 `textContent`로 출력하고 engine 템플릿에는 외부 문자열을 직접 삽입하지 않는다. engine ID는 내부 안전 ID로 변환한다. HTML escaping만으로 이미지 URL 검사를 대체하지 않는다.
|
||||
|
||||
구조·참조·cycle·보안 실패는 Error 상태로 전환하고 engine mount/update를 중단한다. 사용자 메시지는 해당 데이터가 표시 불가능함을 설명하되 원본 개인정보를 노출하지 않는다. partial 데이터 자동 제거는 별도 승인 전 하지 않는다. 직원 0명과 조직 0개의 Empty 정의, 갱신 실패 시 직전 유효 화면 유지 여부는 TBD다. 미확정 집계 정책은 차트 유효성과 분리하여 집계를 `unavailable`로 표시하는 방식을 제안한다.
|
||||
|
||||
fatal Validation issue는 차트 Error로 처리한다. 집계 정책 미확정은 fatal data issue가 아니며 유효한 차트의 ready와 별도 AggregationResult `unavailable`로 공존하는 안을 제안한다. Source의 HTTP/JSON/매핑 실패는 dataSource 위치의 별도 code로 구별한다. warning·업무 승인 필요 issue의 치명도와 공개 반환 형식은 승인 전 TBD다. 어떤 경로도 원본 서버 오류·개인정보를 callback에 그대로 전달하지 않는다.
|
||||
|
||||
Validation·집계·render는 같은 불변 스냅샷을 사용하며 engine는 별도 복사본만 받는다. Source가 여러 API/페이지를 조합할 때의 시간 일관성은 ID 참조 검사와 별도로 보장해야 한다. Source 캡처·revision 반영 계약은 Architecture/Framework Integration을 따른다.
|
||||
|
||||
## 5. 인원 집계와 미확정 정책
|
||||
|
||||
원본 `employees`에서 계산하며 DOM 카드·engine 노드 수를 세지 않는다. Validation 통과 직원 ID 집합을 사용하고 parent 트리의 하위 조직을 통해 집계한다. 추가 관계·허브·접힌/숨겨진 카드·표시 설정은 조직 소속을 변경하지 않는다.
|
||||
|
||||
- 조직 직접 인원: 해당 organizationId의 직원 집합.
|
||||
- 하위 포함 인원: 자기 조직 + 승인된 parent 트리 하위 조직의 직원 ID 합집합.
|
||||
- 직위별 인원: 정해진 대상 집합을 `position`으로 그룹화. 직책과 혼합하지 않는다.
|
||||
- 재직/파견/휴직: 승인된 상태 매핑과 포함 정책을 적용한다. raw 상태 카운트와 재직 정의는 구별한다.
|
||||
- 관계사별 인원: `affiliationId`로 그룹화하고 동일한 승인 정책을 적용한다.
|
||||
- 전체 인원: 승인된 전체 대상의 직원 ID 집합. 하위 포함 조직별 숫자를 더하면 중복되므로 그 합을 총원으로 쓰지 않는다.
|
||||
|
||||
집계 정책은 PresentationConfig와 별도로 제공하는 제안이다. 아래 type의 값이나 default는 아직 승인되지 않았다.
|
||||
|
||||
```ts
|
||||
interface ApprovedAggregationPolicy {
|
||||
policyVersion: string;
|
||||
includedStatuses: WorkStatus[];
|
||||
activeStatuses: WorkStatus[];
|
||||
includeLeaders: boolean;
|
||||
membershipMode: "primaryOnly" | "uniqueEmployee";
|
||||
}
|
||||
type AggregationResult =
|
||||
| { status: "unavailable"; reason: "policyRequired" }
|
||||
| { status: "ready"; policyVersion: string; total: number };
|
||||
```
|
||||
|
||||
TBD: 파견·휴직을 재직/전체/팀/관계사에 포함하는지, 조직장 포함·겸직 조직장 배분, 조직 간 중복 소속, 파견 원소속/파견지 기준, 퇴사·예정 상태 필요 여부, 관계사 합과 총원 관계. 초안의 단일 `organizationId`는 복수 소속을 표현하지 못한다. 복수 소속이 필요하면 별도 Membership 계약과 schema revision을 승인하고 집계 mode를 확정한다. `uniqueEmployee` 후보만으로 현재 복수 소속이 지원된다고 주장하지 않는다.
|
||||
|
||||
정책 없이 임의 총원을 표시하지 않는다. 향후 테스트에는 leader 중복 표시, 상태 변경, 조직 이동, 하위 포함 중복 제거, 관계사 이동, 표시 숨김, 동일 JSON의 Vanilla/React 집계 일치를 포함한다. 같은 조건에 같은 결과가 나와야 한다.
|
||||
|
||||
집계 정책 자체도 policyVersion·지원 membershipMode·허용 status·중복 status·업무상 상호 관계를 검증한다. `activeStatuses`와 `includedStatuses`의 관계, leader 제외가 어느 조직의 어떤 직원에 적용되는지는 승인 전 TBD다. 필드 값만 받아 임의 해석하지 않는다. 환경별 정렬 일치를 위해 ID 비교 기준·locale 의존 정렬 여부도 확정한다.
|
||||
@@ -0,0 +1,148 @@
|
||||
# HMDesign OrgChart P11.2 — 최종 디자인 승인 기록
|
||||
|
||||
**디자인 승인 상태: APPROVED.** 이 문서는 사용자가 승인한 HMDesign OrgChart 기본 디자인을 향후 개발과 AI 생성 작업의 기준으로 기록한다. 디자인 승인은 접근성 준수 판정, 접근성 위험 수용, 보안 위험 수용, main 병합 또는 운영 배포 승인이 아니다.
|
||||
|
||||
## 1. 승인 정보와 근거
|
||||
|
||||
| 항목 | 확정 내용 |
|
||||
|---|---|
|
||||
| 승인일 | 2026-10-13 |
|
||||
| 승인 주체 | 사용자 디자인 승인 |
|
||||
| 기준 Commit | `346c648c093ae3b0dedc1fc754d8ffa52f00ddd0` |
|
||||
| 적용 대상 | HMDesign OrgChart 기본 디자인 |
|
||||
| 승인 상태 | APPROVED |
|
||||
|
||||
실제 담당자의 법적 이름이나 직책은 확인되지 않았으므로 기록하지 않는다. 승인은 이번 사용자 결정과 기존 보고서의 검증 범위에 근거한다. [P10.10 브라우저 시각 검증](visual-review-p10-10.md), [P11 외부 CSS 테마 구현·검증](implementation-report-p11.md), [P11.1 최종 정합성 검증](consistency-review-p11-1.md)을 참조한다. 과거 보고서의 승인 대기·실패·미검증 기록은 당시 기록으로 보존한다.
|
||||
|
||||
| 승인 화면 | 디자인 승인 상태 |
|
||||
|---|---|
|
||||
| Overview | APPROVED |
|
||||
| Department Colors | APPROVED |
|
||||
| Full Composition | APPROVED |
|
||||
| Expanded OrgChart | APPROVED |
|
||||
| Chart Only — React / Vanilla (Auto/Expanded) | APPROVED |
|
||||
|
||||
Storybook 4개 화면과 Chart Only를 포함한다. 기존 NOT VERIFIED인 브라우저·접근성·성능 항목을 승인 완료나 제품 전체 회귀 PASS로 변경하지 않는다.
|
||||
|
||||
## 2. 화면별 승인 기준
|
||||
|
||||
### Overview
|
||||
|
||||
- 관계사 회사 로고 7개와 중복 제목 제거.
|
||||
- 조직·직위별 인원 현황 및 관계사별 인원 현황.
|
||||
- 기존 Storybook Container 스타일, 카드 배경·Border·Radius·간격.
|
||||
|
||||
기존 이미지 및 인원 데이터는 수정하지 않는다.
|
||||
|
||||
### Department Colors
|
||||
|
||||
- 대표자 카드, 조직별 Gradient 6종, 그룹 타이틀.
|
||||
- 그룹장·직원 카드, 파견자·휴직자 미리보기, 직원 이름 `홍 길 동`.
|
||||
- 기존 카드 규격·Typography·Border와 Storybook 미리보기 Container.
|
||||
|
||||
**6개 조직 Gradient는 Expanded의 7개 회사별 컬러와 별개다.** 기존 디자인값을 임의로 통합하거나 변경하지 않는다.
|
||||
|
||||
### Full Composition
|
||||
|
||||
- 대표자 카드와 대표자 아래 화살촉 1개, 협업 화살표 5개.
|
||||
- 조직 카드·연결선과 중앙 조직도 배치.
|
||||
- 상단 제목·인원 현황, 파견·휴직자 위치와 간격, 하단 관계사별 인원 현황.
|
||||
- 세로로 긴 화면의 상단·중앙·하단 배치, 비례 축소 및 반응형.
|
||||
- Core 직원 정보 팝업의 X 닫기 버튼.
|
||||
|
||||
[P10.10](visual-review-p10-10.md)에서 Full Composition 390px 내부 스크롤·잘림 오류는 재현되지 않았다. 과거 실패는 현재 비례 축소 구성과 맞지 않는 구형 내부 스크롤 기대값과 구분하며, 원본 실패 기록을 보존한다. 이 결과를 새로운 전체 디바이스 검증 PASS로 확대하지 않는다. 기존 Fixed Full Composition 디자인 승인은 유지한다.
|
||||
|
||||
### Expanded OrgChart
|
||||
|
||||
- 회사별 20 / 100 / 300명, 전체·개별·복수 회사 선택.
|
||||
- TB / LR / Hybrid, 회사별 그룹 타이틀 컬러, 직원 카드·연결선.
|
||||
- Drag & Drop ON/OFF와 기본 OFF, Zoom/Pan/Fit.
|
||||
- 그룹 접기·펼치기, 직원 정보 팝업, 기존 HMDesign Dialog 연결.
|
||||
|
||||
기존 데이터 Contract와 회사별 컬러값을 유지한다. 승인된 기능 구성의 기록을 모든 데이터·동작 조합의 신규 실행 검증으로 해석하지 않는다.
|
||||
|
||||
**2026-10-14 P11.5 사용자 확정 변경:** Expanded 조직도 내부 viewport / Pan·Zoom 캔버스의 기본 배경은 순수 흰색 `#FFFFFF`다. Fixed 기존 BG·디자인, Storybook 페이지·외부 프레임, 카드·회사색·그룹 제목·연결선·레이아웃 승인은 유지한다. 캔버스 전용 외부 CSS 입력과 기존 테마 fallback은 [사용 가이드의 P11.5 절](org-chart-usage-guide.md#p115-fixedexpanded-캔버스-배경-독립-설정-2026-10-14)을 따른다. 이 한정된 사용자 변경 요청으로 다른 승인 상태·접근성 대비 미달 2건·Guard Runtime NOT VERIFIED를 변경하지 않는다.
|
||||
|
||||
### Chart Only — React / Vanilla
|
||||
|
||||
Auto/Expanded Chart Only의 승인 기준은 다음과 같다.
|
||||
|
||||
- 흰색 캔버스, 외부 제목·설명·요약·툴바 없음.
|
||||
- 최초 Fit to View, 전체 조직도 가로·세로 중앙 정렬.
|
||||
- Ctrl/⌘ 없는 마우스 휠 Zoom과 커서 중심 확대·축소.
|
||||
- 빈 공간 Pan, 직원 팝업, 그룹 접기·펼치기.
|
||||
- React·Vanilla 공통 동작.
|
||||
|
||||
**Fixed Chart Only는 현재 미지원이다.** 이를 승인된 기능으로 기록하지 않는다. 기존 Fixed Full Composition 승인과 구분한다. 공개 옵션과 생성 방식 선택은 [사용 가이드](org-chart-usage-guide.md) 및 [AI 조직도 자동 생성 정책](ai-page-generation-rules.md)을 따른다.
|
||||
|
||||
## 3. 기존 7개 회사 기본 컬러
|
||||
|
||||
| 회사 | 승인된 기본 메인 컬러 |
|
||||
|---|---|
|
||||
| 한맥기술 | `#CB2E28` |
|
||||
| 삼안 | `#F08300` |
|
||||
| 장헌산업 | `#4580A6` |
|
||||
| 장헌 | `#1D418D` |
|
||||
| 피티씨 | `#84338E` |
|
||||
| 한라산업개발 | `#0068B7` |
|
||||
| 바론컨설턴트 | `#0C367F` |
|
||||
|
||||
[회사 컬러 프리셋](../../packages/org-chart/src/company-color-presets.ts)의 기존 값과 일치하며 그룹 제목은 기존 정책대로 **흰색 텍스트**를 유지한다.
|
||||
|
||||
P11의 외부 CSS 테마 변경 기능은 다른 프로젝트가 커스텀 컬러를 의도적으로 지정하기 위한 것이다. 외부 테마로 색상을 변경할 수 있다는 사실은 HMDesign 기본 컬러의 변경을 의미하지 않는다. **외부 테마를 적용하지 않은 기본 조직도는 위 7개 회사 컬러를 유지한다.** Department Colors의 6개 조직 Gradient를 이 팔레트로 대체하지 않는다.
|
||||
|
||||
## 4. 접근성 미해결 사항
|
||||
|
||||
| 회사 | 배경색 | 흰색 제목 대비 | 일반 텍스트 WCAG AA | 현재 상태 |
|
||||
|---|---|---|---|---|
|
||||
| 삼안 | `#F08300` | 약 2.64:1 | 4.5:1 미달 | 미해결 |
|
||||
| 장헌산업 | `#4580A6` | 약 4.29:1 | 4.5:1 미달 | 미해결 |
|
||||
|
||||
비율은 기존 선언색 계산 기록이며 신규 브라우저 픽셀 실측이 아니다. 해당 회사색을 쓰는 Expanded/Auto Chart Only 그룹 제목에 적용된다. 회사 배경색과 흰색 그룹 제목은 유지하고 디자인은 승인하되, **접근성 미준수 사실은 유지한다. 디자인 승인을 WCAG AA 준수 승인 또는 접근성 위험 수용 승인으로 처리하지 않는다.**
|
||||
|
||||
| 미확정 항목 | 상태 |
|
||||
|---|---|
|
||||
| 조직 접근성 정책상 예외 수용 여부 | PENDING (정책 확인 NOT VERIFIED) |
|
||||
| 접근성 위험 승인 주체 | PENDING |
|
||||
| 향후 개선 시점 | PENDING |
|
||||
| WCAG 준수 최종 판정 | NOT VERIFIED |
|
||||
|
||||
기존 미검증 접근성 항목과 두 대비 미달을 숨기거나 PASS 처리하지 않는다.
|
||||
|
||||
## 5. Cell 디자인 처리
|
||||
|
||||
- Cell 전용 Border Only 스타일: **적용하지 않음**.
|
||||
- 연한 배경형 B안: **적용하지 않음**.
|
||||
- 기존 조직 그룹 Header 디자인, 직원 카드·연결선 유지.
|
||||
|
||||
Cell Border Only 디자인 실험은 사용자 결정으로 정식 적용에서 제외됐다. 향후 Cell 전용 디자인을 다시 적용하려면 별도 사용자 승인을 받아야 한다.
|
||||
|
||||
## 6. AI 개발·생성 작업 적용
|
||||
|
||||
[AGENTS.md](../../AGENTS.md)와 [CLAUDE.md](../../CLAUDE.md)에서 OrgChart UI 수정 또는 AI 생성 작업 전에 이 문서를 우선 확인한다. 생성 방식과 데이터 처리는 기존 [AI 생성 정책](ai-page-generation-rules.md)을 그대로 따른다.
|
||||
|
||||
1. 승인된 기존 디자인을 기본 기준으로 사용하고 카드·색상·Typography·간격을 임의 변경하지 않는다.
|
||||
2. 사용자 요청에 따른 조직 계층·직원 데이터 생성은 계속 허용하며 정확한 요청 구조·인원·원본 데이터를 보존한다.
|
||||
3. 외부 CSS 테마를 통한 의도적인 커스터마이징은 허용한다. 기본값 자체 변경과 구분한다.
|
||||
4. 승인된 기본값 자체를 변경할 때는 사전 사용자 확인을 받는다. 이미 해당 변경을 명시적으로 승인받은 요청은 같은 확인을 반복하지 않는다.
|
||||
5. 접근성 미해결 사항을 숨기거나 PASS로 변경하지 않는다.
|
||||
|
||||
이 지침은 변경 방지 기준을 제공하며 실제 코드 수정을 기술적으로 차단하는 보안 장치가 아니다. 생성 방식 사전 선택·Navigation·공개 API·Theme·접근성 계약을 대체하지 않는다.
|
||||
|
||||
## 7. 디자인 승인과 별도로 유지할 상태
|
||||
|
||||
| 항목 | 현재 기록 |
|
||||
|---|---|
|
||||
| 실제 Guard 정적 결과 | P11.1 `status PASS`, `staticStatus PASS`, issues 0 / FAIL 0 |
|
||||
| Guard Runtime | `runtimeStatus NOT VERIFIED` |
|
||||
| Guard CSS phases | STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED |
|
||||
| 결합 styleVerification | 미생성 유지 |
|
||||
| 접근성 위험 수용 | PENDING |
|
||||
| npm 개발 의존성 High Advisory 위험 수용 | PENDING |
|
||||
| 신규 npm audit·현재 전체 의존 그래프 평가 | NOT VERIFIED |
|
||||
| 운영 배포 승인 | PENDING |
|
||||
| main 병합 승인 | PENDING |
|
||||
|
||||
Guard는 [P11.1 기록](consistency-review-p11-1.md)과 기존 로컬 `test-results/org-chart-p11-1/guard-actual.json`을 확인한 결과이며 이번 문서 작업에서 재실행하지 않는다. P10 당시 실제 FAIL 5건 등 과거 원본은 보존한다. 정적 PASS·디자인 승인·별도 브라우저 증거로 원본 Runtime NOT VERIFIED를 덮어쓰지 않는다.
|
||||
|
||||
남은 승인과 운영 확인은 [정식 통합 승인 준비 문서](release-readiness-p10-7.md)를 따른다. 이번 P11.2는 문서화 단계이며 제품 코드·디자인 변경, 제품 전체 회귀, Commit·Push·main Merge·배포 실행을 포함하지 않는다.
|
||||
@@ -0,0 +1,302 @@
|
||||
# Expanded OrgChart 사용 및 스타일 — P7
|
||||
|
||||
2026-10-12 사용자 변경 요구사항으로 **현재 확장형 카드 외형과 크기를 유지**한다.
|
||||
`Group 3391.svg`, `Group 1171288761.svg` 검색·재현·용도 배정은 이번 완료 조건에서 제외한다.
|
||||
이 결정은 이전 P5-1B/P5-2/P5-3 검증 보고의 당시 미검증 기록을 변경하지 않는다.
|
||||
Fixed의 승인 SVG·1920×914.5 구성·컬러·팝업은 기존 정책을 유지한다.
|
||||
|
||||
## Vanilla JS와 JSON Source
|
||||
|
||||
아래는 Vite를 사용하는 호스트의 공개 API 예시다. 순수 HTML 배포에서는 공개 CSS·ESM
|
||||
산출물을 호스트가 제공하고 URL을 지정한다. 실행 가능한 HTML은
|
||||
`examples/org-chart/expanded.html`과 `expanded-app.js`를 참고한다.
|
||||
|
||||
```js
|
||||
import { createOrgChart, JsonOrgChartDataSource, getCompanyColorPreset } from '@hm-design/org-chart';
|
||||
import engineCssUrl from '@hm-design/org-chart/engine.css?url';
|
||||
import coreCssUrl from '@hm-design/org-chart/style.css?url';
|
||||
import dataset from './data-expanded-small.json'; // 기존 20명 가상 JSON을 호스트에 복사
|
||||
|
||||
const chart = createOrgChart({
|
||||
container: document.getElementById('chart'),
|
||||
dataSource: new JsonOrgChartDataSource(dataset),
|
||||
stylesheetUrls: { engine: engineCssUrl, core: coreCssUrl },
|
||||
layoutMode: 'auto', // 'fixed' 또는 생략은 기존 Workdesk
|
||||
displayMode: 'readOnly',
|
||||
selectedCompanyIds: ['hanmac'], // 단일 회사
|
||||
companyTopNodes: { hanmac: { title: '회장', display: 'card' } },
|
||||
employeeConnections: { 'hanmac-office-0': 'left' },
|
||||
zoom: { min: 0.05, max: 3, step: 1.2 },
|
||||
});
|
||||
await chart.mount();
|
||||
await chart.setSelectedCompanies(['hanmac', 'saman']); // 복수 회사
|
||||
await chart.setSelectedCompanies(); // 전체
|
||||
await chart.setSelectedCompanies([]); // 명시적 빈 선택
|
||||
chart.zoomTo(1); // 배율만 변경; 100% 도구 버튼은 이동 위치도 복원
|
||||
chart.panBy(40, 0);
|
||||
chart.fitToView();
|
||||
console.log(chart.getViewportState(), getCompanyColorPreset('hanmac')?.primary);
|
||||
// 호스트가 더 이상 컴포넌트를 사용하지 않을 때 chart.destroy()
|
||||
```
|
||||
|
||||
`JsonOrgChartDataSource`는 인라인 JSON 또는 호스트가 제공하는 HTTP(S)/상대 URL을 받는다.
|
||||
URL을 사용할 경우 `new JsonOrgChartDataSource('./data-expanded-large.json')`처럼 지정한다.
|
||||
file:// Fetch는 지원하지 않는다. 이번 작업에 외부 API·서버·DB 구현은 추가하지 않았다.
|
||||
기존 JSON 예시는 Small 20명/3회사, Medium 100명/5회사, Large 300명/7회사다.
|
||||
P7의 7회사 합산·회사별 선택은 Large를 기반으로 아래 공개 helper를 사용한다.
|
||||
|
||||
## React
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
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 dataset from './data-expanded-small.json';
|
||||
|
||||
const source = new JsonOrgChartDataSource(dataset);
|
||||
const stylesheets = { engine: engineUrl, core: coreUrl };
|
||||
const topNodes = { hanmac: { title: '회장', display: 'card' as const } };
|
||||
const connections = { 'hanmac-office-0': 'left' as const };
|
||||
|
||||
export function ExpandedExample() {
|
||||
const [selected, setSelected] = useState<string[] | undefined>(['hanmac', 'saman']);
|
||||
return <>
|
||||
<button type="button" onClick={() => setSelected(undefined)}>전체 회사</button>
|
||||
<OrgChart dataSource={source} stylesheetUrls={stylesheets} layoutMode="auto"
|
||||
displayMode="readOnly" selectedCompanyIds={selected}
|
||||
companyTopNodes={topNodes} employeeConnections={connections} />
|
||||
</>;
|
||||
}
|
||||
```
|
||||
|
||||
위 native 버튼은 React 계약만 설명하는 최소 예시다. HM 화면에서는 실제 기존
|
||||
`Button`, `Checkbox`, `Label`, `Select`를 조합한다. Expanded Story가 그 사용 예다.
|
||||
Source·옵션 객체를 안정적으로 유지한다. Core 생성 옵션(layoutMode·TopNode·연결 방향·Zoom 범위)을
|
||||
변경하면 React는 handle을 다시 생성한다. Vanilla는 새 handle을 생성한다.
|
||||
`autoLayout`·`interactions`·회사 선택·팝업 선택은 기존 인스턴스에서 갱신한다.
|
||||
회사 선택은 기존 setter/React prop으로 변경하며 원본 JSON과 parentId를 수정하지 않는다.
|
||||
React에는 별도 레이아웃 알고리즘이나 imperative ref API가 없다.
|
||||
|
||||
## 테마·최상위 노드·연결 방향
|
||||
|
||||
회사 Root의 `Organization.companyId`가 자손에게 상속된다. 회사별 독립 Root와 기존
|
||||
Employee 관계를 유지한다. `companyId` 생성 옵션은 기존 JSON의 회사 판별 fallback이며,
|
||||
다중 회사 전체를 한 색으로 바꾸는 설정이 아니다.
|
||||
|
||||
| ID | 회사 | 메인 컬러 |
|
||||
|---|---|---|
|
||||
| hanmac | 한맥기술 | #CB2E28 |
|
||||
| saman | 삼안 | #F08300 |
|
||||
| jangheon-industry | 장헌산업 | #4580A6 |
|
||||
| jangheon | 장헌 | #1D418D |
|
||||
| ptc | 피티씨 | #84338E |
|
||||
| halla | 한라산업개발 | #0068B7 |
|
||||
| baron | 바론컨설턴트 | #0C367F |
|
||||
|
||||
표의 값은 기존 `getCompanyColorPreset(companyId)`에서 조회한다. 카드에 중복 하드코딩하지 않는다.
|
||||
타이틀은 조직 회사, 카드 강조색은 알려진 직원 affiliation(미등록 시 조직 회사)의 프리셋을 쓴다.
|
||||
현재 사용자 확정에 따라 모든 회사 타이틀은 normal/hover/focus/접힘/펼침에서 흰색을 유지한다.
|
||||
이는 P7 시작 전 기존 미커밋 변경을 보존한 결과다. 삼안·장헌산업 흰 글자의 대비 진단은
|
||||
접근성 승인으로 재분류하지 않으며, 이번 기능 확장으로 메인 컬러를 바꾸지 않는다.
|
||||
|
||||
`companyTopNodes`는 `employeeId`, `title`, `name`, `profileImage`, `organizationName`,
|
||||
`display: 'card' | 'titleOnly' | 'hidden'`을 지원한다. 생략 시 Root의 leaderId를 사용하며,
|
||||
employeeId는 그 Root에 소속된 기존 직원이어야 한다. null은 최상위 카드 배정을 제거한다.
|
||||
표시 override는 원본 identity·팝업 데이터·callback·관계를 바꾸지 않는다.
|
||||
|
||||
`employeeConnections`의 지원 값은 `top | left | right`다. Depth를 기준으로 정하지 않는다.
|
||||
직원 연결선의 실제 도착점과 같은 면에만 회사 Border를 표시하며, 조직장은 타이틀 아래
|
||||
TB/Hybrid에서는 상단 연결을 사용한다. LR은 조직장·일반 직원 모두 왼쪽에서 연결되며
|
||||
직원별 실제 도착 면과 왼쪽 Border가 일치한다. 다른 면은 중립, 강조는 inset이라 외형 크기가 변하지 않는다.
|
||||
연결 없는 카드에 임의로 accent를 붙이는 별도 `none` API는 없다. 현재 Auto presenter의
|
||||
렌더링된 카드는 모두 타이틀 또는 이전 카드/분기 rail과 연결된다.
|
||||
|
||||
## 접기·펼치기와 직원 팝업
|
||||
|
||||
그룹 타이틀 전체 native button은 click·Enter·Space로 접기/펼치기하며 `aria-expanded`와
|
||||
표시 기호를 갱신한다. 조직장은 유지하고 일반 직원·자손을 숨기며 형제 상태는 독립이다.
|
||||
부모 재확장 시 유효한 하위 상태를 유지한다. 하위 직원/조직 없는 타이틀은 disabled다.
|
||||
그룹 링크는 별도 아이콘으로 페이지 이동과 토글 동작을 분리한다.
|
||||
공개 프로그래밍 방식 collapse setter는 제공하지 않는다.
|
||||
|
||||
Vanilla 직원 카드는 click·Enter·Space로 공통 직원 정보 dialog를 연다. dialog는 변환 forest 밖의
|
||||
native top layer에 표시되므로 Zoom과 Stage overflow의 영향을 받지 않는다.
|
||||
ESC/닫기 버튼과 focus trap·카드 focus 복귀를 제공한다.
|
||||
`onEmployeeClick(employee, context)` 지정 시 기본 팝업 대신 callback을 호출하며
|
||||
호스트가 dialog 접근성·닫기·focus 복귀를 책임진다.
|
||||
|
||||
P5-4는 Tab으로 화면 밖 카드·타이틀에 도달하면 **배율을 유지하고 Pan으로 표시**한다.
|
||||
드래그 직후 포인터 click만 억제하고 이후 Enter/Space 활성화는 허용한다.
|
||||
실제 스크린리더 음성·실제 모바일 터치는 별도 검증 대상이다.
|
||||
|
||||
## 카드 CSS 관리
|
||||
|
||||
현재 기본 카드 크기는 일반 직원 width 114px/min-height 54px, 조직장·최상위
|
||||
width 154px/min-height 72px이며 사진은 36×36px
|
||||
비원형 슬롯이다. 직위/이름 10/13px, Noto Sans KR, padding 8px, 기본 border 1px,
|
||||
inset accent 3px를 유지한다. 3글자 한글 이름은 `홍 길 동`처럼 표시하며 직위 공백은
|
||||
표시 계층에서만 제거한다. 긴 가상 이름이 두 줄이면 일반 직원의 실측 높이는 67px로
|
||||
자연 확장된다(기존 동작). 실제 profileImage를 우선하고 없음/실패는 기존 공통 사람 SVG다.
|
||||
|
||||
Card geometry는 `packages/org-chart/src/styles/org-chart-expanded.css`에 Auto 전용으로 관리한다.
|
||||
원본 Fixed CSS·SVG·Theme는 수정하지 않는다. `layoutConfig`·카드 교체 API는 없다.
|
||||
Auto 부모·자식 관계선과 직원 연결선은 전용 `--hm-orgchart-expanded-line-color`
|
||||
변수(기본 `#738399`)를 공유한다. 카드의 중립 Border·회사별 Border 강조와 Fixed
|
||||
연결선은 이 변수를 사용하지 않는다. 배경 또는 이 색상을 재정의할 때는 실제
|
||||
합성 배경과 computed 표시색으로 비텍스트 대비 3:1 이상을 별도로 확인한다.
|
||||
Core는 Shadow DOM을 사용하므로 일반 호스트 CSS selector는 내부 카드에 닿지 않는다.
|
||||
|
||||
향후 별도 디자인 변경이 승인된다면 호스트가 공개 Core CSS 뒤에 다음 override를 결합한
|
||||
CSS 자산을 만들고 그 URL을 `stylesheetUrls.core`로 제공할 수 있다(이번에는 적용하지 않음).
|
||||
`--hm-orgchart-expanded-*` 중 내부 선언이 있는 변수는 호스트 div에 설정하면 덮어써질 수
|
||||
있으므로 **Shadow DOM에 로드되는 CSS의 `.hm-orgchart-expanded`에서 재정의**한다.
|
||||
|
||||
```css
|
||||
/* 공개 Core CSS 다음에 결합하는 Auto 전용 예시. 임의 신규 API가 아니다. */
|
||||
.hm-orgchart-expanded {
|
||||
--hm-orgchart-expanded-card-width: 114px;
|
||||
--hm-orgchart-expanded-card-height: 54px;
|
||||
--hm-orgchart-expanded-leader-width: 154px;
|
||||
--hm-orgchart-expanded-leader-height: 72px;
|
||||
--hm-orgchart-expanded-card-accent-width: 3px;
|
||||
--hm-orgchart-expanded-photo-size: 36px;
|
||||
}
|
||||
```
|
||||
|
||||
크기 변경 시 기존 ResizeObserver가 article의 실측 높이와 실제 endpoint를 재계산한다.
|
||||
향후 변경에는 Fit·형제/하위 트리 겹침·텍스트 overflow를 다시 확인해야 한다.
|
||||
P7도 기본 카드 geometry를 바꾸지 않는다. LR/Hybrid 300명과 2,100명의 실측은
|
||||
과거 P5/P6 성능과 분리하여 P7 보고서에 기록한다.
|
||||
|
||||
과거 검증은 [P5-4 보고서](implementation-report-p5-4.md), P7 변경과 실측은
|
||||
[P7 보고서](implementation-report-p7.md)를 참고한다.
|
||||
|
||||
## P7 데이터·레이아웃·독립 옵션
|
||||
|
||||
```js
|
||||
import { createOrgChartExampleDataset } from '@hm-design/org-chart';
|
||||
import template from './data-expanded-large.json';
|
||||
|
||||
const dataset = createOrgChartExampleDataset(template, {
|
||||
scope: 'per-company', // 'total'이면 전체 7회사 합산 employeeCount
|
||||
employeeCount: 300, // 20 | 100 | 300
|
||||
companyIds: ['hanmac', 'saman'], // 생략하면 7회사, 이 예시는 600명
|
||||
});
|
||||
const chart = createOrgChart({
|
||||
...configuredBaseOptions,
|
||||
dataSource: new JsonOrgChartDataSource(dataset),
|
||||
layoutMode: 'auto',
|
||||
autoLayout: { type: 'lr', initialVisibleDepth: 'all' },
|
||||
interactions: { collapse: false, zoom: true, pan: false },
|
||||
});
|
||||
await chart.mount();
|
||||
await chart.setAutoLayout({ type: 'hybrid', hybridStartDepth: 3 });
|
||||
await chart.setInteractions({ collapse: true }); // 다른 설정은 그대로
|
||||
```
|
||||
|
||||
총량 모드는 회사별 가중 분배 후 선택한 회사만 필터링하며 재분배하지 않는다.
|
||||
회사별 모드는 선택 회사마다 정확히 N명, 전체 7회사 × 300명 = 2,100명이다.
|
||||
ID는 deterministic 가상 namespace로 생성하며 기존 직원 기록·조직 ID·부모 관계를 재사용한다.
|
||||
원본은 변경하지 않고 반환 snapshot은 deep freeze한다. helper의 빈 회사 선택은 유효한
|
||||
조직 데이터가 될 수 없어 거절하며, 화면의 빈 선택은 `setSelectedCompanies([])`로 표시한다.
|
||||
|
||||
| 옵션 | TB | LR | Hybrid | 제약 및 전환 |
|
||||
|---|---|---|---|---|
|
||||
| 레이아웃 `type` | 지원 | 지원 | 지원 | 다른 기능을 켤 필요 없음 |
|
||||
| `hybridStartDepth: 2/3/4` | 적용 안 함 | 적용 안 함 | 지원 | Root=1Depth, 기본 3 |
|
||||
| `hybridOrganizationIds` | 지정 그룹 자손 세로 배치 | 조합 거절 | 조합 거절 | LR/Hybrid 전환 전 목록 해제; Story는 자동 해제·disabled |
|
||||
| `initialVisibleDepth: 1/2/3/'all'` | 지원 | 지원 | 지원 | 기본 all; 값을 바꾸면 접힘 상태를 초기 정책으로 재설정 |
|
||||
| `interactions.collapse/zoom/pan` | 각각 지원 | 각각 지원 | 각각 지원 | 각각 기본 true; false도 다른 기능 사용을 막지 않음 |
|
||||
| 회사 선택·직원 팝업 | 지원 | 지원 | 지원 | 특정 레이아웃·Zoom·Pan·접기 사용을 요구하지 않음 |
|
||||
| Compact | 미지원 | 미지원 | 미지원 | 엔진 compact DOM/140×50 grid가 기존 HM 카드·접기 계약과 충돌 |
|
||||
|
||||
TB는 기존 경로를 유지한다. LR은 실제 dabeng `direction: 'l2r'`와 재귀 hierarchy를 사용한다.
|
||||
원본 `.title/.content` 전용 회전 CSS는 HM 복합 article에 맞지 않아 Auto 범위에서
|
||||
회전을 해제하고 부모 좌측·자식 우측·형제 세로 배치를 구성한다. Hybrid는 실제
|
||||
`verticalLevel`/그룹 `hybrid`를 사용한다. 측정한 부모·자식 좌표로 연결선을 갱신하며
|
||||
Fixed의 CSS·엔진 옵션·기존 native 팝업은 보존한다.
|
||||
|
||||
접기·레이아웃 전환은 유효한 조직 ID의 접힘 상태를 유지한다. 데이터·회사·레이아웃
|
||||
전환은 표시된 카드·타이틀·연결선 전체 경계를 다시 측정하여 가로 중앙을 갱신한다.
|
||||
자동 배치는 현재 배율을 유지하며 큰 조직도를 임의로 축소하지 않는다. 사용자가 Pan·Zoom·
|
||||
100% 복원 또는 화면 밖 focus 노출로 이동한 뒤에는 데이터·회사·레이아웃 전환과 Resize에서도
|
||||
배율·이동 위치를 보존한다. 명시적 Fit to View는 전체 표시 영역을 축소해 담고 가로·세로 중앙에
|
||||
배치하며 자동 중앙 배치를 다시 허용한다. 개별 interaction/popup 변경은 기존 viewport와
|
||||
접힘 상태를 보존한다. Pan을 꺼도 키보드로 화면 밖 카드에 도달하면 배율 유지·focus
|
||||
노출을 제공한다. Fit/100% 복원은 Zoom/Pan 사용 여부와 독립적으로 가능하다.
|
||||
2,100명 전체 Fit은 개요용으로 작게 축소되므로 카드 읽기에는 Zoom 또는 초기 Depth를 사용한다.
|
||||
|
||||
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` 연결이 필요하다.
|
||||
기본 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';
|
||||
|
||||
<OrgChart dataSource={source} stylesheetUrls={stylesheets} layoutMode="auto"
|
||||
autoLayout={{ type: 'lr', initialVisibleDepth: 2 }}
|
||||
interactions={{ collapse: false, zoom: true, pan: false }}
|
||||
employeePopup="hm-dialog" selectedCompanyIds={['hanmac', 'saman']} />
|
||||
```
|
||||
|
||||
## P9.3 브라우저 소속 변경
|
||||
|
||||
P9.4의 공통 Core/React 옵션 `dragDropEnabled?: boolean`은 기본 OFF다. 아래 소속 변경은
|
||||
ON에서 허용한다. React는 `dragDropEnabled={true}`, Vanilla는 생성 옵션 또는
|
||||
`handle.setDragDropEnabled(true)`로 활성화한다. `handle.getDragDropEnabled()`로
|
||||
현재 설정을 조회한다. OFF의 `handle.move()`는 `EDITING_DISABLED`를 반환하며 포인터와
|
||||
Alt+M도 시작하지 않는다. 전환 시 진행 중 이동·Pointer capture·Drop 강조를 취소하고
|
||||
이미 완료한 이동을 보존한다. 설정만 바꿀 때 데이터 재요청·재생성·레이아웃 갱신은 없다.
|
||||
직원 정보, 그룹 접기·펼치기, Zoom/Pan/Fit은 기존 독립 설정을 따른다. Fixed는 변경하지 않는다.
|
||||
|
||||
Expanded Story의 데이터 범위 UI는 제거했고 예시 직원 수20/100/300명은 회사별 기준만
|
||||
표시한다. 전체 합산 `createOrgChartExampleDataset` 공개 helper 계약은 유지한다.
|
||||
회사 선택은 즉시 표시를 갱신하고 숨김·복귀에도 완료한 이동을 유지한다. 직원 수는 기존
|
||||
**데이터 적용**으로 새 예시를 생성하며 임시 이동과 새 Fixture를 섞지 않는다. 초기 설정은
|
||||
7개 회사×20명=140명, 선택2개×100명=200명, 선택7개×300명=2,100명이다.
|
||||
|
||||
Expanded Story의 외곽 미리보기는 실제 Center Storybook 공통 카드 CSS의
|
||||
`--sb-center-card-background` / `--sb-center-card-border` / `--sb-center-card-radius`를
|
||||
재사용한다. 전용 `--hm-orgchart-expanded-preview-background`, `-border`, `-radius`
|
||||
입력은 `.hm-orgchart-expanded` 외곽에만 적용한다. 직원 카드·회사 타이틀·Viewport의
|
||||
색상 입력은 그대로이며, 입력을 생략한 Vanilla의 기존 기본 외곽 스타일은 유지한다.
|
||||
|
||||
Auto의 빈 공간 드래그는 기존 Pan이며 직원 카드·그룹 타이틀 드래그는 회사 내부 또는
|
||||
회사 간 소속 변경이다. 직원 이동은 `organizationId`, 조직 그룹 이동은 `parentId`를
|
||||
갱신한다. 같은 회사 내부에서는 파견 직원을 포함한 기존 `affiliationId`를 보존한다.
|
||||
회사 간 직원 이동은 `affiliationId`도 대상 회사로 변경하고, 회사 간 그룹 이동은 이동
|
||||
그룹과 하위 조직 전체의 `companyId`, 소속 직원 전체의 `affiliationId`도 대상 회사로
|
||||
변경한 새 snapshot을 현재 Core 인스턴스에 적용한다. ID·하위 계층·
|
||||
이름·직위·사진·조직장·정렬은 보존하며 회사 색상과 인원 집계는 대상 회사에 맞춰 갱신한다.
|
||||
원본 JSON·회사 프리셋·서버 데이터는 변경하지 않는다.
|
||||
그룹 클릭의 접기·펼치기와 직원 클릭의 정보 팝업은 유지하며 실제 드래그 뒤의 포인터
|
||||
click은 억제한다. 자기 자신·자손을 부모로 지정하는 이동, 회사 Root 이동,
|
||||
조직장 참조를 손상시키는 직원 이동은 거절하며 이유를 안내한다.
|
||||
|
||||
직원 카드 또는 그룹 타이틀에 초점을 둔 뒤 `Alt+M`으로 이동을 시작할 수 있다.
|
||||
방향키로 대상을 선택하고 `Enter`로 확인하거나 `ESC`로 취소한다. 팝업을 꺼도 직원
|
||||
카드는 이동 조작을 위한 초점을 제공한다. Fixed에는 소속 변경을 추가하지 않는다.
|
||||
|
||||
Core와 React의 `onMove(result: OrgChartMoveResult)`는 이동 성공·실패를 알린다.
|
||||
`result.moved`로 성공을 구분하며, React는 성공한 이동 후 열린 HM 직원 Dialog를 닫는다.
|
||||
React callback 변경은 최신 committed callback만 갱신하고 Core를 재생성하거나 reload하지 않는다.
|
||||
|
||||
Expanded Story는 모든 회사의 원본 예시를 하나의 세션 source로 유지하고 회사 선택은
|
||||
`selectedCompanyIds`로 처리한다. 회사 숨김·다시 표시·빈 선택은 성공한 소속 변경을
|
||||
보존한다. **데이터 적용**은 현재 draft 조건으로 새 예시 source를 만들고 이동 내용을
|
||||
초기화한다. **초기화**는 최초 설정과 최초 소속으로 돌아가며 설정값이 이미 최초값이어도
|
||||
이동 내용은 되돌린다. 파일을 다시 작성하거나 React key로 강제 remount하지 않는다.
|
||||
Story의 직원 합계와 그룹 선택은 성공한 `onMove`의 `result.data`를 별도 read model로
|
||||
읽으며, source identity는 유지한다. 표시 직원 수는 최초 예시 인원이 아닌 현재 조직의
|
||||
회사 소속을 기준으로 계산하므로 회사 간 이동 직후와 회사 숨김·다시 표시에도 일치한다.
|
||||
미리보기에는 선택 회사별 현재 직원 수도 표시하며 데이터 설정의 회사당 인원은 예시 생성 기준이다.
|
||||
@@ -0,0 +1,60 @@
|
||||
# OrgChart 최종 Feature 통합 — 2026-10-13
|
||||
|
||||
통합 범위는 **Chart Only + 초기 Fit + 직접 Wheel Zoom**, **GuardFix 해결 5건**, **AI 생성 방식 사전 선택 정책**이다. Cell 디자인 A/B안은 제외했다. 범위 검증·테스트는 PASS이며 전체 Guard는 잔여 FAIL 5건을 유지한다. Feature Commit·Push 완료 SHA와 최종 Working Tree는 이 작업의 최종 응답에서 보고한다.
|
||||
|
||||
## 범위와 원본 보호
|
||||
|
||||
- 대상: `D:\디자인시스템\HMDesign-OrgChart`, `feature/orgchart-package`, 기준 HEAD `0a8daf7ee7ac026952a2e1d3d5f25a922dab07cf`.
|
||||
- Chart Only 원본 Worktree(24개 승인 파일)와 GuardFix 원본(2개 파일)은 같은 기준 HEAD다. 원본 파일을 해시 snapshot으로 복사했고, 통합 뒤 원본 SHA-256이 모두 같음을 확인했다. 원본 Worktree에 수정·Stage·Commit하지 않았다.
|
||||
- 기존 미커밋 `AGENTS.md`, `CLAUDE.md`, `ai-page-generation-rules.md`의 사전 선택 변경을 보존했다. 겹치는 정책 문서는 전체 교체하지 않고 Auto 공개 API·Fixed 제약·현재 통합 범위만 합쳤다.
|
||||
- Cell 실험은 제품 Core에 적용된 적이 없다. 별도 `examples/org-chart-cell-preview`만 경로 검증 후 ignored `test-results/org-chart-cell-withdrawn-2026-10-13/preview`로 옮겼다. Cell 전용 스타일·미리보기·이미지는 Commit 대상에 포함하지 않는다. 기존 그룹 타이틀·직원 카드·연결선 디자인을 유지한다.
|
||||
- 회사 프리셋, 직원 카드, Fixed 승인 아트워크, 기존 Storybook 메뉴/Story, Theme/Token, Guard 분석기, JSON Contract, lockfile을 수정하지 않았다. `git reset --hard`, `git clean`, Force Push는 사용하지 않았다.
|
||||
- main Merge·Docker·npm Registry Publish는 실행하지 않는다.
|
||||
|
||||
## 통합한 동작
|
||||
|
||||
- Auto Core/React 공통 `presentationMode: 'chart-only'`는 흰색 캔버스·조직 노드·연결선만 표시한다. 내장 제목·기준일·선택 회사/인원 요약·Toolbar를 생성하지 않고 기존 접근성 live feedback는 시각적으로 숨긴다. Shadow DOM 소비자 강제 조작은 없다.
|
||||
- Chart Only의 기본은 `initialView: 'fit'`, `wheelZoom: 'direct'`다. 실제 Bounds를 최초 Fit·가로/세로 중앙 배치하며, 직접 휠 Zoom은 캔버스 안에서 포인터를 기준으로 적용한다. 바깥 휠은 페이지 스크롤을 유지한다.
|
||||
- 옵션을 생략한 기존 Expanded는 제목·요약·Toolbar·Modifier 휠·기본 중앙/상단 배치를 유지한다. Fixed Chart Only는 승인 디자인 보호를 위해 TypeError로 거부하며 임의로 Auto로 바꾸지 않는다.
|
||||
- React의 같은 값 inline 옵션 리렌더링에서 불필요한 재마운트/재Fit을 피하고, 데이터/회사/layout 교체에서는 새 Bounds로 Fit한다.
|
||||
- 통합 검토에서 발견한 2개 뷰 보존 문제를 같은 범위에서 수정했다. controller 재생성의 `initialPlacement` default가 명시적 undefined를 center로 바꾸던 문제를 제거했고, 내부 `fitScale()` getter로 기존 Fit floor를 전달한다. 전체 데이터 교체 시 이전 floor를 버리고 새 Fit을 계산한다. 계획된 공개 Props 3개 외 공개 API 확장은 없다.
|
||||
- AI 요청의 생성 방식이 미지정이면 코드 작성 전 Chart Only / Full Page를 질문하고 답변 후 구현한다. 이미 선택된 방식은 재질문하지 않는다. Fixed/Expanded 배치 선택은 생성 방식 선택을 대신하지 않는다.
|
||||
|
||||
## GuardFix 5건과 남은 결과
|
||||
|
||||
`department-colors.ts`의 미선언 contrast 후보 참조 3곳을 원래 fallback과 같은 `#10243b`/`#ffffff`로 교체했다. `expanded-content.ts`의 미소비 `--hm-orgchart-expanded-title-ink` setter와 그 안의 두 토큰 참조를 제거했다. 회사 primary·기존 흰색 제목 텍스트를 바꾸지 않는다. GuardFix 원본의 변경 전후 computed/geometry·캡처 hash 동일 증거도 확인했다.
|
||||
|
||||
현재 동일 범위 Guard는 **F3–F7 해결, 기존 FAIL 10→5, 새 FAIL 0, NOT VERIFIED 9**다. 남은 F1/F2/F8/F9/F10은 다음 5개 TOKEN-001 위치다.
|
||||
|
||||
- `department-colors.ts:47`: current-gradient / current-card-background.
|
||||
- `style-presets.ts:9`: resolved-bg-surface.
|
||||
- `style-presets.ts:74–75`: current-border / current-gradient.
|
||||
|
||||
원본 `status/staticStatus = FAIL`, `runtimeStatus = NOT VERIFIED`를 보존한다. 원본 `styleEvidence`는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 이 값으로 전체 Guard를 PASS로 바꾸지 않는다. 결합 styleVerification은 생성하지 않았다. 기존 P10/P10.3의 FAIL 10건 기록은 수정하지 않는다. `org-chart-content.css`의 추가 미선언 후보 참조까지 해결한 전체 패키지 검사는 이번 GuardFix 5건 범위가 아니다.
|
||||
|
||||
실행: `node scripts/ai-generation-guard.mjs --target . --scope PARTIAL --profile STORYBOOK --files packages/org-chart/src/core/department-colors.ts,packages/org-chart/src/core/expanded-content.ts,packages/org-chart/src/core/style-presets.ts --json`. 결과는 ignored `_local/final-integration/guard-final.json`에 보존한다.
|
||||
|
||||
## 최종 통합 검증
|
||||
|
||||
- **PASS:** 루트 `tsc -b`, Core/React/독립 소비자 TypeScript 검사, 패키지 빌드, 독립 Chart Only production build, Storybook production build, Theme defaults 92 colors/94 defaults, `git diff --check`.
|
||||
- **PASS:** Core 86/86, Guard 회귀 94/94. viewport 수정 후 Core/관련 타입·빌드만 재검사했고 변경하지 않은 Guard 회귀는 반복하지 않았다.
|
||||
- **PASS:** 실제 통합 production 빌드의 React/Vanilla Chart Only **51/51**, 콘솔 오류 0. 20/100/500명·다중 회사·TB/LR/Hybrid·390px/1440px, 초기 Fit/중앙, 직접 휠의 포인터 고정·범위, Pan, 캔버스 밖 페이지 스크롤, 데이터 교체, popup Core/HM Dialog, 접기·직원 보존, 모바일 CDP touch를 확인했다.
|
||||
- **PASS:** 추가 5개 회귀는 미조작 초기 Fit 직후 접기/펼치기, Fit보다 높은 `zoom.min=0.8`로 재생성 후 축소 floor, Vanilla `setInteractions` 재생성 후 뷰 보존을 검사한다. 별도 `verify-min` query는 예제 검증용이며 제품 API/기본값 변경이 아니다.
|
||||
- **PASS:** 새 Storybook의 공개 메뉴 4개·Fixed 구성·Expanded 내장 UI·Fit 버튼 4개 검사는 첫 실행에서 통과했다. Modifier 휠 항목은 페이지 smooth scroll의 좌표 이동 때문에 실패했으므로 actual wheel 이벤트/배율로 진단하고 instant scroll로 테스트 좌표를 고정해 해당 항목만 재검사했다(1/1 PASS, 일반 휠 scale=1, Ctrl 휠=1.2214). 최초 결과 4/5와 재검사 결과를 별도 보존했다. 제품 휠 코드는 이 진단으로 변경하지 않았다.
|
||||
- 원본 Chart Only 작업의 기존 baseline DOM/픽셀 비교 9/9는 원본 작업 당시 증거다. 현재 통합의 5개 Storybook 동작 검사와 구분하며, 통합 후 전체 Storybook 픽셀 baseline 비교 완료를 주장하지 않는다.
|
||||
|
||||
실제 증거: `test-results/org-chart-chart-only/results.json`, `storybook/results.json`, `storybook/results-wheel.json`, 캡처 PNG 및 `_local/final-integration/` 로그. 서버 네트워크 범위를 맞추기 전 connection timeout은 검사 실행 전의 도구 오류였으며, 실제 브라우저 실행 결과와 구분한다.
|
||||
|
||||
Build는 성공했지만 기존 `:host-context(.dark)` 처리·bundle size 경고와 Storybook 기존 경고를 보존한다. 경고 해결을 위해 범위를 넓히지 않았다.
|
||||
|
||||
**NOT VERIFIED / 기존 제약:** Fixed Chart Only, Safari/Firefox, 실제 모바일 기기·스크린리더, 2,100명 전체 회귀, 모든 회사·폭의 수동 시각 검토, 신규 npm tarball 외부 fresh install, 전체 baseline 픽셀 회귀. 기존 Fixed 390px 내부 스크롤 검사 실패는 원본 작업에서 기준 HEAD에도 재현된 기존 기록이며 이번 통합에서 해당 검사를 재실행하거나 수정하지 않았다.
|
||||
|
||||
## 미리보기와 기록
|
||||
|
||||
- React: `http://127.0.0.1:6045/react.html?data=100`
|
||||
- Vanilla: `http://127.0.0.1:6045/vanilla.html?data=100`
|
||||
- 통합 Storybook: `http://127.0.0.1:6046`
|
||||
|
||||
Feature Push 대상은 공식 `https://gitea.hmac.kr/b25027/HMDesign.git`의 `feature/orgchart-package`다. 시작 시 원격 Feature는 기준 HEAD와 같고, 원격 main은 `c45d63a03b84eaac43f01bd400a16488a80e4229`였다. Commit/Push 직전에 다시 확인하며 원격 변경 시 덮어쓰지 않는다.
|
||||
|
||||
작업 시작: 2026-10-13 18:18:29 KST. 최초 Estimated Duration: 20–30분. Commit·Push를 포함한 완료 시각/Actual Duration/Variance는 최종 응답에서 기록한다.
|
||||
@@ -0,0 +1,179 @@
|
||||
# OrgChart Framework Integration
|
||||
|
||||
상태: 설계 계약·사용 예시 제안. **현재 package/Wrapper/HTML bundle은 존재하지 않으며 예시 API·경로도 승인 전이다.** 이 문서는 코드를 생성하거나 지원 완료를 주장하지 않는다. [Architecture](architecture.md)의 facade와 [Data Contract](data-contract.md)를 공통 기준으로 한다.
|
||||
|
||||
## 1. 지원 범위와 책임
|
||||
|
||||
1차 공식 개발 대상은 **Vanilla HTML/JavaScript와 React만**이다. 현재 두 환경 모두 구현 미착수·실행 **NOT VERIFIED**다. Vue·Angular·기타 framework는 이번 개발 범위에서 제외하며 Adapter 구현·배포·지원 검증을 계획에 포함하지 않는다. 최초 추가 요청의 Vue 설계 항목은 최신 지원 범위 확정으로 대체했다. 향후 Core facade를 연결할 수 있도록 framework 독립성만 유지한다.
|
||||
|
||||
우선 검토 구성은 `@hm-design/org-chart`(Core), `@hm-design/org-chart-react`(React Wrapper)다. 실제 경로·두 패키지 분리는 workspace/build/dependency 검토 후 최종 승인한다. React Wrapper는 Engine Adapter와 다르다: Wrapper는 React와 Core 사이, Engine Adapter는 Core와 dabeng 사이에 있다. Data Source Adapter는 외부 형식과 normalized 계약 사이에 있다.
|
||||
|
||||
- Core: 표준 DOM, 공통 계약·Validation·ViewModel·집계·engine·노드 UI·Theme CSS·인터랙션·요청 취소·생명주기.
|
||||
- Vanilla 호스트: container 제공, Source·options 전달, Theme/CSS/script 로딩, 페이지 제거 시 Destroy.
|
||||
- React Wrapper: ref/effect로 Core mount, Props 변경을 update/reload 또는 설정 재생성에 연결, 언마운트 시 Destroy. 공통 업무 로직·렌더링 엔진·노드 디자인을 재구현하지 않는다.
|
||||
- 상위 앱: Layout·Navigation·Routing·API 인증·권한·Theme 공급. Core에 React context나 인증 hook을 요구하지 않는다.
|
||||
|
||||
두 환경은 동일 JSON 입력 → 동일 Validation → 동일 계층·집계 → 동일 Core 노드·Theme·인터랙션을 사용한다. React 외부 HM Component는 페이지 조합이며 Core 내부 노드의 다른 디자인을 만드는 근거가 아니다.
|
||||
|
||||
## 2. 생명주기 계약 초안
|
||||
|
||||
메서드 이름은 확정 생명주기 요구를 구체화한 API 제안이다. engine 자체의 destroy 지원과 혼동하지 않는다.
|
||||
|
||||
1. `createOrgChart(options)`: DOM 생성·요청 없이 Handle을 만든다. framework import와 전역 window 접근 부작용을 ESM import 시 피하는 안을 검토한다.
|
||||
2. `mount(container)`: 연결된 HTMLElement의 전용 자식 root를 소유하고 첫 Source를 읽는다. 호스트의 sibling/기존 콘텐츠를 지우지 않는다. 한 Handle에 한 container이며 중복 mount는 명시적 호출 오류로 거부한다.
|
||||
3. `update(input)`: mounted 상태에서 normalized 스냅샷 후보를 검증·집계·렌더한다. Source fetch 없이 스냅샷만 바꾼다. 현재 Source 객체는 유지하므로 이후 reload는 그 Source를 다시 읽는다.
|
||||
4. `reload(source?)`: mounted 상태에서 현재/교체 Source를 읽고 같은 반영 경로를 사용한다. JSON → Mock API/실제 API 전환의 공개 지점이다.
|
||||
5. `destroy()`: 동기·반복 안전한 최종 정리. 진행 요청 abort·revision 무효화·콜백 중단, Core/engine 이벤트·Observer·frame/timer·DOM·인스턴스 참조를 정리한다. destroyed Handle은 재사용하지 않고 새 Handle을 만든다.
|
||||
|
||||
mount/update/reload는 Promise<void> 제안이다. 데이터·Source 실패는 Error state로 알리고 Promise를 정상 완료하는 방식을 우선 검토한다. 잘못된 호출 순서·사용법은 명시적 오류로 reject한다. abort/새 revision에 의해 폐기된 작업은 unhandled rejection 없이 종료하고 DOM/상태를 바꾸지 않는다. mount 진행 중에도 이미 확보한 container의 update/reload는 이전 작업을 무효화한다. mount 전·destroy 후 update/reload는 오류다. Promise 완료는 해당 revision의 반영 또는 controlled error 전환까지이며 이미지/폰트의 모든 로드 완료를 뜻하지 않는다.
|
||||
|
||||
상태 전이는 loading → ready/empty/error다. update는 동기 validation 중에도 같은 state 의미를 유지한다. Empty 정의와 갱신 중/실패 시 직전 화면 유지 정책은 TBD다. 같은 Metadata 버전이라도 새 입력을 무시하지 않는다. reload/update 경쟁은 마지막 시작 revision이 우선하도록 제안한다.
|
||||
|
||||
엔진 정리 가능성은 최초 PoC에서 입증해야 한다. document 이벤트·관찰자를 해제할 경로를 찾지 못하면 Destroy 완료를 PASS로 표시하지 않는다. core가 소유하지 않은 engine 자원을 `innerHTML = ""`만으로 정리했다고 판단하지 않는다. 제한/통합 대안을 승인받기 전 기능 개발을 확대하지 않는다.
|
||||
|
||||
engine 재생성 전 이전 chart가 살아 있는 동안 기존 pan/zoom·minimap 등의 해제 가능성을 확인한다. update/reload 반복 후에도 이벤트/Observer가 증가하지 않는지 검사한다. Core와 데이터 계층의 DOM 없는 import는 upstream 직접 import와 구별하며 지연 engine 로딩/변환 entry를 검증한다.
|
||||
|
||||
## 3. 데이터 Source 사용 예시
|
||||
|
||||
아래 `createJSONSource`·`createExternalJSONSource`도 제안 API다. 모든 반환값은 같은 validator를 통과한다. 가상 데이터 `data`는 Data Contract §3 스냅샷을 뜻한다.
|
||||
|
||||
```ts
|
||||
// 제안 API; 아직 설치 가능한 공개 함수가 아니다.
|
||||
const jsonSource = createJSONSource(data);
|
||||
const externalSource = createExternalJSONSource({ url: configuredJsonUrl });
|
||||
// URL은 호스트 설정. Core에 회사 서버 주소를 하드코딩하지 않는다.
|
||||
```
|
||||
|
||||
JSON Mock은 객체 스냅샷을 공급하고 consumer 입력을 수정하지 않는다. External JSON은 AbortSignal·HTTP 오류·JSON 구문 오류를 처리한다. source가 JSON을 반환했다는 이유만으로 Validation을 생략하지 않는다.
|
||||
|
||||
JSON Source는 생성 시점 입력을 캡처하는 안, update는 호출 시점 입력을 확보하는 안을 제안한다. 같은 객체의 외부 mutation을 자동 변경 알림으로 취급하지 않는다. 검증·집계·DOM 반영은 Core 소유 불변 스냅샷에서 수행하고 최종 state/DOM commit 직전 revision·destroy 상태를 다시 확인한다.
|
||||
|
||||
API가 마련됐을 때의 공통 계약 예시:
|
||||
|
||||
```ts
|
||||
// OrgChartDataSource, OrgChartData는 문서의 초안 type이다.
|
||||
function createApiSource(
|
||||
request: (signal: AbortSignal) => Promise<unknown>,
|
||||
mapResponse: (raw: unknown) => OrgChartData
|
||||
): OrgChartDataSource {
|
||||
return {
|
||||
async load({ signal }) {
|
||||
return mapResponse(await request(signal));
|
||||
}
|
||||
};
|
||||
}
|
||||
const apiSource = createApiSource(hostApiRequest, mapApiResponse);
|
||||
await handle.reload(apiSource); // Core/노드 UI/집계/React Wrapper는 유지
|
||||
```
|
||||
|
||||
실제 request·mapping은 명세 확정 후 호스트에서 작성한다. mapResponse type 선언은 보안 경계가 아니며 Core가 결과를 재검증한다. Endpoint·인증·재시도·캐시를 임의로 예시에 넣지 않는다. Mock API는 같은 fixture를 Promise로 반환하고 지연/오류를 주입하여 실제 서버 없이 전환을 검증한다.
|
||||
|
||||
## 4. Vanilla HTML 사용 예시
|
||||
|
||||
아래 asset 이름·`window.HMOrgChart` namespace는 미래 browser bundle **제안**이며 파일을 제작한 것이 아니다. React 설치 없이 classic script + 별도 CSS + inline JSON으로 `file://` 직접 실행을 검증할 예시다. 포장된 Theme CSS·font·Core CSS·번들이 모두 상대 경로로 제공돼야 한다.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>가상 조직도 검증</title>
|
||||
<link rel="stylesheet" href="./assets/hm-theme.css">
|
||||
<link rel="stylesheet" href="./assets/orgchart.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="orgchart-host"></div>
|
||||
<script id="orgchart-data" type="application/json">
|
||||
{
|
||||
"organizations": [{"id":"org-a","parentId":null,"name":"가상 조직","type":"root","leaderId":"emp-a","displayOrder":0}],
|
||||
"employees": [{"id":"emp-a","organizationId":"org-a","affiliationId":"aff-a","name":"가상직원 가","position":"책임","jobTitle":"조직장","profileImage":null,"workStatus":"active"}],
|
||||
"affiliations": [{"id":"aff-a","name":"가상 관계사"}],
|
||||
"relations": [],
|
||||
"presentation": {"title":"가상 조직도","description":null,"hubText":null,"groups":{"showEmployees":true,"showSummary":false,"showDispatched":false,"showOnLeave":false,"showAffiliations":false},"organizationOrder":[]},
|
||||
"metadata": {"schemaVersion":"0.1-draft","asOfDate":"2026-10-12","dataVersion":"html-mock-1","updatedAt":null,"updateNote":null}
|
||||
}
|
||||
</script>
|
||||
<script src="./assets/orgchart.browser.js"></script>
|
||||
<script>
|
||||
const data = JSON.parse(document.getElementById("orgchart-data").textContent);
|
||||
const { createOrgChart, createJSONSource } = window.HMOrgChart;
|
||||
const handle = createOrgChart({ dataSource: createJSONSource(data) });
|
||||
handle.mount(document.getElementById("orgchart-host"))
|
||||
.catch(() => console.error("OrgChart lifecycle error"));
|
||||
window.addEventListener("pagehide", () => handle.destroy(), { once: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
위 예시는 실행 검증 전 설계용이다. inline JSON은 가상 데이터만 사용한다. pagehide 이후 bfcache 복귀가 필요한 앱은 새 instance의 재mount를 호스트에서 계획한다. SPA 콘텐츠 제거에서는 pagehide에만 의존하지 않고 해당 제거 시점에 Destroy한다.
|
||||
|
||||
ESM 비교 예시(HTTP(S) static server에서 우선 검증):
|
||||
|
||||
```js
|
||||
import { createOrgChart, createJSONSource } from "./assets/orgchart.esm.js";
|
||||
const handle = createOrgChart({ dataSource: createJSONSource(data) });
|
||||
await handle.mount(document.getElementById("orgchart-host"));
|
||||
```
|
||||
|
||||
bare npm specifier는 일반 HTML에서 자동 해석되지 않는다. file://의 module/fetch 제한과 서버 기반 ESM 실행을 구별한다. `file://`에서는 inline JSON + classic bundle 경로를 검증하고 외부 JSON fetch가 가능하다고 보장하지 않는다. External JSON/API는 HTTP(S)·CORS·호스트 권한 조건에서 별도로 검증한다.
|
||||
|
||||
## 5. React 사용 예시와 Wrapper 계약
|
||||
|
||||
아래 package names·Component/Prop은 우선 분리안의 예시이며 실제 Export가 아니다. 동일 Core CSS·Theme를 사용한다. React 호스트의 기존 CSS entry 순서를 지키고 HM 공개 Component를 사용하는 주변 영역은 해당 HM Tailwind source/Theme 규칙을 유지한다.
|
||||
|
||||
```tsx
|
||||
import { useMemo } from "react";
|
||||
import { createJSONSource } from "@hm-design/org-chart";
|
||||
import { OrgChart } from "@hm-design/org-chart-react";
|
||||
import "@hm-design/org-chart/style.css";
|
||||
// 기존 앱 Theme entry에서 @hm-design/ui/theme.css를 연결한다.
|
||||
|
||||
function OrgChartContent({ data }: { data: OrgChartData }) {
|
||||
const dataSource = useMemo(() => createJSONSource(data), [data]);
|
||||
return <OrgChart dataSource={dataSource} />;
|
||||
}
|
||||
```
|
||||
|
||||
Props 후보는 dataSource·aggregationPolicy·interaction·onStateChange다. title/description/group/order는 공통 데이터의 PresentationConfig를 사용한다. React 전용 표현·집계 옵션을 따로 만들지 않는다. 직접 스냅샷 Prop 추가는 public API 승인 후 검토한다.
|
||||
|
||||
Wrapper 동작: 최초 ref container 확보 → Core 생성·mount; dataSource 교체 → 기존 Handle.reload(newSource); 동일 Source의 내부 값 변경은 명시적 reload 또는 새 immutable Source로 전달한다. 새 객체를 매 렌더 생성해 불필요한 reload를 유발하지 않는다. options 중 집계·인터랙션 변경은 Core의 갱신 API 확장 또는 cleanup 후 instance 재생성 중 PoC로 결정한다. 미확정 옵션을 조용히 무시하지 않는다.
|
||||
|
||||
언마운트는 destroy; effect cleanup→setup이 반복되는 React StrictMode에서도 새 instance만 mount한다. promise 경쟁·늦은 callback·변경되지 않은 props에 의한 중복 engine 생성·callback identity로 인한 무한 reload를 검사한다. Core가 DOM을 소유하는 전용 container 자식에 React가 동시에 노드를 render하지 않는다. Framework wrapper는 engine/node/Validation/집계 CSS를 복제하지 않는다.
|
||||
|
||||
SSR/Server Components의 DOM 렌더링은 이번 지원 완료 범위가 아니다. import 시 브라우저 global 부작용·React client boundary·hydrate 안전성은 검토하고 미확인 기능은 NOT VERIFIED로 남긴다.
|
||||
|
||||
## 6. CSS·Theme·배포 방식
|
||||
|
||||
같은 Core CSS와 HM Variables mapping을 모든 배포가 사용한다. 호스트는 Theme를 로드하고 Core는 컨테이너 내부 전용 selector만 사용한다. Theme 정책과 실제 선언 근거는 [Integration §3](integration.md#3-hm-theme-연결)을 따른다.
|
||||
|
||||
- npm: Core ESM + type + CSS, Wrapper ESM + type 후보. exports/types/files/sideEffects를 설계하고 package tarball 안에서 실제 import·CSS·font/asset 존재를 검증한다. 실제 publish는 별도 승인이다.
|
||||
- ESM: npm bundler import와 브라우저 상대 URL import를 구별한다. Core에 React/React DOM 또는 unresolved bare dependency가 섞이지 않도록 검증한다.
|
||||
- browser bundle: IIFE/UMD 중 직접 script 사용 형식을 선택한다. `window.HMOrgChart` 후보, 엔진 포함 여부·외부 dependency·license·크기·CSP를 검토한다. inline page 예시는 CSP 승인 완료를 뜻하지 않는다.
|
||||
- CSS: Core 스타일을 별도 `style.css` export/상대 asset로 제공하고 Theme를 중복 주입하지 않는다. upstream CSS의 container 밖 누출도 포함해 scope를 검증한다.
|
||||
- TypeScript: Core 선언에는 React type이 없어야 하고 Wrapper만 React Props를 가진다. JS consumer에 TS transpilation을 요구하지 않는다.
|
||||
- Theme 미연결: 승인된 scope 기본 스타일 제공 vs Theme 필수 연결 중 **TBD**다. 어느 정책이든 HTML/React가 같아야 한다. approved 기본값 없이 fallback 팔레트를 새로 만들지 않는다.
|
||||
|
||||
Root의 기존 build script는 두 패키지만 처리하므로 새 Core·Wrapper build 연결 범위는 승인 후 확정한다. 엔진 CommonJS/UMD를 ESM·browser bundle로 변환할 수 있는지, export 기능 dependencies가 필수로 포함되는지, DOM-only engine와 adapter의 tree-shaking 가능성은 **NOT VERIFIED**다. 단순히 빌드 설정을 제안했다는 이유로 배포 가능 PASS를 내지 않는다.
|
||||
|
||||
Vanilla Theme를 위해 Core dependency/peer에 React peer를 가진 `@hm-design/ui` 전체 package를 필수로 추가하지 않는다. 승인된 호스트 asset/별도 Theme 산출 경로에서 React 미설치 consumer 동작을 확인한다. Core/engine CSS 누출, 호스트 Theme의 전역 영향, 전달된 font의 상대 URL은 서로 다른 검증 대상이다.
|
||||
|
||||
Theme owner는 호스트 하나이며 Core는 scope를 상속한다. 별도 Scoped Theme는 기존 `[data-hm-theme]` mapping 재계산·Scoped partial Default 의미를 따르고 Core가 자동 주입/reset하지 않는다. Global A/Scoped B의 두 instance가 서로 영향을 주지 않는지 확인한다.
|
||||
|
||||
## 7. 최소 통합 검증 계획
|
||||
|
||||
동일 가상 fixture·공통 설정·Theme로 두 환경을 비교한다. 테스트가 확인해야 할 차이를 명시하고 같은 검사 반복을 피한다.
|
||||
|
||||
1. React 설치 없는 HTML 단독 페이지에서 표시. `file://` classic bundle과 HTTP(S) ESM의 asset/console 결과를 각각 기록.
|
||||
2. React에서 같은 Core로 동일 계층·노드 UI·인터랙션 표시. 새 렌더링 엔진이 없는지 dependency/code 검토.
|
||||
3. 같은 JSON의 조직 ID·parent·순서·leader·직원 그룹과 공통 집계 결과 일치.
|
||||
4. 조직 추가/삭제/이동·직원 이동/상태/관계사/사진·title/기준일 변경 후 재검증과 갱신. consumer 원본 객체를 바꿔도 이미 캡처/반영한 스냅샷과 집계가 암묵적으로 바뀌지 않음.
|
||||
5. JSON → Mock API Source → JSON 전환, API 매핑 계약·loading/error·응답 경쟁·abort 확인.
|
||||
6. HM Theme 연결 및 승인된 Theme 미연결 정책. computed Variables·node CSS·font·scope가 두 환경에서 일치. Global A + Scoped B + 두 instance의 독립성·Theme owner와 cleanup도 확인.
|
||||
7. mount/update/reload/destroy·연속 remount·StrictMode·다중 instance에서 이벤트/Observer/timer/DOM 잔존과 늦은 callback 확인.
|
||||
8. CSS 전역 충돌과 390/768/1280/1440px Overflow·키보드·focus·읽기 가능성 검증.
|
||||
9. Core build·npm/ESM/browser bundle·CSS/type/license와 React dependency 미포함 검증.
|
||||
10. 공식 운영 main·기존 UI/Theme/Storybook과 무관 파일 보존 증거. 코드 중복·공통 Core 재사용 검토.
|
||||
|
||||
Vue/Angular 실행 검증은 여기 포함하지 않는다. 기술 제약·승인 게이트는 [Implementation Plan](implementation-plan.md)을 따른다.
|
||||
@@ -0,0 +1,106 @@
|
||||
# OrgChart P10.3 — Guard FAIL 10건 원인 분석 및 배포 차단 판정
|
||||
|
||||
RESULT: **PARTIAL** — FAIL 10건 모두 원인·영향·수정 방법 확인, 10/10 재현. 기술적 BLOCKER 0건. 단 Guard 원본 status는 FAIL 그대로이고, CI·조직 배포 정책에서 Guard가 필수인지는 근거가 없어 NOT VERIFIED다. 제품 코드·Guard 수정 없음.
|
||||
|
||||
## 1. 실행 기준
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Worktree / 브랜치 | `D:\디자인시스템\HMDesign-OrgChart-GuardReview` / `feature/orgchart-guard-review` (신규, 커밋 없음) |
|
||||
| 기준 HEAD | `5acaf8a29596317133e54e8ad437ee00beba1cf0` (P10.2 미커밋 변경 미포함) |
|
||||
| Guard | `scripts/ai-generation-guard.mjs` (schemaVersion 2) + `scripts/guard-theme-analysis.mjs`. P10.2 작업본에서도 수정되지 않은 파일 |
|
||||
| 원본 결과 | P10 작업본 `test-results/org-chart-p10/guard.json` (sha256 앞부분 `a83d058656480fbf`): `status FAIL / staticStatus FAIL / runtimeStatus NOT VERIFIED`, STORYBOOK / PARTIAL, FAIL 10 / NOT VERIFIED 19 |
|
||||
| 재현 | Node v22.23.2. `node scripts/ai-generation-guard.mjs --target <GuardReview> --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은 진행하지 않았다.
|
||||
@@ -0,0 +1,110 @@
|
||||
# OrgChart Implementation Plan
|
||||
|
||||
상태: 설계 검토 후 수행할 개발 계획. **이번 작업은 Markdown만 작성하며 구현을 시작하지 않는다.** 1차 공식 개발 대상은 Vanilla HTML/JavaScript와 React다. Vue·Angular·기타 framework는 제외한다. [README](README.md)의 요구사항 추적과 [Framework Integration](framework-integration.md)의 통합 검증을 따른다.
|
||||
|
||||
## 1. 단계별 계획과 완료 조건
|
||||
|
||||
### 1단계 — 설계 승인·핵심 기술 PoC
|
||||
|
||||
사용자가 이 문서를 검토한 뒤 후속 개발 권한을 확인한다. Core/React Wrapper 두 패키지 분리안과 실제 workspace/build 연결 범위를 결정한다. engine 배포 버전을 고정하고 native DOM·module import·custom node·완전 cleanup·CSS scope를 소규모 PoC로 확인한다. 캡처가 제공되면 콘텐츠 8영역·허브·곡선·카드 배치를 대조한다.
|
||||
|
||||
완료 조건: 정확한 engine version/tag/배포 artifact·license와 dependency 범위가 기록됨; React 없는 HTML 구동 가능성, Destroy·다중 instance·custom node 가능성 근거 확보; node Theme mapping·미연결 정책·모바일 선택의 승인 필요 범위가 구분됨. 완전 정리가 불가능하면 기능 구현을 확대하지 않고 차단 사항·대안을 보고한다. 이 기술 PoC는 뒤 단계의 전제라 필요한 최소 중간 검증에 해당한다.
|
||||
|
||||
### 2단계 — 공통 데이터 계약·Validation·집계
|
||||
|
||||
Schema 초안의 필드/enum/null/version/root/leader·복수 소속과 집계 정책을 승인하고 순수 JS/TS 데이터 계층을 구현한다. JSON Mock/외부 JSON Source와 공통 validator를 만든다. API 명세 없이 Mock API Source로 교체 계약을 확인한다.
|
||||
|
||||
완료 조건: 원본 ID 모델·동일 Source Interface; 중복 ID/참조/cycle/상태/관계/안전 입력 실패가 controlled issue로 변환됨; 승인된 집계 정책으로 단일 ID·하위 포함·상태/관계사 이동이 결정적 결과를 생성함. 미승인 정책은 code default로 숨기지 않음. UI·framework 의존 없이 사용할 수 있음.
|
||||
|
||||
### 3단계 — Vanilla Core와 콘텐츠
|
||||
|
||||
승인한 패키지/build 구조에서 engine adapter, DOM 노드 UI, 제목·허브·summary·상태/관계사 영역을 구현한다. mount/update/reload/destroy, Source 경쟁·취소·instance scope·Core CSS·Theme 연결을 구현한다. Overlay/Layout은 PoC와 승인한 디자인만 구현한다.
|
||||
|
||||
완료 조건: Core에 React import 없음; API endpoint/Navigation/로그인 의존 없음; 데이터만 변경해 조직·직원·relation·제목 갱신; 승인 정책의 집계; 요청된 인터랙션·사용자 상태·정리 보장; HTML 단독 배포 예시의 runtime 목표 명확. 임의 추가 검색/Export/메뉴/drag 편집은 없음.
|
||||
|
||||
### 4단계 — React Wrapper와 콘텐츠 Storybook
|
||||
|
||||
Wrapper는 Core 생성·ref·props 전달·Source 변경·언마운트 cleanup만 연결한다. 동일 Core UI와 CSS를 사용하고 Storybook은 콘텐츠 단독으로 구성한다. 기존 Component/Pattern/Theme/대표 Story는 보존한다. 현 [Storybook main](../../.storybook/main.ts)의 glob은 `src/**/*.stories.ts(x)`이며 패키지 내부 Story는 자동 포함되지 않는다. `src/org-chart/*.stories.tsx`는 제안 위치다.
|
||||
|
||||
완료 조건: engine/Validation/집계/노드 UI 중복 없음; 데이터 변경·StrictMode·unmount 시 동일 Core 계약 준수; 모든 Story는 가상 데이터만 사용; Header/Dashboard/신규 페이지 조합 없음. 지원 Props와 stale response 동작을 명시함.
|
||||
|
||||
### 5단계 — 배포·최종 통합 검증
|
||||
|
||||
npm/ESM/browser bundle·CSS·`.d.ts`와 license·asset·dependency 포함 범위를 실제 build와 consumer에서 확인한다. §3의 최종 검증을 변경 최종 상태 기준으로 한 번 종합 실행한다. 실패를 수정한 경우 영향받은 검사만 다시 수행한다. 같은 조건으로 이미 성공한 검사를 형식적으로 반복하지 않는다.
|
||||
|
||||
완료 조건: HTML과 React의 공통 기능/디자인/데이터/집계/인터랙션 일치; 네 컨테이너 너비의 실행 증거; JSON → Mock API 교체; lifecycle/CSS/접근성·asset 결과; build와 browser 결과 별도 보고; NOT VERIFIED·TBD를 감추지 않음; 운영 main 및 요청 밖 파일 보존. 실제 API 지원 완료와 npm publish는 이 단계에서 자동 주장하거나 수행하지 않는다.
|
||||
|
||||
## 2. Storybook 구성 제안
|
||||
|
||||
최상위 `OrgChart` 콘텐츠 그룹 후보다. 이름·기본 sidebar 대표 Story와 세부 검증 Story 배치는 승인 후 확정한다. 기존 All Story나 navigation Pattern 대표 Story를 교체하지 않는다.
|
||||
|
||||
- Basic OrgChart: 작은 가상 ID 계층, 기본 노드.
|
||||
- Leader Hierarchy: 최상위/하위 leader, 공석, 허브 처리.
|
||||
- Organization / Team / Cell: 부문·실·Div·셀·팀, 정렬·다단계 계층.
|
||||
- Employee Cards: 이름·직위/직책·이미지 실패/fallback·긴 텍스트.
|
||||
- Summary Table: 직접/하위 포함·직위/상태 집계, 정책 필요 상태.
|
||||
- Multiple Affiliations: 같은 집계 정책의 관계사별 숫자와 총원.
|
||||
- Dispatched / On Leave: 상태 변경, 원소속 표시 정책, 중복 카드와 ID 집계 구별.
|
||||
- Full OrgChart Content: 8개 콘텐츠 영역과 추가 관계. Header·GNB/LNB·Dashboard 전체 조합 제외.
|
||||
- JSON Data Update: 조직/직원 이동·추가/삭제·상태/사진·명칭·기준일 갱신.
|
||||
- Mock API Data Source: 같은 fixture의 지연/실패/응답 경쟁, JSON Source 전환.
|
||||
- Loading / Empty / Error: schema/참조/cycle/정책 오류·제어된 상태.
|
||||
- Responsive Container: 390/768/1280/1440px와 부모 크기 변경, 다중 instance.
|
||||
|
||||
Storybook은 React Wrapper의 실제 Core를 검증한다. Vanilla 단독 consumer 검증을 Storybook 성공으로 대체하지 않는다. 지원하지 않는 API·인증·직원 개인정보나 verification Story의 부가 기능을 신규 요구로 복사하지 않는다.
|
||||
|
||||
## 3. 최종 통합 검증
|
||||
|
||||
[Framework Integration §7](framework-integration.md#7-최소-통합-검증-계획)의 10항목을 최종 증거의 공통 기준으로 사용한다. 다음을 관련 항목에 연결한다.
|
||||
|
||||
- 데이터: 모든 Validation 실패 유형·leader/하위 포함/상태/관계사 집계·immutable 원본·같은 입력의 결정적 결과·JSON/Mock API parity.
|
||||
- HTML/React: 동일 조직 ID/parent/순서/leader/직원 그룹·계산 숫자·노드 UI·Theme·키보드/zoom/확장 상태. Wrapper의 다른 engine 구현 부재와 Core 코드 재사용을 검토.
|
||||
- lifecycle: data update/reload 경쟁·abort·StrictMode·반복 mount/unmount·destroy 두 번·destroy 후 callback 없음·두 instance의 DOM ID/이벤트/scroll/focus 격리. 이벤트/Observer/DOM 수와 제거 후 동작 증거를 남김.
|
||||
- lifecycle 재생성: 이전 chart 참조를 잃기 전 handler/Observer 정리, 반복 init/update/reload의 누적 이벤트 검사. facade import와 engine의 DOM global 부작용을 구별하고 지연 로딩 entry를 확인.
|
||||
- 컨테이너: 네 너비의 캡처·computed style·document/내부 scrollWidth/clientWidth·scrollHeight/clientHeight, 긴 명칭·ID 한 줄 식별·이미지/폰트 로드·absolute 알림 기준·Overlay 좌표.
|
||||
- 접근성: 이름/label 연결, Tab/Enter/Space/Escape 등 해당 기능 키보드·focus 복귀, disabled/readOnly, screen reader 상태 설명. 실행하지 않은 범위는 NOT VERIFIED.
|
||||
- 배포: package tarball imports·browser-ready ESM/classic bundle·별도 CSS/타입/상대 asset·React 미설치 HTML·Theme 미연결 승인 정책·CSP/CORS 범위. typecheck/build/browser를 구분하고 통과한 조건만 PASS.
|
||||
- Theme 배포·소유권: React peer 없는 Vanilla Theme 공급 경로, Core/engine CSS와 호스트 Theme global 영향 구별, 상대 font URL, Global A/Scoped B 두 instance의 computed·Default/partial·cleanup. Wrapper의 별도 summary/state/node 구현 부재 확인.
|
||||
- 영향: 최종 Git diff·요구사항·관련 docs/Storybook 정합성·불필요한 변경·운영 main/기존 Component/Pattern/Theme 보존.
|
||||
|
||||
관련 TypeScript·build·Storybook 명령은 미래 승인된 package scripts로 결정한다. 현재 package의 `prebuild/pretypecheck`가 생성물을 만들 수 있어 문서 전용 작업에서 실행하지 않는다.
|
||||
|
||||
Guard는 React consumer target이 실제 존재할 때 [Guard 정책](../guidelines/ai-generation-guard.md)의 적합한 범위·files/result로 1회 적용한다. PARTIAL 콘텐츠 검사가 전체 페이지 계약을 대체하지 않는다. Vanilla Core는 React bootstrap 전제와 충돌할 수 있으므로 기존 [Runtime Portability](../guidelines/runtime-portability.md)를 바탕으로 검사 적용 범위를 별도 합의하고 자동 PASS를 만들지 않는다. Guard 원본 status/staticStatus와 Runtime 증거를 구별하고 STYLE의 STATIC/RUNTIME/DESIGN_INTENT/FINAL을 함께 보고한다. 현재 지침을 이번 문서 작업에서 고치지 않는다.
|
||||
|
||||
## 4. TBD 결정 목록과 리스크
|
||||
|
||||
- **패키지/배포**: 두 package 최종 분리·경로·exports·type·dependency/peer·build 연결·Core npm 이름·browser namespace/format·engine 고정 버전. vanilla용 bare-import 없는 bundle과 React dependency 격리가 필요하다.
|
||||
- **생명주기**: upstream 완전 disposal·Observer·document 이벤트 해제, 갱신 시 상태/focus/zoom 보존, 옵션 갱신 방식·대용량 전체 rebuild 성능.
|
||||
- **데이터**: schemaVersion·enum/null/필수 값·ID 범위·root 수·무소속·leader 소속/겸직·관계 방향/중복·복수 소속 계약·정렬 우선순위·안전 입력 상한.
|
||||
- **집계**: 파견/휴직 포함·재직 정의·조직장 포함·원소속/파견지·직원 ID 중복·관계사/총원 분모. 승인 없이는 숫자를 업무 기준으로 확정하지 않는다.
|
||||
- **디자인/모바일**: 캡처 자료 미제공, 허브 실체·곡선 Overlay·카드 anchor·노드/그룹 배치·읽기 가능한 최소 폭/배율·zoom limit·모바일 방식·Theme 미연결 정책. computed 일치와 디자인 승인도 구별한다.
|
||||
- **API/보안**: 실제 endpoint/auth/권한·캐시/재시도·갱신·버전·이미지 권한·개인정보·외부 입력/URL 정책. 명세 없는 부분은 상상해 구현하지 않는다.
|
||||
- **지침 정합성**: 기존 React/HM 재사용 원칙과 framework-independent 노드의 예외, 공식 일반 수정 경로와 Feature 문서 경로, Guard runtime 전제. README의 차이를 보존하고 작업별 사용자 지시를 우선한다.
|
||||
|
||||
Vue·Angular·기타 framework Adapter는 TBD 개발 범위가 아니라 **이번 범위 제외**다. 미래 별도 요청이 있어야 개발 대상으로 편입한다.
|
||||
|
||||
## 5. 이번 문서 작업의 검증 기록
|
||||
|
||||
기준 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`, 경로 `HMDesign-OrgChart`, 브랜치 `feature/orgchart-package`. 최초/최신 추가 요청의 확정 요구를 README에서 추적한다.
|
||||
|
||||
- PASS: 작업 전 Git root/branch/HEAD/origin/운영 Worktree·원격 main/Feature SHA·깨끗한 Working Tree 확인.
|
||||
- PASS: 현재 workspace·package export·Storybook glob·CSS entry·선택 Component/Pattern·Token 실제 파일 확인. 이는 읽기 전용 구조 확인이며 신규 API/배포 검증은 아니다.
|
||||
- PASS: 문서 7개에 콘텐츠 독립성·Core/React Wrapper·같은 계약/집계/노드/Theme/인터랙션·Source 교체·페이지 제외·TBD·개발 게이트를 반영.
|
||||
- PASS: 기존 `node scripts/check-doc-links.mjs` 실행에서 Markdown 118개·내부 링크 620개·problems 0. 신규 OrgChart 문서의 내부 링크는 53개다. 외부 URL 도달성 전체 검사는 포함하지 않는다.
|
||||
- PASS: JSON code/inline 블록 3개 구문, JavaScript/HTML inline script 2개 구문, 가상 fixture의 required/type/enum/null·ID 참조·parent cycle·raw 수 2/1/1, Schema와 TypeScript 초안의 필드 일치를 읽기 전용으로 확인했다. Schema에 사용한 일부 keyword의 구조 검사이며 완전 Draft 2020-12 validator나 TypeScript typecheck가 아니다.
|
||||
- PASS: 최종 확인 시 개발 Worktree는 `docs/org-chart/*.md` 7개만 untracked, 기존 tracked/staged diff 0. 운영 Worktree는 깨끗하고 Local/원격 main·Feature SHA는 시작 HEAD 그대로였다. package/lockfile·기존 UI/Theme/Storybook/AGENTS 수정, 설치·새 Worktree·Stage·Commit·Push 없음.
|
||||
- NOT VERIFIED: 캡처 시각 대조, engine 고정 tag/npm 배포물 동일성, 실행/빌드/HTML bundle/React Wrapper/Theme computed/접근성·반응형·생명주기·실제 API. 구현 전 문서 작업이라 실행 검사 PASS를 내지 않는다.
|
||||
- NOT VERIFIED: 완전한 JSON Schema validation과 TypeScript compiler/typecheck. 현재 Worktree의 관련 dependency가 없어 설치하지 않았고 필드/구문 확인으로 지원 완료를 주장하지 않는다.
|
||||
- 적용 대상 없음: 이번 화면 생성 Guard(typecheck/build 포함). React bootstrap·UI 변경 없이 Markdown만 작성하므로 실행하지 않는다.
|
||||
|
||||
이 단계의 최종 산출물은 사용자 검토 대상 설계 문서다. 다음 기능 개발·설치·Stage·Commit·Push는 시작하지 않고 사용자 검토 및 승인을 기다린다.
|
||||
|
||||
## 6. 병렬 읽기 전용 검토 통합
|
||||
|
||||
사용자 요청으로 분석 작업자 3개를 동시에 사용했다. 각 작업자는 읽기 전용으로 검토했고 문서 작성·수정은 메인 작업자만 수행했다. 기존 링크/fixture 검사는 작업자마다 중복 실행하지 않았다.
|
||||
|
||||
- **엔진 HTML·React 호환성**: 이전 chart를 잃기 전 update/reload cleanup, 문자열 template + createNode 안전 DOM 우선안, DOM global import 부작용·지연 로딩, 우선 두 패키지 표현 통일을 Integration/Architecture/Framework Integration에 통합했다.
|
||||
- **HM Theme·Token·UI**: React peer 없는 Vanilla Theme 공급, host Theme와 Core/engine CSS 영향 구별, 상대 font, Scoped mapping/Default/owner, Wrapper의 summary/state/node 중복 금지를 Architecture/Integration/UI Composition/Framework Integration에 통합했다.
|
||||
- **데이터·Validation·API 확장**: fatal data issue와 집계 unavailable/Source 실패의 경계, immutable 스냅샷·최종 revision 확인, 여러 API/페이지의 완전한 동일 기준 스냅샷 조립, 집계 정책 자체 검사·결정적 정렬 조건을 Data Contract/Architecture/Integration/Framework Integration에 통합했다.
|
||||
|
||||
각 검토는 설계·공식 소스의 정적 근거다. 실제 이벤트 누수·빌드·computed·API 일관성이 검증됐다는 뜻이 아니며 해당 NOT VERIFIED를 유지한다. 중복 문서·구현 코드·별도 리뷰 파일은 만들지 않았다.
|
||||
@@ -0,0 +1,73 @@
|
||||
# HMDesign OrgChart — Chart Only 구현 보고서 (2026-10-13)
|
||||
|
||||
RESULT: **PASS** (Auto/Expanded · React · Vanilla). Fixed Chart Only는 지원하지 않으며 제약으로 구분한다. Commit·Push·Merge는 하지 않았다.
|
||||
|
||||
## 1. 작업 위치
|
||||
|
||||
- Worktree: `D:\디자인시스템\HMDesign-OrgChart-ChartOnly-Impl`, 브랜치 `feature/orgchart-chart-only-implementation`, 기준 HEAD `0a8daf7ee7ac026952a2e1d3d5f25a922dab07cf`(변경은 미커밋).
|
||||
- 의존성은 이 Worktree에서 `npm ci --ignore-scripts`(lockfile 그대로)로 설치했다. 버전 변경 없음.
|
||||
|
||||
## 2. UI 소유 위치 (STEP 1)
|
||||
|
||||
| UI | 소유 | Chart Only 처리 |
|
||||
|---|---|---|
|
||||
| 제목·기준일(`createHeading`) | Core `expanded-content.ts` | 생성하지 않음 |
|
||||
| 선택 회사 수·인원 요약 | Core `index.ts` | 생성하지 않음 |
|
||||
| 확대/축소/100%/Fit 버튼·배율·조작 안내 | Core `expanded-viewport.ts` | toolbar 없이 같은 viewport 컨트롤러 사용 |
|
||||
| Drag & Drop 상태 문구 | Core `index.ts` | 요소는 유지(aria-live·aria-describedby), 화면에서는 시각적으로 숨김 |
|
||||
| 회색 패딩·테두리 외곽 | Core CSS `.hm-orgchart-expanded` | chart-only에서 흰색·패딩 0·테두리 0 |
|
||||
| Storybook 필터바·페이지 제목·설명 | Storybook Preview | 변경 없음(사용하지 않음) |
|
||||
| React Wrapper | 컨테이너 div와 선택형 Popup Adapter만 | 변경 없음 |
|
||||
|
||||
기존 API만으로는 Vanilla `fitToView()` 호출까지 가능했고, 내장 UI 제거·React 초기 Fit·직접 휠 Zoom은 불가능했다.
|
||||
|
||||
## 3. 추가한 공개 API (Core 옵션, React Props로 그대로 전달)
|
||||
|
||||
- `presentationMode?: 'default' | 'chart-only'` — 기본 `'default'`. Auto 전용. Fixed와 함께 쓰면 `TypeError`.
|
||||
- `initialView?: 'center' | 'fit'` — chart-only 기본 `'fit'`, 그 외 `'center'`(기존 동작).
|
||||
- `wheelZoom?: 'modifier' | 'direct'` — chart-only 기본 `'direct'`, 그 외 `'modifier'`(기존 Ctrl/⌘ + 휠).
|
||||
- 타입 export: `OrgChartPresentationMode`, `OrgChartInitialView`, `OrgChartWheelZoom`.
|
||||
- React Wrapper: 세 옵션 전달, 객체 옵션(`autoLayout`·`interactions`·`selectedCompanyIds`·`zoom`·`companyTopNodes`·`employeeConnections`·`organizationLinks`·`aggregationPolicy`)을 값으로 비교해 같은 값의 inline 객체 리렌더링이 재마운트·재Fit을 일으키지 않게 했다. 마운트 직후 중복 `setSelectedCompanies` 호출도 사라졌다.
|
||||
- JSON Contract, `/hm-dialog`, `employeePopupAdapter`, 기본 Entry 의존성, 카드 SVG, 회사 컬러는 변경하지 않았다.
|
||||
|
||||
## 4. 동작
|
||||
|
||||
- 초기 Fit: 실제 Forest Bounds(조직·연결선 path 포함) 기준, 16px 여백, 최대 100%, 가로·세로 중앙. 측정 전에는 forest를 `visibility: hidden`으로 두어 첫 프레임이 튀지 않는다. 캔버스 크기 0이면 실행하지 않고 ResizeObserver/레이아웃 완료 경로에서 실행한다.
|
||||
- 재Fit: 최초 렌더링, `update`/`reload`/React `dataSource` 교체, 회사 선택·layout 변경. Zoom·Pan·팝업·그룹 접기·Drag & Drop 전환·단순 리렌더링에서는 재Fit하지 않는다.
|
||||
- 휠: 캔버스 안에서만 `preventDefault` 후 포인터 기준 Zoom. delta 정규화(line/page)와 이벤트당 ±120 제한. 캔버스 밖은 페이지 스크롤. `interactions.zoom=false`이면 휠을 가로채지 않는다.
|
||||
- Fit 배율이 `zoom.min`보다 작으면 휠 축소는 Fit 배율에서 멈춘다(`initialView: 'fit'`일 때만).
|
||||
|
||||
## 5. 브라우저 검증 (실제 Chrome 155, Playwright 1.64)
|
||||
|
||||
[`examples/org-chart-chart-only/verify.mjs`](../../examples/org-chart-chart-only/verify.mjs) **46/46 PASS, 콘솔 오류 0**. 증거: `test-results/org-chart-chart-only/`(ignored).
|
||||
|
||||
| 데이터 (1440×900) | 배율 | 중앙 오차 X/Y | 상하좌우 여백 |
|
||||
|---|---|---|---|
|
||||
| 20명·3개 회사 | 1.0000 | 0 / 0 px | 174 / 29.75 px |
|
||||
| 100명·5개 회사 | 0.5135 | 0 / 0 px | 16 / 89.66 px |
|
||||
| 500명·삼안 6본부 17팀 | 0.3254 | 0 / 0 px | 231.58 / 16 px |
|
||||
| 100명 LR / Hybrid | 0.1819 / 0.5309 | 0 / 0 px | — |
|
||||
| 모바일 390×844 (20/100/500) | 0.3278 / 0.1306 / 0.1193 | 0 / 0 px | 페이지 가로 넘침 없음 |
|
||||
|
||||
- React와 Vanilla의 배율·여백·휠·Pan 수치가 동일하다.
|
||||
- 일반 휠 위/아래 Zoom(0.3254→0.3974→0.3254), 포인터 아래 지점 화면 이동 0.14px, 최대 3·최소 0.05, Zoom 후 Pan 정확히 (120, −80)px, 600ms 뒤 재Fit 없음, Ctrl+휠도 캔버스 안에서만 동작(페이지 확대 없음).
|
||||
- 임베드 페이지: 캔버스 밖 휠은 페이지 스크롤(180px), 안쪽 휠은 스크롤 없이 Zoom.
|
||||
- 직원 팝업(Core·`/hm-dialog` HM Dialog) 열기/닫기, 그룹 접기·펼치기(500→472→500명) 후 배율·위치 유지. Drag & Drop 기본 OFF, 카드 드래그 후 소속 불변.
|
||||
- 데이터 전체 교체(500→100명) 후 다시 Fit·중앙(오차 0px). React 단순 리렌더링 후 사용자 뷰 유지.
|
||||
- 모바일 터치 Pan(CDP touch) 동작. 모바일에서 휠은 검증 대상이 아니다.
|
||||
|
||||
## 6. 기존 화면 보호
|
||||
|
||||
[`verify-storybook.mjs`](../../examples/org-chart-chart-only/verify-storybook.mjs) **9/9 PASS, 콘솔 오류 0**. 이 Worktree의 Storybook(6044)을 기존 Feature Storybook(6024, 같은 HEAD)과 비교했다.
|
||||
|
||||
- 공개 OrgChart 메뉴 4개 유지(Overview, Department Colors, Full Composition, Expanded OrgChart).
|
||||
- 4개 페이지 모두 DOM 동일, 스크린샷 픽셀 동일. Full Composition은 한 실행에서 5픽셀 ±1 래스터 오차가 있었고 재실행에서 픽셀 동일이었다.
|
||||
- Expanded: 제목·기준일·요약·툴바 4버튼·배율·`드래그·방향키 이동 · Ctrl/⌘ + 휠 확대` 안내·Drag & Drop 문구 유지. 일반 휠은 Zoom하지 않고(페이지 스크롤), Ctrl+휠은 Zoom, Fit to View 버튼 동작.
|
||||
- 그 외: Core 단위 테스트 86/86, Guard 테스트 94/94(기준 HEAD와 동일), 루트 `tsc -b`, Core·React typecheck/build, Theme defaults check 통과.
|
||||
- 기존 `examples/org-chart-react/verify-boundaries.mjs`: 8 PASS / 1 FAIL이며 기준 HEAD 임시 Worktree에서도 같은 항목이 같은 결과로 실패한다(Fixed 390px 내부 가로 스크롤 검사, 기존 실패).
|
||||
|
||||
## 7. 제약 · NOT VERIFIED
|
||||
|
||||
- **Fixed Chart Only 미지원**: Workdesk 구성(제목·요약·아트워크·비례 축소)이 서로 묶여 있어 일부만 제거하면 승인 디자인이 바뀐다. 옵션을 함께 쓰면 오류로 알린다. Fixed에는 Auto의 휠 Zoom/Pan이 없다.
|
||||
- NOT VERIFIED: Safari/Firefox, macOS 트랙패드 핀치, 실제 모바일 기기, 스크린리더 낭독, 2,100명 전체 회귀(요청대로 반복하지 않음), `examples/org-chart-react/verify.mjs`(storybook-static 빌드 필요)·기존 P9.x 개별 스크립트.
|
||||
- React imperative ref는 추가하지 않았다(초기 Fit을 옵션으로 해결).
|
||||
@@ -0,0 +1,95 @@
|
||||
# HMDesign OrgChart P1 결과 — 2026-10-12
|
||||
|
||||
RESULT: **PARTIAL**. 최종 요청의 독립 디자인·개별 공개 API·5개 Story 메뉴·HTML/React 기능 검증은 완료했다. 원본 Guard의 정적 토큰 추적 FAIL, 실측 흰색 부서명 컬러의 일부 명도 대비 부족, Workdesk 픽셀 일치 미검증은 아래에 보존한다. 전체 디자인 승인 또는 배포 승인으로 해석하지 않는다.
|
||||
|
||||
## 개발 환경과 보존
|
||||
|
||||
- 실제 수정 경로: `D:\디자인시스템\HMDesign-OrgChart`.
|
||||
- 기존 브랜치: `feature/orgchart-package`; 새 브랜치를 생성하지 않았다.
|
||||
- 작업 전/후 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`. Local `main` 및 기존 `origin/main` ref도 동일 SHA다. Git 쓰기 작업 없이 확인했다. 원격 HEAD는 이번 P1에서 새로 조회하지 않았다.
|
||||
- 공통 UI/Theme/Pattern, 기존 Wrapper 구현, 원본 JSON v1/v2, 기존 v0.1 구현·React 검증 보고서를 보존했다. 시작 시 기존 package.json/lock 수정 및 untracked 패키지 작업을 유지했다.
|
||||
- 시작 snapshot 671개 파일과 비교한 요청 범위 밖 변경은 0건이다(`test-results/org-chart-p1/preservation.json`). 기존 main checkout은 접근 제한으로 직접 Working Tree 검증하지 못했다. 그 경로에 수정 명령을 실행하지 않았다.
|
||||
- 별도 브랜치·Stage·Commit·Push·Merge를 수행하지 않았다. 작업은 미커밋 상태다. `git diff --check` 통과.
|
||||
|
||||
## 병렬 작업과 통합
|
||||
|
||||
사용자의 최초 P1 병렬 구현 요청에 따라 실제 두 Track을 병렬 실행했다. Track A는 공통 EmployeeCard, 조직별 presentation preset, 정식 로고 mapping, 독립 디자인 변수에 집중했다. Track B는 순수 summary selector, Position/Affiliation/Status DOM 모듈, 독립 Story 및 변경 범위 browser verifier를 담당했다. Main은 공통 controller/Tree/Content 통합, 공개 factory, 실제 Workdesk/Figma 근거 확인, 배포 예제와 canonical Story, 빌드·최종 증거 연결을 담당했다. 책임 파일을 분리했고 공유 파일 충돌은 없었다. 정상 테스트를 두 Track에서 반복하지 않았다.
|
||||
|
||||
## 디자인·콘텐츠 구현
|
||||
|
||||
1. `OrgChartContent`가 독립 Tree와 Summary를 조합한다. 리더·허브는 조직 트리의 가짜 parent를 만들지 않는다. 기존 organization ID/parentId를 유지한다.
|
||||
2. 최상위 리더, 조직장, 일반 직원은 같은 Core `createEmployeeCard`를 사용한다. 직위·직책·소속 로고·근무 상태를 표시하며 null/실패 프로필 이미지는 이니셜로 대체한다. React 카드 UI는 추가하지 않았다.
|
||||
3. 중앙 협업·소통 허브는 `presentation.hubText`로 표시한다. 추가 관계선은 구현하지 않았다.
|
||||
4. 사업관리·경영기획·인재성장·통합시스템 Div·디자인기획·솔루션영업과 확인된 셀/팀 명칭을 fixture에 사용한다. 6개 branch는 organization ID preset에 연결하고 하위 셀/팀이 variant를 상속한다. fixture membership/leader/직위는 가상 설정이다.
|
||||
5. 우측 PositionSummary는 organization × position 교차표, 직접/하위 포함 인원, 상태별 Raw Count를 같은 summary/selector에서 계산한다. 좁은 container에서는 표와 트리 각각 내부 스크롤을 제공한다.
|
||||
6. 하단 AffiliationSummary는 7개 관계사 명칭과 실제 로고, 가상 Raw Count를 표시한다. 0명 관계사 3곳도 표시한다.
|
||||
7. 파견·휴직은 독립 StatusSummary 안에서 원본 직원 참조로 표시한다. 전체 인원에 다시 더하지 않는다. 각 그룹 flag를 독립 적용한다.
|
||||
8. 카드·Typography·Spacing·Border·Radius·Gradient는 OrgChart 내부 CSS와 presentation preset으로 관리한다. 공통 HM Token을 필수로 사용하지 않고 Theme가 없는 호스트에서도 표시된다. 전용 public 변수만 외부 override에 사용한다. CSS는 Shadow DOM/내부 shell에 격리된다.
|
||||
9. Full Composition에 Header·GNB·LNB·페이지 Routing을 추가하지 않았다. Storybook 외부 검증 controls만 기존 공개 HM Button을 사용하며 Core 자체의 Theme 의존성과 구분한다.
|
||||
10. 같은 fake JSON, validation/normalize/summary/lifecycle, Core DOM/CSS를 HTML과 React가 공유한다. 기존 React Wrapper 구현과 Props API는 변경하지 않았다.
|
||||
|
||||
## 컬러와 로고 근거
|
||||
|
||||
Workdesk `https://workdesk.hmac.kr/`, 표시 버전 `v1.2610.9c5e`, 2026-10-12 관찰. 6개 visible 조직 heading 부모의 computed background-image와 heading white text를 읽었다.
|
||||
|
||||
- 사업관리 `.h33`: `#194d82 → #1d395c`.
|
||||
- 경영기획 `.h33.org-team-link`: `#2164a8 → #254975`.
|
||||
- 인재성장 `.h38.org-team-link`: `#0f6ace → #044a94`.
|
||||
- 통합시스템 Div `.h310.org-team-link`: `#8c67b6 → #5d3494`.
|
||||
- 디자인기획 `.h312.org-team-link`: `#09a7a2 → #00847f`.
|
||||
- 솔루션영업 `.h36.org-team-link`: `#5a963c → #3f7629`.
|
||||
|
||||
부서명 텍스트 실측값은 모두 `#ffffff`. 이는 헤더 측정이며 직원 카드 전체 원본 스타일을 검증한 값이 아니다. 카드 배경, 기본 Typography/Spacing/Radius 및 헤더 시작색을 재사용한 카드 테두리는 DESIGN_ADAPTATION으로 명시했다. 임의 기본값을 공식 Workdesk 값으로 확정하지 않았다. 기존 공통 Theme 값에 억지로 맞추지 않았다.
|
||||
|
||||
정식 로고 7종은 접근 가능한 Figma 파일 `Plr6RRLi5XnLIn60x9aG1s`의 footer `22:179`, logo nodes `22:181/187/193/199/205/211/217`에서 받은 PNG다. `(주)한맥기술`, `(주)삼안`, `(주)장헌`, `장헌산업`, `PTC`, `한라산업개발`, `바론컨설턴트`. affiliation ID로 Core map에 연결하며 같은 자산을 카드·하단 summary·Logo에서 사용한다. 원본 PNG/크기/SHA256/출처는 `packages/org-chart/src/assets/affiliations/README.md`에 기록한다. 임시 Figma URL은 배포에 필요 없고 bundle에 inline된다. 실제 프로필 사진·개인정보는 가져오지 않았다. 로고 미확보 항목은 이 7종 범위에서 없다. 미매핑 ID/파일 오류는 명칭 텍스트 fallback으로 처리한다.
|
||||
|
||||
## 공개 API·파일 변경
|
||||
|
||||
패키지 root에 `createOrgChartTree`, `createPositionSummary`, `createAffiliationSummary`, `createStatusSummary`, 타입 `OrgChartSummaryComponentOptions`를 additive export했다. 기존 API와 `0.1-draft` schema는 유지한다. Summary는 `stylesheetUrl` 하나만 필요하며 Tree는 기존 engine/core URL pair를 사용한다. 모든 factory는 기존 OrgChartHandle을 반환한다. 다른 업무 페이지의 container에 각각 mount/update/reload/destroy할 수 있으며 전체 OrgChart나 Header를 요구하지 않는다. [사용법과 override 예시](standalone-components-p1.md).
|
||||
|
||||
- 주요 Core 변경: `core/index.ts`, `render.ts`, `employee-card.ts`, `affiliation-mark.ts`, `style-presets.ts`, `src/index.ts`, `build.mjs`, `vendor.d.ts`.
|
||||
- 독립 모듈: `org-chart-content.ts`, `org-chart-tree.ts`, `position-summary.ts`, `affiliation-summary.ts`, `status-summary.ts`, `logo-legend.ts`, `department-colors.ts`, `components.ts`.
|
||||
- 데이터: `content-selectors.ts`와 신규 7개 selector 테스트. 기존 validator/schema/source/aggregation 계약을 바꾸지 않았다.
|
||||
- 스타일: `design-tokens.css`, `employee-card.css`, `summary-sections.css`, `org-chart-content.css`, `org-chart.css`. 빌드가 모든 내부 CSS를 `dist/style.css` 하나로 inline한다.
|
||||
- 예제: 가상 `data-p1.json`과 생성기, Theme 없는 `p1.html/p1-classic.html/p1-app.js/p1.css`, React `p1.html/p1.tsx/p1.css`; 기존 examples entry 유지. React Vite input에 P1 entry만 추가했다.
|
||||
- Storybook: Core component preview, card/internal summary Story, canonical P1 Story. 기존 8개 Story에는 `internal` tag만 추가하여 메뉴에서 숨기고 테스트/내용은 보존했다. 신규 card/status verification Story도 숨겼다.
|
||||
- 사용자용 OrgChart 메뉴는 **Logo / Department Colors / Position Summary / Affiliation Summary / Full Composition** 5개뿐이다. Department Colors는 실제 swatch/HEX/CSS variable, 카드 배경·직원 텍스트·부서명 텍스트·border/Gradient, OrgChart 전용 여부와 실측/기본값 근거를 표시한다.
|
||||
- 추가 npm dependency: 없음. 현재 React 19/Storybook 10.6.1/engine 6.0.0을 유지한다. 별도 Summary npm 패키지를 만들지 않았다.
|
||||
|
||||
## 검증 결과와 범위
|
||||
|
||||
- Core typecheck: 초기 점검 후 Department Colors의 literal union Set 타입 오류를 수정했다. 최종 Core 빌드에 포함된 TS declaration compiler 통과.
|
||||
- Story/source graph typecheck: `npm.cmd exec -- tsc -p tsconfig.app.json --noEmit` 최종 PASS.
|
||||
- Core ESM/IIFE/declaration/CSS build PASS. 공개 root import는 DOM 없는 Node에서 정상이고 기존 createOrgChart와 4개 신규 factory 모두 function으로 확인했다.
|
||||
- 기존 Wrapper typecheck/build PASS. 그 이후 Wrapper 구현은 변경하지 않았다.
|
||||
- HTML prepare, 별도 React consumer production build, Storybook production build PASS. 500kB chunk와 Storybook plugin timing 경고는 남았으며 오류는 아니다.
|
||||
- 새 selector 7개 테스트 **7/7 PASS**. 원본 data 테스트/기존 69·79 browser suite를 재실행하지 않았다.
|
||||
- 최초 신규 browser 검증 **197 checks: 196 PASS / 1 FAIL**. 유일 실패는 개별 meta의 storySort global-only 콘솔 오류였다. 해당 옵션을 제거하고 Storybook만 재빌드했다.
|
||||
- 오류 및 실제 메뉴 선택 재검증 **4/4 PASS**: 수정된 canonical 5개 iframe 콘솔 오류 0, 실제 manager/sidebar 5개 항목 정확 노출, 내부 검증 그룹 기본 숨김, manager 오류 0. 정상 196개 검사는 반복하지 않았다. 최초 report의 FAIL은 삭제하지 않고 후속 해소 증거로 연결한다.
|
||||
- Browser Chrome `155.0.8059.39`. HTML/React 가상41명, 18조직, 상태·소속·이동 JSON, 원본 집계/정책 미확정 메시지, group별 표시, keyboard collapse/expand/focus, 긴 이름/직위/직책, null/broken profile, 실로고 로딩·텍스트 fallback, destroy 검증 PASS.
|
||||
- `390/768/1280/1440px`: HTML/React/Full Composition 문서 overflow·글자 bounds·Navigation 없음 PASS. 트리/교차표 내부 overflow는 의도한 스크롤이다. 전체 6개 branch가 작은 폭에서 한 화면에 모두 보인다는 의미는 아니다.
|
||||
- Theme 미연결 HTML/React 실제 CSS PASS; host 공통 HM 변수 override에 영향 없음, public OrgChart override 적용 및 다른 instance/page에 영향 없음 PASS. 6개 조직별 Shadow DOM Gradient와 Department Colors legend/실제 카드색 일치 PASS.
|
||||
- 독립 Summary 3종 및 Tree의 공개 dist API mount/update/destroy PASS. Summary에 engine stylesheet 없고 Tree root leader가 누락되지 않음 확인.
|
||||
- 로컬 첫 브라우저 접근은 sandbox `ERR_NETWORK_ACCESS_DENIED`로 검사 전 중단됐다. 승인된 로컬 서버 환경에서 신규 검증을 실행했으며 운영 사이트를 수정하지 않았다.
|
||||
|
||||
증거 파일: `test-results/org-chart-p1/browser.json`, `storybook-console-recheck.json`, `preservation.json`, `contrast.json`, `storybook-build.log`; HTML/React/Full Composition 각 4개 폭 PNG와 `storybook-sidebar.png`. 원본 v0.1/React 보고서·검증 artifacts는 유지한다.
|
||||
|
||||
## Guard·명도 대비·남은 한계
|
||||
|
||||
Storybook PARTIAL Guard 원본: **status/staticStatus FAIL**, TOKEN-001 3개(`resolved-bg-surface`, `current-border`, `current-gradient`) 및 TOKEN-002 동적 변수 미검증. 최신 menu 파일 재검사도 같은 static FAIL을 보존한다. Guard는 `?url`로 가져온 private CSS를 일반 consumer CSS 적용으로 간주하지 않아 Shadow DOM 주입과 `style.setProperty` 선언을 static graph에서 연결하지 못한다. `resolved-bg-surface`는 `design-tokens.css`, current 변수는 `applyOrganizationStyle`에서 선언하며 실제 렌더의 6개 swatch/card/Gradient와 override 검증은 PASS다. 공통 Theme/Guard를 변경해 이 결과를 숨기지 않았다. 이 정적 FAIL을 Runtime PASS로 덮거나 전체 Guard PASS로 보고하지 않는다.
|
||||
|
||||
Storybook Style: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**(Guard 자체 기준). React consumer Guard: **status NOT VERIFIED / static NOT APPLICABLE**, RENDER-002 실제 bootstrap 브라우저 확인 요구; Style **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED**. 관련 브라우저 증거는 위의 별도 결과다. 원본 Guard JSON을 유지한다. 일반 HM React Component style override를 추가하지 않았다. OrgChart 독립 디자인 의도는 사용자의 2026-10-12 최종 지시를 근거로 적용했다.
|
||||
|
||||
흰색 텍스트와 측정 Gradient stop의 WCAG 대비를 계산했다. 일반 텍스트 4.5:1 기준: 통합시스템 시작색 4.44:1, 디자인기획 시작색 2.97:1, 솔루션영업 시작색 3.58:1은 미달한다. 다른 stop은 4.5:1 이상이다. 디자인기획 시작색은 큰 텍스트 3:1 기준도 약간 미달한다. 실측 디자인을 임의로 공식 보정하지 않고 원래 값을 유지했으며 접근성 개선 시 OrgChart 전용 department-color/gradient를 교체할 수 있다. 대비 검증을 전체 AA PASS로 표시하지 않는다. 기본 직원 foreground/흰 배경은 충분한 대비를 가진다.
|
||||
|
||||
Workdesk 최신 화면의 큰 배치와 실제 heading 컬러를 반영했으나 픽셀 일치, 카드 모든 spacing/radius/typography의 Figma 상세 정밀 측정은 NOT VERIFIED다. 실제 조직 parent/leader/membership 및 운영 인원은 Mock의 검증 대상이 아니다. Screen reader speech, 물리 touch, SSR/hydration, heap/GC 완전 회복은 NOT VERIFIED다. 원본이 비활성화한 pan/zoom/drag/export는 추가하지 않았다. 복잡한 협업 관계선·직원 상세·정책 승인 및 실제 HR/API 전환은 P2 범위다.
|
||||
|
||||
## 시간 기록 (KST)
|
||||
|
||||
- Started At: 2026-10-12 10:49:28 KST
|
||||
- Completed At: 2026-10-12 11:32:48 KST
|
||||
- Actual Duration: 0시간 43분 20초
|
||||
- Estimated Duration: 최초 2–3시간
|
||||
- Variance: 예상보다 76분 40초–136분 40초 단축
|
||||
|
||||
Commit·Push·Merge: **진행하지 않음**.
|
||||
@@ -0,0 +1,196 @@
|
||||
# HMDesign OrgChart P10 — main 비교 및 최종 품질 검증
|
||||
|
||||
RESULT: **PARTIAL — 확인한 기능은 PASS, 신규 제품 회귀 0. 기존 Guard FAIL·접근성 잔여·디자인 승인 및 배포 미검증은 남아 있다.**
|
||||
|
||||
이번 작업은 비교·검증·보고만 수행했다. 기존 구현과 승인 카드/회사색을 수정하지 않았다. main 변경 적용, Stage·Commit·Push·PR은 진행하지 않았다. 정식 main 통합을 무조건 PASS로 승인하는 보고서는 아니다.
|
||||
|
||||
## 1. 기준과 증거 재사용
|
||||
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- Local HEAD 및 live remote Feature: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- Fetch 후 origin/main: `c45d63a03b84eaac43f01bd400a16488a80e4229`
|
||||
- 공통 조상: `d18e3c33801e165e17b6d971e2f6198eed483c3c`
|
||||
- 시작 시 같은 폴더의 P9.5 세션은 idle였으며, P10 동안 기존 authored 파일 변경은 없었다. 다른 세션에 메시지·중단 요청을 보내지 않았다.
|
||||
- 기존 main Worktree와 AI Playground는 그대로 보존했다. Playground 페이지·JSON·변환 도구·실험 캡처를 이관하지 않았다.
|
||||
|
||||
AGENTS.md, CLAUDE.md, AI 생성 정책, P9.4 및 P9.5 중앙 배치/Full Composition/X 버튼/소비 화면 팝업 검증 문서를 확인했다. 최신 P9.5 popup-parity 시작 snapshot **839개 기존 파일의 SHA-256이 현재와 모두 동일**하며, Core와 Vanilla 예제의 `index.js`, `style.css`도 마지막 검증 artifact와 동일하다. 따라서 기존 정상 suite와 패키지·Storybook 빌드를 반복하지 않았다. P9.4 이후 중앙/Fit 변경은 P9.5 geometry 증거, 팝업 변경은 최종 popup-parity 증거로 연결했다.
|
||||
|
||||
근거: [증거 유효성](../../test-results/org-chart-p10/evidence-validity.json), [P9.5 소비 화면 검증](verification-p9-5-popup-parity.md). 보고서들의 PASS 수는 서로 합산하지 않는다.
|
||||
|
||||
## 2. origin/main 양방향 비교 — PASS
|
||||
|
||||
`git fetch origin main`을 실행했다. main에만 3개 커밋, Feature에만 7개 커밋이 있다. 공통 조상 이후 main 변경은 **6개 파일**, Feature 변경은 **192개 파일**이다. 양쪽 커밋의 공통 변경 파일 **0**, main 변경과 현재 미커밋 파일의 겹침 **0**이다. 이는 파일 변경 집합 비교이며 실제 통합 실행의 성공 증거는 아니다.
|
||||
|
||||
main 전용 커밋:
|
||||
|
||||
- `4d597a16506809a97086f7e65cfeb7fbfc486561`: Docker/nginx 정적 Storybook과 Gitea 배포 설정 추가.
|
||||
- `9118c2c602791f9b90417af14f029ba9069ac265`: 실행 커밋의 immutable source archive를 SSH로 전달하고 별도 release directory에서 빌드하도록 보완.
|
||||
- `c45d63a03b84eaac43f01bd400a16488a80e4229`: 위 변경의 main merge commit. 별도 UI 변경은 없다.
|
||||
|
||||
Feature 전용 7개 커밋의 전체 SHA·목적과 양쪽 파일 목록은 [main-comparison.json](../../test-results/org-chart-p10/main-comparison.json)에 기록했다. HEAD끼리의 큰 diff를 main이 OrgChart를 삭제한 변경으로 해석하지 않았다.
|
||||
|
||||
### 분류 및 적용 권고
|
||||
|
||||
**A. 반영 필요: 0건.** main에서 UI 컴포넌트, Dialog/Switch/Checkbox, 토큰/Typography/공통 CSS, Storybook 소스 설정, exports, React/TypeScript/build scripts, package manifest/lockfile 또는 OrgChart 인터페이스가 변경되지 않았다. 현재 OrgChart 동작을 복구하기 위한 필수 반영은 없다.
|
||||
|
||||
**B. 반영 권장: 6개 파일.** 아래는 동일한 배포 변경 묶음이며, 승인 후 함께 통합하는 편이 낫다.
|
||||
|
||||
1. `Dockerfile` — `4d597a16506809a97086f7e65cfeb7fbfc486561`. Node 24 Alpine에서 npm ci·앱·Storybook build 후 nginx 1.29 Alpine으로 제공. 현재 브라우저 동작 영향 없음, 배포 빌드에는 새 OrgChart workspace 전체가 포함돼야 한다. 승인 후 main의 최종 파일을 통합. 파일 충돌 위험 낮음; Linux/컨테이너 빌드 호환성은 NOT VERIFIED. 파일 적용 약 2~3분.
|
||||
2. `compose.yaml` — 위 커밋과 `9118c2c602791f9b90417af14f029ba9069ac265`. `name: hmdesign`, 기본 6024:80 포트, restart 정책. 현재 로컬 6064 미리보기와 독립적이다. Dockerfile과 같이 통합. 파일 충돌 위험 낮음; 실제 서버 포트·Compose 상태 확인 필요. 약 1~2분.
|
||||
3. `.gitea/workflows/deploy-storybook.yml` — 위 두 커밋. main push/수동 실행 시 SHA archive 전송·release directory Docker build·서비스 교체. Feature commit만으로 배포되지 않으나 추후 main push의 외부 영향이 생긴다. **초기 커밋만 cherry-pick하지 말고 archive 보완까지 포함**한다. 파일 충돌 위험 낮음; Actions 변수/Secret/SSH/서버 권한/실제 배포는 NOT VERIFIED. 파일 검토·적용 약 3~5분, 배포 검증 시간 별도.
|
||||
4. `.dockerignore` — `4d597a16506809a97086f7e65cfeb7fbfc486561`. Git·node_modules·dist·검증 생성물·로컬 작업본·env 제외. OrgChart authored 소스는 포함되고 dist는 컨테이너 안에서 생성된다. Dockerfile과 같이 통합. 파일 충돌 위험 낮음. 약 1분.
|
||||
5. `.gitignore` — `9118c2c602791f9b90417af14f029ba9069ac265`. `.storybook-deploy/` 제외만 추가. 현재 authored 작업과 겹침 없다. 기존 ignore 규칙을 유지하며 이 한 줄 또는 최종 main patch를 반영. 충돌 위험 낮음. 약 1분.
|
||||
6. `README.md` — 위 두 커밋. Docker/PowerShell 실행·archive 기반 Gitea 배포 안내 추가. OrgChart 문서 수정과 파일 겹침 없다. 현재 OrgChart 설명을 보존하여 main의 배포 섹션만 통합. 충돌 위험 낮음. 약 2~3분.
|
||||
|
||||
전체 검토·통합 예상 **10~20분**, 실제 Linux Docker/서버 배포 검증 별도. 위 시간은 예상이며 실행하지 않았다.
|
||||
|
||||
**C. 반영 불필요: 별도 변경 0건.** 모든 신규 파일은 같은 배포 목적이다. 현재 로컬 OrgChart 기능 확인을 위해 이 묶음을 적용할 필요는 없다.
|
||||
|
||||
**D. 현재 미커밋/승인 디자인을 덮어쓰는 파일 충돌: 0건.** Docker/Actions를 적용하거나 실제 merge하여 검증하지 않았으므로 통합·배포 위험이 없는 것으로 단정하지 않는다. main 변경을 실제 반영하려면 별도 승인이 필요하다.
|
||||
|
||||
## 3. P9.5 최종 완료 여부 — 기능 PASS / 디자인 승인 NOT VERIFIED
|
||||
|
||||
**대표자 화살촉:** 마지막 `arrowhead-touch/after.json` 25/25 PASS를 재사용했다. 원본 상향 Chevron 모양, 카드 하단 중앙, 카드와 간격 약 0px(최대 0.0001 디자인px), 확대/축소/세로 이동 보존. 대표자 화살촉 1개는 협업 화살표 5개와 별개다. 이전 8.7px 간격 기록은 당시 결과로 보존하며 최종값으로 사용하지 않는다.
|
||||
|
||||
**협업 화살표:** PNG 원본 4종을 사용하는 5개 SVG image를 유지한다. Full의 69/69 배치 검사 및 화살표 픽셀/원본 hash 검증을 재사용했고 현재 Full 캡처에서도 5개가 표시된다. 새 화살표나 카드/연결선 디자인 변경 없음.
|
||||
|
||||
**파견·휴직:** 기존보다 12 디자인px 위, bottom anchor 82. 관계사 바와 기준 간격 14.609375 디자인px. 축소 화면의 실제 간격은 배율에 따라 줄어든다. 제목·직원 카드 스타일 변경 없음.
|
||||
|
||||
**Full 적응형 배치:** [최종 Full 보고서](implementation-report-p9-5-full-composition.md)의 1920×1080, 1920×1440, 1440×900, 1280×1024, 390×844 측정을 재사용했다. 상단/하단은 콘텐츠 내부 앵커이고 중앙은 확보된 여유 높이에 배치한다. 동일 mount에서 높이 +360px이면 하단 +360px, 중앙 +180px, 상단 유지. 카드 실제 교차·표시 잘림·실사용 스크롤 0, 가로 중심 최대 오차 약 0.000061px. 높이가 부족한 1440×900은 기존 비례 축소/상대 배치를 유지하므로 세로 중심 오차 −24.756px를 숨기지 않는다. transform 전 raw scroll 치수와 실제 표시 overflow를 구분했다.
|
||||
|
||||
**Core X 버튼:** [X 버튼 보고서](implementation-report-p9-5-core-dialog-close.md) 52 PASS와 [실제 소비 경로](verification-p9-5-popup-parity.md) 118 PASS를 재사용했다. 버튼 28×28, 아이콘 16×16, stroke 2, padding 0, CSS top/right 8px, HM 색/Hover/Focus 일치. Core 외부 클릭 유지, React HM Dialog 외부 클릭 닫힘 유지. 상세 버튼 검사는 반복하지 않았다.
|
||||
|
||||
## 4. Fixed / Expanded / Storybook — PASS (확인 범위)
|
||||
|
||||
- **Fixed:** 현재 React Full Composition 로딩·화면 확인 PASS. P9.5 Full 배치·화살촉·Core 팝업 근거를 재사용. 원본 SVG·회사색·카드 크기·집계 유지. Fixed는 기존 `createOrgChartContent` 경로를 사용한다.
|
||||
- **Expanded:** P9.4 회사별 20/100/300명, 회사 dropdown·scope UI 제거·기본 Drag & Drop OFF·OFF 이동 차단/독립 Pan·ON 직원/그룹 이동·소속/집계·접기·컬러·TB/LR/Hybrid 검증을 재사용. P9.5 중앙/Fit 124/124, 변환/실제 pointer 62/62 PASS가 최근 viewport 변경을 검증한다. 자동 배치는 현재 100%를 유지하며 명시적 Fit이 전체 bounds에 맞춰 축소한다. 최초 100%에서 모든 회사가 동시에 보인다고 주장하지 않는다.
|
||||
- **Overview:** 현재 캡처에서 회사 로고·조직/직위 표·관계사 인원 표시 확인. 승인된 요약 스타일은 보존. P9 집계/반응형 결과 재사용.
|
||||
- **Department Colors:** 현재 캡처에서 대표자와 배지 없음, `홍 길 동` 표시 13개, 6개 조직 컬러·146×68 그룹장/108×52 직원 카드 및 Center preview 외곽을 확인. 장문 설명·불필요 안내 없음. 기존 7/7 이름·자산 보존 결과 재사용.
|
||||
- **Full Composition:** 현재 1440px 캡처와 5개 지정 화면의 기존 측정 근거를 연결. 중앙/상하단·5개 협업 화살표·붙은 대표자 화살촉·하단 현황·Core 팝업 정상.
|
||||
- **Expanded OrgChart:** 현재 7회사×20명=140명, 회사 dropdown, 직원 수20/100/300, Drag & Drop OFF, HM Dialog 선택·TB/LR/Hybrid 설정·기존 탐색 도구 확인. 상세 편집/데이터 suite는 반복하지 않음.
|
||||
- **메뉴:** 실제 manager DOM에서 Components → Patterns → 최상위 OrgChart, 공개 페이지 Overview → Department Colors → Full Composition → Expanded OrgChart 확인. 내부 23개 Story는 기존 tags 기본 제외 정책을 유지하며 직접 URL/정적 index에는 존재할 수 있다.
|
||||
- **공통 Container:** 네 페이지 `.sb-page`, `.sb-page-inner`의 computed background/border/radius 동일. [frame.json](../../test-results/org-chart-p10/frame.json). 공통 페이지 프레임과 조직도 내부 preview를 구분한다. Fixed Workdesk의 기존 `#F5F8FB` 면과 Department/Expanded Center의 `rgba(217,217,217,.48)`·border·8px radius는 승인된 서로 다른 specimen 디자인이며 일괄 변경하지 않았다. Department/Expanded의 Center surface 동등성은 P9.4 7/7 증거를 재사용.
|
||||
|
||||
## 5. Core / React / Vanilla / 데이터 — PASS (확인 범위)
|
||||
|
||||
Core manifest의 직접 의존성은 dabeng `orgchart@6.0.0`이며 Core source에 React/HM UI import가 없다. React Wrapper는 공개 Core `createOrgChart`를 소비하고 HM Dialog는 adapter의 lazy module이다. Core에 새 React 의존성을 추가하지 않았다. 전체 transitive package graph 인증은 NOT VERIFIED다.
|
||||
|
||||
Vanilla 실제 3개 예제의 최신 팝업/실행 경로는 P9.5 118 PASS로 확인돼 있다. 현재 Core JS/CSS와 Vanilla dist JS/CSS의 SHA도 동일하므로 빌드/상세 버튼 검사를 재실행하지 않았다.
|
||||
|
||||
이번 P10에서 **독립 production React 예제**를 기존 `npm.cmd run build:org-chart-react-example`로 빌드했다. public package entry·StrictMode의 index/Full readOnly를 1440×900·390×900에서 실행하여 렌더링·lifecycle 오류 없음·Core popup open·ESC close·카드 focus 복귀 **12 PASS**를 추가 확보했다. Storybook의 HM Dialog 및 Core 선택 경로는 기존 118 PASS로 연결했다. 예제에 신규 화면/기능은 추가하지 않았다.
|
||||
|
||||
데이터 1회사×20=20, 2회사×100=200, 7회사×300=2,100과 회사 Root 집계·고유 Employee ID는 P9.4 78/78 증거를 재사용했다. 이동 후 회사색·소속·집계는 P9.3/P9.4 Core/React 이동과 P9.5 실제 pointer 결과를 재사용했다. P7~P9.4 성능 suite는 반복하지 않았다. 2,100명 mount/Fit 관측은 이전 P7 기록이며 P9.5에서 전체 Fit/중앙 정렬도 확인했다. 최신 대용량 전체 interaction latency, 장시간 반복·누수·대용량 Drag 성능은 NOT VERIFIED다.
|
||||
|
||||
## 6. AI 생성 정책 — 문서 PASS / 범용 생성 실행 NOT VERIFIED
|
||||
|
||||
현재 `AGENTS.md`와 `CLAUDE.md`가 실제 `docs/org-chart/ai-page-generation-rules.md`를 참조하며 관련 문서 링크가 존재한다.
|
||||
|
||||
10개 문서 조건 모두 확인했다:
|
||||
|
||||
1. Codex의 AGENTS 참조.
|
||||
2. Claude의 CLAUDE 참조.
|
||||
3. Fixed/Auto 동일 최소 화면 생성 원칙.
|
||||
4. 회사별 서로 다른 계층·분기·Depth 지원 요구.
|
||||
5. 특정 회사/조직명·Depth·팀 수·인원 템플릿을 기본값으로 고정하지 않는 규칙.
|
||||
6. 기존 Core/React public API·Validator 재사용 및 지원 한계 보고.
|
||||
7. 별도 요청이 없으면 조직도·기존 탐색 기능만 표시.
|
||||
8. 설명/안내/테스트/JSON/AI 입력/추가 통계/Navigation 기본 제외.
|
||||
9. JSON/CSV/Excel 원본·필드 대응·ID·관계·인원 보존.
|
||||
10. 기존 Full/Expanded/P9.5/P-AI1 비소급·변경 금지.
|
||||
|
||||
경로·참조·정책 내용 PASS다. 이 정책이 임의의 모든 구조를 현재 Core가 렌더링할 수 있음을 뜻하지 않는다. P10에서는 데이터 생성/CSV·XLSX 변환/새 AI 페이지 실험을 실행하지 않았으므로 **실제 범용 생성 테스트 NOT VERIFIED**. Playground 산출물을 이관하지 않았다.
|
||||
|
||||
## 7. Build / 품질 / Guard
|
||||
|
||||
**PASS — 새로 실행:** 전체 root `node node_modules/typescript/bin/tsc -b`, 독립 React example production build, `git diff --check`, 현재 compiled Storybook 4개 페이지/manager 로딩, 독립 React 12개 검사, 공통 frame 비교. [root-typecheck.log](../../test-results/org-chart-p10/root-typecheck.log), [react-example-build.log](../../test-results/org-chart-p10/react-example-build.log).
|
||||
|
||||
**PASS — 유효한 기존 결과 재사용:** Core 최신 Typecheck/Build·Storybook 최종 Build(P9.5 Core X), React 패키지 Typecheck/Build(P9.4 완료 기록; 그 뒤 Wrapper 소스 변경 없음), Vanilla prepare·최신 dist 동등성(P9.5 popup-parity). 로그와 실행 보고서를 확인했다. Core/Storybook 정상 build를 이유 없이 재실행하지 않았다. 독립 React build의 기존 500kB chunk 경고는 남으며 오류는 아니다.
|
||||
|
||||
현재 브라우저 확인 결과는 [browser-final.json](../../test-results/org-chart-p10/browser-final.json)의 관련 **19 PASS / FAIL0**, 신규 console/pageerror0이다. 공통 frame 1 PASS는 별도 실행으로 유지한다. 상세 기존 suite와 합산하지 않는다. Chromium `153.0.8010.12`, Windows headless 한 환경이다.
|
||||
|
||||
### Guard: 기존 FAIL / 신규 회귀 / 미검증 구분
|
||||
|
||||
누적 Story/직접 영향 Core·CSS 15개 경로의 STORYBOOK/PARTIAL Guard를 1회 실행했다. [원본 guard.json](../../test-results/org-chart-p10/guard.json), [비교 guard-comparison.json](../../test-results/org-chart-p10/guard-comparison.json).
|
||||
|
||||
- 원본 `status FAIL / staticStatus FAIL / runtimeStatus NOT VERIFIED`.
|
||||
- **기존 FAIL10:** P9.4와 code/file/line/message가 모두 동일한 TOKEN-00110건. department-colors 5, expanded-content 2, style-presets 3. 사유는 connected consumer/public Theme CSS에서 런타임 토큰 선언을 입증하지 못하는 진단이다. 원본 FAIL을 통과로 덮어쓰지 않았다.
|
||||
- **신규 FAIL0:** P10 구현 변경이 없고 이번 검사에도 새 FAIL 없음. 이는 선택 scope에 대한 결론이며 repository 전체 검사 PASS를 뜻하지 않는다.
|
||||
- **NOT VERIFIED19:** SCOPE-0018(선택된 package 구현/CSS 및 `?url` Department CSS가 proven graph 밖), 동적 TOKEN-00210, 기존 Radix available-height TOKEN-0011. 이전 NOT VERIFIED 수와의 차이는 누적 선택 범위 차이이며 신규 기능 실패 수가 아니다.
|
||||
- `styleEvidence`: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**. 결합 `styleVerification`은 생성하지 않았으며 NOT VERIFIED. 제한된 computed 비교·기능 PASS를 전체 CSS 디자인 승인으로 확대하지 않는다.
|
||||
- **사용자 승인된 디자인 예외:** 삼안·장헌산업 흰 그룹 제목/기존 배경색 유지. 일반 텍스트 대비 미해결 진단을 보존하며 접근성 PASS로 처리하지 않는다. 이 대비 이슈는 위 Guard TOKEN FAIL10과 별개다. 배경색·회사색을 변경하지 않았다.
|
||||
|
||||
### 테스트 환경·검사 코드 진단
|
||||
|
||||
샌드박스 Fetch의 공유 Git 메타데이터 write 거부, 로컬 HTTP `EACCES`, sandbox DNS 차단은 허용된 동일 작업 실행으로 해소했다. 초기 로그를 보존하며 제품 회귀로 분류하지 않는다. CIM process 접근은 제한됐으므로 OS 전체 개발 프로세스를 완전히 확인했다고 주장하지 않는다; 앱 세션 상태, 실제 HTTP·브라우저 응답, authored 해시 보존으로 관측 범위를 확인했다.
|
||||
|
||||
보조 browser probe의 첫 readiness selector가 Overview의 다른 summary 구조를 포함하지 않아 timeout이었다. 수정 후 로딩은 성공했다. 정적 index의 internal `OrgChart/Content`까지 공개 메뉴로 집계한 1FAIL도 있었다. 같은 실행에서 캡처한 manager에는 정확히 공개4개만 표시됐다. [raw browser.json](../../test-results/org-chart-p10/browser.json) 26PASS/1FAIL을 보존하고, 그 manager DOM으로 메뉴 assertion만 정정하며 내부8개 로딩 검사를 보고 범위에서 제외한 **19PASS/0FAIL** 파생 결과를 작성했다. 성공한 브라우저/React 구간을 재실행하지 않았다. 재실행용 probe는 internal/verification tags를 제외하도록 바로잡았다. 제품 소스 수정 없음.
|
||||
|
||||
### 남은 NOT VERIFIED
|
||||
|
||||
최종 사용자 디자인 승인, 전체 package/CSS graph, 결합 CSS 디자인 의도·최종 상태, 다른 브라우저/OS/Theme, 실제 스크린리더·터치 기기, 모든 props/Depth/Hybrid 옵션 조합, 장시간 2,100명 성능·대용량 Drag, Classic 패키징, callback/팝업 none의 전 조합, SSR/hydration·서버 저장·운영 배포, main 통합 후 Linux Docker/npm ci/Actions 실행은 NOT VERIFIED다. Compact는 기존 NOT SUPPORTED이며 새 기능을 추가하지 않았다.
|
||||
|
||||
## 8. 영역별 최종 판정
|
||||
|
||||
- **main 비교: PASS.** Fetch/live SHA·양방향 커밋/파일·미커밋 겹침 확인. 적용은 미진행.
|
||||
- **Fixed: PASS.** P9.5 배치·화살표·팝업 근거 재사용 및 현재 화면 확인.
|
||||
- **Expanded: PASS.** 데이터/편집 기본값·TB/LR/Hybrid·탐색 기존 증거, 최신 중앙/실제 pointer 근거 연결.
|
||||
- **Storybook: PASS.** 공개4페이지·실제 메뉴 순서·공통 frame 확인.
|
||||
- **Vanilla: PASS.** 최신 Core 사용·동작 기존 증거·dist 동일 hash.
|
||||
- **React: PASS.** Wrapper·HM Dialog 기존 증거와 독립 production consumer 추가 실행.
|
||||
- **AI 정책: 문서 PASS / 범용 생성 런타임 NOT VERIFIED.**
|
||||
- **데이터: PASS.** 회사당20/100/300, 20/200/2,100·중복 및 이동 집계 기존 증거. 성능 전체는 NOT VERIFIED.
|
||||
- **Build: PASS.** 정상 패키지/Storybook/Vanilla 결과 재사용, root 타입/독립 React build 추가.
|
||||
- **Guard: FAIL.** 기존10·신규0·NOT VERIFIED19; 대비 예외는 미해결로 보존.
|
||||
- **Git 보존: PASS.** 기존 파일841개 byte 동일, HEAD/branch/staged 유지. 신규 보고서만 authored 추가.
|
||||
|
||||
## 9. 수정 파일 / Git 준비 상태
|
||||
|
||||
P10에서 기존 authored 파일을 수정하지 않았다. **새 authored 파일은 이 `implementation-report-p10.md` 1개**다. `test-results/org-chart-p10/*`는 비교 JSON·검증 probe·로그·캡처 생성물이며, `examples/org-chart-react/dist`와 root tsbuildinfo는 빌드 생성물이다. 생성물을 자동 Stage하지 않는다.
|
||||
|
||||
시작 상태는 tracked 수정33개·untracked authored46개였다. P10 보고서 추가 후 tracked33·untracked47개이며, staged0이다. P7 이후 구현/검증 스크립트·보고서·AI 정책은 기존 미커밋 작업으로 보존했다. 파일별 목록은 [commit-inventory.json](../../test-results/org-chart-p10/commit-inventory.json), byte 보존은 [final-state.json](../../test-results/org-chart-p10/final-state.json)에 기록한다.
|
||||
|
||||
**Feature Commit·Push:** 확인한 기능과 빌드 기준으로 검토 준비는 되어 있으나, 이번에는 승인되지 않았고 실행하지 않았다. 전체 품질 gate PASS는 아니다. Guard 잔여·디자인/접근성 예외의 수용 여부를 정리한 뒤 명시 승인을 받아야 한다.
|
||||
|
||||
승인 후 권장 commit 구분:
|
||||
|
||||
1. P7~P9.5 공유 Core/React·데이터 helper·이동/레이아웃 정책·React optional HM peer 및 lockfile·관련 API/사용 문서. 새 `expanded-editing`, `auto-layout-policy`, `examples`, `moves`, HMEmployeeDialog 등을 누락하지 않는다.
|
||||
2. Storybook 메뉴/Overview/Department/Full/Expanded composition·helper·preview CSS. 1번의 공개 계약을 전제로 묶는다.
|
||||
3. authored 검증 스크립트·P7~P10 보고서. 실행 probe/실패 로그/캡처/dist 등 generated 출력은 제외한다. 기존 examples의 authored `verify-p*.mjs`는 cache로 오인해 제외하지 않는다.
|
||||
4. AI 정책 문서와 AGENTS/CLAUDE 참조 변경을 별도 문서 commit으로 검토.
|
||||
|
||||
이는 인벤토리 기반 제안이다. pathname만으로 stage하거나 `git add -A`하지 않고 정확한 patch와 의존성을 검토한다. `package-lock.json` 변경은 Wrapper의 optional HM UI peer 변경과 함께 검토하며 기존 unrelated 변경 포함 여부도 staged diff에서 확인한다.
|
||||
|
||||
**main 병합 전 추가 조치:** main 배포6파일 통합 승인 → 기존 미커밋 patch의 검토/보존과 commit 경계 확정 → 승인된 feature 정리 → 통합 상태에서 필요한 빌드/회귀·Linux Docker 검증 → 실제 push 직전 live main 재확인. main push가 Gitea 배포를 trigger할 수 있으므로 운영 변수/권한과 배포 결과도 별도 확인한다. 이번 비교에서는 파일 충돌 후보0이지만 실제 merge/rebase/cherry-pick·main checkout·pull을 수행하지 않았다.
|
||||
|
||||
사용자 검토 필요 항목은 **배포 변경 적용 여부, 최종 디자인 승인, Guard/대비 잔여 수용·추가 대응 범위, Feature Stage/Commit/Push 허용 여부**다. 승인된 SVG·회사색 변경이나 새 AI 실험은 이번 후속 제안에 포함하지 않는다.
|
||||
|
||||
## 10. 주요 화면과 원본 증거
|
||||
|
||||
이번 현재 빌드 캡처:
|
||||
|
||||
- [Overview1440](../../test-results/org-chart-p10/orgchart--logo-personnel-summary-1440.png)
|
||||
- [Department1440](../../test-results/org-chart-p10/orgchart--department-colors-1440.png)
|
||||
- [Full1440](../../test-results/org-chart-p10/orgchart--full-composition-1440.png)
|
||||
- [Expanded1440](../../test-results/org-chart-p10/orgchart--expanded-org-chart-1440.png)
|
||||
- [독립 React Full1440 팝업](../../test-results/org-chart-p10/react-full-1440.png), [390 팝업](../../test-results/org-chart-p10/react-full-390.png)
|
||||
|
||||
재사용/실제 시각 확인:
|
||||
|
||||
- [Full1920×1080](../../test-results/org-chart-p9-5-full/after-1920x1080.png), [1920×1440](../../test-results/org-chart-p9-5-full/after-1920x1440.png), [1440×900](../../test-results/org-chart-p9-5-full/after-1440x900.png), [1280×1024](../../test-results/org-chart-p9-5-full/after-1280x1024.png), [390×844](../../test-results/org-chart-p9-5-full/after-390x844.png). 이 5개 캡처는 화살촉 후속 붙임 전 기록이며 최종 화살촉 근거와 구분한다.
|
||||
- [최종 붙은 화살촉 상세](../../test-results/org-chart-p9-5-arrowhead-touch/after-1920x1080-detail.png), [25/25 측정](../../test-results/org-chart-p9-5-arrowhead-touch/after.json).
|
||||
- [Expanded 중앙/Fit124](../../test-results/org-chart-p9-5/after.json), [실제 pointer/geometry62](../../test-results/org-chart-p9-5/geometry.json).
|
||||
- [P9.4 데이터/편집78](../../test-results/org-chart-p9-4/browser.json), [Vanilla API21](../../test-results/org-chart-p9-4/core-api-cancel-default-fixed.json), [소비 화면 팝업118](../../test-results/org-chart-p9-5-popup-parity/result.json).
|
||||
|
||||
검토 중인 실제 정적 Storybook은 `http://127.0.0.1:6064/`이며, 기존 서버·포트를 재시작하거나 교체하지 않았다. 별도 React probe 서버는 임시 loopback port를 사용하고 종료했다.
|
||||
|
||||
## 11. 시간
|
||||
|
||||
- Started At: 2026-10-13 14:15:23 KST
|
||||
- Estimated Duration: 최초 약35분 (main8분·증거/정책7분·선별 검증15분·보고5분).
|
||||
- Completed At: 2026-10-13 14:32:33 KST
|
||||
- Actual Duration: 17분10초
|
||||
- Variance: 예상보다17분50초 짧음. 최신 증거의 해시 일치를 확인하여 기존 정상 suite/build를 재사용했다.
|
||||
|
||||
제출 직전 live main/Feature SHA는 위 기준과 동일했다. 보고서 링크33개 모두 실제 대상이 존재한다. 기존841개 파일 byte 동일·branch/HEAD/staged 유지, authored 추가는 P10 보고서1개이며 최종 Git 감사에 기록했다.
|
||||
@@ -0,0 +1,139 @@
|
||||
# OrgChart P11 — 외부 CSS 테마 및 Guard 결과
|
||||
|
||||
RESULT: **PASS** (아래 명시된 구현·검증 범위)
|
||||
|
||||
| 구분 | 결과 |
|
||||
|---|---|
|
||||
| 외부 CSS 테마 실제 적용 | **PASS** — React / Vanilla / 순수 HTTP Core 소비자 |
|
||||
| 기존 TOKEN-001 5건 Guard 실제 재검사 | **PASS** — 원본 `status PASS`, `staticStatus PASS`, FAIL **5 → 0**, issues 0 |
|
||||
| 다른 공용 Guard 검사 결과 보존 | **PASS** — UI/CENTER/Foundation/Pattern의 기존 67개 이슈 동일, FAIL 0 유지 |
|
||||
| 기본 디자인 보존 | **PASS** — 6개 기본 화면의 변경 전후 PNG SHA-256 동일 |
|
||||
|
||||
Guard의 자체 `runtimeStatus NOT VERIFIED`는 그대로 보존했다. 정적 Guard PASS를 브라우저 결과로 덮어쓰지 않았다. 외부 테마 브라우저 검증과 원본 Guard 판정은 각각 보고한다. 전체 제품·전체 접근성·모든 브라우저의 PASS를 뜻하지 않는다.
|
||||
|
||||
## 작업 기준 및 Git
|
||||
|
||||
- 폴더: `D:\디자인시스템\HMDesign-OrgChart-Recovered`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD: `dcfa4e0895eecc97ce516cbf3d4424bbaadfddeb`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- Git 작성자 설정: `b25027 <shkim4@samaneng.com>`
|
||||
- 시작 Working Tree: clean. 완료 Working Tree: 아래 P11 변경만 unstaged. Staged 파일 0.
|
||||
- 이번 첨부 요청의 경로·Feature 브랜치·Commit/Push 보류 지시를 적용했다. Stage·Commit·Push·Merge·운영 서버·Docker·Registry Publish 미진행.
|
||||
- 범위: 기존 OrgChart Core의 컬러 구현 및 소비 검증. 공개 API·데이터 계약·엔진·회사 프리셋·기존 4개 공개 Story 소스·HM UI Theme를 변경하지 않았다. 신규 업무 화면/Navigation/FilterBar를 추가하는 작업이 아니다.
|
||||
|
||||
## 구현과 CSS 변수
|
||||
|
||||
회사색과 Fixed 6개 부서색의 JS inline 주입을 제거하고 `data-company-id`, `data-group-variant`에 따른 CSS 계산으로 옮겼다. `color-presets.css`에 실제 사용 범위의 `current-card-background`, `current-card-color`, `current-department-color`, `current-border`, `current-gradient`와 compatibility alias 기본 선언을 추가했다. Auto의 `expanded-primary`, `expanded-card-primary`, 기존 secondary alias도 CSS로 계산한다.
|
||||
|
||||
외부 공개 입력은 조상 CSS가 Shadow DOM을 통과해 상속되도록 내부에 고정 기본 선언하지 않는다. 내부 `resolved-*`/`current-*`가 해당 공개 입력과 기존 색상 fallback을 해석한다. 외부 테마를 JS inline 기본색으로 막지 않는다.
|
||||
|
||||
주요 추가/연결 입력은 다음과 같다. 전체 이름·기본 정책·회사/부서 ID 목록은 [사용 가이드](org-chart-usage-guide.md#p11-외부-css-테마-2026-10-13)에 있다. 접두사는 모두 `--hm-orgchart-`다.
|
||||
|
||||
| 영역 | 입력 |
|
||||
|---|---|
|
||||
| 회사 | 기존 `company-{id}` 경로를 공개 입력으로 연결, 7개 기본색 유지 |
|
||||
| 그룹 | `group-title-background`, `group-title-color`, `group-border` |
|
||||
| 카드 | `card-background`, `card-color`, `card-position-color`, `card-border`, `card-accent` |
|
||||
| 역할 | `leader-card-background`, `top-leader-card-background` (Auto) |
|
||||
| 상태/Focus | `card-hover-background`, `card-focus-color` |
|
||||
| 연결선 | 기존 `expanded-line-color`를 상속 입력으로 전환, `fixed-line-color` 추가 |
|
||||
| 파견/휴직 | `status-*`, `dispatched-*`, `on-leave-*`의 제목·카드 gradient/Border/글자 입력 |
|
||||
| Core 팝업 | `dialog-background`, `dialog-color`, `dialog-border`, `dialog-close-color`, `dialog-close-hover-background`, `dialog-focus-color` |
|
||||
| 기존 공통 | `bg-surface`, `bg-content`, `fg-primary`, `stroke-default` 등 재사용 |
|
||||
| Fixed | 기존 `{department}-gradient/border/department-color/card-background/card-color` 입력 유지 |
|
||||
|
||||
상태 요약 카드의 고정 gradient/Border를 변수로 바꾸고 기존 `!important`를 제거했다. 미지정 Hover는 원래 카드/상태 gradient를 유지한다. SVG/PNG 자산을 수정하거나 Cell Border Only를 재적용하지 않았다.
|
||||
|
||||
## 외부 파일 연결 및 실제 결과
|
||||
|
||||
- [external-theme.css](../../examples/org-chart-react/external-theme.css): 기본 공개 CSS를 먼저 import하고 `:host`의 컬러 입력을 뒤에 선언한다. Vite에서는 `?url`로 import해 기존 `stylesheetUrls.core`에 전달한다.
|
||||
- 일반 HTTP Vanilla에서는 기본 `style.css`를 상대/절대 HTTP 경로로 import하는 같은 파일을 사용한다. 검증 fixture는 공개 Core ESM만 로드했으며 React 또는 node_modules 요청 0건이었다.
|
||||
- [host-theme.css](../../examples/org-chart-react/host-theme.css): 호스트 HTML에 연결하는 변수 전용 예제. 호스트 class 적용/제거만으로 회사색·배경이 변경/복원됐다.
|
||||
- [external-group-theme.css](../../examples/org-chart-react/external-group-theme.css): 회사색과 별도로 공통 제목 배경을 바꾸는 CSS 파일. 실제 `rgb(97, 62, 139)` 적용 확인.
|
||||
- Shadow 파일의 `:host` 선언이 조상 상속보다 우선한다. 호스트 selector는 Shadow 내부 카드에 접근하지 못하므로 내부 selector 수정은 `stylesheetUrls.core`에 포함한다. 기본 CSS 전체를 포함하지 않은 변수 전용 파일로 core URL을 대체하면 안 된다.
|
||||
|
||||
| 확인 대상 | 실제 적용 결과 |
|
||||
|---|---|
|
||||
| 한맥 / 삼안 | 제목·소속 카드 강조선이 `#2457a7` / `#385ca8`로 변경 |
|
||||
| 나머지 5개 회사 | 각각 개별 CSS 색상 변경. 7개 승인 기본색도 브라우저 확인 |
|
||||
| 그룹 | 제목 글자 `#fff4d6`, 공통 배경 별도 파일 override 확인 |
|
||||
| 직원·대표자·그룹장 | Auto 배경 `#f4f8ff` / `#d2e3ff` / `#e3eeff`, Border `#527aa9` |
|
||||
| Hover / Focus | 실제 pointer Hover `#c1d8f8`, 키보드 Focus `#7650a5` |
|
||||
| Auto 연결선 | 직원 SVG 경로 및 조직 pseudo 연결선 `#249a79` |
|
||||
| Fixed 연결선 | 실제 overlay path `#297ab0`. 이미지 화살표 내부 색은 그대로 |
|
||||
| 조직도 배경 | Auto canvas `#edf4ff`, Fixed readOnly 내용 `#dce8f8` |
|
||||
| 파견·휴직 | 서로 다른 녹색/보라색 gradient·Border, Fixed 및 Auto 상태 카드 확인 |
|
||||
| Core 팝업 | 배경 `#eef2ff`, 글자 `#283568`, Border `#6879ba`, 실제 닫기 Hover/Focus 확인 |
|
||||
| 런타임 복원 | mount 상태에서 호스트 class 제거 시 기본 색상·크기 즉시 복원 |
|
||||
|
||||
React HM Dialog는 Shadow 밖의 별도 HM Theme/Portal 계약을 계속 따른다. `dialog-*`는 Core 팝업에만 적용된다. `@hm-design/ui/theme` 연계 및 Portal 주의사항을 사용 가이드에 추가했으며 HM Dialog 구현 자체는 바꾸지 않았다.
|
||||
|
||||
## 기본 디자인 및 Fixed 한계
|
||||
|
||||
React/Vanilla 각각 Auto Chart Only, Fixed readOnly Full Composition, Fixed Interactive를 변경 전 Core/Wrapper dist로 캡처했다. 변경 후 빌드에서 모든 카드·제목·캔버스·팝업의 주요 computed 색상/Border/크기를 비교했다. 화면 6개의 PNG SHA-256이 정확히 동일했다([이미지 비교 결과](../../test-results/org-chart-p11/default-image-comparison.json)).
|
||||
|
||||
기존 공개 Story 4개(Full Composition, Expanded OrgChart, Department Colors, Overview)를 실제 브라우저에서 열었다. 4/4 렌더 확인, 오류 0, Department Colors의 서로 다른 기본 gradient 6개 유지. Story 소스·ID·등록은 변경하지 않았다. Story 화면은 현재 렌더 확인이며 변경 전 Story 전체의 픽셀 비교를 수행한 것은 아니다.
|
||||
|
||||
Fixed의 CSS 제목·상태 카드·Focus·배경·실제 SVG overlay path는 변경할 수 있다. `cards/*.svg` 카드 이미지, 회사 로고 PNG, Workdesk `Group.svg`, `bg_sky.png`, `bg_main.png`, `Frame2@3x.png`, `1-8-4x-1@2x.png`, `team-arrow-*.png` 내부 색상은 CSS 변수로 바꿀 수 없다. 대표자/부서 SVG의 불투명 영역은 카드 CSS 배경을 바꿔도 유지된다. 자산 전면 재작성·필터 착색 없음.
|
||||
|
||||
## Guard 5건 해결과 공용 영향
|
||||
|
||||
기존 분석기에서 다음 명령으로 TOKEN-001 5건을 먼저 재현했다. [원본 재현 JSON](../../test-results/org-chart-p11/guard-before.json)은 보존한다.
|
||||
|
||||
```powershell
|
||||
node scripts/ai-generation-guard.mjs --target . --scope PARTIAL --profile STORYBOOK --files packages/org-chart/src/core/department-colors.ts,packages/org-chart/src/core/style-presets.ts,packages/org-chart/src/core/expanded-content.ts --json
|
||||
```
|
||||
|
||||
| 기존 FAIL | 해결 |
|
||||
|---|---|
|
||||
| Department Colors의 `current-gradient`, `current-card-background` (2건) | 실제 `data-group-variant` 범위의 CSS 기본 선언 및 공개 CSS 연결 |
|
||||
| Style presets의 `resolved-bg-surface` (1건) | 기존 공개 design-tokens.css 선언 정상 수집 |
|
||||
| Style presets의 `current-border`, `current-gradient` (2건) | 실제 범위 기본 선언. JS 색상/alias 주입 제거 |
|
||||
|
||||
CSS 선언만으로는 `?url`로 연결된 공개 CSS를 기존 분석기가 볼 수 없어 5건을 해결할 수 없었다. 필요한 분석기 변경은 `guard-theme-analysis.mjs`의 **공개 OrgChart CSS 두 import 형태와 상대 CSS import 선언 수집**으로 한정했다. 임의 CSS URL 전체를 적용 스타일로 인정하지 않고, setProperty 정의 자동 승인·예외 목록·규칙 비활성화를 하지 않았다. 미선언 HM 토큰은 fallback이 있어도 FAIL이다.
|
||||
|
||||
동일 명령의 [최종 JSON](../../test-results/org-chart-p11/guard-final.json): `status PASS`, `staticStatus PASS`, `issues []`, FAIL 0. 원본 CSS phases는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED다. 실제 테마 적용은 별도 브라우저 결과로 확인했다. Guard에 그 Runtime 증거를 결합하여 원본 결과를 변경하지 않았다.
|
||||
|
||||
기존 Guard 94/94 및 신규 공개 CSS 회귀 5/5 PASS. 이전 분석기를 임시 복사해 UI/CENTER/Foundation/Pattern의 실제 현재 소스를 검사한 뒤 수정 분석기와 비교했다. 기존 67개 이슈·status·staticStatus가 동일했다([비교 JSON](../../test-results/org-chart-p11/guard-other-comparison.json)). 해당 다른 범위의 원본 status는 NOT VERIFIED, FAIL 0이며 이를 PASS로 올리지 않았다. 비교용 임시 분석기 파일은 제거했다.
|
||||
|
||||
P11 React 소비 fixture는 `--result p11.html`도 별도 실행했다([소비 Guard](../../test-results/org-chart-p11/guard-consumer.json)). 기존 공용 Guard는 OrgChart를 HM UI 화면 재사용 계약 대상으로 모델링하지 않아 `staticStatus NOT APPLICABLE`, 원본 NOT VERIFIED다. 이를 기존 5건의 PASS와 혼동하지 않는다. 패키지 CSS 구현 자체는 해당 소비 graph 밖의 범위이며 빌드·타입·실제 브라우저로 검증했다.
|
||||
|
||||
## 기술 검증
|
||||
|
||||
| 검사 | 결과 / 증거 |
|
||||
|---|---|
|
||||
| 최종 빌드 React/Vanilla 테마·기본값·동작·4개 폭 | **104/104 PASS**, 콘솔/page 오류 0 — [browser-result.json](../../test-results/org-chart-p11/browser-result.json) |
|
||||
| 순수 HTTP Vanilla Core | **8/8 PASS**, 오류 0. 외부 CSS @import, 팝업, 공개 zoomTo/panBy/fitToView/destroy — [standalone 결과](../../test-results/org-chart-p11/standalone-vanilla-result.json) |
|
||||
| 기존 공개 Story | **4/4 PASS**, 오류 0 — [Storybook 결과](../../test-results/org-chart-p11/storybook-result.json) |
|
||||
| 회사 프리셋·Expanded company/layout 관련 테스트 | **17/17 PASS** (bundled Node 24.19.0 사용) |
|
||||
| 공용 Guard 회귀 | **94/94 PASS** — [로그](../../test-results/org-chart-p11/guard-tests.txt) |
|
||||
| 신규 Guard/CSS 기본값 회귀 | **5/5 PASS** — [로그](../../test-results/org-chart-p11/guard-orgchart-tests.txt) |
|
||||
| Core / React Typecheck | PASS. P11 TSX fixture 별도 Typecheck도 PASS |
|
||||
| Core / React Wrapper / 소비 예제 Build | PASS. 외부 CSS URL이 실제 배포 assets에 산출됨 |
|
||||
| 390/768/1280/1440px | Auto canvas 폭 일치, 문서 가로 overflow 없음. Fixed 문서 overflow 없음 |
|
||||
| 상호작용 | 직접 Wheel, Pan, 접기/펼치기, Enter 팝업, Escape 닫기/Focus 복귀, Hover/Focus, Drag & Drop ON/OFF 속성 확인 |
|
||||
| git diff --check | PASS, Staged 파일 0, HEAD 유지 |
|
||||
|
||||
빌드의 기존 `:host-context(.dark)` minifier 경고와 큰 bundle 경고는 남아 있다. 최초 시스템 Node 20에서 기존 unit runner의 `node:fs globSync` import가 실패해, 해당 runner는 제공된 Node 24로 실행하여 관련 17개를 통과했다. 의존성/패키지 버전 업그레이드 없음.
|
||||
|
||||
NOT VERIFIED: Firefox/Safari/Edge, screen reader 음성, 물리 터치, 실제 Drag & Drop 데이터 이동 전체 회귀, `/hm-dialog` 별도 Theme를 변경한 새 브라우저 비교, 기존 Story 4개 전체의 변경 전 픽셀 비교, 전체 제품 테스트·배포. Fixed 이미지 내부 색상 테마화는 이번 범위에서 지원하지 않는다. 기본 렌더·팝업의 픽셀/computed 비교와 실제 테마 결과의 검사 범위는 위 증거로 한정한다.
|
||||
|
||||
## 변경 파일
|
||||
|
||||
- Core: `packages/org-chart/src/core/style-presets.ts`, `expanded-content.ts`, `department-colors.ts`
|
||||
- CSS: `packages/org-chart/src/styles/color-presets.css`(신규), `org-chart.css`, `employee-card.css`, `employee-info-dialog.css`, `org-chart-content.css`, `org-chart-expanded.css`, `org-chart-readonly.css`, `summary-sections.css`
|
||||
- CSS packaging: `packages/org-chart/build.mjs`
|
||||
- Guard: `scripts/guard-theme-analysis.mjs`, `scripts/guard-orgchart-theme.test.mjs`(신규)
|
||||
- 소비/검증: `examples/org-chart-react/p11.html`, `p11.tsx`, `external-theme.css`, `external-group-theme.css`, `host-theme.css`, `verify-p11.mjs`, `verify-p11-stories.mjs`(모두 신규), `vite.config.ts`
|
||||
- 순수 Vanilla 검증: `examples/org-chart/verify-p11-standalone.mjs`(신규)
|
||||
- 문서: `docs/org-chart/org-chart-usage-guide.md`, `docs/guidelines/ai-generation-guard.md`, 이 보고서
|
||||
- ignored 산출물: Core/Wrapper/소비 dist, `test-results/org-chart-p11/*`. Stage하지 않았다.
|
||||
|
||||
## 시간 (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-13 21:43:34 KST
|
||||
- Completed At: 2026-10-13 22:20:11 KST
|
||||
- Actual Duration: 36분 37초
|
||||
- Estimated Duration: 최초 약 60분 (분석 10 / 구현·가이드 25 / 검증 25)
|
||||
- Variance: 예상보다 23분 23초 단축
|
||||
@@ -0,0 +1,109 @@
|
||||
# OrgChart P2 — Depth별 카드와 Workdesk Full Composition
|
||||
|
||||
RESULT: **PASS — 현재 P1 fixture의 조회 전용 Full Composition 수정 범위**.
|
||||
|
||||
실제 조직 계층에 따라 1Depth 조직장만 대형 카드를 사용하고, 2Depth 조직장은 소형 카드로 표시한다. Workdesk 원본 연결선·협업 화살표·장식을 공통 Core에 적용했다. 기존 Guard 미검증과 이전 검사의 FAIL은 이 결과와 별도로 보존한다. Workdesk 전체 픽셀 일치나 모든 데이터 조합의 지원을 뜻하지 않는다.
|
||||
|
||||
## 작업 범위와 보존
|
||||
|
||||
- 수정 경로: `D:\디자인시스템\HMDesign-OrgChart`, 브랜치 `feature/orgchart-package`.
|
||||
- Workdesk 원본 `D:\디자인시스템\gpd-workdesk\sample\조직도_v1.3`는 읽기 전용으로 참조했다. 운영 main에는 수정하지 않았다.
|
||||
- 기존 미커밋 변경은 시작 파일 해시와 비교해 보존했다. 원본 카드 SVG·회사 로고·공통 Theme·Component·Pattern·Storybook 메뉴·독립 컴포넌트 API를 유지했다.
|
||||
- 직원 41명의 원본 fixture는 변경하지 않았다. 직원 ID·이름·리더 정보·직위·소속·상태·조직 관계·집계 계약도 그대로다.
|
||||
- Stage·Commit·Push·Merge·Reset·Rebase를 수행하지 않았다.
|
||||
|
||||
## 카드·이름·간격
|
||||
|
||||
- 변경 전: 하위 조직장도 모두 대형 `leader` 자산을 사용했다. 조회 전용 외부 margin이 후속 직원 margin 규칙에 덮여 실제 헤더↔카드 간격은 0px이었다.
|
||||
- 변경 후 대표자: 전용 SVG **146×74px** 유지.
|
||||
- 변경 후 1Depth: 주요 조직장 6명 모두 기존 대형 SVG **146×68px** 사용.
|
||||
- 변경 후 2Depth: fixture의 기존 조직장 3명 모두 소형 SVG **108×52px** 사용. 다른 일반 직원도 소형 카드를 유지한다.
|
||||
- 대형 여부는 직책 문자열이나 리더 플래그 단독이 아니라 원본 `parentId` 조상 관계로 계산한다. semantic leader 구분과 직책 뱃지는 보존하며 독립·interactive 화면의 카드 정책은 유지한다.
|
||||
- 최신 사용자 수정 기준으로 1Depth 외부 간격은 **8px**, 2Depth 이하 헤더↔첫 카드 간격은 **10px**다. 화면의 17개 조직 노드에서 Depth별 간격을 확인했다.
|
||||
- Storybook 1920px 측정값을 Stage 좌표로 환산하면 1Depth 헤더 Y **254.1875px**, 높이 **36px**, 대형 카드 상단 **298.1875px**다. 6개 외부 간격은 약 **8.000002px**, 하위 조직 간격은 **9.99998–10.00001px**였다.
|
||||
- Full Composition의 대표자·조직장·직원·파견·휴직을 포함한 렌더 카드 43개의 이름은 모두 **홍 길 동**으로 표시한다. 접근성 이름은 **홍길동**이다. 직위·직책도 요청한 한글 자간 표시를 적용하며 원본 데이터는 변경하지 않는다.
|
||||
- 확정 Typography·카드 크기·프로필 크기와 직위↔이름 내부 gap **0px**를 유지했다. 외부 헤더 간격과 내부 행 간격을 분리했다.
|
||||
|
||||
## Workdesk 원본 디자인 적용
|
||||
|
||||
- 활성 compact `screenD` 곡선 3개, 대표자↔허브 중앙선, 실제 헤더 상단 중앙을 따라가는 사업관리 곡선의 **SVG Path 5개**를 적용했다. 사업관리 곡선은 원본 renderer의 계산식을 사용한다.
|
||||
- Stroke `#45639F`, width `0.893023`, dasharray `1.79 1.79`, fill `none`을 사용한다. 기존 상단 pseudo 연결선은 해당 원본 artwork preset에서 숨겨 중복을 제거했다.
|
||||
- 원본에서 하위 조직 SVG 블록은 비활성 상태이며 현재 사용 가능한 별도 원본 Path가 없다. 하위 셀·팀은 기존 엔진의 실제 계층 경로를 보존하고 원본 점선 스타일을 적용했다. 원본에 없는 관계를 추가하지 않았다.
|
||||
- 협업 화살표는 원본 PNG 4종으로 **5개**를 배치했다. 원본 SVG image 방식과 `preserveAspectRatio="none"`을 유지한다.
|
||||
- `Group.svg`, `bg_sky.png`, `bg_main.png`, `Frame2@3x.png`, `1-8-4x-1@2x.png`와 화살표 4종, 총 **9개 자산**을 복사했다. SHA-256이 Workdesk 원본과 모두 일치한다.
|
||||
- `?inline` import로 Core ESM/IIFE에 자산을 포함해 외부 Workdesk 서버 의존성을 제거했다. HTML·React·직접 열기 classic에서 동일한 자산을 사용한다.
|
||||
- 장식 0, SVG 선·화살표 4, 조직 7, 허브 12, 통계 18, 파견·휴직 32, 대표자 35의 레이어를 구성했다. 전체 artwork는 카드와 같은 Stage 안에서 함께 축소된다.
|
||||
- 우측 조직별 총인원, 좌하단 파견·휴직의 가로 2개 그룹, 하단 관계사 한 줄 현황을 유지한다. 독립 PositionSummary의 상세 교차표 기능은 보존했다.
|
||||
|
||||
## HTML·React·Storybook과 반응형
|
||||
|
||||
- HTML `p1.html?mode=readOnly`, 직접 열기 `p1-classic.html?mode=readOnly`, React `p1.html?mode=readOnly`, 기존 Full Composition Story가 같은 Core DOM/CSS를 사용한다.
|
||||
- React 예제에 HTML과 같은 `mode` query 처리를 연결했다. 별도 React 노드 UI를 만들지 않았다.
|
||||
- 기준 Stage **1920×914.5px**와 기존 폭·높이 기반 단일 비례축소를 유지한다. 작은 화면에서 카드 종류를 바꾸거나 조직을 재배열하지 않는다.
|
||||
- 1920×1080: scale **0.983333**.
|
||||
- 1440×900: scale **0.733333**.
|
||||
- 1280×800: scale **0.650000**.
|
||||
- 768×1024: scale **0.383333**.
|
||||
- 390×844: scale **0.186458**.
|
||||
- 네 진입 경로의 조직·카드·간격·통계·자산·축소 측정값이 일치했다. Storybook의 베이지 호스트 배경과 HTML의 흰 배경 때문에 바깥 배경 톤은 다르다.
|
||||
- 390px에서도 구조 전체가 함께 축소되지만 글자는 매우 작다. 모바일에서 원본 크기 가독성을 제공한다는 의미는 아니다.
|
||||
|
||||
## 검증과 근거
|
||||
|
||||
- OrgChart Core·React Wrapper Typecheck: **PASS**.
|
||||
- Core ESM/IIFE·타입·CSS 및 Wrapper Build: **PASS**.
|
||||
- React 소비 예제 production Build: **PASS**. 약 2.53MB chunk 경고는 원본 이미지 inline 포함에 따른 것으로 남겨 두었다.
|
||||
- 현재 Storybook dev **6041**의 Full Composition: **PASS**. 전체 Storybook production Build는 이번에 재실행하지 않았다.
|
||||
- Chrome `155.0.8059.39`, 네 진입 경로·다섯 뷰포트의 P2 브라우저 검사: **484/484 PASS**, 새 console/runtime 오류 **0**.
|
||||
- 대표자·Depth별 카드 크기, 실제 조상 Depth, 17개 조직의 외부 간격, 이름·직책 뱃지·원본 fixture 보존, Stage geometry·overflow, 카드·헤더·화살표·통계의 사각형 겹침을 확인했다.
|
||||
- Tooltip 검증은 실제 표시된 홍 길 동 이름 1개의 폭을 일시적으로 **8px**로 제한해 overflow를 유도했다. 실제 hover·tooltip hover·키보드 Tab·Escape·focus 유지·숨김 scroll extent·복구를 확인하고 폭을 원복했다. fixture나 표시 이름을 검증용 긴 이름으로 바꾸지 않았다.
|
||||
- 1920/1440/390px HTML·Storybook PNG를 육안 확인했다. 실제 텍스트 glyph 충돌과 화살표의 카드 침범은 발견하지 않았다. 확정 내부 gap 0에서 Noto 글꼴의 DOM Range ascent/descent envelope가 교차할 수 있으므로 해당 원시 측정값은 실제 glyph 충돌과 구분해 보존했다.
|
||||
- 곡선 Path의 bounding box만으로 실제 곡선 겹침을 판정하지 않았다. 라우팅·레이어는 저장된 전체 화면 캡처로 확인했고 자동 곡선 충돌 검증은 **NOT VERIFIED**다.
|
||||
|
||||
증거 파일:
|
||||
|
||||
- [P2 browser 결과·측정값](../../test-results/org-chart-p2/browser.json).
|
||||
- [기존 소스 변경 범위](../../test-results/org-chart-p2/source-changes.json), [시작 해시](../../test-results/org-chart-p2/source-baseline.json), [원본 자산 SHA 비교](../../test-results/org-chart-p2/asset-preservation.json).
|
||||
- [Storybook 1920](../../test-results/org-chart-p2/storybook-1920x1080.png), [1440](../../test-results/org-chart-p2/storybook-1440x900.png), [390](../../test-results/org-chart-p2/storybook-390x844.png).
|
||||
- [HTML 1920](../../test-results/org-chart-p2/html-1920x1080.png), [1440](../../test-results/org-chart-p2/html-1440x900.png), [390](../../test-results/org-chart-p2/html-390x844.png).
|
||||
- [Storybook Full Composition](http://127.0.0.1:6041/?path=/story/orgchart--full-composition), [독립 iframe](http://127.0.0.1:6041/iframe.html?id=orgchart--full-composition&viewMode=story).
|
||||
|
||||
## Guard·기존 FAIL·미검증
|
||||
|
||||
- React 소비 예제에 PARTIAL Guard를 실행했다. 원본 `staticStatus=NOT APPLICABLE`, `status=NOT VERIFIED`, `RENDER-002`를 보존한다.
|
||||
- `styleEvidence`: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **PASS**, FINAL **NOT VERIFIED**. Guard CSS checks는 `[]`이며 Core Shadow CSS를 검사 범위에 포함하지 못했다. 이를 Core 스타일 전체 정적 PASS로 확대하지 않는다.
|
||||
- [Guard 원본](../../test-results/org-chart-p2/guard-react.json)과 실제 P2 브라우저 증거는 별도 파일로 보존하며 Runtime PASS로 원본 status를 덮어쓰지 않았다.
|
||||
- 기존 `verify-p1.mjs` [baseline](../../test-results/org-chart-full-composition/verify-p1-baseline.json)·[after](../../test-results/org-chart-full-composition/verify-p1-after.json)의 **197건 중 145 PASS / 동일 52 FAIL**을 보존하고 재실행하지 않았다. 이전 fixture·root·교차표·내부 스크롤·Typography 기대값 관련 실패와 P2 검사를 구분한다.
|
||||
- 사용자가 언급한 v0 `verify.mjs` FAIL **9건**은 이번에 독립 확인하지 못했다. 현재 저장된 [v0 report](../../test-results/org-chart-browser/report.json)는 **6건 중 2 FAIL**로, 이 기록만으로 9건을 확정하지 않는다. 검사 삭제나 무조건적인 기대값 PASS 변경은 없다.
|
||||
- **NOT VERIFIED**: 실제 운영 Workdesk 최신 화면과의 완전한 픽셀 일치, 전체 스크린리더, 자동 곡선/글꼴 ink 충돌 판정, 모든 미매핑 조직·혼합 root 데이터 조합, 전체 Storybook production Build.
|
||||
|
||||
## 변경 파일
|
||||
|
||||
기존 소스 6개:
|
||||
|
||||
- [render.ts](../../packages/org-chart/src/core/render.ts): 실제 조상 Depth에 따른 readOnly 카드 자산·헤더 역할.
|
||||
- [employee-card.ts](../../packages/org-chart/src/core/employee-card.ts): readOnly 표시 이름·한글 간격·접근성 이름.
|
||||
- [status-summary.ts](../../packages/org-chart/src/core/status-summary.ts): 상태 카드에도 동일 표시 옵션 전달.
|
||||
- [org-chart-content.ts](../../packages/org-chart/src/core/org-chart-content.ts): 대표자 표시와 공통 artwork 조합.
|
||||
- [org-chart-readonly.css](../../packages/org-chart/src/styles/org-chart-readonly.css): 헤더·카드 간격, 원본 장식·레이어·계층선, 파견/휴직 가로 그룹.
|
||||
- [React p1.tsx](../../examples/org-chart-react/p1.tsx): HTML과 같은 readOnly query 진입.
|
||||
|
||||
추가 소스·자산·문서:
|
||||
|
||||
- [workdesk-artwork.ts](../../packages/org-chart/src/core/workdesk-artwork.ts): 원본 Path·5개 image 화살표·장식·동적 사업관리 연결선.
|
||||
- [Workdesk 자산 README](../../packages/org-chart/src/assets/workdesk/README.md) 및 해당 디렉터리의 원본 자산 9개.
|
||||
- [verify-p2.mjs](../../examples/org-chart/verify-p2.mjs): 기존 검사와 분리된 P2 브라우저 검증.
|
||||
- 이 보고서와 현재 계약을 반영한 [UI Composition](ui-composition.md), [카드 디자인 2단계](card-design-stage2.md).
|
||||
- [React 예제 README](../../examples/org-chart-react/README.md): 조회 전용 query와 P2 검증 실행 안내.
|
||||
|
||||
패키지·HTML/React build 산출물과 `test-results/org-chart-p2/` 증거를 생성했다. 이전 보고서·검증 기록을 보존했으며 Commit·Push·Merge는 미진행이다.
|
||||
|
||||
별도 [런타임 증거](../../test-results/org-chart-p2/runtime-evidence.json)에 원본 Guard 해시와 브라우저·시각 검토 상태를 연결했다. 현재 source/contract fingerprint가 원본 Guard와 일치함을 확인했다. 원본 status는 변경하지 않았다. 카드 41개의 직위/뱃지 행 하단과 이름 행 상단 간격도 실측 0px(부동소수점 오차 내)이다.
|
||||
|
||||
## 시간 기록
|
||||
|
||||
- Started At: 2026-10-12 14:52:51 KST
|
||||
- Completed At: 2026-10-12 15:15:24 KST
|
||||
- Actual Duration: 22분 33초
|
||||
- Estimated Duration: 약 45~60분
|
||||
- Variance: 예상보다 22분 27초~37분 27초 단축
|
||||
@@ -0,0 +1,43 @@
|
||||
# HMDesign OrgChart P3.1 — 잔여 디자인 정밀 보완
|
||||
|
||||
RESULT: **PASS — 요청한 P3.1 변경 범위**.
|
||||
|
||||
집중 브라우저 검사 **486/486 PASS**, 신규 콘솔·런타임 오류 **0건**. 기존 [P3의 732/732 기록](implementation-report-p3.md)은 역사 근거로 보존했고 전체 suite를 재실행하지 않았다. Guard 원본 및 결합 전체 상태는 **NOT VERIFIED**로 유지한다.
|
||||
|
||||
1. **Noto Sans KR**: OrgChart 전용 기본 token에 적용했다. 기존 `public/fonts/NotoSansKR-variable.ttf`를 HTML의 기존 복사 경로와 React bundler 경로에서 재사용하고 Storybook의 기존 HM font-face를 사용한다. HM Theme는 변경하지 않았다. 카드·직위·타이틀·허브·표·관계사·파견/휴직·팝업의 computed family를 확인했으며 네 실행 경로의 대표 9개 텍스트에서 Chrome CDP로 실제 custom-font 글리프 사용을 확인했다. 파일 내부 family 이름은 `Noto Sans KR Thin`, PostScript 이름은 `NotoSansKR`와 가중치별 suffix다. 이는 fallback 글꼴이 아니다. 한국어 Core shell의 `lang="ko"`로 `en` Storybook host와 `ko` HTML의 가운데점 글리프 폭 차이도 제거했다.
|
||||
2. **1Depth·2Depth**: 1Depth를 20px/700/36px·실제 헤더 36px로 맞췄다. 경영기획 2Depth는 원본 `.hw`의 18px/600/16.9px를 적용했다. 인재성장은 같은 기존 값, 나머지는 20px/700/19.7px를 유지한다. 2Depth 114×30.4px·기존 Gradient·Radius 7.1px·Shadow·정렬을 유지했다. 모든 타이틀↔첫 카드 Gap 8px를 실제 측정했다.
|
||||
3. **경영진 뱃지**: Full Composition의 `workdeskPresentation`에서 `jobTitle.trim() === '경영진'`인 뱃지 DOM만 생성하지 않는다. 빈 attribute container도 생성하지 않는다. 실제 직위·직책 데이터·팝업·통계는 그대로다. 현재 fixture의 대표자 `position="경영진"`, `jobTitle="실장"`은 유지한다. 따라서 대표자 직위 글자와 표의 `경영진` 열은 정상적으로 남는다.
|
||||
4. **기타 뱃지·이름**: 실장·팀장·디비전장 등의 원본 뱃지는 유지한다. 이름은 기존 `홍 길 동`, 직위는 표시 계층에서만 공백을 제거한다. JSON을 수정하지 않는다. 기존 fixture에 경영진 직책 뱃지가 없어 대표자/1Depth/2Depth/일반/파견/휴직의 메모리 전용 변형을 별도로 검사했다. 여섯 역할에서 뱃지 미표시·직위 유지, 팝업의 원본 직책 보존, interactive 공통 렌더러의 별도 표시 정책을 확인했다.
|
||||
5. **허브**: 원본 21px/750/24px·아이콘 20px·Gap 8px·좌우 Padding 22/24px·높이 42px를 유지했다. 기존 상속 `max-width:100%`가 198px 축 폭으로 텍스트를 제한하던 부분을 해제했다. 중심을 X 935에 명시하여 실제 폭 약 219.016px에서도 연결선 축을 유지한다. 기존 말풍선 SVG·배경·곡선은 변경하지 않았다.
|
||||
6. **사진 없음**: 신규 공통 `person-placeholder.svg` 한 종을 모든 카드에서 같은 inline 자산으로 사용한다. 연한 사각 배경·중립 사람 실루엣이며 실사/원형 아바타/장식 없음. 실제 이미지 우선, 이미지 decode 실패 시 공통 SVG fallback을 확인했다. 승인 카드의 예약 슬롯 36×62/32×56/24×44 및 P3 파견/휴직 28×28을 유지한다. 원본 슬롯 55×76/54×70/38×52는 승인 카드보다 커서 복사하지 않았다.
|
||||
7. **인원 현황 표**: 기존 표 폭 330px·위치·14px/400/1.15·좌우 조직명 padding 7px·행 19px/합계 20px·정렬·Border를 유지했다. 조직명 열 112px, 경영진 열 46px, 나머지는 잔여 폭을 배분한다. `nowrap`을 적용했으며 모든 셀의 실제 텍스트 Range가 셀 내부 한 줄로 들어가는지 확인했다. 숨김·말줄임·글자 축소 없음. 표 높이는 약 200.375px에서 172px로 줄었다. 가상 41명·조직별/직위별 계산·관계사 바는 보존했다.
|
||||
8. **변경 파일**: 구현·예제·문서 14개. 재생성된 dist와 전체 해시 차이는 [파일 기록](../../test-results/org-chart-p3.1/source-changes.json)에 있다.
|
||||
|
||||
- `packages/org-chart/src/core/employee-card.ts`, `src/core/index.ts`
|
||||
- `packages/org-chart/src/styles/design-tokens.css`, `employee-card.css`, `org-chart-readonly.css`, `org-chart-p3-summary.css`
|
||||
- 신규 `packages/org-chart/src/assets/person-placeholder.svg`
|
||||
- `examples/org-chart/p1.css`, `examples/org-chart-react/p1.css`, 신규 `examples/org-chart/verify-p3.1.mjs`
|
||||
- `packages/org-chart/README.md`, `docs/org-chart/ui-composition.md`, `card-design-stage2.md`, 이 보고서
|
||||
|
||||
9. **화면별 결과**: HTML HTTP·직접 `file://` classic·React production HTTP·실제 Storybook 6041 모두 1920×1080, 1440×900, 1280×800, 768×1024, 390×844에서 PASS. 문서 scroll/client 폭·높이가 같고 모든 카드가 Stage 내부에 들어간다. 모바일은 기존 전체 비례 축소라 글자가 작지만 팝업은 축소하지 않는다. [1920 HTML](../../test-results/org-chart-p3.1/html-1920x1080.png), [1920 Storybook](../../test-results/org-chart-p3.1/storybook-1920x1080.png), [390 Storybook](../../test-results/org-chart-p3.1/storybook-390x844.png)을 육안 검토했다.
|
||||
10. **HTML·React 동일성**: 네 경로의 카드 내용/좌표/크기·허브 위치/폭·표 내용/폭을 화면별로 비교하여 일치했다. 같은 Core와 SVG를 사용하며 HTML 모방 화면을 만들지 않았다. React Wrapper와 Storybook 소스·사용자 메뉴 세 개는 변경하지 않았다.
|
||||
11. **회귀·검증**: Core/Wrapper Typecheck, Core ESM/IIFE/타입/CSS build, Wrapper build, HTML prepare, React 소비 production build PASS. 여섯 카드 역할의 데스크톱/모바일 팝업·focus trap·ESC 복귀, 대표자 Enter/Space/닫기, HTML/React의 실제 키보드 그룹 링크 이동을 확인했다. 146×74/146×68/108×52 카드 및 P3 상태 카드 114×54 보존. 상태 카드 114×54와 슬롯 28×28은 이번 도입값이 아니라 기존 P3 측정값이다. 기존 큰 이미지 inline chunk 약 2.54MB 경고는 유지한다. 마지막 들여쓰기 정리 뒤 재빌드한 runtime 파일 73개는 검증 시점과 byte 동일하여 브라우저를 반복하지 않았다.
|
||||
12. **남은 Workdesk 차이**: 원본 Pretendard 대신 이번 확정 Noto Sans KR을 적용한다. 승인 카드 때문에 원본보다 작은 사진 슬롯을 유지한다. 표 열/행과 인원은 현재 가상 fixture를 따른다. 원본 운영 화면과 완전한 픽셀 일치 및 곡선/글리프 ink 자동 교차 검사는 NOT VERIFIED다. 확장형 Auto Layout/layoutConfig API 없음.
|
||||
13. **Guard·NOT VERIFIED**: 최종 유효 PARTIAL Guard는 React bootstrap `p1.html`에 실행했다. 원본 staticStatus `NOT APPLICABLE`, status `NOT VERIFIED`, `RENDER-002`를 보존한다. 원본 및 결합 CSS 상태는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED다. 별도 Runtime은 PASS이며 이름 있는 computed 측정은 PASS이나 Core Shadow CSS 전체의 정적/의도 인증으로 확대하지 않는다. 최초 Guard는 target 기준 result 경로를 중복 지정해 ENTRY/SCOPE 미검증이 발생했다. 해당 실행은 [잘못된 인자 진단](../../test-results/org-chart-p3.1/guard-initial-invalid-result-path.json)으로 보존하고 인자만 바로잡았다. 실제 운영 Workdesk 픽셀 일치, 모든 임의 데이터/테마/브라우저, 스크린리더 발화, 실물 touch, 전체 Storybook production build는 NOT VERIFIED다.
|
||||
14. **보존·Git**: 작업 경로 `D:\디자인시스템\HMDesign-OrgChart`, 브랜치 `feature/orgchart-package`, HEAD `d18e3c33801e165e17b6d971e2f6198eed483c3c` 유지. Workdesk 원본·운영 main 미수정. JSON·승인 SVG·로고·Workdesk 장식·HM Theme·Guard·과거 검사/보고서·package manifests 등 보존 대상 216개 해시 동일. 시작 시 기존 미커밋 package.json/package-lock.json 및 OrgChart 작업을 보존했다. Stage·Commit·Push·Merge·Reset·Rebase·Force Push 미진행.
|
||||
|
||||
초기 집중 검사 102 FAIL도 [초기 기록](../../test-results/org-chart-p3.1/browser-initial.json)에 남긴다. 다섯 화면의 실행 경로 동일성 실패는 host 언어에 따른 허브 폭 차이였고 `lang="ko"`로 수정했다. 나머지 97건은 기존 상태 카드 크기/slot, `.hw` ID, variable font 내부 이름, fixture에 없는 뱃지에 대한 검사 기준을 정확히 잡지 못한 진단 오류였다. 원본 P3의 기대값이나 FAIL 기록은 수정하지 않았다.
|
||||
|
||||
검증 근거: [최종 browser JSON](../../test-results/org-chart-p3.1/browser.json), [원본 Guard](../../test-results/org-chart-p3.1/guard-react.json), [Runtime](../../test-results/org-chart-p3.1/runtime-evidence.json), [결합 증거](../../test-results/org-chart-p3.1/linked-evidence.json), [runtime byte 동등성](../../test-results/org-chart-p3.1/artifact-format-equivalence.json).
|
||||
|
||||
Workdesk 읽기 전용 근거: `index.css`의 `.hw` 2858–2865, 그룹 헤더 3703–3751, 2Depth 표면 3776–3786, 폭 3884–3899, 허브 2472–2542, 표 1432–1673; `index.html` 255–282 및 `org-dashboard-renderer.js` 1638–1663. P3가 남긴 원본 수치 근거도 보존한다.
|
||||
|
||||
미리보기: [Storybook Full Composition](http://127.0.0.1:6041/?path=/story/orgchart--full-composition).
|
||||
|
||||
작업 시간:
|
||||
|
||||
- Started At: 2026-10-12 16:01:00 KST
|
||||
- Completed At: 2026-10-12 16:30:44 KST
|
||||
- Actual Duration: 29분 44초
|
||||
- Estimated Duration: 35–50분
|
||||
- Variance: 예상보다 5분 16초–20분 16초 단축
|
||||
@@ -0,0 +1,73 @@
|
||||
# HMDesign OrgChart P3.2 — 카드 Typography·파견/휴직·로고 크기 보정
|
||||
|
||||
RESULT: **PASS — 요청한 P3.2 변경 범위**. 집중 브라우저 **431/431 PASS**, 정책 측정 보완 **13/13 PASS**, 신규 콘솔·런타임 오류 **0건**. Typecheck·관련 Build PASS. P3.1의 486건 전체 suite를 반복하지 않았으며 기존 보고서·검증 증거 115개 파일을 보존했다. Guard 원본과 결합 전체 상태는 **NOT VERIFIED**로 유지한다.
|
||||
|
||||
## 변경 전후 px
|
||||
|
||||
아래는 실제 Chrome computed style과 Stage 축소 배율을 제거한 크기다. 측정 원본의 소수점 오차는 약 0.0001px 이내이며 아래 정수는 반올림한 값이다. [43개 카드 전체 측정](../../test-results/org-chart-p3.2/before-after-px.json)에 원시 값·컬러·로고 alpha 영역을 남겼다.
|
||||
|
||||
- **1Depth 대형 카드**: 이름 **24→24px**, weight 600·line-height 30px 유지. 직위 **20→18px**, weight 400·line-height 22px 유지. 기존 이름 #071D3A·직위 #797979·letter-spacing 유지. 카드 **146×68**, 사진 **32×56**, 사진↔텍스트 **10px** 유지.
|
||||
- **2Depth/일반 소형 카드**: 이름 **20→18px**, weight 600·line-height 24px 유지. 직위 **15→15px**, weight 400·line-height 18px 유지. 기존 이름 #323232·직위 #797979·letter-spacing 유지. 카드 **108×52**, 사진 **24×44**, 사진↔텍스트 **4px** 유지. 2Depth의 semantic leader도 동일한 소형 규격이다.
|
||||
- **대표자**: 이름 **24px/600/30px**, 직위 **20px/400/22px**, 기존 #071D3A/#224387·letter-spacing 유지. 카드 **146×74**, 사진 **36×62** 유지.
|
||||
- **파견·휴직 타이틀**: **14px/700/20px → 20px/700/19.7px**. 일반 2Depth의 기본 Typography와 같은 CSS 변수 세 개를 공유한다. 전용 회색 Gradient, Border·Shadow·108×24 타이틀 표면은 유지한다. 경영기획·인재성장의 기존 **18px/600/16.9px** 예외는 변경하지 않았다.
|
||||
- **파견·휴직 직원**: 이름 **13px/700/18px → 18px/600/24px**, 직위 **10px/400/13px → 15px/400/18px**. 일반 소형 카드와 같은 콘텐츠 DOM·Typography 변수·사진 영역·텍스트 좌표·간격을 재사용한다. 이름 #3E4650·직위 #555F6A, 기존 회색 Gradient·Border·Radius·로고 grayscale(1)/opacity 0.72 유지. 이름 아래 별도 상태 문구 없음.
|
||||
- **파견·휴직 크기 차이**: P3.1 실제값은 카드 **114×54**, 사진 **28×28**이었다. 이번 명시된 최신 요구값인 카드 **108×52**, 사진 **24×44**로 보정했다. 이를 기존 108×52를 그대로 보존한 것으로 표현하지 않는다. 1px Border를 유지하면서 padding을 3px로 조정하여 일반 소형의 4px 사진 원점·32px 텍스트 원점과 일치한다.
|
||||
- **타이틀↔첫 카드 Gap**: 일반 조직 및 파견·휴직 모두 **8→8px**. 직위↔이름 행 Gap **0px**.
|
||||
- **Font/name/badge**: Noto Sans KR·기존 호스트 font-face/폰트 파일 유지. 이름은 **홍 길 동**, 직위·뱃지의 글자 사이 공백은 표시에서만 제거한다. 직원 JSON·팝업의 원문 데이터는 변경하지 않는다.
|
||||
|
||||
## 카드 로고 적용 범위
|
||||
|
||||
렌더링된 43개 직원 카드(대표자 1, 대형 6, 소형 34, 파견·휴직 2) 모두 기존 회사/관계사 PNG를 실제로 표시한다. 원본 SVG·PNG를 수정하지 않고 직원 카드 selector에만 확대를 적용했다.
|
||||
|
||||
- **소형**: 기존 실제 contain 영역 **12×12→18×18px**, 정확히 **1.5배**. 컨테이너는 투명한 1px inset을 포함해 14×14→20×20이다. 카드 기준 image origin **(88, 3)px**.
|
||||
- **대형·대표자**: 실제 contain 영역 **14×14→21×21px**, 정확히 **1.5배**. 컨테이너 16×16→23×23, image origin **(124, 1)px**. top/right 0으로 옮겨 뱃지와의 여유를 확보했다.
|
||||
- **파견·휴직**: 실제 contain 영역 **14×14→21×21px**, 정확히 **1.5배**. image origin **(85, 1)px**. 파견의 이전 CSS image box는 14×24였지만 object-fit:contain으로 원본 정사각 로고가 실제 표시된 영역은 14×14였다. 원시 box 높이를 단순 1.5배하는 방식으로 확대하지 않았다.
|
||||
- 모든 원본은 transparent padding을 포함하므로 이미지 box와 실제 유색 로고 영역은 다르다. 예를 들어 한맥 21px box의 유색 범위는 약 **15.5835×9.5011px**다. **contain 영역과 PNG alpha 영역의 폭·높이 모두 1.5배**를 비교했다.
|
||||
- 원본 비율 유지, 추가 배경·테두리·그림자 없음. 승인 카드 전체 크기 변경 없음. 로고 alpha 영역과 이름·직위·뱃지·프로필 영역 충돌 **0건**, 카드 밖 잘림 **0건**. 이번 fixture에서 1.5배를 모두 수용하므로 별도 최대 크기 제약은 없다.
|
||||
- 하단 관계사별 인원 현황의 로고 box·바 전체 좌표·집계 문자열은 이전 측정과 동일하다. 확대 selector는 footer에 적용되지 않는다.
|
||||
|
||||
## 수정 파일
|
||||
|
||||
기존 파일 **6개**를 변경하고 **2개**를 추가했다. 기존 미커밋 package.json/package-lock.json 및 OrgChart 작업은 보존했다.
|
||||
|
||||
1. [employee-card.ts](../../packages/org-chart/src/core/employee-card.ts): readOnly 상태 카드의 공통 compact 콘텐츠 DOM, 표시 뱃지 공백 제거. 공개 API 변경 없음.
|
||||
2. [employee-card.css](../../packages/org-chart/src/styles/employee-card.css): 요청 Typography, 일반/상태 공통 스타일, 카드 한정 로고 확대·위치.
|
||||
3. [org-chart-readonly.css](../../packages/org-chart/src/styles/org-chart-readonly.css): 일반 2Depth/상태 타이틀 Typography 변수 공유, 대형 로고 top 조정.
|
||||
4. [ui-composition.md](ui-composition.md): P3.2 최신 계약 추가. 과거 절 보존.
|
||||
5. [card-design-stage2.md](card-design-stage2.md): P3.2 우선 안내 추가. 과거 절 보존.
|
||||
6. [Core README](../../packages/org-chart/README.md): P3.2 현재 규격·적용 범위 추가.
|
||||
7. 신규 [verify-p3.2.mjs](../../examples/org-chart/verify-p3.2.mjs): 변경 전 capture, 변경 항목 집중 검사, 정책 측정 보완 모드.
|
||||
8. 신규 이 보고서.
|
||||
|
||||
관련 Core ESM/IIFE/타입/CSS, Wrapper, HTML example dist, React production dist만 재생성했다. 기존 package manifests/lock·JSON·승인 카드 SVG·로고 PNG·공통 사람 아이콘·Storybook 소스·Theme는 보존한다. 기존 파일 해시와 6개 변경 파일 diff는 [source-changes](../../test-results/org-chart-p3.2/source-changes.json) 및 같은 디렉터리의 diff/에 기록했다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
- **PASS**: [집중 브라우저 431/431](../../test-results/org-chart-p3.2/browser.json). HTML HTTP, 직접 file:// classic, React production HTTP, 실제 Storybook의 **1920×1080 / 1440×900 / 1280×800 / 768×1024 / 390×844**, 총 20 captures. 요청 Typography·상태 규격·로고 1.5·카드 크기·Gap·사진 아이콘·집계·하단 로고·경영진 뱃지 미표시를 확인했다.
|
||||
- **PASS**: 모든 화면의 document scrollWidth=clientWidth, scrollHeight=clientHeight. 모든 카드와 콘텐츠는 Stage 안에 들어간다. 기존 전체 자동 축소 정책을 유지한다. 390px는 기존처럼 전체 비례 축소되어 글자가 작다.
|
||||
- **PASS**: 대표자·1Depth·소형·파견·휴직 팝업을 네 실행 경로의 데스크톱/390에서 확인했다. 원문 정보·축소하지 않는 팝업·Tab focus trap·ESC 카드 focus 복귀 유지. HTML/React의 1Depth 링크 설정과 실제 Enter navigation 확인.
|
||||
- **PASS**: 같은 Core의 카드 Typography·위치·크기가 네 실행 경로에서 동일하다. Storybook 사용자 메뉴는 기존 세 개다. Core/Wrapper/Storybook 공개 계약·메뉴를 추가하지 않았다.
|
||||
- **PASS**: [정책 보완 13/13](../../test-results/org-chart-p3.2/policy-browser.json). 초기 검사 도구의 배경/화살표 selector는 빈 shell/잘못된 attribute를 측정했다. 구현은 그대로 두고 selector만 바로잡아 readOnly·collapse 0·#F5F8FB·연결 path 5·협업 arrow 5를 네 경로의 1920에서 별도 확인했다. 통과한 431건은 다시 실행하지 않았다.
|
||||
- **PASS**: Core/Wrapper Typecheck, Core ESM/IIFE/타입/CSS Build, Wrapper Build, HTML prepare, React 소비 production Build. 로그는 test-results/org-chart-p3.2/의 typecheck-*.log/build-*.log/prepare-html.log. 기존 inline image chunk 약 2.54MB의 Vite 경고는 유지된다. 신규 브라우저 console/runtime 오류 **0건**.
|
||||
- **PASS**: 구현 세 파일의 before 대비 코드 검토에서 추가 수정 필요 사항 없음. 실제 [1920 Storybook](../../test-results/org-chart-p3.2/storybook-1920x1080.png), [1920 React](../../test-results/org-chart-p3.2/react-1920x1080.png), [390 Storybook](../../test-results/org-chart-p3.2/storybook-390x844.png) 육안 확인.
|
||||
- **PASS**: 보존 스냅샷의 기존 **266개 중 260개 동일**, 예정한 **6개만 변경**. Workdesk **45개 모두 동일**, 기존 P3.1 증거 **115개 모두 동일**. JSON·직원 41명·원본 자산 보존. [변경 전 스냅샷](../../test-results/org-chart-p3.2/baseline-preservation.json), [최종 보존 결과](../../test-results/org-chart-p3.2/source-changes.json).
|
||||
- **PARTIAL / NOT VERIFIED**: [PARTIAL Guard 원본](../../test-results/org-chart-p3.2/guard-react.json)은 staticStatus **NOT APPLICABLE**, status **NOT VERIFIED**, RENDER-002를 유지한다. p1 React bootstrap 소비 graph 검사이며 내부 Core Shadow CSS의 전체 정적 인증을 의미하지 않는다. 원본/결합 CSS는 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED**. [별도 Runtime](../../test-results/org-chart-p3.2/runtime-evidence.json) 및 [결합 증거](../../test-results/org-chart-p3.2/linked-evidence.json)의 runtimeStatus는 **PASS**, 이름 있는 computed CSS 측정은 PASS다. 원본 미검증을 덮어쓰지 않았다.
|
||||
- **FAIL**: 최종 요청 범위에서 **0건**. 최초 baseline의 Storybook 연결 실패는 sandbox/server 실행 환경 차이였다. 서버 실행 환경을 맞춘 뒤 baseline 20개 capture를 완료했다. [초기 진단](../../test-results/org-chart-p3.2/baseline-browser-initial-server-isolation.json)은 보존했다.
|
||||
|
||||
## 미해결·검증 한계
|
||||
|
||||
요청한 fixture의 1.5배 크기 제한이나 구현 미해결 사항은 없다. 다만 임의의 새 데이터/외부 Theme/다른 브라우저, 운영 Workdesk 전체 픽셀 일치, 전체 Storybook production Build, 스크린리더 발화·실물 touch는 **NOT VERIFIED**다. 기존 P3.1 전체 486건을 현재 결과로 재인증하지 않았다.
|
||||
|
||||
PNG alpha bounding box와 텍스트 Range/프로필의 교차는 검사했다. Noto Sans KR의 인접 행 Range 외곽은 line-height보다 커 겹칠 수 있으므로 이를 곧바로 글자 충돌로 판정하지 않았다. 실제 글리프 ink 간 교차를 자동 인증한 것은 아니며 이 항목은 **NOT VERIFIED**다. 대표 화면은 육안으로 읽힘·로고/글자 분리를 확인했다.
|
||||
|
||||
## 작업 시간·Git
|
||||
|
||||
- Started At: 2026-10-12 16:34:08 KST
|
||||
- Completed At: 2026-10-12 16:51:16 KST
|
||||
- Actual Duration: 17분 8초
|
||||
- Estimated Duration: 25분
|
||||
- Variance: 7분 52초 단축
|
||||
- 작업 폴더: D:\디자인시스템\HMDesign-OrgChart
|
||||
- 브랜치: feature/orgchart-package
|
||||
- 시작/완료 HEAD: d18e3c33801e165e17b6d971e2f6198eed483c3c (동일)
|
||||
- 원본 Workdesk·운영 HMDesign main 미수정. Stage·Commit·Push·Merge·Reset·Rebase 없음.
|
||||
@@ -0,0 +1,54 @@
|
||||
# HMDesign OrgChart P3 — 디자인 및 조회 기능
|
||||
|
||||
RESULT: **PASS — 보존된 41명 fixture와 P3 변경 범위**.
|
||||
|
||||
HTML·직접 열기 classic·React·실제 Storybook에서 P3 검사 **732/732 PASS**, 신규 브라우저 오류 **0**. 기존 P2·P1 검사를 재실행하거나 기대값을 바꾸지 않았다. Guard의 원본 미검증은 별도로 보존한다.
|
||||
|
||||
1. **P2 대비 수정**: 조회 배경, 하위 연결선, 이름/직위 표시, 직책 뱃지, 타이틀 간격, 하위 조직 제목, 허브, 사진 슬롯, 상태 카드, 조직별 교차표, 관계사 바, 링크, 정보 팝업, 컨테이너 높이 맞춤 및 Storybook 메뉴를 수정했다. 조직 관계·순서·41명 데이터와 승인 자산은 유지했다.
|
||||
2. **Workdesk 반영**: 읽기 전용 원본 `D:\디자인시스템\gpd-workdesk\sample\조직도_v1.3`의 실제 HTML/CSS/SVG를 확인했다. 배경 `#F5F8FB`, 하위 타이틀의 크기·그라데이션·그림자·글꼴 수치, 직책 뱃지의 팀별 색, 허브 SVG/텍스트, 통계 표 및 관계사 바를 반영했다. [원본 수치·경로 증거](../../test-results/org-chart-p3/workdesk-summary-source-evidence.json)를 보존했다.
|
||||
3. **카드·타이틀·뱃지·허브**: 대표자 146×74, 1Depth 조직장 146×68, 2Depth 조직장/일반 직원 108×52px 유지. 조회 이름은 `홍 길 동`, 직위와 직책은 원본 문자열이다. 직위와 직책 뱃지를 한 줄에 gap 4px로 놓고 이름과의 gap 0px 유지. 모든 타이틀↔첫 카드 gap 8px. 하위 제목 표면은 114×30.4px이며 부모 팀 색을 상속한다. 허브는 원본 말풍선 SVG, 아이콘 20px/gap 8px, 텍스트 21px/750/24px. 대표자 및 긴 디비전장 뱃지는 동일한 22px 행 내부 하단 정렬로 승인 SVG 우상단 로고와의 충돌을 피했다.
|
||||
4. **연결선**: 조회 전용에서 엔진의 하위 pseudo 연결선을 숨겼다. 기존 대표자↔허브↔주요 조직 연결 SVG Path 5개, 협업 화살표 5개 및 장식은 유지했다. interactive 연결선·접기 동작은 유지했다. 선을 지우거나 조직 관계를 바꾸지 않았다.
|
||||
5. **사진/상태**: 사진 없음은 비원형의 빈 직사각형 슬롯으로 표시한다. 실제 직원 사진이나 임의 인물 이미지를 사용하지 않았다. 기존 승인 카드의 예약 너비 36/32/24px를 유지하고 높이를 62/56/44px로 조정했다. 조회 compact 파견·휴직 카드의 이름 하단 상태 문구만 숨기고 그룹 제목과 원본 상태 데이터는 유지한다.
|
||||
6. **조직별 표 데이터**: 원본 employees에서 `조직 × 직위`를 계산한다. 최상위 직접 소속 1명과 주요 6개 조직의 하위 포함 집계를 분리해 중복 없이 41명. 행 소계는 총괄기획실 1, 사업관리 2, 경영기획 7, 인재성장 7, 통합시스템 10, 디자인기획 10, 솔루션영업 4. 직위 합계는 경영진 1/수석 6/책임 12/선임 11/연구 11. 독립 PositionSummary 공개 API는 유지한다.
|
||||
7. **관계사 집계**: 기존 회사 로고를 사용한 제목 없는 한 줄 바. 한맥 11, 삼안 11, 장헌 0, 장헌산업 0, 피티씨 9, 한라 0, 바론 10, 총 41명. 바 높이 약 55.4px, 로고 39.3px, 글꼴 19.6px 및 원본 간격·그림자 반영. DOM의 43개 카드는 상태 요약 2개 중복 표시를 포함하며 직원 수는 41명이다.
|
||||
8. **1Depth 링크**: 공개 `organizationLinks?: Readonly<Record<string, string | undefined>>` 옵션을 Core와 React에 추가했다. 원본 조상 관계가 1Depth이며 URL이 지정된 제목만 링크다. 미지정 제목은 일반 제목. HTTP(S)/상대 URL을 지원하며 다른 scheme·credentials는 거부한다. 기본 실제 URL은 넣지 않았다. 검증에서만 로컬 증명 경로를 사용해 실제 이동·키보드·focus 및 카드 이벤트 분리를 확인했다.
|
||||
9. **직원 팝업**: 대표자/1Depth/2Depth/일반/파견/휴직 카드 click·Enter·Space로 조회한다. 기존 데이터의 이름·직위·소속·직책·관계사·상태만 표시한다. 닫기/ESC/focus trap/원래 카드 focus 복귀 확인. 팝업은 Stage 밖 native dialog top layer로 표시되며 transform 없음, 데스크톱 폭 420px/390px 화면 폭 358px. 공개 `onEmployeeClick(employee, context)` 지정 시 기본 팝업 대신 호출한다. React callback의 최신 값과 동시 이벤트를 검증했다. 기존 tooltip은 팝업 직전 정리한다.
|
||||
10. **Scale/Overflow**: 기준 디자인 1920×914.5px. 실제 host·조상 높이·padding·max-height와 viewport를 사용하며 임의 Header 48px 차감 없음. 네 경로 모두 같은 측정값이며 문서 client/scroll 폭·높이 동일, 카드/주요 영역 사각형 충돌 및 Stage 밖 콘텐츠 0건.
|
||||
|
||||
- 1920×1080: scale 0.9833333333, Stage 1888×899.2584px, 문서 1920×1080.
|
||||
- 1440×900: scale 0.7333333333, Stage 1408×670.6334px, 문서 1440×900.
|
||||
- 1280×800: scale 0.6500000000, Stage 1248×594.4250px, 문서 1280×800.
|
||||
- 768×1024: scale 0.3833333333, Stage 736×350.5583px, 문서 768×1024.
|
||||
- 390×844: scale 0.1864583333, Stage 358×170.5161px, 문서 390×844.
|
||||
- 추가 높이 조건: 1920×650 scale 0.6754098256, Stage 높이 617.6623px. 고정/max-height 400px host scale 0.4371584574, Stage 높이 399.7814px. 재부착된 400px host의 외부 20px/내부 12px padding에서 가용 높이 336px, scale 0.3672130903, Stage 높이 335.8164px.
|
||||
|
||||
Stage의 layout scroll extent는 transform 전 1920×915px로 남는다. 실제 변환된 모든 콘텐츠 경계가 가용 영역 안에 들어오는 것을 별도로 확인했으므로 `overflow:hidden`만으로 잘림을 숨긴 결과가 아니다. offsetHeight 915는 CSS 높이 914.5의 정수 반올림이며 높이 계산은 소폭 안전 여유를 둔다.
|
||||
|
||||
11. **Storybook**: 사용자 메뉴는 `Logo · Personnel Summary` / `Department Colors` / `Full Composition` 3개. 통합 페이지에서 기존 Logo·PositionSummary·AffiliationSummary Core를 각각 렌더링한다. 기존 독립 검증 Story는 internal로 보존했다. 실제 6041 index·manager·페이지를 확인했으며 메뉴 통합과 Full Composition 배치는 분리했다.
|
||||
12. **수정 파일**: 기존 소스/문서 27개와 신규 구현·검사·보고 파일은 아래 목록. 재생성된 dist 및 정확한 시작 해시 차이는 [전체 파일 기록](../../test-results/org-chart-p3/source-changes.json)에 있다. 기존 package.json/package-lock.json의 미커밋 변경은 추가로 수정하지 않았다.
|
||||
|
||||
- Core: `packages/org-chart/src/core/{employee-card,status-summary,render,types,index,org-chart-content,organization-totals,affiliation-bar}.ts`, `src/index.ts`, `src/styles/{employee-card,org-chart-readonly,org-chart}.css`, `build.mjs`, `README.md`.
|
||||
- 신규 Core: `packages/org-chart/src/core/employee-info-dialog.ts`, `src/styles/employee-info-dialog.css`, `src/styles/org-chart-p3-summary.css`.
|
||||
- React: `packages/org-chart-react/src/{OrgChart.tsx,types.ts}`, `README.md`.
|
||||
- 예제: `examples/org-chart/{p1-app.js,p1.css}`, 신규 `verify-p3.mjs`; `examples/org-chart-react/{p1.tsx,p1.css,README.md}`.
|
||||
- Story: `src/org-chart/{org-chart-p1.stories.tsx,org-chart-stories.css}`.
|
||||
- 문서: `docs/org-chart/{integration,ui-composition,card-design-stage2}.md` 및 이 P3 보고서. P1/P2 보고서는 수정하지 않았다.
|
||||
- 산출물: Core/Wrapper/HTML/React dist, `test-results/org-chart-p3`의 브라우저 JSON·원본 Guard·별도 runtime/linked evidence·진단 이력·캡처·시작/변경 해시. `assemble-evidence.mjs`는 저장된 증거만 연결하며 테스트를 재실행하지 않는다.
|
||||
|
||||
13. **HTML·React 검증**: Core/Wrapper typecheck, Core ESM/IIFE/타입/CSS 및 Wrapper build, HTML prepare, React 소비 예제 production build PASS. 공통 Core의 geometry·표·카드가 HTML HTTP/file classic/React HTTP/Storybook에서 일치했다. Chrome 155.0.8059.39의 5개 화면, 링크/callback/키보드, 6종 카드×2화면×4경로 팝업 48개 측정, 높이 제한 조건을 포함해 **732/732 PASS**. 원본 이미지 inline 때문에 React 약 2.54MB chunk 경고는 유지한다. 실제 테스트는 live Storybook 6041이며 오래된 `storybook-static` 결과를 최종 근거로 사용하지 않는다.
|
||||
14. **기존 PASS/신규 FAIL**: 승인 SVG·로고·장식·41명 fixture·P1/P2 보고서·기존 검사 등 보존 대상 38개 해시 동일. 관련 interactive/독립 API/공통 Core 회귀는 P3 검사에서 PASS. P2 484건과 오래된 verify/verify-p1 suite 전체는 재실행하지 않았으므로 전체 기존 PASS를 재인증하지 않는다. 알려진 과거 FAIL 기록·기대값을 그대로 보존했다. 초기 P3 522건/63 FAIL과 중간 554건/10 FAIL JSON도 보존했으며 최종 신규 FAIL 0. 제품의 focus 복귀/뱃지 겹침/재부착 host padding 오류 및 검사 환경의 오래된 Storybook 경로·선택자·Chrome 0px letter-spacing 직렬화 근거를 수정했다. 실패를 필터링하지 않았다.
|
||||
15. **남은 시각적 차이**: 원본 Pretendard Variable 대신 기존 승인 글꼴을 유지한다. 원본 사진 55×76/54×70/38×52는 승인 카드보다 커서 그대로 복사하지 않고 슬롯 크기를 맞췄다. 긴 뱃지 행 정렬은 로고 충돌 방지를 위한 조정이다. 표의 실제 열/행은 현재 fixture를 기반으로 하며 원본 API의 직위 목록은 제공되지 않았다. 긴 `경영진`/`통합시스템 Div` 줄바꿈으로 표 높이는 약 200.375px. 바 폭은 기준 좌표에 맞춰 1806px(원본 약 1805.7px). 최종 Storybook 1920/390 및 HTML 1920 캡처를 육안 확인했다. 모바일은 전체 화면 비례축소로 글자가 작지만 정보 팝업은 축소하지 않는다.
|
||||
16. **Guard/NOT VERIFIED**: PARTIAL Guard를 최종 React bootstrap에 1회 실행. 원본 staticStatus `NOT APPLICABLE`, status `NOT VERIFIED`, `RENDER-002` 보존. 원본 CSS STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED, checks `[]`. [별도 runtime](../../test-results/org-chart-p3/runtime-evidence.json)은 동일 소스·계약 fingerprint와 브라우저 증거 SHA로 연결했다. [결합 증거](../../test-results/org-chart-p3/linked-evidence.json)는 Runtime PASS·명시한 computed CSS PASS이나 Guard의 정적 적용 범위/스타일 미검증 때문에 전체 status NOT VERIFIED를 유지한다. Core Shadow CSS 전체 Guard 인증, 실제 운영 Workdesk 픽셀 일치, 자동 glyph/곡선 교차 검사, 모든 임의 데이터/테마/브라우저, screen reader 발화·실물 touch, 전체 Storybook production build는 NOT VERIFIED.
|
||||
17. **Git/원본 보존**: `D:\디자인시스템\HMDesign-OrgChart`, `feature/orgchart-package`, HEAD `d18e3c33801e165e17b6d971e2f6198eed483c3c`. Workdesk 원본과 HMDesign main을 수정하지 않았다. 기존 미커밋 작업을 보존하고 Stage·Commit·Push·Merge·Reset·Rebase를 수행하지 않았다.
|
||||
|
||||
검증 근거: [최종 browser JSON](../../test-results/org-chart-p3/browser.json), [1920 캡처](../../test-results/org-chart-p3/storybook-1920x1080.png), [390 캡처](../../test-results/org-chart-p3/storybook-390x844.png), [원본 Guard](../../test-results/org-chart-p3/guard-react.json), [초기 진단](../../test-results/org-chart-p3/browser-initial.json), [중간 진단](../../test-results/org-chart-p3/browser-after-card-fixes.json).
|
||||
|
||||
미리보기: [Storybook Full Composition](http://127.0.0.1:6041/?path=/story/orgchart--full-composition).
|
||||
|
||||
작업 시간:
|
||||
|
||||
- Started At: 2026-10-12 15:19:27 KST
|
||||
- Completed At: 2026-10-12 15:56:54 KST
|
||||
- Actual Duration: 37분 27초
|
||||
- Estimated Duration: 45~60분
|
||||
- Variance: 예상보다 7분 33초~22분 33초 단축
|
||||
@@ -0,0 +1,74 @@
|
||||
# HMDesign OrgChart P5-1A 구현 및 커밋 준비 결과
|
||||
|
||||
RESULT: **PASS — P5-1A 요청 범위**. Fixed/Auto 렌더링 경계를 분리하고 확장형 초기 Storybook 화면을 추가했다. 자동 재귀 배치, 접기·펼치기, Zoom/Pan은 구현하지 않았다.
|
||||
|
||||
## 작업 환경과 시간
|
||||
|
||||
- 경로: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 범위: Core·React 패키지와 요청된 Storybook 초기 화면. 운영 main 수정 없음.
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`
|
||||
- Origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- 실제 원격 `main`·`feature/orgchart-package` SHA 조회 결과: 위 HEAD와 동일.
|
||||
- Started At: 2026-10-12 17:23:00 KST
|
||||
- Completed At: 2026-10-12 17:42:41 KST
|
||||
- Actual Duration: 19분 41초
|
||||
- Estimated Duration: 약 45분 (분석 10분, 구현 20분, 검증·커밋 준비 15분)
|
||||
- Variance: 예상보다 25분 19초 짧음
|
||||
|
||||
## 구현 결과
|
||||
|
||||
공개 `OrgChartOptions.layoutMode?: 'fixed' | 'auto'`를 추가했다. 생략 시 `fixed`이며 기존 `displayMode`와 독립적이다. Fixed는 기존 `createOrgChartContent`, Workdesk 좌표·아트워크·크기 맞춤 경로를 그대로 사용한다. 기존 세 사용자 Story 파일도 시작 스냅샷과 바이트 단위로 동일하다.
|
||||
|
||||
Auto는 `expandedPreviewRoots` → 공유 `engineTree`/dabeng engine → `createExpandedOrgChartContent` 경계로 초기 그룹을 표시한다. 정규화된 실제 hierarchy의 직계 그룹과 원본 `parentId`를 보존한다. 하위 데이터는 삭제하지 않고 이번 표시만 보류하며 집계에는 남긴다. 새로운 presenter는 `.hm-orgchart-expanded` 아래에서 일반 흐름·줄바꿈·스크롤을 사용한다. Fixed Stage, scale observer, 고정 좌표는 사용하지 않는다.
|
||||
|
||||
직원·대표자 카드는 기존 `createEmployeeCard`, 승인 SVG 대형/소형/대표자 자산, 사진 없음 아이콘, Noto Sans KR 및 Typography를 재사용한다. 초기 fixture는 대표자와 기존 부서 ID의 1Depth 그룹 3개, 조직장 3명·직원 6명을 표시한다. 대표자 146×74, `대표이사` / `홍 길 동`, 뱃지·상태·속성 wrapper 0개를 확인했다. 대형 카드 146×68, 소형 카드 108×52도 확인했다. 소형 카드 재사용은 2Depth 자동 계층 배치 완료를 뜻하지 않는다.
|
||||
|
||||
회사 설정 `companyId`는 Auto에서만 `getCompanyColorPreset`을 조회한다. 기본값 `hanmac`; 미등록 ID는 TypeError로 거부한다. Fixed는 회사 옵션을 무시하고 조직별 Gradient를 유지한다. 실제 ID와 Main은 `hanmac` #CB2E28, `saman` #F08300, `jangheon-industry` #4580A6, `jangheon` #1D418D, `ptc` #84338E, `halla` #0068B7, `baron` #0C367F다. 보조 컬러 전체는 기존 프리셋에서 조회하며 노드의 indexed secondary 변수로 연결한다. 기존 프리셋 파일은 변경하지 않았다.
|
||||
|
||||
React는 `layoutMode`·`companyId`를 동일 Core에 전달하고 설정 변경 시 기존 lifecycle 계약으로 인스턴스를 재생성한다. 데이터 검증·JSON Source·엔진·카드·팝업·update/reload/destroy는 Vanilla와 공유한다. React나 Story에 레이아웃 알고리즘을 구현하지 않았다. Auto 초기 preview의 카드 표시 정책과 후속 기능 보류는 명시적이며 `displayMode` 공개 값은 변경하지 않는다.
|
||||
|
||||
## Storybook과 재사용 판단
|
||||
|
||||
- [Expanded OrgChart](http://127.0.0.1:6052/?path=/story/orgchart--expanded-org-chart)
|
||||
- [직접 화면](http://127.0.0.1:6052/iframe.html?id=orgchart--expanded-org-chart&viewMode=story)
|
||||
- 기존 `Logo · Personnel Summary`, `Department Colors`, `Full Composition`을 보존하고 네 번째 공개 화면을 추가했다. 내부 Story ID·태그는 유지했다.
|
||||
- 회사 선택에는 실제 공개 HM `Label`, `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`을 렌더링한다. Label ID와 control의 접근성 이름을 연결했다.
|
||||
- FilterBar 미사용: 목록 검색·조회 조건 요구가 없고 단일 preset 선택만 필요하므로 기존 Select composition을 사용했다.
|
||||
- 페이지 Navigation 미적용: 요청된 Component Storybook preview이며 신규 업무 페이지·GNB/LNB/Dashboard를 만들지 않았다.
|
||||
- 팝업은 기존 Core native dialog를 재사용하며 Enter·Tab·Escape 및 카드 focus 복귀를 확인했다.
|
||||
|
||||
## 변경 범위 통합 검증
|
||||
|
||||
- Core Typecheck·Build: PASS (`npm.cmd run typecheck:org-chart`, `npm.cmd run build:org-chart`)
|
||||
- React Typecheck·Build: PASS (`npm.cmd run typecheck:org-chart-react`, `npm.cmd run build:org-chart-react`)
|
||||
- Storybook 포함 앱 Typecheck: PASS (`node node_modules/typescript/bin/tsc -p tsconfig.app.json --incremental`)
|
||||
- 집중 단위 테스트: **4/4 PASS**, hierarchy 보존·독립 root·기본 호환성·옵션 오류 경계.
|
||||
- 집중 브라우저 검증: **89/89 PASS**, 콘솔 오류·경고 0. [원본 JSON](../../test-results/org-chart-p5-1a/browser.json), [검증 소스](../../examples/org-chart/verify-p5-1a.mjs).
|
||||
- 기존 빌드를 `baseline-dist`에 보존해 Fixed 기본값 및 명시적 Fixed의 카드 좌표·자산·내용·Typography·헤더 Gradient·집계와 팝업을 비교했다. 이번 최소 회귀 범위에서 변화 없음.
|
||||
- 실제 React/Vanilla의 카드·자산·폰트가 일치했다. 7개 회사 선택 후 Main·보조 컬러 연결을 확인했다. Auto update/reload/destroy의 stale DOM·중복 없음, 후속 3Depth 데이터는 보존되며 아직 배치하지 않음을 확인했다.
|
||||
- 실제 신규 Storybook을 390/768/1280/1440px에서 검사했다. 페이지 가로 넘침·카드 겹침 없음; 작은 폭에서 그룹이 일반 흐름으로 줄바꿈된다. 문서·Core viewport의 client/scroll 크기를 원본 JSON에 기록했다.
|
||||
- `git diff --check`: PASS. P5-1A 대부분이 untracked이므로 시작 스냅샷 대비 별도 변경 목록과 공백 검사를 함께 기록한다.
|
||||
- 최종 소스 리뷰: Fixed 분기·lifecycle·CSS 격리·요구 범위에서 수정이 필요한 버그 없음.
|
||||
|
||||
Guard는 최초 통합 Story 검사에서 기존 private Core 참조의 동적 토큰을 해석하지 못해 TOKEN-001 FAIL을 기록했다. 진단을 [보존](../../test-results/org-chart-p5-1a/guard-initial-truncated.txt)했고 기존 코드를 임의 수정하지 않았다. 신규 화면을 독립 public-API Story로 분리한 후 해당 변경 파일만 PARTIAL 검사했다. [최종 Guard](../../test-results/org-chart-p5-1a/guard-expanded.json)는 원본 status **PASS**, staticStatus **PASS**, runtimeStatus **NOT VERIFIED**다.
|
||||
|
||||
CSS styleEvidence: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **NOT VERIFIED**, FINAL **NOT VERIFIED**. [결합 증거](../../test-results/org-chart-p5-1a/guard-combined.json)는 해시 연결 `linked=true`, 정적 PASS이지만 Storybook profile의 Production `--result` bootstrap 증명 및 위치별 CSS 승인 증거가 없어 **NOT VERIFIED**다. styleVerification도 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED다. 실제 브라우저 89 PASS로 원본 Guard나 디자인 의도 승인을 덮어쓰지 않는다. 초기 디자인의 사용자 최종 승인은 미검증이다.
|
||||
|
||||
## 변경 파일과 커밋 준비
|
||||
|
||||
시작 기존 P0~P5-0 변경은 **163개**(tracked 수정 2개 + untracked 161개)였다. 현재 전체 미커밋 파일은 **172개**다. P5-1A는 기존 파일 수정 9개와 신규 9개이며 삭제된 baseline 파일은 없다. [전체·단계별 파일 목록, 의존성, 제외 대상과 추천 커밋 분리](commit-preparation-p5-1a.txt)를 제공한다.
|
||||
|
||||
기존 파일 수정: Core `README.md`, `build.mjs`, `src/index.ts`, `src/core/index.ts`, `src/core/render.ts`, `src/core/types.ts`, `src/styles/org-chart.css`; React `README.md`, `src/OrgChart.tsx`.
|
||||
|
||||
신규: Core `src/core/expanded-content.ts`, `src/core/expanded-layout.ts`, `src/core/expanded-layout.test.ts`, `src/styles/org-chart-expanded.css`; `src/org-chart/org-chart-expanded.stories.tsx`; `examples/org-chart/data-expanded.json`, `examples/org-chart/verify-p5-1a.mjs`; 이 보고서와 `commit-preparation-p5-1a.txt`.
|
||||
|
||||
HEAD에는 두 OrgChart 패키지 자체가 없으므로 P5-1A 파일만 단독 커밋하면 카드·프리셋·Core·wrapper·workspace 의존성이 누락된다. 권장 방식은 저장한 **163개 시작 소스 스냅샷으로 P0~P5-0 baseline 커밋**을 구성하고, 이후 **P5-1A 변경 커밋**을 분리하는 것이다. 현재 untracked 파일을 단순히 경로별 Stage하면 과거 내용과 P5-1A를 분리할 수 없다. 시작 스냅샷·SHA-256 목록은 `test-results/org-chart-p5-1a/baseline-source/`, `baseline-files.json`에 보존했다. 이후 승인 단계에서 정확한 patch·staged diff를 검토해야 한다. 과거 P0/P1별 소유 파일은 Git만으로 확정할 수 없다.
|
||||
|
||||
`dist`, `storybook-static`, `test-results`, `node_modules`, `_local`, log/cache/tsbuildinfo는 커밋에서 제외한다. authored test·verify·asset 생성 소스는 임시 출력과 구분한다. 기존 미커밋 작업 및 과거 증거는 보존했다. **Stage·Commit·Push·Merge는 실행하지 않았다.**
|
||||
|
||||
## 남은 항목과 NOT VERIFIED
|
||||
|
||||
P5-1B: 동일 Depth 가로 배치, 부모 아래 하위 그룹 배치, 재귀 3Depth 이상, 조직 증가 시 재계산, 카드 겹침 방지 알고리즘. 접기·펼치기·Zoom/Pan은 후속 범위다. P5-1A의 일반 그룹 표시를 자동 배치 완성으로 판정하지 않았다.
|
||||
|
||||
전체 과거 P2/P3 suite, Storybook 전체 production build, 임의 조직 데이터·모든 테마·다른 브라우저, 실기기 touch·스크린리더 발화·SSR/운영 연동, 최종 디자인 승인·CSS site별 결합 검증은 NOT VERIFIED다. P5-1B를 자동으로 시작하지 않았으며 사용자 검토를 기다린다.
|
||||
@@ -0,0 +1,103 @@
|
||||
# HMDesign OrgChart P5-2 + P5-3 구현 보고
|
||||
|
||||
RESULT: **PARTIAL** — 요청된 기능 구현·변경 범위 검증은 PASS. 참고 SVG 두 개의 원본 일치와 최종 카드 디자인 승인, 물리적 터치·스크린리더 검증은 NOT VERIFIED.
|
||||
|
||||
## 작업 환경 및 시간
|
||||
|
||||
- 실제 폴더: `D:\디자인시스템\HMDesign-orgchart-package`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- HEAD: `a92913d6bcc950dacc6fdf21cf3b0843ee79416d` (시작/완료 동일)
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- P5-1B 채팅 완료 상태와 미커밋 변경을 확인한 뒤 확장. 시작 시 변경 12파일·신규 검증 스크립트 1개를 보존. 시작 diff는 `test-results/org-chart-p5-23/p5-1b-before.patch`, 비교 dist는 같은 폴더의 `baseline-dist`.
|
||||
- 작업 범위: BOTH — 공통 Core·HTML·React와 요청된 기존 Expanded Story. 운영 프로젝트·main 수정 없음. 신규 페이지 Navigation 없음(임베드 조직도 컴포넌트).
|
||||
- Started At: 2026-10-12 19:27:14 KST
|
||||
- Completed At: 2026-10-12 19:52:30 KST
|
||||
- Actual Duration: 25분 16초
|
||||
- Estimated Duration: 60–90분 (확인 10분 / 구현 35–55분 / 검증 15–25분)
|
||||
- Variance: 최초 예상 하한보다 34분 44초, 상한보다 64분 44초 빠르게 완료.
|
||||
|
||||
## 기능 결과
|
||||
|
||||
| 항목 | 결과·근거 |
|
||||
|---|---|
|
||||
| 회사 전체·단일·복수·해제 | PASS. 독립 Root Forest를 가로 배치. undefined=전체, []=빈 상태. 원본 직원 수 불변, 선택 인원 별도 표시. |
|
||||
| 회사 ID·계층 | PASS. 선택적 `Organization.companyId`, 부모 상속, 다른 회사 부모·자식 연결 오류. 기존 JSON은 알려진 Root leader affiliation 또는 기존 companyId fallback. |
|
||||
| 회사별 최상위 | PASS. 기존 Root·Employee 그대로 사용. 직위·이름·조직명 override, card/titleOnly/hidden, employeeId/null. 예시는 회장·사업총괄·본부장 등과 최상위 직원 없는 바론. 이미지 URL 검증과 표시 경로 지원; 별도 프로필 이미지 변경의 시각 검증은 NOT VERIFIED. |
|
||||
| 그룹 타이틀 | PASS. 전체 타이틀 native button, Enter/Space, aria-expanded, 빈 그룹 disabled. 조직장 유지·직원/하위 조직 숨김. 부모 재확장·회사 재선택 시 유효한 자식 상태 보존. 별도 링크 아이콘으로 이동. |
|
||||
| 재귀 자동 배치 | PASS. dabeng 6.0.0 Forest 유지. 형제 가로 배치, 자신의 부모 아래 자식, 동일 Depth 높이 예약. Small 0–2, Medium 0–3, Large 0–4 Depth. 카드 겹침 0, 실제 DOM parentId 일치. |
|
||||
| Zoom/Pan/Fit | PASS. 기본 5–300%, 설정 min/max/step. 확대/축소/100%/Fit, 현재 배율. Ctrl/Command+휠, 드래그, 방향키. 일반 휠에는 preventDefault하지 않음. 직원 카드 드래그 팝업 억제. 합성 touch PointerEvent 경로 PASS; 물리적 터치는 NOT VERIFIED. |
|
||||
| 회사 테마 | PASS. 기존 7개 ID·메인 색상 모두 정확. P5-0 파일 변경 없음. 보조색 변수 보존. 삼안·장헌산업 제목은 대비를 위해 중립 짙은 텍스트. |
|
||||
| 직원 Border | PASS. 직원 회사 affiliation의 기존 preset(미등록 affiliation은 조직 회사)을 CSS 변수로 참조. 실제 SVG endpoint와 top/left/right 면 일치, 나머지 중립. 3방향 실제 예시 모두 확인. inset accent 두께 변수로 외형 크기 보존. |
|
||||
| 확장형 카드 | PASS(기능). 공통 비원형 사람 아이콘, 실제 이름·직위, Noto Sans KR, 홍길동→홍 길 동. Auto 전용 Width/Height/Padding/Border/Radius/사진/Font/간격 변수. 승인 SVG 재현은 NOT VERIFIED. |
|
||||
| 직원 정보 팝업 | PASS. 공통 Core dialog, 변환 Forest 밖, focus·Escape 복귀. 표시 override에도 원본 직원 identity·callback 데이터 유지. |
|
||||
| Fixture | PASS. Small 20명·3회사, Medium 100명·5회사, Large 300명·7회사. 고유 Employee ID, 유효 parentId·leader membership, 중복 없는 조직장 집계. 고정 JSON·결정적 생성 스크립트. 실제 개인정보 없음. |
|
||||
| HTML·React | PASS. 실제 HTML 예시와 StrictMode React Story의 ID·직위·이름·크기·회사·색·방향 일치. 필터 로직은 Story가 아닌 Core. DataSource 계약 유지, API/DB 구현 없음. |
|
||||
| Fixed | PASS. P5-1B 시작 dist와 현재 dist의 카드 좌표·크기·SVG·이름·직위·색 정확 비교 및 공통 팝업 최소 회귀. Fixed CSS·SVG·Full Composition Story 변경 없음. |
|
||||
| Storybook | PASS. 사용자 메뉴 4개 유지. Expanded 하나에 20/100/300명·회사 체크박스·전체/해제 및 공통 확대 도구. |
|
||||
|
||||
## 실제 사용 요소·선택 근거
|
||||
|
||||
React Story는 공개 `@hm-design/ui`의 Button, Checkbox, Label, Select/Trigger/Value/Content/Item을 실제 렌더링한다. Label은 실제 control ID와 연결한다. FilterBar를 검토했으며 이번 도구는 검색·조회 조건이 아니라 Fixture 전환과 즉시 회사 표시 상태이므로 기존 Pattern의 목록 검색 구조를 강제하지 않았다. 체크박스 복수 선택과 Select를 직접 조합한다.
|
||||
|
||||
공통 Core는 React 의존성 없는 명시적 HTML Runtime adapter다. 그룹·Zoom native button과 HTML 예시의 native select/checkbox/label은 이 계약의 접근성 요소이며 HM React UI 모방이 아니다. Core·Wrapper·DataSource 역할을 유지한다.
|
||||
|
||||
## 성능 측정
|
||||
|
||||
Windows 로컬 Headless Chrome, 1440×1000, 실제 300명 JSON. 아래는 측정치이며 임의 성능 기준에 따른 PASS 판정이 아니다.
|
||||
|
||||
| 처리 | 실측 |
|
||||
|---|---:|
|
||||
| Small 최초 mount (모듈·CSS + 2 animation frames) | 93.3ms |
|
||||
| Medium 최초 mount (동일 방식) | 93.9ms |
|
||||
| Large 최초 mount (동일 방식) | 195.6ms |
|
||||
| 300명 그룹 접기 (native click + 2 frames, 두 측정) | 222.7 / 281.9ms |
|
||||
| 자동화 Enter/Space 접기·펼치기 (조작·focus·프레임 대기 포함) | 317.9–656.8ms |
|
||||
| 300명→단일 회사 선택 + 재배치 2 frames | 42.0ms |
|
||||
| Zoom + 60회 panBy + 1 frame (두 측정) | 20.5 / 0.9ms |
|
||||
|
||||
대용량 전체 DOM 누락·중복·카드 겹침 없음. 위 pan 일괄 호출은 실기기 프레임율/FPS를 뜻하지 않는다. 최초 mount 수치에는 초기 스크립트 경로 비용이 포함되며 다운로드 성능 인증이 아니다. 별도 가상화·엔진 교체·신규 라이브러리 설치 없음.
|
||||
|
||||
## 변경 범위 검증
|
||||
|
||||
- Core typecheck PASS; Core ESM/IIFE 및 선언 build PASS.
|
||||
- React Wrapper typecheck·선언/ESM build PASS; 앱/Story TypeScript 검사 PASS.
|
||||
- Expanded Story만 소비하는 독립 React bootstrap의 Vite production build PASS. 500kB 초과 chunk 경고 있음(기존 Core 이미지 자산 포함); 빌드 오류 아님. 결과: `test-results/org-chart-p5-23/react-build/dist/index.html`.
|
||||
- 영향 범위 단위 테스트 **7/7 PASS**. Node 20의 globSync 부재로 최초 실행 불가였으며, 제공된 Node 런타임으로 정상 실행. 기존 전체 테스트를 반복하지 않음.
|
||||
- 브라우저 **96/96 PASS**, 신규 console/runtime error 0. 실행기 선택자 오류를 수정하고 미완료 범위만 이어서 확인. 390/768/1280/1440px 가로 넘침·핵심 도구 확인.
|
||||
- `git diff --check` PASS. 원래 CRLF 전환 안내는 공백 오류와 구분.
|
||||
- 원본 Guard: PARTIAL/STORYBOOK profile, staticStatus **PASS**, 원본 status **PASS**, issues 0. Runtime은 자체 수행하지 않으므로 **NOT VERIFIED**.
|
||||
- CSS 원본 styleEvidence: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **NOT VERIFIED**, FINAL **NOT VERIFIED**.
|
||||
- 별도 browser/runtime 증거 연결: fingerprint 일치 linked=true. 결합 status와 styleVerification은 **NOT VERIFIED** 유지. STORYBOOK profile에는 Production `--result` bootstrap 판정이 없으며 최종 디자인 승인·개별 CSS 적용 전 상태 검증도 미완료. 직접 실행된 React 브라우저 결과 PASS와 혼동하지 않음.
|
||||
- Guard는 Story host 소비 범위이며 Core 내부의 모든 CSS를 승인하는 검사가 아니다. Core 연결 방향·색상·좌표는 별도 브라우저 근거에 기록.
|
||||
- P5-1B 879/879 PASS 기록 보존. 과거 전체 회귀·기존 미검증 결과를 새 결과로 덮어쓰지 않음.
|
||||
|
||||
자동 승인 검토는 `0.0.0.0` Storybook 실행을 LAN 노출 위험으로 거부했다. `127.0.0.1`로만 바인딩한 안전한 실행은 승인·검증됐으며, 초기 sandbox 서버는 종료했다. 기능·작업에 남은 승인 차단 없음.
|
||||
|
||||
## 실행 URL·캡처·원시 근거
|
||||
|
||||
- [Expanded OrgChart](http://127.0.0.1:6054/?path=/story/orgchart--expanded-org-chart)
|
||||
- [HTML 예시](http://127.0.0.1:4189/expanded.html)
|
||||
- `test-results/org-chart-p5-23/browser.json`, `guard.json`, `runtime.json`, `combined.json`
|
||||
- `storybook-large-1440.png`: 7회사·300명 Fit 전체 화면.
|
||||
- `storybook-small-hanmac-100.png`: 한맥기술·100%·상단/좌측/우측 Border와 연결선.
|
||||
- `storybook-{390,768,1280,1440}.png`: 반응형 실제 React 화면.
|
||||
- `html-{small,medium,large}-fit.png`, `fixed-1440.png`: HTML Fixture 및 최소 Fixed 회귀.
|
||||
|
||||
## 이번 추가·수정 파일
|
||||
|
||||
기존 P5-1B 변경 전체를 이번 변경으로 주장하지 않는다. P5-1B의 `data-expanded.json`, `expanded-layout.test.ts`, `verify-p5-1b.mjs`는 그대로 보존했다.
|
||||
|
||||
- Core: `packages/org-chart/src/core/{index,types,render,employee-card,expanded-content,expanded-layout}.ts`; 신규 `expanded-company.ts`, `expanded-company.test.ts`, `expanded-viewport.ts`.
|
||||
- 데이터 계약: `packages/org-chart/src/data/{types,validate}.ts`, `packages/org-chart/src/index.ts`, `docs/org-chart/data-contract.md`.
|
||||
- 스타일: `packages/org-chart/src/styles/org-chart-expanded.css`.
|
||||
- React: `packages/org-chart-react/src/OrgChart.tsx`.
|
||||
- Story: `src/org-chart/org-chart-expanded.stories.tsx`, `org-chart-stories.css`.
|
||||
- HTML/Fixture: `examples/org-chart/prepare.mjs`; 신규 `expanded.html`, `expanded-app.js`, `expanded.css`, `create-expanded-fixtures.mjs`, `data-expanded-small.json`, `data-expanded-medium.json`, `data-expanded-large.json`, `verify-p5-23.mjs`.
|
||||
- 문서: `packages/org-chart/README.md`, `packages/org-chart-react/README.md`, 이 보고서.
|
||||
- generated dist·검증 JSON·스크린샷·비교본은 기존 gitignore의 test-results/dist 경로. 소스 Stage 없음.
|
||||
|
||||
## 남은 미검증·Git 상태
|
||||
|
||||
참고 `Group 3391.svg`, `Group 1171288761.svg`의 원본 구조·최종 디자인 일치, 물리적 터치, 스크린리더 음성, 다른 브라우저·운영 소비자, 전체 heap/GC 회수, 전체 과거 회귀는 NOT VERIFIED. 최상위 profileImage 변경의 별도 시각 검증도 NOT VERIFIED. 기존 미검증을 기능 PASS로 제거하지 않았다.
|
||||
|
||||
**Commit·Push·Merge·Stage 미진행. Reset/Rebase/Force Push/Clean 미진행. main·운영 프로젝트·Fixed SVG/CSS 미수정. P5-4 미시작.**
|
||||
@@ -0,0 +1,123 @@
|
||||
# HMDesign OrgChart P5-4 완료 보고
|
||||
|
||||
RESULT: **PASS — 변경된 P5-4 요구사항의 구현·변경 영향 범위.**
|
||||
전체 접근성 인증·실기기·모든 CSS 의도 검증은 **PARTIAL / NOT VERIFIED**이며 아래에 구분한다.
|
||||
|
||||
## 확정 범위와 환경
|
||||
|
||||
- 실제 작업 폴더: `D:\디자인시스템\HMDesign-orgchart-package`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD: `a92913d6bcc950dacc6fdf21cf3b0843ee79416d`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- 범위: BOTH — 기존 Vanilla/React 공통 Core와 Expanded Story의 안내·문서 보완.
|
||||
- 사용자 후속 확정: 현재 Expanded 카드 외형·크기·사진·직위·이름 구조를 유지한다.
|
||||
`Group 3391.svg`, `Group 1171288761.svg`는 검색·적용·용도 배정 대상에서 제외하며,
|
||||
SVG 미확보는 완료 차단 사유가 아니다. SVG 역할을 추정하지 않았다.
|
||||
- 기존 P5-1B 879/879 및 P5-2·3 96/96·단위 7/7 기록은 과거 근거로 보존한다.
|
||||
이번 결과는 그 전체 검사를 다시 실행했다는 의미가 아니다.
|
||||
|
||||
## 구현과 보존 결과
|
||||
|
||||
| 항목 | P5-4 결과 |
|
||||
|---|---|
|
||||
| 카드 외형·크기 | PASS. 시작 dist와 Small 20명의 카드 size/photo/font/color/border 정확 비교 일치. 기본 일반 직원 width 114/min-height 54px, 긴 가상 이름 실측 114×67px. 조직장·최상위 width 154/min-height 72px. 변경 전후 동일. |
|
||||
| 사진·직위·이름 | PASS. 비원형 36×36 슬롯·공통 사람 SVG·Noto Sans KR computed family·직위 10px/이름 13px 유지. 표시용 직위 공백만 제거, raw 데이터 불변·원문 aria-label 제공. 가상 SVG profile URL override와 error-event placeholder 복귀 확인. 실제 사람 사진은 미사용. |
|
||||
| 그룹 타이틀 | PASS. 기존 전체 버튼·상태 기호·aria-expanded·키보드 Focus·별도 링크 유지. 활성 타이틀 및 링크에 Hover 배경 피드백 추가. Enter 접힘·Space 펼침·rerender 후 focus 복귀 확인. |
|
||||
| 회사 컬러·Border | PASS. 기존 7개 preset·회사 ID 불변. top/left/right 실제 endpoint 오차 1px 미만, 강조 한 면·나머지 중립·외형 크기 유지. 연결 없는 카드용 `none`은 지원 API가 아니며 별도 PASS 처리하지 않음. |
|
||||
| 자동 배치·연결선 | PASS. 기존 dabeng 재귀 엔진·실측 ResizeObserver 유지. Medium/Large 직원 중복·누락·카드 겹침 없음, DOM 부모 관계와 부모 아래 자식 확인. 엔진 재작성·간격/색/크기 재작업 없음. |
|
||||
| 다중 회사 | PASS. Large에서 두 회사 독립 Root 및 단일 선택 확인. 원본 JSON 및 source 인원 불변. 전체/해제·자식 collapse state 보존 등 변경 없는 상세 동작은 P5-2·3 근거 재사용. |
|
||||
| Zoom/Pan/Fit | PASS. 기존 도구와 배율 범위 유지. 새 Tab focus가 화면 밖 카드/타이틀에 도달하면 배율 유지·Pan으로 노출, hidden scrollLeft/Top 0 확인. 실제 mouse Pan 및 Medium/Large Fit에 전체 카드 포함 확인. |
|
||||
| 드래그·키보드 | PASS. 드래그의 pointer click 억제가 다음 Enter/Space 활성화를 막지 않도록 수정. 이후 Space 직원 팝업 활성화 확인. 기존 Pan 알고리즘 재구현 없음. |
|
||||
| 직원 팝업 | PASS. 직원 이름·직위의 의미 있는 카드 이름, dialog 제목·닫기 이름, forest 밖·transform 없음, focus 내부·ESC 복귀 확인. 기존 공통 native dialog 재사용. |
|
||||
| 20/100/300명 | PASS. 기존 JSON 그대로 사용. Medium/Large HTML의 고유 인원·전체 Fit, React Small 및 Large 300명 선택·겹침 없음 확인. 새 fixture·실제 개인정보 없음. |
|
||||
| HTML/React | PASS. Small의 카드 identity·size·photo·표시 텍스트·font-size·중립 border 동일. 실제 Story와 production React bootstrap 모두 같은 Core 실행. 별도 React 엔진 없음. |
|
||||
| Fixed | PASS(최소 회귀). 시작 dist와 카드 좌표·size·SVG asset·직위·이름·컬러·사진 정확 일치. Auto 토글 0, 공통 팝업 최소 회귀 확인. Fixed CSS·SVG·Full Composition Story 미수정. |
|
||||
| Storybook | PASS. 사용자용 4개 메뉴 유지. 기존 internal Story는 사용자 메뉴 집계에서 제외. 안내의 SVG 미확보 문구 제거·P5-4 접근성 이름 갱신. 신규 Story 없음. |
|
||||
|
||||
## 접근성·검증 범위
|
||||
|
||||
- 실제 키보드 자동화: 그룹 Focus outline·Enter/Space·직원 Space·Tab으로 화면 밖 요소 표시·ESC/Focus 복귀 **PASS**.
|
||||
- DOM/ARIA: 타이틀 이름·펼침 상태·카드 직원 정보 이름·dialog 제목·닫기 버튼·React 회사 Checkbox 및 데이터 Select 이름 **PASS**.
|
||||
- 터치: Chrome mobile/hasTouch 에뮬레이션의 title tap, CDP touch Pan, card tap 팝업·닫기 **PASS**. 실제 모바일 기기는 **NOT VERIFIED**.
|
||||
- 실제 스크린리더 발화, 다른 브라우저 엔진·운영 소비자, 전체 과거 회귀, 종합 자동 접근성 audit는 **NOT VERIFIED**.
|
||||
- Noto는 computed family 및 기존 host font-face 로딩 확인이다. 실제 font glyph의 CDP 재측정은 하지 않았으며 P3.1 검증 기록을 보존한다.
|
||||
- 390/768/1280/1440px: 실제 React 문서 가로 overflow 없음·카드 20명 유지·hidden viewport scroll 0 **PASS**.
|
||||
문서 및 viewport scrollHeight/clientHeight 원시 값은 `browser.json`에 보존한다.
|
||||
- 100%에서 큰 조직도가 viewport 밖으로 이어지는 것은 기존 Pan 탐색 동작이며, 전체 표시 여부는 Fit 상태에서 판정했다.
|
||||
- 기존 조직 ID는 dataset·ARIA 동작 대상에 유지된다. 이번 화면은 개발용 조직 ID 텍스트를 표시하지 않으므로 ID 한 줄 표시 검사는 NOT APPLICABLE.
|
||||
- 빈 그룹 disabled는 기존 구현을 유지했다. disabled/readOnly의 모든 입력 조합 재검사는 NOT VERIFIED.
|
||||
|
||||
기존 실제 React UI는 `@hm-design/ui` Button/Checkbox/Label/Select를 사용한다.
|
||||
이 작업은 fixture/즉시 회사 표시 도구이며 FilterBar 목록 검색 구조를 추가할 필요가 없어
|
||||
P5-2·3의 미사용 근거를 유지한다. 페이지 Navigation 없는 임베드 컴포넌트이며 신규 Navigation 없음.
|
||||
Core native button/dialog는 명시적 Vanilla adapter 계약이다.
|
||||
|
||||
## 최종 통합 검증
|
||||
|
||||
- Core Typecheck 및 ESM/IIFE·선언 Build **PASS**.
|
||||
- React Wrapper Typecheck·선언/ESM Build 및 앱/Story TypeScript **PASS**.
|
||||
- 기존 Expanded Story를 소비하는 별도 production React Build **PASS**.
|
||||
기존 이미지 자산에 의한 500kB 초과 chunk 경고는 유지하며 빌드 오류와 구분한다.
|
||||
- 변경 영향 브라우저 **46/46 PASS**, 마지막 세 실행 경로 console/runtime errors **0**.
|
||||
초기 검사 runner 오류(부모 중심 범위의 잘못된 제한·검증 URL data scheme·internal Story 집계·자산 라우팅·favicon 404)를 수정했다.
|
||||
초기 실패 JSON을 보존하고 성공 25개→45개를 재사용하여 실패·미완료만 확인했다.
|
||||
제품 코드 FAIL을 숨기거나 과거 Guard 결과를 바꾸지 않았다.
|
||||
- 기존 단위 7/7 및 과거 전체 브라우저 suite는 **재실행하지 않음**. DOM 동작 변경은 위 통합 브라우저로 검증.
|
||||
- `git diff --check` **PASS**. CRLF 전환 안내는 공백 오류가 아니다.
|
||||
- Guard는 변경한 Expanded 소비 Story에 PARTIAL/STORYBOOK profile로 **1회** 실행.
|
||||
원본 `status=PASS`, `staticStatus=PASS`, issues=0, `runtimeStatus=NOT VERIFIED`.
|
||||
- 원본 styleEvidence: STATIC **PASS** / RUNTIME **NOT VERIFIED** / DESIGN_INTENT **NOT VERIFIED** / FINAL **NOT VERIFIED**.
|
||||
- 별도 Runtime 증거 fingerprint 연결 `linked=true`; 결합 status **NOT VERIFIED**.
|
||||
결합 styleVerification도 STATIC **PASS**, 나머지 세 항목 **NOT VERIFIED** 유지.
|
||||
Storybook profile에는 Production `--result` 증명이 없고 전체 host CSS 의도 검증을 수행하지 않는다.
|
||||
실제 production React 빌드·브라우저 PASS와 Guard 자동 판정을 구분한다.
|
||||
- 현재 카드 유지의 사용자 확정은 모든 소비 CSS·외부 입력 상태의 포괄적 승인으로 확대하지 않는다.
|
||||
- 카드 geometry·엔진이 동일하므로 성능을 재측정하지 않았다. 이전 195.6ms/42ms는 다른 작업의 조건부 기록으로 보존한다.
|
||||
|
||||
## 실행 화면·캡처·문서
|
||||
|
||||
- [Expanded OrgChart](http://127.0.0.1:6054/?path=/story/orgchart--expanded-org-chart)
|
||||
- [Vanilla HTML 예시](http://127.0.0.1:4189/expanded.html)
|
||||
- production React: `test-results/org-chart-p5-4/react-build/dist/index.html` (브라우저 검증 중 로컬 서버로 실행, file:// 직접 실행용 아님).
|
||||
- `storybook-large.png`: 기존 7회사·300명 전체 Expanded 화면.
|
||||
- `cards-100-percent.png`: 카드·상단/좌측 강조 연결선 100% 탐색 화면.
|
||||
- `group-collapsed.png` / `group-expanded.png`: 동일 그룹 접힘·펼침 비교.
|
||||
- `two-companies.png`: 2회사 독립 Root.
|
||||
- `html-large-fit.png` / `html-medium-fit.png`: 300/100명 HTML Fit.
|
||||
- `storybook-{390,768,1280,1440}.png`: 4개 폭 실제 React 화면.
|
||||
- `touch-emulation.png`, `fixed-1440.png`: 터치 에뮬레이션·Fixed 최소 회귀.
|
||||
- 캡처와 원시 근거는 모두 `test-results/org-chart-p5-4/`. 실제 직원 개인정보 없음.
|
||||
- [사용 예제 및 스타일 관리](expanded-usage.md): Fixed/Auto, 컬러, 단일/복수 회사,
|
||||
collapse, Zoom/Pan, JSON Source, popup, Shadow DOM CSS 변수 및 연결 방향을 실제 공개 API로 설명.
|
||||
없는 collapse setter·layoutConfig·React imperative ref를 지원한다고 문서화하지 않음.
|
||||
|
||||
## 이번 실제 변경 파일
|
||||
|
||||
시작 시 미커밋 32개 파일을 snapshot/hash로 기록했다. 아래 기존 파일 7개만 이번에 추가 보완했으며,
|
||||
나머지 25개는 시작 hash와 정확히 일치한다. 기존 미커밋 변경을 이번 신규 구현으로 주장하지 않는다.
|
||||
|
||||
1. `packages/org-chart/src/core/employee-card.ts` — Auto 직위 표시·원문 접근성 이름.
|
||||
2. `packages/org-chart/src/core/expanded-viewport.ts` — 화면 밖 focus 표시·drag 이후 keyboard click·frame cleanup.
|
||||
3. `packages/org-chart/src/core/expanded-content.ts` — current presentation 상태.
|
||||
4. `packages/org-chart/src/styles/org-chart-expanded.css` — Auto title/link Hover·radius 상속, 기준 주석.
|
||||
5. `src/org-chart/org-chart-expanded.stories.tsx` — P5-4 이름·SVG 안내 제거.
|
||||
6. `packages/org-chart/README.md` — 현재 카드 기준·접근성·문서 링크.
|
||||
7. `packages/org-chart-react/README.md` — 동일 기준·공통 Core 보완 설명.
|
||||
8. 신규 `docs/org-chart/expanded-usage.md` — 사용·스타일·지원 경계.
|
||||
9. 신규 `examples/org-chart/verify-p5-4.mjs` — 변경 영향 검증 runner.
|
||||
10. 신규 이 보고서.
|
||||
|
||||
과거 보고·fixture·공개 타입/API·회사 preset·자동 레이아웃 엔진·Fixed 자산은 이번에 변경하지 않았다.
|
||||
검증 산출물·시작 patch/dist/hash는 기존 ignored `test-results`에 저장했다.
|
||||
|
||||
**Stage·Commit·Push·Merge·PR 미진행.** 시작/완료 HEAD 동일, staged diff 없음.
|
||||
Reset·Rebase·Force Push·Clean 없음. 운영 main·다른 프로젝트 수정 없음.
|
||||
|
||||
## 작업 시간 (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-12 21:23:35 KST
|
||||
- Completed At: 2026-10-12 21:41:32 KST
|
||||
- Actual Duration: 17분 57초
|
||||
- Estimated Duration: 최초 45–60분 (자료 확인 10분 / 구현 20–30분 / 검증·보고 15–20분)
|
||||
- Variance: 예상 하한보다 27분 03초, 상한보다 42분 03초 짧음.
|
||||
- 시간 차이는 사용자 확정으로 SVG 재작업이 제외되고 기존 PASS를 재사용한 결과다.
|
||||
@@ -0,0 +1,101 @@
|
||||
# HMDesign OrgChart P6.1 — 연결선 접근성 및 배포 품질 보완
|
||||
|
||||
RESULT: **PASS** (요청한 P1 1건·P2 2건의 변경 범위)
|
||||
|
||||
Auto 관계선 대비, 과거 브라우저 runner의 도구 경로, 두 배포 README의 링크를 수정했다. Fixed CSS·Core/React API·레이아웃 알고리즘·회사 프리셋·package.json·lockfile은 변경하지 않았다. 전체 운영 배포 인증 또는 기존 미검증 영역 승인으로 확대하지 않는다.
|
||||
|
||||
## 작업 환경과 Git
|
||||
|
||||
- 저장소: `D:\디자인시스템\HMDesign-orgchart-package`
|
||||
- 범위: BOTH의 기존 공통 Core Auto CSS 및 검증 도구·패키지 문서. 신규 UI/Story 없음.
|
||||
- 브랜치 및 시작/종료 HEAD: `feature/orgchart-package`, `d2b76e766466e1f156f6a0f8f63710e023af900e`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 읽기 전용 `git ls-remote`의 feature SHA도 일치했다. 최초 sandbox 네트워크 오류 후 허용된 실행 환경에서 확인했다.
|
||||
- Git 작성자 유지: `b25027` / `shkim4@samaneng.com`
|
||||
- 기존 미커밋 파일: untracked `docs/org-chart/quality-diagnosis-p6.md` 1개. 수정하지 않았다. 최종 SHA-256: `224aa06880b9cad222f510b6e633d5b0faccece742f720584fd2b42912cdb10f`.
|
||||
- 최종 Working Tree: 기존 추적 파일 23개 수정, 이번 신규 문서 2개·실행 도우미 1개, 기존 untracked P6 보고서 1개. 실행기·로그·캡처·Tarball은 ignored `test-results/org-chart-p6-1/`에 보존했다.
|
||||
- Staged diff 없음. **Stage·Commit·Push·Merge·PR·main Checkout·Reset·Rebase·Clean·Force Push 미진행.**
|
||||
|
||||
## 수정 결과
|
||||
|
||||
| 항목 | 수정 및 검증 결과 |
|
||||
|---|---|
|
||||
| P1 Auto 대비 | Auto 전용 `--hm-orgchart-expanded-line-color: #738399`. 부모/자식 pseudo 관계선과 직원 SVG path만 참조한다. 이전 computed `rgb(186, 199, 215)` → `rgb(115, 131, 153)`. 흰 배경 **1.7159036307:1 → 3.8635120805:1**. |
|
||||
| 배경·상태 | browser computed 배경/색상·opacity·background image/filter/blend를 기록하고 투명 배경을 합성했다. 기본 실제 관계선 배경은 흰색이다. 기존 light surface override `rgb(240, 241, 242)`도 **3.4163257790:1**. Zoom/Pan 좌표 probe가 반환한 content 배경 `rgb(244,247,251)`의 보수적 비율은 3.5956293943:1이며, 이 probe에는 viewport 밖의 clipped 좌표도 포함될 수 있다. 최소 확인 비율은 3.4163:1. 임의 외부 배경 재정의는 별도 검증이 필요하다. |
|
||||
| 기준 | 내용을 전달하는 그래픽의 인접 색상 대비 **3:1 이상 충족**. [W3C SC 1.4.11](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html). Anti-aliasing/전체 Fit의 축소는 기존 상태이며 굵기를 변경하지 않았다. |
|
||||
| 기존 스타일 | 같은 페이지에서 수정 전 공개 CSS → 수정 후 CSS를 교체하여 Fixed 전체 DOM의 관련 computed 스타일 및 `::before/::after`를 비교했다. Fixed 연결선·협업 화살표·카드 스타일 불변. Auto 직원 path 좌표·굵기, 카드 offset/크기·Border·회사 accent 불변. |
|
||||
| P2 실행 경로 | Core 17개·React 2개 runner가 공통 `scripts/orgchart-browser-runtime.mjs`를 사용한다. 기본은 표준 프로젝트 `playwright` 모듈 및 관리 Chromium. 기존 외부 모듈·브라우저는 `ORGCHART_PLAYWRIGHT_MODULE` / `ORGCHART_CHROME`으로 선택형 지정. 하드코딩된 개인 경로·고정 Chrome 경로 제거. 원래 모듈/실행 오류를 cause와 함께 안내한다. |
|
||||
| 새 PC | 경로 이식성·선택형 환경변수·오류 메시지 검증 PASS. 이 PC의 프로젝트에는 Playwright가 없어 이미 설치된 외부 모듈과 Chrome을 환경변수로 지정하여 실제 실행했다. 신규 설치/업그레이드 없음. **실제 다른 PC의 fresh install과 실행은 NOT VERIFIED**. 기존 fixture·baseline·서버 origin 전제는 유지한다. [실행 안내](browser-verification.md). |
|
||||
| P2 README | Core 4개, React 3개 링크(타 패키지 README 상대링크 포함), 총 7개를 기존 문서가 존재하는 공식 Gitea **고정 Commit URL**로 교체했다. 문서 복사·배포 파일 구조 변경 없음. |
|
||||
| Tarball | Build 후 `npm pack --ignore-scripts`로 실제 2개 tgz 생성·추출. README의 깨진 상대 링크 0개, 외부 URL 형식/고정 Commit의 Git blob 존재 7/7. 공개 export 대상 파일과 README named import/type export 일치. Gitea URL의 HTTP 접근 권한은 별도 NOT VERIFIED(저장소 권한 필요 가능). |
|
||||
|
||||
## 검증 결과와 재사용
|
||||
|
||||
| 검증 | 결과·범위 |
|
||||
|---|---|
|
||||
| Core Typecheck / Build | PASS / PASS, 기존 workspace 명령 사용 |
|
||||
| React Typecheck / Build | PASS / PASS, 공통 Core 공개 선언 기반 |
|
||||
| 변경 범위 브라우저 | **17/17 PASS**, 실제 Chrome. 300명·7회사, 부모/자식 3종 및 직원 관계선 computed 대비, 제목 Hover, Enter 접기/Space 펼치기, 회사 단일/복수 선택, UI Zoom·실제 pointer Pan·Fit, 기존 light background override 포함. 콘솔/page/request 오류 0. |
|
||||
| 실행기·패키지 감사 | **87/87 PASS**. 19개 runner 및 공통 helper syntax, 도구 연결 변경을 제외한 원래 시나리오/Assert/기대값의 텍스트 동일성, PC 경로 제거, 기본/선택형 도구 설정과 명확한 실패, Tarball 링크·공개 export/import/type, `git diff --check`. 실제 과거 전체 suite 실행 수가 아니다. |
|
||||
| 기존 P6 브라우저 | 59/59 PASS 재사용. 전체 재실행 없음. |
|
||||
| 기존 패키지·CSS | Export/Tarball/타입 16/16, CSS 격리 9/9, Noto Sans KR PASS 재사용. 이번 pack 링크 검사는 변경된 README에 대한 보완이며 과거 증거를 덮어쓰지 않았다. |
|
||||
| 기존 Guard | Static 및 연결 Runtime PASS 재사용. 제품 bootstrap/import/Navigation 계약 변경 없이 패키지 내부 CSS·도구·README만 수정했다. 기존 consumer Guard 전체 반복 없음. |
|
||||
| Guard CSS 범위 | 기존 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**를 유지한다. 이번 연결선 computed 및 Fixed 영향 검증 PASS를 Core 전체 CSS/동적 입력/디자인 의도 승인으로 확대하지 않는다. 원본 RENDER-002 NOT VERIFIED 유지. |
|
||||
|
||||
초기 browser 실행의 `ERR_NETWORK_ACCESS_DENIED`는 sandbox localhost 제한이었다. 초기 로그/JSON을 별도 보존하고 동일 실행기를 localhost 접근 가능한 권한으로 실행했다. 성공한 과거 suite를 반복하거나 기대값을 수정하지 않았다. 신규 제품 FAIL 0건.
|
||||
|
||||
근거: [Build](../../test-results/org-chart-p6-1/build-results.json), [브라우저 raw](../../test-results/org-chart-p6-1/browser.json), [대비 raw](../../test-results/org-chart-p6-1/contrast.json), [감사](../../test-results/org-chart-p6-1/audit.json), [Pack 목록](../../test-results/org-chart-p6-1/pack.json), [캡처](../../test-results/org-chart-p6-1/auto-after.png), [Git 상태](../../test-results/org-chart-p6-1/final-state.json). 이 파일들의 기존 test-results 정책은 유지하며 Git Stage하지 않았다.
|
||||
|
||||
## 변경 파일
|
||||
|
||||
제품·패키지·문서:
|
||||
|
||||
- `packages/org-chart/src/styles/org-chart-expanded.css`
|
||||
- `packages/org-chart/README.md`
|
||||
- `packages/org-chart-react/README.md`
|
||||
- `docs/org-chart/expanded-usage.md`
|
||||
- `scripts/orgchart-browser-runtime.mjs` (신규)
|
||||
- `docs/org-chart/browser-verification.md` (신규)
|
||||
- `docs/org-chart/implementation-report-p6.1.md` (신규)
|
||||
|
||||
도구 경로만 변경한 Core runner 17개:
|
||||
|
||||
- `examples/org-chart/verify.mjs`
|
||||
- `examples/org-chart/verify-p1.mjs`
|
||||
- `examples/org-chart/verify-p2.mjs`
|
||||
- `examples/org-chart/verify-p3.mjs`
|
||||
- `examples/org-chart/verify-p3.1.mjs`
|
||||
- `examples/org-chart/verify-p3.2.mjs`
|
||||
- `examples/org-chart/verify-p5-1a.mjs`
|
||||
- `examples/org-chart/verify-p5-1b.mjs`
|
||||
- `examples/org-chart/verify-p5-23.mjs`
|
||||
- `examples/org-chart/verify-p5-4.mjs`
|
||||
- `examples/org-chart/verify-card-content.mjs`
|
||||
- `examples/org-chart/verify-card-design.mjs`
|
||||
- `examples/org-chart/verify-final-card-check.mjs`
|
||||
- `examples/org-chart/verify-final-logo-check.mjs`
|
||||
- `examples/org-chart/verify-final-status-check.mjs`
|
||||
- `examples/org-chart/verify-quality-boundaries.mjs`
|
||||
- `examples/org-chart/verify-quality-p1.mjs`
|
||||
|
||||
React runner 2개:
|
||||
|
||||
- `examples/org-chart-react/verify.mjs`
|
||||
- `examples/org-chart-react/verify-boundaries.mjs`
|
||||
|
||||
## 남은 결함과 미검증
|
||||
|
||||
이번 요청의 배포 전 결함은 P1 1건·P2 2건 모두 해결했다. **이번 확인 범위의 남은 P0/P1/P2: 0/0/0**, 신규 회귀 결함 0. 과거 Fixed 대비 REVIEW와 오래된 테스트 FAIL을 새 PASS로 재분류하지 않았다.
|
||||
|
||||
계속 NOT VERIFIED: Guard 전체 CSS Runtime/디자인 의도 및 임의 동적 입력, 실제 화면 리더·물리 터치·타 브라우저, 전체 역사 회귀/과거 FAIL 전체 재분류, 모든 Fixed 폭·전체 픽셀 일치, 임의 데이터/override/접힘 전 조합, 실제 새 PC fresh install, 실제 운영 base/font/CSP/origin·배포·SSR/hydration·실제 API. 390/768/1280/1440 기존 P6 결과를 재사용했으며 이번에는 1440px의 변경 범위 시나리오를 확인했다.
|
||||
|
||||
Stage·Commit·Push는 사용자 검토 후 별도 요청으로 진행한다.
|
||||
|
||||
## 작업 시간
|
||||
|
||||
- Started At: 2026-10-12 22:33:57 KST
|
||||
- Completed At: 2026-10-12 22:43:52 KST
|
||||
- Actual Duration: 9분 55초
|
||||
- Estimated Duration: 최초 35~45분
|
||||
- Variance: 예상 하한보다 25분 5초, 상한보다 35분 5초 짧음
|
||||
|
||||
준비된 기존 의존성/브라우저, 공통 실행 도우미, P6 정상 기록 재사용과 변경 범위 검사로 예상보다 빠르게 완료했다.
|
||||
@@ -0,0 +1,188 @@
|
||||
# HMDesign OrgChart P7 구현·검증 보고
|
||||
|
||||
RESULT: **PASS — P7 변경 범위.** 전체 접근성·CSS 디자인 의도·운영 배포 승인으로 확대하지 않는다.
|
||||
TB/LR/Hybrid 독립 모드, 데이터 선택, 선택형 HM Dialog를 구현했다. 최신 dabeng LR 예제 비교에서 추가 기능 수정이 필요한 결함은 발견하지 않았다.
|
||||
Compact는 **현재 Auto adapter에서 NOT SUPPORTED**이며 요구사항의 허용된 제외 사유를 아래에 명시한다.
|
||||
|
||||
## 요청 항목별 결과
|
||||
|
||||
| 번호 | 항목 | 결과와 근거 |
|
||||
|---|---|---|
|
||||
| 1 | 폴더·브랜치·HEAD | `D:\디자인시스템\HMDesign-OrgChart` / `feature/orgchart-package` / `7614507583927a25a270963a74ff044be0557b9f`. origin `https://gitea.hmac.kr/b25027/HMDesign.git`. 시작·완료 HEAD 동일. |
|
||||
| 2 | 작업 시간 | 아래 KST 시간 기록 참조. 최초 예상 60–90분. |
|
||||
| 3 | 전체 합산 20/100/300 | **PASS**. 모든 7회사, 고유 Employee ID 기준 정확히 20/100/300. 가중 분배 후 선택 회사만 필터링하며 재분배하지 않음. |
|
||||
| 4 | 회사별 20/100/300 | **PASS**. 각 회사 정확히 N명. helper 단위 검사는 모든 회사·규모 조합, 실제 화면은 단일 회사 20/100/300 확인. |
|
||||
| 5 | 다중 회사 | **PASS**. 한맥기술+삼안 ×100=200명, 전체 7×300=2,100명. 빈 선택은 Core empty view. 전환 전 예상 규모 표시·데이터 적용 버튼 제공. |
|
||||
| 6 | TB 유지 | **PASS**. 기존 Small fixture의 직원/그룹 위치·치수를 시작 전 dist와 직접 비교해 동일. 일반 TB 관계선 경로 보존. |
|
||||
| 7 | LR | **PASS**. 실제 dabeng `direction:'l2r'`, 부모 좌측·자식 우측·형제 세로 정렬. 회사마다 독립 tree. 아래 공식 예제 비교 참조. |
|
||||
| 8 | Hybrid | **PASS**. 실제 `verticalLevel` + 명시적 engine level. Root=1Depth, 시작 Depth 2/3/4. 부모별 지역 세로 배치·재귀 구조 검증. |
|
||||
| 9 | 초기 Depth | **PASS**. 1/2/3/all, 모든 레이아웃에서 독립. 변경하면 초기 접힘 정책으로 재설정; 이후 사용자 접힘 상태는 유효한 조직 ID에 대해 유지. |
|
||||
| 10 | 그룹 Hybrid·Compact | 그룹 Hybrid **PASS**, TB의 지정 그룹 자손에만 실제 적용. LR/전체 Hybrid와 조합 거절·Story disabled/자동 해제. Compact **NOT SUPPORTED**, 설명 참조. |
|
||||
| 11 | 접기·펼치기 | **PASS**. click/Enter/Space, 유효한 하위 상태·형제 상태 유지. 독립 `collapse` 설정 제공. LR 접기·재확장 후 실제 양끝 연결 좌표·재배치 확인. |
|
||||
| 12 | Zoom/Pan/Fit | **PASS**. 각 독립 설정·실제 pointer Pan·Ctrl-wheel gate·도구 버튼. Fit/100% 복원 및 키보드 focus 노출은 기능 스위치와 독립. |
|
||||
| 13 | 연결선·Border | **PASS**. 실제 parentId·제목 좌표 및 직원 카드 도착점에 SVG 배치. LR 모든 직원 left 연결/left accent, TB/Hybrid 기존 top/left/right. 다른 면 중립·카드 치수 보존. |
|
||||
| 14 | 컬러·흰 타이틀 | **PASS**. 7회사 프리셋 파일/배경 동일, 시작 전 `#ffffff` 변경 보존. normal/hover/focus/collapsed/expanded 흰 글자 확인. 삼안·장헌산업 대비 진단은 유지. |
|
||||
| 15 | HM Dialog | **PASS**. 실제 공개 HM Dialog/Portal·Avatar·Button. 직원 identity, 필드, 단일 팝업, Enter/Space, ESC/닫기·focus trap/복귀, source 변경 시 stale 팝업 정리. 390px 필드/팝업 가로 overflow 없음. |
|
||||
| 16 | Vanilla 호환 | **PASS**. 기존 Core native dialog 및 callback 계약 유지. React/HM은 Core 의존성이 아님. 동일 공개 helper/DataSource/Core로 직원 기록 공유. |
|
||||
| 17 | 300/2,100 성능 | **PASS — 이 PC의 측정 시나리오**. 신규 LR/Hybrid 별도 실측·DOM/JS heap 기록. 아래 표 참조. 장시간·타 기기·peak memory는 NOT VERIFIED. |
|
||||
| 18 | Fixed 영향 | **PASS — 최소 회귀**. 시작 전 dist와 geometry/SVG/text/gradient/border 동일, 새 Auto control 없음, 기존 팝업 open/ESC. 보호 자산 33개·고정형 관련 파일/프리셋/fixture 해시 동일. 전체 Fixed 픽셀 인증은 별도 미검증. |
|
||||
| 19 | Storybook URL | [Expanded OrgChart](http://127.0.0.1:6056/?path=/story/orgchart--expanded-org-chart). 실제 React Story, 기존 사용자용 4개 메뉴 유지. internal/verification Story는 기존 제외 정책 적용. |
|
||||
| 20 | 캡처 | 아래 현재 화면·LR·Hybrid·2,100명·Dialog·390px 링크 참조. |
|
||||
| 21 | 수정 파일 | 아래 27개 authored 파일 목록. 기존 흰색 타이틀 변경과 새 P7 CSS는 같은 파일에 공존. |
|
||||
| 22 | 변경 범위 검사 | focused unit **25/25**, Core/React typecheck/build, scoped Story typecheck, 실제 React consumer production build, 변경 범위 브라우저, `git diff --check` **PASS**. 초기 실패·수정 검사 provenance는 아래 기록. |
|
||||
| 23 | 기존 FAIL/미검증 | 과거 기록을 변경하지 않음. Guard 정적 PASS / runtime·CSS intent/final NOT VERIFIED, 기존 제목 대비 진단, 실기기·스크린리더·전체 역사 회귀 등을 아래에 분리. |
|
||||
| 24 | Git 미진행 | **Stage·Commit·Push·Merge·checkout·pull·reset·rebase·PR 모두 미진행.** staged 파일 0, branch/HEAD 동일. |
|
||||
|
||||
## 독립 옵션과 호환 범위
|
||||
|
||||
공개 사용 예와 세부 계약은 [expanded-usage.md](expanded-usage.md)에 있다.
|
||||
`autoLayout.type`은 TB/LR/Hybrid 중 하나이며 다른 기능을 켤 필요가 없다.
|
||||
|
||||
| 옵션 | TB | LR | Hybrid | 제한 |
|
||||
|---|---|---|---|---|
|
||||
| 접기·펼치기 / Zoom / Pan | 각각 선택 | 각각 선택 | 각각 선택 | 각각 기본 true, 다른 스위치의 필수 사용 없음 |
|
||||
| 초기 Depth / 회사 선택 | 독립 | 독립 | 독립 | 초기 Depth 변경은 접힘 상태를 초기 정책으로 재설정 |
|
||||
| React popup core/hm-dialog/none | 독립 | 독립 | 독립 | HM 모드만 HM UI optional peer·호스트 theme 필요; callback은 기본 팝업보다 우선 |
|
||||
| Hybrid 시작 Depth 2/3/4 | 적용 안 함 | 적용 안 함 | 선택 | 값은 보관하되 TB/LR에서는 적용하지 않음 |
|
||||
| 지정 그룹 Hybrid | 지원 | 조합 거절 | 조합 거절 | Story는 비호환 모드로 전환할 때 목록을 해제 |
|
||||
| Compact | 미지원 | 미지원 | 미지원 | 엔진 기능 자체의 미지원이라는 뜻은 아님 |
|
||||
|
||||
dabeng 6 Compact는 자식 `.node`를 부모 `.node` 내부에 넣고 140×50 grid 및 별도 compact/loose 토글을 사용한다.
|
||||
현재 HM article·카드 크기·측정 연결선·그룹 접기 계약과 충돌한다. 기존 카드 디자인을 유지하면서
|
||||
안전하게 연결할 수 없어 옵션을 제공하지 않았다. 새 엔진이나 레이아웃 라이브러리는 추가하지 않았다.
|
||||
`visibleLevel` 요구는 Core 초기 접힘 projection으로 제공한다. upstream hidden 자손을 그대로 둘 경우
|
||||
직원 카드·연결선 측정이 충돌하므로 engine에는 `visibleLevel:999`를 전달하고 실제 보이는 트리를 넘긴다.
|
||||
|
||||
## dabeng LR 공식 예제 비교
|
||||
|
||||
[사용자 제공 예제](https://dabeng.github.io/OrgChart/left2right.html) 및
|
||||
[공식 원본](https://github.com/dabeng/OrgChart/blob/master/demo/left2right.html)을 현재 소스와 비교했다.
|
||||
원본은 `direction:'l2r'`와 `.title/.content`를 사용한다. 설치된 dabeng 6도 동일 옵션이며
|
||||
차트 회전·50×140 node·title/content 반회전 CSS를 사용한다.
|
||||
현재 구현은 해당 방향 API·재귀 DOM을 재사용하고, 기존 HM 복합 article의 정방향·치수 유지에 필요한
|
||||
Auto 전용 flex/측정 SVG adapter를 적용한다. 원본 화면과 픽셀 동일성을 요구하거나 주장하지 않는다.
|
||||
|
||||
| 사용자 확인 항목 | 판정 | 증거 |
|
||||
|---|---|---|
|
||||
| 1 실제 `l2r` 우선 재사용 | PASS | engine options 단위 검사, 실제 7개 `.orgchart.l2r`, 공식 원본/설치 소스 비교 |
|
||||
| 2 최상위 왼쪽·하위 오른쪽 | PASS | 모든 parentId 대상 child-right 실측 |
|
||||
| 3 형제 세로 정렬 | PASS | 모든 같은-parent 형제의 y 범위 검사 |
|
||||
| 4 3Depth 이상 관계선 | PASS | 5Depth fixture, parentId 및 실제 heading endpoint 검사 |
|
||||
| 5 LR 카드 Border 일치 | PASS | 모든 카드 left 방향·실제 도착점 <1px·한 면 accent 검사 |
|
||||
| 6 접기 후 갱신 | PASS | 닫기/재확장 시 자손 수·겹침·부모/자식 선 양끝 재측정 |
|
||||
| 7 전환 잔상 | PASS | TB→Hybrid→LR에서 tree 7 / l2r 7 / SVG 7 / 관계선 70 / 부모 있는 node 70 / vertical list 0 |
|
||||
| 8 독립 회사 Root·겹침 | PASS | 7회사 300/2,100명 전체, 카드/그룹 겹침 없음, 회사 선택 독립 |
|
||||
| 9 사진·직위·이름 정방향 | PASS | 모든 관련 element transform=none, 사진 36×36, 100% 캡처 확인 |
|
||||
| 10 Zoom/Pan/Fit·팝업 | PASS | 기존 LR 통과 증거 재사용 + 독립 스위치/실제 HM Dialog 검증 |
|
||||
|
||||
이번 예제 비교 이후 제품 코드에 LR 추가 수정은 하지 않았다. 미확인한 정방향·접기 양끝·잔상만 추가 검사했다.
|
||||
|
||||
## 실행 검증과 초기 실패 처리
|
||||
|
||||
기존 P5/P6 전체 suite는 재실행하지 않았다. 새로운 helper/policy와 직접 영향받는 경로만 확인했다.
|
||||
단위 검사 25개는 data example 11 + auto policy 7 + 기존 expanded layout 4 + company 3이며,
|
||||
중간 수정 이후에는 영향받은 policy 검사만 갱신하고 나머지 유효한 결과를 재사용했다.
|
||||
|
||||
| 검사 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| Core/React typecheck·build | PASS | 공개 패키지별 명령 실행. 마지막 Core build는 변경된 선언 주석까지 dist에 반영. |
|
||||
| scoped Story typecheck | PASS | P7 Story만 포함한 tsconfig, 최종 그룹 이름 변경 후 확인. |
|
||||
| 실제 React production consumer | PASS | 공개 패키지·HM theme/CSS·StrictMode bootstrap, production build 및 실제 브라우저. 기존 asset 크기로 chunk size 경고 1종, build 오류 없음. |
|
||||
| Core 최초 변경 검사 | 84 PASS / 10 FAIL → 수정 확인 PASS | [초기 raw](../../test-results/org-chart-p7/browser-core-initial.json). update 후 Fit, LR 형제 CSS 우선순위, Hybrid engine level/부모별 list를 최소 수정. 실패 경로 및 연결선/2,100명 관련 [31/31 repair](../../test-results/org-chart-p7/browser-repair.json). |
|
||||
| Story·production React | 50 PASS / timing 검사 2 FAIL → PASS | [초기 raw](../../test-results/org-chart-p7/browser-story-react.json). Radix 종료 animation/presence 전 검사였음. 종료 hidden을 기다리는 [3/3 targeted 확인](../../test-results/org-chart-p7/browser-react-repair.json). 제품 결함으로 재분류하지 않음. |
|
||||
| 독립 interaction·최종 popup | 79 PASS 후 runner 중단 | [raw](../../test-results/org-chart-p7/browser-options-react-repair.json). 실제 그룹 옵션의 중복 회사 접두사 정리, 남은 옵션/폭/색상 [12 PASS](../../test-results/org-chart-p7/browser-options-tail.json). 최초 메뉴 집계는 internal 3개까지 포함해 실패; 기존 제외 정책 적용한 [2/2 메뉴 검사](../../test-results/org-chart-p7/browser-story-menu.json). |
|
||||
| LR reference 추가 영향 | 4 PASS 후 잔상 집계 1 FAIL → PASS | [raw](../../test-results/org-chart-p7/browser-lr-reference.json). 빈 parentId Root까지 관계선 수에 포함한 테스트 오류; Root 제외 후 해당 검사만 [2/2 PASS](../../test-results/org-chart-p7/browser-lr-residue.json). 제품 코드 변경 없음. |
|
||||
| 신규 성능·390px Dialog | 6/6 PASS | [raw](../../test-results/org-chart-p7/browser-performance-dialog-narrow.json). 측정 시간을 새로 확보한 검사이며 전체 suite 반복 아님. |
|
||||
| 390/768/1280/1440 | PASS | 최종 설정 UI 줄바꿈, document 가로 overflow 없음, stage scrollLeft/Top=0, scrollWidth/clientWidth·scrollHeight/clientHeight 일치, Fit 전체 bounds. 문서 세로 스크롤은 정상. |
|
||||
| 최소 Fixed 회귀 | PASS | 최초 Core raw의 Fixed geometry/SVG/text/gradient/border, Auto control 부재·native 팝업 open/ESC 및 보호 hash. |
|
||||
| Guard 1회 | static PASS, issues 0 | [원본](../../test-results/org-chart-p7/guard.json), STORYBOOK/PARTIAL. raw status PASS, runtimeStatus NOT VERIFIED. 아래 CSS 범위 구분 유지. |
|
||||
| 최종 diff·보호 파일 | PASS | `git diff --check`, [baseline](../../test-results/org-chart-p7/baseline.json), [최종 audit](../../test-results/org-chart-p7/final-state.json). |
|
||||
|
||||
Chrome `155.0.8059.39`, 기존 설치 Playwright/Chrome를 사용했으며 패키지 설치/업그레이드 없음.
|
||||
localhost sandbox `ERR_NETWORK_ACCESS_DENIED` 초기 로그를 보존하고 허용된 로컬 브라우저 실행으로 확인했다.
|
||||
서로 다른 실행의 PASS 수를 중복 합산해 총 검사 수라고 주장하지 않는다. 모든 성공 실행의 신규 console/page 오류는 0이다.
|
||||
|
||||
## 신규 모드 성능
|
||||
|
||||
Windows 이 PC, Chrome headless, 1440×1100, full depth·7회사·가상 fixture, 각 1회 관측이다.
|
||||
Core mount는 `performance.now()`의 `handle.mount()` 시간, ready/interaction은 Playwright 동작과
|
||||
font 준비·5 animation frame 대기를 포함한 경과 시간이다. 네트워크/브라우저 warm 상태와 GC 영향을 받으며 서로 다른 지표를 동일 benchmark로 비교하지 않는다.
|
||||
|
||||
| 데이터·모드 | Core mount | 화면 준비 | 접기 | Zoom | Fit | used JS heap | 내부 DOM |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 300 LR | 31.6ms | 442ms | 195ms | 116ms | 116ms | 9,839,068B | 3,306 |
|
||||
| 300 Hybrid Depth2 | 19.2ms | 300ms | 182ms | 131ms | 118ms | 12,887,564B | 3,152 |
|
||||
| 300 Hybrid Depth3 | 18.2ms | 325ms | 182ms | 133ms | 117ms | 17,325,639B | 3,194 |
|
||||
| 300 Hybrid Depth4 | 17.1ms | 321ms | 183ms | 116ms | 118ms | 15,098,350B | 3,243 |
|
||||
| 2,100 LR | 160.5ms | 미측정 | 미측정 | 미측정 | 전체 Fit PASS | 15,640,127B | 17,706 |
|
||||
| 2,100 Hybrid Depth3 | LR→Hybrid 전환+준비 917ms | 앞 열 지표 | 미측정 | 미측정 | 전체 Fit PASS | 28,410,298B | 17,594 |
|
||||
|
||||
2,100명의 실제 카드 수·고유 ID·겹침 없음·전체 bounds를 따로 확인했다. 전체 Fit은 가독성이 작은 개요 화면이다.
|
||||
카드 읽기에는 Zoom 또는 초기 표시 Depth가 필요하다. Story에서 2,100명 전환 규모를 적용 전에 표시하고
|
||||
초기 Depth2로 전환하는 실제 UI도 확인했다. `performance.memory.usedJSHeapSize`는 한 시점의 JS heap이며
|
||||
DOM native memory·프로세스 전체 메모리·peak·누수/장시간 반복을 증명하지 않는다. 2,100명 interaction latency 전체는 NOT VERIFIED.
|
||||
|
||||
## 미검증·기존 진단 보존
|
||||
|
||||
- Guard `styleEvidence`: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**.
|
||||
이 실행에 결합 `styleVerification`은 생성하지 않았으며 NOT VERIFIED로 취급한다. Story 정적 graph와
|
||||
별도 실제 React build/browser 증거를 전체 Core CSS·임의 외부 props/style의 인증으로 합치지 않는다.
|
||||
- 기존 삼안·장헌산업 흰 타이틀의 일반 텍스트 대비 미달 진단을 유지한다. 요청한 흰색·회사 색은 변경하지 않았다.
|
||||
P6.1의 Auto 관계선 대비 수정과 과거 FAIL/미검증 기록은 그대로 보존한다.
|
||||
- 실제 스크린리더 발화·물리 터치·다른 브라우저, 전체 Fixed 픽셀/모든 폭·전체 과거 회귀,
|
||||
모든 임의 데이터/override/옵션 상태 조합, HM peer 없는 fresh install/bundler, 운영 배포·SSR/hydration은 **NOT VERIFIED**.
|
||||
실제 API/서버/DB·실제 개인정보는 요청 범위 밖이며 추가하지 않았다.
|
||||
|
||||
## 주요 화면
|
||||
|
||||
- [최종 설정 화면 1440px](../../test-results/org-chart-p7/story-1440.png), [390px](../../test-results/org-chart-p7/story-390.png)
|
||||
- [LR 300명·7회사](../../test-results/org-chart-p7/corrected-lr-group.png), [LR 카드 100% 정방향](../../test-results/org-chart-p7/lr-card-upright.png)
|
||||
- [Hybrid Depth2](../../test-results/org-chart-p7/corrected-hybrid-2.png), [Depth3](../../test-results/org-chart-p7/corrected-hybrid-3.png), [Depth4](../../test-results/org-chart-p7/corrected-hybrid-4.png)
|
||||
- [HM Dialog LR](../../test-results/org-chart-p7/Final-lr-dialog.png), [HM Dialog 390px](../../test-results/org-chart-p7/hm-dialog-390.png)
|
||||
- [2,100명 LR](../../test-results/org-chart-p7/2100-lr.png), [2,100명 Hybrid](../../test-results/org-chart-p7/2100-hybrid.png), [Story 초기 Depth2](../../test-results/org-chart-p7/story-2100-depth2.png)
|
||||
- [보존된 Fixed](../../test-results/org-chart-p7/fixed-1440.png)
|
||||
|
||||
Popup full-page 캡처는 고정 overlay가 현재 viewport만 덮는 특성상 문서 바깥 영역과 차이가 보일 수 있다.
|
||||
실제 popup bounds·overflow·focus는 DOM 실측으로 별도 확인했다.
|
||||
|
||||
## Authored 변경 파일
|
||||
|
||||
1. `package-lock.json` — optional HM UI peer metadata만 반영, 라이브러리 버전/설치 변경 없음.
|
||||
2. `packages/org-chart/src/data/examples.ts` — 공통 deterministic 가상 데이터 helper.
|
||||
3. `packages/org-chart/src/data/examples.test.ts` — 11개 데이터 계약 검사.
|
||||
4. `packages/org-chart/src/data/index.ts` — 공개 export.
|
||||
5. `packages/org-chart/src/core/auto-layout-policy.ts` — 모드/Depth/독립 interaction 계약.
|
||||
6. `packages/org-chart/src/core/auto-layout-policy.test.ts` — 7개 policy 검사.
|
||||
7. `packages/org-chart/src/core/types.ts` — additive 공개 옵션/setter types.
|
||||
8. `packages/org-chart/src/core/engine.ts` — 기존 dabeng options 전달.
|
||||
9. `packages/org-chart/src/core/vendor.d.ts` — 설치 엔진 option typing.
|
||||
10. `packages/org-chart/src/core/render.ts` — 명시적 level/그룹 hybrid projection.
|
||||
11. `packages/org-chart/src/core/index.ts` — Auto 상태·setter·전환 Fit/engine lifecycle.
|
||||
12. `packages/org-chart/src/core/expanded-content.ts` — Auto mode/interaction/방향 표시.
|
||||
13. `packages/org-chart/src/core/expanded-layout.ts` — Auto 크기 측정·조직 관계선.
|
||||
14. `packages/org-chart/src/core/expanded-viewport.ts` — 독립 Zoom/Pan gate 및 focus reveal.
|
||||
15. `packages/org-chart/src/index.ts` — 공개 types export.
|
||||
16. `packages/org-chart/src/styles/org-chart-expanded.css` — Auto 전용 LR/Hybrid CSS, 기존 흰 타이틀 보존.
|
||||
17. `packages/org-chart/README.md` — data/API/연결 방향 문서.
|
||||
18. `packages/org-chart-react/package.json` — optional `@hm-design/ui` peer.
|
||||
19. `packages/org-chart-react/src/types.ts` — React 옵션 및 popup contract.
|
||||
20. `packages/org-chart-react/src/OrgChart.tsx` — 기존 Core setter·popup state adapter.
|
||||
21. `packages/org-chart-react/src/HMEmployeeDialog.tsx` — 실제 HM Dialog와 focus 복귀.
|
||||
22. `packages/org-chart-react/README.md` — 선택형 HM peer·옵션 사용 안내.
|
||||
23. `src/org-chart/org-chart-expanded.stories.tsx` — 기존 Expanded Story에 실제 선택 UI.
|
||||
24. `src/org-chart/org-chart-expanded-story.css` — Expanded 한정 콘텐츠 layout.
|
||||
25. `examples/org-chart/verify-p7.mjs` — 변경 범위 integration runner.
|
||||
26. `docs/org-chart/expanded-usage.md` — P7 공개 API·호환 범위.
|
||||
27. `docs/org-chart/implementation-report-p7.md` — 이 보고서.
|
||||
|
||||
`packages/*/dist`, `test-results/org-chart-p7`의 baseline/React consumer/JSON/캡처/tsconfig/audit 등은
|
||||
기존 ignore 정책의 로컬 산출물이며 Stage하지 않았다. 별도 역사 보고서·승인 SVG·fixture·Theme·UI Component·기타 Story는 수정하지 않았다.
|
||||
필수 소스·metadata는 함께 검토할 수 있도록 목록에 포함했다. 이번에는 Git 반영 계획이나 자동 Stage를 실행하지 않는다.
|
||||
|
||||
## 시간 기록 — Asia/Seoul
|
||||
|
||||
- Started At: 2026-10-13 09:07:58 KST
|
||||
- Completed At: 2026-10-13 09:40:32 KST
|
||||
- Actual Duration: 32분 34초
|
||||
- Estimated Duration: 60–90분 (분석 10–15분 / 구현 30–45분 / 통합 검증 20–30분)
|
||||
- Variance: 예상보다 27분 26초–57분 26초 빨리 완료
|
||||
@@ -0,0 +1,132 @@
|
||||
# HMDesign OrgChart P8 — Storybook 메뉴·페이지 통일
|
||||
|
||||
RESULT: **PASS — P8 요청의 메뉴·페이지 및 최소 기능 검증 범위.**
|
||||
기존 Core 토큰 관련 Guard FAIL과 CSS 디자인 의도 미검증은 별도로 보존한다.
|
||||
P9·Stage·Commit·Push·Merge·PR은 진행하지 않았다.
|
||||
|
||||
## 작업 기준
|
||||
|
||||
- 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작·완료 HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- 범위: `STORYBOOK_ONLY`, 실제 React Storybook. 시작 시 P7 미커밋 변경 27개를 보존했다.
|
||||
- Started At: `2026-10-13 09:43:40 KST`
|
||||
- Completed At: `2026-10-13 10:09:15 KST`
|
||||
- Actual Duration: `25분 35초`
|
||||
- Estimated Duration: `30분` — 분석 7분 / 구현 12분 / 검증 11분
|
||||
- Variance: `예상보다 4분 25초 짧음`
|
||||
|
||||
## 최종 메뉴와 페이지
|
||||
|
||||
```text
|
||||
Components
|
||||
Patterns
|
||||
OrgChart
|
||||
├─ Overview
|
||||
├─ Department Colors
|
||||
├─ Full Composition
|
||||
└─ Expanded OrgChart
|
||||
```
|
||||
|
||||
기존 Foundation·Center Graphic 등 다른 카테고리는 유지한다. OrgChart는 Patterns 바로 다음의 독립 최상위 root이며, 공개 페이지는 정확히 4개다. 다른 292개 Story의 상대 순서는 유지한다.
|
||||
|
||||
슬래시 없는 `title: OrgChart`는 Storybook이 root 없는 component로 분류해 정렬 설정과 관계없이 최상단에 표시했다. 각 페이지를 `OrgChart/페이지명`의 단일 Story CSF로 나누어 표준 root/단일 Story 표시 정책을 사용했다. 기존 Story URL 27개는 리터럴 `parameters.__id`와 기존 export를 통해 모두 유지한다. 내부 23개 Story는 기본 제외 정책으로 숨기며 삭제하지 않았다.
|
||||
|
||||
| 페이지 | 적용 결과 |
|
||||
|---|---|
|
||||
| Overview | 기존 통합 페이지의 로고·조직/직위별 현황·관계사별 현황을 3개 공통 섹션으로 구성. 동일 Core 독립 컴포넌트와 가상 fixture 유지. |
|
||||
| Department Colors | 공통 제목·설명, 표시 옵션, 기존 고정형 컬러/카드 미리보기. 긴 이름 버튼 유지. Auto 회사색은 혼합하지 않음. |
|
||||
| Full Composition | 공통 제목·설명·섹션만 추가. 실제 가용 폭·높이를 기존 Core가 측정하며 비례 축소. 내부 카드·허브·화살표·현황·파견/휴직·링크·팝업·Workdesk 디자인 유지. |
|
||||
| Expanded OrgChart | 데이터 / 레이아웃 / 인터랙션·직원 정보 / 미리보기의 4개 섹션. 기존 FilterBar·Label·Select·Checkbox·Button·모든 이벤트와 옵션 유지. 기존 Zoom/Pan/Fit 도구는 트리 안에 유지. |
|
||||
|
||||
## 재사용한 공통 스타일
|
||||
|
||||
실제 대표 페이지 3개인 Filter Bar, Page Header, Loading Screen을 비교했다.
|
||||
`src/components/story-page.tsx`의 `StoryPage / StoryPageHeader / StorySection`과 기존 CSS를 그대로 재사용한다.
|
||||
|
||||
- 제목 28px / 700 / 36px, 설명 14px / 20px
|
||||
- 콘텐츠 envelope 최대 1280px(양측 gutter 포함), gutter 16–40px
|
||||
- 섹션 간격 32px, 섹션 header 하단 12px / 본문 간격 16px
|
||||
- 기존 semantic 테두리·Typography·배경, fullscreen bootstrap
|
||||
- 별도 페이지 디자인 시스템이나 공통 Theme 변경 없음
|
||||
|
||||
추가 CSS는 Story 전용 최대 폭·미리보기 host, 접근성 legend, 팝업 설정 폭에 한정한다.
|
||||
기존 `body:has(.hm-orgchart-story-full)` 배경 적용을 제거하고 고정형 배경은 해당 견본 안에 유지한다.
|
||||
공통 CSS가 Shadow DOM의 폰트·카드·색·Border·치수를 변경하지 않는 것을 실제 computed 값으로 확인했다.
|
||||
|
||||
## 변경 범위 검증
|
||||
|
||||
| 검사 | 결과·근거 |
|
||||
|---|---|
|
||||
| 메뉴 위치·Depth·4개 순서·중복·내부 기본 숨김 | **PASS**, [최종 manager 8/8](../../test-results/org-chart-p8/browser-manager.json) |
|
||||
| 기존 URL·최종 CSF 연결 | **PASS**, [최종 route 5/5](../../test-results/org-chart-p8/browser-route-final.json). 4개 기존 공개 URL의 실제 제목·Core 견본 및 오류 없음 확인. |
|
||||
| 4페이지와 반응형 | **PASS**, 390/768/1280/1440px × 4페이지. document 가로 overflow 없음, 제목/컨트롤 줄바꿈 정상. |
|
||||
| 기존 Pattern과 스타일 비교 | **PASS**, 실제 공통 frame·Typography·spacing·border·gutter 비교. |
|
||||
| Fixed 표시·비례 축소·잘림·내부 스크롤 | **PASS**, 4폭에서 전체 stage/card/summary bounds와 휠 후 offset 확인. [Full 표적 6/6](../../test-results/org-chart-p8/browser-full-repair.json) |
|
||||
| Expanded 최소 기능 | **PASS**, 20→100명 draft/apply, TB/LR/Hybrid, 초기 Depth, Enter/Space 접기/펼치기, 독립 Zoom/Pan, 실제 pointer Pan, Fit. |
|
||||
| HMDesign 직원 팝업 | **PASS**, 실제 공개 HM Dialog/Portal·Avatar·Button. 직원 identity, 단일 팝업, Tab focus trap, ESC/닫기, 카드 focus 복귀, 390px 표시 확인. Vanilla Core 책임 분리 유지. |
|
||||
| P7 소스·디자인 보존 | **PASS**, 기준 803개 파일 중 798개 동일. 변경 5개는 Storybook 설정/Story/CSS. Core·React 패키지·카드 SVG·회사색·직원 JSON·흰 타이틀·Theme 모두 동일. Expanded 상태/데이터 로직 및 컨트롤 속성 21개 보존. |
|
||||
| Typecheck | **PASS**, 변경 Story·설정·새 CSF와 의존 helper를 포함한 [범위 config](../../test-results/org-chart-p8/story-tsconfig.json), 최종 구조 기준. |
|
||||
| Storybook production build | **PASS**, [최종 build log](../../test-results/org-chart-p8/build.log). 기존 대형 asset의 chunk-size 경고는 유지. |
|
||||
| diff·Git 상태 | **PASS**, `git diff --check`, staged 0, branch/HEAD 동일. |
|
||||
| 신규 브라우저 오류 | **0**, 실제 검증의 console/page error. |
|
||||
|
||||
P7의 단위 25/25·Core/React build 및 기존 P5/P6/P7 전체 회귀는 재실행하지 않았다.
|
||||
20/100/300명·2,100명 데이터 정책과 배치 알고리즘은 소스 동일 및 기존 P7 결과로 보존 확인하며, 이번에 성능을 새로 측정한 것으로 보고하지 않는다.
|
||||
|
||||
### 초기 결과와 해결 근거
|
||||
|
||||
[초기 runtime](../../test-results/org-chart-p8/browser-initial-runtime.json)은 83 PASS / 6 FAIL이었다.
|
||||
실제 메뉴 위치 문제 1건은 표준 CSF root 분리로 수정하고 manager 범위만 다시 검사했다.
|
||||
나머지 5건은 runner가 auto margin의 CSSOM 표현값 및 CSS transform의 숨긴 원본 scroll extent를 화면 결함으로 집계한 경우였다. Full의 실제 양측 gutter, 표시 bounds, 휠·scroll offset을 표적 확인해 해결했으며 제품 CSS를 변경하지 않았다.
|
||||
서로 다른 실행의 PASS 수를 단순 합산하지 않는다. [최종 증거 연결](../../test-results/org-chart-p8/browser-final-summary.json)에 초기·표적 raw와 해결 사유를 남겼다.
|
||||
|
||||
초기 6057 sandbox 서버 접속 실패와 6058 사용 중 오류는 환경 문제였다. 화면 검사 전 network raw를 보존하고, 접근 가능한 6061 서버에서 실제 검증했다.
|
||||
|
||||
## Guard 원본·기존 FAIL·NOT VERIFIED
|
||||
|
||||
최종 구조 변경 후 PARTIAL/STORYBOOK Guard로 영향 범위만 확인했다.
|
||||
[원본 Guard](../../test-results/org-chart-p8/guard.json)의 **status/staticStatus는 FAIL**, runtimeStatus는 **NOT VERIFIED**다. 원본을 PASS로 덮어쓰지 않았다.
|
||||
|
||||
- 변경 P8 파일의 issue: **0**
|
||||
- 변경 전 Core의 TOKEN-001 **10 FAIL**, 동적 Token/공통 helper class의 TOKEN-002 **10 NOT VERIFIED**
|
||||
- 해당 Core·공통 helper 소스는 시작 hash와 동일. Story용 Core 내부 import와 Shadow stylesheet/동적 선언의 정적 분석 경계에 대한 기존 진단을 보존한다.
|
||||
- styleEvidence: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**
|
||||
- 실제 페이지 스타일 동등성과 Shadow 격리의 브라우저 PASS는 별도 근거다. 디자인 의도 승인 또는 전체 Guard runtime 승인으로 확대하지 않는다.
|
||||
|
||||
[범위 분류](../../test-results/org-chart-p8/guard-scope.json), [소스 보존·Git·ID audit](../../test-results/org-chart-p8/final-state.json)을 연결했다.
|
||||
기존 삼안·장헌산업 제목 대비 진단, Compact **NOT SUPPORTED**, 전체 Fixed 픽셀/역사 회귀, 모든 옵션 조합, 물리 기기·스크린리더·다른 브라우저, 2,100명 장시간/peak memory, SSR/hydration·운영 배포는 기존 미검증 상태를 유지한다.
|
||||
|
||||
## 실제 화면
|
||||
|
||||
- [좌측 메뉴 전체](../../test-results/org-chart-p8/manager-full-sidebar.png), [좌측 메뉴](../../test-results/org-chart-p8/manager-sidebar.png)
|
||||
- [Overview](../../test-results/org-chart-p8/overview-1440.png), [390px](../../test-results/org-chart-p8/overview-390.png)
|
||||
- [Department Colors](../../test-results/org-chart-p8/department-colors-1440.png), [390px](../../test-results/org-chart-p8/department-colors-390.png)
|
||||
- [Full Composition](../../test-results/org-chart-p8/full-composition-1440.png), [390px](../../test-results/org-chart-p8/full-composition-390.png)
|
||||
- [Expanded OrgChart](../../test-results/org-chart-p8/expanded-org-chart-1440.png), [390px](../../test-results/org-chart-p8/expanded-org-chart-390.png)
|
||||
- [실제 HM Dialog](../../test-results/org-chart-p8/expanded-hm-dialog.png), [390px Dialog](../../test-results/org-chart-p8/expanded-hm-dialog-390.png)
|
||||
|
||||
현재 미리보기: [Overview](http://127.0.0.1:6061/?path=/story/orgchart--logo-personnel-summary), [Department Colors](http://127.0.0.1:6061/?path=/story/orgchart--department-colors), [Full Composition](http://127.0.0.1:6061/?path=/story/orgchart--full-composition), [Expanded OrgChart](http://127.0.0.1:6061/?path=/story/orgchart--expanded-org-chart).
|
||||
Overview는 기존 `orgchart--logo-personnel-summary` URL을 유지한다.
|
||||
|
||||
## P8 변경 파일
|
||||
|
||||
기존 미커밋 P7 변경과 아래 P8 delta를 구분한다.
|
||||
|
||||
| 파일 | P8 변경 |
|
||||
|---|---|
|
||||
| `.storybook/preview.tsx` | OrgChart root 및 4개 페이지 경로 정렬, 기존 leaf 정렬 정책 유지 |
|
||||
| `src/org-chart/org-chart-p1.stories.tsx` | Overview 단일 CSF, 기존 ID 보존 |
|
||||
| `src/org-chart/org-chart-expanded.stories.tsx` | 공통 섹션·root title·기존 ID |
|
||||
| `src/org-chart/org-chart-stories.css` | 최소 host 규칙, body 배경 범위 축소 |
|
||||
| `src/org-chart/org-chart-expanded-story.css` | 기존 P7 CSS 보존 + legend/설정 host |
|
||||
| `src/org-chart/org-chart-page-previews.tsx` | 기존 페이지 함수를 공통 재사용 helper로 이동 |
|
||||
| `src/org-chart/org-chart-department-colors.stories.tsx` | 단일 페이지 CSF, 기존 ID |
|
||||
| `src/org-chart/org-chart-full-composition.stories.tsx` | 단일 페이지 CSF, 기존 ID |
|
||||
| `src/org-chart/org-chart-overview-internal.stories.tsx` | 기존 내부 3개 CSF·ID·기본 숨김 유지 |
|
||||
| `examples/org-chart/verify-p8.mjs` | 요청한 화면/최소 기능 검증, 실패 범위 선택 실행 |
|
||||
| `docs/org-chart/implementation-report-p8.md` | 결과·증거·시간 기록 |
|
||||
|
||||
`test-results/org-chart-p8`의 baseline·raw JSON·캡처·static build·logs는 생성된 검증 산출물이다.
|
||||
**Stage·Commit·Push·Merge·main checkout·Pull·Reset·Rebase·Force Push·PR은 미진행. P8에서 종료한다.**
|
||||
@@ -0,0 +1,83 @@
|
||||
# HMDesign OrgChart P9.1 — Department Colors 간소화 · Full Composition 중앙 정렬
|
||||
|
||||
RESULT: **PASS — P9.1 변경 범위.**
|
||||
|
||||
두 페이지의 Storybook 표시만 수정했다. 기존 Core/React 카드·연결선·통계·Expanded 소스와 P7~P9 결과를 보존했다. Expanded 추가 디자인·기능 작업은 시작하지 않았다.
|
||||
|
||||
## 기준 및 시간
|
||||
|
||||
- 범위: `STORYBOOK_ONLY`, 실제 React Storybook.
|
||||
- 경로: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD 및 기존 remote-tracking feature HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 이번 원격 조회/Fetch 없음.
|
||||
- Started At: `2026-10-13 10:35:50 KST`
|
||||
- Completed At: `2026-10-13 10:48:05 KST`
|
||||
- Actual Duration: `12분 15초`
|
||||
- Estimated Duration: `25분` — 확인 5분 / 수정 10분 / 검증 10분
|
||||
- Variance: `예상보다 12분 45초 짧음`
|
||||
|
||||
## Department Colors
|
||||
|
||||
- 기존 조직 그룹 타이틀, 1Depth 그룹장 대형 카드 146×68, 2Depth 소형 직원 카드 108×52만 남겼다.
|
||||
- 제거: 장문 페이지/섹션 설명, 표시 옵션/긴 이름 검증 버튼, Core 예제 안내 2개, 별도 대표자 견본, 출처·버전·selector, CSS 변수/HEX/Gradient legend와 구현 설명, 카드 자산/크기 caption, 대비 MODEL 비교·진단/보조 안내.
|
||||
- Story 전용 `presentation="design"`에서 생성된 예제 DOM만 정리한다. 카드 하위 DOM·SVG/로고·Typography·6개 조직 색/Gradient와 Core generator는 그대로다. 삭제된 대비 요소를 참조하는 updater는 이 옵션에서 호출하지 않는다.
|
||||
- 2행 subgrid로 같은 행 제목/카드를 정렬한다. 기존 카드 grid margin-top 12px와 공통 StoryPage 제목·여백/섹션 스타일을 유지한다. 390px 단일 열에서 카드 잘림/가로 overflow 없음.
|
||||
|
||||
## Full Composition 실측
|
||||
|
||||
측정 기준: 실제 6개 tree의 조직 헤더와 직원 카드 `getBoundingClientRect()` 합집합. 폭 1920인 빈 forest/Stage 박스 중심을 본문 중심으로 사용하지 않았다.
|
||||
|
||||
원본 Stage 내부 본문 bounds는 X=32~1835, 중심 X=933.5이다. Stage 중심 X=960과 차이가 있으며, 높이 제한으로 축소된 Stage가 좌측에 붙는 경우 추가 여백도 발생했다.
|
||||
|
||||
| 브라우저 크기 | 실제 가용 영역 X/폭 | 가용 중심 | 수정 전 본문 중심 | 수정 전 오차 | 수정 후 본문 중심 | 수정 후 오차 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| 1920×1080 | 360 / 1200 | 960 | 943.4375 | -16.5625 | 960.00003 | +0.00003px |
|
||||
| 1440×900 | 120 / 1200 | 720 | 701.52458 | -18.47542 | 719.99991 | -0.00009px |
|
||||
| 390×844 | 16 / 358 | 195 | 190.05885 | -4.94115 | 195.00026 | +0.00026px |
|
||||
|
||||
Story 전용 helper가 본문 중심을 실측하고 전체 Stage에 독립 CSS scale/translate를 적용한다. 기존 Core의 가로/세로 fit transform은 유지하며, 본문 중심에서 Stage 좌·우 끝까지의 최대 거리에 맞춰 동일 비율을 추가 보정한다. Stage 전체/통계까지 영역 안에 들어오도록 계산하며 overflow 규칙을 추가·변경하지 않았다.
|
||||
|
||||
- 원본 1920×914.5 유지. DOM offsetHeight의 정수 반올림 915와 computed height 914.5를 각각 확인.
|
||||
- 카드/조직별 좌표 수정 없음. 대표자·허브·조직·연결선·협업 화살표·장식·우측 통계·좌측 하단 파견/휴직·하단 관계사 집계를 전체 Stage와 함께 이동.
|
||||
- Stage 내부 DOM SHA-256, 자산 hash와 152개 요소의 정규화 상대 좌표/크기·computed style 보존. 3개 폭에서 부적절한 겹침 0.
|
||||
- 비례 축소와 전체 실측 bounds 포함, 문서 및 사용자 조작 가능한 내부 가로/세로 스크롤 없음. 원래 fitted wrapper의 숨김 overflow에서 보고되는 변환 전 layout scrollWidth는 실제 스크롤로 오인하지 않고, 원본 동일 규칙/화면 전체 포함/휠 offset 0을 함께 확인.
|
||||
- 같은 화면을 remount 없이 1920×1080→390×844→1440×640→1440×900으로 변경해 중앙 정렬/균일 축소/스크롤 없음 확인. 10프레임 동안 Stage 스타일 mutation 0, observer 안정성 확인.
|
||||
|
||||
## 변경 파일 및 보존
|
||||
|
||||
| 파일 | 이번 변경 |
|
||||
|---|---|
|
||||
| `src/org-chart/org-chart-page-previews.tsx` | Department 설명/옵션 제거 및 디자인 모드 선택, Full Composition 정렬 helper 연결. Overview JSX 보존 |
|
||||
| `src/org-chart/core-component-preview.tsx` | Department에만 적용되는 선택형 디자인 예제 간소화. 기존 workdesk/standalone 경로 보존 |
|
||||
| `src/org-chart/center-composition-preview.ts` | 신규 Story-only 실측/Stage 정렬 helper와 observer cleanup |
|
||||
| `examples/org-chart/verify-p9-1.mjs` | 신규 범위 검증/수정 전후 캡처/동일 mount 리사이즈 runner |
|
||||
| `docs/org-chart/implementation-report-p9-1.md` | 이 보고서 |
|
||||
|
||||
시작 시점 authored 파일 811개 중 기존 파일 2개만 변경, 809개 SHA-256 동일. 신규 authored 파일 3개. Core/React 패키지, 카드·색상·공통 CSS, Expanded Story/CSS/기능, 메뉴/ID 설정과 P7~P9 보고서/runner는 변경하지 않았다. [시작 snapshot](../../test-results/org-chart-p9-1/baseline.json), [최종 audit](../../test-results/org-chart-p9-1/final-state.json).
|
||||
|
||||
## 변경 범위 검증
|
||||
|
||||
| 검증 | 결과 |
|
||||
|---|---|
|
||||
| 두 페이지 실제 Chromium | **44/44 PASS**, 신규 console/pageerror 0. [원본](../../test-results/org-chart-p9-1/browser.json) |
|
||||
| 동일 mount 리사이즈/스타일 안정성 | **17/17 PASS**, 신규 console/pageerror 0. [원본](../../test-results/org-chart-p9-1/browser-resize.json) |
|
||||
| 변경 Story/helper Typecheck | **PASS**: `node node_modules/typescript/bin/tsc -p test-results/org-chart-p9-1/story-tsconfig.json` |
|
||||
| Storybook production Build | **PASS**: `npm.cmd exec -- storybook build -o storybook-static --disable-telemetry`. [log](../../test-results/org-chart-p9-1/build.log), 기존 500kB chunk 경고/플러그인 timing 진단 유지, build 오류 없음 |
|
||||
| Storybook 공개 메뉴 | **PASS**, 기존 Overview / Department Colors / Full Composition / Expanded OrgChart 4개 ID/index 유지. manager 설정 동일 hash와 P8 메뉴 배치 PASS 재사용 |
|
||||
| Git diff/공백 | **PASS**, `git diff --check` 및 신규 authored 파일 공백 확인 |
|
||||
| Git 보호 | **Stage·Commit·Push·Merge·main 수정/checkout·PR·Reset·Rebase·Force Push 모두 미진행**, staged 파일 0, branch/HEAD 동일 |
|
||||
|
||||
P7~P9 정상 결과는 동일 소스 근거와 함께 재사용했다. 전체 Guard·과거 브라우저 suite·Expanded 상호작용·2,100명 성능 검사는 반복하지 않았다.
|
||||
|
||||
## NOT VERIFIED / 검토 대기
|
||||
|
||||
- P9 Guard 원본의 FAIL / RUNTIME·DESIGN_INTENT·FINAL NOT VERIFIED 및 기존 접근성 대비 잔여 사항을 그대로 보존한다. 이번 브라우저 PASS로 원본 상태를 덮어쓰지 않았다.
|
||||
- 최종 사용자 디자인 승인, 다른 브라우저/실기기/스크린리더/SSR·배포는 **NOT VERIFIED**.
|
||||
- Expanded 기능은 소스 hash 보존 및 P7~P9 PASS 재사용 근거다. 이번 기능 재검증/추가 디자인 수정은 진행하지 않았다. 사용자 검토를 기다린다.
|
||||
|
||||
## Storybook 및 화면
|
||||
|
||||
- [Department Colors](http://127.0.0.1:6061/?path=/story/orgchart--department-colors): [1920px](../../test-results/org-chart-p9-1/after-department-1920.png), [1440px](../../test-results/org-chart-p9-1/after-department-1440.png), [390px](../../test-results/org-chart-p9-1/after-department-390.png)
|
||||
- [Full Composition](http://127.0.0.1:6061/?path=/story/orgchart--full-composition): [1920px](../../test-results/org-chart-p9-1/after-fixed-1920.png), [1440px](../../test-results/org-chart-p9-1/after-fixed-1440.png), [390px](../../test-results/org-chart-p9-1/after-fixed-390.png)
|
||||
- 수정 전 캡처는 같은 결과 폴더의 `before-*.png`로 보존.
|
||||
@@ -0,0 +1,112 @@
|
||||
# HMDesign OrgChart P9.2 — Full Composition 중앙 정렬 · 회사 선택 통합
|
||||
|
||||
RESULT: **PASS — 요청한 P9.2 변경 범위.** 기존 Guard FAIL/NOT VERIFIED 및 사용자 디자인 승인 상태는 별도로 유지한다.
|
||||
|
||||
## 작업 기준·시간
|
||||
|
||||
- 범위: `STORYBOOK_ONLY`, 기존 실제 React Storybook 수정.
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 원격 조회·Fetch·Pull 없음.
|
||||
- Started At: 2026-10-13 10:53:44 KST
|
||||
- Completed At: 2026-10-13 11:06:16 KST
|
||||
- Actual Duration: 12분 32초
|
||||
- Estimated Duration: 최초 35~50분 — 구조 확인 8분 / 수정 15~20분 / 검증·캡처·보고 12~22분.
|
||||
- Variance: 예상보다 22분 28초~37분 28초 짧음.
|
||||
|
||||
## Full Composition 원인·수정
|
||||
|
||||
P9.1은 6개 조직 카드의 시각적 합집합 중심을 전체 Stage 중심으로 취급했다. 가용 폭도 부모의 border-box 폭으로 구해 Border/Padding이 있을 때 실제 콘텐츠 영역과 달라졌다. 따라서 1920·1440px에서 Stage가 약 16.12px 오른쪽으로 이동했고, 비대칭 여백에서 약 51.44px 오차와 실제 콘텐츠 밖 배치가 발생했다.
|
||||
|
||||
실제 사용자에게 보이는 회색 배경 `section.hm-orgchart-story-full[aria-label="Full Composition"]`을 Content Wrapper로 사용한다. 경로는 StoryPage → StorySection의 `.sb-page-section-body` → 이 section → React OrgChart host/Shadow DOM → Core Scale Wrapper `.hm-orgchart-composition` → `.hm-orgchart-stage[data-workdesk-artwork]`다.
|
||||
|
||||
Wrapper의 BoundingRect에서 왼쪽 Border·Padding을 더하고 오른쪽 Border·Padding을 빼 콘텐츠 가용 영역을 계산한다. 그 중심과 변환된 전체 Stage BoundingRect 중심의 차이를 실측해 전체 Stage 하나에 CSS translate를 적용한다. 임의 고정 이동값·조직별 좌표 수정이 없으며 Core transform/width-height fit과 원본 1920×914.5px을 유지한다. P9.1의 카드 중심에 맞춘 추가 scale 보정은 제거했다.
|
||||
|
||||
| 화면 폭 | Wrapper Left | Wrapper Right | Wrapper Center X | Stage Left | Stage Right | Stage Center X | 수정 전 오차 | 수정 후 오차 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 1920px | 360.000 | 1560.000 | 960.000 | 360.000000 | 1560.000000 | 960.000000 | +16.117645 | 0.000000 |
|
||||
| 1440px | 120.000 | 1320.000 | 720.000 | 121.967201 | 1318.032753 | 719.999977 | +16.117462 | -0.000023 |
|
||||
| 390px | 16.000 | 374.000 | 195.000 | 16.000000 | 374.000000 | 195.000000 | +4.808695 | 0.000000 |
|
||||
|
||||
canonical 화면에서 Wrapper 중심이 Viewport 중심과 우연히 일치한다. 기준이 Viewport가 아님은 비대칭 조건에서 확인했다: 1440px Viewport 중심720, 실제 콘텐츠 중심680, Stage 중심680. Padding만 재분배하면 콘텐츠/Stage 중심714로 같이 이동한다. 1010/390px 동일 mount resize에서도 오차 최대0.000016px이다.
|
||||
|
||||
- Full Composition 브라우저 **43/43 PASS**, canonical3개+비대칭 Border/Padding+동일 폭 Padding 재분배+1010/390 resize7조건.
|
||||
- 152개 내부 요소의 DOM·정규화 상대 좌표·computed style 및 이미지139개의 SHA-256을 수정 전과 비교해 일치.
|
||||
- 카드43개·조직6개·연결경로5개·대표자·허브·협업 화살표·우측통계·좌측파견/휴직·하단관계사·배경/장식 유지.
|
||||
- 실제 rendered Stage와 모든 측정 콘텐츠가 부모 콘텐츠 영역에 포함됨. 잘림·카드/통계의 부적절한 충돌0건, 문서/사용 가능한 내부 스크롤바0건.
|
||||
- 기존 Core `overflow:hidden`은 변경하지 않았다. transform 전 자연1920×915 layout 때문에 Scale Wrapper raw scrollWidth/scrollHeight가 client 크기보다 큰 기존 값은 보존된다. 이를 raw overflow0으로 보고하지 않으며, 실제 변환 후 Stage/콘텐츠 포함 검사로 잘림이 없음을 별도로 증명했다.
|
||||
- 정렬 후12프레임 Stage style mutation0, observer 반복 루프 없음.
|
||||
- [수정 전 측정](../../test-results/org-chart-p9-2/before-composition.json), [수정 후 원본](../../test-results/org-chart-p9-2/browser-composition.json).
|
||||
|
||||
## Expanded 회사 선택
|
||||
|
||||
외부에 펼쳐진 회사 체크박스7개와 전체 회사/선택 해제 버튼을 제거하고, 기존 FilterBar 데이터 범위·직원 수 사이에 회사 선택 드롭다운1개를 배치했다. 공개 MultiSelect export가 없고 Select는 단일값 API이므로 실제 HM `Popover + Checkbox + Label + Button + Separator`를 렌더링한다. 기존 library 의존성을 사용하며 추가 설치 없음.
|
||||
|
||||
실제 P5-0 preset ID/이름 `hanmac/한맥기술, saman/삼안, jangheon-industry/장헌산업, jangheon/장헌, ptc/피티씨, halla/한라산업개발, baron/바론컨설턴트`를 그대로 사용한다.
|
||||
|
||||
| 선택 상태 | 닫힘 요약 | 전체 선택 상태 | 실제 결과 |
|
||||
|---|---|---|---|
|
||||
| 7개 | 전체 회사 (7개) | checked | 전체 회사 Root/기존 합산20명 |
|
||||
| 1개 | 한맥기술 또는 실제 선택회사명 | mixed | 해당 회사만 표시 |
|
||||
| 2개 | 2개 회사 선택 | mixed | 한맥기술+삼안 반영 |
|
||||
| 0개 | 회사를 선택하세요 | unchecked | 빈 forest·0명·오류 없음 |
|
||||
|
||||
- 전체 선택·해제·단일·복수·개별 제외·부분 선택에서 전체 선택을 확인했다. 닫고 다시 열어도 상태 유지.
|
||||
- 회사 선택은 draft/applied의 companyIds에 즉시 반영한다. 데이터 범위·직원 수의 기존 draft→데이터 적용 정책은 보존한다. 미적용 범위/인원수를 바꾼 뒤 회사만 체크해도 적용된 범위/인원수는 그대로다.
|
||||
- 회사별100명: 한맥기술100명 → 삼안 추가 즉시200명. 회사별20/300명도 단일회사 실제 확인.
|
||||
- 전체 합산20/100/300명: 기존 P7 weighted partition 후 선택회사만 필터하는 계약 유지, 선택회사에 재분배하지 않음. 데이터 helper/직원JSON/Core 계약 source hash 동일.
|
||||
- TB/LR/Hybrid 대표모드에서 선택된6개 회사/직원 및 제외된회사 반영 확인. 흰 제목·연결 방향별 Border, 그룹 접기/Space 펼치기, HM 직원 Dialog identity/ESC/카드focus복귀, 실제 Zoom·pointer Pan·Fit PASS.
|
||||
- Enter로 열기, Tab으로 전체/회사checkbox 접근, Space 조작, ESC/trigger 클릭/외부 클릭 닫기, ESC trigger focus복귀 PASS. 접근성 이름 및 aria-checked true/mixed/false 제공, 기존 HM focus 스타일 사용.
|
||||
- 실제 public Popover Portal 사용. 1920/1440/768/390px에서 표시 및 요청한 닫힘/열림/전체/부분선택 캡처. 1920/768/390px 모든8개checkbox의 panel 포함·viewport 포함·가로overflow없음 확인.
|
||||
- Expanded 최초 원본44PASS/1FAIL 중 FAIL은 검증 코드가 존재하지 않는 `.hm-orgchart-empty` 안내요소를 찾은 문제였다. 기존 Core empty 계약은 자식이 없는 forest이므로 probe만 수정하고 실패한 항목만 재실행2/2PASS. 앱 코드는 변경하지 않았으며 성공한 테스트를 재실행하지 않았다. **최종45개 항목PASS**. 원본 실패기록은 보존하고 [종합/provenance](../../test-results/org-chart-p9-2/verification-summary.json)에 해소 근거를 연결했다.
|
||||
- [최초 원본](../../test-results/org-chart-p9-2/browser-expanded.json), [empty항목 보정](../../test-results/org-chart-p9-2/browser-expanded-empty.json).
|
||||
|
||||
## 기술 검증·보존
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---|
|
||||
| 변경 Story/helper Typecheck | PASS — `node node_modules/typescript/bin/tsc -p test-results/org-chart-p9-2/story-tsconfig.json` |
|
||||
| Storybook production build | PASS — `npm.cmd exec -- storybook build -o storybook-static --disable-telemetry`. 기존500kB chunk 경고/plugin timing 진단 유지, 오류없음 |
|
||||
| 신규 console/pageerror | 두 변경 및 보정/변수근거 실행 모두0건 |
|
||||
| 기존 공개메뉴·StoryID | Overview / Department Colors / Full Composition / Expanded OrgChart4개 동일. OrgChart root/Patterns 다음 메뉴설정 hash 동일, 기존P8 manager증거 재사용 |
|
||||
| Fixed/Auto CSS 격리 | Core/React 패키지와 공통CSS hash 동일. 신규StoryCSS는 설정컨트롤/Portal만 대상으로 하며 Shadow 내부 Fixed/Auto 카드·레이아웃을 변경하지 않음 |
|
||||
| Overview·Department Colors | JSX/스타일/helper/CSF hash 동일. P9.1 간소화 결과 보존 |
|
||||
| 코드리뷰·변경범위 | 독립 read-only 리뷰 finding0. 소스3개만 수정, baseline 나머지 파일 SHA-256 동일 |
|
||||
| diff/공백 | `git diff --check` PASS 및 신규 authored파일 whitespace 확인 |
|
||||
|
||||
Core/React 데이터 생성·레이아웃 알고리즘·Theme·카드SVG·회사색은 수정하지 않았다. 기존 P7~P9.1 PASS를 동일 소스 근거로 재사용하며 전체2100명 성능·과거전체suite·전체Guard는 반복하지 않았다.
|
||||
|
||||
## Guard·미검증
|
||||
|
||||
이번 PARTIAL/STORYBOOK Guard 원본은 **status/staticStatus/runtimeStatus NOT VERIFIED**, 신규FAIL0. 원본 issue2개:
|
||||
|
||||
- `TOKEN-001 NOT VERIFIED`: StoryCSS의 `--radix-popover-content-available-height`. HM 공개 Popover/Radix가 런타임에 선언하는 위치변수이므로 연결consumerTheme 정적 선언만으로 확인되지 않는다. 실제 변수값과 maxHeight 일치를 [추가 런타임 근거](../../test-results/org-chart-p9-2/browser-expanded-token.json)로 확인했다. 원본 미검증을 덮어쓰지 않았다.
|
||||
- `TOKEN-002 NOT VERIFIED`: 기존 StoryPage 동적class site. 소스불변.
|
||||
|
||||
styleEvidence는 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**, 결합styleVerification은 생성하지 않았으며 NOT VERIFIED다. 실제 computed/상호작용PASS는 사용자 디자인의도 승인으로 확대하지 않는다.
|
||||
|
||||
기존 P9 Guard의 status/staticStatus **FAIL**, runtime **NOT VERIFIED**, TOKEN-00110FAIL/TOKEN-00210NOT VERIFIED 및 패키지CSS SCOPE-0012NOT VERIFIED 원본/보고서/hash를 그대로 보존한다. 이번 PARTIAL 검사가 이전 전체범위 결과를 대체하지 않는다. 삼안·장헌산업 흰 제목 대비 잔여진단, Compact NOT SUPPORTED도 유지한다.
|
||||
|
||||
사용자 최종 디자인 승인·다른브라우저/실기기/스크린리더·모든옵션조합·SSR/배포는 NOT VERIFIED. 요청범위 완료 후 사용자 검토 대기하며 P10은 시작하지 않았다.
|
||||
|
||||
## 변경파일
|
||||
|
||||
| 파일 | P9.2 변경 |
|
||||
|---|---|
|
||||
| src/org-chart/center-composition-preview.ts | 실제부모 content-box 기준 전체Stage 중앙정렬 |
|
||||
| src/org-chart/org-chart-expanded.stories.tsx | HM 회사선택Popover·전체/부분/미선택·즉시회사반영 |
|
||||
| src/org-chart/org-chart-expanded-story.css | trigger/요약/Portal 콘텐츠레이아웃 |
|
||||
| examples/org-chart/verify-p9-2-composition.mjs | 중앙정렬 수정전후·포함·내부보존·scroll/resize검증 |
|
||||
| examples/org-chart/verify-p9-2.mjs | 회사선택·데이터·키보드·레이아웃·Portal 검증 |
|
||||
| docs/org-chart/implementation-report-p9-2.md | 이 보고서 |
|
||||
|
||||
test-results/org-chart-p9-2 및 storybook-static은 생성검증산출물이다. [시작baseline](../../test-results/org-chart-p9-2/baseline.json), [최종audit](../../test-results/org-chart-p9-2/final-state.json), [typecheck log](../../test-results/org-chart-p9-2/typecheck.log), [build log](../../test-results/org-chart-p9-2/build.log), [Guard원본](../../test-results/org-chart-p9-2/guard.json).
|
||||
|
||||
**Stage·Commit·Push·Merge·main checkout·PR·Pull·Reset·Rebase·Force Push 모두 미진행.** 시작과완료branch/HEAD 동일, staged파일0. 기존미커밋P7~P9.1변경 보존.
|
||||
|
||||
## 실제 Storybook·캡처
|
||||
|
||||
- [Full Composition](http://127.0.0.1:6062/?path=/story/orgchart--full-composition): [1920px](../../test-results/org-chart-p9-2/after-full-composition-1920.png), [1440px](../../test-results/org-chart-p9-2/after-full-composition-1440.png), [390px](../../test-results/org-chart-p9-2/after-full-composition-390.png).
|
||||
- [Expanded OrgChart](http://127.0.0.1:6062/?path=/story/orgchart--expanded-org-chart): [닫힘](../../test-results/org-chart-p9-2/expanded-closed.png), [열림](../../test-results/org-chart-p9-2/expanded-open-all.png), [전체선택](../../test-results/org-chart-p9-2/expanded-all-selected.png), [부분선택](../../test-results/org-chart-p9-2/expanded-open-partial.png), [2회사100명씩](../../test-results/org-chart-p9-2/expanded-closed-partial.png), [390px열림](../../test-results/org-chart-p9-2/expanded-open-390.png).
|
||||
- [수정전회사UI](../../test-results/org-chart-p9-2/before-expanded-1440.png). 수정전FullComposition은 같은폴더before-full-composition-1920/1440/390.png에 보존.
|
||||
@@ -0,0 +1,148 @@
|
||||
# HMDesign OrgChart P9.3 — Pan 유지 · 회사 안팎 소속 변경 Drag & Drop
|
||||
|
||||
RESULT: **PASS — P9.3에서 요청한 기능 변경 범위.** 회사 간 이동 금지 조건은 2026-10-13 사용자의 최신 요청으로 취소되었다. Guard의 NOT VERIFIED와 디자인 승인 상태는 기능 PASS와 별도로 보존한다.
|
||||
|
||||
## 작업 기준·시간
|
||||
|
||||
- 범위: 기존 Expanded React Storybook과 공유 Vanilla Core의 Auto 동작. 신규 업무 화면·Story·Navigation·서버 API 없음.
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 원격 조회·Fetch·Pull 없음.
|
||||
- Started At: 2026-10-13 11:11:19 KST
|
||||
- Completed At: 2026-10-13 11:50:58 KST
|
||||
- Actual Duration: 39분 39초
|
||||
- Estimated Duration: 최초 50~70분 — 분석10분 / 구현25~35분 / 통합 검증15~25분. 회사 간 허용 정책 변경 시 추가15~25분 예상 안내.
|
||||
- Variance: 최초 예상보다 10분21초~30분21초 짧음.
|
||||
|
||||
## 엔진·입력 처리
|
||||
|
||||
기존 `orgchart@6.0.0`의 `filterAllowedDropNodes`, `dropHandler`, `dragendHandler` 및 `nodedrop.orgchart`/`otherdropped.orgchart` 이벤트를 재사용한다. 이벤트의 기본 DOM 변경은 `preventDefault()`로 취소하고 검증한 불변 데이터로 Core를 갱신한다. 여러 회사는 독립 엔진 인스턴스이므로 모든 표시 트리에 Drop 후보를 계산하고 다른 회사 트리의 대상도 최종 검증한다. 새로운 DnD 의존성을 추가하지 않았다.
|
||||
|
||||
Core의 Pointer 처리에서 화면 좌표6px 임계값, Pointer capture, 실제 Shadow DOM hit-test를 담당한다. 빈 공간에서 시작하면 기존 Pan, 직원 카드에서 시작하면 직원 이동, 그룹 타이틀에서 시작하면 그룹 이동이다. 임계값 미만의 클릭은 기존 직원 팝업·그룹 접기를 실행한다. Drag 후 발생하는 포인터 click은 차단하며 키보드 click은 유지한다. ESC·Pointer cancel·포커스 상실은 이동을 취소한다. Viewport 밖 Drop은 데이터 변경 없이 거부한다.
|
||||
|
||||
브라우저 touch 에뮬레이션에서 카드 Drag와 빈 공간 Pan 모두 PASS. 자식의 implicit touch capture 변경을 상위 Viewport 취소로 오인하지 않도록 대상 요소를 구분한다. 실기기 터치는 NOT VERIFIED다.
|
||||
|
||||
## 소속 변경 계약·결과
|
||||
|
||||
기존 `organization.companyId`는 조직 트리의 회사, `employee.affiliationId`는 직원의 관계사 소속이다. 조직의 회사 값은 기존 상위 조직 상속 계약으로 판정하며, 같은 회사 이동에서 파견 등 기존 affiliation을 덮어쓰지 않는다. 기존 필드만 사용하고 데이터 스키마를 확장하지 않았다.
|
||||
|
||||
| 이동 | 실제 데이터 변경 | 보존 |
|
||||
|---|---|---|
|
||||
| 같은 회사 직원 | 대상 `organizationId` | `affiliationId`, 직원 ID·개인정보·전체 인원 |
|
||||
| 회사 간 직원 | 대상 `organizationId`와 대상 회사 `affiliationId` | 직원 ID·개인정보·전체 인원 |
|
||||
| 같은 회사 그룹 | 그룹 `parentId` | 하위 관계·회사·직원 소속·조직장·ID |
|
||||
| 회사 간 그룹 | 그룹 `parentId`, 전체 하위 조직 `companyId`, 전체 하위 직원 `affiliationId` | 하위 조직 연결·조직장·ID·직원 총원 |
|
||||
|
||||
회사 간 예시에서 `virtual-006`을 `saman-division-1`로 옮기면 실제 `organizationId=saman-division-1`, `affiliationId=saman`이 된다. Story의 회사별20명·한맥기술/삼안 표시 조건에서 한맥의10명 하위 그룹을 삼안으로 옮기면 양쪽 집계는 **한맥20→10명, 삼안20→30명, 선택 합계40명 유지**다. 숨긴5개 회사를 포함하는 세션 전체140명도 유지된다. 조직·직원 ID 중복은 없다.
|
||||
|
||||
다음 이동은 이유를 표시하고 데이터 변경 없이 거부한다: 조직장 직원, 회사 Root, 자기 자신, 자손 조직, 기존 부모/소속, 없는 source/target, 잘못된 데이터 및 회사 참조. 회사 간 이동 대상의 affiliation 항목이 없으면 임의 소속을 만들지 않고 거부한다. 회사 Root는 이동 대상이 될 수 있지만 Root 자체를 옮길 수 없다.
|
||||
|
||||
성공 시 검증된 새 데이터는 깊이 동결된 브라우저 상태가 된다. `OrgChartHandle.move()`, `getData()`, `onMove`를 통해 Vanilla/React에서 같은 정책을 사용한다. 원본 DataSource·JSON·서버·DB·localStorage에 기록하지 않는다. 전체 페이지 재로딩은 원래 데이터로 돌아간다.
|
||||
|
||||
## 재배치·색상·집계·팝업
|
||||
|
||||
현재 데이터를 기준으로 기존 자동 레이아웃, 부모 연결선, 직접/하위 포함 인원, 회사별 인원, affiliation 현황과 직원 팝업 context를 다시 만든다. 성공한 이동 시 열린 React HM Dialog를 닫아 이전 소속 정보가 남지 않으며, 다시 클릭하면 현재 조직·회사 정보를 표시한다. 기존 회사 palette에서 대상 회사 main color를 그룹 제목과 직원의 연결 방향 강조 Border에 적용한다. palette·카드 SVG·카드 크기 변경 없음.
|
||||
|
||||
이동 시 Zoom/Pan 값과 기존 조직 ID 기준 접기 상태를 보존한다. 숨긴 조직의 Drop 후보는 현재 표시 범위에서 제외한다. 이동 후 Fit은 다시 계산한 forest 전체를 포함한다. 회사 선택은 세션의 표시 필터로 처리하므로 숨겼다 다시 표시해도 소속 변경이 유지된다. `데이터 적용`은 지정 조건의 샘플을 다시 만들며 `초기화`는 최초 설정/소속/이동 안내로 돌아간다.
|
||||
|
||||
| 표시 유형 | 같은 회사 직원/그룹 | 회사 간 직원 | 회사 간 하위 그룹 | 연결선·Border·Fit |
|
||||
|---|---|---|---|---|
|
||||
| TB | PASS | PASS | PASS | PASS |
|
||||
| LR | PASS | PASS | PASS | PASS |
|
||||
| Hybrid | PASS | PASS | PASS | PASS |
|
||||
|
||||
Hybrid의 회사 간 직원 이동은 별도 실제 포인터 시나리오에서 회사·조직·색상·양쪽 집계·HM Dialog 갱신을 확인했다. Hybrid 시작 Depth 전체 조합, 특정 그룹 세로 배치 전체 조합은 NOT VERIFIED다. Compact 지원 범위는 기존과 동일하다.
|
||||
|
||||
TB에서 Zoom100%·65%·140%, Fit 이후 Pan한 상태의 Drop을 확인했다. LR/Hybrid 대표 실제 그룹 Drag와 회사 간 이동 후 세 유형의 Fit 포함 검사를 확인했다. 모든 Zoom×모드 조합을 재검증하지 않았다.
|
||||
|
||||
## 접근성·화면
|
||||
|
||||
직원 카드/그룹 타이틀에 초점을 둔 뒤 **Alt+M → 방향키 또는 Home/End로 대상 선택 → Enter/Space로 이동**, ESC/Tab으로 취소할 수 있다. 이동 가능·거부 후보는 선 모양과 텍스트로도 구분하고 live 안내를 제공한다. 성공 후 이동된 카드 또는 대상 제목으로 초점이 이동하며, 거부·취소 시 source 초점을 복구한다. Collapse가 꺼져 있어도 제목을 이동 source로 사용할 수 있다. 직원 팝업이 꺼져 있어도 키보드 이동을 위한 카드 초점을 제공한다.
|
||||
|
||||
Story는 기존 HM FilterBar·Button·Checkbox·Popover·Label·Select·Separator와 실제 HM Dialog를 유지한다. 데이터 조건은 기존 FilterBar API와 맞으므로 그대로 재사용한다. 기존 네 메뉴만 유지하며 신규 Navigation은 없다. 최종 회사별 인원/이동 안내를 390·768·1280·1440px에서 확인해 문서와 텍스트 overflow가 없었다. 실제 스크린리더·운영체제 보조기술 검증은 NOT VERIFIED다.
|
||||
|
||||
## 통합 검증
|
||||
|
||||
| 검사 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 새 이동 정책 단위 테스트 | 11/11 PASS | `node packages/org-chart/test.mjs src/data/moves.test.ts`, 실행 출력은 도구 기록에 보존 |
|
||||
| Vanilla Core 변경 검사 | 51개 고유 assertion PASS / FAIL0 | [통합 결과](../../test-results/org-chart-p9-3/verification-summary.json), `core-browser*.json` |
|
||||
| 실제 React Storybook 이동/입력 검사 | 117개 고유 assertion PASS / FAIL0 | 같은 통합 결과, `browser-editing*.json` |
|
||||
| Fixed·기존 메뉴·최초 반응형 최소 회귀 | 9/9 PASS | [원본](../../test-results/org-chart-p9-3/preservation.json) |
|
||||
| 최신 안내·회사별 집계 반응형 | 5/5 PASS | [원본](../../test-results/org-chart-p9-3/responsive-current.json) |
|
||||
| Core Typecheck·Build | PASS | `typecheck-core.log`, `build-core.log` |
|
||||
| React Typecheck·Build | PASS | `typecheck-react.log`, `build-react.log`; 최종 React 소스 기준 빌드 유지 |
|
||||
| 변경 Story Typecheck·Storybook production build | PASS | `story-tsconfig.json`, `typecheck-story.log`, `build-storybook.log` |
|
||||
| 신규 console/page 오류 | 0 | 각 브라우저 실행의 오류 결과 |
|
||||
| JSON·palette·원본 artwork 불변 / 기존 작업 보존 | PASS | [SHA-256·Git 감사](../../test-results/org-chart-p9-3/final-state.json) |
|
||||
| `git diff --check` | PASS | 최종 변경 상태 검사 |
|
||||
|
||||
고유 assertion 합계는 원본 실행 횟수의 합이 아니다. 같은 이름은 최종 결과로 교체했다. Reset의 잘못된 root selector 검사는 명시적 이름 대응표로 수정된 실제 Root/원래 소속 검사의 PASS를 연결했다. 최신 정책으로 취소된 회사 간 거부 검사6건은 최종 합계에서 제외하고 원본을 보존한다. 단위 테스트와 브라우저 assertion 수를 하나의 테스트 수로 합하지 않는다.
|
||||
|
||||
최초 실제 Drag hit-test가 detached fragment를 참조한 문제와 touch capture 문제는 소스 수정 후 실패 범위만 재검증했다. LR 검증에서 Pan 첫 좌표가 Viewport 밖으로 나간 문제와 Reset 검증의 존재하지 않는 root selector는 검증 코드만 수정했다. 초기 FAIL/진단 JSON을 삭제하거나 PASS로 덮어쓰지 않았다. 현재 통합 기능 결과에 미해결 FAIL 없음. 기존 P7~P9.2 전체 브라우저 suite·20/100/300명 전체 조합을 반복하지 않았다.
|
||||
|
||||
### Guard·미검증
|
||||
|
||||
최종 `STORYBOOK / PARTIAL` Guard 대상은 변경된 Expanded Story와 Auto editing CSS다. [Guard 원본](../../test-results/org-chart-p9-3/guard.json)의 **FAIL0 / status NOT VERIFIED / staticStatus NOT VERIFIED / runtimeStatus NOT VERIFIED**를 유지한다.
|
||||
|
||||
- `SCOPE-001`: package CSS가 증명된 Story graph 밖이다. 별도 Core build와 실제 Shadow DOM 브라우저 검사 결과를 제공한다.
|
||||
- 기존 `TOKEN-001`: Radix의 `--radix-popover-content-available-height` 선언 범위.
|
||||
- 기존 `TOKEN-002`: 공통 StoryPage의 동적 className.
|
||||
- `styleEvidence`: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**. 결합 `styleVerification`도 NOT VERIFIED다.
|
||||
|
||||
브라우저에서 회사 색상과 Border 및 editing token을 확인한 것은 해당 기능의 런타임 근거다. 전체 HM CSS 결합 디자인 승인으로 해석하지 않는다. 기존 P9~P9.2 전체 Guard FAIL/NOT VERIFIED와 보고서는 변경하지 않았다. 사용자 디자인 승인, 실제 터치 기기, 실제 스크린리더, Chrome 외 브라우저, 전체 레이아웃/옵션 조합, 300명 이상 Drag 성능, 서버 저장·SSR은 NOT VERIFIED다.
|
||||
|
||||
## Fixed 보호·변경 파일
|
||||
|
||||
Fixed Full Composition의 DOM hash, 이미지139개 SHA-256, 요소152개 computed style/정규화 좌표가 P9.2 최종 스냅샷과 일치했다. 중앙 정렬·직원 카드43개·연결경로5개·충돌0 유지. Fixed에 편집 상태·Drop 후보·안내를 추가하지 않았다. 이 검사 이후 회사 간 정책 변경은 Auto 데이터/bridge/Story에 한정되므로 정상 Fixed 검사를 반복하지 않았다.
|
||||
|
||||
시작 시 미커밋 authored 파일47개를 SHA-256과 패치로 보존했다. 그중 관련13개에 P9.3을 추가하고 나머지34개는 byte 단위로 동일하다. Full Composition Story·P9.2 보고서·기존 카드·palette·fixture·lockfile·package dependency는 변경하지 않았다. Git 전체 diff에는 누적 변경이 포함되므로 이번 범위를 Git 전체 경로 목록으로 간주하지 않는다.
|
||||
|
||||
관련 기존 파일13개:
|
||||
|
||||
```text
|
||||
docs/org-chart/expanded-usage.md
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/src/core/engine.ts
|
||||
packages/org-chart/src/core/expanded-content.ts
|
||||
packages/org-chart/src/core/expanded-viewport.ts
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/core/vendor.d.ts
|
||||
packages/org-chart/src/data/index.ts
|
||||
packages/org-chart/src/styles/org-chart-expanded.css
|
||||
src/org-chart/org-chart-expanded.stories.tsx
|
||||
```
|
||||
|
||||
새 authored 파일7개:
|
||||
|
||||
```text
|
||||
docs/org-chart/implementation-report-p9-3.md
|
||||
examples/org-chart/verify-p9-3-core.mjs
|
||||
examples/org-chart/verify-p9-3-preservation.mjs
|
||||
examples/org-chart/verify-p9-3.mjs
|
||||
packages/org-chart/src/core/expanded-editing.ts
|
||||
packages/org-chart/src/data/moves.test.ts
|
||||
packages/org-chart/src/data/moves.ts
|
||||
```
|
||||
|
||||
`packages/*/dist`, `storybook-static`, `test-results`, `_local`, logs 등은 검증 생성물이며 authored 변경 목록에서 제외한다. 기존 baseline 기록은 `_local/p9-3/baseline-files.json`과 `baseline.patch`에 남겼다.
|
||||
|
||||
**Stage·Commit·Push·Merge·main checkout·PR·Reset·Clean·Rebase·Force Push 미진행.** Staged0, 시작/완료 branch·HEAD 동일. P9.3에서 종료하며 P10을 시작하지 않았다.
|
||||
|
||||
## 실제 Storybook·캡처
|
||||
|
||||
- [Expanded OrgChart](http://127.0.0.1:6064/?path=/story/orgchart--expanded-org-chart): 실제 React production Storybook.
|
||||
- [TB 회사 간 그룹 이동 후 Fit](../../test-results/org-chart-p9-3/tb-cross-company-fit.png)
|
||||
- [LR 회사 간 그룹 이동 후 Fit](../../test-results/org-chart-p9-3/lr-cross-company-fit.png)
|
||||
- [Hybrid 회사 간 그룹 이동 후 Fit](../../test-results/org-chart-p9-3/hybrid-cross-company-fit.png)
|
||||
- [직원 이동 후보](../../test-results/org-chart-p9-3/tb-staff-drop-hover.png) / [직원 이동 완료](../../test-results/org-chart-p9-3/tb-staff-moved.png)
|
||||
- [키보드 이동 완료](../../test-results/org-chart-p9-3/keyboard-move-complete.png)
|
||||
- [최신390px](../../test-results/org-chart-p9-3/expanded-current-390.png) / [768px](../../test-results/org-chart-p9-3/expanded-current-768.png) / [1280px](../../test-results/org-chart-p9-3/expanded-current-1280.png) / [1440px](../../test-results/org-chart-p9-3/expanded-current-1440.png)
|
||||
- [Fixed 보존1440px](../../test-results/org-chart-p9-3/fixed-preserved-1440.png)
|
||||
- [공유 Vanilla Core](../../test-results/org-chart-p9-3/vanilla-cross-company.png)
|
||||
|
||||
미리보기 서버는 `_local/p9-3/serve.mjs`로 실행한 로컬6064 세션이다. 정적 Storybook 배포나 외부 공개는 수행하지 않았다.
|
||||
@@ -0,0 +1,125 @@
|
||||
# HMDesign OrgChart P9.4 — Department Colors · 회사별 데이터 · Drag & Drop ON/OFF
|
||||
|
||||
RESULT: **PASS — 요청한 P9.4 기능과 두 추가 수정 범위.** 기존 Guard FAIL과 미검증은 별도로 유지한다. P10과 Git 작업은 시작하지 않았다.
|
||||
|
||||
## 작업 기준과 시간
|
||||
|
||||
- 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/완료 HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`. 원격 조회·Fetch·Pull 없음; 현재 원격 SHA는 NOT VERIFIED.
|
||||
- 범위: 기존 React Storybook 두 페이지와 공유 Vanilla Core/React Auto 편집 설정. 신규 화면·메뉴·API·의존성 없음.
|
||||
- Started At: 2026-10-13 12:19:16 KST
|
||||
- Completed At: 2026-10-13 12:43:09 KST
|
||||
- Actual Duration: 23분 53초
|
||||
- Estimated Duration: 최초 45~60분. 이후 Department 이름 및 Expanded 외곽 스타일 추가 요청도 같은 작업에 반영.
|
||||
- Variance: 예상보다 21분 7초~36분 7초 짧음
|
||||
|
||||
## Department Colors
|
||||
|
||||
간소화 코드가 기존 Core에서 만든 대표자 DOM을 제외하고 있었다. 그 DOM을 보존하여 대표자 → 조직 타이틀 → 대형 그룹장 → 소형 직원 순으로 표시한다. 대표자 한 개, 146×74px, 대표이사, Noto Sans KR, 대표자 뱃지 0개이며 Full Composition과 SVG·사진·Typography가 같다. 원본 카드 Presenter/SVG를 수정하지 않았다.
|
||||
|
||||
실제 `src/center/center-stories.css`와 Tunnel의 `sample / stage / variant-grid` 클래스를 Department 전용 Shadow DOM에 연결했다. 공통 배경 `rgba(217,217,217,.48)`, Border `1px solid rgba(64,5,0,.14)`, Radius 8px, Shadow 없음, 내부 Padding 24px와 Gap 14px를 재사용한다. 조직별 Gradient·흰색 타이틀·직원 카드 모양·146×68 / 108×52 크기는 보존한다. 2열→모바일 1열이며 설명·Legend·대비 진단·카드별 Caption을 다시 추가하지 않았다.
|
||||
|
||||
추가 이름 요청은 **Department 디자인 미리보기에서만** 처리한다. 표본 이름 `홍길동`을 `홍 길 동`으로 형식화한 표시 데이터 사본으로 Core를 호출하며, 이름의 접근성 표기는 `홍길동`이다. 표시 중인 13개 카드와 Tooltip이 같은 이름을 사용한다. 모든 직원 레코드에 이름만 적용하므로 그룹장/일반 직원에도 동일 정책이다. 원본 JSON·Employee ID·직위·직책·뱃지·조직·사진은 그대로다. 공통 EmployeeCard renderer와 Full Composition/Expanded 데이터는 변경하지 않았다.
|
||||
|
||||
## Expanded 데이터 선택
|
||||
|
||||
데이터 범위 UI와 전체 합산 설명을 제거했다. 기존 HM `FilterBar / Popover / Checkbox / Label / Button / Separator / Select`를 실제 렌더링한다. 회사 선택·전체 선택/해제·복수 선택·선택 요약·ESC/외부 클릭·키보드 조작을 유지한다. 공개 helper의 `scope: 'total'` 기능과 Fixture는 보존했다.
|
||||
|
||||
직원 수는 항상 회사별20/100/300명이다. 최초7개×20명=140명, 1개×20/100/300명, 2개×100명=200명, 7개×300명=2,100명을 실제 DOM Employee ID와 회사 Root 집계로 확인했다. ID 중복과 조직장 중복 집계가 없고 0개 회사는 명시적 빈 상태다.
|
||||
|
||||
회사 선택은 즉시 표시를 갱신하며 숨김·복귀 시 완료한 이동을 보존한다. 직원 수는 기존 **데이터 적용** 정책을 유지한다. 적용/초기화 때 새 예시 Source를 만들고 세션 이동을 초기화하므로 새 Fixture와 섞이지 않는다. 전체7개 회사의 세션 Source를 유지하고 선택된 회사의 현재 소속 기준 인원 합계를 별도로 표시한다. 숨긴 회사를 포함한 `전체` 세션 인원과 선택 합계를 구분한다.
|
||||
|
||||
## 공유 Drag & Drop 정책
|
||||
|
||||
Core 옵션과 React prop에 `dragDropEnabled?: boolean`을 추가했으며 기본값은 **false**다. Vanilla는 `setDragDropEnabled(boolean)` / `getDragDropEnabled()`를 제공한다. React는 독립 layout effect로 같은 handle의 setter를 호출한다. 옵션 변경으로 재생성·Source 재요청·레이아웃 재계산을 하지 않는다.
|
||||
|
||||
실제 HM Switch의 명칭은 **Drag & Drop**, 상태는 ON/OFF다. OFF는 pointer·Alt+M·engine drop 판정·공개 `handle.move()`에서 차단하며 공개 이동은 `EDITING_DISABLED`를 반환한다. 순수 데이터 이동 helper는 기존 계약을 유지한다. Highlight·이동 커서/단축키 안내도 상태에 맞춘다.
|
||||
|
||||
ON은 기존 P9.3 엔진/검증/불변 이동을 재사용한다. 직원·그룹의 회사 안팎 이동, 하위 조직·직원 소속, 대상 회사색·인원 집계·Border·연결선·배치를 유지한다. Root·자기 자신·자손/순환·잘못된 대상 등 기존 보호 정책을 교체하지 않았다.
|
||||
|
||||
OFF 전환은 진행 중 pointer/keyboard 이동을 취소하고 capture·Drop 강조·pending 상태를 해제한다. Drag 후 잘못된 pointer click을 차단하며 이미 완료한 이동을 보존한다. Source load는1회, 데이터·DOM forest·Viewport가 옵션 변경 후 동일함을 Vanilla에서 확인했다. Pan·Zoom·Fit·접기/펼치기·직원 Dialog는 독립 기능이다. Fixed는 옵션을 무시하고 Auto setter/move를 거부하며 ON/OFF UI를 추가하지 않는다.
|
||||
|
||||
## Expanded 외곽 스타일 추가 요청
|
||||
|
||||
「확장형 조직도 · 가상 데이터」의 외곽만 실제 Center Storybook 공통 카드의 배경·Border·Radius로 통일했다. 기존 Wrapper에 `sb-center`를 연결하고 선언된 `--sb-center-card-*` 값을 전용 `--hm-orgchart-expanded-preview-*` 입력으로 전달한다. Wrapper의 기존 간격을 보존하고 `.hm-orgchart-expanded` 외곽에서만 세 입력을 소비한다.
|
||||
|
||||
내부 Viewport·직원 카드 배경·회사별 그룹 타이틀/흰 글자·컨트롤·Padding·레이아웃을 유지한다. Vanilla에서 입력을 생략하면 기존 외곽 기본값을 사용한다. 새 색상 복사·Theme/공통 Center CSS 수정·직원 카드 override는 없다.
|
||||
|
||||
## 통합 검증과 증거
|
||||
|
||||
- [기본 변경 범위 브라우저](../../test-results/org-chart-p9-4/browser.json): **78/78 PASS**, console/pageerror0. Department 공통 카드 비교, 회사별 인원/선택, OFF/ON 이동·Pan·Dialog·접기·취소, TB/LR/Hybrid 대표 동작,390/768/1280/1440px, 공개4개 Story ID.
|
||||
- [공유 Vanilla API](../../test-results/org-chart-p9-4/core-api-cancel-default-fixed.json): **21/21 PASS**, 기본 OFF·4종 이동 차단/허용·Source load1·capture 해제·키보드 취소·total helper 호환·Fixed 보호.
|
||||
- [Fixed 최소 회귀](../../test-results/org-chart-p9-4/fixed-preservation.json): **4/4 PASS**. P9.2 DOM hash·139개 자산 hash·152개 스타일/정규화 좌표 동일, 중앙 오차 약0px, 카드43개·연결선5개·충돌0, 편집 UI 없음.
|
||||
- [Department 추가 이름](../../test-results/org-chart-p9-4/browser-department-names.json): **7/7 PASS**, 13개 이름과 ID·직위·뱃지·SVG·사진·Typography·Gradient·크기 보존.
|
||||
- [외곽 스타일와 이름 모바일](../../test-results/org-chart-p9-4/browser-expanded-frame-names-mobile.json): **7/7 PASS**, 실제 공통 카드와3개 computed 값 동일, 내부 카드/타이틀색 보존, OFF/ON·390px, console/pageerror0. 회사 이동은 최종 스타일 캡처를 위한 예시이며 기존 이동 PASS를 반복 집계하지 않는다.
|
||||
- Core/React 패키지 Typecheck·Build PASS. 변경 Story/helper Typecheck PASS. 추가 이름과 외곽 CSS를 반영한 최종 [Storybook build](../../test-results/org-chart-p9-4/storybook-build-frame.log) PASS. 기존500kB chunk 경고와 plugin timing 진단은 유지한다.
|
||||
- `git diff --check` PASS. 독립 읽기 전용 Core/React/Department 리뷰에 새 구체적 버그 없음.
|
||||
|
||||
Vanilla 최초 실행은 테스트 HTML의 `id="chart"`가 `window.chart`와 충돌했고 Fixed probe가 기존 thrown Error를 반환값으로 오인했다. 앱 변경 없이 테스트만 수정했다. [원본 실패](../../test-results/org-chart-p9-4/core-initial.json)를 보존하고 실패한 구간만 재검사했다. 위 실행들의 중복 공통 오류 검사를 합산하여 고유 테스트 수로 주장하지 않는다. 추가 요청 후 기존 정상 기능 suite를 재실행하지 않았다.
|
||||
|
||||
## Guard와 NOT VERIFIED
|
||||
|
||||
[초기 PARTIAL Guard](../../test-results/org-chart-p9-4/guard.json): **status/staticStatus FAIL, runtimeStatus NOT VERIFIED**, 기존TOKEN-00110FAIL / NOT VERIFIED13. [P9/P9.3 비교](../../test-results/org-chart-p9-4/guard-comparison.json)에서 FAIL10 모두 기존과 같고 신규FAIL0이다. 새 Department CSS의 `?url` 로딩은 정적 graph에 증명되지 않아 SCOPE-001 NOT VERIFIED이며 실제 Shadow DOM computed 비교를 별도로 제공한다.
|
||||
|
||||
추가 요청 이후에는 수정 범위만 다시 검사했다.
|
||||
|
||||
- [이름 변경 Guard](../../test-results/org-chart-p9-4/guard-department-names.json): 기존FAIL10 / NOT VERIFIED10, status/staticStatusFAIL / runtimeNOT VERIFIED. styleEvidence STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS(해당 정적 scope) / FINAL NOT VERIFIED.
|
||||
- [Expanded 외곽 Guard](../../test-results/org-chart-p9-4/guard-expanded-frame.json): **FAIL0 / NOT VERIFIED3**, status/staticStatus/runtimeStatusNOT VERIFIED. package CSS scope, 기존Radix 위치 변수, 기존StoryPage 동적class 미검증. styleEvidence STATIC PASS / RUNTIME·DESIGN_INTENT·FINAL NOT VERIFIED.
|
||||
|
||||
초기 Guard 원본은 추가 요청 이전 소스 fingerprint의 기록이며, 뒤의 수정 범위 결과와 각각 보존한다. 결합 styleVerification은 생성하지 않았고 NOT VERIFIED다. 실제 기능/computed PASS를 전체 CSS 디자인 승인으로 확대하지 않는다. 기존 전체 Guard FAIL·삼안/장헌산업 흰 제목 대비 잔여 진단·Compact NOT SUPPORTED를 보존한다.
|
||||
|
||||
사용자 최종 디자인 승인·실제 터치 기기·스크린리더·Chrome 외 브라우저·모든 옵션 조합·2,100명 전체 성능/Drag 성능·서버 저장·SSR/배포는 NOT VERIFIED다. 기존 P7~P9.3의 승인 카드·배치·성능/메뉴 증거를 필요한 범위에서 재사용했다.
|
||||
|
||||
## 변경 파일과 작업 보존
|
||||
|
||||
시작 baseline697개(일반684 + 승인 카드 SVG byte 사본13)를 비교했다. 이번 요청 관련 기존 파일12개 변경,685개 동일, 누락0. [Source 감사](../../test-results/org-chart-p9-4/source-preservation.json)에 파일별 SHA와 보호 범위를 기록했다. Fixture7·승인 SVG13·카드 Presenter·palette·Full Composition·Overview·메뉴·의존성·HM UI/Pattern/Theme·Center 공통 CSS와 기존 설정/Popover CSS를 보존했다. baseline에서 빠진 일부 기존 한글 경로는 별도로 기록했으며 모든 저장소 파일의 byte 보존을 주장하지 않는다.
|
||||
|
||||
수정한 기존 authored12개:
|
||||
|
||||
```text
|
||||
docs/org-chart/expanded-usage.md
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/expanded-editing.ts
|
||||
packages/org-chart/src/data/moves.ts
|
||||
packages/org-chart/src/styles/org-chart-expanded.css
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
src/org-chart/core-component-preview.tsx
|
||||
src/org-chart/org-chart-expanded.stories.tsx
|
||||
src/org-chart/org-chart-expanded-story.css
|
||||
```
|
||||
|
||||
새 authored5개:
|
||||
|
||||
```text
|
||||
src/org-chart/department-colors-preview.css
|
||||
examples/org-chart/verify-p9-4.mjs
|
||||
examples/org-chart/verify-p9-4-core.mjs
|
||||
examples/org-chart/verify-p9-4-preservation.mjs
|
||||
docs/org-chart/implementation-report-p9-4.md
|
||||
```
|
||||
|
||||
`packages/*/dist`, `storybook-static`, `test-results`와 로그는 검증 생성물이다. 현재 Git 설정은 `김수현 <shkim4@samaneng.com>`이며 사용자 문서의 이름과 다르지만 설정을 변경하지 않았다. [최종 Git 상태](../../test-results/org-chart-p9-4/final-state.json)를 별도로 기록했다.
|
||||
|
||||
**Stage·Commit·Push·Merge·main checkout·PR·Pull·Reset·Rebase·Force Push 미진행.** 시작/완료 branch·HEAD 동일, staged0. 기존 P7~P9.3 미커밋 변경을 보존했으며 P9.4에서 사용자 검토를 기다린다.
|
||||
|
||||
## 실제 Storybook과 최종 캡처
|
||||
|
||||
- [Department Colors](http://127.0.0.1:6064/?path=/story/orgchart--department-colors)
|
||||
- [Expanded OrgChart](http://127.0.0.1:6064/?path=/story/orgchart--expanded-org-chart)
|
||||
|
||||
요청된7개 캡처는 추가 수정까지 반영한 현재 Storybook에서 새로 촬영했다.
|
||||
|
||||
1. [Department 대표자](../../test-results/org-chart-p9-4/department-representative-current.png)
|
||||
2. [Department 전체 간소화/이름 통일](../../test-results/org-chart-p9-4/department-full-current.png)
|
||||
3. [Expanded 데이터 범위 제거/최종 외곽](../../test-results/org-chart-p9-4/expanded-frame-scope-removed.png)
|
||||
4. [회사 선택 드롭다운](../../test-results/org-chart-p9-4/expanded-frame-company-dropdown.png)
|
||||
5. [Drag & Drop OFF](../../test-results/org-chart-p9-4/expanded-frame-off.png)
|
||||
6. [Drag & Drop ON](../../test-results/org-chart-p9-4/expanded-frame-on.png)
|
||||
7. [회사 간 이동 후 최종 화면](../../test-results/org-chart-p9-4/expanded-frame-company-moved.png)
|
||||
|
||||
추가: [Department390px](../../test-results/org-chart-p9-4/department-names-390.png), [Expanded390px](../../test-results/org-chart-p9-4/expanded-frame-390.png), [Fixed1440px](../../test-results/org-chart-p9-4/fixed-preserved-1440.png). 이름/외곽 추가 이전 캡처도 원본 기록으로 보존하고 최종 캡처와 구분한다.
|
||||
@@ -0,0 +1,49 @@
|
||||
# P9.5 추가 — Core 직원 팝업 X 닫기 버튼
|
||||
|
||||
RESULT: PASS (요청한 닫기 버튼 및 팝업 동작 범위)
|
||||
|
||||
## 변경
|
||||
|
||||
- 실제 HM Dialog의 `Button variant="ghost" size="icon-sm"`과 Lucide `XIcon`을 확인했다. HM Dialog/Button 자체는 수정하지 않았다.
|
||||
- Core에서 같은 두 SVG 경로와 viewBox, Stroke 2, Round cap/join을 native DOM으로 생성한다. React 의존성은 추가하지 않았다.
|
||||
- 버튼 28×28px, 아이콘 16×16px, Padding 0, 투명 Border 1px, 기본 Radius 3.2px, 상단/우측 CSS 8px를 적용했다. BoundingRect 기준 외곽에서 9px이며 팝업 Border 1px를 포함한 값이다.
|
||||
- HM 선언값/상속 변수를 사용한다. 기본 색 #0a221d, Hover #f4f1ea, Focus Border #266651 + 같은 색 50%의 3px Ring, 150ms transition, 눌림 Y +1px. Vanilla fallback은 실제 HM 선언된 기본값이다.
|
||||
- 우상단 absolute 버튼으로 바꾸면서 기존 텍스트 버튼의 Header 높이를 유지해 Title/본문/팝업 크기를 보존했다. 직원 정보 데이터와 닫기 이벤트 로직은 수정하지 않았다.
|
||||
|
||||
## 검증
|
||||
|
||||
- React Full Composition의 Core 팝업과 React 없는 public Vanilla Core에서 각각 1440×900 / 390×900 검증: 48 PASS, 0 FAIL.
|
||||
- 실제 HM Dialog의 Normal/Hover/Focus와 버튼 및 아이콘의 CSS/BoundingRect/경로를 비교했다. X 클릭, ESC, Tab/Shift+Tab trap, 직원 카드 Focus 복귀를 확인했다.
|
||||
- 기존 외부 클릭 정책 유지: Backdrop 클릭은 닫히지 않는다.
|
||||
- Dark Hover 및 눌림 실제 이동량만 추가 비교: React/Vanilla 4 PASS. HM은 CSS translate, Core는 transform을 사용하므로 초기 속성 이름 비교 실패를 실제 이동량 비교로 바로잡았다. 최초 결과는 states-initial.json에 보존했다. 제품 코드 추가 수정은 없었다.
|
||||
- 1440px React/Vanilla 팝업: 420×276.5px → 420×276.5px. 390px React/Vanilla 팝업: 358×276.5px → 358×276.5px. 모두 Title/본문 좌표 변화 0px; 본문 시작 y=386.25px 유지. 배경/Border/Typography/데이터도 동일하다.
|
||||
- 기존 닫기 버튼: 약 41.922×29.5px → 28×28px, 외곽 상단/우측 inset 25px → 9px.
|
||||
- 최초 실제 1440px 기준 측정을 저장했고, 나머지 크기의 전후 비교는 저장한 기존 CSS와 기존 닫기 markup을 동일 팝업에 잠시 적용해 측정했다.
|
||||
- Core Typecheck / Core Build / Storybook Build / git diff --check PASS. 신규 Console/Page 오류 0. P9.4/P9.5 전체 회귀는 반복하지 않았다.
|
||||
|
||||
Guard 1회: 원본 status/staticStatus NOT VERIFIED (SCOPE-001: package 구현이 proven Storybook graph 밖). styleEvidence STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS (검사 없음) / FINAL NOT VERIFIED. Guard 원본을 보존했고 위 실제 브라우저 증거를 별도로 기록했다. 디자인 최종 승인과 다른 브라우저/OS는 NOT VERIFIED.
|
||||
|
||||
## 파일과 증거
|
||||
|
||||
수정 구현: `packages/org-chart/src/core/employee-info-dialog.ts`, `packages/org-chart/src/styles/employee-info-dialog.css`.
|
||||
|
||||
추가 검증/보고: `examples/org-chart/verify-p9-5-core-dialog-close.mjs`, 본 문서. 생성물과 로그는 `test-results/org-chart-p9-5-core-dialog-close/`에 보관한다.
|
||||
|
||||
- 측정: `result.json`, `states.json`, `baseline-popup.json`, `reference.json`.
|
||||
- 캡처: `react-1440-popup.png`, `react-390-popup.png`, `vanilla-1440-popup.png`, `vanilla-390-popup.png`, `hm-reference.png`.
|
||||
- 검토 URL: http://127.0.0.1:6064/?path=/story/orgchart--full-composition
|
||||
- 기존 6064 서버 종료를 확인해 동일 URL의 loopback static Storybook/Core 서버를 복구했다. 사용자 기존 탭은 오류 화면의 data URL 정책으로 제어되지 않아 새 검토 탭에 실제 팝업을 열어 확인했다.
|
||||
|
||||
## 보존 및 Git
|
||||
|
||||
- 시작 시점 authored 파일 SHA256을 캡처했다. 기존 P9.5 레이아웃/화살촉, Expanded, HM UI, 기존 미커밋 파일은 이 작업에서 수정하지 않았다.
|
||||
- 작업 중 `docs/org-chart/ai-page-generation-rules.md`의 외부 변경을 감지했다. 이 작업에서 편집/복구/덮어쓰기하지 않았다.
|
||||
- `feature/orgchart-package`, HEAD `7614507583927a25a270963a74ff044be0557b9f` 유지, Staged 파일 0. Stage/Commit/Push/Merge/PR/Reset/Rebase 미진행.
|
||||
|
||||
Started At: 2026-10-13 13:51:35 KST
|
||||
Completed At: 2026-10-13 14:04:24 KST
|
||||
Actual Duration: 12분 49초
|
||||
Estimated Duration: 15분
|
||||
Variance: -131초
|
||||
|
||||
사용자 시각 검토 대기.
|
||||
@@ -0,0 +1,118 @@
|
||||
# HMDesign OrgChart P9.5 — Full Composition 적응형 배치
|
||||
|
||||
RESULT: **PASS — 요청한 Full Composition의 배치·디자인 및 직접 영향 범위.** 기존 Expanded P9.5 보고서와 P7~P9.4 기록을 보존했다. 사용자 디자인 승인은 별도 검토를 기다린다.
|
||||
|
||||
## 상단·중앙·하단 분리
|
||||
|
||||
기존 DOM에 내부 `data-composition-region`을 부여했으며 전체 DOM을 다시 만들지 않았다. 상단은 제목·기준일과 우측 인원 표, 중앙은 대표자·허브·6개 조직/직원·연결선·화살표·장식, 하단은 파견/휴직과 관계사 바로 구분한다. 상단은 콘텐츠 컨테이너의 앵커이며 sticky/스크롤 추적이 아니다. 하단 두 모듈은 각각 bottom 앵커를 유지한다.
|
||||
|
||||
원본1920×914.5는 디자인 기준으로 보존한다. 하나의 배율 `min(1, 실제 가용 폭/1920, 실제 가용 높이/914.5)`로 표시한다. 세로 여유가 있으면 Stage 사용 높이를 `가용 높이/배율`로 확장하되 카드·장식·Typography를 확대하지 않는다. 실제 상단 Rect의 가장 아래 지점과 하단 Rect의 가장 위 지점 사이에서 중앙 콘텐츠 합집합 Rect의 중심을 계산한다. 기준보다 새로 확보된 높이 범위 안에서만 수직 오프셋을 적용한다.
|
||||
|
||||
대표자·허브·조직 viewport·원본914.5px SVG overlay·배경 장식은 같은 중앙 오프셋으로 이동한다. meeting/grid의 bottom 좌표는 원본 기준 top 좌표로 환산했고 sky 높이도914.5로 유지한다. management 곡선은 Stage 대신 overlay 원점에서 측정하여 이동량이 두 번 적용되지 않도록 했다. 장식과 다른 요소의 전후 좌표 차이는 브라우저 서브픽셀 정밀도 안이며 grid의 bottom→top 변환에서 약0.016디자인px 반올림 차이가 있다.
|
||||
|
||||
높이가 부족하면 Stage914.5와 중앙 오프셋0을 유지하여 전체 비례 축소를 재사용한다. 기준 배치가 원래 사용하는 일부 세로 범위는 가로 위치가 다른 상단/하단 모듈과 공유한다. BoundingRect 합집합만으로 충돌을 판단하지 않고 실제 카드·타이틀·고정 모듈의 x/y 교차를 검사했으며 실제 겹침은0건이다. 모든 실제 콘텐츠가 가용 영역 안에 들어옴을 검사하여 기존 `overflow:hidden`만으로 잘림을 감추지 않았다.
|
||||
|
||||
P9.2 Content Wrapper 기준 가로 중앙 계산식과 전체 Stage 이동 단위는 그대로다. 1440×900의 새 소수점 배율에서 기존4자리 반올림이 `translate:1.6401↔1.6402px`로 왕복하는 현상이 확인되어0.001px 미만이면 보정 쓰기를 멈추는 조건만 추가했다. 계산 기준·공식·Stage X좌표계는 변경하지 않았다. 20프레임 style mutation은20→0건으로 안정화됐다.
|
||||
|
||||
부모 컨테이너의 높이만 바뀌는 경우도 같은 ResizeObserver 경로를 사용한다. 배율이 동일해도 중앙 위치가 바뀌면 기존 `hm-orgchart-scale` 내부 위치 변경 알림을 재사용하여 이름 툴팁을 이전 좌표에 남기지 않는다. 새 팝업·새 이벤트 API·새 라이브러리는 추가하지 않았다.
|
||||
|
||||
## 협업 화살표·하단 간격
|
||||
|
||||
수정 전 실제 화면에서 **5개 모두 정상 표시**되어 복원할 누락 자산은 없었다. `team-arrow-blue-short.png`2회와 blue-purple/purple-teal/teal-green각1회, 총4개 원본PNG·5개 SVG image를 보존했다. 중복 경로나 화살표를 추가하지 않았다. 로딩·SHA-256·카드/타이틀 비충돌·실제 캡처에서 화살표를 숨기기 전후의 픽셀 기여까지 확인했다. 원본 overlay z4와 조직 viewport z7은 유지하며, 수직 transform으로 생기는 axis stacking context는 z12를 명시하여 허브를 곡선 위에 유지한다.
|
||||
|
||||
파견·휴직 CSS 하단 앵커70→82디자인px로 **12px 위로** 옮겼다. 실제 기준 Rect는 `(x32,y748.5,w240,h84) → (x32,y736.5,w240,h84)`다. 관계사 바는 `(x57,y835.109375,w1806,h55.390625)`와 로고·문자·높이를 유지한다. 그룹/바 간격은 **2.609375→14.609375디자인px**다. CSS Grid의12px Stage padding 때문에 실제 관계사 바의 하단 inset은24px이며 이 값도 유지한다.
|
||||
|
||||
실제1920×914.5 콘텐츠 벤치마크에서 배율1, Stage 높이914.5, 중앙 오프셋0, 카드43개, 가로 오차0이며 위 간격은 화면상 **14.609375px**다. 축소 화면의 가시 간격은 같은 배율로 축소되며 모든 화면에서12~16화면px라고 주장하지 않는다.
|
||||
|
||||
## 화면별 실제 측정
|
||||
|
||||
모든 수치는 CSS px다. 중앙 이동은 기존 기준 좌표에서의 화면상 추가 이동이며, 가로 오차는 Viewport가 아닌 실제 `.hm-orgchart-story-full`의 Border/Padding을 제외한 content-box 중심과 전체 Stage 중심의 차이다.
|
||||
|
||||
- **1920×1080**: 배율62.50%, 중앙+114.043px, 가로 오차0. 상단Y297.50~420.752, 중앙Y433.604~915.898, 파견/휴직Y928.75~981.25, 관계사Y990.381~1025.00, 하단 모듈 간격9.131px.
|
||||
- **1920×1440**: 배율62.50%, 중앙+294.043px, 가로 오차0. 상단Y297.50~420.752, 중앙Y613.604~1095.898, 파견/휴직Y1288.75~1341.25, 관계사Y1350.381~1385.00, 간격9.131px.
|
||||
- **1440×900**: 배율62.329%, 중앙 이동0, 가로 오차+0.000061. 상단Y297.479~420.395, 중앙Y319.480~800.456, 파견/휴직Y749.054~801.411, 관계사Y810.517~845.041, 간격9.106px. 부족한 높이에서 기존 상대 배치를 유지하므로 중앙 합집합의 가용 영역 세로 중심 오차−24.756px를 숨기지 않는다.
|
||||
- **1280×1024**: 배율62.656%, 중앙+86.994px, 가로 오차−0.000050. 상단Y295.909~419.469, 중앙Y405.019~888.520, 파견/휴직Y874.070~926.701, 관계사Y935.855~970.561, 간격9.154px. 일부 세로 범위 공유가 있지만 실제 직원/고정 하단 요소 교차0건이다.
|
||||
- **390×844**: 배율18.646%, 중앙+183.147px, 가로 오차0. 상단Y308.237~345.008, 중앙Y497.966~641.851, 파견/휴직Y794.809~810.472, 관계사Y813.196~823.524, 간격2.724px.
|
||||
|
||||
1920×1080→1920×1440 동일 mount 높이 변경에서 상단은 그대로, 하단은360px 아래로, 중앙은180px 아래로 이동한다. 추가 세로 여백을 중앙 위아래로 분배하며 실제 가용 세로 중앙 오차는 충분한 화면에서 약0.000046px 이하이다. 1920/1440/390의 기존1px 가로 허용 범위를 충분히 만족한다.
|
||||
|
||||
문서 가로·세로 스크롤0, 실제 사용 가능한 내부 스크롤바0, 카드/타이틀/통계/화살표/하단 바 잘림0이다. transform 전의 자연 layout scrollWidth/scrollHeight가 더 큰 기존 raw 값은 [원본 JSON](../../test-results/org-chart-p9-5-full/after.json)에 함께 기록했다. raw overflow와 실제 표시 영역 포함 여부를 혼동하지 않는다.
|
||||
|
||||
- [수정 전 원본 Rect](../../test-results/org-chart-p9-5-full/before.json)
|
||||
- [수정 후 원본 Rect](../../test-results/org-chart-p9-5-full/after.json)
|
||||
- [요약 Rect·간격·배율](../../test-results/org-chart-p9-5-full/layout-summary.json)
|
||||
- [동일 mount·정렬 안정성·실제 기준 크기](../../test-results/org-chart-p9-5-full/stability-recheck.json)
|
||||
|
||||
## 직접 영향 검증과 보호
|
||||
|
||||
- 5개 화면의 배치·상대 좌표·크기/타이포·집계/로고·5화살표·장식·잘림·충돌·스크롤: **69/69 PASS**.
|
||||
- 실제 화살표 픽셀·PNG SHA, 4개 동일 mount Resize의 상대 좌표/5경로, management 끝점, native 직원 팝업·ESC·초점 복귀, 공개 그룹 링크: **기존 성공43개 보존**. 최초 정렬 안정성1FAIL은 원본에 보존하고 [영향 구간9/9 PASS](../../test-results/org-chart-p9-5-full/stability-recheck.json)로 연결했다. 동일 성공 검사와 공통 오류 검사를 하나의 총 테스트 수로 중복 합산하지 않는다.
|
||||
- 현재 Full Story는 organizationLinks를 지정하지 않아 그룹 anchor0개다. 이를 원래와 동일하게 보존했으며 공개 Core의 `organizationLinks`는 임시 readOnly mount에서 실제 href·keyboard focus·Enter activation까지 확인했다. 외부 페이지 이동은 차단했으므로 목적지 서버/네트워크 동작은 NOT VERIFIED다.
|
||||
- [부모 높이만 변경](../../test-results/org-chart-p9-5-full/anchor-notification.json): **5/5 PASS**, 오류0. viewport1920×1080을 유지하고 parent914.5→1014.5px로 변경했을 때 배율1, 중앙 오프셋0→89.7188px, 대표자·허브·조직의 이동89.718811px가 일치하며 기존 위치 변경 알림은1회 발생했다. 실제 paint 포함·문서/내부 스크롤 없음도 확인했다. 성공한69/43/9개 구간은 재실행하지 않았다.
|
||||
- Core typecheck/build, 수정된 Story helper 단독 Typecheck, 변경 반영 최종 Storybook build PASS. Storybook의 기존 큰 chunk/plugin timing 안내는 유지한다.
|
||||
- `git diff --check` PASS. 기존 CRLF 안내만 남는다. 독립 읽기 전용 구조/코드 검토에서 현재 Full 기준의 새 구체적 결함 없음.
|
||||
- Fixed SVG·Gradient·Noto Sans KR·사진·뱃지·Typography·원본 직원JSON·통계·로고·팝업/링크 구현은 수정하지 않았다. Expanded의 이전 P9.5 중앙 정렬 코드/Story/CSS·Overview·Department Colors·HM Component/Pattern/Theme·의존성도 시작 SHA-256을 유지한다. 기존 P7~P9.4 전체 suite는 반복하지 않았다.
|
||||
|
||||
[최초 interaction 원본](../../test-results/org-chart-p9-5-full/interactions.json) 및 [수정 전 정렬 루프](../../test-results/org-chart-p9-5-full/stability-before-fix.json)를 보존한다. 첫 Core Typecheck의 DOM 타입 선택 문제는 HTMLElement generic으로 수정했고 원본 로그를 보존했다. helper Typecheck의 최초 CLI는 TypeScript7의 `--ignoreConfig` 요구로 중단됐으며 해당 옵션으로 파일 범위를 검사했다. 이를 앱 오류로 보고하지 않는다.
|
||||
|
||||
## Guard·NOT VERIFIED
|
||||
|
||||
Core/CSS [초기 PARTIAL Guard](../../test-results/org-chart-p9-5-full/guard.json)는 FAIL0 / NOT VERIFIED3(SCOPE-001), 원본 status/staticStatus/runtimeStatus NOT VERIFIED다. 마지막 [위치 알림 수정1파일](../../test-results/org-chart-p9-5-full/guard-position-notify.json)도 FAIL0 / NOT VERIFIED1(SCOPE-001)이며 같은 package graph 한계다. 실제 build/browser 근거를 별도로 제공한다.
|
||||
|
||||
[가로 helper PARTIAL Guard](../../test-results/org-chart-p9-5-full/guard-alignment.json)는 issue0, status/staticStatus NOT APPLICABLE, runtime NOT VERIFIED다. 처음 잘못 지정한 file target의 Storybook graph 범위 진단은 별도 원본을 보존하고 프로젝트 root의 수정 파일 범위로 바로잡았다. 세 결과의 styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS(검사 항목 없음) / FINAL NOT VERIFIED이며 결합 styleVerification과 전체 CSS 디자인 승인은 NOT VERIFIED다. 기존 P9.4 Guard FAIL 기록을 덮어쓰지 않았다.
|
||||
|
||||
최종 사용자 디자인 승인, Chrome 외 브라우저·실기기·스크린리더·모든 숨김 모듈/동적 데이터 조합·외부 링크 목적지·SSR/배포는 NOT VERIFIED다. 이번 결과는 현재 Workdesk Full Composition과 직접 영향 경로다.
|
||||
|
||||
## 수정 파일과 Git
|
||||
|
||||
기존 authored 파일5개:
|
||||
|
||||
1. `packages/org-chart/src/core/org-chart-content.ts` — 논리 영역·가용 높이/중앙 Rect 배치·기존 위치 알림.
|
||||
2. `packages/org-chart/src/core/workdesk-artwork.ts` — overlay 기준 곡선 원점.
|
||||
3. `packages/org-chart/src/styles/org-chart-readonly.css` — 하단 간격·중앙 레이어/장식 앵커.
|
||||
4. `src/org-chart/center-composition-preview.ts` — 계산식 유지,0.001px 미만 왕복 보정 억제.
|
||||
5. `docs/org-chart/ui-composition.md` — 현재 Full 계약과 이전 기록의 구분.
|
||||
|
||||
새 authored 파일은 `verify-p9-5-full.mjs`, `verify-p9-5-full-interactions.mjs`, 이 보고서다. `test-results/org-chart-p9-5-full`, Core dist, storybook-static과 로그는 검증 생성물이다.
|
||||
|
||||
[시작 파일 SHA-256 감사](../../test-results/org-chart-p9-5-full/source-preservation.json):832개 중 위5개 변경,827개 동일,누락0. 브랜치`feature/orgchart-package`, 시작/완료 HEAD`7614507583927a25a270963a74ff044be0557b9f`, origin`https://gitea.hmac.kr/b25027/HMDesign.git`, staged0. 기준 원격 SHA는 사용자 제공값과 로컬 HEAD가 일치하며 현재 live remote SHA는 조회하지 않았다.
|
||||
|
||||
**Stage·Commit·Push·Merge·main Checkout·PR·Reset·Rebase·Force Push·Clean·Fetch·Pull 미진행.** Expanded 등 기존 미커밋 작업을 보존했다. 요청 범위 완료 후 사용자 검토를 기다린다.
|
||||
|
||||
## 실제 Storybook과 캡처
|
||||
|
||||
[Full Composition](http://127.0.0.1:6064/?path=/story/orgchart--full-composition)
|
||||
|
||||
- [1920×1080](../../test-results/org-chart-p9-5-full/after-1920x1080.png)
|
||||
- [1920×1440](../../test-results/org-chart-p9-5-full/after-1920x1440.png)
|
||||
- [1440×900](../../test-results/org-chart-p9-5-full/after-1440x900.png)
|
||||
- [1280×1024](../../test-results/org-chart-p9-5-full/after-1280x1024.png)
|
||||
- [390×844](../../test-results/org-chart-p9-5-full/after-390x844.png)
|
||||
- [화살표 실제 표시](../../test-results/org-chart-p9-5-full/interaction-artwork-1920x1080.png)
|
||||
- [직원 팝업](../../test-results/org-chart-p9-5-full/interaction-employee-popup.png)
|
||||
|
||||
## 시간
|
||||
|
||||
- Started At:2026-10-13 13:05:08 KST
|
||||
- Estimated Duration:약35분 (탐색10분·보완15분·직접 검증10분)
|
||||
- Completed At:2026-10-13 13:33:06 KST
|
||||
- Actual Duration:27분 58초
|
||||
- Variance:예상보다 7분 2초 짧음
|
||||
|
||||
## 대표자 화살촉 후속 정정
|
||||
|
||||
사용자 첨부에서 표시한 대표자 카드 아래의 상향 화살촉은 조직 간 PNG 화살표5개와 별개였다. 앞선5개 화살표 검증에는 이 누락 요소가 포함되지 않았다. Workdesk 원본 `org-arrow-jang-head`의 solid Chevron path와 색을 복원하고, Core의 Stage padding을 포함한 실제 카드 하단에 원본8.7디자인px 간격으로 배치했다. 점선 상단만 Chevron 아래로 이어지도록 보완했고 허브 방향 끝점과5개 기존 연결선·팀 화살표는 보존한다.
|
||||
|
||||
[후속 측정](../../test-results/org-chart-p9-5-arrowhead/after.json):1920×1080·1920×1440·390×844에서25/25 PASS, 화살촉0→1개, 실제 픽셀 표시·축 정렬·카드 비충돌·세로 이동·축소 확인. 이전 수치와 PASS는 당시 검사 범위의 기록이며 이 후속 보완과 구분한다. [복원 후 확대 캡처](../../test-results/org-chart-p9-5-arrowhead/after-1920x1080-detail.png).
|
||||
|
||||
최초3개 원본 모양 비교는 테스트 정규식이 `d` 대신 `kind`를 추출하여 FAIL이었다. [원본](../../test-results/org-chart-p9-5-arrowhead/after-initial.json)을 보존하고 실제 캡처된 DOM의3개 비교만 다시 평가했다. 성공한22개 브라우저 검사를 반복하지 않았다. Core Typecheck/Build·최종 Storybook Build·diff check PASS, console/pageerror0. [화살촉 변경1파일 Guard](../../test-results/org-chart-p9-5-arrowhead/guard.json)는 FAIL0/SCOPE-001 NOT VERIFIED1, 원본 status/staticStatus/runtimeStatus 및 전체 CSS 승인은 NOT VERIFIED다.
|
||||
|
||||
시작835개 파일 중 artwork와 이 후속 보고서2개만 변경,833개 동일,누락0. Expanded·카드·Theme·팀 PNG4종·이전 중앙/하단 배치 코드와 미커밋 작업을 보존했다. 열려 있던 Full Composition 미리보기 탭을 새 빌드로 갱신하고 실제 화살촉 DOM 표시를 확인했다. Stage·Commit·Push·Merge 미진행.
|
||||
|
||||
- Started At:2026-10-13 13:37:01 KST
|
||||
- Completed At:2026-10-13 13:46:55 KST
|
||||
- Actual Duration:9분 54초
|
||||
- Estimated Duration:약10분
|
||||
- Variance:예상보다 0분 6초 짧음
|
||||
|
||||
사용자 후속 요청에 따라 화살촉 위쪽 끝을 실제 대표자 카드 하단에 직접 붙였다. 이전8.7디자인px 간격을0으로 바꾸고 연결선 시작점도 함께 조정했다. [붙임 검증](../../test-results/org-chart-p9-5-arrowhead-touch/after.json):3개 화면25/25 PASS, 측정 간격 약0px(최대0.0001디자인px), 기존5개 협업 화살표·카드·허브·세로 배치 보존. [최종 확대 캡처](../../test-results/org-chart-p9-5-arrowhead-touch/after-1920x1080-detail.png). 기존 간격 기록은 당시 상태로 보존한다.
|
||||
@@ -0,0 +1,84 @@
|
||||
# HMDesign OrgChart P9.5 — Expanded 중앙 정렬
|
||||
|
||||
Expanded의 실제 표시 영역에 중앙 오차가 있어 공유 Core의 배치만 보완했다. 기존 P9.4 PASS suite는 재실행하지 않았다.
|
||||
|
||||
## 실제 측정 기준과 수정 전후
|
||||
|
||||
실제 React Storybook의 Shadow DOM에서 다음 경로를 측정했다.
|
||||
|
||||
`.hm-orgchart-content → .hm-orgchart-expanded → .hm-orgchart-viewport → .hm-orgchart-forest → .hm-orgchart-tree`
|
||||
|
||||
중앙 기준은 외곽 카드 전체가 아닌 Pan/Zoom 콘텐츠 캔버스 `.hm-orgchart-viewport`의 client 영역이다. `getBoundingClientRect().left + clientLeft + clientWidth / 2`를 사용하여 Border를 제외한다. 전체 표시 경계는 모든 회사의 조직 article(타이틀·직원 카드)과 조직 연결선 path의 합집합이다. Forest와 개별 Tree Wrapper의 BoundingRect도 별도로 기록했다. 연결선은 Forest 전체와 같은 transform을 사용한다.
|
||||
|
||||
1440×1100px 브라우저에서 외곽 Expanded Wrapper는 `left=120, width=1200`, Viewport는 `left=137, width=1166`, client 콘텐츠는 `left=138, width=1164`, 가로 중앙은 `720px`였다.
|
||||
|
||||
Forest/Tree Wrapper 자체의 수정 전 가로 오차는 약 0px였지만, LR의 뒤쪽 margin과 Hybrid의 앞쪽 padding 때문에 실제 카드·타이틀의 중앙은 달랐다. 단일 회사·20명 기준:
|
||||
|
||||
- TB 가로: `0 → 0px`.
|
||||
- LR 가로: `-14.697845 → 0px` (음수는 왼쪽).
|
||||
- Hybrid 가로: `+3.631989 → 0px`.
|
||||
- TB Fit 세로: `-10.751465 → 0px`.
|
||||
|
||||
1/2/7개 회사 × 회사당 20/100/300명 × TB/LR/Hybrid의 27개 조합에서 수정 전 최대 절대 가로 오차는 `14.697845px`, 수정 후 자동 정렬·Fit 최대 절대 가로 오차는 `0.00006103515625px`다. Fit 최대 세로 오차는 `10.751465 → 0px`다. 수치는 CSS px이며 브라우저 부동소수점 측정 정밀도를 포함한다. 자동 배치는 가로 중앙과 상단 여백만 맞추므로 세로 중앙 오차를 완료 조건으로 주장하지 않는다.
|
||||
|
||||
기존 자동 Fit과 수정 후 자동 중앙 배치는 배율 정책이 다르므로 각각 배율을 함께 기록했다. 단일 회사·20명은 TB `44.34% → 100%`, LR `61.24% → 100%`, Hybrid `30.27% → 100%`; 최초 7회사·20명은 `22.07% → 100%`다. 실제 카드/엔진 크기는 변경하지 않았다. 명시적 Fit에서는 TB 약45.78%, LR 약62.59%, Hybrid 약30.27%로 전체 표시 영역을 담는다.
|
||||
|
||||
LR/Hybrid에서는 여백을 포함한 Wrapper 박스 중심과 표시 영역 중심이 의도적으로 다르다. 예를 들어 LR 100%에서 Forest/Tree 박스는 콘텐츠 중앙보다24px 오른쪽에 있으며, 실제 표시 영역 중심은 중앙에 일치한다. Wrapper와 표시 영역을 혼동하여 박스 중앙만 PASS로 판단하지 않는다.
|
||||
|
||||
- [수정 전 1/2회사 측정](../../test-results/org-chart-p9-5/before.json)
|
||||
- [수정 전 7회사 측정](../../test-results/org-chart-p9-5/before-seven.json)
|
||||
- [수정 후 전체 측정](../../test-results/org-chart-p9-5/after.json)
|
||||
- [동일 조건 비교](../../test-results/org-chart-p9-5/centering-comparison.json)
|
||||
|
||||
## 최소 변경
|
||||
|
||||
- `expanded-viewport.ts`: 렌더링된 콘텐츠의 경계를 Forest 로컬 좌표로 측정한다. 자동 배치는 현재 배율을 유지하고, 명시적 Fit만 전체 표시 영역에 맞춰 축소하여 가로·세로 중앙에 배치한다. Center/Fit/수동 이동을 구분하므로 observer가 Fit 직후 상단 배치로 되돌리지 않는다.
|
||||
- `core/index.ts`: 재생성 전에 수동 이동 여부를 보관하여 새 Expanded controller에 전달한다. Pan·Zoom·100% 복원·화면 밖 focus 노출 이후에는 회사/데이터/레이아웃/Resize에 의해 배율과 이동 위치를 임의로 바꾸지 않는다. 명시적 Fit은 수동 보존을 해제한다.
|
||||
- `expanded-usage.md`: 자동 배치와 명시적 Fit, 수동 위치 보존 정책을 현재 동작에 맞췄다.
|
||||
|
||||
공개 API/React props/CSS/카드/엔진/연결선 renderer/Drag & Drop hit testing은 변경하지 않았다. Fixed는 기존 `createOrgChartContent` 경로를 유지하고 Expanded controller를 사용하지 않는다.
|
||||
|
||||
## 이번 레이아웃 검증
|
||||
|
||||
- 실제 Chromium `153.0.8010.12`, React Storybook: 중앙·Fit·탐색 **124/124 PASS**, console/pageerror0.
|
||||
- 자동 중앙27개: 가로 중앙 일치, 배율100% 유지.
|
||||
- Fit27개: 가로·세로 중앙 일치, 전체 표시 경계가 콘텐츠 안의16px 여백에 들어옴. 최대7개×300명=2,100명 Forest 포함.
|
||||
- 최초 렌더링과 회사 선택·20/100/300명 적용·TB/LR/Hybrid 변경에 새 경계 반영.
|
||||
- 수동 Pan 이후 Resize·회사 선택·직원 수·TB→LR에서 scale/x/y 동일. 이후 명시적 Fit으로 중앙 복귀.
|
||||
- 390/768/1280/1440px: 단일 회사의 자동 가로 중앙 및 Fit 가로·세로 중앙.
|
||||
- [변환 좌표 및 실제 포인터](../../test-results/org-chart-p9-5/geometry.json): **62/62 PASS**, console/pageerror0. 카드·연결선의 Forest 로컬 좌표는 Pan/Zoom/Fit 전후 최대 `0.000370833px` 차이로 동일하다. 각 상태·방향에서 직원 연결선20개, LR/Hybrid에서 조직 SVG route10개를 확인했다. 끝점 오차는 최대 `0.500552px`이며 정수 offset 치수와 소수점 렌더링 차이를 고려한0.75로컬px 허용치 안이다. TB의 기존 조직 pseudo-element 연결선 renderer/소스는 보존하고 공통 transform을 확인했으며 pseudo-route 내부 전체 재검증은 반복하지 않았다.
|
||||
- 중앙 배치→Fit→수동 Pan 상태에서 TB `virtual-022 → hanmac-cell-0`, LR `virtual-032 → hanmac-office-1`, Hybrid `virtual-027 → hanmac-team-sibling` 실제 pointer 이동 성공. 화면상의 source/target hit·allowed highlight·release 후 소속이 의도한 조직과 일치한다. [Hybrid 이동 캡처](../../test-results/org-chart-p9-5/geometry-pointer-hybrid.png).
|
||||
- Core typecheck/build와 수정 코드가 포함된 Storybook build PASS. 빌드는 실행 결과를 확인했으며 기존 P9.4 기능 suite는 반복하지 않았다.
|
||||
- `git diff --check` PASS. 기존 CRLF 안내 경고만 존재.
|
||||
- 독립 읽기 전용 검토에서 새 구체적 결함 없음.
|
||||
|
||||
수정 전 첫 측정의7회사 구간은 테스트의 회사명 매칭 오류로 중단됐다. 정상 완료한1/2회사 수치37개를 보존하고, 전체 선택을 사용하는7회사 구간19개만 보완 실행했다. 최초 측정1개가 중복이며 비교 JSON은 이름으로 합쳤다. localhost/브라우저 실행 제한은 도구 환경에서 해결했으며 앱 변경으로 해결했다고 주장하지 않는다.
|
||||
|
||||
좌표 probe의 [원본33PASS/18FAIL](../../test-results/org-chart-p9-5/geometry-initial.json)도 보존했다. 끝점12개 판단은 이미 캡처된 실제0.5px 차이를0.75로컬px 기준으로 해석했으며 브라우저를 다시 실행하지 않았다. 포인터 source/target을 조직도 캔버스와 브라우저 viewport 양쪽 안으로 제한하고 focus 후 client 좌표를 다시 측정하여 실패한 pointer 구간만 재실행했다. 최종62개는 보존/재해석한41개와 새 pointer20개 및 최종 오류 검사1개이며 원본 수와 중복 합산하지 않았다. 앱 소스를 변경하여 테스트 실패를 감추지 않았다.
|
||||
|
||||
## Guard 및 미검증
|
||||
|
||||
[변경 Core 2파일 PARTIAL Guard](../../test-results/org-chart-p9-5/guard.json)는 FAIL0 / NOT VERIFIED2다. 원본 status/staticStatus/runtimeStatus는 **NOT VERIFIED**이며 패키지 구현2파일이 proven Storybook graph 밖이라는 SCOPE-001이다. 실제 패키지 build와 브라우저 증거를 별도로 기록했다. styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS(검사 항목 없음) / FINAL NOT VERIFIED다. 결합 styleVerification은 생성하지 않았으며 전체 CSS 디자인 승인을 주장하지 않는다. 기존 P9.4 Guard FAIL과 미검증 기록은 보존한다.
|
||||
|
||||
실제 터치 기기·스크린리더·Chrome 외 브라우저·모든 Depth/개별 Hybrid override 조합·2,100명 성능·사용자 디자인 승인·서버/SSR/배포는 NOT VERIFIED다. 이번 검증은 중앙 배치와 그 변환 좌표의 영향 범위에 한정한다.
|
||||
|
||||
## 기존 작업 보존과 Git
|
||||
|
||||
[시작 파일 감사](../../test-results/org-chart-p9-5/source-preservation.json):829개 중 기존3개만 변경,826개 SHA-256 동일,누락0. Fixed Full Composition·카드·연결선·Drag & Drop·React·P9.4 Story/CSS/데이터·Theme/Component/Pattern의 시작 파일을 보존했다. Fixed의 기존 브라우저 PASS는 재실행하지 않고 소스 보존과 호출 경로 분리로 영향 범위를 확인했다.
|
||||
|
||||
- 폴더:`D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치:`feature/orgchart-package`
|
||||
- 시작/완료 HEAD:`7614507583927a25a270963a74ff044be0557b9f`
|
||||
- staged0. Stage·Commit·Push·Merge·Checkout·Reset·Clean·Fetch·Pull·Rebase·PR 미진행. 현재 원격 SHA는 NOT VERIFIED.
|
||||
|
||||
새 authored 파일은 `examples/org-chart/verify-p9-5.mjs`, `examples/org-chart/verify-p9-5-geometry.mjs`, 이 보고서3개다. `test-results`, `packages/org-chart/dist`, `storybook-static`는 검증 생성물이다. 기존 미커밋 변경을 커밋 대상으로 묶지 않았다.
|
||||
|
||||
[실제 Expanded Storybook](http://127.0.0.1:6064/?path=/story/orgchart--expanded-org-chart), [최초100% 캡처](../../test-results/org-chart-p9-5/after-initial.png), [단일 회사 Fit 캡처](../../test-results/org-chart-p9-5/after-single-fit.png).
|
||||
|
||||
## 시간
|
||||
|
||||
- Started At:2026-10-13 12:45:15 KST
|
||||
- Estimated Duration:약25분 (탐색7분·보완8분·레이아웃 검증10분)
|
||||
- Completed At:2026-10-13 13:04:27 KST
|
||||
- Actual Duration:19분 12초
|
||||
- Variance:예상보다 5분 48초 짧음
|
||||
@@ -0,0 +1,89 @@
|
||||
# HMDesign OrgChart P9 — Storybook 디자인 보완 보고
|
||||
|
||||
RESULT: **PASS — P9 변경 범위.**
|
||||
Overview와 Fixed의 표·관계사 내부 디자인을 공유하고 Department Colors 카드 미리보기 행을 정렬했다.
|
||||
기존 Guard FAIL·미검증과 접근성 잔여 이슈를 별도로 보존한다. P10은 시작하지 않았다.
|
||||
|
||||
## 작업 기준과 시간
|
||||
|
||||
- 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작·완료 HEAD: `7614507583927a25a270963a74ff044be0557b9f`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- 범위: `STORYBOOK_ONLY`, 실제 React Storybook, 기존 P7·P8 미커밋 변경 보존.
|
||||
- Started At: `2026-10-13 10:13:21 KST`
|
||||
- Completed At: `2026-10-13 10:28:23 KST`
|
||||
- Actual Duration: `15분 2초`
|
||||
- Estimated Duration: `35–50분` — 분석 10분 / 최소 수정 15–20분 / 통합 검증·보고 10–20분
|
||||
- Variance: `예상보다 19분 58초–34분 58초 짧음`
|
||||
|
||||
## 요청 항목별 결과
|
||||
|
||||
| 항목 | 결과와 근거 |
|
||||
|---|---|
|
||||
| Overview 제목 3개 | **관계사·회사 로고 / 조직·직위별 인원 현황 / 관계사별 인원 현황**. 기존 외부 제목 두 곳을 요청한 정확한 명칭으로 보완. |
|
||||
| 조직·직위별 인원 표 | **PASS**. Fixed와 동일한 제목·7개 조직 행·5개 직위 열·소계·합계 41명. 각 행·직위 합계 검산, 실제 DOM 및 computed Typography·배경·Border·Height·Padding·정렬·줄바꿈·Radius·Shadow·폰트 동일. |
|
||||
| Fixed Summary 재사용 | 기존 `createOrganizationTotals`와 `createAffiliationBar`를 Story 전용 선택형 `presentation="workdesk"`로 호출. 동일 `org-chart-p3-summary.css` selector를 공유. 표 디자인 중복 CSS 및 Fixed 절대 좌표 복사 없음. 기본 standalone Presenter와 공개 API는 유지. |
|
||||
| 관계사별 인원 현황 | **PASS**. 동일 로고·회사명·인원수·총계 41명·내부 Typography·Border·배경·Radius·Shadow 재사용. Overview 제목 유지. 페이지 폭에 따른 wrapping과 높이만 독립 조정하며 모바일 줄 구분선은 숨김. |
|
||||
| 로고 영역 | **PASS**. 기존 승인 로고 7개 모두 로드, 원본 자산·색·비율 및 `object-fit:contain` 보존. 로고 자체에 새 배경·Border·Shadow 추가 없음. 기존 외부 견본 컨테이너 유지. |
|
||||
| Department Colors | **PASS**. 기존 Fixed 조직색 6개·HEX·SVG 카드·폰트 보존. CSS subgrid로 같은 행의 제목·설명·컬러 legend 블록·카드·대비 안내 시작점을 정렬. 1920/1440에서 같은 행 카드 preview Y가 정확히 일치. 390에서는 단일 열·잘림 없음. 카드 종류별 146×68/108×52 및 대표자 146×74 유지. |
|
||||
| Full Composition | **PASS**. 수정 전후 원본 1920×915, Stage DOM SHA-256 및 검사한 모든 표·관계사·카드 computed 값 동일. 확정 배경·카드·허브·화살표·장식·파견/휴직·통계·하단 바 보존. 3개 화면에서 전체 bounds·비례 축소·휠 후 내부 offset 0, 문서 가로/세로 스크롤 없음. Fixed 레이아웃/JS 수정 없음. |
|
||||
| Expanded OrgChart | **PASS**. TB/LR/Hybrid를 1920×1080·1440×900·390×844에서 확인. 기존 20명 fixture·Fit·연결선·방향별 Border·7회사 색·흰 제목 유지. 대표 그룹 Hover·Focus·Collapsed·Expanded 흰색 확인. 카드 원래 외형과 Placeholder 유지. |
|
||||
| HMDesign UI Dialog | **PASS**. 실제 공개 Export `Dialog`와 `DialogContent` 등 사용. 직원 카드 클릭→단일 HM Portal·identity·focus, Core native popup 중복 없음, Tab 유지·ESC·닫기 버튼·카드 focus 복귀, 390px 표시 확인. React/Fixed/Vanilla 연결 소스 변경 없음. Fixed/Vanilla native 경로는 기존 P7·P8 PASS와 동일 소스 근거 재사용. |
|
||||
| 공통 Storybook 스타일 | **PASS**. 기존 `StoryPage / StoryPageHeader / StorySection` 및 공통 CSS 유지. 4페이지 실제 제목·설명·섹션 Typography/Border 동일. 페이지 envelope/gutter 설정 소스 보존. Core Shadow DOM 독립 Scope 유지. 코드 예제 형식 변경 없음. |
|
||||
| 메뉴·ID | **PASS**. 공개 4페이지·기존 ID/URL 유지. 독립 OrgChart root·Patterns 다음 순서·내부 숨김 정책은 변경하지 않은 P8 설정/CSF 및 기존 manager PASS 재사용. 내부 Story 삭제 없음. |
|
||||
| 변경 범위 검사 | 본 브라우저 **85/85 PASS**, 누락된 Expanded 1920px만 추가 **10/10 PASS**. 서로 다른 실행의 중복 오류 체크를 합산한 전체 고유 검사 수는 주장하지 않음. Core/Story 타입 검사·Core/Storybook production build·diff 검사 PASS. |
|
||||
| 신규 콘솔 오류 | 두 실행 모두 **0**, pageerror/console error 없음. |
|
||||
| 기존 검증 재사용 | P7 데이터 정책·2,100명·Zoom/Pan·독립 옵션·회사색/상태, P8 메뉴 위치/공통 frame/최소 기능 PASS를 관련 소스 동일 근거와 함께 재사용. 전체 과거 suite·2,100명 성능 검사 재실행 없음. |
|
||||
| Git 보호 | **Stage·Commit·Push·Merge·main checkout·Pull·Reset·Rebase·Force Push·PR 모두 미진행.** staged 파일 0, branch/HEAD 동일. 기존 809개 authored 파일 중 P9 대상 4개만 변경, 805개 hash 동일. 신규 authored 파일은 runner와 이 보고서 2개. |
|
||||
|
||||
## 수정 범위와 검증
|
||||
|
||||
| P9 파일 | 변경 |
|
||||
|---|---|
|
||||
| `src/org-chart/org-chart-page-previews.tsx` | Overview 정확한 제목, Fixed Summary Presenter 선택, 설명 정합화 |
|
||||
| `src/org-chart/core-component-preview.tsx` | Story-only 선택형 Workdesk Presenter, 기본 독립 Summary 유지 |
|
||||
| `packages/org-chart/src/styles/org-chart-p3-summary.css` | 기존 Fixed 스타일 selector 공유, Overview 흐름·wrapping만 보완 |
|
||||
| `packages/org-chart/src/styles/org-chart-content.css` | Department Colors 5개 콘텐츠 블록 행 정렬 |
|
||||
| `examples/org-chart/verify-p9.mjs` | P9 표·집계·스타일 동등성/반응형/보존/Dialog/메뉴 검증 |
|
||||
| `docs/org-chart/implementation-report-p9.md` | 이 보고서 |
|
||||
|
||||
집계 selector·fixture·회사색·승인 이미지·독립 Summary API·Fixed 좌표/레이아웃·Auto 알고리즘·React Dialog·공통 Theme·StoryPage·메뉴 설정은 변경하지 않았다.
|
||||
P8 helper의 다른 페이지 구현은 유지하고 Overview 부분만 수정했다.
|
||||
`packages/*/dist` 및 `test-results/org-chart-p9`는 기존 ignore 정책의 생성 산출물이며 Stage하지 않았다.
|
||||
|
||||
| 실행 | 결과 |
|
||||
|---|---|
|
||||
| `npm.cmd run typecheck:org-chart` | PASS, JS/API 변경 없이 공유 Core CSS의 패키지 계약 확인 |
|
||||
| `npm.cmd run build:org-chart` | PASS, 최종 CSS specificity 보완 반영 |
|
||||
| `node node_modules/typescript/bin/tsc -p test-results/org-chart-p9/story-tsconfig.json` | PASS, 수정 Story/helper 및 전체 관련 CSF 포함 |
|
||||
| Storybook 최종 production build | PASS, [최종 log](../../test-results/org-chart-p9/build-final.log). 기존 500kB chunk-size 경고와 bundler plugin timing 진단, build 오류 없음. |
|
||||
| P9 실제 브라우저 | [85/85 raw](../../test-results/org-chart-p9/browser.json), [1920 Expanded 추가 10/10 raw](../../test-results/org-chart-p9/browser-expanded-1920.json). Chrome 155.0.8059.39, 기존 Playwright 사용, 패키지 설치/업데이트 없음. |
|
||||
| 독립 Summary 최소 회귀 | 원래 internal Story의 상세 직접/하위 포함/합계 및 Affiliation 카드 layout 유지 확인. |
|
||||
| 소스 보존·Git | [baseline](../../test-results/org-chart-p9/baseline.json), [최종 audit](../../test-results/org-chart-p9/final-state.json), `git diff --check` PASS. |
|
||||
|
||||
동일 조건의 성공 검사를 반복하지 않았다. 최초 캡처에서 Overview bar wrapping의 CSS specificity 충돌을 발견해 preview-only selector를 보완했다. 해당 CSS 보완으로 기존 build가 무효화됐으므로 영향을 받는 Core/Storybook build만 갱신했다. 소스 변경이 없는 타입 검사는 반복하지 않았다.
|
||||
브라우저 85개 검사 이후 누락된 1920px Expanded 9개 모드/스타일 검사와 오류 검사를 추가했으며 기존 폭 검사는 반복하지 않았다.
|
||||
|
||||
## Guard·NOT VERIFIED·P10 잔여
|
||||
|
||||
[Guard 원본](../../test-results/org-chart-p9/guard.json)의 **status/staticStatus FAIL / runtimeStatus NOT VERIFIED**를 보존한다.
|
||||
[범위 분류](../../test-results/org-chart-p9/guard-scope.json)에서 P8 기존 **TOKEN-001 10 FAIL / TOKEN-002 10 NOT VERIFIED**가 모두 동일함을 확인했다. **신규 FAIL 0**이다.
|
||||
선택한 Core CSS 두 파일에는 **SCOPE-001 2 NOT VERIFIED**가 추가된다. 패키지 stylesheet는 Storybook 정적 import graph 밖이므로 Core build와 실제 브라우저 동등성/보존 결과를 별도 증거로 연결한다. 이를 Guard PASS로 덮어쓰지 않았다.
|
||||
|
||||
- Guard styleEvidence: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**.
|
||||
- 결합 `styleVerification`은 생성하지 않았으며 **NOT VERIFIED**. 기록한 실제 computed PASS는 전체 CSS/임의 외부 props의 인증 또는 사용자 디자인 의도 승인이 아니다.
|
||||
- 기존 삼안·장헌산업 흰색 제목의 일반 텍스트 대비 미달 진단 유지. 확정 회사색·흰색을 임의 변경하지 않았다.
|
||||
- Compact **NOT SUPPORTED**, 전체 옵션 조합·스크린리더·다른 브라우저·실기기·SSR/hydration·API/배포·장시간 2,100명은 기존 상태 유지/이번 **NOT VERIFIED**.
|
||||
- P10에서 사용자 최종 디자인 검토, 접근성 대비 해결 정책과 Guard 패키지 CSS 근거 범위를 우선 확정할 수 있다. 새 기능/Compact/API/Docker/알고리즘 수정은 별도 범위 확정이 필요하다.
|
||||
- **P10 및 Git 작업은 사용자 검토를 기다린다.**
|
||||
|
||||
## 실제 화면
|
||||
|
||||
- [Overview 1920px](../../test-results/org-chart-p9/overview-1920.png), [1440px](../../test-results/org-chart-p9/overview-1440.png), [390px](../../test-results/org-chart-p9/overview-390.png)
|
||||
- [Department Colors 1440px](../../test-results/org-chart-p9/department-colors-1440.png), [390px](../../test-results/org-chart-p9/department-colors-390.png), [모바일 상세](../../test-results/org-chart-p9/department-first-390.png)
|
||||
- [Full Composition 1920px](../../test-results/org-chart-p9/full-composition-1920.png), [1440px](../../test-results/org-chart-p9/full-composition-1440.png), [390px](../../test-results/org-chart-p9/full-composition-390.png)
|
||||
- [Expanded TB 1920px](../../test-results/org-chart-p9/expanded-tb-1920.png), [LR 1920px](../../test-results/org-chart-p9/expanded-lr-1920.png), [Hybrid 1920px](../../test-results/org-chart-p9/expanded-hybrid-1920.png), [LR 390px](../../test-results/org-chart-p9/expanded-lr-390.png), [원래 크기 카드](../../test-results/org-chart-p9/expanded-card-100-percent.png)
|
||||
- [실제 HM Dialog 1440px](../../test-results/org-chart-p9/hm-dialog-1440.png), [390px](../../test-results/org-chart-p9/hm-dialog-390.png)
|
||||
|
||||
현재 미리보기: [Overview](http://127.0.0.1:6061/?path=/story/orgchart--logo-personnel-summary), [Department Colors](http://127.0.0.1:6061/?path=/story/orgchart--department-colors), [Full Composition](http://127.0.0.1:6061/?path=/story/orgchart--full-composition), [Expanded OrgChart](http://127.0.0.1:6061/?path=/story/orgchart--expanded-org-chart).
|
||||
@@ -0,0 +1,72 @@
|
||||
# HMDesign OrgChart v0.1 구현·최종 검증 보고
|
||||
|
||||
RESULT: **PASS** — 이번 요청의 Core/JSON/Vanilla 최소 완료 조건 12개 충족. React Wrapper·Storybook 및 미검증 후속 항목을 포함한 전체 MVP 완료 판정은 아닙니다.
|
||||
|
||||
## 개발 환경과 보존
|
||||
|
||||
- 작업 경로: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 작업 전/후 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c` (동일)
|
||||
- origin(fetch/push): `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- Local main, 공식 원격 main/Feature도 위 SHA 그대로. 운영 `D:\디자인시스템\HMDesign` Working Tree 깨끗함을 작업 전/후 읽기 전용으로 확인.
|
||||
- 시작 시 기존 미추적 파일: `docs/org-chart/*.md` 7개. 모두 SHA-256 일치 확인(7/7); 삭제·초기화·전면 수정 없음.
|
||||
- 기존 `packages/ui`, `packages/center-graphic`, Theme/Token/Storybook/AGENTS 및 Spike 수정 없음. 최종 tracked diff는 루트 package.json·package-lock.json뿐.
|
||||
- Stage/Commit/Push/Merge 없음. Staged diff 비어 있음. main/운영 프로젝트 수정 없음.
|
||||
- 수행 방식: Track A(Data), Track B(Core/CSS) 병렬 구현 + 읽기 전용 검토 작업자. Main만 package/export/build/examples와 최종 통합을 수정·관리함.
|
||||
|
||||
## 구현 결과
|
||||
|
||||
1. 신규 패키지: `packages/org-chart`, `@hm-design/org-chart@0.1.0`. React/React DOM peer·런타임 의존성 없음.
|
||||
2. 직접 의존성: `orgchart@6.0.0` 고정. upstream 전이 의존성 `html2canvas@1.4.1`, `json-digger@2.0.2`, `jspdf@4.2.1` 및 그 의존성이 lockfile에 추가됨. OrgChart 외 무관한 직접 의존성 추가 없음. export UI는 비활성화하며 export 라이브러리는 브라우저 번들에 포함되지 않음. upstream MIT 고지를 배포 산출물에 포함.
|
||||
3. Data Contract: 조직/직원/관계사를 ID로 분리하고 문서의 `displayOrder/jobTitle/profileImage/workStatus` 유지. 버전은 문서 초안의 `0.1-draft`. 최신 요청에 따른 최상위 schemaVersion 필수 추가; metadata.schemaVersion은 선택 호환 mirror로 일치 검사. relations/presentation 및 일부 metadata 확장 필드는 선택. 조직·직원 ID namespace는 독립.
|
||||
4. JSON Source: 객체 또는 주입 URL, 선택 fetch를 지원. 객체는 생성 시 캡처하고 load마다 독립 불변 snapshot 반환. HTTP/JSON/취소 오류를 구별하고 서버 원문을 노출하지 않음. 기본 native fetch를 globalThis에 바인딩. 외부 JSON은 HTTP에서 검증; file:// 예제는 inline JSON 사용.
|
||||
5. Validation: 필수 값·타입·버전·날짜·중복 ID·참조·parent cycle·leader/관계사·상태·관계·presentation·위험한 HTML/이미지 URL 검증. 구조화된 code/path/message 반환. 외부 문자열은 textContent로 렌더링하고 engine ID는 내부 prefix+숫자로 생성.
|
||||
6. Summary: 고유 직원 ID 기준 raw total, 조직 direct/inclusive, 직위·관계사·상태 집계. 조직 이동/추가/삭제와 표시 옵션 독립성 테스트. 업무상 총원 포함 정책을 기본값으로 숨기지 않으며 미주입은 policyRequired, 미지원 정책은 unsupportedPolicy. 지원 정책 범위는 명시적 primaryOnly·leader 포함 및 유효 상태 목록. raw 숫자는 승인된 재직 총원이 아님.
|
||||
7. Core: createOrgChart, mount(container?), update, reload(source?), destroy, subscribe, getState. 마지막 시작 revision 우선·AbortSignal·immutable 입력·최종 commit 검사·재진입 callback 방어. mount 중 CSS load/error를 확인. 요청 및 data 실패는 error state, 잘못된 호출 순서는 사용 오류. destroy는 동기·반복 안전·terminal.
|
||||
8. Node UI: 조직·부서, 조직장·직원, 이름·직위·직책·상태, 사진/fallback, 원본 조직별 인원. 계층 연결선과 native 버튼 키보드 확장/축소. 여러 root는 독립 엔진으로 실제 조직 관계를 보존. 갱신 시 확장 상태는 초기화; 제거된 focus는 viewport로 복귀.
|
||||
9. Theme: 호스트의 기존 HM Theme 변수/폰트를 상속. node와 연결선은 기존 Semantic 변수, radius/font 사용. Shadow DOM 안에 upstream engine.css와 전용 style.css를 로드하여 전역 selector 누출 방지. 예제 Theme 산출물은 실제 공개 CSS Entry에서 생성하고 기존 폰트/OFL을 포함; 독립 유지되는 Theme 복제본 없음.
|
||||
10. Export: ESM index.js + index.d.ts, style.css, engine.css, classic browser subpath. IIFE는 window.HMOrgChart. 브라우저 번들에 unresolved bare import 없음. stylesheets의 실제 URL을 호스트가 stylesheetUrls로 주입해야 함.
|
||||
11. 예제: `examples/org-chart/dist/index.html`(HTTP ESM/외부 JSON), `classic.html`(직접 열기/Classic inline JSON). build outputs/public Entry만 소비하며 Spike 코드 참조 없음. reload·이동/추가 update·destroy·새 instance 생성 controls. 가상 조직·직원만 사용.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- **PASS** TypeScript: `npm run typecheck --workspaces --if-present` — OrgChart/UI/Center Graphic 모두 통과. root 앱 production build를 실행한 결과로 해석하지 않음.
|
||||
- **PASS** Package Build: ESM 123.50 kB(gzip 29.36), IIFE 126.34 kB(gzip 29.55), 선언/CSS/license 생성.
|
||||
- **PASS** 데이터 테스트 22/22: 계약/참조/순환/URL/정책/추가·삭제·이동/중복 방지/불변성/취소·늦은 응답/native fetch receiver 회귀.
|
||||
- **PASS** Chrome 155.0.8059.39 실제 브라우저 검사 69/69: HTTP ESM + file:// Classic 각각 mount/custom node/summary/reload/update; 네 너비 document overflow·내부 scroll geometry·HM color/font/radius·1줄 ID·Theme 연결선 정렬; keyboard Enter/Space·focus 복귀; depth-3 확장 state; 두 instance/Scoped Theme; source/validation error·latest revision·reentrant destroy·late callback 차단; 5회 mount/reload/destroy 자원 안정성. Console/runtime errors 0.
|
||||
- **PASS** Destroy 관찰 범위: 전용 DOM 제거·호스트 sibling과 다른 instance 보존·chart observer 0·전역 listener 수 안정·요청 abort·destroy 이후 callback 0. Playwright 자체 observer/listener를 chart 자원과 구분함. 전체 heap 회수를 판정한 결과가 아님.
|
||||
- **PASS** npm tarball consumer: 실제 tarball 추출, DOM 없는 public ESM import, public CSS/browser export, 엄격한 NodeNext 타입 소비(React types 없음), fixture validation/summary. React가 미설치된 사용자 npm 환경에서 새로 dependency install하는 테스트까지 수행한 것은 아님; HTML은 React를 로드하지 않음.
|
||||
- **PASS** 기존 main/설계 문서/패키지 소스 보존, Git diff --check, 문서 링크 검사(119 files/620 links/problems 0).
|
||||
- **NOT VERIFIED** AI Guard 원본 status/staticStatus/runtimeStatus. 1회 PARTIAL 실행에서 ENTRY-001/SCOPE-001, checkedFiles=[]: Vanilla package가 React consumer graph 전제에 포함되지 않음. Guard/Theme/Component 규칙 수정 없음. 원본 styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED, checks=[]이며 비어 있는 검사 PASS를 디자인 승인으로 사용하지 않음.
|
||||
- 별도 CSS 증거: 실제 선언 존재 확인 PASS, 위 제한된 browser computed·overflow/interaction PASS. **DESIGN_INTENT와 FINAL은 NOT VERIFIED**: reference 디자인 승인·전체 시각 parity를 주장하지 않음.
|
||||
|
||||
검증 증거: `test-results/org-chart-browser/report.json` 및 HTTP/file:// 네 너비 PNG, `test-results/org-chart-consumer/report.json`, `test-results/org-chart-preservation.json`, `test-results/org-chart-pack.json`. 이 생성물은 Git ignored이며 source test harness는 보존함.
|
||||
|
||||
## 변경 내역
|
||||
|
||||
- 수정: `package.json`(6개 OrgChart 실행 scripts), `package-lock.json`(workspace link와 upstream dependency graph). 이미 등록된 packages/* workspace는 변경하지 않음. 기존 @babel/runtime의 dev 표시 제거는 새 production 전이 dependency 영향이며 버전 교체가 아님.
|
||||
- 신규 패키지: package.json, README.md, tsconfig.json, tsconfig.build.json, build.mjs, test.mjs, src/index.ts; data/{types,index,validate,normalize,summary,snapshot,source,data.test}.ts; core/{types,index,engine,render}.ts, core/vendor.d.ts; styles/org-chart.css.
|
||||
- 신규 예제: index.html, classic.html, app.js, demo.css, data-v1.json, data-v2.json, prepare.mjs, serve.mjs, verify.mjs, verify-package.mjs, create-fixtures.mjs.
|
||||
- 신규 보고서: 이 파일. 기존 7개 설계 문서는 수정하지 않음.
|
||||
- dependency install은 초기 sandbox DNS 실패 후 승인된 실행으로 완료. npm cleanup에서 ignored node_modules 일부 EPERM 경고가 있었으나 설치 exit 0이며 후속 typecheck/build/runtime 모두 통과.
|
||||
- 공식 tag 웹 조회는 실패했으므로 npm 6.0.0 배포물과 특정 upstream git tag의 byte 동일성은 **NOT VERIFIED**. 설치 manifest와 lock integrity를 기준으로 버전을 고정함.
|
||||
|
||||
## 설계 정합성과 후속 작업
|
||||
|
||||
- Architecture/Framework Integration의 공통 데이터→검증→ViewModel→engine→DOM 및 terminal lifecycle 유지. 공개 target snippet에 필요한 CSS URL 계약 추가와 schemaVersion 위치 변화는 위에 명시. 기존 문서의 구현 전/미착수 상태는 과거 기록으로 보존; 신규 package README/이 보고서가 현재 실행 계약.
|
||||
- 다중 root는 임의 parent 없이 forest 렌더링. leader cross-organization은 정책 미정으로 LEADER_APPROVAL_REQUIRED. 조직 0개는 오류, 직원 0명은 valid ready.
|
||||
- hubText/showDispatched/showOnLeave는 reserved 필드이며 별도 허브/상태 영역 UI는 이번 최소 범위에서 구현하지 않음. showEmployees/showSummary/showAffiliations는 실제 지원. 추가 관계는 검증하고 계층선 미포함 안내만 표시; 복잡한 relation overlay 제외.
|
||||
- pan/zoom/minimap/drag/export 비활성화. 390px은 읽을 수 있는 원래 크기와 내부 가로 스크롤로 표시; 고급 반응형 처리·확장/scroll state 보존 제외.
|
||||
- React Wrapper는 ref/effect·Props/Source→동일 Core·unmount cleanup만 연결. Validation/집계/engine/node/style 중복 구현 금지. Storybook은 이후 가상 fixture·error/loading·policy·Theme scope·lifecycle 콘텐츠 Story로 구성.
|
||||
- 기술적 리스크: 6.0.0 내부 init observer hook에 의존하는 managed subclass. 버전 upgrade 전 재검토 필요. upstream anonymous node/transition 이벤트는 제거된 DOM과 함께 GC 가능한 참조가 되며 완전 GC·heap 회수는 **NOT VERIFIED**. 매우 큰 데이터의 성능/깊이 상한·보안 CSP/origin 정책·실제 API/auth·screen reader announcements·reference 시각 일치·Theme 미연결 동작·React runtime은 **NOT VERIFIED**.
|
||||
- Commit/Push/Merge는 요청대로 보류. 이후 사용자 승인 없이 수행하지 않음.
|
||||
|
||||
## 시간 기록 (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-12 10:00:28 KST
|
||||
- Completed At: 2026-10-12 10:19:19 KST
|
||||
- Actual Duration: 18분 51초
|
||||
- Estimated Duration: 최초 약 2~3시간
|
||||
- Variance: 예상 하한 2시간 대비 101분 9초 단축; 예상 상한 3시간 대비 161분 9초 단축
|
||||
|
||||
예상은 참고값입니다. 기존 PoC/설치 환경 재사용과 병렬 구현으로 실제 시간이 줄었으며 미검증 항목을 PASS로 대체하지 않았습니다.
|
||||
@@ -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초 짧음
|
||||
@@ -0,0 +1,91 @@
|
||||
# OrgChart Integration
|
||||
|
||||
상태: 공식 문서·소스 읽기 기반 기술 검토. 설치·실행·번들·브라우저 PoC는 수행하지 않았다. 모든 지원 완료 판정은 **NOT VERIFIED**다. 최신 범위는 Vanilla HTML/JavaScript + React이며 [Framework Integration](framework-integration.md)에서 같은 Core를 연결한다.
|
||||
|
||||
## 1. 엔진 버전·라이선스·근거
|
||||
|
||||
확정 엔진은 [dabeng/OrgChart](https://github.com/dabeng/OrgChart)다. 별도 프로젝트 `react-orgchart`를 채택한 것으로 해석하지 않는다. 확인일은 2026-10-12(KST)이며 웹 자료는 이후 변경될 수 있다.
|
||||
|
||||
- [공식 릴리스 목록](https://github.com/dabeng/OrgChart/releases)은 6.0.0을 최신으로 표시하고 v6부터 native JavaScript로 전환하여 jQuery 의존성을 제거했다고 설명한다. **6.0.0을 우선 검증 후보로 제안**하되 지원/고정 버전 확정은 아니다. 구버전 jQuery 연동 예제를 그대로 사용하지 않는다.
|
||||
- 확인한 [master package.json](https://raw.githubusercontent.com/dabeng/OrgChart/master/package.json)의 version은 6.0.0, main은 `dist/js/orgchart.min.js`, dependencies는 html2canvas/json-digger/jspdf다. 이 의존성을 Core의 필수 기능 또는 배포물에서 자동 제외 가능하다고 단정하지 않는다. Export/download는 요청되지 않았으므로 UI에는 추가하지 않으며 transitive bundle 영향은 조사한다.
|
||||
- [공식 LICENSE](https://raw.githubusercontent.com/dabeng/OrgChart/master/LICENSE)와 manifest는 MIT를 표시한다. 엔진 재배포 시 고지·라이선스 포함을 계획하고 실제 배포 artifact의 라이선스와 transitive 의존성은 별도로 확인한다.
|
||||
- [README](https://github.com/dabeng/OrgChart#readme), [JS 소스](https://raw.githubusercontent.com/dabeng/OrgChart/master/src/js/orgchart.js), [CSS 소스](https://raw.githubusercontent.com/dabeng/OrgChart/master/src/css/orgchart.css), [데모 index](https://dabeng.github.io/OrgChart/)를 읽었다. 데모를 브라우저에서 실행하거나 화면을 대조하지 않았다.
|
||||
- v6.0.0 tag의 raw 파일 조회와 release API 조회는 도구에서 실패했다. 따라서 **master 소스와 고정 tag/npm 배포물의 동일성은 NOT VERIFIED**다. 구현 전에 배포된 버전·tag·소스 hash·라이선스를 다시 대조하고 설치 후보를 고정한다. npm registry 배포/ESM entry/type 선언 존재도 아직 확인하지 않았다.
|
||||
|
||||
## 2. 기능·구조 검토와 책임 분리
|
||||
|
||||
아래는 upstream 문서/소스에 기능 경로가 있음을 확인한 결과이며 HMDesign에서의 동작 PASS가 아니다.
|
||||
|
||||
- **데이터·계층**: README는 JSON/UL 데이터와 `children` 기반 계층을 설명한다. Core의 flat ID 계약을 Engine Adapter가 파생 트리로 변환한다. engine에 UI 데이터를 원본 그대로 넘기지 않는다.
|
||||
- **커스텀 노드**: `nodeTemplate(data)`와 `createNode(node, data)` 경로가 있다. 읽은 master는 template 반환값을 문자열 결합하므로 Element 반환 지원을 가정하지 않는다. **정적 skeleton 문자열 + createNode callback의 DOM/textContent 구성**을 우선 검토한다. engine edge/계층을 지우지 않고 콘텐츠 전용 자식만 관리한다. 고정 버전의 callback 순서·크기 측정·확장/축소는 PoC로 확인하며 React root를 노드 안에 만들지 않는다.
|
||||
- **확장·축소**: show/hide parent/children/siblings 계열 API가 문서화돼 있다. 깊은 조직·custom 노드·focus·키보드와 함께 검증한다.
|
||||
- **pan·zoom**: pan/zoom과 한계 옵션이 있다. CSS transform·touch·wheel·호스트 스크롤·Overlay 좌표 결합은 미검증이다. 기능 노출과 설정 기본값은 PoC 후 확정한다.
|
||||
- **가로·세로 배치**: direction/verticalLevel/hybrid/compact 관련 문서가 있다. 일부 조합 제약이 안내되므로 모든 조합 지원으로 쓰지 않는다. 가로/세로 스크롤은 콘텐츠 컨테이너 책임과 함께 검사한다.
|
||||
- **데이터 갱신**: `init`/`setOptions`의 데이터 교체 경로가 있다. 일관성·성능·확장/zoom 상태 유지까지 보장된 것은 아니다.
|
||||
|
||||
JS 소스의 init은 기존 chart DOM을 제거·재생성하고 node 생성은 입력에 parentId를 추가하는 경로가 있다. 템플릿 문자열은 `insertAdjacentHTML` 경로로 삽입되므로 외부 텍스트 직접 주입은 금지한다. Core가 복사한 engine 데이터와 안전한 텍스트/ID를 제공해야 한다. [소스 근거](https://raw.githubusercontent.com/dabeng/OrgChart/master/src/js/orgchart.js).
|
||||
|
||||
같은 소스에 이벤트 해제 함수와 Observer 사용은 있으나 공개 완결 `destroy()`는 읽은 소스/README에서 확인하지 못했다. Core의 Destroy는 **engine API가 아니라 우리가 보장해야 할 facade 계약**이다. 인스턴스 제거만으로 document 이벤트·Observer가 정리됐다고 주장하지 않는다. 소스 검토에서 관찰된 정리 한계는 최초 PoC의 차단 조건이며 필요하면 engine 버전/기능 제한/명시적 disposal 지원 또는 승인된 별도 통합 대안을 검토한다. 이번에는 upstream/기존 코드를 수정하지 않는다. [JS 검토 근거](https://raw.githubusercontent.com/dabeng/OrgChart/master/src/js/orgchart.js).
|
||||
|
||||
여러 인스턴스는 chartClass 관련 문서만으로 격리를 보장할 수 없다. 컨테이너 Element 전달·인스턴스별 DOM ID·container 내부 query·이벤트·focus·zoom을 실제 검증한다. `.orgchart .node .title/.content` 등 upstream CSS의 고정 크기·색상·generic descendant selector가 custom 콘텐츠와 충돌할 수 있다. engine CSS와 Core CSS를 컨테이너 내부로 제한하고 다른 HM 요소를 비교한다. [CSS 근거](https://raw.githubusercontent.com/dabeng/OrgChart/master/src/css/orgchart.css).
|
||||
|
||||
특수 곡선 연결선·중앙 허브·그룹별 자유 카드 배치·교차 조직 relation을 기본 기능만으로 표현할 수 있다는 근거는 확보하지 못했다. **NOT VERIFIED / 별도 PoC**다. 계층 연결선은 engine, 추가 관계는 Relation Overlay, 허브/summary/상태 그룹은 콘텐츠 Layout의 책임으로 나눠 검토한다. fixed 좌표·engine hierarchy 왜곡으로 reference를 맞추지 않는다.
|
||||
|
||||
update/reload도 cleanup 대상이다. master의 pan/zoom 해제 경로는 현재 chart에 저장된 handler를 조회하므로 init 후 이전 handler를 회수하지 못할 위험이 있다. **이전 chart 참조를 잃기 전에** pan/zoom·minimap·관련 observer와 이벤트를 정리할 수 있는지 PoC로 확인한다. 반복 재생성 후 container/document 이벤트 수·응답 횟수 증가를 검사한다. 소스상 위험이며 실제 누수 재현은 NOT VERIFIED다.
|
||||
|
||||
master는 모듈 평가 시 window/document를 참조하는 경로도 있어 DOM 없는 import 안전성을 주장할 수 없다. Core facade·데이터 계층 import를 engine 로딩에서 분리하고 지연 로딩 또는 승인된 변환 entry를 검증한다. native ESM export가 이미 존재한다고 가정하지 않는다.
|
||||
|
||||
## 3. HM Theme 연결
|
||||
|
||||
Core 노드는 framework와 무관한 동일 HTML/CSS다. React Wrapper가 다른 노드 디자인을 만들지 않는다. Token 정책은 [Runtime Theme](../guidelines/runtime-theme.md), [Token 가이드](../guidelines/테마-토큰-가이드.md), [Runtime Portability](../guidelines/runtime-portability.md)를 따른다.
|
||||
|
||||
확인한 선언은 [default.css](../../packages/ui/src/theme/default.css)의 `--hm-sem-bg-content`, `--hm-sem-fg-primary`, `--hm-sem-fg-secondary`, `--hm-sem-stroke-default`, `--hm-radius-container` 등이다. 노드 surface/상태/연결선의 **최종 매핑은 디자인 승인 전**이며 content 배경 변수를 Card White 면으로 자동 적용하지 않는다. Typography와 spacing은 모든 것이 공개 runtime 입력은 아니다. [card.css](../../packages/ui/src/theme/card.css), [avatar.css](../../packages/ui/src/theme/avatar.css), [table.css](../../packages/ui/src/theme/table.css), [tailwind.css](../../packages/ui/src/tailwind.css)의 실제 규칙을 참고하되 없는 `--hm-spacing-*` 같은 Token을 추측하지 않는다.
|
||||
|
||||
React 호스트는 기존 CSS entry `index.css → hm-theme.css`와 공개 HM CSS 순서를 유지한다. Core CSS는 별도로 연결한다. Vanilla는 browser-ready Theme CSS와 상대 fonts/assets가 필요하며 Tailwind 원본 CSS/bare npm import를 그대로 `<link>`에 넣지 않는다. [Theme-only 배포](../guidelines/theme-delivery.md)의 현재 `theme-dist/themes`는 JSON 산출물이므로 standalone CSS가 이미 제공된다고 오인하지 않는다.
|
||||
|
||||
Core scope는 전용 컨테이너 class 내부로 제한하고 HM 전역 변수 재선언·global reset·HM Component selector override를 금지한다. OrgChart 전용 alias가 필요하면 기존 의미의 변수를 참조하고 scope에서만 선언한다. Theme 없는 환경의 기본 스타일 제공/필수 연결/오류 정책은 TBD다. 임의 fallback 색으로 미정의 Token을 감추지 않는다. 정확한 standalone Theme artifact 경로·포함 범위·CSS import flatten·font 경로는 배포 PoC에서 확정한다.
|
||||
|
||||
Vanilla의 dependency/peer에 `@hm-design/ui` 전체 패키지를 추가해 Theme를 해결하면 React/React DOM/Tailwind peer가 설치 조건에 영향을 줄 수 있다. 승인된 호스트 CSS asset 또는 React peer 없는 Theme 산출 경로를 비교하고 React 미설치 consumer에서 실제 설치·동작을 검증한다. 현재 설치 동작은 NOT VERIFIED다.
|
||||
|
||||
현재 `packages/ui/src/theme.css` 원본은 전체 Component/Pattern CSS import와 절대 `/fonts/` 경로를 포함한다. Core prefix만으로 호스트 Theme까지 scope된다고 주장하지 않는다. Vanilla Theme는 전체 CSS와 필요한 변수·font 제공 범위를 구별하고 상대 font 경로를 확인한다. Core/engine CSS 누출과 호스트 Theme 자체의 전역 영향을 별도로 기록한다.
|
||||
|
||||
Theme 적용/해제의 owner는 호스트다. Core는 제공된 scope를 상속하고 Theme를 자동 주입/reset하지 않는다. 별도 Scoped Theme는 기존 `[data-hm-theme]`에서 mapping을 재계산하는 계약과 Scoped partial의 미지정 값이 HM Default를 쓰는 의미를 따른다. 단순 후손 override로 Global 값을 재계산한 것처럼 간주하지 않는다. Global A + Scoped B + 두 instance의 computed 값·독립성·cleanup을 검증한다.
|
||||
|
||||
## 4. JSON에서 REST API로 전환
|
||||
|
||||
공통 `OrgChartDataSource.load({signal}) → Promise<unknown>`은 normalized 계약 후보를 반환한다. JSON Mock·외부 JSON·API adapter는 모두 같은 validator·ViewModel·집계·Core renderer를 사용한다. [사용 예시](framework-integration.md#3-데이터-source-사용-예시)를 참고한다.
|
||||
|
||||
바뀌는 영역: 호스트의 Source 생성/선택, 실제 API client 설정, 원본 응답 매핑, 취소·권한·이미지 접근·향후 캐시/재시도 정책, Source 계약 테스트. 고정 계약과 다른 응답은 API adapter에서 변환한다.
|
||||
|
||||
유지되는 영역: normalized 필드/ID 계약(승인 버전 기준), runtime Validation, 집계 정책과 공통 함수, engine 변환, Core DOM·노드 디자인·Theme CSS, Core facade, React Wrapper의 Props 구조, 콘텐츠 단독 Story. Core나 Wrapper가 서버 URL/원본 필드에 직접 의존하면 완료 조건 실패다.
|
||||
|
||||
실제 API가 생겨 schema를 확장해야 하면 Source 전환만으로 끝나는 것처럼 표현하지 않고 contract migration을 별도 승인한다. Mock API Story는 지연/오류/응답 경쟁을 흉내내는 가상 Source이며 실제 서버·인증 검증으로 보고하지 않는다.
|
||||
|
||||
여러 endpoint/pagination으로 수집하면 Source Adapter가 완전한 collection과 같은 기준의 스냅샷을 조립해 한 번에 반환한다. 부분 페이지·서로 다른 버전의 조직/직원/관계를 Core에 순차 반영하지 않는다. ID 참조 검사를 통과해도 시간 일관성을 입증한 것은 아니다. 서버 일관성·버전 확인·재시도 방법은 실제 API 명세 전 TBD다.
|
||||
|
||||
## 5. API 전환 전 결정 항목
|
||||
|
||||
실제 명세가 없어 다음은 모두 TBD다: Endpoint·메서드·응답 schema·인증/인가·데이터 권한, 갱신 주기·캐시/만료·재시도/오류 처리, 프로필 이미지 권한/origin/만료, 개인정보 노출/로그/보존 정책, dataVersion·기준일·updatedAt의 의미와 충돌 처리. 임의 회사 주소·토큰·인증 규칙은 예시에 넣지 않는다.
|
||||
|
||||
HTML 환경도 상위 앱 API client가 인증을 담당한다. React 전용 hook/context를 API adapter의 필수 전제로 삼지 않는다. 공유 스냅샷이 같으면 두 환경의 조직 구조·집계·Theme·인터랙션 결과가 같아야 한다.
|
||||
|
||||
## 6. P3 조회 API 추가 — 2026-10-12
|
||||
|
||||
위 초기 기술 검토 기록은 보존한다. 현재 패키지의 추가 API는 [Core README](../../packages/org-chart/README.md#organization-links-and-employee-information-p3)와 [React README](../../packages/org-chart-react/README.md#public-api)에 기술한다. 런타임 검증 결과는 해당 구현 보고서에서 별도로 확인한다.
|
||||
|
||||
- `organizationLinks`는 기존 조직 ID별 외부 설정 URL map이다. 원본 계층의 1Depth 제목에만 적용하며 누락·빈 URL은 일반 제목으로 남는다. 실제 URL을 패키지에서 만들지 않는다. HTTP(S)·상대 URL을 허용하고 다른 scheme·잘못된 URL·credentials는 생성 시 `TypeError`로 거부한다. 링크는 native 키보드 이동·Focus 표시를 지원하며 카드 클릭과 분리된다.
|
||||
- 대표자·조직장·직원·파견·휴직 카드의 클릭·Enter·Space는 기본 조회 dialog를 연다. `readOnly`에서도 조회하며 수정 UI는 없다. 이름·직위·조직·직책·관계사·근무 상태 중 기존 값만 표시하고 없는 선택 필드는 생략한다. readOnly 가상 표시 이름은 카드의 `홍 길 동`과 일치하며 데이터는 변경하지 않는다.
|
||||
- 기본 native modal은 축소 Stage 밖의 같은 Core shell에 위치한다. 닫기 버튼·ESC·Focus trap·카드로 Focus 복귀를 처리하고 정보 팝업을 열면 이름 tooltip을 닫는다. HTML과 React는 같은 구현을 사용한다.
|
||||
- 선택적 `onEmployeeClick(employee, context)`를 제공하면 기본 팝업 대신 호스트 callback을 실행한다. context는 기존 `organization`·`affiliation`, Focus 가능한 `trigger` 카드, `originalEvent`를 담는다. 이 경우 호스트가 팝업 접근성과 정리를 담당한다. 기존 API를 제거하지 않는 추가 옵션이다.
|
||||
- Vanilla는 생성 옵션으로 전달하고 설정을 변경할 때 새 handle을 만든다. React는 같은 이름의 Props로 전달한다. 새 URL map 또는 callback 유무 변경은 재생성하며, callback 함수 identity만 바뀌면 최신 committed callback을 사용한다. 공개 Core `setOptions`는 추가하지 않았다.
|
||||
|
||||
```js
|
||||
const chart = createOrgChart({
|
||||
...configuredBaseOptions,
|
||||
organizationLinks: configuredOrganizationLinks,
|
||||
// 생략하면 Core 기본 조회 팝업 사용
|
||||
onEmployeeClick(employee, context) {
|
||||
openEmployeeInformation(employee, context);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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';
|
||||
<OrgChart dataSource={source} stylesheetUrls={stylesheets}
|
||||
layoutMode="auto" employeePopup="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하지 않았다.
|
||||
@@ -0,0 +1,903 @@
|
||||
# HMDesign OrgChart 공통 사용 가이드
|
||||
|
||||
개발자·Codex·Claude가 공개 API로 조직 데이터를 연결하기 위한 가이드다. **2026-10-13 P10.7의 검증 기준 Commit은 `d56d587fd20316f19554105f33c200060394962b`**(`feature/orgchart-package`)이다. 이 SHA는 당시 검증한 버전이며 향후 최신 버전을 뜻하지 않는다. 실제 받은 버전은 `git rev-parse HEAD`로 확인한다. 기능 검증 근거는 [최종 Feature 통합 보고서](final-integration-2026-10-13.md), 승인 대기 항목은 [P10.7 정식 통합 승인 준비](release-readiness-p10-7.md)를 따른다. 과거 P-AI1/P10.2의 제한을 현재 지원 여부로 사용하지 않는다.
|
||||
|
||||
빠른 시작은 아래 데이터 파일과 React 또는 Vanilla 진입점을 함께 복사한다. 예제는 Vite의 TypeScript 환경을 전제로 한다. 기본 예제는 제목·요약·툴바가 있는 Auto 화면이다. **Auto/Expanded Chart Only는 구현 및 기존 브라우저 검증이 완료됐으며**, 외부 UI 없이 삽입할 때는 [Chart Only 공개 API](#chart-only-공개-api-auto)를 사용한다. **Fixed Full Composition은 지원하지만 Fixed Chart Only는 미지원**이다.
|
||||
|
||||
## P11 외부 CSS 테마 (2026-10-13)
|
||||
|
||||
Core와 React는 같은 Shadow DOM CSS 정책을 사용한다. 공개 JS API·데이터 계약은 바뀌지 않는다. 외부 입력 `--hm-orgchart-*`는 호스트에서 상속되며, 내부 `resolved-*`/`current-*`는 실제 사용 요소에서 기본값을 계산한다. 회사색과 Fixed 부서색을 JS inline으로 덮어쓰지 않는다. 기본 파일만 연결하면 기존 회사색·카드·디자인을 유지한다.
|
||||
|
||||
### Shadow DOM 안에 외부 파일 연결
|
||||
|
||||
`stylesheetUrls.core`는 **기본 스타일 전체를 포함하는 한 개의 CSS URL**이다. 테마 변수만 있는 파일로 교체하면 기본 레이아웃이 누락된다. `engine`에는 기존 엔진 CSS URL을 유지한다. 일반 HTTP 서버에서는 공개 `style.css`를 복사한 경로로 import한다.
|
||||
|
||||
```css
|
||||
/* /themes/external-theme.css — 기본 CSS를 먼저, 컬러 override를 뒤에 */
|
||||
@import url('/assets/org-chart/style.css');
|
||||
:host {
|
||||
--hm-orgchart-bg-surface: #edf4ff;
|
||||
--hm-orgchart-bg-content: #dce8f8;
|
||||
--hm-orgchart-company-hanmac: #2457a7;
|
||||
--hm-orgchart-company-saman: #385ca8;
|
||||
--hm-orgchart-group-title-color: #fff4d6;
|
||||
--hm-orgchart-card-background: #f4f8ff;
|
||||
--hm-orgchart-card-border: #527aa9;
|
||||
--hm-orgchart-expanded-line-color: #249a79;
|
||||
}
|
||||
```
|
||||
|
||||
Vite에서는 `@import '@hm-design/org-chart/style.css';`를 사용하고 CSS URL로 import한다. 실행 가능한 전체 예제는 [external-theme.css](../../examples/org-chart-react/external-theme.css), [React·Vanilla 소비 fixture](../../examples/org-chart-react/p11.tsx)다. `!important`가 필요하지 않다. `:host`에 선언한 값은 내부 기본값의 fallback보다 우선한다. 동일 속성의 CSS 규칙을 직접 재작성할 때는 보통의 specificity·source order가 적용된다.
|
||||
|
||||
React:
|
||||
|
||||
```tsx
|
||||
import { OrgChart } from '@hm-design/org-chart-react';
|
||||
import engineUrl from '@hm-design/org-chart/engine.css?url';
|
||||
import externalThemeUrl from './external-theme.css?url';
|
||||
|
||||
<OrgChart dataSource={dataSource} layoutMode="auto"
|
||||
stylesheetUrls={{ engine: engineUrl, core: externalThemeUrl }} />
|
||||
```
|
||||
|
||||
Vanilla (공개 ESM을 서빙하는 환경):
|
||||
|
||||
```js
|
||||
import { createOrgChart } from '/assets/org-chart/index.js';
|
||||
const container = document.getElementById('chart');
|
||||
if (!container) throw new Error('chart 요소가 없습니다.');
|
||||
const chart = createOrgChart({
|
||||
container, dataSource,
|
||||
layoutMode: 'auto',
|
||||
stylesheetUrls: {
|
||||
engine: '/assets/org-chart/engine.css',
|
||||
core: '/themes/external-theme.css',
|
||||
},
|
||||
});
|
||||
await chart.mount();
|
||||
```
|
||||
|
||||
### 호스트 HTML CSS 상속 방식
|
||||
|
||||
호스트에서 변수만 설정할 때는 기본 `stylesheetUrls.core`를 유지한다. 다음 파일을 호스트 HTML의 `<link rel="stylesheet">`로 연결하고, React의 `className="orgchart-custom-theme"` 또는 Vanilla 컨테이너의 같은 class를 사용한다.
|
||||
|
||||
```css
|
||||
.orgchart-custom-theme {
|
||||
--hm-orgchart-company-hanmac: #2457a7;
|
||||
--hm-orgchart-company-saman: #385ca8;
|
||||
--hm-orgchart-expanded-line-color: #249a79;
|
||||
}
|
||||
```
|
||||
|
||||
이 방식은 **Custom Property 상속만** Shadow DOM을 통과한다. 호스트의 `.hm-orgchart-employee { ... }` 같은 selector는 Shadow 내부 카드에 적용되지 않는다. 내부 selector를 수정해야 하면 `stylesheetUrls.core` 파일 안에 작성한다. 호스트 class를 제거하면 기본값이 즉시 복원된다. 외부 테마를 동시에 쓸 경우 Shadow 파일의 `:host`에 같은 입력을 선언하면 조상에서 상속된 값보다 우선한다. 소비자가 직접 inline으로 입력 변수를 설정하면 소비자 inline이 우선하므로 CSS 파일 운영 시 이를 제거한다.
|
||||
|
||||
### P11.5 Fixed·Expanded 캔버스 배경 독립 설정 (2026-10-14)
|
||||
|
||||
캔버스 전용 입력은 `--hm-orgchart-fixed-canvas-background`와 `--hm-orgchart-expanded-canvas-background`다. 새 JS 옵션이나 React Props 없이 같은 Core CSS로 React·Vanilla에 적용된다.
|
||||
|
||||
- Fixed readOnly / Full Composition: 미지정 시 기존 `--hm-orgchart-bg-content` → `#F5F8FB` 유지. composition과 내부 stage에 함께 적용된다. Fixed interactive는 미지정 시 기존 투명 배경을 유지한다.
|
||||
- Expanded / Auto: 내부 viewport에만 적용. 미지정 시 기존 `--hm-orgchart-bg-surface` → `#FFFFFF`를 유지하여 기존 외부 테마 override가 무효화되지 않는다. 카드 배경·바깥 프레임의 `--hm-orgchart-expanded-preview-background`는 바뀌지 않는다.
|
||||
- Chart Only: 기본 흰 캔버스와 기존 Fit·Wheel Zoom·Pan 유지. Auto 내부 viewport이므로 Expanded 캔버스 입력을 지정하면 같은 방식으로 커스터마이징할 수 있다. Fixed Chart Only는 계속 미지원이다.
|
||||
- 각 전용 입력이 기존 공통 배경 입력보다 우선한다. 한쪽 전용 입력은 다른 유형에 영향을 주지 않는다. Storybook 페이지·Header·외부 Container는 적용 대상이 아니다.
|
||||
|
||||
호스트 HTML에 연결하는 CSS 예시:
|
||||
|
||||
```css
|
||||
.orgchart-canvas-theme {
|
||||
--hm-orgchart-fixed-canvas-background: #F5F8FB;
|
||||
--hm-orgchart-expanded-canvas-background: #FFFFFF;
|
||||
}
|
||||
```
|
||||
|
||||
React (앞 절의 공개 CSS URL import와 `dataSource` 사용):
|
||||
|
||||
```tsx
|
||||
import coreUrl from '@hm-design/org-chart/style.css?url';
|
||||
|
||||
<OrgChart className="orgchart-canvas-theme" dataSource={dataSource}
|
||||
displayMode="readOnly" stylesheetUrls={{ engine: engineUrl, core: coreUrl }} />
|
||||
<OrgChart className="orgchart-canvas-theme" dataSource={autoDataSource}
|
||||
layoutMode="auto" stylesheetUrls={{ engine: engineUrl, core: coreUrl }} />
|
||||
```
|
||||
|
||||
Vanilla (앞 절의 공개 ESM import와 CSS 서빙 경로 사용):
|
||||
|
||||
```js
|
||||
fixedContainer.classList.add('orgchart-canvas-theme');
|
||||
expandedContainer.classList.add('orgchart-canvas-theme');
|
||||
const stylesheetUrls = { engine: '/assets/org-chart/engine.css', core: '/assets/org-chart/style.css' };
|
||||
const fixed = createOrgChart({ container: fixedContainer, dataSource,
|
||||
displayMode: 'readOnly', stylesheetUrls });
|
||||
const expanded = createOrgChart({ container: expandedContainer, dataSource: autoDataSource,
|
||||
layoutMode: 'auto', stylesheetUrls });
|
||||
await fixed.mount();
|
||||
await expanded.mount();
|
||||
```
|
||||
|
||||
Shadow DOM 내부의 외부 CSS 파일을 사용할 때는 기존 기본 CSS import 뒤의 `:host`에 위 두 입력을 선언한다. 호스트 상속·`:host`·소비자 inline의 기존 우선순위는 그대로다. 같은 페이지에서 서로 다른 class 또는 각 컨테이너의 CSS 입력으로 인스턴스별 배경을 지정할 수 있다. 회사색·카드·연결선·팝업은 이 입력의 대상이 아니다.
|
||||
|
||||
### 공식 컬러 변수
|
||||
|
||||
아래 이름은 모두 `--hm-orgchart-` 접두사를 사용한다. `resolved-*`, `current-*`, `expanded-primary`, `expanded-card-primary`는 내부 계산 결과이므로 테마는 아래 공개 입력을 사용한다. 표에 없는 geometry API와 기존 table 변수는 그대로 유지한다.
|
||||
|
||||
| 입력 이름 (접두사 생략) | 적용 범위 / 기본값 정책 |
|
||||
|---|---|
|
||||
| `fixed-canvas-background`, `expanded-canvas-background` | Fixed composition/stage 및 Auto 내부 viewport의 독립 배경. 위 P11.5 절의 기존 입력 fallback 유지 |
|
||||
| `bg-surface`, `bg-content`, `bg-subtle`, `bg-brand` | 캔버스·내용·보조 배경·브랜드 배경. Fixed readOnly의 `bg-content` 미지정 기본값은 기존 `#F5F8FB` |
|
||||
| `fg-primary`, `fg-secondary` | 일반 텍스트 / 보조 텍스트 |
|
||||
| `stroke-default`, `stroke-input-default`, `stroke-input-focus` | 일반 Border / 입력 Border / Focus |
|
||||
| `accent-color`, `on-accent`, `action-primary`, `on-action-primary` | 기존 공통 강조·액션 색 |
|
||||
| `company-{id}` | Auto 회사 제목과 해당 직원의 소속 회사 강조선. 소속이 부모와 다르면 직원 소속색을 사용 |
|
||||
| `company-{id}-secondary-{n}` | 기존 보조색 alias 입력. `{n}=1`은 hanmac/saman/jangheon-industry/jangheon/ptc/halla, `{n}=2`는 ptc만 있다. baron에는 보조색 입력이 없다. 현재 주요 제목·카드에는 primary만 사용 |
|
||||
| `group-title-background`, `group-title-color` | Fixed·Auto 그룹 제목 배경(단색/gradient), 글자. 배경 미지정 시 부서/회사색 |
|
||||
| `group-border` | Fixed 일반 조직 카드의 상단 Border. Auto에는 기본 그룹 외곽 Border가 없음 |
|
||||
| `card-background`, `card-color`, `card-position-color`, `card-border`, `card-accent` | CSS 카드 배경·이름/텍스트·Fixed 직위·Border·강조선. Fixed 부서별 배경과 일반 fallback 카드의 텍스트 입력은 공통 값보다 우선한다. SVG/compact 카드의 HTML 이름은 공통 `card-color`를 사용한다. 직원 카드 Border는 공통 `card-border`가 부서 Border보다 우선하며 상단 강조선은 `card-accent`가 우선한다. SVG 이미지 내부 색은 바뀌지 않는다 |
|
||||
| `leader-card-background`, `top-leader-card-background` | Auto 그룹장 / 대표자 카드의 별도 배경. 미지정 시 공통 카드 배경 |
|
||||
| `card-hover-background`, `card-focus-color` | Auto 카드 Hover 배경 / Core 직원 버튼 Focus 색(Fixed에도 Focus 적용) |
|
||||
| `expanded-line-color` | Auto CSS pseudo선과 SVG 조직·직원 연결선. 기존 기본 `#738399` |
|
||||
| `fixed-line-color` | Fixed 엔진 연결선, Workdesk overlay의 실제 SVG path/arrowhead. PNG 화살표 제외 |
|
||||
| `gradient`, `border`, `department-color` | 기존 Fixed 공통 부서 gradient·Border·제목 글자색 |
|
||||
| `{department}-gradient`, `{department}-border`, `{department}-department-color`, `{department}-card-background`, `{department}-card-color` | Fixed 6개 부서별 override. 기존 측정 gradient·기본값 유지 |
|
||||
| `status-title-background`, `status-title-color`, `status-title-border` | 파견·휴직 요약 제목의 공통 색. 기존 회색 gradient, readOnly 흰 글자 유지 |
|
||||
| `status-card-gradient`, `status-card-border`, `status-card-color`, `status-position-color` | Fixed 파견·휴직 요약 카드의 공통 gradient·Border·이름·직위. 기존 회색 기본값 |
|
||||
| `dispatched-title-background`, `on-leave-title-background` | 파견 / 휴직 제목의 독립 gradient, 공통보다 우선 |
|
||||
| `dispatched-card-gradient`, `dispatched-card-border`, `dispatched-card-color` | 파견 카드 독립 입력. Fixed 요약·Auto 상태 직원 카드에 적용 |
|
||||
| `on-leave-card-gradient`, `on-leave-card-border`, `on-leave-card-color` | 휴직 카드 독립 입력. Fixed 요약·Auto 상태 직원 카드에 적용 |
|
||||
| `dialog-background`, `dialog-color`, `dialog-border` | Core native 팝업 배경·글자·Border |
|
||||
| `dialog-close-color`, `dialog-close-hover-background`, `dialog-focus-color` | Core 닫기 버튼 기본·Hover·Focus. 미지정 시 기존 HM 호스트 값/기본 fallback |
|
||||
|
||||
회사 `{id}`와 기본색은 `hanmac #CB2E28`, `saman #F08300`, `jangheon-industry #4580A6`, `jangheon #1D418D`, `ptc #84338E`, `halla #0068B7`, `baron #0C367F`다. 등록된 ID와 기존 프리셋은 그대로다. Fixed `{department}`는 `business-management`, `business-planning`, `talent-growth`, `integrated-system`, `design-planning`, `solution-sales`다. 회사색 override로 Fixed 부서색을 바꾸지 않는다.
|
||||
|
||||
```css
|
||||
:host {
|
||||
--hm-orgchart-company-saman: #385ca8; /* Auto 회사 제목·소속 카드 강조선 */
|
||||
--hm-orgchart-business-planning-gradient: linear-gradient(180deg, #385ca8, #23417f);
|
||||
--hm-orgchart-business-planning-border: #385ca8; /* Fixed 부서: 별도 설정 */
|
||||
--hm-orgchart-dispatched-card-gradient: linear-gradient(180deg, #e9f9f0, #a9d8bd);
|
||||
--hm-orgchart-dispatched-card-border: #278857;
|
||||
--hm-orgchart-on-leave-card-gradient: linear-gradient(180deg, #f7efff, #d6bbeb);
|
||||
--hm-orgchart-dialog-background: #eef2ff;
|
||||
--hm-orgchart-dialog-focus-color: #8958ba;
|
||||
}
|
||||
```
|
||||
|
||||
색상 입력에는 유효한 CSS color를 사용한다. `*-background`는 적용되는 CSS 속성에 맞는 배경 표현식을, `*-gradient`는 `linear-gradient(...)` 같은 CSS image 표현식을 사용한다(Fixed 요약 카드는 `background-image`에 적용). CSS 파일만으로 동작하며 소스를 수정하거나 JS 색상 설정을 추가할 필요가 없다. 그룹 공통 배경을 설정하면 회사별 제목 배경보다 우선하므로 회사별 배경을 유지하려면 공통 배경을 생략한다. 상태 카드도 Auto의 회사 강조선을 유지한다.
|
||||
|
||||
### 런타임 변경과 기본 테마 복원
|
||||
|
||||
호스트 변수 class/값을 바꾸면 이미 mount된 차트에 반영되며 데이터·Zoom·Pan을 재생성하지 않는다. 서버의 CSS 파일 값을 바꾼 뒤 기존 link는 브라우저 캐시에 따라 다시 요청되지 않을 수 있으므로 CSS link를 갱신하거나 새 URL로 mount한다. React는 `stylesheetUrls.core` URL 변경 시 기존 lifecycle로 remount한다. Vanilla는 기존 handle을 `destroy()`한 뒤 새 CSS URL로 `createOrgChart()`·`mount()`한다. 새 테마 변경 API는 추가하지 않았다.
|
||||
|
||||
복원은 호스트 테마 class/link/소비자 inline 입력을 제거하고 Core URL을 기본 `@hm-design/org-chart/style.css?url` 또는 `/assets/org-chart/style.css`로 되돌린다. 일부 값만 복원하려면 외부 파일의 해당 선언을 삭제한다. `resolved-*`에 `unset`을 주는 방식은 사용하지 않는다.
|
||||
|
||||
### HM Dialog와 Fixed 이미지 한계
|
||||
|
||||
React `/hm-dialog`의 `employeePopup="hm-dialog"`는 Shadow 밖의 HM React Dialog다. 위 `dialog-*` 변수는 Core 팝업에만 적용된다. HM Dialog에는 기존 `@hm-design/ui/theme.css`와 `@hm-design/ui/theme`의 `applyRuntimeTheme`/`resetRuntimeTheme`를 사용한다([Runtime Theme 가이드](../guidelines/runtime-theme.md)). Portal은 DOM 경계를 따르므로 차트 컨테이너에만 적용한 Scoped Theme는 body Portal로 전달되지 않는다. 같은 팔레트를 원하면 HM Global Runtime Theme 입력과 OrgChart CSS 입력에 각각 연결한다. 자동 동기화나 HM Dialog 디자인 변경은 하지 않는다.
|
||||
|
||||
Fixed에서 회사 로고 PNG, `cards/*.svg`의 대표자·6개 부서 직원/그룹장 카드 이미지, `workdesk/Group.svg`, `bg_sky.png`, `bg_main.png`, `Frame2@3x.png`, `1-8-4x-1@2x.png`, `team-arrow-*.png`는 이미지 내부 픽셀 색상을 CSS 변수로 바꿀 수 없다. 카드 바깥의 Focus, CSS 제목·파견/휴직·배경과 실제 inline SVG 연결 path는 변경할 수 있다. 카드 CSS 배경을 바꿔도 SVG 이미지의 불투명 영역은 그대로다. 자산 재작성·필터 착색·이미지 교체는 하지 않았다.
|
||||
|
||||
검증 범위·Guard 원본 상태는 [P11 결과 보고서](implementation-report-p11.md)를 참고한다.
|
||||
|
||||
## 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다. 공유 방식은 공식 Gitea 소스 연결이며 Git Clone 후 의존성 설치·빌드를 별도로 수행한다. npm Registry Publish와 ZIP 배포는 현재 공유 방식에 포함하지 않는다.
|
||||
- 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
|
||||
# 검증 기준 고정: 기존 변경이 없는 새 Clone에서 실행한다.
|
||||
git checkout --detach d56d587fd20316f19554105f33c200060394962b
|
||||
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 소비자의 최소 의존성을 의미하지 않는다.
|
||||
|
||||
Git으로 받은 소스만으로 의존성 설치나 빌드가 완료되는 것은 아니다. `npm.cmd ci`는 해당 Commit의 lockfile을 사용하며 `npm.cmd run build:packages`가 공식 workspace 패키지 빌드 명령이다. 위 타입·테스트·앱 빌드 명령은 소비 환경 검증용 안내이며 P10.7 문서 작업에서 실행한 결과가 아니다.
|
||||
|
||||
### 버전 고정과 업데이트
|
||||
|
||||
- 브랜치 Clone은 이동하는 브랜치의 HEAD를 받는다. 재현 가능한 소비 버전은 전체 Commit SHA로 고정하고 `git rev-parse HEAD` 결과를 소비 프로젝트 기록에 남긴다. 위 detached checkout은 브랜치 포인터를 변경하지 않는다.
|
||||
- 업데이트 시 기존 로컬 변경을 먼저 확인·보존하고 `git fetch origin feature/orgchart-package`로 후보를 가져온다. 새 승인/검증 SHA를 확인한 뒤 `git checkout --detach <새로 확인한 전체 SHA>`로 바꾼다. 예시 자리표시자를 그대로 실행하지 않는다.
|
||||
- Commit을 바꾼 뒤 `npm.cmd ci`와 `npm.cmd run build:packages`를 수행하고 호스트 타입·빌드·핵심 동작을 확인한다. 같은 `0.1.0` package version만으로 두 소스 버전이 동일하다고 판단하지 않는다.
|
||||
- 기존 프로젝트에는 필요에 따라 Git Submodule 또는 Subtree로 소스를 연결할 수 있다. Submodule은 부모 저장소의 gitlink Commit, Subtree는 가져온 upstream Commit을 기록한다. 두 방식도 의존성 설치·빌드·호스트 연결은 별도다. 기존 UI Submodule 사용 사례를 **OrgChart Submodule/Subtree 통합 검증 완료**로 표현하지 않는다. OrgChart의 두 통합 방식은 **NOT VERIFIED**다.
|
||||
|
||||
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 로컬 검증 자료이며 Git으로 받은 소스에서 소비자가 만드는 설치 artifact다. tarball/ZIP을 별도 배포하거나 Registry에 게시하는 절차가 아니다. A/B/C는 별도 호스트 폴더에서 아래처럼 설치한다.
|
||||
|
||||
```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 <OrgChart
|
||||
dataSource={source} stylesheetUrls={stylesheetUrls}
|
||||
layoutMode="auto" displayMode="readOnly" autoLayout={autoLayout}
|
||||
dragDropEnabled={false} employeePopup="core"
|
||||
onStateChange={state => {
|
||||
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(<App />);
|
||||
window.addEventListener('pagehide', () => root.unmount(), { once: true });
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- React index.html -->
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>OrgChart</title></head>
|
||||
<body style="margin:0">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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 <OrgChart dataSource={source}
|
||||
stylesheetUrls={{ engine: engineUrl, core: coreUrl }}
|
||||
layoutMode="auto" dragDropEnabled={false} employeePopup="hm-dialog" />;
|
||||
}
|
||||
```
|
||||
|
||||
### 외부 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<HTMLDialogElement>(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 ? <dialog ref={dialog} aria-label="직원 정보"
|
||||
onCancel={event => { event.preventDefault(); onClose(); }}>
|
||||
<p>{selection.employee.name} · {selection.employee.position}</p>
|
||||
<button type="button" onClick={onClose}>닫기</button>
|
||||
</dialog> : null;
|
||||
}
|
||||
const source = new JsonOrgChartDataSource(singleCompany);
|
||||
export function CustomChart() {
|
||||
return <OrgChart dataSource={source}
|
||||
stylesheetUrls={{ engine: engineUrl, core: coreUrl }} layoutMode="auto"
|
||||
employeePopup="adapter" employeePopupAdapter={EmployeePopup} />;
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<!-- Vanilla index.html -->
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>OrgChart</title></head>
|
||||
<body style="margin:0">
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`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은 아니다. Chart Only는 Auto 전용 `presentationMode: 'chart-only'`이며 Fixed에서는 거부된다([Chart Only 공개 API](#chart-only-공개-api-auto)).
|
||||
|
||||
### 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는 없다.
|
||||
- `presentationMode: 'default'`에서 `initialView`를 생략하면 초기 배율 1·Bounds의 가로 중앙·상단 여유 배치다. `initialView: 'fit'` 또는 Chart Only 기본값은 전체 Bounds를 맞추고 가로·세로 중앙에 배치한다. 사용자가 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/presentationMode/initialView/wheelZoom을 바꾸려면 destroy 후 새 handle을 만든다.
|
||||
|
||||
React에서는 대응 prop으로 변경한다. dataSource 변경은 reload, autoLayout/interactions/dragDropEnabled/selectedCompanyIds와 Auto 팝업 변경은 기존 handle을 갱신한다. aggregationPolicy·CSS URL·layoutMode·displayMode·companyId·companyTopNodes·employeeConnections·zoom·organizationLinks·presentationMode·initialView·wheelZoom 값 변경은 handle을 재생성한다. 객체 옵션은 값으로 비교하므로 같은 값의 inline 객체 자체가 재마운트를 일으키지는 않는다. `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/⌘ + 휠, Chart Only는 modifier 없이 직접 휠 Zoom을 사용한다(`wheelZoom`으로 선택 가능). 휠은 커서 중심 확대·축소이며 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<OrgChartMoveResult> {
|
||||
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 <OrgChart dataSource={source} stylesheetUrls={stylesheetUrls}
|
||||
layoutMode="auto" dragDropEnabled={false}
|
||||
onStateChange={state => {
|
||||
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. [AI 페이지 생성 정책](ai-page-generation-rules.md#11-생성-방식-사전-선택)에 따라 생성 방식이 미지정이면 Chart Only / Full Page 선택 질문에 대한 답변을 받은 뒤 진행한다. 이미 명시된 방식은 다시 묻지 않는다. 현재 사용자 요청·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의 연결을 따른다. **생성 방식이 미지정이면 코드 작성 전에 Chart Only / Full Page를 질문하고 답변 후 구현한다.** Chart Only를 선택하면 조직도 캔버스만 표시하고 외부 제목·통계·설명·조작 패널을 만들지 않는다. Full Page를 선택하면 기존 HM UI와 OrgChart Core·React Wrapper로 요청된 UI와 전체 레이아웃을 구성한다. 이미 명시된 방식은 다시 묻지 않으며 Fixed·Auto는 생성 방식 선택을 대신하지 않는다.
|
||||
|
||||
#### Chart Only 공개 API (Auto)
|
||||
|
||||
Auto(`layoutMode: 'auto'`)에서 `presentationMode: 'chart-only'`를 지정하면 Core가 **흰색 캔버스 + 조직 노드 + 연결선**만 렌더링한다. React와 Vanilla는 같은 Core 옵션을 사용한다. 기본값은 기존 화면과 동일하다(`'default'`).
|
||||
|
||||
| 옵션 | 값 | 기본값 | 동작 |
|
||||
|---|---|---|---|
|
||||
| `presentationMode` | `'default' \| 'chart-only'` | `'default'` | chart-only는 외부 제목·기준일·회사/인원 요약·Zoom/Fit 버튼 바·배율·조작 안내·Drag & Drop 문구·카드형 외곽(회색 패딩·테두리)을 만들지 않는다. 노드 안 조직명·직원 정보는 유지한다. 이동 결과 live status는 화면에서 숨긴 채 보조기술에만 제공한다. |
|
||||
| `initialView` | `'center' \| 'fit'` | chart-only `'fit'`, 그 외 `'center'` | fit은 전체 Forest Bounds(연결선 포함)를 16px 여백 안에 담고 가로·세로 중앙에 둔다. 100%를 넘겨 확대하지 않는다. 측정 전에는 조직도를 숨겨 첫 화면이 튀지 않는다. |
|
||||
| `wheelZoom` | `'modifier' \| 'direct'` | chart-only `'direct'`, 그 외 `'modifier'` | direct는 캔버스 안에서 일반 휠로 포인터 기준 확대·축소한다. 캔버스 밖은 페이지 스크롤을 유지한다. modifier는 기존 Ctrl/⌘ + 휠이다. |
|
||||
|
||||
```tsx
|
||||
// ChartOnly.tsx — 기본 Entry, HMDesign UI 불필요. 앞의 data.ts를 사용한다.
|
||||
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);
|
||||
export function ChartOnly() {
|
||||
return <OrgChart dataSource={source}
|
||||
stylesheetUrls={{ engine: engineUrl, core: coreUrl }}
|
||||
layoutMode="auto" presentationMode="chart-only"
|
||||
initialView="fit" wheelZoom="direct" dragDropEnabled={false}
|
||||
style={{ height: '100vh' }} />;
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// chart-only.ts — 앞의 data.ts와 호스트가 제공하는 HTMLElement를 사용한다.
|
||||
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 mountChartOnly(container: HTMLElement): Promise<() => void> {
|
||||
const chart = createOrgChart({
|
||||
container, dataSource: new JsonOrgChartDataSource(singleCompany),
|
||||
stylesheetUrls: { engine: engineUrl, core: coreUrl },
|
||||
layoutMode: 'auto', presentationMode: 'chart-only',
|
||||
initialView: 'fit', wheelZoom: 'direct', dragDropEnabled: false,
|
||||
});
|
||||
try {
|
||||
await chart.mount();
|
||||
if (chart.getState().status !== 'ready') throw new Error('조직도를 표시할 수 없습니다.');
|
||||
return () => chart.destroy();
|
||||
} catch (error) {
|
||||
chart.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 두 예제는 기본값과 같은 `initialView`/`wheelZoom`을 명시해 선택 가능한 값을 보여준다. Vanilla 호스트는 높이를 정하고 반환 cleanup을 lifecycle 종료 때 호출한다. `/hm-dialog` Entry의 Chart Only도 같은 Props를 쓰며 팝업은 `employeePopup="hm-dialog"`를 명시한다.
|
||||
- 캔버스는 컨테이너 높이를 채운다(`height: 100%`). 컨테이너 높이가 정해지지 않으면 최소 320px이다.
|
||||
- 자동 Fit 시점: 최초 렌더링, `update()`/`reload()`/React `dataSource` 교체, 회사 선택·Auto layout 변경. 휠 Zoom·Pan·직원 팝업·그룹 접기/펼치기·Drag & Drop ON/OFF·React 단순 리렌더링에서는 다시 Fit하지 않는다. 사용자가 조작하기 전 캔버스 크기가 바뀌면 다시 Fit하고, 조작한 뒤에는 사용자 뷰를 유지한다.
|
||||
- Fit 배율이 `zoom.min`보다 작을 수 있다(대규모 조직). 이때 휠 축소는 Fit 배율에서 멈추고, 확대는 `zoom.max`까지 가능하다. 카드 크기·글자 크기는 바꾸지 않는다.
|
||||
- 빈 공간 드래그 Pan, 방향키/`+`/`-`, 직원 팝업(기본 Core, `/hm-dialog` 선택형, `employeePopupAdapter`), 그룹 접기·펼치기, Drag & Drop 기본 OFF는 그대로다. 공개 `fitToView()`/`zoomTo()`/`panBy()` handle도 그대로 동작한다.
|
||||
- React Wrapper는 `autoLayout`·`interactions`·`selectedCompanyIds`·`zoom` 등 객체 옵션을 값으로 비교한다. 같은 값의 inline 객체로 리렌더링해도 다시 마운트하거나 Fit하지 않는다.
|
||||
- **Fixed는 지원하지 않는다.** `presentationMode: 'chart-only'`와 Fixed(`layoutMode` 생략 포함)를 함께 쓰면 `TypeError`를 던진다. 승인된 Workdesk 구성(제목·요약·아트워크·비례 축소)을 임의로 걷어내지 않기 위해서다. Fixed에 Auto와 같은 휠 Zoom/Pan도 없다.
|
||||
- 실행 예제와 브라우저 검증: [`examples/org-chart-chart-only`](../../examples/org-chart-chart-only/README.md).
|
||||
|
||||
P10.2 당시에는 내장 UI와 바깥 회색 영역이 남아 Chart Only가 PARTIAL이었으며 React 초기 Fit 옵션도 없었다. 이 **과거 검증 원본은 [P10.2 통합 보고서](integration-report-p10-2.md)에 보존**한다. 이후 위 공개 옵션 3개가 구현됐고 [최종 통합 검증](final-integration-2026-10-13.md)은 Auto Chart Only React/Vanilla 51/51 PASS를 기록한다. 과거 PARTIAL을 현재 기능의 미완료 상태로 해석하지 않는다. `chartOnly`/`showToolbar`/`showHeader`라는 prop은 현재도 없으며 정확한 API는 `presentationMode`다. `displayMode: 'readOnly'`나 dataset의 `presentation` 생략은 Chart Only 선택을 대신하지 않는다. `createOrgChartTree`도 Auto Chart Only의 대체 API가 아니다.
|
||||
|
||||
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는 Auto 전용 `presentationMode`로만 제공하며 Fixed Chart Only는 없다.
|
||||
- 제공 데이터의 최대 인원/Depth·모든 브라우저·실제 모바일 터치/스크린리더·외부 fresh install을 이번 소스 분석으로 보장하지 않는다.
|
||||
- 기존 P10 PARTIAL/Guard FAIL 10건의 원본 기록은 보존한다. [P10 Feature 통합](final-integration-2026-10-13.md)은 Chart Only·초기 Fit·직접 Wheel Zoom, GuardFix의 F3–F7 해결 5건, AI 생성 방식 사전 선택 정책을 포함했고 당시 잔여 FAIL 5건·NOT VERIFIED 9건이었다. 이후 [P11](implementation-report-p11.md)의 동일 대상 실제 Guard는 `status/staticStatus PASS`, FAIL 0건이다. Guard `runtimeStatus NOT VERIFIED`와 제품 전체 미검증을 계속 구분한다. Cell 디자인·미리보기는 제외한다.
|
||||
- [P10.3 GuardReview](guard-review-p10-3.md)는 별도 Worktree에서 당시 FAIL 10건을 재현한 원본 분석이다. 현재 Auto Chart Only와 P11 외부 CSS 테마는 지원한다. Fixed Chart Only·Fixed SVG/PNG 이미지 내부 테마화는 미지원이다. [P11.2 최종 디자인 승인](design-approval.md)은 Storybook 4개 화면과 Auto Chart Only React/Vanilla의 기본 디자인 APPROVED를 기록하며 접근성·보안 위험 수용과 구분한다. 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다.
|
||||
|
||||
**P11.6 추가 검증(2026-10-14):** 현재 가이드의 TS/TSX 블록 14개를 깨끗한 빌드의 공개 `.d.ts`에 연결해 strict·skipLibCheck=false로 검사했다(PASS). 앞 절의 변수를 사용하는 외부 CSS/P11.5 부분 예제 2개에는 명시된 데이터·Import 문맥을 보충했다. JSON 예제 2개는 실제 Validator PASS다. React·Vanilla 배경 독립 설정·테마 제거 복구·Chart Only 커스텀 배경과 새 Storybook 빌드는 추가 브라우저 20/20 PASS다. 예제 각각의 전체 브라우저 실행이나 Guard Runtime PASS를 뜻하지 않는다. 검사 범위·기존 증거 재사용·배포 플랫폼 미검증은 [P11.6 정합성 보고서](consistency-review-p11-6.md)를 따른다.
|
||||
|
||||
## 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/P10.2의 문서 예제 검증 기록은 위에 보존했다. P10.7에서는 검증 기준 SHA·Gitea Commit 고정 절차·현재 Chart Only 지원 상태와 옵션 설명을 공개 소스와 대조했으며 새 전체 타입 검사·제품 테스트·빌드·브라우저 검사는 수행하지 않았다. P11/P11.1의 후속 검증은 [P11 보고서](implementation-report-p11.md)와 [P11.1 정합성 검증](consistency-review-p11-1.md)을 따른다. P11.1은 공개 API·제품 스타일·Story 소스를 변경하지 않는다.
|
||||
@@ -0,0 +1,131 @@
|
||||
# HMDesign OrgChart P6 핵심 품질과 배포 준비 진단
|
||||
|
||||
RESULT: **PARTIAL**
|
||||
|
||||
현재 HEAD에서 Core·React Typecheck/Build, 공개 패키지 경계, Fixed/Auto 대표 실행은 통과했다. 확인한 범위의 P0 결함은 없다. Auto 관계 연결선의 낮은 대비(P1)를 배포 전 보완하도록 권장한다. Guard Runtime은 별도 증거 연결로 PASS가 되었으나 전체 CSS 디자인 의도와 모든 동적 상태는 NOT VERIFIED다. 기술적인 패키징·실행 준비는 가능하며, 무조건적인 배포 승인으로 해석하지 않는다.
|
||||
|
||||
## 작업 환경과 Git
|
||||
|
||||
- 실제 폴더: `D:\디자인시스템\HMDesign-orgchart-package`
|
||||
- 범위: BOTH, Vanilla HTML/Classic·React 공통 Core 진단. 신규 업무 화면 개발 없음.
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 시작/종료 HEAD 및 시작/종료 원격 feature SHA: `d2b76e766466e1f156f6a0f8f63710e023af900e`
|
||||
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- Git 작성자: `b25027`, `shkim4@samaneng.com`
|
||||
- 시작 Working Tree: clean. 완료 후 기존 추적 파일의 전체 SHA-256은 시작과 동일하며 staged diff 없음.
|
||||
- 신규 보고서: 이 파일. 실행기·로그·캡처·tarball·빌드 결과는 ignored `test-results/org-chart-p6/`에 분리했다.
|
||||
- **제품 소스, Fixed/Auto CSS·자산·데이터 스키마·Guard 수정 없음. Stage·Commit·Push·Merge·PR·main Checkout 미진행. Reset·Rebase·Clean·Force Push 없음.**
|
||||
|
||||
사용자가 이번 작업에 지정한 폴더·feature 브랜치와 Commit/Push 금지가 일반 AGENTS의 공식 수정 경로/main 정책보다 우선한다. 원격 확인은 `git ls-remote`로 읽기만 수행했다.
|
||||
|
||||
## 검사 결과
|
||||
|
||||
| 영역 | 현재 HEAD 결과 | 근거와 범위 |
|
||||
|---|---|---|
|
||||
| Git·추적 파일 | PASS | 폴더/브랜치/SHA/origin·remote 일치. Core/React/JSON/CSS/로고/placeholder/font 소스 추적. Git에 node_modules/dist/test-results/.env/log가 포함된 흔적 없음. 전체 민감정보 보안 감사는 수행하지 않음. |
|
||||
| Core Typecheck / Build | PASS | 실제 workspace script. ESM·Classic IIFE·선언·CSS·engine license 생성. |
|
||||
| React Typecheck / Build | PASS | Core 공개 선언 기반 Wrapper. React/Core 외부 dependency 유지. |
|
||||
| 새 PC 실행 | PASS | 설치된 기존 node_modules와 bundled Node 24.19.0 사용. 의존성 설치/업그레이드·lockfile 수정 없음. |
|
||||
| React production consumer | PASS | 기존 React 예제 build와 별도 Expanded 소비자 build. 500kB 초과 chunk 경고는 오류와 구분. |
|
||||
| 공개 패키지·tarball | 16/16 PASS | 실제 pack(`--ignore-scripts`), 추출 후 Core/React ESM import·NodeNext 선언 소비. export 대상 파일 존재. |
|
||||
| 핵심 브라우저 | 59/59 PASS | 실제 Chrome, 대표 Fixed/Auto/HTML/React 경로. 44개 성공 결과는 초기 실행에서 재사용하고 실패한 검사 조건·미완료 항목만 후속 확인. |
|
||||
| 추가 CSS | 9/9 PASS | 7개 회사 title color, 3방향 Border 면, HM Theme/Fixed 격리, 입력 변수·Auto CSS override, placeholder error 복귀. |
|
||||
| 실제 폰트 | 1/1 PASS | theme-free Classic HTML의 CDP glyph font: Noto Sans KR custom font. computed family도 4개 React 폭에서 동일. |
|
||||
| Guard Static | PASS | 최종 diagnostic React bootstrap, PARTIAL, consumer graph. FAIL 0, 원본 RENDER-002 NOT VERIFIED 유지. |
|
||||
| Guard Runtime 연결 | PASS | source/contract fingerprint 일치, `linked=true`, 4개 폭·핵심 상호작용·실제 build 자산 hash 연결. |
|
||||
| CSS 포괄 판정 | NOT VERIFIED | STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED. 명시한 computed font 4건의 cssEvidence는 PASS지만 위치별 전체 CSS 승인으로 확대하지 않음. |
|
||||
|
||||
[빌드 로그 집계](../../test-results/org-chart-p6/build-results.json), [패키지 검사](../../test-results/org-chart-p6/package-audit.json), [브라우저 원시값](../../test-results/org-chart-p6/browser.json), [CSS 원시값](../../test-results/org-chart-p6/css.json), [glyph font](../../test-results/org-chart-p6/font.json), [최종 Git 해시 비교](../../test-results/org-chart-p6/final-state.json).
|
||||
|
||||
## 기존 PASS 재사용과 현재 HEAD의 관계
|
||||
|
||||
| 과거 결과 | 재사용 근거 | 현재 HEAD 판정과의 구분 |
|
||||
|---|---|---|
|
||||
| P5-1B 879/879 | 보존된 browser.json에서 PASS 879·errors 0 확인 | 이후 P5-2/3/4 제품 변경이 있으므로 현재 HEAD 전체 회귀 879건으로 재인증하지 않음. |
|
||||
| P5-2/3 96/96 | 보존된 browser.json PASS 96·errors 0 | P5-4의 카드 표시/viewport 변경 영향은 당시 46건 및 이번 대표 검사를 사용. |
|
||||
| P5-4 46/46 | 보존된 browser.json PASS 46·errors 0 | 기록 HEAD는 `a92913d`이고 검증 대상은 당시 미커밋 소스. 해당 구현은 `064517e`, 현재 `d2b76e7`은 문서만 변경. 최종 전체 소스 manifest 부재 때문에 byte 단위 대응 전체는 NOT VERIFIED. |
|
||||
| P5-0 preset unit 10/10 | 사용자 제공 결과를 과거 근거로 유지. preset 소스의 마지막 변경 Commit `80583eb` 확인 | 이번 PC에서 원본 해당 unit 로그는 찾지 못함. 전체 unit 반복 없이 현재 7회사 runtime 색상을 별도로 확인. |
|
||||
| Fixed 이전 PASS | 이전 보고서의 단계별 결과·P5-4 최소 회귀 기록 보존 | 현재 Fixed 대표 실행·React parity·CSS 격리로 보완. 전체 픽셀 비교 없음. |
|
||||
| 300명 성능 | P5-2/3 보고서 195.6ms 초기 mount, 42.0ms 단일 회사 선택 | 과거 조건부 수치이며 새 PC 성능 측정값이 아님. 동일 성능 benchmark를 반복하지 않음. |
|
||||
|
||||
P5-4 시작 manifest 중 변경 대상 제외 25개를 비교해 23개가 현재 byte hash와 일치했다. 불일치 2개는 `verify-p5-1b.mjs`, `verify-p5-23.mjs`로 Playwright 경로 이식성 변경 대상이다. 이 불일치를 제품 회귀로 단정하지 않았다. P5-1B/2·3/4 runner의 Node syntax 검사와 현재 Playwright module/Chrome 경로 실행은 가능하며, 기존 localhost·baseline dist가 필요한 전체 runner는 재실행하지 않았다.
|
||||
|
||||
[과거 집계와 hash](../../test-results/org-chart-p6/historical-evidence.json), [소스 대응 범위](../../test-results/org-chart-p6/historical-source-mapping.json). 과거 raw 증거는 Git에 포함되지 않는 test-results이며 배포물의 필수 제품 소스와 구분한다.
|
||||
|
||||
## Fixed 대표 회귀
|
||||
|
||||
- Full Composition의 6개 주요 조직 표시, 하위 셀/팀 추가 표시 정상. 조직 제목 전체 수가 6이라는 뜻은 아니다.
|
||||
- 대표자 badge 없음, 기존 카드 자산·실측 크기·Workdesk Gradient 유지. Auto를 동시에 mount해도 Fixed 카드 geometry와 Gradient 불변.
|
||||
- 우측 인원 현황 및 footer 관계사 41명 집계 표시. Fixed canvas Fit/scroll 치수는 browser.json의 `Fixed canvas Fit and scroll dimensions`에 보존.
|
||||
- 직원 정보 popup의 변환 없는 layer·focus 내부·ESC 원래 카드 복귀 확인.
|
||||
- 실제 React Fixed 소비자와 Vanilla Fixed의 카드 ID/이름/직위/크기/font/border 동일.
|
||||
- Fixed 전체 픽셀 비교, 모든 폭/소비자, 이전 Full Composition 전체 suite는 NOT VERIFIED.
|
||||
|
||||
[Fixed 1440 캡처](../../test-results/org-chart-p6/fixed-1440.png).
|
||||
|
||||
## Auto 대표 회귀
|
||||
|
||||
- Small 20명/3회사, Medium 100명/5회사, Large 300명/7회사. 고유 직원 ID의 누락·중복·카드 겹침 없음.
|
||||
- 실제 DOM 부모와 데이터 parent ID 일치. Small 최대 2Depth, Medium 3Depth, Large 4Depth를 포함한 대표 재귀 배치 확인.
|
||||
- 전체 Fit에서 카드가 viewport 안에 포함. 300명에서 실제 pointer Pan, UI Zoom In/Out, 그룹 Enter collapse/Space expand 확인.
|
||||
- 단일 한맥 회사 선택 시 독립 root 1개, 원본 summary.raw.total은 300 유지. React 회사 해제→단일→전체 복귀 확인.
|
||||
- 직원 popup, 7회사 그룹 색상, top/left/right endpoint 오차 1px 미만과 연결 방향의 한 면만 회사색 적용 확인.
|
||||
- HTML 준비 예제의 300명 전환, React의 300명 선택과 고유 ID 렌더링 확인. 대용량 성능 benchmark는 재실행하지 않음.
|
||||
- React 390/768/1280/1440px 가로 문서 overflow 없음, viewport hidden scrollLeft/Top=0. 문서/viewport의 scrollWidth·clientWidth·scrollHeight·clientHeight는 원시 측정에 보존.
|
||||
- 임의 데이터 전 조합·모든 접힘 state·다른 브라우저·물리적 터치는 NOT VERIFIED.
|
||||
|
||||
[Auto 300명 Fit](../../test-results/org-chart-p6/auto-300-fit.png), [React 390px](../../test-results/org-chart-p6/react-390.png), [React 1440px](../../test-results/org-chart-p6/react-1440.png).
|
||||
|
||||
## HTML React Core와 CSS
|
||||
|
||||
HTML은 `createOrgChart`와 `JsonOrgChartDataSource`, React Wrapper는 같은 공개 `createOrgChart`를 사용한다. React bundle은 Core를 external로 남기며 별도 조직도 엔진을 포함하지 않는다. 같은 20명 JSON의 HTML/React 카드 identity·표시 내용·geometry·폰트·border·연결 방향을 비교했다. Fixed/Auto 분기·회사 선택·collapse·popup 책임도 공통 Core다.
|
||||
|
||||
Core `/browser`는 실제 `window.HMOrgChart`를 제공하며 준비된 `p1-classic.html`을 file URL에서 렌더링했다. public CSS는 private `@import` 없이 self-contained, 자산은 bundle의 data URL 또는 검증된 font/CSS URL을 사용한다. 실제 소비자에서 깨진 요청·console/page error 0이다. Core tarball은 engine MIT notice도 포함한다.
|
||||
|
||||
OrgChart CSS는 dedicated Shadow DOM에 로드된다. 외부 probe의 색/font가 변경되지 않았고 전역 HM 색상·font probe에도 Auto/Fixed 스타일이 유지됐다. Noto는 호스트 font-face 제공 계약이다. `--hm-orgchart-bg-surface`의 instance-local 상속과 Shadow 안 `.hm-orgchart-expanded`의 name-size override는 적용되고 다른 Fixed instance에는 영향을 주지 않았다. Expanded 내부 선언 변수를 host div에 설정하는 방식은 문서대로 보장하지 않는다.
|
||||
|
||||
CSS에서 선언이 없는 모든 `--hm-orgchart-*` 입력을 Theme 누락으로 처리하지 않았다. design-tokens.css는 optional host inputs를 의도적으로 미선언하고 local 기본값을 사용하며, current gradient·border와 Auto 회사색·배치 변수는 Core setProperty로 정의된다. 유한 회사 후보는 기존 7개 preset 선언과 실측으로 대조했다. [token 목록](../../test-results/org-chart-p6/token-inventory.json), [소스 정의 분류](../../test-results/org-chart-p6/token-source-review.json). 임의 외부 입력과 모든 동적 조합은 NOT VERIFIED다.
|
||||
|
||||
React 소비자에는 공개 HM Button/Checkbox/Label/Select가 실제 렌더링됐다. Core native button/dialog는 기존 Vanilla adapter 계약이다. FilterBar는 검색 요구 없는 fixture 전환·즉시 회사 표시 구성이라 미사용 근거를 유지했다. 임베드 조직도에는 페이지 Navigation을 추가하지 않았다. 전체 접근성 이름·disabled/readOnly 조합·screen reader 인증은 이번 대표 점검을 넘어선다.
|
||||
|
||||
## Guard 결과와 실행기 오류
|
||||
|
||||
Guard는 정적 분석기이며 브라우저를 자체 실행하지 않는다. 최종 `--result`는 실제 production build에 사용한 React bootstrap(`test-results/org-chart-p6/react-build/main.tsx`)이다. 그 source/contract fingerprint와 실제 production HTML·JS/CSS/font SHA, 4개 폭·named interaction을 연결했다. 원본의 RENDER-002는 지우지 않았으며 결합 결과만 Runtime PASS다.
|
||||
|
||||
[원본 Guard](../../test-results/org-chart-p6/guard.json), [Runtime 입력](../../test-results/org-chart-p6/runtime.json), [결합 결과](../../test-results/org-chart-p6/combined.json). PARTIAL consumer graph 결과는 Core 내부 전체 CSS 검사나 운영 배포 검증이 아니다. 결합 status는 **NOT VERIFIED**, styleVerification은 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**다.
|
||||
|
||||
초기 생성 HTML의 `/assets`를 저장소 root로 찾지 못한 Guard 원본을 [보존](../../test-results/org-chart-p6/guard-built-html-unresolved.json)했다. 검사 규칙·기대값·Guard 소스를 변경하지 않고 bootstrap 지정 경로를 교정했다. browser sandbox localhost 차단, Fixed 전체 source 직원 수를 카드 수로 기다린 오류, 전체/주요 조직 구분 및 footer selector 오류, React 자산 서버 mapping 오류도 초기 로그에 보존했다. 이는 신규 제품 결함에 합산하지 않았다. CDP font node 조회 오류는 DOM 문서를 초기화한 별도 font probe에서 해결했으며 CSS 9개 PASS는 반복하지 않았다.
|
||||
|
||||
과거 `verify-p1.mjs`의 197건 중 145 PASS/52 FAIL 및 v0 `verify.mjs`의 알려진 FAIL은 [P2 보고서](implementation-report-p2.md)의 계약 변경 기록으로 보존한다. 이번 PC에는 그 원본 JSON 경로가 없어서 재분류 전체는 NOT VERIFIED다. v0의 사용자 언급 9건과 당시 저장 기록 6건/2 FAIL 차이도 확정하지 않는다. 오래된 기대값을 PASS로 변경하거나 전체 suite를 반복하지 않았다.
|
||||
|
||||
## 배포 전 결함과 후속 조치
|
||||
|
||||
| 우선순위 | 위치와 재현 | 실제 결과와 기대 결과 | 영향 및 신규 여부 | 권장 조치와 예상 시간 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | `design-tokens.css:14`, `org-chart-expanded.css:165`. 기본 Auto 300명 직원 연결선 | computed stroke `rgb(186,199,215)` / 흰 면. 대비 **1.715904:1**, 관계 전달 그래픽의 기대 3:1 미달 | Auto 관계 식별. 기존 standalone stroke를 Auto에도 사용하는 미해결 항목을 현 HEAD에서 새로 확인. P6 코드 변경으로 생긴 결함 아님 | 관계선 전용 의미 token/색상 선택 후 scoped 적용 및 관련 경로 검사. 모든 중립 border/브랜드 일괄 변경 금지. 승인 이후 약 30~60분. |
|
||||
| P2 | `verify-p2.mjs`, `verify-p3*.mjs`, `verify-p5-1a.mjs` 및 일부 과거 check runner | 이전 `C:/Users/user/...` fallback 또는 고정 Chrome/tool import. 다른 PC에서 기본 실행 실패 가능 | 과거 검증 도구 이식성, shipped runtime에는 영향 없음. 기존. P5 최신 3개 runner는 env override 가능·syntax PASS | 공통 tool resolver 또는 실행 문서/환경변수 안내 정리. 약 15~30분. |
|
||||
| P2 | pack의 Core/React README `../../docs/org-chart/...` 링크 | docs는 files 목록에 없어 추출 tarball의 로컬 문서 링크가 해결되지 않음 | 독립 패키지 문서 소비. 기존 README/packaging 경계, runtime 무관 | 공개 문서 URL 사용 또는 필요한 사용 문서 포함. 약 10~20분. |
|
||||
|
||||
연결선 기준은 [W3C Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)의 내용 이해에 필요한 그래픽에 대한 3:1 요구를 적용했다. 자동 UI 경계 전체에 이 기준을 확장하지 않았다. [현재 computed 색·계산 근거](../../test-results/org-chart-p6/contrast.json).
|
||||
|
||||
Fixed의 디자인기획·솔루션영업·허브에 대한 [과거 대비 REVIEW](quality-review-p1.md)는 별도 검토 이력이다. 현재 header는 20px/700으로 과거 15px 측정과 달라, 과거 수치만으로 현재 텍스트 FAIL을 확정하지 않았다. 현재 computed/크기/Gradient는 css.json에 기록했으며 텍스트 영역의 최종 pixel 대비 판정은 NOT VERIFIED다. 허브의 13px 흰 텍스트도 새 픽셀 재인증은 하지 않았다. 이전 브랜드 디자인 선택을 임의 변경하지 않는다.
|
||||
|
||||
P0 0건(확인 범위). P1 1건과 P2 2건. 예상 후속 수정은 약 **55~110분**이며 디자인 선택/승인 대기와 운영 환경 점검은 별도다. 전체 Guard CSS Runtime/디자인 의도 증거를 추가하려면 소비 위치별 상태·4개 폭 근거 정리가 더 필요하다(약 45~90분, 수정 시간과 구분).
|
||||
|
||||
## 남은 미검증과 배포 판단
|
||||
|
||||
배포 전 우선 조치는 Auto 관계선 대비 보완 여부 결정, 현재 consumer의 CSS 의도/입력 범위 확정, 실제 배포 base/font/CSP 경로의 smoke 확인이다. 이번 진단에서 자동 수정은 하지 않았다.
|
||||
|
||||
NOT VERIFIED: fresh `npm ci`/원격 registry 설치, 운영 소비자·임의 base/CSP/origin, 전체 Storybook production build, 전 역사 회귀 및 기존 FAIL 전체 재분류, 모든 임의 데이터/override/collapse 조합, 모든 Fixed 폭·픽셀 완전 일치, 실제 화면 리더·물리 모바일·다른 엔진·SSR/hydration·heap/GC·실제 API. P0 없음은 이러한 범위의 무결성을 인증하지 않는다.
|
||||
|
||||
패키지의 기술적 실행·빌드와 대표 기능은 배포 준비 가능하다. **P1 개선과 위 배포 환경 확인이 남아 최종 배포 준비 판정은 PARTIAL**이다. 현재 운영 main 병합/배포/Push는 실행하지 않았다.
|
||||
|
||||
## 작업 시간
|
||||
|
||||
- Started At: 2026-10-12 22:07:12 KST
|
||||
- Completed At: 2026-10-12 22:27:09 KST
|
||||
- Actual Duration: 19분 57초
|
||||
- Estimated Duration: 최초 60~90분 (근거 확인 10~15분 / 실행 40~60분 / 보고 10~15분)
|
||||
- Variance: 예상 하한보다 40분 3초, 상한보다 70분 3초 짧음
|
||||
|
||||
기존 PASS 재사용, 준비된 의존성/브라우저 환경, 전체 suite 제외로 최초 예상보다 빠르게 완료했다.
|
||||
@@ -0,0 +1,163 @@
|
||||
# HMDesign OrgChart P1 — 품질 보완 및 디자인 검수
|
||||
|
||||
RESULT: **PARTIAL**
|
||||
|
||||
이번 변경은 기존 P1의 Gradient 대비 검사, Department Colors, 실제 로고 표시, Guard 원인 분석으로 제한했다. 사용자가 최종 확인한 대로 Workdesk 전체 디자인 재현 첨부는 참고 자료로만 사용했다. 전체 배치·카드 구조를 다시 개발하지 않았다. 기존 기능·데이터 계약·공개 API·React Wrapper·공통 Theme/Token·운영 main은 보존했다.
|
||||
|
||||
## 작업 정보
|
||||
|
||||
- 작업 폴더: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- 전/후 HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`
|
||||
- 실제 병렬 수행: Track A/B/C 읽기 전용 분석을 병렬 수행했다. 이후 B는 대비 모듈과 Department Colors, C는 두 Summary Story의 가상 데이터 안내, 메인은 로고 렌더링·CSS·검증·보고서를 담당했다. 같은 파일의 동시 수정은 없었다.
|
||||
- Started At: 2026-10-12 11:55:44 KST
|
||||
- Completed At: 2026-10-12 12:25:20 KST
|
||||
- Actual Duration: 29분 36초
|
||||
- Variance: 최초 예상 하한 45분 대비 15분 24초 단축, 상한 90분 대비 60분 24초 단축. 병렬 분석 및 기존 PASS 재사용으로 재검증 범위를 줄였다.
|
||||
- Estimated Duration: 45–90분. 필요한 범위만 검사하며 시간을 채우기 위한 재검증은 하지 않았다.
|
||||
|
||||
## Track A — Guard: 원인 분석 PASS, 전체 해결 PARTIAL
|
||||
|
||||
공통 Guard를 변경하거나 검사 규칙을 비활성화하지 않았다. 실제 `themeAnalysis`에 인메모리 fixture를 전달한 최소 재현 8/8이 통과했다. 이는 **오탐 재현 PASS이며 실제 Guard PASS가 아니다**.
|
||||
|
||||
최종 PARTIAL/STORYBOOK Guard 결과는 원본 `status=FAIL`, `staticStatus=FAIL`로 보존했다. TOKEN-001 FAIL 8건, TOKEN-002 NOT VERIFIED 6건, SCOPE-001 NOT VERIFIED 3건이다.
|
||||
|
||||
- Shadow CSS 연결 누락 1건: `--hm-orgchart-resolved-bg-surface`는 `design-tokens.css`에 선언되고 공개 CSS로 번들되어 Shadow DOM에 연결된다. Guard graph는 `?url` import와 Shadow link의 연결을 증명하지 못한다.
|
||||
- `setProperty` 정의 미수집 4건: `current-gradient`, `current-card-background`, `current-border`는 `applyOrganizationStyle`이 정의한다. Guard가 이 DOM 선언을 수집하지 않아 참조를 미선언으로 판정한다.
|
||||
- 선택적 입력 오분류 3건: 대비 후보의 `--hm-orgchart-contrast-candidate-color` 및 `--hm-orgchart-contrast-candidate-chip-background`는 로컬 fallback을 가진 외부 override 입력이다. 기본값은 각각 `#10243b`, `#ffffff`이며 실제로 정상 적용된다. 공식 Theme 변수의 누락과 동일하게 처리하는 현재 검사 정책은 이 계약을 표현하지 못한다.
|
||||
- 동적 TOKEN-002 6건: 유한한 조직 preset/legend 문자열 조합을 정적으로 해석하지 못한다.
|
||||
- CSS SCOPE-001 3건: 카드·콘텐츠·통계 CSS는 실제 Core CSS 번들과 Shadow link에 포함되지만 Storybook graph 범위 밖으로 판정된다.
|
||||
|
||||
브라우저에서 Shadow CSS 로딩, 6개 preset의 computed 값, fallback과 HTML/React 동일 렌더링을 확인했다. 이 증거로 raw FAIL을 덮어쓰지 않았다. 검사한 범위에서 실제 Production 미선언 변수 결함은 발견하지 못했다.
|
||||
|
||||
`styleEvidence`: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **NOT VERIFIED**, FINAL **NOT VERIFIED**. 이 값은 Guard의 증거 연결 상태이며 별도 브라우저 PASS와 구분한다. 이번에는 결합 `styleVerification`으로 승격하지 않았다.
|
||||
|
||||
승인 필요한 최소 공통 Guard 변경안: 증명 가능한 Shadow CSS 연결, `setProperty` 정의 수집, 유한 후보 해석, 명시적 선택적 입력 계약 지원. Guard 분석기·style inventory/증거 연결·관련 회귀 fixture와 문서에 영향이 있으므로 별도 승인 전에 구현하지 않는다. 전역 CSS 강제 연결, dummy 선언, 변수 namespace 변경, fallback 일괄 면제는 적용하지 않았다.
|
||||
|
||||
근거: `test-results/org-chart-quality/guard-reproduction.json`, `guard-storybook.json`. 분석기 SHA 전/후 동일하며 기존 P1 Guard 원본도 유지했다.
|
||||
|
||||
## Track B — Color Contrast: PARTIAL
|
||||
|
||||
기준은 [WCAG 2.2 Contrast Minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)의 일반 텍스트 4.5:1, 큰 텍스트 3:1 및 [Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)의 의미 있는 UI/그래픽 3:1이다. 판정에는 반올림 전 값을 사용했다. 로고를 변색하지 않았다.
|
||||
|
||||
### 측정 방법과 범위
|
||||
|
||||
Core의 제한된 opaque sRGB/세로 Gradient 모델과 실제 Chrome DPR1 PNG를 분리해 기록했다. 모델은 Gradient stop, 전체 구간, DOM Range의 텍스트 line 구간을 구분한다. PNG에서는 텍스트 옆 빈 padding 열의 실제 배경 픽셀과 computed foreground를 사용한다. 안티앨리어싱된 글자 픽셀로 전경색을 추측하지 않았다. 모델과 픽셀 차이가 2 channel 값을 초과하거나 스크롤에 가려지면 NOT VERIFIED다.
|
||||
|
||||
지원하지 않는 alpha·복수 배경·다른 각도·다른 색 공간·합성 효과 등은 임의로 PASS하지 않는다. 이번 PNG 검사의 84개 가려진/일치하지 않은 probe는 NOT VERIFIED로 남겼다. 그중 솔루션영업 두 부서명은 마지막 가지로 스크롤한 별도 캡처에서 추가 검증했다. 숫자는 화면별 반복 측정이며 서로 다른 접근성 요구사항의 개수가 아니다. 전체 WCAG 인증으로 확대하지 않는다.
|
||||
|
||||
### 기존 HEX와 실제 텍스트 대비
|
||||
|
||||
6개 헤더 Gradient와 흰 부서명 `#ffffff`는 Workdesk `v1.2610.9c5e`, 2026-10-12의 기존 computed 근거를 유지했다. 직원 카드 배경은 자체 기본 `#ffffff`이고 테두리 기본은 해당 조직 시작색이다. 카드 면·테두리·geometry는 **DESIGN_ADAPTATION**, 공식 Workdesk 실측으로 승격하지 않았다. 변수 prefix에 `-card-background`, `-department-color`, `-border`, `-gradient`를 각각 사용한다.
|
||||
|
||||
- 사업관리: `#194d82 → #1d395c`, `--hm-orgchart-business-management-*`. 흰색 stop 최소 8.659:1. Full 실제 부서명 9.357:1 **PASS**.
|
||||
- 경영기획: `#2164a8 → #254975`, `--hm-orgchart-business-planning-*`. stop 최소 6.082:1. Full 실제 부서명 약 6.654–6.676:1 **PASS**.
|
||||
- 인재성장: `#0f6ace → #044a94`, `--hm-orgchart-talent-growth-*`. stop 최소 5.286:1. Full 실제 부서명 약 5.879–5.882:1 **PASS**.
|
||||
- 통합시스템 Div: `#8c67b6 → #5d3494`, `--hm-orgchart-integrated-system-*`. stop 최소 4.439:1이지만 Full 실제 텍스트 구간은 약 5.008–5.197:1 **PASS**. stop 하나만으로 실제 텍스트를 FAIL 처리하지 않았다.
|
||||
- 디자인기획: `#09a7a2 → #00847f`, `--hm-orgchart-design-planning-*`. stop 최소 2.974:1. Full의 15px bold 부서명 약 3.188–3.263:1 **REVIEW**. Department Colors의 19.5px bold 제목은 약 3.228:1로 큰 텍스트 기준 **PASS**, 일반 크기 비교는 약 3.263:1 **REVIEW**다. 큰 제목의 PASS를 Full에 대입하지 않았다.
|
||||
- 솔루션영업: `#5a963c → #3f7629`, `--hm-orgchart-solution-sales-*`. stop 최소 3.582:1. 스크롤 후 Full 15px 부서명 약 3.916–3.917:1 **REVIEW**. Department Colors 큰 제목 약 3.922:1 **PASS**, 일반 크기 비교 약 3.922:1 **REVIEW**다.
|
||||
|
||||
위 Full 수치는 1440px 대표 실측이다. Department Colors의 화면 내 MODEL 값은 픽셀 실측과 별도이며 실제 geometry·브라우저 반올림에 따라 차이가 있다. UI에 MODEL임을 명시했다. Default 값만 검사했으며 모든 소비자 override 조합의 대비를 보장하지 않는다.
|
||||
|
||||
기타 확인:
|
||||
|
||||
- 직원 이름 `#10243b / #ffffff`: 15.701:1 **PASS**.
|
||||
- 직위·상태의 보조 텍스트 `#596575 / #ffffff`: 5.927:1 **PASS**. 기본 콘텐츠 면 `#f4f7fb` 기준 약 5.516:1.
|
||||
- 직책 배지 및 통계 헤더/숫자 텍스트: 검사한 solid 조합 **PASS**. 원본 값 변경 없음.
|
||||
- 협업 허브 `#45639f → #809dd7` 위 흰 일반 텍스트: 실제 약 3.321:1 **REVIEW**; stop 최소 약 2.720:1. 허브 색은 Workdesk 실측으로 확인된 브랜드 값이 아니라 자체 디자인 기본값이다.
|
||||
- 관계 표현 연결선 `#bac7d7`: 실제 Storybook 배경 `#f5f0e5`와 약 1.510:1, 흰 면과 1.716:1 **REVIEW**. 모든 카드 프레임·테이블 divider를 무조건 의미 있는 UI 경계로 판정하지는 않았다. 부모-자식 관계 연결선은 정보 전달 그래픽으로 검토했다.
|
||||
- 키보드 focus-visible: 실제 2px solid `#45639f`, Storybook 배경 대비 5.222:1 **PASS**. 전체 키보드/스크린리더 검증으로 확대하지 않았다.
|
||||
|
||||
### 적용한 보완과 승인 대기안
|
||||
|
||||
**실제 Production 브랜드 HEX 변경: 0건.** Department Colors에 `Aa`, 실제 변수/HEX, 카드 배경과 부서명 전경 구분, 일반/큰 텍스트 대비, stop 보수값, MODEL/NOT VERIFIED 및 미적용 후보 표시를 추가했다. resize 시 실제 텍스트 구간을 다시 계산한다.
|
||||
|
||||
중요한 색상·geometry 변경은 승인 없이 적용하지 않았다. 후보는 `UNAPPROVED_NOT_APPLIED`로 표시하며 Full Composition에 반영하지 않았다.
|
||||
|
||||
- 디자인기획 흰 부서명 → `#000000` 후보: 전체 Gradient stop 최소 4.607:1. 텍스트 브랜드 표현 변경에 대한 승인 후 실제 화면 재검증 필요.
|
||||
- 모든 Gradient의 단순 `#10243b` 전경 후보는 어두운 조직에서 실패한다. Department Colors에 실패 수치도 표시해 보편적 해결안으로 제시하지 않았다.
|
||||
- 디자인기획/솔루션영업: 흰 label backing `#ffffff` + `#10243b` 후보는 15.701:1. 기존 Gradient를 유지하지만 새 시각 요소이므로 승인 대기. 솔루션영업은 검정/흰색 어느 하나도 전체 Gradient 전 구간을 일반 텍스트 기준으로 통과하지 않는다(검정 stop 최소 3.835:1).
|
||||
- 큰 bold 텍스트 사용은 별도 대안이며 layout 영향을 확인하고 승인한 뒤 실제 geometry에서 다시 검사해야 한다.
|
||||
- 허브: 흰 backing + 짙은 텍스트 또는 배경/크기 보완 승인 필요. 후보 수치와 actual 허브 적용은 구분했다.
|
||||
- 의미 있는 연결선 `#bac7d7 → #7b899d` 후보: 실제 Storybook 배경 3.128:1, 흰 면 3.555:1, `#f4f7fb` 3.308:1. `--hm-orgchart-stroke-default` 보완 범위와 시각 의도 승인 후 적용할 수 있다. 적용하지 않았다.
|
||||
|
||||
근거: `browser.json`, `contrast-pixels.json`, `contrast-last-branch.json`, `boundaries.json` 및 대응 PNG.
|
||||
|
||||
## Track C — Storybook: 변경 표시 범위 PASS
|
||||
|
||||
사용자용 메뉴 **Logo / Department Colors / Position Summary / Affiliation Summary / Full Composition** 5개 유지. 기존 내부 Story 8개 및 테스트 로직 유지.
|
||||
|
||||
- Logo: Figma 정식 Footer에서 확보한 7개 PNG(한맥·삼안·장헌·장헌산업·피티씨·한라·바론)와 ID 매핑 유지. 473×473 원본 SHA와 RGBA 투명성 유지, 수정·재제작·가상 대체 없음. 누락 Asset **0건**. 원본 이미지 비율은 contain으로 보존했다.
|
||||
- 로고 최소 수정: 이미지의 흰 CSS 배경을 transparent로 변경하고, 로고 정상일 때 fallback 글자가 투명 영역 뒤에 보이지 않도록 숨겼다. 이미지 실패 시 fallback을 다시 표시하며 관계사명 접근성 이름을 유지했다. 기존 실제 broken-image PASS를 재사용하고 이번에는 변경된 handler 표시만 검사했다.
|
||||
- 한라처럼 가로 wordmark인 원본은 직원 16px mark에서 실제 ink 높이가 약 4px, Summary 28px에서는 약 7px라 작은 크기의 판독성 한계가 남는다. 원본 비율을 임의로 crop/recolor하지 않았다. 크기/여백 변경은 후속 디자인 검토 대상이다.
|
||||
- Department Colors: 6개 조직명, 실제 HEX/CSS Variable, Gradient/Border, 원본 출처와 자체 디자인 구분, 실제 computed/model 대비·미승인 후보 표시 정상. 390px 줄바꿈 정상.
|
||||
- Position Summary / Affiliation Summary: 두 Story에 가상 직원 자동 집계·운영 인원 아님 안내를 추가했다. Core selector/표/정책은 변경하지 않았다. 가상 raw total 41, 관계사 0명 세 항목 표시를 유지한다. 실제 인사 집계 정책 승인으로 간주하지 않는다.
|
||||
- Full Composition: 리더·허브·6개 그룹과 부서/팀·직원 카드·직위 통계·관계사 통계·파견/휴직을 유지했다. Header·GNB·LNB·Dashboard Layout 없음. 390/768에서는 통계 아래 배치, 1280/1440에서는 오른쪽 통계 배치. 조직도 및 표의 내부 스크롤 유지, 변경으로 인한 새 겹침/페이지 overflow 없음.
|
||||
- Position Summary 최초 390px PNG의 안내 첫 paint가 불완전해 그 화면만 다시 확인했다. DOM·최종 PNG에서 두 줄 전체 문장 정상. 원본 캡처를 보존하고 `position-note-recheck.json`과 `storybook-position-summary-390-final.png`를 최종 근거로 사용한다.
|
||||
- HTML/React: 4개 폭의 computed 카드·로고 스타일 일치. 이미 촬영한 PNG를 비교한 결과 390/768/1280/1440 모두 픽셀 동일. 별도 React UI를 추가하지 않았다.
|
||||
|
||||
독립 API는 변경하지 않았다. `OrgChartTree`, `PositionSummary`, `AffiliationSummary`, `StatusSummary`의 기존 factory/React Wrapper/P1 독립 사용 PASS를 재사용한다. 두 Summary의 변경 Story 렌더링은 이번 브라우저 검사로 확인했다. 사용법은 기존 [공개 컴포넌트·스타일 API](standalone-components-p1.md)를 유지한다.
|
||||
|
||||
확인된 Workdesk 차이는 유지했다: IS/디자인 하위 팀의 세로 배치, 추가 root 카드·ID/raw metadata·collapse control, 더 큰 전체 높이, 18행 상세 통계, 1440px Story host 제한과 마지막 가지 내부 스크롤, 파견/휴직의 after-tree 배치. 이는 이번 변경으로 생긴 회귀가 아니며 사용자가 예고한 별도 전체 디자인 작업에서 다룬다. 픽셀 완전 일치 PASS가 아니다.
|
||||
|
||||
근거: `test-results/org-chart-quality/visual-review.json`. Workdesk 최신 화면은 조직명/색/배치 참고로만 사용했고 실제 직원 개인정보·운영 인원수를 fixture에 복제하지 않았다. Figma Dev Mode 미확인 속성을 실측이라고 기재하지 않았다.
|
||||
|
||||
## 검증 및 재사용
|
||||
|
||||
이번에 실행:
|
||||
|
||||
- Core Typecheck: `npm.cmd run typecheck:org-chart` **PASS**.
|
||||
- 변경 Story/preview source Typecheck: `npm.cmd exec -- tsc -p tsconfig.app.json --noEmit` **PASS**.
|
||||
- Core Package Build: `npm.cmd run build:org-chart` **PASS**(ESM/IIFE/types/CSS).
|
||||
- HTML demo prepare 및 React demo Vite Build **PASS**. React Wrapper 구현 변경 없음.
|
||||
- Storybook production Build **PASS**. 기존 큰 chunk 경고가 있으며 이번 작업에서 배포 구조를 리팩터링하지 않았다.
|
||||
- 신규 대비 unit **10/10 PASS**: 수학/비반올림 경계/일반·큰 텍스트/Gradient 구간/unsupported NOT VERIFIED. 기존 unit output을 지우지 않는 별도 폴더로 실행.
|
||||
- 변경 UI browser **62/62 PASS**, console/runtime error 0. 5개 Story 390/1440 및 Full 768/1280, Theme 미연결 HTML/React 4개 폭 확인.
|
||||
- Position 390 안내만 후속 **PASS**, 가려진 솔루션영업 제목 2곳 후속 대비 **REVIEW**. 정상 데이터/생명주기 검사를 반복하지 않았다.
|
||||
- Guard 재현 **8/8 PASS**; 최종 관련 Guard 원본 **FAIL**(앞서 원인/범위 기록).
|
||||
- 실제 텍스트 대비: 검증된 부분 PASS/REVIEW 구분, 가려지거나 모델 불일치한 부분 NOT VERIFIED 유지.
|
||||
- `git diff --check` **PASS**. 운영 `D:\디자인시스템\HMDesign`은 읽기 전용 확인: `main`, 동일 HEAD, Working Tree clean. Commit/Push/Merge/새 브랜치/공통 코드 수정 없음.
|
||||
|
||||
재사용한 근거: 기존 Core 22/22, HTML 69/69, React 79/79, P1 selector 7/7, P1 browser의 성공 196건 및 console/menu 후속 4/4. 원본 기록은 [P1 구현 보고](implementation-report-p1.md)와 `test-results/org-chart-p1/`에 유지한다. Data Source/validation/집계/JSON 갱신/lifecycle/API/Wrapper/원본 Story 로직이 변경되지 않아 전체 테스트를 다시 실행하지 않았다. 현재 검사는 변경된 스타일·표시·신규 대비 모델·관련 Guard 문제에 한정했다.
|
||||
|
||||
## 변경 파일 — 이번 품질 작업만
|
||||
|
||||
시작 706파일 SHA snapshot과 비교해 기존 파일 삭제 0건, 수정 7건, 신규 소스/검증 6건과 이 보고서 1건이다. 기존 `package.json`, `package-lock.json` 미커밋 변경, 원본 자산·7개 설계 문서·P1 보고/Story·Spike·공통 코드의 시작값은 보존했다.
|
||||
|
||||
수정:
|
||||
|
||||
1. `packages/org-chart/src/core/affiliation-mark.ts` — fallback span과 정상/오류 표시 연결.
|
||||
2. `packages/org-chart/src/core/department-colors.ts` — 대비 정보와 미적용 비교 후보.
|
||||
3. `packages/org-chart/src/styles/employee-card.css` — 로고 투명 배경 및 fallback 숨김.
|
||||
4. `packages/org-chart/src/styles/org-chart-content.css` — Department Colors 비교 표시만.
|
||||
5. `packages/org-chart/src/styles/summary-sections.css` — standalone 주석 정정만.
|
||||
6. `src/org-chart/core-component-preview.tsx` — Department Colors resize 계산/cleanup.
|
||||
7. `src/org-chart/org-chart-p1.stories.tsx` — 두 독립 Summary의 가상 데이터 안내.
|
||||
|
||||
신규:
|
||||
|
||||
8. `packages/org-chart/src/core/contrast.ts` — 내부 대비 모델(공개 API 추가 없음).
|
||||
9. `packages/org-chart/src/core/contrast.test.ts` — 신규 모델 10개 검증.
|
||||
10. `examples/org-chart/verify-guard-p1.mjs` — Guard 최소 오탐 재현.
|
||||
11. `examples/org-chart/verify-quality-p1.mjs` — 변경 UI 62개 검사/PNG/값 수집.
|
||||
12. `examples/org-chart/audit-quality-pixels.py` — PNG 읽기 전용 대비 감사.
|
||||
13. `examples/org-chart/verify-quality-boundaries.mjs` — 실제 연결선/포커스 CSS 확인.
|
||||
14. `docs/org-chart/quality-review-p1.md` — 이번 결과 기록.
|
||||
|
||||
`test-results/org-chart-quality/`에는 신규 증거, 실행 로그, PNG 및 baseline을 별도 보존했다. Build 산출물은 기존 생성 경로에서 갱신했다.
|
||||
|
||||
## 최종 판정 / 승인 / 후속
|
||||
|
||||
**PARTIAL**: 로고 표시·Department Colors 설명·가상 안내·변경 렌더링 보완은 PASS다. 실제 대비 부족과 공통 Guard 개선 승인이 남아 전체 P1을 PASS로 처리하지 않는다. 원본 Guard FAIL은 분석상 오탐으로 구분했지만 승인·수정 없이 PASS로 치환하지 않았다.
|
||||
|
||||
승인 대기: 디자인기획/솔루션영업/허브의 텍스트·backing·크기 선택, 의미 있는 연결선 대비 보완, 공통 Guard 제한적 추적 개선. 모두 미적용이다. Workdesk 전체 배치 수정은 사용자가 지정한 별도 Codex 작업으로 남긴다.
|
||||
|
||||
NOT VERIFIED: 전체 Heap/GC 회수, 전체 스크린리더, 미확인 Figma Dev Mode 속성, Workdesk 픽셀 완전 일치, 실제 HR API, 미승인 집계 정책, 무관한 Guard 항목, 지원하지 않거나 가려진 contrast probe, 모든 소비자 override의 접근성.
|
||||
|
||||
후속 P2: 전체 디자인 작업 결과의 확인, 승인된 대비 대안만 적용/부분 재검사, 작은 wordmark 크기 검토, 승인된 집계 정책·실제 API·전체 접근성·메모리 검증. Commit·Push·Merge는 수행하지 않았다.
|
||||
|
||||
## 완료 기록
|
||||
|
||||
Started At: **2026-10-12 11:55:44 KST**. Completed At: **2026-10-12 12:25:20 KST**(최종 검사 종료 시각). Actual Duration: **29분 36초**. Estimated Duration: **45–90분**. Variance: **하한 대비 −15분 24초 / 상한 대비 −60분 24초**. 보고서 정리와 결과 표시 시간은 별도다.
|
||||
|
||||
최종 보존 근거: `test-results/org-chart-quality/preservation.json` — baseline 706개 중 수정 7개, 유지 699개, 삭제 0개, 신규 7개(보고서 포함), HEAD/main 동일, diff check 0. Preview: `http://127.0.0.1:6027/?path=/story/orgchart--full-composition`.
|
||||
@@ -0,0 +1,83 @@
|
||||
# HMDesign OrgChart v0.1 React Wrapper · Storybook 통합 검증
|
||||
|
||||
RESULT: **PASS** — 이번 요청의 React 통합 MVP 완료 조건 13개를 충족했습니다. 아래 NOT VERIFIED는 별도 한계로 보존하며 모든 기능·디자인·배포 환경의 검증 완료를 뜻하지 않습니다.
|
||||
|
||||
## 구현 결과
|
||||
|
||||
1. 작업 경로 `D:\디자인시스템\HMDesign-OrgChart`, 브랜치 `feature/orgchart-package`. 사용자 첨부 요청의 별도 작업/Git 지시를 적용했습니다. 운영 `D:\디자인시스템\HMDesign` 및 `main`은 수정하지 않았습니다.
|
||||
2. 작업 전/후 HEAD, Local main, 공식 원격 main/Feature 모두 `d18e3c33801e165e17b6d971e2f6198eed483c3c`. 공식 origin은 `https://gitea.hmac.kr/b25027/HMDesign.git`입니다. 시작/완료 시 원격을 읽기 전용으로 확인했습니다.
|
||||
3. 신규 `packages/org-chart-react`, `@hm-design/org-chart-react@0.1.0`. Core `@hm-design/org-chart@0.1.0` dependency, React `^19.0.0` peer. React DOM은 Wrapper가 사용하지 않아 별도 dependency/peer를 추가하지 않았으며 소비 앱이 제공합니다. 설치 package/lock과 React/DOM `19.3.0`, Storybook `10.6.1`, 엔진 `6.0.0` 일치를 확인했습니다.
|
||||
4. Runtime Export는 `OrgChart`. Type Export는 `OrgChartProps` 및 Core 공개 타입의 type-only re-export입니다. Props는 공개 `OrgChartOptions`에서 container/onStateChange를 제외하여 재사용합니다. 필수 `dataSource`, `stylesheetUrls`; 선택 `aggregationPolicy`, `className`, `style`, `onStateChange`, `onLifecycleError`. 제목·직원·Zoom 등 임의 Props는 추가하지 않았습니다.
|
||||
5. 생명주기는 Core 생성 → 구독 → mount, Source identity 교체 → reload, cleanup → 구독 해제·destroy입니다. 최초 추가 Reload가 없고 Callback 변경만으로 재생성하지 않습니다. Core가 변경 setter를 제공하지 않는 Policy identity/CSS URL 값 변경은 정리 후 재생성합니다. 같은 URL 문자열의 새 객체는 재생성하지 않습니다. Source/Policy 내부 mutation은 변경 알림으로 취급하지 않습니다.
|
||||
6. committed Callback, 소유권, Source 갱신을 순서대로 layout effect에 연결했습니다. commit과 passive cleanup 사이의 오래된 요청 Callback 가능 구간을 닫습니다. generation/operation 검사는 이전 인스턴스 Promise의 늦은 rejection을 차단하며 Core revision/AbortSignal이 오래된 데이터 응답을 차단합니다. Wrapper 안에 DOM 엔진·Validation·Summary·노드 UI·엔진 정리 로직을 복제하지 않았습니다.
|
||||
7. ESM과 선언을 자체 최소 build.mjs로 생성하며 React, JSX runtime, Core를 external로 유지합니다. 산출물의 client boundary를 보존했습니다. 기존 공통 build 스크립트는 수정하지 않았습니다. 최종 Wrapper ESM 약 3.09 kB/gzip 0.92 kB입니다.
|
||||
8. 기존 HM Theme/폰트를 호스트에서 제공하고 공개 engine.css/style.css의 실제 URL을 Shadow DOM에 주입합니다. Storybook의 기존 preview CSS/decorator를 그대로 사용하며 별도 소비자는 공개 Theme entry를 사용합니다. Production의 해시 Asset URL과 폰트 로드를 실제 확인했습니다. 새 Theme·브랜드 팔레트나 reset을 만들지 않았습니다.
|
||||
9. `src/org-chart/org-chart.stories.tsx`, 분류 `OrgChart/Content`. 기존 glob에 포함되므로 `.storybook/main.ts`/preview 변경이 없습니다. 기존 Component/Pattern Story를 바꾸지 않았습니다. 모든 Story는 공개 Wrapper와 Strict Mode를 사용합니다.
|
||||
10. 필수 Story 8개: Basic Organization, Organization Hierarchy, Employee Cards, Organization Summary, JSON Data Update, Loading / Empty / Error, Responsive Container, Multiple Instances. 기존 HTML v1/v2 JSON을 직접 import하며 추가 시나리오는 파생 가상 데이터 및 같은 Source Interface만 사용합니다. 주변 조작은 공개 HM Button을 실제 렌더링합니다.
|
||||
11. Navigation·Header·Dashboard·CSV·추가 업무 메뉴·API는 없습니다. FilterBar/DataTable은 검색·필터·정렬·선택 요구가 없어 사용하지 않습니다. 노드/상태/집계는 Framework-independent Core의 승인된 책임이며 React HM 위젯으로 복제하지 않습니다.
|
||||
12. 실제 empty 화면을 구현한 것으로 표시하지 않습니다. 직원 0명은 Ready, 조직 0개는 Error입니다. Zoom/Pan은 비활성 상태로 유지합니다. hubText/showDispatched/showOnLeave 등 기존 reserved 영역, 복잡한 relation overlay도 추가 구현하지 않았습니다.
|
||||
|
||||
## 최종 통합 검증
|
||||
|
||||
- **PASS** Wrapper Typecheck: `npm run typecheck:org-chart-react`. 최종 생명주기 수정 후 다시 확인했습니다.
|
||||
- **PASS** 루트 앱·Storybook TypeScript graph: `tsc -p tsconfig.app.json --noEmit`. 별도 React 소비자도 strict Bundler 타입 검사를 통과했습니다.
|
||||
- **PASS** Wrapper Package Build: ESM/선언/client boundary/external 확인. Storybook prebuild의 기존 workspace 빌드도 Core/UI/Center Graphic을 포함해 성공했습니다.
|
||||
- **PASS** 실제 npm tarball Public Export 및 선언: 추출된 Core/Wrapper에서 DOM 없는 ESM import, strict NodeNext consumer, 필수 Source와 미지원 Zoom/container 타입 경계 확인. React/compiler는 현재 검증 환경에서 제공합니다. 새 PC의 fresh dependency install 결과로 해석하지 않습니다.
|
||||
- **PASS** Storybook Production Build: 최종 Wrapper/Story/CSS 상태의 `storybook build` 성공. 큰 기존 chunk 및 plugin timing 경고가 있으나 build 실패는 없습니다.
|
||||
- **PASS** Storybook 실제 브라우저: Production의 8개 Story와 개발 6025의 실제 Wrapper 노드 실행. JSON v2 갱신, Unmount/Mount, Source 오류·복구, 직원/조직 0개, 취소를 무시하는 늦은 응답, A 갱신/제거 시 B identity·데이터·Callback 유지 확인.
|
||||
- **PASS** 독립 React Consumer: `examples/org-chart-react`의 공개 package import·Vite Production Build·실제 Chrome 실행, 초기 mount/Source 교체/최신 Callback/Unmount. Storybook 성공을 소비자 성공으로 대신하지 않았습니다.
|
||||
- **PASS** 개발 React Strict Mode: setup → cleanup → setup에서 두 번의 Source load와 최종 Ready 1회, 활성 Core host 1개. Production은 replay 없이 초기 load/Ready 각 1회임을 별도로 확인했습니다. 두 Source load는 Strict Mode의 의도된 setup 반복이며 Wrapper의 초기 이중 Reload가 아닙니다.
|
||||
- **PASS** 생명주기: Callback/className 변경 시 같은 Core·추가 load 없음; Source 교체 시 같은 Core reload; Policy/CSS URL 변경 시 새 Core; 같은 CSS URL 값은 유지. 실패한 구성의 lifecycle callback, 정상 오류→Ready 복구, 늦은 응답·Unmount 후 Callback 차단, 5회 반복 mount/unmount의 DOM/관찰자 기준 유지, 최종 Core DOM/관찰자 0 확인.
|
||||
- **PASS** HTML/React 공통 데이터: v1 조직 3개·직원 4명·직접 Root/A/B 1/2/1, v2 조직 3개·직원 5명·직접 1/1/3. DOM의 organization ID/parentId·employee ID·노드 텍스트·Heading·Summary를 비교하여 일치했습니다.
|
||||
- **PASS** Theme CSS: HTML/React의 전경·surface·radius·폰트 computed 일치, 기존 Theme 변수 상속, 두 CSS link의 실제 stylesheet 로드, scoped Theme 변경 시 다른 인스턴스 유지, Engine selector의 light DOM 오염 없음.
|
||||
- **PASS** 반응형: 실제 390/768/1280/1440px에서 소비자 및 Basic/Employee/Responsive Story의 document overflow·노드 겹침·텍스트 폭·null 사진 fallback 확인. scrollWidth/clientWidth 및 scrollHeight/clientHeight 측정을 저장했습니다. 390px은 원래 노드 크기와 내부 가로 스크롤을 유지합니다. 실제 scrollLeft 이동도 확인했습니다. 소비자 Grid item min-width와 Story Grid track을 수정하여 문서 overflow를 제거했습니다.
|
||||
- **PASS** 키보드: Enter/Space 접기·펼치기 및 데이터 교체로 기존 focus가 제거될 때 안정된 Core viewport로 복귀. 접기 애니메이션이 끝나기 전 재입력은 기존 Core가 거부하므로 검사도 실제 transition 완료를 기다립니다.
|
||||
- **PASS** 기존 Vanilla: 기존 HTTP HTML의 v1→v2 비교 및 `file://` classic의 Ready 4명→Update 5명 smoke. 변경 없는 기존 22개 데이터 테스트/69개 전체 브라우저 검사는 반복하지 않았습니다.
|
||||
- **PASS** Chrome `155.0.8059.39`: 통합 검사 **70/70**, 추가 경계 검사 **9/9**, console/pageerror **0**. 대표 390px 긴 텍스트·Responsive 및 1440px 소비자 캡처도 육안 확인했습니다.
|
||||
- **PASS** 보존: 시작 파일 **614/614 SHA-256 일치**, 기존 Core/HTML/7개 설계 문서/이전 구현 보고/UI/Center Graphic/Theme/Story/공유 build 설정 보존. `git diff --check` 통과. 운영 main Working Tree는 시작·완료 시 깨끗합니다. Staged diff는 비어 있으며 Stage/Commit/Push/Merge가 없습니다.
|
||||
|
||||
## Guard와 미검증
|
||||
|
||||
- Storybook과 독립 소비자에 범위별 PARTIAL Guard를 실행했습니다. 최종 원본 status/staticStatus는 **NOT VERIFIED**, FAIL 0입니다. Storybook의 동적 width, 소비자의 동적 className 및 실제 브라우저 증명 요구를 그대로 유지합니다.
|
||||
- 원본 CSS styleEvidence는 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED**입니다. 별도 브라우저 computed·컨테이너 검사 PASS는 기록한 요소/상태/폭의 증거이며 Guard 원본이나 디자인 의도 승인을 덮어쓰지 않습니다. Guard 결합 판정은 수행하지 않았습니다.
|
||||
- 최초 소비자 Guard의 target 상대경로 입력이 맞지 않아 checkedFiles=[] 진단이 발생했습니다. 원본 `guard-consumer-initial.json`을 보존하고 상대경로/entry를 교정한 실제 graph 검사를 별도로 저장했습니다. Story CSS 수정 후 CSS-only PARTIAL은 NOT APPLICABLE이어서 최종 Story graph 검사를 별도로 저장했습니다. 빈 검사 결과를 PASS로 계산하지 않았습니다.
|
||||
- **NOT VERIFIED** 완전한 Heap/GC 회수, 전체 스크린리더·SSR/hydration, 모든 브라우저/모바일 touch, 매우 큰 데이터 성능·깊이, Workdesk 원본과의 시각적 일치, Theme 미연결 스타일, 실제 API/auth, fresh npm registry 설치 및 임의 배포 base/CSP/origin입니다.
|
||||
- Scope 한계: 공개 Props의 arbitrary className/style 및 모든 외부 데이터·Theme 조합은 전수 검사하지 않았습니다. readOnly/disabled 입력 필드는 없는 콘텐츠 패키지이며 전체 업무 화면 접근성 검증으로 해석하지 않습니다.
|
||||
- 기술적 리스크: 기존 Core의 엔진 6.0.0 내부 init observer hook 의존은 유지됩니다. 엔진 upgrade 시 정리 계약 재검토가 필요합니다. Policy identity 변경은 재생성되므로 immutable/memoized Props를 권장합니다. Core 갱신 시 expansion/scroll state 보존은 추가하지 않았습니다.
|
||||
- 도구 이슈: 로컬 npm symlink EPERM 및 sandbox Chrome loopback 차단은 허용된 실행 권한으로 재시도해 해결했습니다. 최초 sandbox Storybook의 사용자 설정/registry 쓰기 경고도 접근 가능한 개발 서버 재실행 후 사라졌습니다. 운영 6024 서버를 중단하지 않았습니다.
|
||||
|
||||
## 파일과 증거
|
||||
|
||||
이번 변경(기존 작업 위에 추가):
|
||||
|
||||
- 수정 `package.json`: 기존 OrgChart scripts 유지, Wrapper build/typecheck·소비자 build/dev/browser scripts, Core/Wrapper root dependency 연결.
|
||||
- 수정 `package-lock.json`: root Core/Wrapper dependency, Wrapper workspace link·manifest. 해당 추가를 제거하고 원래 CRLF로 재직렬화한 SHA가 시작 lockfile SHA와 일치하므로 기존 dependency graph/버전은 보존했습니다. npm 재직렬화로 line ending은 LF입니다.
|
||||
- 신규 `packages/org-chart-react/{package.json,build.mjs,tsconfig.json,tsconfig.build.json,README.md,src/OrgChart.tsx,src/types.ts,src/index.ts}`.
|
||||
- 신규 `src/org-chart/{org-chart.stories.tsx,org-chart-stories.css}`.
|
||||
- 신규 `examples/org-chart-react/{index.html,index.css,app.tsx,vite.config.ts,README.md,verify.mjs,verify-package.mjs,verify-boundaries.mjs}`.
|
||||
- 신규 이 보고서. 시작 시 기존 package/lock의 Core 관련 diff와 untracked docs/examples/Core는 이번 신규 변경과 구별하여 보존했습니다.
|
||||
|
||||
검증 생성물(Git ignored):
|
||||
|
||||
- `test-results/org-chart-react/report.json`: 70개 검사, HTML/React snapshots, four-width 측정과 PNG.
|
||||
- `test-results/org-chart-react/boundaries.json`: 추가 9개 경계 검사.
|
||||
- `test-results/org-chart-react/tarball-consumer/report.json`: public archive/declaration 검사.
|
||||
- `test-results/org-chart-react/preservation.json`, `baseline.json`, `baseline-lock-reconstructed.json`: 시작/완료 보존 근거.
|
||||
- `test-results/org-chart-react/guard-storybook-final.json`, `guard-consumer.json`: 최종 원본 Guard. 초기/부분 진단도 별도 보존.
|
||||
- `test-results/org-chart-react/storybook-build.log`: 최종 Production Build 로그.
|
||||
|
||||
확인용 개발 URL:
|
||||
|
||||
- Storybook `http://127.0.0.1:6025/?path=/story/orgchart-content--basic-organization`.
|
||||
- 독립 React 소비자 `http://127.0.0.1:6026/`.
|
||||
- 기존 운영 Storybook `6024`, 원래 PID `19932` 유지.
|
||||
|
||||
## 시간 기록 (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-12 10:24:24 KST
|
||||
- Completed At: 2026-10-12 10:42:39 KST
|
||||
- Actual Duration: 18분 15초
|
||||
- Estimated Duration: 최초 2~3시간
|
||||
- Variance: 예상 하한 2시간 대비 101분 45초 단축; 예상 상한 3시간 대비 161분 45초 단축
|
||||
|
||||
예상 시간은 참고값입니다. 기존 Core/설치 환경과 병렬 구현을 재사용해 실제 시간이 줄었으며 미검증을 PASS로 대체하지 않았습니다. Commit·Push·Merge는 사용자 요청대로 진행하지 않았습니다.
|
||||
@@ -0,0 +1,188 @@
|
||||
# HMDesign OrgChart P10.7 — 정식 통합 승인 준비
|
||||
|
||||
문서 정리 대상: 2026-10-13. **main 병합 준비 상태: PENDING — READY 아님.** P11.2 사용자 결정으로 Storybook 4개 화면과 Auto Chart Only React/Vanilla의 기본 디자인은 **APPROVED**다. P11.1 실제 Guard는 정적 PASS / FAIL 0이며 Runtime은 NOT VERIFIED다. 보안·접근성 위험 수용·main 병합·운영 배포 승인은 **PENDING**이다. 이 문서는 병합/배포 실행 승인이 아니다.
|
||||
|
||||
현재 디자인 기준은 [P11.2 최종 디자인 승인 기록](design-approval.md)을 따른다. 아래 P10.7 Git·검증 snapshot과 과거 Guard 실패 근거는 당시 기록으로 보존하며, 현재 상태와 구분한다.
|
||||
|
||||
## 1. 범위와 Git 기준
|
||||
|
||||
| 항목 | 확인 결과 |
|
||||
|---|---|
|
||||
| 작업 경로 | `D:\디자인시스템\HMDesign-OrgChart-Recovered` |
|
||||
| 브랜치 | `feature/orgchart-package` |
|
||||
| 공식 origin | `https://gitea.hmac.kr/b25027/HMDesign.git` |
|
||||
| 작업 전·후 Local HEAD | `d56d587fd20316f19554105f33c200060394962b` |
|
||||
| 원격 Feature | `d56d587fd20316f19554105f33c200060394962b` |
|
||||
| 원격 main | `c45d63a03b84eaac43f01bd400a16488a80e4229` |
|
||||
| 원격 확인 방법 | P10.7에서 `git ls-remote origin refs/heads/feature/orgchart-package refs/heads/main` 읽기 전용 조회 성공 |
|
||||
| 작성자 설정 | `b25027` / `shkim4@samaneng.com` |
|
||||
| 시작 Working Tree | 깨끗함 |
|
||||
| 완료 Working Tree | 사용 가이드 수정 1개 + 이 문서 신규 1개, unstaged |
|
||||
|
||||
이 작업의 명시적 경로·Commit/Push 보류 요청을 적용했다. 보존용 `HMDesign-OrgChart`는 증거를 읽기만 했으며 수정하지 않았다. 제품 코드·Storybook·Theme·Guard·package.json·lockfile·Docker 및 AGENTS.md/CLAUDE.md/AI 생성 정책을 변경하지 않는다. Stage·Commit·Push·main Checkout·Merge·PR·Workflow 실행·서버 접속·Registry Publish 미진행.
|
||||
|
||||
## 2. 개발 완료와 재사용 근거
|
||||
|
||||
현재 공개 API와 기준 SHA의 소스를 대조했다. [사용 가이드](org-chart-usage-guide.md), [최종 통합 보고서](final-integration-2026-10-13.md), [Chart Only 구현 보고서](implementation-report-chart-only.md), [최소 설치 보고서](minimal-install-report-2026-10-13.md)가 기능 근거다.
|
||||
|
||||
| 대상 | 기존 검증 상태 | P10.7 해석 |
|
||||
|---|---|---|
|
||||
| Fixed Full Composition / Expanded | 지원, 기존 기능 검사 PASS | Fixed Chart Only 지원을 뜻하지 않음 |
|
||||
| Core / React / Vanilla 독립 사용 | 기존 설치·타입·빌드·실행 PASS | HM UI는 `/hm-dialog` 선택 시에만 연결 |
|
||||
| Auto/Expanded Chart Only | 최종 통합 React/Vanilla 51/51 PASS, 콘솔 오류 0 | 초기 Fit·Bounds 가로/세로 중앙·직접 휠 Zoom·Pan·팝업·접기 유지 |
|
||||
| 기존 Expanded 기본 표시 | 기존 제목·요약·툴바·modifier 휠 유지 확인 | Chart Only는 명시적 선택 옵션 |
|
||||
| Core 단위 테스트 | 86/86 PASS | 기존 결과, 재실행 안 함 |
|
||||
| Guard 회귀 테스트 | 94/94 PASS | 분석기 자체 검사이며 실제 Guard FAIL과 별개 |
|
||||
| AI 생성 정책 | 생성 방식 사전 질문 정책 통합 | 기존 정책 문서 수정 없음 |
|
||||
|
||||
Chart Only 공개 값은 `presentationMode: 'default' | 'chart-only'`, `initialView: 'center' | 'fit'`, `wheelZoom: 'modifier' | 'direct'`다. Core 옵션을 React Props가 상속한다. Chart Only의 기본은 fit/direct이며 외부 제목·요약·툴바·설명을 생성하지 않는다. Fixed Full Composition에서 Chart Only를 함께 지정하면 TypeError로 거부한다. Fixed Chart Only는 **미지원**이지 승인 대기 구현이 아니다.
|
||||
|
||||
**P10.6 재사용:** [HMDesign OrgChart P10.6 점검](codex://threads/01a1350c-f2eb-7ca0-9230-abda73022abc) 채팅의 2026-10-13 최종 보고를 확인했다. 관련 파일 143개가 보존본과 줄바꿈을 제외하고 동일하다는 기존 확인, 정적 main 비교, 브랜치 정책 조회, 캡처 검토 결과를 재사용한다. P10.6은 파일 변경 0개였고 별도 tracked 보고서가 없다. 이번 원격 SHA가 같으므로 전체 비교·제품 테스트·빌드·Guard·npm audit·브라우저 검사를 반복하지 않았다.
|
||||
|
||||
복구 Clone에는 ignored 캡처/JSON이 없다. 아래 기존 증거는 **보존 폴더의 로컬 자료**이며 다른 PC/새 Clone에는 포함되지 않는다. 파일 존재와 기록은 확인했지만 P10.7에서 캡처를 새로 생성하거나 화면을 다시 승인하지 않았다. 과거 보고서와 원본 결과는 수정하지 않는다.
|
||||
|
||||
## 3. Guard 현재 상태와 P10.7 잔여 FAIL 5건의 과거 예외 요청
|
||||
|
||||
**현재 상태(P11/P11.1): 실제 Guard `status PASS`, `staticStatus PASS`, issues 0 / FAIL 0, `runtimeStatus NOT VERIFIED`.** [P11 구현·검증](implementation-report-p11.md)과 [P11.1 정합성 검증](consistency-review-p11-1.md)의 실제 검사 결과를 따른다. CSS phases는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 결합 styleVerification은 미생성이다. 정적 PASS를 Runtime 또는 제품 전체 회귀 PASS로 확대하지 않는다. P11.2에서는 기존 결과만 확인하고 Guard를 재실행하지 않았다.
|
||||
|
||||
아래는 **P10.7 당시 snapshot 및 예외 요청 이력**이다. P11의 공개 CSS 연결 분석 개선으로 아래 5건은 해결되어 현재 잔여 FAIL이나 진행 중인 예외 승인 요청이 아니다. 당시 실패·제안·승인 대기 기록은 보존하며 후속 기술적 해결을 당시 위험 수용 승인으로 바꾸지 않는다.
|
||||
|
||||
P10.7 당시 기록: **TOKEN-001 FAIL 5 / NOT VERIFIED 9**. F3–F7 해결로 기존 FAIL 10→5, 새 FAIL 0이었다. 기존 [P10 보고서](implementation-report-p10.md), [P10.3 GuardReview](guard-review-p10-3.md)의 FAIL 10건 기록을 보존한다.
|
||||
|
||||
원본 로컬 결과: `D:\디자인시스템\HMDesign-OrgChart\_local\final-integration\guard-final.json`. P10.7에서 읽은 원본은 `status/staticStatus = FAIL`, `runtimeStatus = NOT VERIFIED`이며 수정하지 않았다. `styleEvidence`는 **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED**, 결합 `styleVerification`은 미생성이다. 디자인 의도 상태나 회귀 94/94로 원본 Guard를 PASS로 바꾸지 않는다.
|
||||
|
||||
공용 분석기는 Shadow DOM에 주입되는 공개 OrgChart CSS와 런타임 `style.setProperty` 정의를 충분히 추적하지 못한다. 현재 선언을 소스에서 다시 대조했다.
|
||||
|
||||
| 기존 ID / 검사 | 실제 토큰명 | 사용 위치 | 실제 선언·연결 근거 | 기술적 영향 / 예외 사유 | 책임자·승인자 / 승인일 / 결과 |
|
||||
|---|---|---|---|---|---|
|
||||
| F1 / TOKEN-001 | `--hm-orgchart-current-gradient` | `department-colors.ts:47` | 같은 preview row에 `applyOrganizationStyle(row,index)` 호출(`:28`), `style-presets.ts:71` setter → swatch 자손 상속 | Department Colors 미리보기 참조. 실제 선언은 있으나 Guard 정의 목록 밖 | PENDING / PENDING / PENDING |
|
||||
| F2 / TOKEN-001 | `--hm-orgchart-current-card-background` | `department-colors.ts:47` | 위 row 경로, `style-presets.ts:63–64` setter | F1과 같은 상속 경로, 미선언 토큰을 새로 허용하는 요청 아님 | PENDING / PENDING / PENDING |
|
||||
| F8 / TOKEN-001 | `--hm-orgchart-resolved-bg-surface` | `style-presets.ts:9` | `src/styles/design-tokens.css:8` 선언 → `build.mjs`의 CSS inline → 공개 `@hm-design/org-chart/style.css` → Core Shadow DOM link | 실제 CSS load가 필요한 경로. 연결이 없는 소비자까지 정상으로 간주하지 않음 | PENDING / PENDING / PENDING |
|
||||
| F9 / TOKEN-001 | `--hm-orgchart-current-border` | `style-presets.ts:74` | 같은 함수·element의 `:69–70` setter 뒤 참조 | Fixed/공통 스타일의 alias. 분석기 setter 수집 한계 | PENDING / PENDING / PENDING |
|
||||
| F10 / TOKEN-001 | `--hm-orgchart-current-gradient` | `style-presets.ts:75` | 같은 함수·element의 `:71–72` setter 뒤 참조 | F9와 동일. 선언과 사용 범위가 일치 | PENDING / PENDING / PENDING |
|
||||
|
||||
위 파일의 기준 경로는 `packages/org-chart/src/core/`이며 CSS는 `packages/org-chart/src/styles/`다. [style-presets 소스](../../packages/org-chart/src/core/style-presets.ts), [Department Colors 소스](../../packages/org-chart/src/core/department-colors.ts), [CSS 선언](../../packages/org-chart/src/styles/design-tokens.css), [공개 CSS 생성](../../packages/org-chart/build.mjs), [Shadow DOM 연결](../../packages/org-chart/src/core/index.ts)을 근거로 한다.
|
||||
|
||||
**권장 결정:** 위 SHA·5개 위치·실제 연결/선언이 확인된 경로에 한정해 위험 예외 수용을 요청하고 공용 분석기 개선을 별도 과제로 분리한다. 새 파일·새 토큰·외부 입력·다른 element·미연결 CSS에는 예외를 확대하지 않는다. 새 Commit에서 이 경로가 바뀌면 근거를 다시 검토한다. 예외를 문서화하거나 승인하더라도 원본 FAIL은 남는다.
|
||||
|
||||
조직 정책상 Guard PASS 필수 여부와 예외 수용 가능 여부는 **NOT VERIFIED**다. 정책이 필수라면 권한 있는 승인자의 예외 또는 후속 개선이 병합 전 필요하다. 제품 실행을 막는 결함이 확인되지 않았다는 기존 분석을 배포 허가로 해석하지 않는다.
|
||||
|
||||
후속 개선: OrgChart 공개 CSS/`?url` 연결과 제한적인 literal setter 추적. 무조건 전역 정의로 수집하면 다른 요소·조건부 setter·미연결 CSS의 실제 미선언 참조까지 통과시킬 수 있다. 분석기 변경 범위와 회귀 기준을 별도 승인하고 기존 차단 판정을 보존해야 한다. 이번 검사기 수정·재실행·결과 변조 없음.
|
||||
|
||||
## 4. npm 보안 위험 수용 요청
|
||||
|
||||
**과거 감사 snapshot:** [2026-10-10 퍼블리셔 안정화 보고서](../guidelines/publisher-stabilization-2026-10-10.md)의 전체 감사 High 7 / 고유 Advisory 1 / 운영(`--omit=dev`) 감사 0. `GHSA-vfj7-8cjw-p6xm`이며 분석 당시 수정 패치 미공개였다. 현재 전체 패키지의 신규 audit 결과가 아니다. P10.6에서 해당 경로의 버전 동일성을 확인했지만 이후 OrgChart 연동 때문에 lockfile 전체는 과거 감사본과 다르다. 따라서 운영 0을 현재 그래프의 보안 보증으로 사용하지 않는다.
|
||||
|
||||
공식 [Advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm)는 deeply nested brace pattern으로 Node stack을 소진하는 DoS를 설명한다. P10.7에서는 링크·권고 내용을 읽기만 했고 npm audit나 의존성 업데이트를 하지 않았다.
|
||||
|
||||
| 패키지 | 과거 설치 버전 | 개발 의존 경로 |
|
||||
|---|---|---|
|
||||
| shadcn | 4.21.1 | UI workspace 직접 devDependency |
|
||||
| @shadcn/registry | 0.1.0 | shadcn → registry |
|
||||
| ts-morph | 26.0.0 | shadcn 또는 registry → ts-morph |
|
||||
| @ts-morph/common | 0.27.0 | ts-morph → common |
|
||||
| fast-glob | 3.3.3 | shadcn/registry 또는 common → fast-glob |
|
||||
| micromatch | 4.0.8 | fast-glob → micromatch |
|
||||
| braces | 3.0.3 | micromatch → braces, 근본 취약 패키지 |
|
||||
|
||||
7건은 상위 경로 전파를 포함하며 독립 취약점 7개가 아니다. 개발 도구 전용 경로라는 기존 분석을 유지하되, 개발/CI 중 공격자가 입력을 제어할 가능성은 별도로 관리한다.
|
||||
|
||||
| 수용 조건·결정 항목 | 근거 / 처리 방침 | 책임자·승인자 / 승인일 / 결과 |
|
||||
|---|---|---|
|
||||
| 고정 Glob 사용 | 기존 Guard STORYBOOK discovery는 로컬 설정의 literal glob을 fast-glob에 전달. 신뢰하는 저장소 설정만 사용 | PENDING / PENDING / PENDING |
|
||||
| 공격자 입력 경로 | 비신뢰 Storybook config·registry·중첩 pattern을 CLI/Guard에 전달하면 프로세스 종료 위험. 개발 도구 외부 노출 금지, 비신뢰 입력 경로 차단 | PENDING / PENDING / PENDING |
|
||||
| 소비 앱 영향 | 기존 공개 UI pack은 CLI 개발 의존 그래프를 배포하지 않는다는 분석. 현재 모든 소비 경로의 무위험을 보장하지 않음 | PENDING / PENDING / PENDING |
|
||||
| CI 필수 감사 | P10.6 main Workflow에 Guard/audit 차단 단계 없음. 조직 정책·현재 감사 요구 수준은 NOT VERIFIED. 보안 담당자가 신규 audit 필요 여부와 gate/예외 조건 결정 | PENDING / PENDING / PENDING |
|
||||
| 패치 후 업데이트 | 패치 공개 시 영향 범위·호환성 확인 → 별도 package/lockfile 변경 → 관련 설치·타입·빌드·audit 검증 | PENDING / PENDING / PENDING |
|
||||
| 잔여 위험 수용 | 개발 전용·고정 입력 조건, 예외 적용 환경·재검토 시점·해제 조건을 보안 담당자/사용자가 확정 | PENDING / PENDING / PENDING |
|
||||
|
||||
패치 없음은 위험 없음이 아니다. 신규 감사와 최신 전체 그래프 평가는 **NOT VERIFIED**. `npm audit fix --force`, override/major 역행, package.json/lockfile 변경 없음.
|
||||
|
||||
## 5. 화면별 최종 디자인 승인 체크리스트
|
||||
|
||||
기능 PASS와 사용자 디자인 승인을 분리한다. 아래 기능 검증은 기존 보고서와 P10.6 검토를 재사용하며 P11.2에서 브라우저 재검사는 없다. P10.7 당시 5개 화면의 디자인 승인·승인자·승인일은 모두 PENDING이었다. **현재는 [P11.2 사용자 디자인 승인](design-approval.md)에 따라 5개 화면 APPROVED / 사용자 디자인 승인 / 2026-10-13**이며 기준 Commit은 `346c648c093ae3b0dedc1fc754d8ffa52f00ddd0`이다. 실제 담당자의 법적 이름·직책은 확인되지 않았다. 기존 미검증 항목은 유지한다.
|
||||
|
||||
기존 통합 캡처 기준 폴더: `D:\디자인시스템\HMDesign-OrgChart\test-results\org-chart-chart-only\`. 아래 링크는 현재 PC의 보존 자료이며 Git 배포 문서의 필수 실행 파일이 아니다.
|
||||
|
||||
| 화면 | 사용자 검토·승인 항목 | 실제 기능 검증·근거 / 캡처 | 미해결 문제 | 디자인 승인 / 승인자 / 승인일 |
|
||||
|---|---|---|---|---|
|
||||
| A. Overview | 관계사 로고 7개, 중복 제목 제거, 인원 현황, 기존 페이지 스타일 | 기존 P9/P10·P10.6 확인 범위 PASS. [캡처](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/storybook/orgchart--logo-personnel-summary.png) | 전체 폭 수동 시각 검토는 NOT VERIFIED | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
| B. Department Colors | 대표자 카드, 기존 조직 Gradient **6종**, 파견·휴직 미리보기, `홍 길 동`, 카드 크기·Typography | 기존 P10·최종 통합·P10.6 확인 범위 PASS. [캡처](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/storybook/orgchart--department-colors.png) | Guard F1/F2는 P11에서 해결. 6개 조직 Gradient와 Expanded의 7회사 primary 팔레트를 구분 | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
| C. Full Composition | 대표자 아래 화살촉, 협업 화살표 5개, 상단·중앙·하단 반응형 배치, 파견·휴직 간격, 관계사별 인원, Core 팝업 | [P9.5 Full](implementation-report-p9-5-full-composition.md)·[팝업 검증](verification-p9-5-popup-parity.md) 및 P10.6 재사용. [캡처](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/storybook/orgchart--full-composition.png) | [P10.10](visual-review-p10-10.md)에서 390px 내부 스크롤·잘림 오류 재현 안 됨. 구형 내부 스크롤 기대값의 과거 실패 기록 보존, 전체 디바이스 PASS 아님 | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
| D. Expanded OrgChart | 회사별 20/100/300명, 회사 컬러 7종, TB/LR/Hybrid, Drag & Drop ON/OFF, Pan/Zoom/Fit, 직원 Dialog, 그룹·연결선 | 기존 P9.4/P9.5·최종 통합·P10.6 확인 범위 PASS. [캡처](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/storybook/orgchart--expanded-org-chart.png) | 삼안·장헌산업 일반 제목 대비 미달. 모든 회사·폭 수동 검토 미완료 | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
| E. Chart Only | 흰 캔버스, 외부 UI 없음, 초기 Fit, 가로·세로 중앙, 직접 휠 Zoom, React/Vanilla 동일 동작 | 최종 통합 51/51 PASS(기존). [React](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/react-100-desktop.png) · [Vanilla](D:/디자인시스템/HMDesign-OrgChart/test-results/org-chart-chart-only/vanilla-100-desktop.png) | Auto 전용. Fixed Chart Only 미지원. 같은 회사 제목 대비 위험 적용 | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
|
||||
Full의 대표자 화살촉 1개와 협업 화살표 5개는 별개다. Cell Border Only 실험 디자인은 사용자 요청으로 **정식 적용 제외**했으며 승인 대상 기능에 포함하지 않는다. 이를 재적용하지 않는다.
|
||||
|
||||
화면별 승인 시 체크할 접근성 항목: control ID/접근성 이름, 키보드 탐색과 focus 복귀, disabled/readOnly, 팝업 열기·닫기, 접기 후 정보 보존, 390/768/1280/1440px overflow·가독성. 기존 검사 범위 밖의 전체 조합은 **NOT VERIFIED**이며 특히 실제 스크린리더·물리 모바일·Safari/Firefox·모든 회사/폭의 수동 검토는 남아 있다. 모든 조합을 위 기능 PASS에 포함시키지 않는다.
|
||||
|
||||
## 6. 색상 대비와 접근성 예외 요청
|
||||
|
||||
[회사 primary 선언](../../packages/org-chart/src/company-color-presets.ts)과 흰색 그룹 제목 정책을 유지한다. 아래 값은 P10.6의 선언색 계산 결과이며 새 브라우저 픽셀 실측이 아니다. [W3C SC 1.4.3](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)의 일반 텍스트 AA 기준은 4.5:1이다.
|
||||
|
||||
| 회사 | 배경 / 제목 | 기존 대비 계산 | 접근성 상태 | 사용자 디자인 결정 | 조직 예외 수용 / 승인자 / 승인일 |
|
||||
|---|---|---|---|---|---|
|
||||
| 삼안 | `#F08300` / `#FFFFFF` | 약 2.64:1 | 일반 텍스트 AA 미달 | 회사 메인색·흰 제목 유지(기존 결정) | PENDING / PENDING / PENDING |
|
||||
| 장헌산업 | `#4580A6` / `#FFFFFF` | 약 4.29:1 | 일반 텍스트 AA 미달 | 회사 메인색·흰 제목 유지(기존 결정) | PENDING / PENDING / PENDING |
|
||||
|
||||
대상은 해당 회사색을 쓰는 Expanded/Auto Chart Only 그룹 제목이다. 기존 디자인 유지 결정은 접근성 위험 수용이나 WCAG 준수 승인이 아니다. 예외를 수용해도 WCAG PASS로 표시하지 않는다. 조직 정책상 예외 허용 여부·책임자·적용 화면·재검토 기한을 확정해야 한다. 현재 정책은 **NOT VERIFIED**다.
|
||||
|
||||
후속 개선 후보는 제목 영역·글자/배경 조합을 별도 디자인 검토하는 것이다. 회사색·흰 제목 정책을 바꾸는 구현은 별도 사용자 결정 후 수행한다. 이번 색상·카드·Typography 변경 없음.
|
||||
|
||||
## 7. main 정적 비교와 자동 배포 승인
|
||||
|
||||
원격 SHA가 P10.6과 동일하므로 당시 분석을 재사용한다. 공통 조상 `d18e3c33801e165e17b6d971e2f6198eed483c3c`, main 전용 Commit **3**, Feature 전용 Commit **12**, main의 배포 관련 **6개 파일**과 Feature 변경 파일의 충돌 후보 **0**. 이 결과는 파일 집합의 정적 비교이며 실제 Merge나 통합 Linux/Docker 빌드 성공 증거가 아니다.
|
||||
|
||||
P10.6 Gitea API 조회 결과: main 브랜치 보호 없음, 필수 승인 0, 필수 상태 검사 비활성. 이번 서버 설정 재조회는 하지 않았다. 이 설정을 사용자 병합 승인으로 해석하지 않는다. main Workflow의 Guard/npm audit 차단 단계 부재와 조직의 필수 gate 정책을 구분한다.
|
||||
|
||||
[기준 main 배포 Workflow](https://gitea.hmac.kr/b25027/HMDesign/src/commit/c45d63a03b84eaac43f01bd400a16488a80e4229/.gitea/workflows/deploy-storybook.yml)는 **main Push 또는 수동 실행** 조건으로 해당 SHA 소스를 SSH로 보내 release directory에서 Docker 빌드 후 Storybook 서비스를 교체하는 경로를 갖는다(P10.6 및 [P10 분석](implementation-report-p10.md)). 따라서 main Merge/Push는 운영 서비스 변경으로 이어질 수 있다. 실제 Actions 활성·Runner/Secret 상태가 확인되지 않았으므로 배포 실행 여부를 단정하지 않는다. Workflow 실행·서버 접속·Docker 검토/구성/배포를 이번에 수행하지 않았다.
|
||||
|
||||
| 필수 확인·승인 | 확정할 내용 | 현재 확인 상태 | 담당자·승인자 / 승인일 / 승인 결과 |
|
||||
|---|---|---|---|
|
||||
| main 병합 승인자 | 병합 권한자, 승인할 Feature SHA 및 문서 변경 포함 여부 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
| 배포 담당자 | 운영 서비스 책임자와 실행·연락 주체 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
| Gitea Actions 활성 | 저장소/서버 활성 여부, 최근 실행 여부 | NOT VERIFIED(P10.6 API 인증 제한) | PENDING / PENDING / PENDING |
|
||||
| Runner 상태 | 온라인 상태·label·실행 환경·작업 가능 여부 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
| 서버 접근 권한 | SSH Secret/변수, 대상 서버·서비스 교체 권한 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
| 사용자 영향 | 서비스 대상·접속 경로·점검 시간·중단 가능성 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
| 배포 실패 복구 | 이전 정상 SHA/artifact·release 경로·복구 절차·담당자·성공 확인 방법 | NOT VERIFIED, 실제 복구 절차 미확정 | PENDING / PENDING / PENDING |
|
||||
| 배포 실행 허용 | 승인된 SHA의 자동/수동 배포 허용 여부와 실행 시점 | NOT VERIFIED | PENDING / PENDING / PENDING |
|
||||
|
||||
**READY 전제:** 현재 디자인은 APPROVED, 실제 Guard 정적 FAIL은 0이다. Guard Runtime/조직 gate 정책 확인, 보안·접근성 위험 수용과 위 운영 항목 확정, 권한 있는 승인자의 명시적 main 병합·배포 허가가 필요하다. 후속 승인 작업에서 실행 직전 원격 SHA를 다시 확인하고 승인된 통합 상태의 필요한 빌드 검증을 수행한다. P10.7 당시 문서 변경은 미커밋이었다. 현재 P11.2 변경도 미커밋이며 기준 Feature SHA에 새 승인 기록이 포함됐다고 주장하지 않는다. main에 기존 Docker 설정이 있다는 이유로 새 Docker 구성 작업을 추가하지 않는다.
|
||||
|
||||
## 8. 후속 개선 목록과 사용자 승인 기록
|
||||
|
||||
| ID | 후속 결정/작업 | 선행 조건 | 상태 / 승인자 / 승인일 |
|
||||
|---|---|---|---|
|
||||
| G-01 | P10.7 Guard 5건 한정 예외 요청 이력 | 당시 요청은 §3에 보존. P11/P11.1 실제 정적 FAIL 0, 조직 gate/Runtime은 별도 | 기술적 해결로 현재 예외 요청 대상 아님 / 당시 PENDING 보존 |
|
||||
| G-02 | 공용 Guard 공개 CSS 연결 추적 개선 | P11은 공개 CSS 연결 분석만 개선, setter 자동 승인·규칙 우회 없음 | 구현 완료(P11) / P11.1 정적 PASS / Runtime NOT VERIFIED |
|
||||
| S-01 | 개발 보안 위험 수용/추가 audit 필요 여부 | 보안 담당자·입력 통제·재검토 조건 결정 | PENDING / PENDING / PENDING |
|
||||
| D-A~E | 화면 5개 최종 디자인 승인 | [승인 기록](design-approval.md), 기준 Commit `346c648c093ae3b0dedc1fc754d8ffa52f00ddd0` | APPROVED / 사용자 디자인 승인 / 2026-10-13 |
|
||||
| A-01 | 삼안·장헌산업 대비 위험 수용/후속 디자인 | 조직 접근성 정책·책임자·적용 범위 확정 | PENDING / PENDING / PENDING |
|
||||
| M-01 | main 병합 승인 | 위 결정과 운영 배포 항목 확정, 원격 SHA 재확인 | PENDING / PENDING / PENDING |
|
||||
| R-01 | 자동 배포 실행 승인 | Actions/Runner/서버/영향/복구 담당자 확인 | PENDING / PENDING / PENDING |
|
||||
|
||||
P10.7 당시 확인된 사용자 결정: 회사 메인색·흰 제목 유지, Cell Border Only 정식 적용 제외, Gitea 소스 공유 유지, 문서만 수정하고 Commit/Push 보류. 당시 최종 화면 승인과 각 위험 수용·병합·배포는 미승인이었다. **P11.2에서 화면 5개 최종 디자인 승인만 APPROVED로 갱신한다.** 7회사 기본색·흰 제목은 유지하며 Cell Border Only와 연한 배경형 B안은 적용하지 않는다. 접근성·보안 위험 수용·main 병합·운영 배포는 PENDING으로 유지한다. Guard 5건의 기술적 해결을 위험 수용 승인으로 해석하지 않는다.
|
||||
|
||||
## 9. 문서 검증과 미검증 경계
|
||||
|
||||
아래 표와 13/13 결과는 **P10.7 당시 문서 검증 snapshot**이다. 당시 FAIL·승인 PENDING을 현재 상태로 해석하지 않는다. 현재 디자인·Guard 상태는 위 P11.2 갱신을 따른다.
|
||||
|
||||
P10.7 검증은 문서 정합성 범위다. 기존 기능 PASS 수는 재실행 결과로 제출하지 않는다.
|
||||
|
||||
| 문서 확인 항목 | 결과·방법 |
|
||||
|---|---|
|
||||
| Markdown 파일·캡처 링크 | PASS — 두 문서의 로컬 대상 86개 존재, 내부 앵커 3개 존재 확인 |
|
||||
| 외부·채팅 참조 | PASS — 공식 Advisory/W3C 읽기 성공, 기준 Gitea Workflow HTTP 200, P10.6 채팅 읽기 성공 |
|
||||
| 공개 API·React/Vanilla 예제 | PASS(정적) — 타입 선언, Props 상속, Core/React 전달, package exports, 공개 CSS, HM Entry 및 빌드 명령을 소스와 대조 |
|
||||
| 현재 Feature Commit | PASS — Local HEAD와 원격 Feature, 문서 SHA 일치 |
|
||||
| Chart Only·Fixed 제한 | PASS(정적) — Auto 기본 fit/direct와 Fixed TypeError 경로 대조. 기존 브라우저 결과 재사용 |
|
||||
| Guard 원본 | PASS(보존 확인) — 원본 FAIL 5 / NOT VERIFIED 9 및 status/styleEvidence 유지. Guard 자체 판정은 FAIL |
|
||||
| 보안 Advisory | PASS(문서 정합성) — 과거 High 7/고유 1/운영 0과 출처·시점 구분. 신규 audit 미실행 |
|
||||
| 승인 상태·배포 위험 | PASS(문서 정합성) — 최종 승인 PENDING, READY 아님, main Push 배포 경로·운영 미확인 항목 명시 |
|
||||
| 변경 범위 | PASS — 허용 문서 2개만 변경, staged 0, 제품/정책/과거 결과 변경 없음 |
|
||||
| 공백·diff 검사 | PASS — 최종 `git diff --check` 및 신규 문서 whitespace 확인. 최종 작업 보고에 결과 연결 |
|
||||
|
||||
위 API/기준/보존/범위 정적 확인 스크립트는 13/13 항목 PASS다. 이는 문서 검사 수이며 TypeScript 컴파일·제품 테스트 수가 아니다. 새 TS/TSX 예제의 전체 타입 검사·실제 브라우저 실행은 이번 작업에서 수행하지 않았다.
|
||||
|
||||
**NOT VERIFIED:** 신규 npm audit/현재 전체 의존 그래프, OrgChart Submodule/Subtree 통합, 실제 Merge/통합 Linux·Docker build, Gitea Actions 실제 활성/Runner/서버/복구, 현재 조직의 Guard/audit/접근성 필수 정책, 현재 인증 계정의 Merge/Push 권한, Safari/Firefox·실제 모바일·스크린리더, 2,100명 전체 회귀·전체 baseline 픽셀·모든 회사/폭의 수동 시각 승인. Fixed Chart Only는 미검증 후보가 아니라 미지원이다.
|
||||
@@ -0,0 +1,60 @@
|
||||
# OrgChart P1 독립 디자인과 개별 컴포넌트
|
||||
|
||||
2026-10-12 최종 요청을 적용한다. 기존 공통 Theme 필수 연결 규칙은 OrgChart에 적용하지 않는다. 과거 v0.1 검증 기록은 당시 구현 기록으로 보존한다.
|
||||
|
||||
## 공개 API와 스타일
|
||||
|
||||
패키지 root 공개 API에 `createOrgChartTree`, `createPositionSummary`, `createAffiliationSummary`, `createStatusSummary`를 추가한다. 기존 `createOrgChart`, Source, schema, summary 및 React Wrapper API는 유지한다. 모든 factory가 기존 `mount/update/reload/destroy/subscribe/getState` handle을 반환한다.
|
||||
|
||||
```js
|
||||
import { createPositionSummary, createAffiliationSummary, createOrgChartTree,
|
||||
JsonOrgChartDataSource } from '@hm-design/org-chart';
|
||||
import coreUrl from '@hm-design/org-chart/style.css?url';
|
||||
import engineUrl from '@hm-design/org-chart/engine.css?url';
|
||||
|
||||
const source = new JsonOrgChartDataSource(dataset);
|
||||
const position = createPositionSummary({ container: positionElement,
|
||||
dataSource: source, stylesheetUrl: coreUrl });
|
||||
const affiliation = createAffiliationSummary({ container: affiliationElement,
|
||||
dataSource: source, stylesheetUrl: coreUrl });
|
||||
const tree = createOrgChartTree({ container: treeElement, dataSource: source,
|
||||
stylesheetUrls: { core: coreUrl, engine: engineUrl } });
|
||||
await Promise.all([position.mount(), affiliation.mount(), tree.mount()]);
|
||||
// 상위 페이지가 DOM container를 자유롭게 배치하고 표시·숨김을 관리한다.
|
||||
await position.update(nextDataset);
|
||||
position.destroy(); affiliation.destroy(); tree.destroy();
|
||||
```
|
||||
|
||||
`?url`은 Vite 사용 예시다. 일반 HTML은 배포한 CSS 실제 URL을 전달한다. `/browser` classic bundle도 같은 factory를 `window.HMOrgChart`로 제공한다. ESM import/handle 생성은 DOM 없이 가능하며 mount에만 연결된 HTMLElement가 필요하다. Summary는 engine을 로드하지 않고 engine.css도 필요 없다. 각 모듈은 Header·Routing·전체 페이지에 의존하지 않는다.
|
||||
|
||||
React에서는 기존 전체 `OrgChart` Wrapper를 그대로 사용한다. 개별 모듈은 위 factory를 ref/effect에서 mount하고 cleanup에서 destroy하면 된다. JSX 카드·테이블·데이터 계산을 복제하지 않는다. Source는 공유할 수 있으며 각 handle은 동일한 validate/normalize/calculateSummary 흐름을 사용한다. 상위 페이지가 여러 handle에 nextDataset을 전달한다. 자동 전역 데이터 저장소는 추가하지 않는다.
|
||||
|
||||
## 독립 디자인 입력
|
||||
|
||||
OrgChart는 Shadow DOM 안에서 `engine.css → style.css`를 적용한다. `style.css`는 모든 내부 CSS를 포함하며 공통 HM Theme가 없어도 기본 디자인을 가진다. 공통 Theme·Token·Component는 변경하지 않는다.
|
||||
|
||||
```css
|
||||
.other-page-summary {
|
||||
--hm-orgchart-font-family: Arial, sans-serif;
|
||||
--hm-orgchart-spacing-lg: 20px;
|
||||
--hm-orgchart-radius-container: 6px;
|
||||
--hm-orgchart-bg-surface: #ffffff;
|
||||
}
|
||||
.custom-chart {
|
||||
--hm-orgchart-design-planning-card-background: #f4ffff;
|
||||
--hm-orgchart-design-planning-card-color: #10243b;
|
||||
--hm-orgchart-design-planning-department-color: #ffffff;
|
||||
--hm-orgchart-design-planning-border: #00847f;
|
||||
--hm-orgchart-design-planning-gradient: linear-gradient(#00847f, #005b57);
|
||||
}
|
||||
```
|
||||
|
||||
위 예시는 외부 사용자 override 예시이며 공식 Workdesk 값 확정이 아니다. public 변수를 container에 설정한다. `resolved-*`, `current-*`는 내부 구현 변수다. 기본 변수 전체 목록은 `packages/org-chart/src/styles/design-tokens.css`, 조직별 목록·실측 근거는 `core/style-presets.ts`에서 관리한다. 조직별 ID에 연결하고 하위 셀·팀은 상위 variant를 상속한다. JSON 직원 객체에 컬러를 추가하지 않는다. 카드 배경·직원 텍스트·부서명 텍스트·테두리·Gradient를 각각 바꿀 수 있다.
|
||||
|
||||
## Composition·데이터·디자인 근거
|
||||
|
||||
`core/org-chart-content.ts`에서 독립 Tree와 Summary 모듈을 선택해 배치한다. 기본 placement는 직위=side, 관계사=footer, 파견·휴직=belowTree이며 내부 `ContentComposition.placements`로 바꿀 수 있다. 외부 호출자는 공개 개별 API의 container를 직접 배치한다. 기존 presentation flags로 전체 조합의 표시를 관리하며 계약 필드 추가 없이 처리한다.
|
||||
|
||||
Workdesk `v1.2610.9c5e` 운영 화면에서 확인한 조직 명칭과 6개 조직 헤더 Gradient·흰색 부서명 텍스트를 사용한다. 일반 배경·카드 테두리·Typography·간격·Radius는 독립 구현 기본값(DESIGN_ADAPTATION); 원본 카드 세부값은 NOT VERIFIED다. Figma 접근 가능한 정식 회사 로고 7종은 Core에서 affiliation ID로 매핑하고 카드·현황·Logo에서 재사용한다. 출처·원본 SHA256은 `packages/org-chart/src/assets/affiliations/README.md`에 기록한다. 미매핑/파일 오류는 회사 명칭 텍스트 대체를 사용하고 임의 로고를 생성하지 않는다.
|
||||
|
||||
P1 fixture는 실제 조직명과 관계사명만 사용한다. 직원 41명과 직위·직책·소속·근무 상태는 가상이며 프로필은 null/이니셜 placeholder다. 관계사 3곳은 0명을 유지한다. Raw Count는 가상 JSON 고유 employee ID로 자동 계산하고 실제 HR 인원수나 승인된 집계 정책으로 표시하지 않는다.
|
||||
@@ -0,0 +1,124 @@
|
||||
# OrgChart UI Composition
|
||||
|
||||
## P9.5 Full Composition 적응형 앵커 (2026-10-13)
|
||||
|
||||
Workdesk 조회 전용 Full Composition은 원본1920×914.5를 디자인 좌표계로 유지한다. 상단 제목·기준일·인원 표, 중앙 대표자·허브·조직/직원·연결선·화살표·장식, 하단 파견/휴직·관계사 바를 논리적 영역으로 표시한다. 상단 앵커는 콘텐츠 내 배치이며 sticky/스크롤 추적이 아니다.
|
||||
|
||||
배율은 `min(1, 실제 콘텐츠 가용 폭 / 1920, 실제 가용 높이 / 914.5)`다. 원본보다 키우지 않는다. 높이가 남으면 Stage의 사용 높이를 `가용 높이 / 배율`로 확보하고, 상단·하단의 실제 Rect 사이에 중앙 내용 Rect의 중심을 맞춘다. 장식은 원본 크기와 좌표를 유지하고 중앙과 같은 수직 오프셋으로 움직인다. 높이가 부족하면914.5 기준의 기존 상대 좌표를 유지하여 전체를 비례 축소한다. 하단 그룹과 가로 중앙 정렬은 중앙 오프셋에 포함하지 않는다.
|
||||
|
||||
파견·휴직은 하단 CSS 앵커를70→82디자인px로 바꾸어 기준에서12px 위로 옮겼다. 관계사 바의55.4px 높이·로고·문자와 하단 앵커는 유지하며 실제 그룹/바 간격은 약14.61디자인px다. 협업 화살표5개는 이미 정상 표시 중이어서 원본4개PNG·5개 배치를 보존했다. management 연결선은 이동된 overlay 원점을 기준으로 계산하여 중앙 이동의 이중 적용을 막는다.
|
||||
|
||||
P9.2의 실제 Content Wrapper 기준 가로 중앙 계산과 Expanded/Overview/Department 경로는 유지한다. 기존 P2~P9.4 보고서는 당시 기록으로 보존하고, 현재 측정은 [Full Composition P9.5 보고서](implementation-report-p9-5-full-composition.md)에서 구분한다.
|
||||
|
||||
## P3.2 후속 — 대표자 뱃지 미사용 (2026-10-12)
|
||||
|
||||
대표자 카드는 모든 실행 경로와 독립 견본에서 직책·역할·상태 뱃지 및 빈 attributes/뱃지 행을 생성하지 않는다. 표시 직위 `대표이사`, 이름 `홍 길 동`, 승인 카드 146×74 및 기존 Typography·사진·로고를 유지하며 직위↔이름 간격은 0px이다. 원본 fixture의 대표자 직위는 `경영진`이므로 `대표이사`는 카드 표시에만 적용한다. 직원 JSON·정보 팝업 원문·공개 API와 다른 1Depth/2Depth 카드의 뱃지 정책은 그대로 유지한다. 아래 P3.2 및 이전 보고서·검증은 당시 기록으로 보존한다.
|
||||
|
||||
## P3.2 현재 계약 (2026-10-12)
|
||||
|
||||
[P3.2 보고서](implementation-report-p3.2.md)의 카드 Typography·상태 영역·로고 보정이 아래 P3.1 수치보다 우선한다. 과거 검증 기록은 그대로 보존한다.
|
||||
|
||||
- 대표자 Typography는 유지한다. 대형 이름 24px/600/30px, 직위 18px/400/22px; 소형 이름 18px/600/24px, 직위 15px/400/18px이다. 기존 컬러·letter-spacing·Noto Sans KR 및 `홍 길 동`을 유지한다. 직위·뱃지 공백은 표시 계층에서만 제거한다.
|
||||
- readOnly 파견·휴직은 SVG 없이 일반 소형 카드의 공통 콘텐츠 레이아웃과 Typography를 재사용한다. 카드 108×52, 사진 24×44, 사진↔텍스트 4px, 직위↔이름 0px이다. 기존 회색 표면·문자색·로고 grayscale을 유지하며 별도 상태 행은 없다. P3.1의 실제 114×54/사진 28×28에서 이번 요청값으로 보정한다.
|
||||
- 상태 타이틀은 일반 2Depth 기본 20px/700/19.7px를 공유하며 회색 Gradient를 유지한다. 경영기획/인재성장 18px/600/16.9px 예외는 보존한다. 타이틀↔첫 카드 Gap 8px를 유지한다.
|
||||
- 카드 로고의 실제 contain 표시 영역은 소형 12×12→18×18, 대형·대표자·파견·휴직 14×14→21×21이다. 원본 PNG·승인 SVG 및 하단 관계사 현황 로고는 수정하지 않는다. 직원 카드 selector에만 적용하며 비율·투명 배경을 유지한다.
|
||||
|
||||
## P3.1 현재 계약 (2026-10-12)
|
||||
|
||||
잔여 디자인 보완의 최신 계약은 [P3.1 보고서](implementation-report-p3.1.md)가 우선한다. 아래 P3/P2 및 초기 설계의 검증 기록은 당시 범위 그대로 보존한다.
|
||||
|
||||
- OrgChart 전용 기본 폰트는 Noto Sans KR이다. 기존 `public/fonts/NotoSansKR-variable.ttf`와 호스트 font-face를 재사용하며 HM Theme는 수정하지 않는다. 한국어 Core shell은 `lang="ko"`로 HTML/React/Storybook의 글리프 조형을 맞춘다.
|
||||
- 이름은 `홍 길 동`, 직위는 표시에서만 공백을 제거한다. Full Composition은 `jobTitle`이 `경영진`인 뱃지만 생성하지 않으며 기타 직책 문자열·원본 데이터·팝업·통계는 유지한다. 원본 `position="경영진"` 텍스트는 유지한다.
|
||||
- 1Depth는 20px/700/36px·헤더 높이 36px이다. 2Depth 경영기획/인재성장은 18px/600/16.9px, 나머지는 기존 20px/700/19.7px다. 114×30.4 표면·Gradient·Radius·Shadow 및 모든 타이틀↔첫 카드 Gap 8px를 보존한다.
|
||||
- 허브는 원본 21px/750/24px·아이콘 20px·Gap 8px·좌우 Padding 22/24px·높이 42px를 유지한다. 최대 폭 상속 제약을 해제하고 X 935 중심축을 유지한다.
|
||||
- 사진 없음/로드 실패는 공통 `person-placeholder.svg`를 사용한다. 실제 사진이 있으면 우선한다. 승인 사진 슬롯 36×62/32×56/24×44, P3 상태 카드 슬롯 28×28 및 비원형 처리를 유지한다.
|
||||
- 우측 표는 전체 330px 안에서 조직명/경영진 열을 112/46px로 재배분해 한 줄로 표시한다. 나머지 열은 잔여 폭을 나눈다. 숨김/말줄임/폰트 축소 없이 원본 14px, 기존 계산 41명·위치·셀 정렬·행 높이를 보존한다.
|
||||
- 대표자 146×74, 1Depth 146×68, 하위 108×52 및 P3 상태 카드 114×54를 유지한다. 확장형 Auto Layout/layoutConfig·메뉴·Navigation·공개 API 추가 없음.
|
||||
|
||||
## P3 현재 계약 (2026-10-12)
|
||||
|
||||
아래 초기 설계와 P2 기록은 보존한다. 충돌하는 표시 계약은 [P3 보고서](implementation-report-p3.md)가 우선한다. 범위는 기존 독립 HTML·React 공통 Core와 해당 Storybook 페이지이며 Navigation·Header를 추가하지 않는다. 기존 semantic table/native dialog는 공통 Vanilla Core adapter를 유지하기 위한 선택이다. FilterBar·DataTable의 검색/정렬/페이지 기능은 요구되지 않아 추가하지 않는다.
|
||||
|
||||
- Full Composition 기본 배경 `#F5F8FB`, 원본 장식·허브 곡선·협업 화살표 유지. 조회 전용 엔진의 하위 연결선 생성 경로는 모두 숨긴다.
|
||||
- 대표자 146×74, 1Depth 146×68, 하위 108×52 유지. 모든 조직 타이틀↔첫 카드 간격 8px, 직위/뱃지↔이름 행 0px. 이름만 `홍 길 동`, 직위·뱃지는 원문.
|
||||
- 하위 헤더는 Workdesk 소스의 114×30.4 표면·상위 조직 Gradient·Radius 7.1·Shadow를 적용한다. HR은 18px/600/16.9, 다른 헤더는 20px/700/19.7이다. 허브는 원본 SVG 아이콘 20px·Gap 8px·텍스트 21px/750/24를 사용한다.
|
||||
- 사진 없음은 원형 기본 아바타 대신 빈 사각형 예약 영역이다. 원본에 기본 이미지가 없으며 승인 카드의 기존 폭을 유지하는 디자인 조정이다. 실제 직원 사진은 포함하지 않는다.
|
||||
- 우측은 직위별 표(주요 조직 하위 포함, 루트 직속, 직위별 열, 소계, 계). 가상 직원 41명·원본 조직 관계·독립 PositionSummary API를 보존한다. 상태 그룹의 카드 하단 상태 문구만 제거한다. 관계사 바는 제목 없는 한 줄이다.
|
||||
- 축소는 단일 비례 배율 `min(1, 가용 폭 / Stage 폭, 실제 컨테이너 가용 높이 / Stage 높이)`다. 연결 후 실제 Shadow/호스트 조상과 viewport를 관찰한다. 임의 Header 높이나 viewport 절반 offset 제한은 없다.
|
||||
- `organizationLinks`가 지정된 1Depth 제목만 링크다. `onEmployeeClick`이 없으면 Stage 바깥 native modal에서 기존 직원 정보를 조회한다. 닫기·ESC·키보드 Focus를 지원한다.
|
||||
- 사용자 메뉴는 `Logo · Personnel Summary`, `Department Colors`, `Full Composition`이다. 이전 단독 Story ID는 내부 태그로 보존한다.
|
||||
|
||||
실제 브라우저 측정과 미검증은 P3 보고서에 기록한다. 과거 PASS/FAIL을 이번 상태로 덮어쓰지 않는다.
|
||||
|
||||
상태: 콘텐츠 설계, 실행·시각 검증 미수행. [README](README.md)의 확정 범위와 최신 Core 독립성을 따른다. Workdesk 캡처 파일은 이번 첨부에 없어 아래 구성은 사용자 텍스트의 8영역을 기준으로 한다. 실제 이미지와의 배치·색상·카드·곡선 비교는 **NOT VERIFIED**다.
|
||||
|
||||
위 상태와 초기 설계 절은 과거 기록이다. 현재 조회 전용 구현은 3a절과 [P2 구현·검증 보고서](implementation-report-p2.md)를 따른다. P2에서는 읽기 전용 Workdesk 로컬 소스와 자산을 확인하고 실제 HTML·React·Storybook을 검증했다. 운영 Workdesk와의 전체 픽셀 일치 여부는 별도 미검증이다.
|
||||
|
||||
## 1. 콘텐츠 구성과 데이터 연결
|
||||
|
||||
1. **제목·설명**: PresentationConfig의 title/description과 Metadata 기준일. Header·PageHeader·브랜드 Navigation을 생성하지 않는다.
|
||||
2. **최상위 리더 카드**: 승인된 root 조직의 leaderId를 Employee에 연결한다. 공석·여러 root·겸직 leader 표현은 TBD다. 같은 직원을 중복 집계하지 않는다.
|
||||
3. **중앙 허브**: hubText의 콘텐츠. 조직 entity인지 설명 허브인지, 리더와의 연결 방식은 TBD다. 조직장 ID나 직원 ID를 허브에 임의 부여하지 않는다.
|
||||
4. **조직별 그룹**: Organization parent/type/order로 부문·실·Div를 구성한다. 조직 사이 기본 계층 연결선은 engine가 담당한다.
|
||||
5. **셀·팀·구성원**: 같은 조직 계약에서 cell/team을 표현하고 직원은 organizationId로 조회한다. 이름·직위·직책·이미지/fallback·상태를 조합한다. 직원 전체를 조직 트리의 nested 원본으로 저장하지 않는다.
|
||||
6. **우측 인원 현황**: 조직 직접/하위 포함·직위별 등 승인된 집계. 우측은 넓은 컨테이너에서의 배치 후보이며 작은 화면에서는 문서 흐름으로 이동하는 안을 검토한다.
|
||||
7. **파견·휴직 영역**: 같은 직원 원본의 상태별 그룹. 본 조직 카드와 동시 노출/이동 표시 정책은 TBD이며 표시와 집계를 분리한다.
|
||||
8. **관계사별 인원·총원**: Affiliation과 승인 정책을 사용하는 집계. 조직별 하위 포함 수를 단순 합산하지 않는다. 집계 정책 미승인 시 미확정 상태를 설명한다.
|
||||
|
||||
추가 조직 관계는 OrganizationRelation에서 별도 표시한다. 관계 의미·방향·범례·곡선·anchor는 승인 전 확정하지 않는다. 관계를 parent hierarchy로 오인하지 않도록 텍스트 설명을 함께 제공하는 안을 검토한다. 고정 좌표로 인원 수나 조직 배치를 하드코딩하지 않는다.
|
||||
|
||||
## 2. HM 재사용과 Core DOM 경계
|
||||
|
||||
최신 사용자 요구에 따라 Core는 React Component를 import하지 않는다. OrgChart 전용 표준 HTML/CSS가 **공통 Component를 대체하는 신규 HM primitive**가 되지 않도록 범위를 콘텐츠에 제한한다. 기존 Component·Pattern·Theme·대표 Story는 보존한다.
|
||||
|
||||
재사용의 두 경로를 구별한다.
|
||||
|
||||
- Core: HM CSS Custom Properties와 기존 Typography·Spacing·Radius 기준을 참조한다. 노드에는 전용 class를 쓰고 HM `data-slot` 또는 일반 Card/Avatar selector를 흉내내지 않는다. 이 예외와 기존 React 지침의 충돌은 README에 기록했다.
|
||||
- React 소비 앱(Core 콘텐츠 밖): 적합한 주변 콘텐츠에서 공개 `@hm-design/ui` API를 실제 렌더링하는 방향을 검토한다. Wrapper는 Core summary/loading/error/node를 다른 HM UI로 대체하지 않는다. Core 노드마다 React root를 만드는 설계도 채택하지 않는다. 공통 Core의 콘텐츠·디자인·인터랙션을 유지한다.
|
||||
|
||||
확인한 공개 자료:
|
||||
|
||||
- [Card 문서](../components/card.md), [Card All](../../src/components/card.stories.tsx), [구현](../../packages/ui/src/components/ui/card.tsx): surface/content와 Header/Title/Description/Content 조합. Core 외부의 React 콘텐츠 묶음에서 재사용 후보다.
|
||||
- [Avatar 문서](../components/avatar.md), [Avatar All](../../src/components/avatar.stories.tsx), [구현](../../packages/ui/src/components/ui/avatar.tsx): 이미지/fallback, sm/default/lg. Core는 `<img>`/안전한 텍스트 fallback을 사용하고 React 호스트에서는 공개 Avatar 후보를 검토한다.
|
||||
- [Table 문서](../components/table.md), [Table All](../../src/components/table.stories.tsx): 정적 표이며 정렬/선택/페이지는 DataTable 책임이다. 요청된 Summary는 동일 Core의 semantic table로 설계한다. React 소비 앱의 별도 주변 표에서 공개 Table을 검토할 수 있지만 Wrapper가 Summary를 중복 구현하지 않는다. 기존 Table 최소 폭 520px은 Core의 확정 최소 폭이 아니며 사용한 주변 표는 390px 내부 스크롤을 확인한다.
|
||||
- [공개 인덱스](../guidelines/component-pattern-reference.md)의 Badge/Alert/Skeleton/ScrollArea/Button/Label은 해당 기능 확정 시 문서·대표 Story·Props를 더 확인한다. 현재 import나 재사용 PASS로 보고하지 않는다.
|
||||
|
||||
Pattern 선택 근거: FilterBar는 검색·조회 조건용이다. 현재 검색/필터 요구가 없어 추가하지 않는다. DataTable의 정렬/선택/페이지도 요구가 없어 사용하지 않는 방향이다. DashboardLayout/GNB/LNB/PageHeader는 페이지 Shell/Navigation 책임이므로 제외한다. LoadingScreen은 전체 화면용이라 콘텐츠 loading에 바로 사용하지 않는다. 상태 콘텐츠는 Core가 관리하고 React 주변 조합은 기존 Alert/Skeleton을 검토한다. 이는 개발 시 선택 근거이며 구현 재사용 검증은 **NOT VERIFIED**다.
|
||||
|
||||
## 3. 컨테이너·반응형·Overflow
|
||||
|
||||
Core는 부모의 실제 콘텐츠 크기를 사용한다. viewport에서 Header 높이를 빼거나 GNB/LNB/Dashboard 모드를 읽지 않는다. Dashboard 소비자가 960px 내부 콘텐츠를 주는 경우에도 이를 Core 전역 최소 폭으로 일반화하지 않는다.
|
||||
|
||||
390/768/1280/1440px 각각에서 콘텐츠 컨테이너를 직접 제한하여 검사한다. 숫자는 검증 기준이며 확정된 breakpoint·카드 폭이 아니다.
|
||||
|
||||
- 390px: 가로 조직도 내부 스크롤·읽을 수 있는 텍스트 배율, 요약/상태 영역 흐름 배치, touch와 focus 접근을 검토한다. 전체 화면을 축소해 텍스트를 읽을 수 없게 하는 기본값은 확정하지 않는다.
|
||||
- 768px: 조직 분기 wrap과 가로 스크롤, summary의 옆/아래 배치, 긴 조직명·직위·ID를 검사한다.
|
||||
- 1280/1440px: 전체 계층·리더·허브·우측 summary·추가 관계·직원 카드의 겹침과 여백을 검사한다.
|
||||
- 조직 수·깊이·직원 수가 증가하면 내부 scroll container가 확장되고 페이지 전체 가로 Overflow는 발생하지 않아야 한다. CSS transform zoom은 layout 크기와 다를 수 있으므로 보이는 영역과 scrollWidth/clientWidth를 함께 측정한다.
|
||||
- `ResizeObserver`·이미지 로드·폰트 로드·노드 확장·줌·스크롤마다 Overlay 좌표가 동일 좌표계에서 갱신돼야 한다. transform 중복 적용과 stale anchor를 검사한다.
|
||||
- 모바일 방식은 내부 가로 scroll, branch 축소, 세로 대체 표현 중 PoC로 결정한다. 대체 표현은 승인 전 추가 개발 요구로 만들지 않는다. 최소 콘텐츠 폭·zoom 한계·초기 배율·wheel 우선순위·높이 제약·스크롤 책임은 TBD다.
|
||||
|
||||
문서와 내부 컨테이너의 scrollWidth/clientWidth 및 scrollHeight/clientHeight를 기록한다. absolute 알림/overlay의 위치 기준은 해당 인스턴스의 relative 컨테이너여야 한다. 긴 label 줄바꿈과 ID 한 줄 식별 가능성, 접힌 영역에 숨은 focus, 두 인스턴스의 독립 scroll/zoom을 검사한다.
|
||||
|
||||
### 3a. 조회 전용 Full Composition (`displayMode: 'readOnly'`, 2026-10-12)
|
||||
|
||||
- Storybook `OrgChart / Full Composition`, HTML·React `p1.html?mode=readOnly`가 사용한다. 기본값 `interactive`와 내부 검증 Story·독립 컴포넌트는 변경하지 않는다.
|
||||
- 생성하지 않는 요소: 하위 조직 접기/펼치기 button, 조직 종류·ID·원본 직원 수·조직장 안내 문구, 제목 설명·추가 관계 안내, 상태 그룹 정책 문구. 조직 그룹 외곽 카드(배경·Border·Radius·Padding)는 CSS로 제거하고 조직/셀·팀 헤더·카드·연결선은 유지한다.
|
||||
- 배치: Workdesk 실측 1920 × 914.5 캔버스, 대표자·허브 축 X 935, 조직 헤더 X 32/268/505/858/1336/1689(`core/workdesk-layout.ts`, organization ID 기준 presentation preset). 통합시스템 Div·디자인기획 하위는 가로, 나머지는 세로. preset이 없는 조직이 있으면 중앙 흐름 배치로 대체한다.
|
||||
- 요약: 우측 상단 `조직별 인원 현황`(루트 직속 + 주요 조직 하위 포함 인원 + 계), 좌하단 파견·휴직(제목 108px pill, 카드 유지), 하단 관계사 바(제목 없음, 로고·회사명·인원·총계). 모두 같은 Summary/selector에서 계산한다.
|
||||
- 축소: `scale = min(1, 가용 폭 / 1920, (viewport 높이 − 문서 offset) / 914.5)`(offset은 viewport 절반까지만 반영). transform scale 1회, 내부 가로 스크롤 없음.
|
||||
- P2 카드: 실제 `parentId` 조상 관계로 Depth를 계산한다. 대표자 146×74, 1Depth 조직장만 146×68, 2Depth 이하 조직장은 해당 부서의 108×52 소형 SVG를 사용한다. semantic leader와 직책 뱃지는 유지하며 interactive·독립 카드 선택은 보존한다. 전달 자산이 없는 조직의 기존 fallback 정책은 유지한다.
|
||||
- 최신 확정 표시: 조회 전용 직원 이름은 대표자·파견·휴직을 포함해 모두 `홍 길 동`. 직위·직책·이름의 연속 한글 사이에는 표시 계층에서 공백을 넣고 접근성 이름은 원문을 사용한다. 원본 JSON 이름·ID·조직·인원 계산은 변경하지 않는다. 기존 font-size/weight/line-height/color와 내부 0px 행 간격을 유지한다.
|
||||
- P2 간격: 1Depth 헤더 높이 36px, Y 약 254.19px, 대형 카드 사이 실제 8px. 2Depth 헤더와 첫 카드 사이 실제 10px. 하단 상태/관계사 슬롯은 측정 캔버스 내에서 고정 배치하여 전체 높이 914.5px와 겹침 없는 배치를 유지한다.
|
||||
- P2 장식: `core/workdesk-artwork.ts`가 원본 compact 허브 곡선 3개·중앙선·사업관리 추가 곡선, PNG 협업 화살표 5개와 배경/허브 이미지 5개를 같은 Stage에 렌더한다. 원본 9종 자산은 byte 그대로 복사하고 ESM/IIFE에 포함한다. 기존 상단 pseudo 연결선은 이 preset에서 제거한다. 하위 팀의 실사용 원본 SVG는 없어 기존 engine 관계 경로를 유지한다.
|
||||
- P2 검증: HTML HTTP·classic file://·React production·Storybook에서 1920/1440/1280/768/390px 484건 PASS. Guard 원본의 NOT VERIFIED와 런타임 PASS는 별도 증거로 보존한다. 상세 측정·남은 차이·기존 FAIL은 P2 보고서에 기록한다.
|
||||
|
||||
## 4. 접근성·상태·개인정보
|
||||
|
||||
Core DOM은 제목 계층·조직/직원 그룹·표의 caption/header를 semantic하게 구성한다. engine 기본 DOM이 완전한 ARIA tree라고 가정하지 않는다. tree 역할 채택 시 키보드 계약 전체를 검증하고, 그렇지 않으면 계층 설명을 제공할 구조를 검토한다.
|
||||
|
||||
확장·축소/zoom 조작이 구현될 경우 키보드로 접근 가능한 표준 button과 접근성 이름·focus indicator·disabled 상태를 제공한다. readOnly 데이터 조직도에 수정·drag 메뉴를 추가하지 않는다. 상호작용 후 focus 복귀, 제거된 노드 focus 이동, 오류/로딩 안내의 screen reader 노출과 과도한 알림을 검사한다. 입력 컨트롤이 새로 승인되면 React 호스트는 공개 Label, Core는 framework 독립성 때문에 native label/control 연결을 명시한다.
|
||||
|
||||
이미지 접근 실패는 가상 fallback을 보여주며 불필요한 이름 중복 읽기를 피한다. 사진의 origin·권한·실패 정책은 API 연동 시 결정한다. 직원 ID·이름·사진 원본을 로그/오류 메시지에 무분별하게 남기지 않는다. reference의 실제 인물을 fixture로 재사용하지 않는다.
|
||||
|
||||
Loading/Empty/Error/Ready, 직원 없는 조직, 공석 leader, 상태 그룹 비어 있음, 정책 없는 집계, 갱신 중 상태를 별도 검증한다. 갱신 중 기존 화면 유지와 부분 오류 허용 정책은 TBD다. 사용자 검토 전에 이러한 설계 후보를 기능 PASS로 표시하지 않는다.
|
||||
@@ -0,0 +1,59 @@
|
||||
# P9.5 Core X 버튼 — 실제 소비 화면 비교 검증
|
||||
|
||||
RESULT: PASS (요청 화면의 기본/선택 팝업 디자인과 기존 동작)
|
||||
|
||||
## 실제 팝업 사용 경로
|
||||
|
||||
- Storybook Full Composition: 화면은 React OrgChart Wrapper지만 팝업은 Core native dialog. Fixed는 Core 팝업을 사용한다.
|
||||
- Storybook Expanded OrgChart 기본값: React HMDesign Dialog. 실제 공개 HM Dialog를 사용하는 HMEmployeeDialog Portal이다.
|
||||
- Storybook Expanded OrgChart에서 직원 정보 팝업을 Core 기본 팝업으로 선택: Core native dialog.
|
||||
- 실제 Vanilla index.html, p1.html?mode=readOnly (Full Composition), expanded.html: 모두 Core native dialog. React 없이 공식 Core API를 사용한다.
|
||||
|
||||
열린 DOM과 위치로 구분했다. Core는 ShadowRoot 안의 dialog.hm-orgchart-employee-dialog[open], React HM은 Document Portal의 data-component=HMOrgChartEmployeeDialog / data-state=open이다. HM 모드에서도 닫힌 Core dialog DOM은 존재할 수 있으므로 단순 DOM 존재를 팝업 사용 증거로 보지 않았다.
|
||||
|
||||
## 실행 범위와 결과
|
||||
|
||||
- 위 6개 경로 × 1440×900 / 390×900: 118 PASS, 0 FAIL.
|
||||
- 기준: 실제 Components/Overlay/Dialog 대표 All Story에서 여는 HM Dialog의 ghost/icon-sm X 버튼. Expanded의 실제 HM 직원 Dialog도 같은 기준값을 확인했다.
|
||||
- 모든 버튼 28×28px, SVG 16×16px, 두 Lucide X 경로 동일, Stroke 2, round cap/join, Padding 0, Radius 3.2px, CSS top/right 8px. 팝업 Border 1px를 포함한 실제 외곽 inset은 각각 9px.
|
||||
- 기본색 rgb(10,34,29), Hover rgb(244,241,234), Focus Border rgb(38,102,81) 및 같은 색 50%의 3px Ring이 일치했다. HM의 투명 shadow layers와 Core의 단일 ring 표현은 동일한 가시 링이다.
|
||||
- X 클릭/ESC 닫기, 카드 Focus 복귀, 키보드 Enter 재열기, 선택된 직원/본문 데이터 유지 PASS. 신규 Console/Page 오류 0.
|
||||
- 외부 클릭 정책은 각각 유지: Core는 열린 상태 유지, React HMDesign Dialog는 닫고 직원 카드로 Focus 복귀.
|
||||
- 팝업 본문 구성은 각 기존 디자인을 유지한다. HM에는 Description/Avatar/Footer 닫기 버튼이 있고 Core에는 기존 간단한 직원 정보 목록이 있다. 이번 검증에서 이를 동일 본문으로 변경하지 않았다.
|
||||
- 소스와 실제 캡처를 비교 검토했다. 전체 레이아웃/Drag & Drop 회귀는 반복하지 않았다.
|
||||
|
||||
## Vanilla 예제 산출물 갱신
|
||||
|
||||
시작 시 packages/org-chart/dist는 2026-10-13 수정본이었으나 examples/org-chart/dist/assets는 2026-10-12 복사본이었다. 실제 예제는 assets/index.js 및 style.css를 읽으므로 이전 복사본에서는 새 X가 반영되지 않는 상태였다.
|
||||
|
||||
기존 node examples/org-chart/prepare.mjs 경로로 최신 Core 배포 파일을 실제 예제 dist에 복사했다. 제품 소스 수정 없이 생성물만 갱신했다. 갱신 후 index.js / style.css 각각의 SHA256이 Core 배포 파일과 정확히 동일함을 확인했다. 이전/이후 manifest는 artifacts-before.json / artifacts-after.json이다.
|
||||
|
||||
기존 Typecheck/Core Build/Storybook Build PASS는 제품 소스가 그대로이므로 재사용했다. Guard도 UI 변경이 없어 반복하지 않았다. 이전 Guard 원본의 SCOPE-001 및 CSS Runtime/Final NOT VERIFIED는 유지하며 본 브라우저 측정은 별도 증거다.
|
||||
|
||||
## 증거 및 검토 URL
|
||||
|
||||
결과: test-results/org-chart-p9-5-popup-parity/result.json
|
||||
스크립트: examples/org-chart/verify-p9-5-popup-parity.mjs
|
||||
보존: test-results/org-chart-p9-5-popup-parity/preservation.json
|
||||
|
||||
- Full Composition: http://127.0.0.1:6064/?path=/story/orgchart--full-composition
|
||||
- Expanded OrgChart: http://127.0.0.1:6064/?path=/story/orgchart--expanded-org-chart
|
||||
- Vanilla 기본: http://127.0.0.1:4178/index.html
|
||||
- Vanilla Full Composition: http://127.0.0.1:4178/p1.html?mode=readOnly
|
||||
- Vanilla Expanded: http://127.0.0.1:4178/expanded.html
|
||||
|
||||
팝업 및 전체 문맥 캡처는 test-results/org-chart-p9-5-popup-parity/의 각 경로명-폭-popup.png / context.png에 보관한다. 주요 비교 캡처는 storybook-expanded-hm-1440-popup.png, storybook-expanded-core-1440-popup.png, vanilla-full-1440-popup.png이다.
|
||||
|
||||
## 보존 / 미검증 / 시간
|
||||
|
||||
시작 시점 기존 authored 파일 SHA256 변화 0, 기존 P9.5 미커밋 변경 보존. 제품 소스 추가 수정 없음. 검증 스크립트와 본 보고서만 추가했고 예제 dist 생성물을 갱신했다.
|
||||
|
||||
feature/orgchart-package 및 HEAD 7614507583927a25a270963a74ff044be0557b9f 유지. Staged 파일 0. Stage/Commit/Push/Merge/PR/Reset/Rebase 미진행.
|
||||
|
||||
NOT VERIFIED: 다른 브라우저/OS/Theme, Classic 패키징, 별도 React 독립 예제, 팝업 사용 안 함/소비자 Custom callback 경로, 최종 디자인 승인. 요청 화면의 Core/HM 기본/선택 경로 검증에는 해당 누락이 없다.
|
||||
|
||||
Started At: 2026-10-13 14:05:05 KST
|
||||
Completed At: 2026-10-13 14:12:21 KST
|
||||
Actual Duration: 7분 16초
|
||||
Estimated Duration: 12분
|
||||
Variance: -284초
|
||||
@@ -0,0 +1,117 @@
|
||||
# HMDesign OrgChart P10.10 — 현재 브라우저 시각 검증
|
||||
|
||||
**RESULT: PASS — 요청 범위의 브라우저 렌더링·대표 동작·390px 원인 판정 완료. 사용자 최종 디자인 승인: PENDING.** 제품/Storybook 소스 수정이 필요한 오류는 재현되지 않았다. 이 결과는 main 병합·배포 승인이나 기존 Guard/보안/접근성 승인 완료를 뜻하지 않는다.
|
||||
|
||||
## 실행 환경과 Git
|
||||
|
||||
| 항목 | 현재 확인 |
|
||||
|---|---|
|
||||
| 프로젝트 | `D:\디자인시스템\HMDesign-OrgChart-Recovered` |
|
||||
| 브랜치 | `feature/orgchart-package` |
|
||||
| 시작·최종 Local HEAD | `e525d557c400b989be3c5ec0f74319815da3e4ff` |
|
||||
| 시작·최종 원격 Feature | 위 SHA와 동일, `git ls-remote` 읽기 전용 조회 |
|
||||
| origin | `https://gitea.hmac.kr/b25027/HMDesign.git` |
|
||||
| 작성자 설정 | `b25027` / `shkim4@samaneng.com` |
|
||||
| 시작 Working Tree | 깨끗함 |
|
||||
| 완료 변경 | 이 보고서 신규 1개, unstaged. 제품 코드 변경 0개 |
|
||||
| 브라우저 | 실제 Google Chrome / Chromium `155.0.8059.40`, Playwright, device scale 1 |
|
||||
|
||||
요청한 복구 경로·Feature 브랜치·Commit/Push 보류 지시를 적용했다. 보존용 `HMDesign-OrgChart`는 수정하지 않았다. Stage·Commit·Push·Checkout·Merge·PR·Docker·Publish·Reset·Rebase·Force Push 미진행. 공식 원격 HEAD를 변경하지 않았다.
|
||||
|
||||
복구 저장소에는 `node_modules`와 workspace `dist`가 없었다. 기존 lockfile을 그대로 사용하는 `npm.cmd ci --no-audit --no-fund`로 552개 패키지를 설치하고, 실행에 필요한 `npm.cmd run build:packages`를 한 번 완료했다. package.json/lockfile/의존성 버전을 변경하지 않았다. 전체 앱/Storybook production build·전체 Typecheck·Core 86개·Chart Only 51개·Guard 94개 회귀·npm audit는 직접 영향이 없어 반복하지 않았다.
|
||||
|
||||
시작 시 6045/6046/6024 Listener가 없었다. 기존 서버를 종료하지 않고 이 작업의 서버만 실행했다. 서버는 검토용으로 유지한다.
|
||||
|
||||
- Storybook: [공개 메뉴](http://127.0.0.1:6046/), [Full Composition](http://127.0.0.1:6046/?path=/story/orgchart--full-composition).
|
||||
- Chart Only [React](http://127.0.0.1:6045/react.html?data=100) / [Vanilla](http://127.0.0.1:6045/vanilla.html?data=100).
|
||||
- Storybook 명령: `npm.cmd run storybook -- --port 6046 --host 127.0.0.1 --exact-port`.
|
||||
- Chart Only 명령: `node node_modules/vite/bin/vite.js --config examples/org-chart-chart-only/vite.config.ts --port 6045 --strictPort --host 127.0.0.1`.
|
||||
|
||||
## 공개 화면과 대표 동작
|
||||
|
||||
| 화면 | 실제 확인 결과 |
|
||||
|---|---|
|
||||
| Overview | PASS. 회사 로고 7개, 중복 로고 제목 제거, 조직·직위별 표, 관계사별 현황·총계 41명, 공통 Container·카드 간격/정렬 표시. 이미지 로딩 정상 |
|
||||
| Department Colors | PASS. 대표자, 기존 6개 조직 Gradient, 그룹장/직원, 파견/휴직 미리보기, `홍 길 동`, 기존 카드/글꼴, 공통 미리보기 배경/Border 표시. Auto 회사색 7종과 별도 유지 |
|
||||
| Full Composition | PASS. 상단 제목/기준일·인원 표, 중앙 대표자/허브·6조직/직원·연결선, 하단 파견/휴직·관계사 현황 표시. 협업 화살표 5개와 대표자 화살촉 1개 유지. Core 팝업·ESC 종료·카드 focus 복귀를 390/1440에서 확인 |
|
||||
| Expanded OrgChart | PASS. 회사 복수 선택, 직원 수 20/100/300 옵션, 7회사×20명 실제 초기 표시. 한맥기술 하나를 선택한 100/300명 실제 카드 수와 현황 일치. TB/LR/Hybrid 전환 후 140명 보존, 연결선/그룹 제목, Zoom/Pan/Fit, HM Dialog·ESC/focus 복귀, 접기·펼치기 확인 |
|
||||
| Chart Only React | PASS. 100명 대표 화면, 흰색 캔버스·외부 제목/설명/툴바 없음. 초기 Fit/가로·세로 중앙, 직접 휠 Zoom·Pan, Core 팝업/focus, 그룹 접기·펼치기, 동일 Props 리렌더 후 사용자 뷰 유지 |
|
||||
| Chart Only Vanilla | PASS. 같은 100명 대표 화면과 같은 초기 Fit/중앙, 직접 휠 Zoom·Pan·Core 팝업/focus·접기·펼치기, 조작 이후 자동 Fit 없음 |
|
||||
|
||||
Expanded 기본 Drag & Drop은 OFF다. 기존 plain wheel은 확대하지 않고 Ctrl+wheel은 확대한다. Fit 버튼 후 7회사 전체를 확인했다. 초기 Expanded는 기존 100% 탐색 모드라 일부 카드가 canvas 밖에 있으며 이는 Full Composition의 전체 축소 정책과 다르다. 초기 이미지 미로딩 106개는 lazy loading 상태였고, Fit 후 140/140 이미지 로딩을 확인했다(`expanded-assets.json`).
|
||||
|
||||
Chart Only React/Vanilla 초기 배율은 둘 다 `0.513493800145879`, 가로 중앙 오차 0px, 세로 오차 약 -0.000019px다. 직접 휠 이후 `0.6271827437963237`, Pan 이후 Δx=60/Δy=-40px이며 팝업·접기·펼치기 후 배율/위치를 유지했다(100→96→100명). Fixed Chart Only는 기존 미지원 계약과 TypeError 코드 경계를 보존했으며 이번 작업에서 신규 개발하지 않았다.
|
||||
|
||||
회사색·카드 SVG/PNG·Typography·데이터·배경·레이아웃 소스는 변경하지 않았다. Cell Border Only 실험은 정식 적용 대상에서 제외하며 이번 검증에도 적용하지 않았다. 현재 캡처에서 요구된 시각 구성과 자산은 확인했지만, 과거 승인 캡처와의 자동 픽셀 일치 검증이나 사용자 최종 디자인 승인은 **NOT VERIFIED / PENDING**이다.
|
||||
|
||||
## Full Composition 390px 판정
|
||||
|
||||
**실제 내부 가로·세로 스크롤 오류: 재현 안 됨. 제품 수정: 없음.**
|
||||
|
||||
| 실제 Viewport | 문서 scroll/client W·H | 콘텐츠 Wrapper Rect (x,y,w,h) | Stage 실제 Rect (x,y,w,h) | 배율 |
|
||||
|---|---|---|---|---|
|
||||
| 390×844 | 390/390 · 844/844 | 16,306,358,522 | 16,306,358,521.999 | 0.186458… (`data-scale=0.1865`) |
|
||||
| 1440×900 | 1440/1440 · 900/900 | 120,290,1200,570 | 121.640,290,1196.720,570 | 0.623291… (`0.6233`) |
|
||||
| 1920×1080 | 1920/1920 · 1080/1080 | 360,290,1200,750 | 360,290,1200,750 | 0.625 |
|
||||
|
||||
Storybook iframe의 `#storybook-root`, Story Wrapper, Core host/composition/stage/viewport의 BoundingRect, scrollWidth/clientWidth, scrollHeight/clientHeight, overflow는 `measurements.json`에 저장했다. 검증 Viewport는 iframe 콘텐츠의 실제 크기이며 Storybook manager 전체 폭과 구분한다.
|
||||
|
||||
390px 내부 viewport는 `scrollWidth/clientWidth=1920/1920`, `scrollHeight/clientHeight=599/599`, overflow X/Y=`visible`이다. 그 viewport에 실제 `scrollLeft/scrollTop` 최대 이동을 시도해도 0/0이다. composition Wrapper는 `1920/358`, `2800/522`, 기존 overflow=`hidden`이다. CSS transform은 레이아웃 박스를 축소하지 않으므로 원래 Stage 크기가 scroll 지표에 남는다. 이 수치를 표시 크기 또는 실제 사용자 스크롤로 오인하면 안 된다. 기존 overflow를 새로 추가하거나 콘텐츠 잘림을 숨기는 수정은 하지 않았다.
|
||||
|
||||
390/1440에서 측정한 카드·조직 제목·협업 화살표·대표자 화살촉·SVG 연결선의 Wrapper 이탈/잘림은 0개다. Stage 가로 중앙 오차는 0px다. 390px에서는 Stage bottom 약 828px < Viewport 844px이며, 상단→중앙/중앙→하단 간격은 각각 약 152.958px다. 1440에서는 상단/중앙/하단 사각형의 실제 교차가 0개다. 파견·휴직과 관계사 footer도 구분되어 있다. 1920 기준 화면에서도 전체 구성이 콘텐츠 영역 안에 표시된다. 수정/오류가 없어 1280×1024, 1920×1440 추가 검사 조건은 발생하지 않았다.
|
||||
|
||||
기존 `examples/org-chart-react/verify-boundaries.mjs`는 390×900의 다른 production fixture에서 `documentScrollWidth <= 390 && internalScrollLeft > 0`을 요구한다. 최신 Fixed Full Composition은 P9.5 비례 축소 정책상 **내부 가로 스크롤 없음**을 의도한다. 이번 실제 Full Story 390×844에서 동일 이동/판정 부분을 재현하면 `internalScrollLeft=0`, 구형 predicate=false다. 따라서 과거 실패는 현재 Full Composition에 적용할 수 없는 구형 내부 스크롤 기대값 때문이며, 사용자 화면의 스크롤 결함 증거가 아니다. 원본 스크립트·과거 실패 기록을 수정하지 않았고, 원본 전체 production fixture 검사를 이번 결과로 PASS 처리하지 않았다.
|
||||
|
||||
390px는 전체 구성 보존과 중앙 배치 때문에 텍스트가 매우 작고 상하 여백이 크다. 기술적 잘림/겹침 오류는 없지만 이 표현의 가독성·여백은 사용자 최종 디자인 검토 사항이다.
|
||||
|
||||
## 실행한 검증과 증거
|
||||
|
||||
로컬 증거: `test-results/visual-review-p10-10/` (Git ignored, 새 Clone에는 포함되지 않음).
|
||||
|
||||
| 명령/검사 | 결과 |
|
||||
|---|---|
|
||||
| `npm.cmd ci --no-audit --no-fund` | PASS, lockfile 기반 설치 |
|
||||
| `npm.cmd run build:packages` | PASS, 실행에 필요한 4 workspace 산출물 생성 |
|
||||
| `node test-results/visual-review-p10-10/capture.mjs` | 8개 현재 화면/해상도 렌더·측정·캡처, 새 콘솔/runtime 오류 0 |
|
||||
| `node test-results/visual-review-p10-10/interactions.mjs` | 최초 12/16 PASS, 최초 FAIL 4개를 `interactions.json`에 보존 |
|
||||
| `node test-results/visual-review-p10-10/recheck.mjs` | 실패 4개 한정 재검사 4/4 PASS + 선택 회사 100/300명/오류 검사 2/2 PASS |
|
||||
| `git diff --check` | PASS |
|
||||
| 시작/최종 `git ls-remote origin refs/heads/feature/orgchart-package` | 같은 SHA |
|
||||
|
||||
Playwright는 프로젝트 helper의 `ORGCHART_PLAYWRIGHT_MODULE`에 이미 설치된 bundled Playwright를 연결했고 `ORGCHART_CHROME=C:\Program Files\Google\Chrome\Application\chrome.exe`를 사용했다. 브라우저 도구 다운로드/의존성 업그레이드는 하지 않았다. Codex 내장 브라우저에서도 현재 Storybook manager/Full Composition을 열어 확인하고 검토 탭을 유지했다.
|
||||
|
||||
최초 FAIL 원인과 후속 근거:
|
||||
|
||||
- Full 1440: 상단 인원표·하단 파견 영역은 중앙 카드와 서로 다른 x 위치에 있다. 세로 범위만 비교한 최초 검사에서 겹침으로 오판했다. 실제 x/y 사각형 교차로 확인한 겹침 0개(`recheck.json`).
|
||||
- React/Vanilla popup: Pan 후 화면 밖 카드를 Playwright로 클릭하면 도구가 자동 scrollIntoView를 실행하며 위치가 바뀐다. 화면 안 카드 클릭으로 확인한 popup/접기·펼치기는 기존 배율·Pan 유지. 제품 자동 Fit으로 판정하지 않는다.
|
||||
- HM Dialog focus: ESC 직후 애니메이션/비동기 종료 완료 전에 focus를 읽었다. Dialog hidden 및 focus 복귀 완료를 기다린 재검사 PASS.
|
||||
|
||||
상호작용/재검사 콘솔·runtime 오류는 모두 0건이다. 통과한 검사를 전체 반복하지 않았으며 제품 코드는 진단으로 변경하지 않았다. 범용 측정 JSON의 `outside` 값은 Full Composition containment 판정에 사용했고, 독립 Department specimen의 작은 status Wrapper나 탐색 모드 Expanded를 Full Stage 경계로 해석하지 않았다.
|
||||
|
||||
필수 새 캡처:
|
||||
|
||||
1. [Overview](../../test-results/visual-review-p10-10/overview.png)
|
||||
2. [Department Colors](../../test-results/visual-review-p10-10/department-colors.png)
|
||||
3. [Full Composition 1440×900](../../test-results/visual-review-p10-10/full-1440.png)
|
||||
4. [Full Composition 390×844](../../test-results/visual-review-p10-10/full-390.png)
|
||||
5. [Expanded 초기 화면](../../test-results/visual-review-p10-10/expanded.png), [Fit 후 7회사 전체](../../test-results/visual-review-p10-10/expanded-fit.png)
|
||||
6. [Chart Only React](../../test-results/visual-review-p10-10/chart-only-react.png)
|
||||
7. [Chart Only Vanilla](../../test-results/visual-review-p10-10/chart-only-vanilla.png)
|
||||
|
||||
추가 증거: `full-1920.png`, 390/1440 Core popup, React/Vanilla popup, 회사 선택, Expanded TB/LR/Hybrid, 애니메이션 종료 후 `expanded-hm-dialog.png`. 기존 캡처를 이번 증거로 제출하지 않았다.
|
||||
|
||||
## 기존 미해결 상태와 승인
|
||||
|
||||
- Guard: 기존 TOKEN-001 FAIL 5 / NOT VERIFIED 9 / 분석기 회귀 94/94 기존 PASS 보존. Guard 재실행·검사기 수정·결과 덮어쓰기 없음. 기존 styleEvidence STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED와 결합 styleVerification 미생성을 그대로 유지.
|
||||
- 보안: 기존 개발 High 7 / 고유 Advisory 1 / 과거 운영 감사 0 유지. 신규 감사 결과 또는 현재 의존 그래프의 무취약 보증으로 표현하지 않는다.
|
||||
- 접근성: 기존 삼안 제목 약 2.64:1, 장헌산업 약 4.29:1. 회사색·흰색 그룹 제목 변경 없음. 기존 정책 승인 대기.
|
||||
- 사용자 최종 디자인 승인: 4 Story와 React/Vanilla Chart Only 모두 PENDING. 특히 390px 축소 가독성·큰 여백과 1440px 배치/간격 확인 필요. 이번 브라우저 PASS를 디자인 승인으로 처리하지 않는다.
|
||||
- NOT VERIFIED: 과거 baseline 픽셀 동일성, 실제 모바일 기기·touch/스크린리더, Safari/Firefox, 모든 회사×100/300명×레이아웃 조합, 1280×1024·1920×1440, 기존 20/100/500명 전체 51개 회귀 재실행, 전체 Typecheck/Guard/npm 신규 감사. 직접 영향이 없는 기존 검증을 반복하지 않은 항목들이다.
|
||||
|
||||
## 작업 시간 (KST)
|
||||
|
||||
- Started At: 2026-10-13 21:15:18 KST
|
||||
- Completed At: 2026-10-13 21:27:30 KST
|
||||
- Actual Duration: 12분 12초
|
||||
- Estimated Duration: 최초 약 25–40분
|
||||
- Variance: 최초 예상보다 12분 48초–27분 48초 짧음
|
||||
@@ -0,0 +1,42 @@
|
||||
# OrgChart Chart Only example
|
||||
|
||||
Standalone Vanilla and React consumers of the public Core option
|
||||
`presentationMode: 'chart-only'` (Auto only). Each page is one white canvas that
|
||||
fills the window: no title, as-of date, summary, toolbar or guidance. The chart
|
||||
opens fitted and centered, and the plain mouse wheel zooms at the pointer.
|
||||
Not a Storybook page.
|
||||
|
||||
```bash
|
||||
npm run build:org-chart
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run build:ui
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run build:org-chart-react
|
||||
```
|
||||
|
||||
```bash
|
||||
npx vite --config examples/org-chart-chart-only/vite.config.ts
|
||||
```
|
||||
|
||||
Pages (port 6041): `vanilla.html`, `react.html` (default entry, Core popup) and
|
||||
`react-hm-dialog.html` (optional `/hm-dialog` entry with the HM theme).
|
||||
Query: `data=20|100|500` (20/100 are the existing multi-company fixtures, 500 is a
|
||||
virtual single-company dataset with 6 divisions and 5·2·4·1·3·2 teams),
|
||||
`layout=tb|lr|hybrid`, `embed=1` (verification only: a 480px canvas inside a
|
||||
scrolling page).
|
||||
|
||||
Browser verification against the running server. Playwright and a browser are
|
||||
resolved through `ORGCHART_PLAYWRIGHT_MODULE` / `ORGCHART_CHROME` as in the other
|
||||
OrgChart checks; evidence goes to the ignored `test-results/org-chart-chart-only/`.
|
||||
|
||||
```bash
|
||||
node examples/org-chart-chart-only/verify.mjs
|
||||
```
|
||||
|
||||
`verify-storybook.mjs` checks the four public OrgChart Storybook menus against
|
||||
`STORYBOOK_URL` (default `http://127.0.0.1:6044`) and, with `BASELINE_URL`,
|
||||
compares DOM and pixels with a baseline Storybook.
|
||||
@@ -0,0 +1,6 @@
|
||||
/* Page layout only: the chart canvas fills the window. */
|
||||
html, body { height: 100%; margin: 0; background: #ffffff; }
|
||||
#chart { height: 100%; }
|
||||
/* Verification only: page content above and below a bounded canvas. */
|
||||
body.embed { height: auto; }
|
||||
body.embed #chart { height: 480px; margin: 600px 0 1400px; }
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Employee, OrgChartDataset, Organization } from '@hm-design/org-chart';
|
||||
import small from '../org-chart/data-expanded-small.json';
|
||||
import medium from '../org-chart/data-expanded-medium.json';
|
||||
|
||||
/** Deterministic single-company dataset: 6 divisions with 5·2·4·1·3·2 teams.
|
||||
* Virtual names only; no live data. Leaders are counted in `total`. */
|
||||
export function createSamanDataset(total = 500): OrgChartDataset {
|
||||
const organizations: Organization[] = [];
|
||||
const employees: Employee[] = [];
|
||||
const add = (organizationId: string, position: string) => {
|
||||
const id = `saman-e${String(employees.length + 1).padStart(3, '0')}`;
|
||||
employees.push({ id, organizationId, affiliationId: 'saman', name: `가상직원 ${String(employees.length + 1).padStart(3, '0')}`, position, jobTitle: null, profileImage: null, workStatus: 'active' });
|
||||
return id;
|
||||
};
|
||||
const org = (id: string, parentId: string | null, name: string, type: Organization['type'], displayOrder: number) => {
|
||||
const node: Organization = { id, companyId: 'saman', parentId, name, type, leaderId: null, displayOrder };
|
||||
organizations.push(node);
|
||||
return node;
|
||||
};
|
||||
const root = org('saman-root', null, '삼안', 'root', 0);
|
||||
root.leaderId = add(root.id, '대표이사');
|
||||
const teams: Organization[] = [];
|
||||
[5, 2, 4, 1, 3, 2].forEach((teamCount, d) => {
|
||||
const division = org(`saman-d${d + 1}`, root.id, `가상 ${d + 1}본부`, 'division', d);
|
||||
division.leaderId = add(division.id, '본부장');
|
||||
for (let t = 0; t < teamCount; t++) {
|
||||
const team = org(`saman-d${d + 1}-t${t + 1}`, division.id, `가상 ${d + 1}-${t + 1}팀`, 'team', t);
|
||||
team.leaderId = add(team.id, '팀장');
|
||||
teams.push(team);
|
||||
}
|
||||
});
|
||||
let cursor = 0;
|
||||
while (employees.length < total) add(teams[cursor++ % teams.length]!.id, ['사원', '선임', '책임', '수석'][cursor % 4]!);
|
||||
return {
|
||||
schemaVersion: '0.1-draft', organizations, employees, affiliations: [{ id: 'saman', name: '삼안' }], relations: [],
|
||||
presentation: { title: '삼안 조직도 · 가상 데이터', description: null, hubText: null,
|
||||
groups: { showEmployees: true, showSummary: false, showDispatched: false, showOnLeave: false, showAffiliations: false }, organizationOrder: [] },
|
||||
metadata: { asOfDate: '2026-10-13', dataVersion: `chart-only-saman-${total}`, updateNote: '가상 데이터 · 운영 정보 아님' },
|
||||
} as OrgChartDataset;
|
||||
}
|
||||
|
||||
export const datasets: Record<string, () => OrgChartDataset> = {
|
||||
'20': () => structuredClone(small) as OrgChartDataset,
|
||||
'100': () => structuredClone(medium) as OrgChartDataset,
|
||||
'500': () => createSamanDataset(500),
|
||||
};
|
||||
|
||||
export function readQuery() {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const data = datasets[query.get('data') ?? ''] ? query.get('data')! : '20';
|
||||
const layout = (['tb', 'lr', 'hybrid'] as const).find(type => type === query.get('layout')) ?? 'tb';
|
||||
// embed=1 is a verification layout: a bounded canvas inside a scrolling page.
|
||||
return { data, layout, embed: query.get('embed') === '1' };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/* Optional HM Dialog integration only; the default entry needs none of this. */
|
||||
@import "@hm-design/ui/theme.css";
|
||||
@import "tailwindcss" source("./");
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>OrgChart Chart Only · React HM Dialog</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="./react-hm-dialog.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { StrictMode, useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
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 { datasets, readQuery } from './data';
|
||||
import './hm-theme.css';
|
||||
import './chart-only.css';
|
||||
|
||||
const { data, layout } = readQuery();
|
||||
const stylesheetUrls = { engine: engineUrl, core: coreUrl };
|
||||
|
||||
function ChartOnlyPage() {
|
||||
const dataSource = useMemo(() => new JsonOrgChartDataSource(datasets[data]!()), []);
|
||||
return <OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} displayMode="readOnly" layoutMode="auto"
|
||||
autoLayout={{ type: layout }} presentationMode="chart-only" employeePopup="hm-dialog" style={{ height: '100%' }} />;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('chart')!).render(<StrictMode><ChartOnlyPage /></StrictMode>);
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>OrgChart Chart Only · React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="./react.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { StrictMode, useMemo, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
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 { datasets, readQuery } from './data';
|
||||
import './chart-only.css';
|
||||
|
||||
const { data, layout, embed } = readQuery();
|
||||
if (embed) document.body.classList.add('embed');
|
||||
const stylesheetUrls = { engine: engineUrl, core: coreUrl };
|
||||
let replace: (key: string) => void = () => {};
|
||||
let rerender: () => void = () => {};
|
||||
|
||||
function ChartOnlyPage() {
|
||||
const [key, setKey] = useState(data);
|
||||
const [, setTick] = useState(0);
|
||||
replace = setKey;
|
||||
rerender = () => setTick(value => value + 1);
|
||||
const dataSource = useMemo(() => new JsonOrgChartDataSource(datasets[key]!()), [key]);
|
||||
// Inline option literals are intentional: equal values must not refit on re-render.
|
||||
return <OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} displayMode="readOnly" layoutMode="auto"
|
||||
autoLayout={{ type: layout }} presentationMode="chart-only"
|
||||
zoom={new URLSearchParams(location.search).has('verify-min') ? { min: 0.8 } : undefined}
|
||||
style={{ height: '100%' }} />;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('chart')!).render(<StrictMode><ChartOnlyPage /></StrictMode>);
|
||||
// Verification seam only.
|
||||
Object.assign(window, { replaceData: (key: string) => replace(key), rerenderPage: () => rerender() });
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.app.json",
|
||||
"compilerOptions": { "composite": false, "noEmit": true, "types": ["vite/client"], "resolveJsonModule": true, "tsBuildInfoFile": "../../_local/org-chart-chart-only.tsbuildinfo" },
|
||||
"include": ["*.ts", "*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>OrgChart Chart Only · Vanilla</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="./vanilla.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
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 { datasets, readQuery } from './data';
|
||||
import './chart-only.css';
|
||||
|
||||
const { data, layout, embed } = readQuery();
|
||||
if (embed) document.body.classList.add('embed');
|
||||
const chart = createOrgChart({
|
||||
container: document.getElementById('chart')!,
|
||||
dataSource: new JsonOrgChartDataSource(datasets[data]!()),
|
||||
stylesheetUrls: { engine: engineUrl, core: coreUrl },
|
||||
displayMode: 'readOnly',
|
||||
layoutMode: 'auto',
|
||||
autoLayout: { type: layout },
|
||||
// Chart Only implies initialView "fit" and wheelZoom "direct".
|
||||
presentationMode: 'chart-only',
|
||||
// Regression fixture: Fit below the manual zoom floor, without huge data.
|
||||
zoom: new URLSearchParams(location.search).has('verify-min') ? { min: 0.8 } : undefined,
|
||||
});
|
||||
void chart.mount();
|
||||
// Verification seam only: replace the whole dataset through the public handle.
|
||||
Object.assign(window, { orgChart: chart, replaceData: (key: string) => chart.update(datasets[key]!()) });
|
||||
@@ -0,0 +1,154 @@
|
||||
// Storybook regression for the Chart Only change. Compares this worktree's
|
||||
// Storybook (STORYBOOK_URL) with a baseline Storybook (BASELINE_URL, optional)
|
||||
// and checks that Expanded keeps its controls and Ctrl/⌘ + wheel policy.
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const output = path.join(root, 'test-results/org-chart-chart-only/storybook');
|
||||
await mkdir(output, { recursive: true });
|
||||
const target = process.env.STORYBOOK_URL ?? 'http://127.0.0.1:6044';
|
||||
const baseline = process.env.BASELINE_URL;
|
||||
const browser = await launchChromium();
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
const wheelOnly = process.argv.includes('--wheel-only');
|
||||
const record = (name, pass, detail) => { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}`, detail === undefined ? '' : JSON.stringify(detail)); };
|
||||
const check = async (name, fn) => { try { record(name, true, await fn()); } catch (error) { record(name, false, error.message); } };
|
||||
const stories = ['orgchart--logo-personnel-summary', 'orgchart--department-colors', 'orgchart--full-composition', 'orgchart--expanded-org-chart'];
|
||||
|
||||
async function open(origin, id) {
|
||||
const page = await context.newPage();
|
||||
page.on('console', message => { if (message.type() === 'error') consoleErrors.push({ origin, id, text: message.text() }); });
|
||||
page.on('pageerror', error => consoleErrors.push({ origin, id, text: String(error) }));
|
||||
await page.goto(`${origin}/iframe.html?id=${id}&viewMode=story`);
|
||||
await page.waitForFunction(() => {
|
||||
const hosts = [...document.querySelectorAll('.hm-orgchart-host')];
|
||||
// Overview renders summaries without an OrgChart host.
|
||||
return document.getElementById('storybook-root')?.childElementCount && hosts.every(host => host.shadowRoot?.querySelector('.hm-orgchart-shell')?.getAttribute('aria-busy') === 'false');
|
||||
}, null, { timeout: 60000 });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.waitForTimeout(800);
|
||||
return page;
|
||||
}
|
||||
/** Story DOM plus every OrgChart shadow root, without instance/React IDs. */
|
||||
const snapshot = page => page.evaluate(() => [document.getElementById('storybook-root').innerHTML,
|
||||
...[...document.querySelectorAll('.hm-orgchart-host')].map(host => host.shadowRoot.innerHTML)].join('\n')
|
||||
.replace(/hm-orgchart-\d+/g, 'hm-orgchart-N').replace(/«[^»]*»|_r_[0-9a-z]+_/g, 'react-id'));
|
||||
|
||||
async function pixelDiff(a, b) {
|
||||
const page = await context.newPage();
|
||||
const result = await page.evaluate(async ([ua, ub]) => {
|
||||
const load = src => new Promise(resolve => { const image = new Image(); image.onload = () => resolve(image); image.src = src; });
|
||||
const [ia, ib] = await Promise.all([load(ua), load(ub)]);
|
||||
if (ia.width !== ib.width || ia.height !== ib.height) return { size: [ia.width, ia.height, ib.width, ib.height], diffPixels: Infinity };
|
||||
const read = image => { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const c = canvas.getContext('2d'); c.drawImage(image, 0, 0); return c.getImageData(0, 0, image.width, image.height).data; };
|
||||
const da = read(ia), db = read(ib); let diffPixels = 0, maxDelta = 0; const box = [Infinity, Infinity, -1, -1];
|
||||
for (let i = 0; i < da.length; i += 4) {
|
||||
const delta = Math.max(Math.abs(da[i] - db[i]), Math.abs(da[i + 1] - db[i + 1]), Math.abs(da[i + 2] - db[i + 2]));
|
||||
if (!delta) continue;
|
||||
diffPixels++; maxDelta = Math.max(maxDelta, delta);
|
||||
const p = i / 4, x = p % ia.width, y = Math.floor(p / ia.width);
|
||||
box[0] = Math.min(box[0], x); box[1] = Math.min(box[1], y); box[2] = Math.max(box[2], x); box[3] = Math.max(box[3], y);
|
||||
}
|
||||
return { size: [ia.width, ia.height], diffPixels, maxDelta, box };
|
||||
}, [`data:image/png;base64,${a.toString('base64')}`, `data:image/png;base64,${b.toString('base64')}`]);
|
||||
await page.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
const index = await (await fetch(`${target}/index.json`)).json();
|
||||
if (!wheelOnly) await check('Storybook OrgChart public menus', () => {
|
||||
const visible = Object.values(index.entries).filter(entry => entry.title.startsWith('OrgChart/') && !(entry.tags ?? []).includes('internal')).map(entry => `${entry.title} › ${entry.name}`);
|
||||
assert.deepEqual(visible.sort(), ['OrgChart/Department Colors › Department Colors', 'OrgChart/Expanded OrgChart › Expanded OrgChart', 'OrgChart/Full Composition › Full Composition', 'OrgChart/Overview › Overview']);
|
||||
return visible;
|
||||
});
|
||||
|
||||
for (const id of wheelOnly ? ['orgchart--expanded-org-chart'] : stories) {
|
||||
const page = await open(target, id);
|
||||
const buffer = await page.screenshot({ path: path.join(output, `${id}.png`), fullPage: true });
|
||||
const dom = await snapshot(page);
|
||||
if (baseline) {
|
||||
const reference = await open(baseline, id);
|
||||
const referenceBuffer = await reference.screenshot({ path: path.join(output, `${id}.baseline.png`), fullPage: true });
|
||||
const referenceDom = await snapshot(reference);
|
||||
// Pixel identity is reported, not assumed: when it differs, a second
|
||||
// baseline capture shows whether the baseline itself is deterministic.
|
||||
const pixels = buffer.equals(referenceBuffer) ? { identical: true } : {
|
||||
target: await pixelDiff(buffer, referenceBuffer),
|
||||
baselineSelf: await pixelDiff(referenceBuffer, await (await open(baseline, id)).screenshot({ fullPage: true })),
|
||||
};
|
||||
await check(`${id} matches baseline (DOM + pixels)`, () => {
|
||||
assert.equal(dom, referenceDom, 'shadow DOM differs');
|
||||
// Identical DOM with a handful of ±1 channel pixels is rasterization
|
||||
// jitter of the scaled Fixed stage, not a design change.
|
||||
if (!pixels.identical) assert.ok(pixels.target.diffPixels <= 50 && pixels.target.maxDelta <= 2, `pixel diff ${JSON.stringify(pixels)}`);
|
||||
return { domLength: dom.length, pixels };
|
||||
});
|
||||
await reference.close();
|
||||
}
|
||||
if (id === 'orgchart--expanded-org-chart') {
|
||||
const state = () => page.evaluate(() => {
|
||||
const sr = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const vp = sr.querySelector('.hm-orgchart-viewport');
|
||||
return { scale: +vp.dataset.scale, x: +vp.dataset.panX, y: +vp.dataset.panY,
|
||||
heading: sr.querySelector('.hm-orgchart-title')?.textContent, date: sr.querySelector('.hm-orgchart-date')?.textContent,
|
||||
selection: sr.querySelector('.hm-orgchart-expanded-selection')?.textContent,
|
||||
toolbar: [...sr.querySelectorAll('.hm-orgchart-expanded-toolbar > *')].map(e => e.textContent),
|
||||
feedback: sr.querySelector('.hm-orgchart-move-feedback')?.textContent, presentation: sr.querySelector('.hm-orgchart-shell').dataset.presentationMode ?? null };
|
||||
});
|
||||
const before = await state();
|
||||
if (!wheelOnly) await check('Expanded keeps heading, date, summary, toolbar and guidance', () => {
|
||||
assert.equal(before.presentation, null);
|
||||
assert.ok(before.heading && before.date?.startsWith('기준일') && before.selection?.startsWith('선택'));
|
||||
assert.deepEqual(before.toolbar.slice(0, 4), ['확대', '축소', '100% 복원', 'Fit to View']);
|
||||
assert.match(before.toolbar[4], /^\d+%$/);
|
||||
assert.equal(before.toolbar[5], '드래그·방향키 이동 · Ctrl/⌘ + 휠 확대');
|
||||
assert.match(before.feedback, /Drag & Drop OFF/);
|
||||
return before;
|
||||
});
|
||||
const canvas = page.locator('.hm-orgchart-viewport');
|
||||
await canvas.evaluate(element => element.scrollIntoView({ block: 'center', behavior: 'instant' }));
|
||||
const box = await canvas.boundingBox();
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.wheel(0, -100);
|
||||
await page.waitForTimeout(150);
|
||||
const plain = await state();
|
||||
const plainScroll = await page.evaluate(() => scrollY);
|
||||
// A plain wheel scrolls the story page; aim at the canvas again.
|
||||
await canvas.evaluate(element => element.scrollIntoView({ block: 'center', behavior: 'instant' }));
|
||||
const moved = await canvas.boundingBox();
|
||||
await page.mouse.move(moved.x + moved.width / 2, moved.y + moved.height / 2);
|
||||
await page.keyboard.down('Control'); await page.mouse.wheel(0, -100); await page.keyboard.up('Control');
|
||||
await page.waitForTimeout(150);
|
||||
const ctrl = await state();
|
||||
await check('Expanded plain wheel does not zoom; Ctrl + wheel zooms', () => {
|
||||
assert.equal(plain.scale, before.scale);
|
||||
assert.ok(ctrl.scale > plain.scale);
|
||||
return { before: before.scale, plain: plain.scale, ctrl: +ctrl.scale.toFixed(4), pageScrolledByPlainWheel: plainScroll };
|
||||
});
|
||||
if (!wheelOnly) {
|
||||
await page.getByRole('button', { name: 'Fit to View' }).click();
|
||||
const fitted = await state();
|
||||
await check('Expanded Fit to View button still works', () => { assert.ok(fitted.scale <= 1 && fitted.scale !== ctrl.scale); return { scale: +fitted.scale.toFixed(4) }; });
|
||||
}
|
||||
}
|
||||
if (id === 'orgchart--full-composition') {
|
||||
await check('Full Composition keeps Fixed composition', () => page.evaluate(() => {
|
||||
const shells = [...document.querySelectorAll('.hm-orgchart-host')].map(host => host.shadowRoot.querySelector('.hm-orgchart-shell'));
|
||||
if (!shells.length || shells.some(shell => shell.dataset.presentationMode)) throw new Error('unexpected presentation mode');
|
||||
return shells.map(shell => ({ layout: shell.dataset.layoutMode, heading: !!shell.querySelector('.hm-orgchart-heading'), stage: !!shell.querySelector('.hm-orgchart-stage') }));
|
||||
}));
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
await browser.close();
|
||||
const failed = results.filter(result => !result.pass);
|
||||
await writeFile(path.join(output, wheelOnly ? 'results-wheel.json' : 'results.json'), JSON.stringify({ target, baseline, results, consoleErrors }, null, 2));
|
||||
console.log(`\n${results.length - failed.length}/${results.length} PASS · console errors ${consoleErrors.length}`);
|
||||
for (const error of consoleErrors) console.log('console:', error.origin, error.id, error.text);
|
||||
if (failed.length || consoleErrors.length) process.exitCode = 1;
|
||||
@@ -0,0 +1,400 @@
|
||||
// Chart Only browser verification against a running preview server:
|
||||
// npx vite --config examples/org-chart-chart-only/vite.config.ts
|
||||
// node examples/org-chart-chart-only/verify.mjs
|
||||
// CHART_ONLY_URL (default http://127.0.0.1:6041) selects the server.
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const output = path.join(root, 'test-results/org-chart-chart-only');
|
||||
await mkdir(output, { recursive: true });
|
||||
const base = process.env.CHART_ONLY_URL ?? 'http://127.0.0.1:6041';
|
||||
const browser = await launchChromium();
|
||||
const results = [];
|
||||
const consoleErrors = [];
|
||||
const record = (id, name, pass, detail) => { results.push({ id, name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${name}`, detail === undefined ? '' : JSON.stringify(detail)); };
|
||||
const check = (id, name, fn) => async (...args) => {
|
||||
try { const detail = await fn(...args); record(id, name, true, detail); }
|
||||
catch (error) { record(id, name, false, error.message); }
|
||||
};
|
||||
|
||||
/** All geometry in CSS pixels relative to the canvas; bounds include connectors. */
|
||||
const measure = page => page.evaluate(() => {
|
||||
const sr = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const vp = sr.querySelector('.hm-orgchart-viewport'); const box = vp.getBoundingClientRect();
|
||||
let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
|
||||
for (const e of sr.querySelectorAll('.hm-orgchart-forest .hm-orgchart-organization, .hm-orgchart-forest .hm-orgchart-organization-connectors path')) {
|
||||
const x = e.getBoundingClientRect(); if (!x.width && !x.height) continue;
|
||||
l = Math.min(l, x.left); t = Math.min(t, x.top); r = Math.max(r, x.right); b = Math.max(b, x.bottom);
|
||||
}
|
||||
const css = getComputedStyle;
|
||||
const feedback = sr.querySelector('.hm-orgchart-move-feedback')?.getBoundingClientRect();
|
||||
return {
|
||||
canvas: { left: box.left, top: box.top, width: box.width, height: box.height },
|
||||
scale: +vp.dataset.scale, x: +vp.dataset.panX, y: +vp.dataset.panY, pending: vp.dataset.placementPending ?? null,
|
||||
dragDrop: vp.dataset.dragDropEnabled,
|
||||
margins: { l: l - box.left, t: t - box.top, r: box.right - r, b: box.bottom - b },
|
||||
centerErrX: (l + r) / 2 - (box.left + box.width / 2), centerErrY: (t + b) / 2 - (box.top + box.height / 2),
|
||||
inside: l >= box.left - 0.5 && t >= box.top - 0.5 && r <= box.right + 0.5 && b <= box.bottom + 0.5,
|
||||
cards: sr.querySelectorAll('.hm-orgchart-employee').length,
|
||||
organizations: sr.querySelectorAll('.hm-orgchart-organization').length,
|
||||
connectors: sr.querySelectorAll('.hm-orgchart-forest .edge, .hm-orgchart-organization-connectors path, .hm-orgchart-employee-connectors path').length,
|
||||
external: {
|
||||
heading: !!sr.querySelector('.hm-orgchart-heading'), date: !!sr.querySelector('.hm-orgchart-date'),
|
||||
selection: !!sr.querySelector('.hm-orgchart-expanded-selection'), toolbar: !!sr.querySelector('.hm-orgchart-expanded-toolbar'),
|
||||
zoomRatio: !!sr.querySelector('.hm-orgchart-expanded output'),
|
||||
feedbackVisible: !!feedback && feedback.width > 1 && feedback.height > 1,
|
||||
siblingsOfCanvas: [...sr.querySelector('.hm-orgchart-expanded').children].filter(e => e !== vp && e.getBoundingClientRect().width > 1).map(e => e.className),
|
||||
},
|
||||
colors: { page: css(document.body).backgroundColor, shell: css(sr.querySelector('.hm-orgchart-shell')).backgroundColor, content: css(sr.querySelector('.hm-orgchart-expanded')).backgroundColor, canvas: css(vp).backgroundColor, canvasBorder: css(vp).borderTopWidth, contentPadding: css(sr.querySelector('.hm-orgchart-expanded')).paddingTop },
|
||||
companyColors: [...new Set([...sr.querySelectorAll('.node[data-company-id]')].map(n => `${n.dataset.companyId}:${n.style.getPropertyValue('--hm-orgchart-expanded-primary')}`))],
|
||||
pageOverflowX: document.documentElement.scrollWidth > innerWidth,
|
||||
};
|
||||
});
|
||||
|
||||
async function ready(page) {
|
||||
await page.waitForFunction(() => {
|
||||
const sr = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
const vp = sr?.querySelector('.hm-orgchart-viewport');
|
||||
return vp && !vp.dataset.placementPending && sr.querySelector('.hm-orgchart-employee') && sr.querySelector('.hm-orgchart-shell')?.getAttribute('aria-busy') === 'false';
|
||||
}, null, { timeout: 30000 });
|
||||
// Two settled frames: no later Fit may move the view.
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
}
|
||||
|
||||
/** Samples the visible transform from navigation until settle; repeated
|
||||
* visible Fits would show up as more than one distinct transform. */
|
||||
async function visibleTransforms(page, ms = 1500) {
|
||||
return page.evaluate(ms => new Promise(resolve => {
|
||||
const seen = []; const start = performance.now();
|
||||
const tick = () => {
|
||||
const vp = document.querySelector('.hm-orgchart-host')?.shadowRoot?.querySelector('.hm-orgchart-viewport');
|
||||
const forest = vp?.querySelector('.hm-orgchart-forest');
|
||||
if (forest && !vp.dataset.placementPending) { const value = forest.style.transform; if (seen.at(-1) !== value) seen.push(value); }
|
||||
if (performance.now() - start < ms) requestAnimationFrame(tick); else resolve(seen);
|
||||
};
|
||||
tick();
|
||||
}), ms);
|
||||
}
|
||||
|
||||
async function open(context, url) {
|
||||
const page = await context.newPage();
|
||||
page.on('console', message => { if (message.type() === 'error') consoleErrors.push({ url, text: message.text() }); });
|
||||
page.on('pageerror', error => consoleErrors.push({ url, text: String(error) }));
|
||||
await page.goto(url);
|
||||
return page;
|
||||
}
|
||||
|
||||
const near = (a, b, tolerance) => Math.abs(a - b) <= tolerance;
|
||||
const desktop = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
|
||||
|
||||
// A/B: output and initial Fit for both consumers and all sizes.
|
||||
for (const kind of ['vanilla', 'react']) {
|
||||
for (const size of ['20', '100', '500']) {
|
||||
const url = `${base}/${kind}.html?data=${size}`;
|
||||
const page = await open(desktop, url);
|
||||
const transforms = await visibleTransforms(page);
|
||||
await ready(page);
|
||||
const m = await measure(page);
|
||||
await page.screenshot({ path: path.join(output, `${kind}-${size}-desktop.png`) });
|
||||
const label = `${kind} ${size}`;
|
||||
await check('A', `${label} Chart Only output`, () => {
|
||||
assert.deepEqual(Object.entries(m.external).filter(([key, value]) => key === 'siblingsOfCanvas' ? value.length : value), []);
|
||||
assert.equal(m.colors.canvas, 'rgb(255, 255, 255)'); assert.equal(m.colors.content, 'rgb(255, 255, 255)'); assert.equal(m.colors.shell, 'rgb(255, 255, 255)');
|
||||
assert.equal(m.colors.canvasBorder, '0px'); assert.equal(m.colors.contentPadding, '0px');
|
||||
assert.ok(m.cards > 0 && m.organizations > 0 && m.connectors > 0);
|
||||
assert.ok(near(m.canvas.width, 1440, 1) && near(m.canvas.height, 900, 1), 'canvas fills window');
|
||||
return { cards: m.cards, organizations: m.organizations, connectors: m.connectors, colors: m.companyColors };
|
||||
})();
|
||||
await check('B', `${label} initial Fit and center`, () => {
|
||||
assert.ok(m.inside, 'bounds inside canvas');
|
||||
assert.ok(Math.abs(m.centerErrX) <= 1 && Math.abs(m.centerErrY) <= 1, `center error ${m.centerErrX}, ${m.centerErrY}`);
|
||||
assert.ok(m.scale <= 1);
|
||||
assert.ok(Math.min(m.margins.l, m.margins.r, m.margins.t, m.margins.b) >= 15.5, 'edge margin');
|
||||
assert.equal(transforms.length, 1, `visible transforms ${JSON.stringify(transforms)}`);
|
||||
return { scale: +m.scale.toFixed(4), centerErrX: +m.centerErrX.toFixed(3), centerErrY: +m.centerErrY.toFixed(3), margins: Object.fromEntries(Object.entries(m.margins).map(([k, v]) => [k, +v.toFixed(2)])), visibleTransforms: transforms.length };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
// LR / Hybrid keep their layout and still fit.
|
||||
for (const layout of ['lr', 'hybrid']) {
|
||||
const page = await open(desktop, `${base}/vanilla.html?data=100&layout=${layout}`);
|
||||
await ready(page);
|
||||
const m = await measure(page);
|
||||
await page.screenshot({ path: path.join(output, `vanilla-100-${layout}.png`) });
|
||||
await check('B', `vanilla 100 ${layout} Fit and center`, () => {
|
||||
assert.ok(m.inside && Math.abs(m.centerErrX) <= 1 && Math.abs(m.centerErrY) <= 1, JSON.stringify(m.margins));
|
||||
return { scale: +m.scale.toFixed(4), centerErrX: +m.centerErrX.toFixed(3), centerErrY: +m.centerErrY.toFixed(3) };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// C/D: wheel, Pan, popup, groups and replacement on the 500-person chart.
|
||||
for (const kind of ['vanilla', 'react']) {
|
||||
const page = await open(desktop, `${base}/${kind}.html?data=500`);
|
||||
await ready(page);
|
||||
const fitted = await measure(page);
|
||||
const card = page.locator('.hm-orgchart-employee').nth(30);
|
||||
const cardBox = await card.boundingBox();
|
||||
const point = { x: cardBox.x + cardBox.width / 2, y: cardBox.y + cardBox.height / 2 };
|
||||
const local = m => ({ x: (point.x - m.canvas.left - m.x) / m.scale, y: (point.y - m.canvas.top - m.y) / m.scale });
|
||||
await page.mouse.move(point.x, point.y);
|
||||
await page.mouse.wheel(0, -100);
|
||||
await page.waitForTimeout(50);
|
||||
const zoomedIn = await measure(page);
|
||||
await check('C', `${kind} plain wheel up zooms in (no Ctrl/⌘)`, () => {
|
||||
assert.ok(zoomedIn.scale > fitted.scale * 1.1, `${fitted.scale} -> ${zoomedIn.scale}`);
|
||||
return { from: +fitted.scale.toFixed(4), to: +zoomedIn.scale.toFixed(4) };
|
||||
})();
|
||||
await check('C', `${kind} zoom anchors at pointer`, () => {
|
||||
const before = local(fitted), after = local(zoomedIn);
|
||||
const error = Math.hypot(before.x - after.x, before.y - after.y) * zoomedIn.scale;
|
||||
const box = { x: cardBox.x, y: cardBox.y };
|
||||
assert.ok(error < 0.5, `screen drift ${error}`);
|
||||
return { screenDriftPx: +error.toFixed(4), card: box };
|
||||
})();
|
||||
await page.mouse.wheel(0, 100);
|
||||
await page.waitForTimeout(50);
|
||||
const zoomedOut = await measure(page);
|
||||
await check('C', `${kind} plain wheel down zooms out`, () => {
|
||||
assert.ok(zoomedOut.scale < zoomedIn.scale);
|
||||
return { from: +zoomedIn.scale.toFixed(4), to: +zoomedOut.scale.toFixed(4) };
|
||||
})();
|
||||
for (let i = 0; i < 40; i++) await page.mouse.wheel(0, -120);
|
||||
await page.waitForTimeout(50);
|
||||
const maximum = (await measure(page)).scale;
|
||||
for (let i = 0; i < 80; i++) await page.mouse.wheel(0, 120);
|
||||
await page.waitForTimeout(50);
|
||||
const minimum = (await measure(page)).scale;
|
||||
await check('C', `${kind} min/max limits`, () => {
|
||||
assert.equal(maximum, 3);
|
||||
assert.ok(near(minimum, Math.min(0.05, fitted.scale), 1e-9), `min ${minimum} fit ${fitted.scale}`);
|
||||
return { max: maximum, min: +minimum.toFixed(4), fitScale: +fitted.scale.toFixed(4) };
|
||||
})();
|
||||
// Back to a readable zoom around the same card, then Pan from empty space.
|
||||
await page.mouse.move(point.x, point.y);
|
||||
for (let i = 0; i < 6; i++) await page.mouse.wheel(0, -100);
|
||||
await page.waitForTimeout(50);
|
||||
const beforePan = await measure(page);
|
||||
const empty = { x: beforePan.canvas.left + 6, y: beforePan.canvas.top + beforePan.canvas.height - 6 };
|
||||
const hit = await page.evaluate(({ x, y }) => { const sr = document.querySelector('.hm-orgchart-host').shadowRoot; const e = sr.elementFromPoint(x, y); return e?.closest('.hm-orgchart-employee, button') ? 'control' : e?.className ?? ''; }, empty);
|
||||
await page.mouse.move(empty.x, empty.y); await page.mouse.down();
|
||||
await page.mouse.move(empty.x + 60, empty.y - 40, { steps: 4 }); await page.mouse.move(empty.x + 120, empty.y - 80, { steps: 4 });
|
||||
await page.mouse.up();
|
||||
const afterPan = await measure(page);
|
||||
await check('C', `${kind} Pan after Zoom`, () => {
|
||||
assert.notEqual(hit, 'control');
|
||||
assert.ok(near(afterPan.x - beforePan.x, 120, 0.5) && near(afterPan.y - beforePan.y, -80, 0.5), `${afterPan.x - beforePan.x}, ${afterPan.y - beforePan.y}`);
|
||||
assert.equal(afterPan.scale, beforePan.scale);
|
||||
return { dx: afterPan.x - beforePan.x, dy: afterPan.y - beforePan.y, scale: +afterPan.scale.toFixed(4) };
|
||||
})();
|
||||
await page.waitForTimeout(600);
|
||||
const settled = await measure(page);
|
||||
await check('C', `${kind} no automatic Fit after user Zoom/Pan`, () => {
|
||||
assert.deepEqual([settled.scale, settled.x, settled.y], [afterPan.scale, afterPan.x, afterPan.y]);
|
||||
})();
|
||||
// Ctrl + wheel also zooms in the canvas and the browser page zoom is not used.
|
||||
await page.keyboard.down('Control'); await page.mouse.move(point.x, point.y); await page.mouse.wheel(0, -100); await page.keyboard.up('Control');
|
||||
await page.waitForTimeout(50);
|
||||
const ctrlZoom = await measure(page);
|
||||
const pageZoom = await page.evaluate(() => [visualViewport.scale, innerWidth]);
|
||||
await check('C', `${kind} Ctrl + wheel stays inside the canvas`, () => {
|
||||
assert.ok(ctrlZoom.scale > settled.scale); assert.deepEqual(pageZoom, [1, 1440]);
|
||||
return { scale: +ctrlZoom.scale.toFixed(4) };
|
||||
})();
|
||||
|
||||
// D: employee popup keeps the view.
|
||||
const view = await measure(page);
|
||||
const visibleCard = page.locator('.hm-orgchart-employee').filter({ hasNot: page.locator('[hidden]') }).nth(30);
|
||||
await visibleCard.click();
|
||||
const dialog = page.locator('dialog.hm-orgchart-employee-dialog');
|
||||
const opened = await dialog.evaluate(element => element.open).catch(() => false);
|
||||
const dialogText = opened ? (await dialog.innerText()).replace(/\s+/g, ' ').slice(0, 80) : '';
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(100);
|
||||
const closed = !(await dialog.evaluate(element => element.open));
|
||||
const afterDialog = await measure(page);
|
||||
await check('D', `${kind} employee popup open/close keeps Zoom/Pan`, () => {
|
||||
assert.ok(opened, 'dialog opened'); assert.ok(closed, 'dialog closed');
|
||||
assert.deepEqual([afterDialog.scale, afterDialog.x, afterDialog.y], [view.scale, view.x, view.y]);
|
||||
return { dialogText };
|
||||
})();
|
||||
// D: group collapse/expand keeps the view and does not Fit.
|
||||
const toggle = page.locator('.hm-orgchart-expanded-toggle[aria-disabled="false"]').nth(3);
|
||||
const id = await toggle.getAttribute('data-organization-id');
|
||||
const cardsBefore = afterDialog.cards;
|
||||
await toggle.click();
|
||||
await page.waitForFunction(id => document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`)?.getAttribute('aria-expanded') === 'false', id);
|
||||
await ready(page);
|
||||
const collapsed = await measure(page);
|
||||
await page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`).click();
|
||||
await page.waitForFunction(id => document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`)?.getAttribute('aria-expanded') === 'true', id);
|
||||
await ready(page);
|
||||
const expanded = await measure(page);
|
||||
await check('D', `${kind} group collapse/expand without auto Fit`, () => {
|
||||
assert.ok(collapsed.cards < cardsBefore && expanded.cards === cardsBefore, `${cardsBefore} -> ${collapsed.cards} -> ${expanded.cards}`);
|
||||
assert.deepEqual([collapsed.scale, expanded.scale], [view.scale, view.scale]);
|
||||
assert.deepEqual([expanded.x, expanded.y], [view.x, view.y]);
|
||||
return { cards: [cardsBefore, collapsed.cards, expanded.cards], scale: +view.scale.toFixed(4) };
|
||||
})();
|
||||
// D: Drag & Drop defaults to OFF; a card drag never changes membership.
|
||||
const dragSource = page.locator('.hm-orgchart-employee').nth(40);
|
||||
const sourceId = await dragSource.getAttribute('data-employee-id');
|
||||
const ownerBefore = await page.evaluate(id => document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`.hm-orgchart-employee[data-employee-id="${id}"]`)?.closest('.node')?.dataset.organizationId, sourceId);
|
||||
const sourceBox = await dragSource.boundingBox();
|
||||
const targetBox = await page.locator('.hm-orgchart-expanded-toggle').nth(1).boundingBox();
|
||||
await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2); await page.mouse.down();
|
||||
await page.mouse.move(targetBox.x + 10, targetBox.y + 5, { steps: 10 }); await page.mouse.up();
|
||||
await page.waitForTimeout(200);
|
||||
const afterDrag = await measure(page);
|
||||
const owner = await page.evaluate(id => document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`.hm-orgchart-employee[data-employee-id="${id}"]`)?.closest('.node')?.dataset.organizationId, sourceId);
|
||||
await page.keyboard.press('Escape');
|
||||
await check('D', `${kind} Drag & Drop default OFF`, () => {
|
||||
assert.equal(afterDrag.dragDrop, 'false');
|
||||
assert.ok(owner && owner === ownerBefore, `${ownerBefore} -> ${owner}`);
|
||||
return { dragDropEnabled: afterDrag.dragDrop, owner };
|
||||
})();
|
||||
// B16 / React re-render: user view survives a plain re-render, replacement Fits again.
|
||||
if (kind === 'react') {
|
||||
const userView = await measure(page);
|
||||
await page.evaluate(() => window.rerenderPage());
|
||||
await page.waitForTimeout(400);
|
||||
const rerendered = await measure(page);
|
||||
await check('C', 'react plain re-render keeps user view (no Fit)', () => {
|
||||
assert.deepEqual([rerendered.scale, rerendered.x, rerendered.y], [userView.scale, userView.x, userView.y]);
|
||||
})();
|
||||
}
|
||||
await page.evaluate(() => window.replaceData('100'));
|
||||
await page.waitForFunction(() => document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('.hm-orgchart-employee').length === 100);
|
||||
await ready(page);
|
||||
const replaced = await measure(page);
|
||||
await page.screenshot({ path: path.join(output, `${kind}-replaced-100.png`) });
|
||||
await check('B', `${kind} full data replacement Fits again`, () => {
|
||||
assert.ok(replaced.inside && Math.abs(replaced.centerErrX) <= 1 && Math.abs(replaced.centerErrY) <= 1, JSON.stringify(replaced.margins));
|
||||
return { scale: +replaced.scale.toFixed(4), centerErrX: +replaced.centerErrX.toFixed(3), centerErrY: +replaced.centerErrY.toFixed(3) };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// C23: outside the canvas the page scrolls; inside, the wheel zooms.
|
||||
for (const kind of ['vanilla', 'react']) {
|
||||
const page = await open(desktop, `${base}/${kind}.html?data=100&embed=1`);
|
||||
await ready(page);
|
||||
await page.mouse.move(700, 300);
|
||||
await page.mouse.wheel(0, 200);
|
||||
await page.waitForTimeout(150);
|
||||
const scrolled = await page.evaluate(() => scrollY);
|
||||
const box = await page.locator('#chart').boundingBox();
|
||||
const before = await measure(page);
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.wheel(0, -100);
|
||||
await page.waitForTimeout(150);
|
||||
const after = await measure(page);
|
||||
const scrollAfter = await page.evaluate(() => scrollY);
|
||||
await check('C', `${kind} page scroll outside canvas / zoom inside`, () => {
|
||||
assert.ok(scrolled >= 150, `outside scrollY ${scrolled}`);
|
||||
assert.equal(scrollAfter, scrolled, 'inside wheel does not scroll the page');
|
||||
assert.ok(after.scale > before.scale);
|
||||
return { outsideScrollY: scrolled, insideScrollY: scrollAfter, scale: [+before.scale.toFixed(4), +after.scale.toFixed(4)] };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// D28: optional HM Dialog adapter.
|
||||
{
|
||||
const page = await open(desktop, `${base}/react-hm-dialog.html?data=20`);
|
||||
await ready(page);
|
||||
const view = await measure(page);
|
||||
await page.locator('.hm-orgchart-employee').nth(4).click();
|
||||
const hmDialog = page.locator('[data-slot="dialog-content"]');
|
||||
await hmDialog.waitFor({ timeout: 5000 });
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: path.join(output, 'react-hm-dialog-open.png') });
|
||||
const text = (await hmDialog.innerText()).replace(/\s+/g, ' ').slice(0, 80);
|
||||
const coreDialogOpen = await page.locator('dialog.hm-orgchart-employee-dialog').evaluate(element => element.open);
|
||||
await page.keyboard.press('Escape');
|
||||
await hmDialog.waitFor({ state: 'detached', timeout: 5000 });
|
||||
const after = await measure(page);
|
||||
await check('D', 'react /hm-dialog Chart Only uses HM Dialog', () => {
|
||||
assert.equal(coreDialogOpen, false);
|
||||
assert.deepEqual([after.scale, after.x, after.y], [view.scale, view.x, view.y]);
|
||||
return { text };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
// Preserve the untouched initial Fit through controller recreation. The manual
|
||||
// minimum is deliberately above Fit to exercise the saved floor without 2,100+ data.
|
||||
for (const kind of ['vanilla', 'react']) {
|
||||
const page = await open(desktop, `${base}/${kind}.html?data=500&verify-min=1`);
|
||||
await ready(page);
|
||||
const initial = await measure(page);
|
||||
const title = page.locator('.hm-orgchart-expanded-toggle').nth(1);
|
||||
await title.click();
|
||||
await page.waitForTimeout(250);
|
||||
const collapsed = await measure(page);
|
||||
await check('R1', `${kind} untouched Fit survives collapse`, () => {
|
||||
assert.ok(initial.scale < 0.8);
|
||||
assert.deepEqual([collapsed.scale, collapsed.x, collapsed.y], [initial.scale, initial.x, initial.y]);
|
||||
})();
|
||||
await title.click(); await page.waitForTimeout(250);
|
||||
const expanded = await measure(page);
|
||||
const viewport = page.locator('.hm-orgchart-viewport'); const box = await viewport.boundingBox();
|
||||
await page.mouse.move(box.x + 8, box.y + box.height - 8); await page.mouse.wheel(0, 120);
|
||||
await page.waitForTimeout(150);
|
||||
const zoomed = await measure(page);
|
||||
await check('R2', `${kind} recreated controller retains below-min Fit floor`, () => {
|
||||
assert.deepEqual([expanded.scale, expanded.x, expanded.y], [initial.scale, initial.x, initial.y]);
|
||||
assert.equal(zoomed.scale, initial.scale, 'wheel out must not jump to 0.8');
|
||||
})();
|
||||
if (kind === 'vanilla') {
|
||||
await page.evaluate(() => window.orgChart.setInteractions({ zoom: false }));
|
||||
await page.evaluate(() => window.orgChart.setInteractions({ zoom: true }));
|
||||
const changed = await measure(page);
|
||||
await check('R3', 'Vanilla interaction recreation retains initial view', () => {
|
||||
assert.deepEqual([changed.scale, changed.x, changed.y], [initial.scale, initial.x, initial.y]);
|
||||
})();
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
await desktop.close();
|
||||
|
||||
// Mobile: initial Fit, touch Pan and layout only (no wheel claims).
|
||||
const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true });
|
||||
for (const kind of ['vanilla', 'react']) {
|
||||
for (const size of ['20', '100', '500']) {
|
||||
const page = await open(mobile, `${base}/${kind}.html?data=${size}`);
|
||||
await ready(page);
|
||||
const m = await measure(page);
|
||||
if (size !== '100') await page.screenshot({ path: path.join(output, `${kind}-${size}-mobile.png`) });
|
||||
let touch;
|
||||
if (size === '20') {
|
||||
const cdp = await mobile.newCDPSession(page);
|
||||
const start = { x: m.canvas.left + 8, y: m.canvas.top + m.canvas.height - 8 };
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [start] });
|
||||
for (let i = 1; i <= 5; i++) await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: start.x + i * 12, y: start.y - i * 10 }] });
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||
const moved = await measure(page);
|
||||
touch = { dx: moved.x - m.x, dy: moved.y - m.y };
|
||||
}
|
||||
await check('B', `${kind} ${size} mobile Fit${touch ? ' + touch Pan' : ''}`, () => {
|
||||
assert.ok(m.inside && Math.abs(m.centerErrX) <= 1 && Math.abs(m.centerErrY) <= 1, JSON.stringify(m.margins));
|
||||
assert.equal(m.pageOverflowX, false);
|
||||
if (touch) assert.ok(Math.abs(touch.dx) > 20 && Math.abs(touch.dy) > 20, JSON.stringify(touch));
|
||||
return { scale: +m.scale.toFixed(4), centerErrX: +m.centerErrX.toFixed(3), centerErrY: +m.centerErrY.toFixed(3), touch };
|
||||
})();
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
await mobile.close();
|
||||
await browser.close();
|
||||
|
||||
const failed = results.filter(result => !result.pass);
|
||||
await writeFile(path.join(output, 'results.json'), JSON.stringify({ base, results, consoleErrors }, null, 2));
|
||||
console.log(`\n${results.length - failed.length}/${results.length} PASS · console errors ${consoleErrors.length}`);
|
||||
for (const error of consoleErrors) console.log('console:', error.url, error.text);
|
||||
if (failed.length || consoleErrors.length) process.exitCode = 1;
|
||||
@@ -0,0 +1,18 @@
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
// Chart Only consumer preview: Vanilla and React use the same public Core.
|
||||
const root = path.resolve(import.meta.dirname);
|
||||
export default defineConfig({
|
||||
root,
|
||||
base: './',
|
||||
publicDir: path.resolve(root, '../../public'),
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: { host: '127.0.0.1', port: 6041 },
|
||||
build: {
|
||||
outDir: 'dist', emptyOutDir: true,
|
||||
rolldownOptions: { input: { vanilla: path.resolve(root, 'vanilla.html'), react: path.resolve(root, 'react.html'), 'react-hm-dialog': path.resolve(root, 'react-hm-dialog.html') } },
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
# OrgChart React public-package consumer
|
||||
|
||||
## P3 follow-up
|
||||
|
||||
`p1.html?mode=readOnly` uses the updated common Core with the position matrix,
|
||||
group links and employee information dialog. No navigation URLs are preset.
|
||||
The development-only `window.OrgChartP1React.setOptions({ organizationLinks,
|
||||
onEmployeeClick })` helper applies public React props to the current fixture;
|
||||
it is not a new Core handle method. `setData` remains available.
|
||||
|
||||
After building Core/Wrapper and both examples, run the focused
|
||||
`node examples/org-chart/verify-p3.mjs`. Evidence goes to
|
||||
`test-results/org-chart-p3`; prior P1/P2 scripts and results remain unchanged.
|
||||
|
||||
This independent Vite consumer imports the built public entries of
|
||||
`@hm-design/org-chart-react`, `@hm-design/org-chart` and `@hm-design/ui/theme.css`.
|
||||
It imports the original Vanilla v1/v2 JSON files directly. It is a verification
|
||||
example, with fictitious data and a `window.OrgChartReactDemo` inspection seam;
|
||||
it has no server/login integration or production business controls.
|
||||
|
||||
From the repository root, build packages before the consumer:
|
||||
|
||||
```sh
|
||||
npm run build:packages
|
||||
npm run build:org-chart-react-example
|
||||
npm run dev:org-chart-react-example
|
||||
```
|
||||
|
||||
Development URL: `http://127.0.0.1:6026/` (strict port). Strict Mode effect replay
|
||||
is checked in development. The production output has its own HTTP browser smoke
|
||||
and update checks. This ESM output is for HTTP deployment; it is not a direct-open
|
||||
`file://` artifact. The existing Vanilla `examples/org-chart/dist/classic.html`
|
||||
remains the direct-open example.
|
||||
|
||||
Core CSS URL imports become deployment assets and are passed into each Shadow DOM.
|
||||
The public HM Theme import emits its existing font assets. Grid/flex consumers
|
||||
must allow their chart-containing items to shrink (`min-width: 0`); horizontal
|
||||
overflow belongs to the Core viewport rather than the document.
|
||||
|
||||
Browser tooling is supplied by the verification environment, not added to the
|
||||
package dependencies. Configure `ORGCHART_PLAYWRIGHT_MODULE` and optionally
|
||||
`ORGCHART_CHROME`, using absolute paths. Start the separate Storybook server on
|
||||
6025 and consumer on 6026, then:
|
||||
|
||||
```sh
|
||||
# Set these environment variables using your shell's syntax:
|
||||
# ORGCHART_CONSUMER_URL=http://127.0.0.1:6026
|
||||
# ORGCHART_STORYBOOK_URL=http://127.0.0.1:6025
|
||||
npm run test:org-chart-react-browser
|
||||
node examples/org-chart-react/verify-boundaries.mjs
|
||||
```
|
||||
|
||||
`verify.mjs` inspects real Core DOM, compares Vanilla/React snapshots, exercises
|
||||
React and Storybook lifecycles and records screenshots/metrics at four widths.
|
||||
`verify-boundaries.mjs` checks production updates, source identity semantics,
|
||||
actual horizontal scrolling, global CSS isolation and direct-open Vanilla smoke.
|
||||
Both write evidence under ignored `test-results/org-chart-react/`.
|
||||
|
||||
To check the actual package archive and declarations without another build:
|
||||
|
||||
```sh
|
||||
npm pack -w @hm-design/org-chart --ignore-scripts --pack-destination test-results
|
||||
npm pack -w @hm-design/org-chart-react --ignore-scripts --pack-destination test-results
|
||||
node examples/org-chart-react/verify-package.mjs
|
||||
```
|
||||
|
||||
The tarball test uses extracted Core/Wrapper packages with the environment's
|
||||
React/compiler; it does not establish a fresh remote-registry installation.
|
||||
|
||||
## P2 read-only Full Composition
|
||||
|
||||
Open `p1.html?mode=readOnly` to use the same read-only Core composition as the
|
||||
Vanilla example and Storybook `orgchart--full-composition`. Plain `p1.html`
|
||||
retains the existing interactive mode. The query changes presentation only.
|
||||
All displayed names are `홍 길 동`; the employee fixture and aggregation remain
|
||||
unchanged. Department leaders use card assets by actual ancestry depth.
|
||||
|
||||
After building the Core, preparing the HTML example and building this consumer,
|
||||
run `node examples/org-chart/verify-p2.mjs` from the repository root. Set
|
||||
`ORGCHART_STORYBOOK_URL` to the current Storybook server when checking dev.
|
||||
The focused verifier records screenshots and measurements in
|
||||
`test-results/org-chart-p2`; it preserves existing v0/P1 test expectations.
|
||||
@@ -0,0 +1,91 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { OrgChart, type OrgChartProps } from '@hm-design/org-chart-react';
|
||||
import { JsonOrgChartDataSource, type OrgChartDataSource, type OrgChartState } 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 v1 from '../org-chart/data-v1.json';
|
||||
import v2 from '../org-chart/data-v2.json';
|
||||
import './index.css';
|
||||
|
||||
// An independent production-built consumer of public package entries. This
|
||||
// inspection seam exercises React Props only; it never reaches into the engine.
|
||||
const events: { instance: string; callback: number; state: OrgChartState }[] = [];
|
||||
const lifecycleErrors: string[] = [];
|
||||
const requests: Record<string, { loads: number; aborts: number }> = {};
|
||||
function track(name: string, source: OrgChartDataSource): OrgChartDataSource {
|
||||
requests[name] = { loads: 0, aborts: 0 };
|
||||
return {
|
||||
load(options) {
|
||||
requests[name].loads++;
|
||||
options?.signal?.addEventListener('abort', () => { requests[name].aborts++; }, { once: true });
|
||||
return source.load(options);
|
||||
},
|
||||
};
|
||||
}
|
||||
const noEmployees = structuredClone(v1);
|
||||
noEmployees.employees = [];
|
||||
for (const organization of noEmployees.organizations) organization.leaderId = null;
|
||||
const noOrganizations = structuredClone(v1);
|
||||
noOrganizations.organizations = [];
|
||||
const sources = {
|
||||
v1: track('v1', new JsonOrgChartDataSource(v1)),
|
||||
v2: track('v2', new JsonOrgChartDataSource(v2)),
|
||||
zeroEmployees: track('zeroEmployees', new JsonOrgChartDataSource(noEmployees)),
|
||||
zeroOrganizations: track('zeroOrganizations', new JsonOrgChartDataSource(noOrganizations)),
|
||||
error: track('error', { async load() { throw new Error('Fictitious source failure'); } }),
|
||||
};
|
||||
let releaseSlow: (() => void) | undefined;
|
||||
const slowSource = track('slow', {
|
||||
load() { return new Promise(resolve => { releaseSlow = () => resolve(structuredClone(v1)); }); },
|
||||
});
|
||||
type Configuration = {
|
||||
sourceA: OrgChartDataSource;
|
||||
sourceB: OrgChartDataSource;
|
||||
mountedA: boolean;
|
||||
mountedB: boolean;
|
||||
callback: number;
|
||||
aggregationPolicy: OrgChartProps['aggregationPolicy'];
|
||||
stylesheetUrls: OrgChartProps['stylesheetUrls'];
|
||||
className: string;
|
||||
};
|
||||
let configuration: Configuration = {
|
||||
sourceA: sources.v1, sourceB: sources.v2, mountedA: true, mountedB: false,
|
||||
callback: 1, aggregationPolicy: undefined,
|
||||
stylesheetUrls: { engine: engineUrl, core: coreUrl }, className: 'consumer-chart',
|
||||
};
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
function render(patch: Partial<Configuration> = {}) {
|
||||
configuration = { ...configuration, ...patch };
|
||||
const callback = configuration.callback;
|
||||
const chart = (instance: 'A' | 'B', source: OrgChartDataSource) => (
|
||||
<section key={instance} aria-label={`OrgChart ${instance}`} data-instance={instance}>
|
||||
<OrgChart
|
||||
dataSource={source}
|
||||
aggregationPolicy={configuration.aggregationPolicy}
|
||||
stylesheetUrls={configuration.stylesheetUrls}
|
||||
className={configuration.className}
|
||||
onStateChange={state => { events.push({ instance, callback, state }); }}
|
||||
onLifecycleError={error => { lifecycleErrors.push(error instanceof Error ? error.message : String(error)); }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<p className="consumer-note">가상 JSON 데이터 · React Strict Mode · 공개 OrgChart 패키지 사용</p>
|
||||
<div className="consumer-charts">
|
||||
{configuration.mountedA && chart('A', configuration.sourceA)}
|
||||
{configuration.mountedB && chart('B', configuration.sourceB)}
|
||||
</div>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
const demo = {
|
||||
render, sources, slowSource, events, lifecycleErrors, requests,
|
||||
releaseSlow: () => releaseSlow?.(),
|
||||
get configuration() { return configuration; },
|
||||
destroy: () => root.unmount(),
|
||||
};
|
||||
declare global { interface Window { OrgChartReactDemo: typeof demo } }
|
||||
window.OrgChartReactDemo = demo;
|
||||
render();
|
||||
@@ -0,0 +1,2 @@
|
||||
@import './external-theme.css';
|
||||
:host { --hm-orgchart-group-title-background: #613e8b; }
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Served to stylesheetUrls.core: imports the complete default package stylesheet.
|
||||
Vite resolves this public export; plain servers use their copied /assets/style.css.
|
||||
To change a theme, edit only values in this file. No !important or inline colors. */
|
||||
@import '@hm-design/org-chart/style.css';
|
||||
:host {
|
||||
--hm-orgchart-bg-surface: #edf4ff;
|
||||
--hm-orgchart-bg-content: #dce8f8;
|
||||
--hm-orgchart-fg-primary: #172b4d;
|
||||
--hm-orgchart-stroke-default: #8b9bb0;
|
||||
--hm-orgchart-company-hanmac: #2457a7;
|
||||
--hm-orgchart-company-saman: #385ca8;
|
||||
--hm-orgchart-company-jangheon-industry: #14786b;
|
||||
--hm-orgchart-company-jangheon: #503a91;
|
||||
--hm-orgchart-company-ptc: #755024;
|
||||
--hm-orgchart-company-halla: #207879;
|
||||
--hm-orgchart-company-baron: #4d6295;
|
||||
--hm-orgchart-group-title-color: #fff4d6;
|
||||
--hm-orgchart-card-background: #f4f8ff;
|
||||
--hm-orgchart-leader-card-background: #e3eeff;
|
||||
--hm-orgchart-top-leader-card-background: #d2e3ff;
|
||||
--hm-orgchart-card-border: #527aa9;
|
||||
--hm-orgchart-card-hover-background: #c1d8f8;
|
||||
--hm-orgchart-card-focus-color: #7650a5;
|
||||
--hm-orgchart-expanded-line-color: #249a79;
|
||||
--hm-orgchart-fixed-line-color: #297ab0;
|
||||
--hm-orgchart-business-management-gradient: linear-gradient(180deg, #2457a7, #123568);
|
||||
--hm-orgchart-business-management-border: #2457a7;
|
||||
--hm-orgchart-business-planning-gradient: linear-gradient(180deg, #385ca8, #23417f);
|
||||
--hm-orgchart-business-planning-border: #385ca8;
|
||||
--hm-orgchart-dispatched-card-gradient: linear-gradient(180deg, #e9f9f0, #a9d8bd);
|
||||
--hm-orgchart-dispatched-card-border: #278857;
|
||||
--hm-orgchart-dispatched-card-color: #145d39;
|
||||
--hm-orgchart-dispatched-title-background: linear-gradient(180deg, #328c61, #1e5b3d);
|
||||
--hm-orgchart-on-leave-card-gradient: linear-gradient(180deg, #f7efff, #d6bbeb);
|
||||
--hm-orgchart-on-leave-card-border: #8050a1;
|
||||
--hm-orgchart-on-leave-card-color: #603080;
|
||||
--hm-orgchart-on-leave-title-background: linear-gradient(180deg, #8d66b3, #573771);
|
||||
--hm-orgchart-dialog-background: #eef2ff;
|
||||
--hm-orgchart-dialog-color: #283568;
|
||||
--hm-orgchart-dialog-border: #6879ba;
|
||||
--hm-orgchart-dialog-close-color: #36478a;
|
||||
--hm-orgchart-dialog-close-hover-background: #dce2f9;
|
||||
--hm-orgchart-dialog-focus-color: #8958ba;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Loaded by the host document. Colors inherit; Shadow selectors stay private. */
|
||||
.orgchart-custom-theme {
|
||||
--hm-orgchart-company-hanmac: #2457a7;
|
||||
--hm-orgchart-company-saman: #385ca8;
|
||||
--hm-orgchart-expanded-line-color: #249a79;
|
||||
--hm-orgchart-bg-surface: #edf4ff;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@import "@hm-design/ui/theme.css";
|
||||
|
||||
/* Consumer content geometry; the chart owns its internal scroll container. */
|
||||
body { margin: 0; background: var(--hm-sem-bg-content); color: var(--hm-sem-fg-primary); font-family: var(--hm-font-family-base); }
|
||||
main { padding: 16px; }
|
||||
.consumer-charts { display: grid; gap: 24px; min-width: 0; }
|
||||
.consumer-charts > section { min-width: 0; }
|
||||
.consumer-chart { min-width: 0; max-width: 100%; }
|
||||
.consumer-note { margin: 0 0 16px; }
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>HM OrgChart React Consumer</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="root"></main>
|
||||
<script type="module" src="./app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Bundler resolves the same font used by HTML and the HM Storybook host. */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans KR';
|
||||
src: url('../../public/fonts/NotoSansKR-variable.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
body { margin: 0; background: #ffffff; }
|
||||
main { padding: 16px; }
|
||||
.consumer-chart { min-width: 0; }
|
||||
html[data-orgchart-mode="readOnly"] body { background: #F5F8FB; }
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HM OrgChart P1 · React Full Composition</title><link rel="icon" href="data:,"></head>
|
||||
<body><main><div id="root"></div></main><script type="module" src="./p1.tsx"></script></body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { OrgChart, type OrgChartProps } from '@hm-design/org-chart-react';
|
||||
import { JsonOrgChartDataSource, type OrgChartState } 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 fixture from '../org-chart/data-p1.json';
|
||||
import './p1.css';
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
const events: OrgChartState[] = [];
|
||||
const errors: string[] = [];
|
||||
const displayMode = new URLSearchParams(location.search).get('mode') === 'readOnly' ? 'readOnly' : 'interactive';
|
||||
document.documentElement.dataset.orgchartMode = displayMode;
|
||||
type QueryOptions = Pick<OrgChartProps, 'organizationLinks' | 'onEmployeeClick'>;
|
||||
let queryOptions: QueryOptions = {};
|
||||
let currentData: unknown = fixture;
|
||||
function setData(data: unknown) {
|
||||
currentData = data;
|
||||
root.render(<StrictMode><OrgChart
|
||||
dataSource={new JsonOrgChartDataSource(data)} stylesheetUrls={{ engine: engineUrl, core: coreUrl }} className="consumer-chart"
|
||||
displayMode={displayMode}
|
||||
{...queryOptions}
|
||||
onStateChange={state => events.push(state)} onLifecycleError={error => errors.push(String(error))}
|
||||
/></StrictMode>);
|
||||
}
|
||||
// Optional public-prop seam for consumer integration checks; no URLs are preset.
|
||||
function setOptions(options: QueryOptions) { queryOptions = options; setData(currentData); }
|
||||
const demo = { data: fixture, events, errors, setData, setOptions, destroy: () => root.unmount() };
|
||||
declare global { interface Window { OrgChartP1React: typeof demo } }
|
||||
window.OrgChartP1React = demo;
|
||||
setData(fixture);
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:,"><title>OrgChart P11 theme verification</title><body><main id="root"></main><script type="module" src="./p11.tsx"></script></body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
// Isolated consumer fixture; no Storybook registration or production controls.
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { OrgChart } from '@hm-design/org-chart-react';
|
||||
import { createOrgChart, JsonOrgChartDataSource } from '@hm-design/org-chart';
|
||||
import coreUrl from '@hm-design/org-chart/style.css?url';
|
||||
import engineUrl from '@hm-design/org-chart/engine.css?url';
|
||||
import externalThemeUrl from './external-theme.css?url';
|
||||
import hostThemeUrl from './host-theme.css?url';
|
||||
import groupThemeUrl from './external-group-theme.css?url';
|
||||
import autoData from '../org-chart/data-expanded-small.json';
|
||||
import allCompanies from '../org-chart/data-expanded-large.json';
|
||||
import fixedData from '../org-chart/data-p1.json';
|
||||
|
||||
const query = new URLSearchParams(location.search);
|
||||
const mode = query.get('mode') ?? 'auto';
|
||||
const data = structuredClone(mode === 'auto' ? query.has('all') ? allCompanies : autoData : fixedData);
|
||||
if (mode === 'auto' && query.has('statuses')) {
|
||||
data.employees.filter(employee => !data.organizations.some(org => org.leaderId === employee.id)).slice(0, 2).forEach((employee, index) => { employee.workStatus = index ? 'onLeave' : 'dispatched'; });
|
||||
}
|
||||
const options = {
|
||||
dataSource: new JsonOrgChartDataSource(data),
|
||||
dragDropEnabled: query.has('drag'),
|
||||
stylesheetUrls: { core: query.has('groups') ? groupThemeUrl : query.has('theme') ? externalThemeUrl : coreUrl, engine: engineUrl },
|
||||
...(mode === 'auto' ? { layoutMode: 'auto' as const, presentationMode: 'chart-only' as const, initialView: 'fit' as const, wheelZoom: 'direct' as const } : { displayMode: mode === 'fixed' ? 'readOnly' as const : 'interactive' as const }),
|
||||
};
|
||||
document.body.style.margin = '0';
|
||||
Object.assign(window, { p11HostThemeUrl: hostThemeUrl });
|
||||
const container = document.getElementById('root')!;
|
||||
container.style.height = '850px';
|
||||
if (query.get('runtime') === 'vanilla') {
|
||||
const chart = createOrgChart({ container, ...options });
|
||||
void chart.mount();
|
||||
Object.assign(window, { p11Chart: chart });
|
||||
} else createRoot(container).render(<OrgChart {...options} />);
|
||||
@@ -0,0 +1,73 @@
|
||||
import { loadPlaywright, launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
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';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
await loadPlaywright();
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url, 'http://localhost');
|
||||
const file = path.resolve(root, 'examples/org-chart-react/dist', '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname));
|
||||
const base = path.join(root, 'examples/org-chart-react/dist');
|
||||
const relative = path.relative(base, file);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) { response.writeHead(403).end(); return; }
|
||||
response.setHeader('Content-Type', { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.ttf': 'font/ttf' }[path.extname(file)] || 'application/octet-stream');
|
||||
response.end(await readFile(file));
|
||||
} catch { response.writeHead(404).end(); }
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const browser = await launchChromium({ headless: true });
|
||||
const results = [], errors = [];
|
||||
const check = (condition, name, detail) => { results.push({ name, status: condition ? 'PASS' : 'FAIL', detail }); assert.ok(condition, name); };
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 390, height: 900 } });
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
page.on('console', message => { if(message.type() === 'error') errors.push(message.text()); });
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}`);
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo?.events.at(-1)?.state.status === 'ready');
|
||||
check(await page.evaluate(() => window.OrgChartReactDemo.requests.v1.loads === 1 && window.OrgChartReactDemo.events.filter(event => event.state.status === 'ready').length === 1), 'production mount has one initial load and one ready callback');
|
||||
const layout = await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport');
|
||||
viewport.scrollLeft = viewport.scrollWidth - viewport.clientWidth;
|
||||
return { width: document.documentElement.clientWidth, documentScrollWidth: document.documentElement.scrollWidth, internalScrollLeft: viewport.scrollLeft };
|
||||
});
|
||||
check(layout.documentScrollWidth <= 390 && layout.internalScrollLeft > 0, 'production 390px page containment and actual internal horizontal scrolling', layout);
|
||||
const pollution = await page.evaluate(() => {
|
||||
const container = document.createElement('div'), node = document.createElement('div');
|
||||
node.textContent = 'Light DOM CSS isolation probe'; container.append(node); document.body.append(container);
|
||||
const read = () => { const style = getComputedStyle(node); return { width: style.width, margin: style.margin, background: style.backgroundColor, textAlign: style.textAlign, border: style.border, font: style.fontFamily }; };
|
||||
const before = read(); container.className = 'orgchart'; node.className = 'node'; const after = read(); container.remove();
|
||||
return { before, after };
|
||||
});
|
||||
check(JSON.stringify(pollution.before) === JSON.stringify(pollution.after), 'Engine selectors do not pollute light DOM', pollution);
|
||||
const hostId = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance');
|
||||
await page.evaluate(async () => {
|
||||
const demo = window.OrgChartReactDemo;
|
||||
const next = await demo.sources.v2.load();
|
||||
demo.sources.v1.load = async () => structuredClone(next);
|
||||
demo.render({ callback: 9, className: 'consumer-chart boundary' });
|
||||
});
|
||||
await page.waitForFunction(() => document.querySelector('.consumer-chart.boundary'));
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total === 4), 'same source internal mutation and unrelated render do not imply reload');
|
||||
await page.evaluate(() => { const demo = window.OrgChartReactDemo; demo.render({ sourceA: { load: options => demo.sources.v1.load(options) } }); });
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1)?.state.dataVersion === 'mock-2');
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total === 5 && window.OrgChartReactDemo.events.at(-1).callback === 9), 'production source identity update uses same Core with new 5-person summary and latest callback');
|
||||
await page.evaluate(() => window.OrgChartReactDemo.render({ mountedA: false }));
|
||||
await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host'));
|
||||
check(await page.locator('.hm-orgchart-host').count() === 0, 'production unmount removes Core');
|
||||
await page.goto(pathToFileURL(path.join(root, 'examples/org-chart/dist/classic.html')).href);
|
||||
await page.waitForFunction(() => window.orgChartReady);
|
||||
check(await page.evaluate(() => window.OrgChartDemo.chart.getState().summary.raw.total === 4), 'existing direct-open Vanilla classic remains ready with common v1 total 4');
|
||||
await page.evaluate(() => window.OrgChartDemo.update());
|
||||
check(await page.evaluate(() => window.OrgChartDemo.chart.getState().summary.raw.total === 5), 'existing direct-open Vanilla classic v2 update remains total 5');
|
||||
check(errors.length === 0, 'boundary browser checks have zero console/runtime errors', errors);
|
||||
} catch(error) {
|
||||
results.push({ name: 'verification completion', status: 'FAIL', detail: error.stack }); process.exitCode = 1;
|
||||
} finally {
|
||||
await writeFile(path.join(root, 'test-results/org-chart-react/boundaries.json'), JSON.stringify({ browser: await browser.version(), results, errors }, null, 2));
|
||||
await browser.close(); await new Promise(resolve => server.close(resolve));
|
||||
console.log(JSON.stringify({ pass: results.filter(result => result.status === 'PASS').length, fail: results.filter(result => result.status === 'FAIL').length, errors }));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import assert from 'node:assert/strict';
|
||||
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
const output = path.resolve('test-results/org-chart-p11');
|
||||
const browser = await launchChromium({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const errors = [], checks = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
try {
|
||||
for (const [id, selector] of [
|
||||
['full-composition', '.hm-orgchart-composition[data-display-mode="readOnly"]'],
|
||||
['expanded-org-chart', '.hm-orgchart-expanded'],
|
||||
['department-colors', '.hm-orgchart-department-colors'],
|
||||
['logo-personnel-summary', '.hm-orgchart-affiliation-mark'],
|
||||
]) {
|
||||
await page.goto(`http://127.0.0.1:6034/iframe.html?id=orgchart--${id}&viewMode=story`);
|
||||
await page.locator(selector).first().waitFor();
|
||||
await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => setTimeout(resolve, 800)); });
|
||||
if (id === 'department-colors') {
|
||||
const gradients = await page.locator('.hm-orgchart-color-preview .hm-orgchart-color-department-name').evaluateAll(headings => headings.map(heading => getComputedStyle(heading).backgroundImage));
|
||||
assert.equal(gradients.length, 6);
|
||||
assert.ok(gradients.every(gradient => gradient !== 'none'));
|
||||
assert.equal(new Set(gradients).size, 6);
|
||||
}
|
||||
await page.screenshot({ path: path.join(output, `storybook-${id}.png`) });
|
||||
checks.push({ name: id, status: 'PASS' });
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
} finally {
|
||||
await fs.writeFile(path.join(output, 'storybook-result.json'), JSON.stringify({ checks, errors }, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
console.log(`Storybook: ${checks.length} checks, ${errors.length} errors`);
|
||||
@@ -0,0 +1,184 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
|
||||
const output = path.resolve('test-results/org-chart-p11');
|
||||
await fs.mkdir(output, { recursive: true });
|
||||
const browser = await launchChromium({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const errors = [], checks = [], measurements = {};
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
const baseline = process.argv.includes('--baseline');
|
||||
const origin = process.env.ORGCHART_P11_ORIGIN ?? 'http://127.0.0.1:6026';
|
||||
async function open(runtime, mode, theme = false, extra = '') {
|
||||
await page.goto(`${origin}/p11.html?runtime=${runtime}&mode=${mode}${theme ? '&theme=1' : ''}${extra}`);
|
||||
await page.locator('.hm-orgchart-employee').first().waitFor();
|
||||
await page.waitForFunction(() => [...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('link')].every(link => link.sheet));
|
||||
await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => setTimeout(resolve, 600)); });
|
||||
}
|
||||
async function styles() {
|
||||
return page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const selectors = '.hm-orgchart-shell,.hm-orgchart-viewport,.hm-orgchart-organization-name,.hm-orgchart-employee,.hm-orgchart-status-group > .hm-orgchart-summary-title,.hm-orgchart-employee-dialog,.hm-orgchart-employee-dialog-close';
|
||||
const fields = ['backgroundColor', 'backgroundImage', 'color', 'borderTopColor', 'borderRightColor', 'borderTopWidth', 'boxShadow', 'width', 'height', 'fontSize', 'padding', 'borderRadius'];
|
||||
return [...shadow.querySelectorAll(selectors)].map(el => ({ tag: el.tagName, class: el.className, company: el.dataset.companyId ?? el.closest('[data-company-id]')?.dataset.companyId, kind: el.dataset.cardKind, status: el.dataset.workStatus, css: Object.fromEntries(fields.map(field => [field, getComputedStyle(el)[field]])) }));
|
||||
});
|
||||
}
|
||||
async function check(name, run) { await run(); checks.push({ name, status: 'PASS' }); }
|
||||
try {
|
||||
for (const runtime of ['vanilla', 'react']) for (const mode of ['auto', 'fixed', 'interactive']) {
|
||||
const key = `${runtime}-${mode}`;
|
||||
await open(runtime, mode);
|
||||
measurements[key] = await styles();
|
||||
await page.screenshot({ path: path.join(output, `${baseline ? 'before' : 'after'}-${key}.png`) });
|
||||
if (!baseline) {
|
||||
const before = JSON.parse(await fs.readFile(path.join(output, 'baseline.json'), 'utf8'));
|
||||
await check(`${key}: default colors and geometry identical`, () => assert.equal(JSON.stringify(measurements[key]), JSON.stringify(before[key])));
|
||||
}
|
||||
// Popup is captured separately so it cannot obscure the chart screenshot.
|
||||
await page.locator('.hm-orgchart-employee').first().click();
|
||||
await page.locator('.hm-orgchart-employee-dialog[open]').waitFor();
|
||||
measurements[`${key}-popup`] = (await styles()).filter(row => row.class.includes('dialog'));
|
||||
if (!baseline) {
|
||||
const before = JSON.parse(await fs.readFile(path.join(output, 'baseline.json'), 'utf8'));
|
||||
await check(`${key}: default popup identical`, () => assert.equal(JSON.stringify(measurements[`${key}-popup`]), JSON.stringify(before[`${key}-popup`])));
|
||||
}
|
||||
}
|
||||
if (baseline) await fs.writeFile(path.join(output, 'baseline.json'), JSON.stringify(measurements, null, 2));
|
||||
else {
|
||||
const expectedCompanies = { hanmac: 'rgb(36, 87, 167)', saman: 'rgb(56, 92, 168)', 'jangheon-industry': 'rgb(20, 120, 107)' };
|
||||
const value = (selector, property, pseudo = null) => page.locator(selector).first().evaluate((el, { property, pseudo }) => getComputedStyle(el, pseudo)[property], { property, pseudo });
|
||||
for (const runtime of ['vanilla', 'react']) {
|
||||
await open(runtime, 'auto', true);
|
||||
for (const [id, color] of Object.entries(expectedCompanies)) {
|
||||
await check(`${runtime}: external CSS ${id} title`, async () => assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color));
|
||||
await check(`${runtime}: external CSS ${id} card accent`, async () => assert.equal(await value(`.hm-orgchart-employee[data-company-id="${id}"]`, 'borderTopColor'), color));
|
||||
}
|
||||
await check(`${runtime}: canvas and title text`, async () => {
|
||||
assert.equal(await value('.hm-orgchart-viewport', 'backgroundColor'), 'rgb(237, 244, 255)');
|
||||
assert.equal(await value('.hm-orgchart-organization-name', 'color'), 'rgb(255, 244, 214)');
|
||||
});
|
||||
await check(`${runtime}: independent card roles and border`, async () => {
|
||||
assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'backgroundColor'), 'rgb(244, 248, 255)');
|
||||
assert.equal(await value('.hm-orgchart-employee[data-card-kind="topLeader"]', 'backgroundColor'), 'rgb(210, 227, 255)');
|
||||
assert.equal(await value('.hm-orgchart-employee', 'borderRightColor'), 'rgb(82, 122, 169)');
|
||||
});
|
||||
await check(`${runtime}: employee and organization connector theme`, async () => {
|
||||
assert.equal(await value('.hm-orgchart-employee-connectors path', 'stroke'), 'rgb(36, 154, 121)');
|
||||
assert.equal(await value('.hm-orgchart-engine > ul > li > ul li > .node', 'backgroundColor', '::before'), 'rgb(36, 154, 121)');
|
||||
});
|
||||
const employee = page.locator('.hm-orgchart-employee[data-card-kind="employee"]').first();
|
||||
await employee.hover();
|
||||
await check(`${runtime}: actual card hover`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'backgroundColor'), 'rgb(193, 216, 248)'));
|
||||
await page.mouse.move(0, 0); await employee.focus();
|
||||
await check(`${runtime}: actual card focus`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'outlineColor'), 'rgb(118, 80, 165)'));
|
||||
await employee.press('Enter');
|
||||
await page.locator('.hm-orgchart-employee-dialog[open]').waitFor();
|
||||
await check(`${runtime}: Core popup background, text and border`, async () => {
|
||||
assert.equal(await value('.hm-orgchart-employee-dialog', 'backgroundColor'), 'rgb(238, 242, 255)');
|
||||
assert.equal(await value('.hm-orgchart-employee-dialog', 'color'), 'rgb(40, 53, 104)');
|
||||
assert.equal(await value('.hm-orgchart-employee-dialog', 'borderTopColor'), 'rgb(104, 121, 186)');
|
||||
});
|
||||
const close = page.locator('.hm-orgchart-employee-dialog-close');
|
||||
await close.hover(); await page.waitForTimeout(200);
|
||||
await check(`${runtime}: popup hover`, async () => assert.equal(await value('.hm-orgchart-employee-dialog-close', 'backgroundColor'), 'rgb(220, 226, 249)'));
|
||||
await page.mouse.move(0, 0); await close.focus(); await page.waitForTimeout(200);
|
||||
await check(`${runtime}: popup focus`, async () => assert.equal(await value('.hm-orgchart-employee-dialog-close', 'borderTopColor'), 'rgb(137, 88, 186)'));
|
||||
await page.keyboard.press('Escape');
|
||||
await check(`${runtime}: popup Escape and focus restoration`, async () => assert.equal(await employee.evaluate(el => el.getRootNode().activeElement === el), true));
|
||||
await page.screenshot({ path: path.join(output, `custom-${runtime}-auto.png`) });
|
||||
|
||||
await open(runtime, 'fixed', true);
|
||||
await check(`${runtime}: Fixed headers and background`, async () => {
|
||||
assert.match(await value('[data-group-variant="0"] .hm-orgchart-organization-name', 'backgroundImage'), /rgb\(36, 87, 167\)/);
|
||||
assert.equal(await value('.hm-orgchart-composition', 'backgroundColor'), 'rgb(220, 232, 248)');
|
||||
assert.equal(await value('.hm-orgchart-workdesk-overlay path[data-connector]', 'stroke'), 'rgb(41, 122, 176)');
|
||||
});
|
||||
for (const [status, color] of [['dispatched', 'rgb(39, 136, 87)'], ['onLeave', 'rgb(128, 80, 161)']]) {
|
||||
await check(`${runtime}: ${status} external gradient and border`, async () => {
|
||||
const selector = `.hm-orgchart-status-group[data-work-status="${status}"] .hm-orgchart-employee`;
|
||||
assert.equal(await value(selector, 'borderTopColor'), color);
|
||||
assert.notEqual(await value(selector, 'backgroundImage'), 'none');
|
||||
});
|
||||
}
|
||||
await page.screenshot({ path: path.join(output, `custom-${runtime}-fixed.png`) });
|
||||
|
||||
await open(runtime, 'auto');
|
||||
const unthemed = await styles();
|
||||
await page.evaluate(() => {
|
||||
const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = window.p11HostThemeUrl; link.id = 'host-theme'; document.head.append(link);
|
||||
document.querySelector('#root').classList.add('orgchart-custom-theme');
|
||||
});
|
||||
await page.waitForFunction(() => document.querySelector('#host-theme').sheet);
|
||||
await check(`${runtime}: host CSS crosses Shadow boundary without inline conflict`, async () => {
|
||||
assert.equal(await value('.node[data-company-id="hanmac"] .hm-orgchart-organization-name', 'backgroundColor'), expectedCompanies.hanmac);
|
||||
assert.equal(await value('.node[data-company-id="saman"] .hm-orgchart-organization-name', 'backgroundColor'), expectedCompanies.saman);
|
||||
assert.equal(await value('.hm-orgchart-viewport', 'backgroundColor'), 'rgb(237, 244, 255)');
|
||||
});
|
||||
await page.evaluate(() => document.querySelector('#root').classList.remove('orgchart-custom-theme'));
|
||||
await check(`${runtime}: removing host theme restores defaults immediately`, async () => assert.equal(JSON.stringify(await styles()), JSON.stringify(unthemed)));
|
||||
|
||||
await open(runtime, 'auto', false, '&all=1');
|
||||
await check(`${runtime}: all seven approved default company colors`, async () => {
|
||||
for (const [id, color] of Object.entries({ hanmac: 'rgb(203, 46, 40)', saman: 'rgb(240, 131, 0)', 'jangheon-industry': 'rgb(69, 128, 166)', jangheon: 'rgb(29, 65, 141)', ptc: 'rgb(132, 51, 142)', halla: 'rgb(0, 104, 183)', baron: 'rgb(12, 54, 127)' })) assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color);
|
||||
});
|
||||
await open(runtime, 'auto', true, '&all=1&statuses=1&drag=1');
|
||||
for (const [id, color] of Object.entries({ ...expectedCompanies, jangheon: 'rgb(80, 58, 145)', ptc: 'rgb(117, 80, 36)', halla: 'rgb(32, 120, 121)', baron: 'rgb(77, 98, 149)' })) {
|
||||
await check(`${runtime}: seven-company theme ${id}`, async () => assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color));
|
||||
}
|
||||
await check(`${runtime}: Auto status cards use external gradients and borders`, async () => {
|
||||
assert.equal(await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'borderRightColor'), 'rgb(39, 136, 87)');
|
||||
assert.match(await value('.hm-orgchart-employee[data-work-status="onLeave"]', 'backgroundImage'), /rgb\(214, 187, 235\)/);
|
||||
});
|
||||
await check(`${runtime}: group leader has an independent background`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="leader"]', 'backgroundColor'), 'rgb(227, 238, 255)'));
|
||||
await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const removeHover = rules => { for (const rule of rules) { rule.style?.removeProperty('--hm-orgchart-card-hover-background'); if (rule.styleSheet) removeHover(rule.styleSheet.cssRules); if (rule.cssRules) removeHover(rule.cssRules); } };
|
||||
for (const link of shadow.querySelectorAll('link')) removeHover(link.sheet.cssRules);
|
||||
});
|
||||
const statusCard = page.locator('.hm-orgchart-employee[data-work-status="dispatched"]').first();
|
||||
const statusGradient = await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'backgroundImage');
|
||||
await statusCard.hover();
|
||||
await check(`${runtime}: unspecified hover preserves status gradient`, async () => assert.equal(await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'backgroundImage'), statusGradient));
|
||||
await check(`${runtime}: Drag & Drop ON`, async () => assert.equal(await page.locator('.hm-orgchart-viewport').getAttribute('data-drag-drop-enabled'), 'true'));
|
||||
await open(runtime, 'auto', true);
|
||||
await check(`${runtime}: Drag & Drop OFF`, async () => assert.equal(await page.locator('.hm-orgchart-viewport').getAttribute('data-drag-drop-enabled'), 'false'));
|
||||
await open(runtime, 'auto', true, '&groups=1');
|
||||
await check(`${runtime}: independent group background via external CSS file`, async () => assert.equal(await value('.hm-orgchart-organization-name', 'backgroundColor'), 'rgb(97, 62, 139)'));
|
||||
await open(runtime, 'auto', true);
|
||||
const forest = page.locator('.hm-orgchart-forest');
|
||||
const transform = await forest.evaluate(el => el.style.transform);
|
||||
const viewport = await page.locator('.hm-orgchart-viewport').boundingBox();
|
||||
const mouseX = viewport.x + viewport.width - 12, mouseY = viewport.y + viewport.height - 12;
|
||||
await page.mouse.move(mouseX, mouseY); await page.mouse.wheel(0, -150); await page.waitForTimeout(150);
|
||||
await check(`${runtime}: direct wheel zoom`, async () => assert.notEqual(await forest.evaluate(el => el.style.transform), transform));
|
||||
const zoomed = await forest.evaluate(el => el.style.transform);
|
||||
await page.mouse.move(mouseX, mouseY); await page.mouse.down(); await page.mouse.move(mouseX - 50, mouseY - 50, { steps: 5 }); await page.mouse.up();
|
||||
await check(`${runtime}: pan`, async () => assert.notEqual(await forest.evaluate(el => el.style.transform), zoomed));
|
||||
await open(runtime, 'auto', true);
|
||||
const collapse = page.locator('.hm-orgchart-expanded-toggle').first();
|
||||
await collapse.click();
|
||||
await check(`${runtime}: collapse`, async () => assert.equal(await page.locator('.hm-orgchart-expanded-toggle').first().getAttribute('aria-expanded'), 'false'));
|
||||
await page.locator('.hm-orgchart-expanded-toggle').first().click();
|
||||
await check(`${runtime}: expand`, async () => assert.equal(await page.locator('.hm-orgchart-expanded-toggle').first().getAttribute('aria-expanded'), 'true'));
|
||||
for (const width of [390, 768, 1280, 1440]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await open(runtime, 'auto', true);
|
||||
await check(`${runtime}: ${width}px canvas fits and document does not overflow`, async () => {
|
||||
const box = await page.locator('.hm-orgchart-viewport').boundingBox();
|
||||
assert.equal(box.width, width);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
|
||||
});
|
||||
await open(runtime, 'fixed', true);
|
||||
await check(`${runtime}: ${width}px Fixed document does not overflow`, async () => assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(errors, [], 'No browser errors');
|
||||
} finally {
|
||||
await fs.writeFile(path.join(output, baseline ? 'baseline-result.json' : 'browser-result.json'), JSON.stringify({ checks, errors, measurements }, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
console.log(`${baseline ? 'Baseline' : 'P11'}: ${checks.length} checks, ${errors.length} errors`);
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const consumer = path.join(root, 'test-results/org-chart-react/tarball-consumer');
|
||||
await mkdir(consumer, { recursive: true });
|
||||
for (const name of ['org-chart', 'org-chart-react']) {
|
||||
const destination = path.join(consumer, 'node_modules/@hm-design', name);
|
||||
await mkdir(destination, { recursive: true });
|
||||
execFileSync('tar.exe', ['-xzf', path.join(root, `test-results/hm-design-${name}-0.1.0.tgz`), '-C', destination, '--strip-components=1']);
|
||||
}
|
||||
await writeFile(path.join(consumer, 'package.json'), JSON.stringify({ name: 'org-chart-react-tarball-consumer', private: true, type: 'module' }));
|
||||
const require = createRequire(path.join(consumer, 'package.json'));
|
||||
const module = await import(pathToFileURL(require.resolve('@hm-design/org-chart-react')).href);
|
||||
assert.deepEqual(Object.keys(module), ['OrgChart']);
|
||||
assert.equal(typeof module.OrgChart, 'function');
|
||||
const manifest = JSON.parse(await readFile(path.join(consumer, 'node_modules/@hm-design/org-chart-react/package.json'), 'utf8'));
|
||||
assert.deepEqual(manifest.dependencies, { '@hm-design/org-chart': '0.1.0' });
|
||||
assert.deepEqual(manifest.peerDependencies, { react: '^19.0.0' });
|
||||
const bundle = await readFile(require.resolve('@hm-design/org-chart-react'), 'utf8');
|
||||
assert.ok(bundle.startsWith('"use client";'));
|
||||
for (const dependency of ['react', 'react/jsx-runtime', '@hm-design/org-chart']) assert.ok(bundle.includes(`from "${dependency}"`) || bundle.includes(`from '${dependency}'`));
|
||||
assert.ok(!bundle.includes('ManagedOrgChart') && !bundle.includes('validateOrgChartDataset') && !bundle.includes('JsonOrgChartDataSource'));
|
||||
await writeFile(path.join(consumer, 'consumer.tsx'), `import { OrgChart, type OrgChartProps, type OrgChartDataSource, type OrgChartState, type OrgChartDataset } from '@hm-design/org-chart-react';
|
||||
import { JsonOrgChartDataSource } from '@hm-design/org-chart';
|
||||
declare const data: OrgChartDataset;
|
||||
const source: OrgChartDataSource = new JsonOrgChartDataSource(data);
|
||||
const props: OrgChartProps = { dataSource: source, stylesheetUrls: { engine: './engine.css', core: './style.css' }, onStateChange(state: OrgChartState) { if(state.status === 'ready') void state.summary.raw.total; }, onLifecycleError(error: unknown) { void error; } };
|
||||
export const consumer = <OrgChart {...props} />;
|
||||
// @ts-expect-error dataSource is a required Core source interface
|
||||
export const missingSource = <OrgChart stylesheetUrls={{engine:'engine.css',core:'style.css'}} />;
|
||||
// @ts-expect-error wrapper does not expose engine controls
|
||||
export const unsupportedZoom = <OrgChart {...props} zoom />;
|
||||
// @ts-expect-error Core-owned container cannot be supplied through React Props
|
||||
export const unsupportedContainer = <OrgChart {...props} container={document.body} />;
|
||||
`);
|
||||
await writeFile(path.join(consumer, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', jsx: 'react-jsx', strict: true, noEmit: true, skipLibCheck: false, types: ['react'] }, include: ['consumer.tsx'] }));
|
||||
const localRequire = createRequire(import.meta.url);
|
||||
const typescriptManifest = localRequire.resolve('typescript/package.json');
|
||||
const typescript = JSON.parse(await readFile(typescriptManifest, 'utf8'));
|
||||
execFileSync(process.execPath, [path.resolve(path.dirname(typescriptManifest), typescript.bin.tsc), '-p', path.join(consumer, 'tsconfig.json')], { stdio: 'inherit' });
|
||||
const report = {
|
||||
status: 'PASS',
|
||||
checks: ['Actual tarball runtime/public export without DOM', 'Strict NodeNext declarations using extracted Core/Wrapper', 'Required source and unsupported Props type boundaries', 'React/Core remain external', 'ESM client boundary retained'],
|
||||
limitation: 'React and compiler supplied by verification environment; fresh remote-registry dependency installation and SSR/hydration are not verified.',
|
||||
};
|
||||
await writeFile(path.join(consumer, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report));
|
||||
@@ -0,0 +1,312 @@
|
||||
import { loadPlaywright, launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const output = path.join(root, 'test-results/org-chart-react');
|
||||
await mkdir(output, { recursive: true });
|
||||
await loadPlaywright();
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.ttf': 'font/ttf', '.woff2': 'font/woff2', '.svg': 'image/svg+xml' };
|
||||
async function serve(directory) {
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url, 'http://localhost');
|
||||
const file = path.resolve(directory, '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname));
|
||||
const relative = path.relative(directory, file);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) { response.writeHead(403).end(); return; }
|
||||
response.setHeader('Content-Type', mime[path.extname(file)] || 'application/octet-stream');
|
||||
response.end(await readFile(file));
|
||||
} catch { response.writeHead(404).end(); }
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
return { server, origin: `http://127.0.0.1:${server.address().port}` };
|
||||
}
|
||||
const consumer = await serve(path.join(root, 'examples/org-chart-react/dist'));
|
||||
const vanilla = await serve(path.join(root, 'examples/org-chart/dist'));
|
||||
const storybook = await serve(path.join(root, 'storybook-static'));
|
||||
const browser = await launchChromium({ headless: true });
|
||||
const results = [], errors = [], metrics = [], snapshots = {};
|
||||
function check(condition, name, detail) {
|
||||
results.push({ name, status: condition ? 'PASS' : 'FAIL', detail });
|
||||
assert.ok(condition, `${name}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
function captureErrors(page, transport) {
|
||||
page.on('pageerror', error => errors.push({ transport, type: 'pageerror', message: error.message }));
|
||||
page.on('console', message => { if (message.type() === 'error') errors.push({ transport, type: 'console', message: message.text() }); });
|
||||
}
|
||||
async function snapshot(page, selector = '.hm-orgchart-host') {
|
||||
return page.locator(selector).first().evaluate(host => {
|
||||
const shadow = host.shadowRoot;
|
||||
const nodes = [...shadow.querySelectorAll('.node')];
|
||||
return {
|
||||
organizations: nodes.map(node => ({
|
||||
id: node.dataset.organizationId,
|
||||
parentId: node.parentElement.parentElement.closest('.hierarchy')?.querySelector(':scope > .node')?.dataset.organizationId || null,
|
||||
employees: [...node.querySelectorAll('.hm-orgchart-employee')].map(employee => employee.dataset.employeeId),
|
||||
text: node.querySelector('.hm-orgchart-organization').textContent,
|
||||
})),
|
||||
summary: shadow.querySelector('.hm-orgchart-summary')?.textContent,
|
||||
heading: shadow.querySelector('.hm-orgchart-heading')?.textContent,
|
||||
css: [...shadow.querySelectorAll('link')].map(link => ({ href: link.href, loaded: !!link.sheet })),
|
||||
style: (() => { const s = getComputedStyle(shadow.querySelector('.hm-orgchart-organization')); return { color: s.color, background: s.backgroundColor, font: s.fontFamily, radius: s.borderRadius }; })(),
|
||||
};
|
||||
});
|
||||
}
|
||||
async function ready(page, instance = 'A', version) {
|
||||
await page.waitForFunction(({ instance, version }) => {
|
||||
const event = window.OrgChartReactDemo.events.filter(event => event.instance === instance).at(-1);
|
||||
const host = document.querySelector(`[data-instance="${instance}"] .hm-orgchart-host`);
|
||||
return !!host?.shadowRoot.querySelector('.hm-orgchart-shell[aria-busy="false"] .node') && event?.state.status === 'ready' && (!version || event.state.dataVersion === version);
|
||||
}, { instance, version });
|
||||
}
|
||||
async function patch(page, value) { await page.evaluate(value => window.OrgChartReactDemo.render(value), value); }
|
||||
async function source(page, name, instance = 'A') {
|
||||
await page.evaluate(({ name, instance }) => window.OrgChartReactDemo.render({ [`source${instance}`]: window.OrgChartReactDemo.sources[name] }), { name, instance });
|
||||
}
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
||||
await context.addInitScript(() => {
|
||||
const NativeObserver = window.MutationObserver;
|
||||
const observers = new Map();
|
||||
window.MutationObserver = class extends NativeObserver {
|
||||
observe(...args) { observers.set(this, args[0]); return super.observe(...args); }
|
||||
disconnect() { observers.delete(this); return super.disconnect(); }
|
||||
};
|
||||
window.chartObservers = () => [...observers.values()].filter(target => target.getRootNode()?.host?.classList.contains('hm-orgchart-host')).length;
|
||||
});
|
||||
const html = await context.newPage();
|
||||
captureErrors(html, 'vanilla-http');
|
||||
await html.goto(`${vanilla.origin}/index.html`);
|
||||
await html.waitForFunction(() => window.orgChartReady);
|
||||
snapshots.htmlV1 = await snapshot(html);
|
||||
const page = await context.newPage();
|
||||
captureErrors(page, 'react-production');
|
||||
if (process.env.ORGCHART_CONSUMER_URL) {
|
||||
const production = await context.newPage(); captureErrors(production, 'react-production');
|
||||
await production.goto(consumer.origin); await ready(production, 'A', 'mock-1');
|
||||
snapshots.reactProduction = await snapshot(production);
|
||||
check(JSON.stringify(snapshots.reactProduction.organizations) === JSON.stringify(snapshots.htmlV1.organizations) && snapshots.reactProduction.css.every(link => link.loaded), 'independent production React Consumer: public imports, assets and identical HTML nodes');
|
||||
await production.close();
|
||||
}
|
||||
await page.goto(process.env.ORGCHART_CONSUMER_URL || consumer.origin);
|
||||
await ready(page, 'A', 'mock-1');
|
||||
snapshots.reactV1 = await snapshot(page);
|
||||
check(JSON.stringify(snapshots.reactV1.organizations) === JSON.stringify(snapshots.htmlV1.organizations), 'HTML/React v1 hierarchy, employee placement and identical node UI', snapshots.reactV1.organizations);
|
||||
check(snapshots.reactV1.summary === snapshots.htmlV1.summary && snapshots.reactV1.heading === snapshots.htmlV1.heading, 'HTML/React v1 identical summary and presentation');
|
||||
check(JSON.stringify(snapshots.reactV1.style) === JSON.stringify(snapshots.htmlV1.style), 'HTML/React identical computed theme', snapshots.reactV1.style);
|
||||
check(snapshots.reactV1.css.length === 2 && snapshots.reactV1.css.every(link => link.loaded), 'production Shadow DOM CSS assets loaded', snapshots.reactV1.css);
|
||||
const initial = await page.evaluate(() => ({ events: window.OrgChartReactDemo.events, requests: window.OrgChartReactDemo.requests, observers: window.chartObservers() }));
|
||||
const expectedLoads = process.env.ORGCHART_CONSUMER_URL ? 2 : 1;
|
||||
check(initial.requests.v1.loads === expectedLoads && initial.events.filter(event => event.state.status === 'ready').length === 1, 'Strict Mode: expected initial load count and single ready (development replay; production single setup)', initial);
|
||||
check(await page.locator('.hm-orgchart-host').count() === 1, 'Strict Mode: exactly one active Core host');
|
||||
const hostId = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance');
|
||||
await patch(page, { callback: 2, className: 'consumer-chart changed' });
|
||||
await page.waitForFunction(() => document.querySelector('.consumer-chart.changed'));
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && (await page.evaluate(() => window.OrgChartReactDemo.requests.v1.loads)) === expectedLoads, 'callback/className-only render retains Core and does not reload');
|
||||
await source(page, 'v2');
|
||||
await ready(page, 'A', 'mock-2');
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId, 'source identity uses reload on the same Core host');
|
||||
const stateV2 = await page.evaluate(() => window.OrgChartReactDemo.events.at(-1));
|
||||
check(stateV2.callback === 2 && stateV2.state.summary.raw.total === 5 && stateV2.state.summary.raw.byOrganization['team-a'].direct === 1 && stateV2.state.summary.raw.byOrganization['team-b'].direct === 3, 'latest callback and v2 expected 1/1/3 placement, total 5', stateV2);
|
||||
await html.evaluate(() => window.OrgChartDemo.update());
|
||||
snapshots.htmlV2 = await snapshot(html);
|
||||
snapshots.reactV2 = await snapshot(page);
|
||||
check(JSON.stringify(snapshots.reactV2.organizations) === JSON.stringify(snapshots.htmlV2.organizations) && snapshots.reactV2.summary === snapshots.htmlV2.summary, 'HTML/React v2 data, hierarchy, employee nodes and aggregation parity');
|
||||
|
||||
for (const width of [390, 768, 1280, 1440]) {
|
||||
await page.setViewportSize({ width, height: 1100 });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
const metric = await page.locator('.hm-orgchart-host').evaluate(host => {
|
||||
const shadow = host.shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport');
|
||||
const rectangles = [...shadow.querySelectorAll('.node')].map(node => node.getBoundingClientRect());
|
||||
const overlap = rectangles.some((a, index) => rectangles.slice(index + 1).some(b => Math.min(a.right, b.right) - Math.max(a.left, b.left) > 1 && Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1));
|
||||
const organization = shadow.querySelector('.hm-orgchart-organization'), style = getComputedStyle(organization);
|
||||
const probe = document.createElement('div');
|
||||
probe.style.color = 'var(--hm-sem-fg-primary)'; probe.style.background = 'var(--hm-sem-bg-surface)'; probe.style.borderRadius = 'var(--hm-radius-container)'; shadow.append(probe);
|
||||
const expectedStyle = getComputedStyle(probe), expected = { color: expectedStyle.color, background: expectedStyle.backgroundColor, radius: expectedStyle.borderRadius };
|
||||
const match = expected.color === style.color && expected.background === style.backgroundColor && expected.radius === style.borderRadius;
|
||||
probe.remove();
|
||||
const nodes = [...shadow.querySelectorAll('.node')], rect = nodes[0].getBoundingClientRect();
|
||||
const centers = nodes.slice(1).map(node => { const r = node.getBoundingClientRect(); return r.x + r.width / 2; });
|
||||
return {
|
||||
document: { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, clientHeight: document.documentElement.clientHeight, scrollHeight: document.documentElement.scrollHeight },
|
||||
host: { clientWidth: host.clientWidth, scrollWidth: host.scrollWidth, clientHeight: host.clientHeight, scrollHeight: host.scrollHeight },
|
||||
viewport: { clientWidth: viewport.clientWidth, scrollWidth: viewport.scrollWidth, clientHeight: viewport.clientHeight, scrollHeight: viewport.scrollHeight, overflowX: getComputedStyle(viewport).overflowX },
|
||||
overlap, themeMatch: match, expected, color: style.color, background: style.backgroundColor, radius: style.borderRadius, font: style.fontFamily,
|
||||
fontLoaded: document.fonts.check('13px "Noto Sans KR"'), idWhiteSpace: getComputedStyle(shadow.querySelector('.hm-orgchart-domain-id')).whiteSpace,
|
||||
connectorsAligned: Math.abs(rect.x + rect.width / 2 - (centers[0] + centers[1]) / 2) < 2,
|
||||
connectorTheme: getComputedStyle(nodes[1], '::before').backgroundColor === getComputedStyle(viewport).borderTopColor,
|
||||
};
|
||||
});
|
||||
metrics.push({ width, ...metric });
|
||||
check(metric.document.scrollWidth <= width && metric.host.scrollWidth <= metric.host.clientWidth && metric.viewport.overflowX === 'auto' && !metric.overlap, `${width}px container overflow/scroll/no overlap`, metric);
|
||||
check(metric.themeMatch && metric.font.includes('Noto Sans KR') && metric.fontLoaded && metric.idWhiteSpace === 'nowrap' && metric.connectorsAligned && metric.connectorTheme, `${width}px theme/fonts/IDs/connectors`, metric);
|
||||
await page.screenshot({ path: path.join(output, `react-consumer-${width}.png`), fullPage: true });
|
||||
}
|
||||
const toggle = page.locator('.hm-orgchart-toggle').first();
|
||||
await toggle.focus(); await toggle.press('Enter');
|
||||
await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.sliding'));
|
||||
check(await toggle.getAttribute('aria-expanded') === 'false', 'React keyboard Enter collapses');
|
||||
await toggle.press('Space');
|
||||
await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.sliding'));
|
||||
check(await toggle.getAttribute('aria-expanded') === 'true', 'React keyboard Space expands');
|
||||
await source(page, 'v1'); await ready(page, 'A', 'mock-1');
|
||||
check(await page.locator('.hm-orgchart-viewport').evaluate(element => element.getRootNode().activeElement === element), 'source reload restores removed keyboard focus to stable viewport');
|
||||
|
||||
await page.evaluate(() => window.OrgChartReactDemo.render({ sourceA: window.OrgChartReactDemo.slowSource }));
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.requests.slow.loads === 1);
|
||||
await source(page, 'v2'); await ready(page, 'A', 'mock-2');
|
||||
const beforeLate = await page.evaluate(() => window.OrgChartReactDemo.events.length);
|
||||
await page.evaluate(() => window.OrgChartReactDemo.releaseSlow());
|
||||
await page.waitForTimeout(100);
|
||||
check(await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.dataVersion === 'mock-2' && window.OrgChartReactDemo.requests.slow.aborts === 1) && (await page.evaluate(() => window.OrgChartReactDemo.events.length)) === beforeLate, 'superseded abort-ignoring source cannot update DOM/callback');
|
||||
await source(page, 'error');
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.status === 'error');
|
||||
check(await page.locator('.hm-orgchart-issues').count() === 1, 'source failure controlled error state');
|
||||
await source(page, 'v1'); await ready(page, 'A', 'mock-1');
|
||||
check(await page.locator('.hm-orgchart-issues').count() === 0, 'Error to Ready recovery via source replacement');
|
||||
await source(page, 'zeroEmployees'); await ready(page);
|
||||
check((await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total)) === 0, 'zero employees remains Ready with total 0');
|
||||
await source(page, 'zeroOrganizations');
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.status === 'error');
|
||||
check(await page.locator('.hm-orgchart-issues').count() === 1, 'zero organizations is Error; no fabricated empty state');
|
||||
await source(page, 'v1'); await ready(page, 'A', 'mock-1');
|
||||
await page.evaluate(() => window.OrgChartReactDemo.render({ aggregationPolicy: { policyVersion: 'consumer-test', includedStatuses: ['active'], activeStatuses: ['active'], includeLeaders: true, membershipMode: 'primaryOnly' } }));
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.aggregation?.status === 'ready');
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') !== hostId && (await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.aggregation.counts.total)) === 2, 'policy change safely recreates Core and uses explicit test policy');
|
||||
let currentHost = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance');
|
||||
await page.evaluate(() => window.OrgChartReactDemo.render({ stylesheetUrls: { ...window.OrgChartReactDemo.configuration.stylesheetUrls } }));
|
||||
await page.waitForTimeout(70);
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === currentHost, 'equal stylesheet URL values retain Core');
|
||||
await page.evaluate(() => { const urls = window.OrgChartReactDemo.configuration.stylesheetUrls; window.OrgChartReactDemo.render({ stylesheetUrls: { engine: urls.engine + '?consumer=2', core: urls.core }, aggregationPolicy: undefined }); });
|
||||
await page.waitForFunction(oldId => document.querySelector('.hm-orgchart-host')?.dataset.orgchartInstance !== oldId, currentHost);
|
||||
await ready(page);
|
||||
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') !== currentHost && (await snapshot(page)).css.every(link => link.loaded), 'changed stylesheet URL safely recreates and loads assets');
|
||||
|
||||
await patch(page, { mountedB: true }); await ready(page, 'B', 'mock-2');
|
||||
const bHost = await page.locator('[data-instance="B"] .hm-orgchart-host').getAttribute('data-orgchart-instance');
|
||||
const bSnapshot = await snapshot(page, '[data-instance="B"] .hm-orgchart-host');
|
||||
await source(page, 'v2'); await ready(page, 'A', 'mock-2');
|
||||
check(await page.locator('.hm-orgchart-host').count() === 2 && JSON.stringify(await snapshot(page, '[data-instance="B"] .hm-orgchart-host')) === JSON.stringify(bSnapshot), 'two simultaneous instances; A update preserves B');
|
||||
await page.evaluate(() => { document.querySelector('[data-instance="A"]').style.setProperty('--hm-sem-bg-surface', getComputedStyle(document.documentElement).getPropertyValue('--hm-sem-bg-content')); });
|
||||
check((await snapshot(page, '[data-instance="A"] .hm-orgchart-host')).style.background !== bSnapshot.style.background && (await snapshot(page, '[data-instance="B"] .hm-orgchart-host')).style.background === bSnapshot.style.background, 'scoped existing Theme variable change inherits through Shadow DOM and isolates B');
|
||||
await patch(page, { mountedA: false });
|
||||
await page.waitForFunction(() => !document.querySelector('[data-instance="A"]'));
|
||||
check(await page.locator('.hm-orgchart-host').count() === 1 && await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === bHost, 'A unmount preserves B and its Core identity');
|
||||
const bObservers = await page.evaluate(() => window.chartObservers());
|
||||
for (let iteration = 0; iteration < 5; iteration++) {
|
||||
await patch(page, { mountedA: true }); await ready(page, 'A', 'mock-2');
|
||||
await page.waitForFunction(() => document.querySelectorAll('.hm-orgchart-host').length === 2);
|
||||
await patch(page, { mountedA: false });
|
||||
await page.waitForFunction(() => document.querySelectorAll('.hm-orgchart-host').length === 1);
|
||||
}
|
||||
check(await page.evaluate(() => window.chartObservers()) === bObservers, 'five Strict Mode mount/unmount cycles preserve B observer baseline');
|
||||
await patch(page, { mountedA: true }); await ready(page, 'A', 'mock-2');
|
||||
await page.evaluate(() => window.OrgChartReactDemo.render({ sourceA: window.OrgChartReactDemo.slowSource }));
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.requests.slow.loads === 2);
|
||||
await patch(page, { mountedA: false });
|
||||
await page.waitForFunction(() => !document.querySelector('[data-instance="A"]'));
|
||||
const unmountEvents = await page.evaluate(() => window.OrgChartReactDemo.events.length);
|
||||
await page.evaluate(() => window.OrgChartReactDemo.releaseSlow()); await page.waitForTimeout(100);
|
||||
check((await page.evaluate(() => window.OrgChartReactDemo.events.length)) === unmountEvents, 'late response after unmount cannot emit callback');
|
||||
check((await page.evaluate(() => window.OrgChartReactDemo.lifecycleErrors.length)) === 0, 'normal lifecycle has no rejected-use errors');
|
||||
await patch(page, { mountedA: true, stylesheetUrls: { engine: '', core: '' } });
|
||||
await page.waitForFunction(() => window.OrgChartReactDemo.lifecycleErrors.length > 0);
|
||||
check(await page.locator('.hm-orgchart-host').count() === 0, 'invalid Core configuration is reported via lifecycle error without leaked hosts');
|
||||
await page.evaluate(() => window.OrgChartReactDemo.destroy());
|
||||
check(await page.locator('.hm-orgchart-host').count() === 0 && await page.evaluate(() => window.chartObservers()) === 0, 'terminal React root unmount removes all Core DOM/observers');
|
||||
|
||||
const index = JSON.parse(await readFile(path.join(root, 'storybook-static/index.json'), 'utf8'));
|
||||
const stories = Object.values(index.entries).filter(entry => entry.title === 'OrgChart/Content' && entry.type === 'story');
|
||||
check(stories.length === 8, 'production Storybook registers exactly eight OrgChart stories', stories.map(story => ({ id: story.id, name: story.name })));
|
||||
const sb = await context.newPage(); captureErrors(sb, 'storybook-production');
|
||||
for (const story of stories) {
|
||||
await sb.goto(`${storybook.origin}/iframe.html?id=${story.id}&viewMode=story`);
|
||||
await sb.waitForFunction(() => [...document.querySelectorAll('.hm-orgchart-host')].some(host => host.shadowRoot.querySelector('.hm-orgchart-organization, .hm-orgchart-issues, .hm-orgchart-state:not(:empty)')));
|
||||
if (story.id.includes('loading')) {
|
||||
check((await sb.locator('.hm-orgchart-state').allTextContents()).some(text => text.includes('불러오는')), `${story.name}: actual loading source`);
|
||||
} else {
|
||||
await sb.waitForFunction(() => [...document.querySelectorAll('.hm-orgchart-host')].every(host => host.shadowRoot.querySelector('.hm-orgchart-organization')));
|
||||
check(await sb.locator('.node').count() >= 3, `${story.name}: real Core nodes rendered`);
|
||||
}
|
||||
if (story.id.includes('json-data-update')) {
|
||||
await sb.getByRole('button', { name: 'JSON v2', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2'));
|
||||
check((await sb.locator('.hm-orgchart-raw-total').textContent()).includes('5명'), 'Storybook JSON v1 to v2 recalculates 5 employees');
|
||||
await sb.getByRole('button', { name: 'Unmount', exact: true }).click();
|
||||
check(await sb.locator('.hm-orgchart-host').count() === 0, 'Storybook Unmount removes Core');
|
||||
await sb.getByRole('button', { name: 'Mount', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelector('.node'));
|
||||
check(await sb.locator('.hm-orgchart-host').count() === 1, 'Storybook remount creates one Core');
|
||||
}
|
||||
if (story.id.includes('loading')) {
|
||||
await sb.getByRole('button', { name: 'Source Error', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('error'));
|
||||
check(await sb.locator('.hm-orgchart-issues').count() === 1, 'Storybook source error controlled by Core');
|
||||
await sb.getByRole('button', { name: 'Ready v2 / 복구', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2'));
|
||||
check(await sb.locator('.node').count() === 3, 'Storybook Error to Ready recovery');
|
||||
await sb.getByRole('button', { name: '직원 0명 (Ready)', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('story-zero-employees'));
|
||||
check((await sb.locator('.hm-orgchart-raw-total').textContent()).includes('0명'), 'Storybook zero employees Ready');
|
||||
await sb.getByRole('button', { name: '조직 0개 (Error)', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('error'));
|
||||
check(await sb.locator('.hm-orgchart-issues').count() === 1, 'Storybook zero organizations Error');
|
||||
await sb.getByRole('button', { name: '느린 v1 (취소 무시)', exact: true }).click();
|
||||
await sb.getByRole('button', { name: 'Ready v2 / 복구', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2'));
|
||||
const outputBefore = await sb.locator('output').textContent();
|
||||
await sb.waitForTimeout(4100);
|
||||
check(await sb.locator('output').textContent() === outputBefore, 'Storybook stale abort-ignoring response produces no callback');
|
||||
}
|
||||
if (story.id.includes('multiple-instances')) {
|
||||
const b = sb.getByRole('region', { name: 'OrgChart B', exact: true });
|
||||
const bIdentity = await b.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance');
|
||||
const bState = await b.locator('output').textContent();
|
||||
await sb.getByRole('button', { name: 'A → JSON v2', exact: true }).click();
|
||||
await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2'));
|
||||
await sb.getByRole('button', { name: 'A Unmount', exact: true }).click();
|
||||
check(await sb.locator('.hm-orgchart-host').count() === 1 && await b.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === bIdentity && await b.locator('output').textContent() === bState, 'Storybook A update/unmount leaves B identity, data and callback count unchanged');
|
||||
}
|
||||
if (story.id.includes('basic-organization') || story.id.includes('employee-cards') || story.id.includes('responsive-container')) {
|
||||
for (const width of [390, 768, 1280, 1440]) {
|
||||
await sb.setViewportSize({ width, height: 1100 });
|
||||
if (story.id.includes('responsive-container')) await sb.getByRole('button', { name: `${width}px`, exact: true }).click();
|
||||
const dimensions = await sb.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const nodes = [...shadow.querySelectorAll('.node')], viewport = shadow.querySelector('.hm-orgchart-viewport');
|
||||
const rects = nodes.map(node => node.getBoundingClientRect());
|
||||
return {
|
||||
clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth,
|
||||
viewport: { clientWidth: viewport.clientWidth, scrollWidth: viewport.scrollWidth, clientHeight: viewport.clientHeight, scrollHeight: viewport.scrollHeight },
|
||||
nodeWidths: rects.map(rect => rect.width),
|
||||
overlap: rects.some((a, index) => rects.slice(index + 1).some(b => Math.min(a.right,b.right)-Math.max(a.left,b.left)>1 && Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top)>1)),
|
||||
avatars: [...shadow.querySelectorAll('.hm-orgchart-avatar')].every(avatar => avatar.textContent && !avatar.querySelector('img')),
|
||||
textFits: [...shadow.querySelectorAll('.hm-orgchart-person-name,.hm-orgchart-person-role,.hm-orgchart-organization-name')].every(element => element.scrollWidth <= element.clientWidth),
|
||||
};
|
||||
});
|
||||
metrics.push({ story: story.name, width, ...dimensions });
|
||||
check(dimensions.scrollWidth <= width && !dimensions.overlap && dimensions.textFits && dimensions.avatars, `${story.name}: ${width}px document scroll/overlap/long text/profile fallback`, dimensions);
|
||||
await sb.screenshot({ path: path.join(output, `${story.id}-${width}.png`), fullPage: true });
|
||||
}
|
||||
}
|
||||
await sb.screenshot({ path: path.join(output, `${story.id}.png`), fullPage: true });
|
||||
}
|
||||
if (process.env.ORGCHART_STORYBOOK_URL) {
|
||||
await sb.goto(`${process.env.ORGCHART_STORYBOOK_URL}/iframe.html?id=orgchart-content--basic-organization&viewMode=story`);
|
||||
await sb.waitForFunction(() => document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelector('.hm-orgchart-organization'));
|
||||
check(await sb.locator('.node').count() === 3, 'development Storybook on reserved separate port executes real Wrapper');
|
||||
}
|
||||
check(errors.length === 0, 'browser console/runtime errors are zero across consumer, Vanilla and Storybook', errors);
|
||||
} catch (error) {
|
||||
results.push({ name: 'verification completion', status: 'FAIL', detail: error.stack });
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await writeFile(path.join(output, 'report.json'), JSON.stringify({ browser: await browser.version(), results, errors, metrics, snapshots }, null, 2));
|
||||
await browser.close();
|
||||
for (const { server } of [consumer, vanilla, storybook]) await new Promise(resolve => server.close(resolve));
|
||||
console.log(JSON.stringify({ pass: results.filter(result => result.status === 'PASS').length, fail: results.filter(result => result.status === 'FAIL').length, errors, report: path.join(output, 'report.json') }));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
root: path.resolve(import.meta.dirname),
|
||||
base: './',
|
||||
plugins: [react()],
|
||||
server: { host: '127.0.0.1', port: 6026, strictPort: true },
|
||||
build: { outDir: 'dist', emptyOutDir: true, rolldownOptions: { input: { index: path.resolve(import.meta.dirname, 'index.html'), p1: path.resolve(import.meta.dirname, 'p1.html'), p11: path.resolve(import.meta.dirname, 'p11.html') } } },
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export function startDemo(api, inlineFixtures) {
|
||||
const { createOrgChart, JsonOrgChartDataSource } = api;
|
||||
const container = document.getElementById('chart-host');
|
||||
const status = document.getElementById('status');
|
||||
const stylesheetUrls = {
|
||||
engine: new URL('./assets/engine.css', location.href).href,
|
||||
core: new URL('./assets/style.css', location.href).href,
|
||||
};
|
||||
const source = () => new JsonOrgChartDataSource(inlineFixtures ? inlineFixtures.initial : './data-v1.json');
|
||||
let chart;
|
||||
function create() {
|
||||
chart?.destroy();
|
||||
chart = createOrgChart({ container, dataSource: source(), stylesheetUrls, onStateChange(state) {
|
||||
status.textContent = state.status === 'ready' ? `Ready · ${state.dataVersion}`
|
||||
: state.status === 'error' ? `Error · ${state.issues.map(issue => issue.code).join(', ')}` : state.status;
|
||||
} });
|
||||
return chart.mount();
|
||||
}
|
||||
async function update() {
|
||||
const data = inlineFixtures ? inlineFixtures.changed : await (await fetch('./data-v2.json')).json();
|
||||
await chart.update(data);
|
||||
}
|
||||
function destroy() { chart.destroy(); status.textContent = 'Destroyed · 새 인스턴스로 다시 생성할 수 있습니다.'; }
|
||||
const actions = { reload: () => chart.reload(), update, destroy, recreate: create };
|
||||
for (const [id, action] of Object.entries(actions)) document.getElementById(id).addEventListener('click', () => {
|
||||
Promise.resolve().then(action).catch(() => { status.textContent = 'Core 호출 순서를 확인하세요.'; });
|
||||
});
|
||||
window.addEventListener('pagehide', () => chart.destroy(), { once: true });
|
||||
// This example's inspection seam uses only public Core APIs, never engine internals.
|
||||
window.OrgChartDemo = { api, container, stylesheetUrls, source, create, update, destroy, get chart() { return chart; } };
|
||||
create().then(() => { window.orgChartReady = true; }).catch(() => { status.textContent = 'Mount failed'; });
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Read browser PNG backgrounds beside text; never edit production images.
|
||||
|
||||
Sample positions cover DOM text line bounds, with a clear padding-column probe
|
||||
for the current vertical opaque gradients. Pixel sampling and the sRGB CSS model
|
||||
are distinct evidence; unsupported/clipped painting is NOT VERIFIED.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT = ROOT / 'test-results' / 'org-chart-quality'
|
||||
report = json.loads((OUT / (sys.argv[1] if len(sys.argv) > 1 else 'browser.json')).read_text(encoding='utf-8'))
|
||||
|
||||
def rgb(value):
|
||||
match = re.fullmatch(r'rgba?\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)(?:,\s*(1))?\)', value)
|
||||
return tuple(map(float, match.groups()[:3])) if match else None
|
||||
|
||||
def luminance(color):
|
||||
def linear(channel):
|
||||
encoded = channel / 255
|
||||
return encoded / 12.92 if encoded <= .04045 else ((encoded + .055) / 1.055) ** 2.4
|
||||
return sum(weight * linear(channel) for weight, channel in zip((.2126, .7152, .0722), color))
|
||||
|
||||
def contrast(first, second):
|
||||
a, b = sorted((luminance(first), luminance(second)))
|
||||
return (b + .05) / (a + .05)
|
||||
|
||||
evidence = []
|
||||
for measurement in report['measurements']:
|
||||
image = Image.open(OUT / measurement['screenshot']).convert('RGB')
|
||||
for item in measurement['colors']:
|
||||
foreground = rgb(item['color'])
|
||||
behind = item['behind']
|
||||
entry = dict(label=measurement['label'], width=measurement['width'], selector=item['className'],
|
||||
organizationId=item.get('organizationId'), variant=item.get('variant'), target=item.get('target'),
|
||||
foreground=item['color'], fontSize=item['fontSize'], fontWeight=item['fontWeight'])
|
||||
threshold = 3 if item['fontSize'] >= 24 or (float(item['fontWeight']) >= 700 and item['fontSize'] >= 14 * 96 / 72) else 4.5
|
||||
entry['threshold'] = threshold
|
||||
if not foreground or not behind:
|
||||
entry.update(status='NOT VERIFIED', reason='Unknown foreground/background composition')
|
||||
elif behind['gradient'] == 'none':
|
||||
background = rgb(behind['background'])
|
||||
if background:
|
||||
value = contrast(foreground, background)
|
||||
entry.update(status='PASS' if value >= threshold else 'REVIEW', method='computed opaque solid', ratio=value, background=behind['background'])
|
||||
else:
|
||||
entry.update(status='NOT VERIFIED', reason='Transparent/unsupported solid composition')
|
||||
else:
|
||||
stops = re.findall(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)\s+(0|100)%', behind['gradient'])
|
||||
box, lines, clip = behind['box'], item['ranges'], item.get('clip')
|
||||
angle = re.search(r'linear-gradient\(([^r][^,]*),', behind['gradient'])
|
||||
if len(stops) != 2 or not lines or (angle and angle.group(1).strip() != '180deg'):
|
||||
entry.update(status='NOT VERIFIED', reason='Unsupported gradient/text range')
|
||||
else:
|
||||
colors = [tuple(map(int, stop[:3])) for stop in stops]
|
||||
# Probe an empty padding column, inside corners and before text.
|
||||
text_left = min(line['x'] for line in lines)
|
||||
x = math.floor(min(text_left - 2, box['x'] + max(4, (text_left - box['x']) / 2)))
|
||||
samples, deltas = [], []
|
||||
for line in lines:
|
||||
for y in range(math.ceil(line['y']), math.floor(line['y'] + line['height']) + 1):
|
||||
if not (0 <= x < image.width and 0 <= y < image.height):
|
||||
continue
|
||||
if clip and not (clip['x'] + 1 <= x < clip['x'] + clip['width'] - 1 and clip['y'] + 1 <= y < clip['y'] + clip['height'] - 1):
|
||||
continue
|
||||
offset = (y + .5 - box['y']) / box['height']
|
||||
if not 0 <= offset <= 1:
|
||||
continue
|
||||
pixel = image.getpixel((x, y))
|
||||
modeled = tuple(a + (b - a) * offset for a, b in zip(*colors))
|
||||
delta = max(abs(a - b) for a, b in zip(pixel, modeled))
|
||||
deltas.append(delta)
|
||||
samples.append(dict(x=x, y=y, background=pixel, contrast=contrast(foreground, pixel), modelDelta=delta))
|
||||
if not samples or max(deltas) > 2:
|
||||
entry.update(status='NOT VERIFIED', reason='Probe clipped, overpainted or disagrees with supported sRGB painting model', sampled=len(samples), maxModelDelta=max(deltas) if deltas else None)
|
||||
else:
|
||||
minimum = min(samples, key=lambda sample: sample['contrast'])
|
||||
entry.update(status='PASS' if minimum['contrast'] >= threshold else 'REVIEW', method='PNG background beside actual DOM text lines',
|
||||
ratio=minimum['contrast'], sampled=len(samples), maxModelDelta=max(deltas), weakest=minimum,
|
||||
conservativeStopMinimum=min(contrast(foreground, color) for color in colors))
|
||||
evidence.append(entry)
|
||||
|
||||
result = dict(method='DPR1 Chrome PNG; opaque foreground from computed CSS; padding-column gradient probes over DOM Range line bounds',
|
||||
thresholds='Unrounded 4.5 normal / 3 large; no logo recoloring; no full WCAG certification',
|
||||
results=evidence, statusCounts={status: sum(item['status'] == status for item in evidence) for status in ('PASS', 'REVIEW', 'NOT VERIFIED')})
|
||||
(OUT / (sys.argv[2] if len(sys.argv) > 2 else 'contrast-pixels.json')).write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(json.dumps(result['statusCounts']))
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HM OrgChart · 직접 열기 예제</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="./assets/hm-theme.css"><link rel="stylesheet" href="./demo.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>OrgChart Core · 직접 열기 예제</h1>
|
||||
<p>가상 JSON을 Classic 번들로 실행합니다. file://에서 외부 JSON Fetch는 사용하지 않습니다.</p>
|
||||
<div class="demo-controls" aria-label="Core 실행 예제">
|
||||
<button id="reload" type="button">초기 JSON 재로드</button>
|
||||
<button id="update" type="button">직원 이동·추가 반영</button>
|
||||
<button id="destroy" type="button">인스턴스 정리</button>
|
||||
<button id="recreate" type="button">새 인스턴스 생성</button>
|
||||
</div>
|
||||
<p id="status" role="status" aria-live="polite">준비 중</p><div id="chart-host"></div>
|
||||
</main>
|
||||
<script src="./assets/orgchart.browser.js"></script><script src="./fixtures.js"></script>
|
||||
<script src="./app-classic.js"></script>
|
||||
<script>startDemo(window.HMOrgChart, window.OrgChartFixtures);</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Extract business-management PNG colors into matching vector card templates.
|
||||
|
||||
Reads the reference only. No bitmap is embedded in the generated SVGs.
|
||||
Usage: python create-business-card-assets.py <reference PNG path>
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
reference = Path(sys.argv[1])
|
||||
image = Image.open(reference).convert('RGBA')
|
||||
pixels = np.asarray(image, dtype=float)
|
||||
width, height = image.size
|
||||
scale = np.array([width / 146, height / 68])
|
||||
start = np.array([124.527, 13.2139]) * scale
|
||||
end = np.array([91.4091, 70.3397]) * scale
|
||||
vector = end - start
|
||||
|
||||
# Probe only the flat outer bands, away from rounded corners and inner face.
|
||||
probes = ([(x, 2) for x in range(20, width - 20)]
|
||||
+ [(3, y) for y in range(20, height - 15)]
|
||||
+ [(width - 4, y) for y in range(20, height - 15)]
|
||||
+ [(x, height - 3) for x in range(20, width - 20)])
|
||||
coordinates = np.array(probes, dtype=float) + 0.5
|
||||
times = np.clip((coordinates - start) @ vector / (vector @ vector), 0, 1)
|
||||
values = np.array([pixels[y, x] for x, y in probes])
|
||||
basis = np.zeros((len(times), 3))
|
||||
first = times <= 0.72
|
||||
basis[first, 0] = 1 - times[first] / 0.72
|
||||
basis[first, 1] = times[first] / 0.72
|
||||
basis[~first, 1] = (1 - times[~first]) / 0.28
|
||||
basis[~first, 2] = (times[~first] - 0.72) / 0.28
|
||||
|
||||
# Test both interpolation representations rather than assuming raster alpha.
|
||||
fits = []
|
||||
for premultiplied in (False, True):
|
||||
samples = values.copy()
|
||||
if premultiplied:
|
||||
samples[:, :3] *= samples[:, 3:] / 255
|
||||
stops = np.linalg.lstsq(basis, samples, rcond=None)[0]
|
||||
predicted = basis @ stops
|
||||
if premultiplied:
|
||||
predicted[:, :3] /= np.maximum(predicted[:, 3:], 1) / 255
|
||||
stops[:, :3] /= np.maximum(stops[:, 3:], 1) / 255
|
||||
error = float(np.abs(predicted - values).mean())
|
||||
fits.append((error, premultiplied, np.clip(stops, 0, 255)))
|
||||
error, premultiplied, stops = min(fits, key=lambda item: item[0])
|
||||
|
||||
# Use plateau medians for the clamped endpoints, reducing raster edge noise.
|
||||
stops[0] = np.median(values[times == 0], axis=0)
|
||||
stops[2] = np.median(values[times == 1], axis=0)
|
||||
colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3]) for row in stops]
|
||||
opacities = [round(float(row[3] / 255), 2) for row in stops]
|
||||
inner_top = pixels[9:35, 20:150].reshape(-1, 4)
|
||||
inner_bottom = pixels[height - 13:height - 6, 20:150].reshape(-1, 4)
|
||||
inner_colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3])
|
||||
for row in [np.median(inner_top, axis=0), np.median(inner_bottom, axis=0)]]
|
||||
|
||||
# Recover the inner face's gradient direction from its unclamped RGB plane.
|
||||
# Colors alone are insufficient: retaining the template's gradient direction
|
||||
# would leave a visible tone difference despite matching all endpoint colors.
|
||||
inner_start_rgb = np.array([int(inner_colors[0][i:i + 2], 16) for i in (1, 3, 5)])
|
||||
inner_end_rgb = np.array([int(inner_colors[1][i:i + 2], 16) for i in (1, 3, 5)])
|
||||
delta = inner_start_rgb - inner_end_rgb
|
||||
inner_points, inner_times = [], []
|
||||
for y in range(12, height - 12, 2):
|
||||
for x in range(20, min(220, width // 2 + 16), 2):
|
||||
rgb = pixels[y, x, :3]
|
||||
t = float((inner_start_rgb - rgb) @ delta / (delta @ delta))
|
||||
if 0.06 < t < 0.94 and pixels[y, x, 3] == 255:
|
||||
inner_points.append([(x + 0.5) / scale[0], (y + 0.5) / scale[1], 1])
|
||||
inner_times.append(t)
|
||||
plane = np.linalg.lstsq(np.array(inner_points), np.array(inner_times), rcond=None)[0]
|
||||
gradient_vector = plane[:2] / (plane[:2] @ plane[:2])
|
||||
inner_start = np.array([78.0741, (-plane[2] - plane[0] * 78.0741) / plane[1]])
|
||||
inner_end = inner_start + gradient_vector
|
||||
|
||||
assets = root / 'packages/org-chart/src/assets/cards'
|
||||
for suffix in ('', '-sm'):
|
||||
template = (assets / f'경영기획팀{suffix}.svg').read_text(encoding='utf-8')
|
||||
# Retain the supplied department geometry and its size-specific gradients.
|
||||
for original, replacement in zip(['#DEE6F0', '#A8BBD1', '#577BA5'], colors):
|
||||
template = template.replace(f'stop-color="{original}"', f'stop-color="{replacement}"')
|
||||
template = template.replace(f'stop-color="{colors[2]}"', f'stop-color="{colors[2]}" stop-opacity="{opacities[2]}"')
|
||||
template = template.replace('stop-color="white"', f'stop-color="{inner_colors[0]}"')
|
||||
template = template.replace('stop-color="#E0E6F4"', f'stop-color="{inner_colors[1]}"')
|
||||
template = template.replace('paint0_linear_', 'business_management_outer_').replace('paint1_linear_', 'business_management_inner_')
|
||||
size_scale = np.array([1, 1]) if not suffix else np.array([108 / 146, 52 / 68])
|
||||
p1, p2 = inner_start * size_scale, inner_end * size_scale
|
||||
template = re.sub(r'(<linearGradient id="business_management_inner_[^"]+")[^>]+>',
|
||||
lambda match: f'{match[1]} x1="{p1[0]:.4f}" y1="{p1[1]:.4f}" x2="{p2[0]:.4f}" y2="{p2[1]:.4f}" gradientUnits="userSpaceOnUse">', template)
|
||||
template = template.replace('offset="0.97"', 'offset="1"')
|
||||
(assets / f'사업관리팀{suffix}.svg').write_text(template, encoding='utf-8')
|
||||
|
||||
output = root / 'test-results/org-chart-card-design'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(reference, output / 'business-management-reference.png')
|
||||
evidence = {
|
||||
'reference': str(reference), 'sha256': hashlib.sha256(reference.read_bytes()).hexdigest(),
|
||||
'referenceSize': [width, height], 'outerStops': colors, 'outerOpacities': opacities,
|
||||
'innerStops': inner_colors, 'outerStopOffsets': [0, 0.72, 1],
|
||||
'innerGradientStart': inner_start.tolist(), 'innerGradientEnd': inner_end.tolist(),
|
||||
'innerSampleCount': len(inner_points),
|
||||
'sampleCount': len(probes), 'fitMeanAbsoluteRGBAError': error,
|
||||
'premultipliedFit': premultiplied,
|
||||
'method': 'Outer-band RGBA samples fitted to the supplied department gradient geometry; endpoint plateau medians and inner-face medians. Inner gradient direction recovered from unclamped RGB plane. Reference PNG is preserved; geometry reuses department SVG paths.',
|
||||
}
|
||||
(output / 'business-management-colors.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(json.dumps(evidence, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'node:fs/promises';
|
||||
const companies = [
|
||||
['hanmac', '한맥기술', '회장'], ['saman', '삼안', '사업총괄'], ['jangheon-industry', '장헌산업', '본부장'],
|
||||
['jangheon', '장헌', '대표이사'], ['ptc', '피티씨', '총괄책임자'], ['halla', '한라산업개발', '사장'], ['baron', '바론컨설턴트', null],
|
||||
];
|
||||
for (const [size, count, companyCount, depth] of [['small', 20, 3, 2], ['medium', 100, 5, 3], ['large', 300, 7, 4]]) {
|
||||
const organizations = [], employees = [], affiliations = [];
|
||||
const addEmployee = (org, position, name) => {
|
||||
const id = `virtual-${String(employees.length + 1).padStart(3, '0')}`;
|
||||
employees.push({ id, organizationId: org.id, affiliationId: org.companyId, name: name ?? (employees.length === 5 ? '홍길동' : `가상직원 ${String(employees.length + 1).padStart(3, '0')}`), position, jobTitle: null, profileImage: null, workStatus: 'active' });
|
||||
return id;
|
||||
};
|
||||
for (const [index, [companyId, companyName, position]] of companies.slice(0, companyCount).entries()) {
|
||||
affiliations.push({ id: companyId, name: companyName });
|
||||
const org = (suffix, parentId, name, type, order) => {
|
||||
const node = { id: `${companyId}-${suffix}`, companyId, parentId, name, type, leaderId: null, displayOrder: order };
|
||||
organizations.push(node); return node;
|
||||
};
|
||||
const root = org('root', null, `${companyName} · 가상 조직`, 'root', index);
|
||||
if (position) root.leaderId = addEmployee(root, position, `가상책임자 ${index + 1}`);
|
||||
for (let branch = 0; branch < 2; branch++) {
|
||||
const division = org(`division-${branch}`, root.id, `가상 ${branch ? '지원' : '사업'}본부`, 'division', branch);
|
||||
if (count > 20) division.leaderId = addEmployee(division, '본부장');
|
||||
const office = org(`office-${branch}`, division.id, `가상 ${branch + 1}실`, 'office', 0);
|
||||
if (depth >= 3) {
|
||||
const cell = org(`cell-${branch}`, office.id, `가상 ${branch + 1}셀`, 'cell', 0);
|
||||
if (depth >= 4) {
|
||||
org(`team-${branch}`, cell.id, `가상 ${branch + 1}팀`, 'team', 0);
|
||||
if (branch === 0) org('team-sibling', cell.id, '가상 협력팀', 'team', 1);
|
||||
}
|
||||
}
|
||||
if (count > 20 && branch === 0) org('office-sibling', division.id, '가상 계획실', 'office', 1);
|
||||
}
|
||||
}
|
||||
// Unequal groups: deterministic weighted membership; no random or live data.
|
||||
const pool = organizations.filter(org => org.parentId !== null);
|
||||
const weighted = pool.flatMap((org, index) => Array.from({ length: 1 + index % 4 }, () => org));
|
||||
let cursor = 0;
|
||||
while (employees.length < count) {
|
||||
const org = weighted[cursor++ % weighted.length];
|
||||
addEmployee(org, ['사원', '선임', '책임', '수석'][cursor % 4]);
|
||||
}
|
||||
const data = { schemaVersion: '0.1-draft', organizations, employees, affiliations, relations: [],
|
||||
presentation: { title: '확장형 조직도 · 가상 데이터', description: '실제 직원 정보가 아닌 고정된 테스트 데이터입니다.', hubText: null,
|
||||
groups: { showEmployees: true, showSummary: false, showDispatched: false, showOnLeave: false, showAffiliations: false }, organizationOrder: [] },
|
||||
metadata: { asOfDate: '2026-10-12', dataVersion: `p5-23-${size}-${count}`, updateNote: '가상 데이터 · 운영 정보 아님' } };
|
||||
await fs.writeFile(new URL(`./data-expanded-${size}.json`, import.meta.url), JSON.stringify(data, null, 2) + '\n');
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// One-time fixture authoring helper; normal example builds read the checked-in fixtures.
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
const document = await readFile(new URL('../../docs/org-chart/data-contract.md', import.meta.url), 'utf8');
|
||||
const snippets = [...document.matchAll(/```json\r?\n([\s\S]*?)\r?\n```/g)].map(match => JSON.parse(match[1]));
|
||||
const initial = snippets.find(value => Array.isArray(value.organizations));
|
||||
if (!initial) throw new Error('No documented fictitious dataset');
|
||||
initial.schemaVersion = '0.1-draft';
|
||||
initial.presentation.description = '가상 데이터 · 원본 집계와 업무상 총원은 구분됩니다.';
|
||||
const changed = structuredClone(initial);
|
||||
changed.metadata.dataVersion = 'mock-2';
|
||||
changed.organizations[1].name = '가상 전략기획팀';
|
||||
changed.employees.find(person => person.id === 'emp-c').organizationId = 'team-b';
|
||||
changed.employees.push({ id: 'emp-e', organizationId: 'team-b', affiliationId: 'aff-a', name: '가상직원 마', position: '책임', jobTitle: null, profileImage: null, workStatus: 'active' });
|
||||
for (const [file, value] of [['data-v1.json', initial], ['data-v2.json', changed]])
|
||||
await writeFile(new URL(file, import.meta.url), JSON.stringify(value, null, 2) + '\n');
|
||||
@@ -0,0 +1,56 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
// Organization names were observed on Workdesk. Membership, employees and
|
||||
// every count below are synthetic, not an export of production HR data.
|
||||
const groups = [
|
||||
['business-management', '사업관리', 'office', []],
|
||||
['business-planning', '경영기획', 'office', ['경영기획 셀', '사업관리 셀']],
|
||||
['talent-growth', '인재성장', 'office', ['HR기획 셀', '자산관리 셀']],
|
||||
['integrated-system', '통합시스템 Div', 'div', ['IS 1팀', 'IS 2팀', 'IS 3팀']],
|
||||
['design-planning', '디자인기획', 'office', ['UX 셀', '영상 셀', '편집 셀']],
|
||||
['solution-sales', '솔루션영업', 'office', ['S/W 셀']],
|
||||
];
|
||||
const affiliations = [
|
||||
['hanmac', '(주)한맥기술'], ['saman', '(주)삼안'], ['jangheon', '(주)장헌'],
|
||||
['jangheon-industry', '장헌산업'], ['ptc', 'PTC'], ['halla', '한라산업개발'], ['baron', '바론컨설턴트'],
|
||||
].map(([id, name]) => ({ id, name }));
|
||||
const organizations = [{ id: 'general-planning', parentId: null, name: '총괄기획실', type: 'root', leaderId: 'mock-leader-00', displayOrder: 0 }];
|
||||
// Invented display names only; never sourced from a Workdesk employee capture.
|
||||
const names = [
|
||||
'김솔', '이도윤', '박하준', '정지우', '최예린', '한준서', '오수아',
|
||||
'강민서', '윤시우', '서다은', '배지안', '임서준', '송하린', '조유찬',
|
||||
'신도하', '문예은', '권지호', '안채원', '유시온', '홍서아', '고은우',
|
||||
'남하윤', '백도현', '노유나', '장태오', '양서진', '진서율', '구민재',
|
||||
'전하은', '심이준', '차소율', '우지율', '손예준', '함다온', '허지유',
|
||||
'황서우', '성유빈', '곽지후', '류다인', '하주원', '이봄',
|
||||
];
|
||||
const employees = [{ id: 'mock-leader-00', organizationId: 'general-planning', affiliationId: 'hanmac', name: names[0], position: '경영진', jobTitle: '실장', profileImage: null, workStatus: 'active' }];
|
||||
let person = 1;
|
||||
for (const [index, [id, name, type, cells]] of groups.entries()) {
|
||||
const leaderId = `mock-leader-${String(index + 1).padStart(2, '0')}`;
|
||||
organizations.push({ id, parentId: 'general-planning', name, type, leaderId, displayOrder: index });
|
||||
employees.push({ id: leaderId, organizationId: id, affiliationId: ['hanmac', 'saman', 'baron'][index % 3], name: names[employees.length], position: '수석', jobTitle: type === 'div' ? '디비전장' : '팀장', profileImage: null, workStatus: 'active' });
|
||||
const employeeOrganizations = cells.length ? cells.map((cell, cellIndex) => {
|
||||
const childId = `${id}-${cellIndex + 1}`;
|
||||
organizations.push({ id: childId, parentId: id, name: cell, type: cell.includes('팀') ? 'team' : 'cell', leaderId: null, displayOrder: cellIndex });
|
||||
return childId;
|
||||
}) : [id];
|
||||
for (const childId of employeeOrganizations) for (let member = 0; member < (cells.length ? 3 : 1); member++) {
|
||||
const employeeId = `mock-employee-${String(person).padStart(2, '0')}`;
|
||||
const isTeamLeader = childId.startsWith('integrated-system-') && member === 0;
|
||||
employees.push({ id: employeeId, organizationId: childId, affiliationId: ['hanmac', 'saman', 'ptc', 'baron'][person % 4], name: names[employees.length], position: ['책임', '선임', '연구'][member % 3], jobTitle: isTeamLeader ? '팀장' : null, profileImage: null, workStatus: person === 3 ? 'dispatched' : person === 25 ? 'onLeave' : 'active' });
|
||||
if (isTeamLeader) organizations.find(item => item.id === childId).leaderId = employeeId;
|
||||
person++;
|
||||
}
|
||||
}
|
||||
const fixture = {
|
||||
schemaVersion: '0.1-draft', organizations, employees, affiliations, relations: [],
|
||||
presentation: {
|
||||
title: '총괄기획실 조직도', description: '실제 조직명 · 가상 직원 정보 및 인원 통계 · 운영 인원수가 아닙니다.', hubText: '협업 · 소통 허브',
|
||||
groups: { showEmployees: true, showSummary: true, showDispatched: true, showOnLeave: true, showAffiliations: true },
|
||||
organizationOrder: ['general-planning', ...groups.map(([id]) => id)],
|
||||
},
|
||||
metadata: { asOfDate: '2026-10-12', dataVersion: 'p1-virtual-employees-v1', updateNote: '운영 HR 정보와 무관한 가상 구성' },
|
||||
};
|
||||
await writeFile(new URL('./data-p1.json', import.meta.url), `${JSON.stringify(fixture, null, 2)}\n`);
|
||||
console.log(`P1 fixture: ${organizations.length} organizations / ${employees.length} synthetic employees`);
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"schemaVersion": "0.1-draft",
|
||||
"organizations": [
|
||||
{
|
||||
"id": "hanmac-root",
|
||||
"companyId": "hanmac",
|
||||
"parentId": null,
|
||||
"name": "한맥기술 · 가상 조직",
|
||||
"type": "root",
|
||||
"leaderId": "virtual-001",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "hanmac-division-0",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-root",
|
||||
"name": "가상 사업본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "hanmac-office-0",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-division-0",
|
||||
"name": "가상 1실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "hanmac-division-1",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-root",
|
||||
"name": "가상 지원본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "hanmac-office-1",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-division-1",
|
||||
"name": "가상 2실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "saman-root",
|
||||
"companyId": "saman",
|
||||
"parentId": null,
|
||||
"name": "삼안 · 가상 조직",
|
||||
"type": "root",
|
||||
"leaderId": "virtual-002",
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "saman-division-0",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-root",
|
||||
"name": "가상 사업본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "saman-office-0",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-division-0",
|
||||
"name": "가상 1실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "saman-division-1",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-root",
|
||||
"name": "가상 지원본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "saman-office-1",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-division-1",
|
||||
"name": "가상 2실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry-root",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": null,
|
||||
"name": "장헌산업 · 가상 조직",
|
||||
"type": "root",
|
||||
"leaderId": "virtual-003",
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry-division-0",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-root",
|
||||
"name": "가상 사업본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry-office-0",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-division-0",
|
||||
"name": "가상 1실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry-division-1",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-root",
|
||||
"name": "가상 지원본부",
|
||||
"type": "division",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry-office-1",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-division-1",
|
||||
"name": "가상 2실",
|
||||
"type": "office",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
}
|
||||
],
|
||||
"employees": [
|
||||
{
|
||||
"id": "virtual-001",
|
||||
"organizationId": "hanmac-root",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상책임자 1",
|
||||
"position": "회장",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-002",
|
||||
"organizationId": "saman-root",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상책임자 2",
|
||||
"position": "사업총괄",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-003",
|
||||
"organizationId": "jangheon-industry-root",
|
||||
"affiliationId": "jangheon-industry",
|
||||
"name": "가상책임자 3",
|
||||
"position": "본부장",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-004",
|
||||
"organizationId": "hanmac-division-0",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 004",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-005",
|
||||
"organizationId": "hanmac-office-0",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 005",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-006",
|
||||
"organizationId": "hanmac-office-0",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "홍길동",
|
||||
"position": "수석",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-007",
|
||||
"organizationId": "hanmac-division-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 007",
|
||||
"position": "사원",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-008",
|
||||
"organizationId": "hanmac-division-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 008",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-009",
|
||||
"organizationId": "hanmac-division-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 009",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-010",
|
||||
"organizationId": "hanmac-office-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 010",
|
||||
"position": "수석",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-011",
|
||||
"organizationId": "hanmac-office-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 011",
|
||||
"position": "사원",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-012",
|
||||
"organizationId": "hanmac-office-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 012",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-013",
|
||||
"organizationId": "hanmac-office-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "가상직원 013",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-014",
|
||||
"organizationId": "saman-division-0",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 014",
|
||||
"position": "수석",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-015",
|
||||
"organizationId": "saman-office-0",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 015",
|
||||
"position": "사원",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-016",
|
||||
"organizationId": "saman-office-0",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 016",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-017",
|
||||
"organizationId": "saman-division-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 017",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-018",
|
||||
"organizationId": "saman-division-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 018",
|
||||
"position": "수석",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-019",
|
||||
"organizationId": "saman-division-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 019",
|
||||
"position": "사원",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "virtual-020",
|
||||
"organizationId": "saman-office-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "가상직원 020",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
}
|
||||
],
|
||||
"affiliations": [
|
||||
{
|
||||
"id": "hanmac",
|
||||
"name": "한맥기술"
|
||||
},
|
||||
{
|
||||
"id": "saman",
|
||||
"name": "삼안"
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry",
|
||||
"name": "장헌산업"
|
||||
}
|
||||
],
|
||||
"relations": [],
|
||||
"presentation": {
|
||||
"title": "확장형 조직도 · 가상 데이터",
|
||||
"description": "실제 직원 정보가 아닌 고정된 테스트 데이터입니다.",
|
||||
"hubText": null,
|
||||
"groups": {
|
||||
"showEmployees": true,
|
||||
"showSummary": false,
|
||||
"showDispatched": false,
|
||||
"showOnLeave": false,
|
||||
"showAffiliations": false
|
||||
},
|
||||
"organizationOrder": []
|
||||
},
|
||||
"metadata": {
|
||||
"asOfDate": "2026-10-12",
|
||||
"dataVersion": "p5-23-small-20",
|
||||
"updateNote": "가상 데이터 · 운영 정보 아님"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
{
|
||||
"schemaVersion": "0.1-draft",
|
||||
"organizations": [
|
||||
{
|
||||
"id": "general-planning",
|
||||
"parentId": null,
|
||||
"name": "총괄기획실",
|
||||
"type": "root",
|
||||
"leaderId": "expanded-representative",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "business-management",
|
||||
"parentId": "general-planning",
|
||||
"name": "사업관리",
|
||||
"type": "office",
|
||||
"leaderId": "expanded-leader-01",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "business-planning",
|
||||
"parentId": "general-planning",
|
||||
"name": "경영기획",
|
||||
"type": "office",
|
||||
"leaderId": "expanded-leader-02",
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "talent-growth",
|
||||
"parentId": "general-planning",
|
||||
"name": "인재성장",
|
||||
"type": "office",
|
||||
"leaderId": "expanded-leader-03",
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "business-control",
|
||||
"parentId": "business-management",
|
||||
"name": "사업관리 셀",
|
||||
"type": "cell",
|
||||
"leaderId": "expanded-cell-leader-01",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "project-support",
|
||||
"parentId": "business-management",
|
||||
"name": "사업지원 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "finance",
|
||||
"parentId": "business-planning",
|
||||
"name": "재무 셀",
|
||||
"type": "cell",
|
||||
"leaderId": "expanded-cell-leader-02",
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "planning-support",
|
||||
"parentId": "business-planning",
|
||||
"name": "기획지원 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 3
|
||||
},
|
||||
{
|
||||
"id": "people-support",
|
||||
"parentId": "talent-growth",
|
||||
"name": "성장지원 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 4
|
||||
},
|
||||
{
|
||||
"id": "project-review",
|
||||
"parentId": "business-control",
|
||||
"name": "사업검토 팀",
|
||||
"type": "cell",
|
||||
"leaderId": "expanded-team-leader-01",
|
||||
"displayOrder": 5
|
||||
},
|
||||
{
|
||||
"id": "review-support",
|
||||
"parentId": "project-review",
|
||||
"name": "검토지원 파트",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 6
|
||||
}
|
||||
],
|
||||
"employees": [
|
||||
{
|
||||
"id": "expanded-representative",
|
||||
"organizationId": "general-planning",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "홍길동",
|
||||
"position": "대표이사",
|
||||
"jobTitle": "대표이사",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-leader-01",
|
||||
"organizationId": "business-management",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "이도윤",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-01",
|
||||
"organizationId": "business-management",
|
||||
"affiliationId": "saman",
|
||||
"name": "김하린",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-02",
|
||||
"organizationId": "business-management",
|
||||
"affiliationId": "jangheon-industry",
|
||||
"name": "박서준",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-leader-02",
|
||||
"organizationId": "business-planning",
|
||||
"affiliationId": "jangheon",
|
||||
"name": "정지우",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-03",
|
||||
"organizationId": "business-planning",
|
||||
"affiliationId": "ptc",
|
||||
"name": "최유진",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-04",
|
||||
"organizationId": "business-planning",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "윤서아",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-leader-03",
|
||||
"organizationId": "talent-growth",
|
||||
"affiliationId": "halla",
|
||||
"name": "강민준",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-05",
|
||||
"organizationId": "talent-growth",
|
||||
"affiliationId": "baron",
|
||||
"name": "한예린",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-employee-06",
|
||||
"organizationId": "talent-growth",
|
||||
"affiliationId": "saman",
|
||||
"name": "오하준",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-cell-leader-01",
|
||||
"organizationId": "business-control",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "김민준",
|
||||
"position": "팀장",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-cell-leader-02",
|
||||
"organizationId": "finance",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "김민준",
|
||||
"position": "팀장",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-team-leader-01",
|
||||
"organizationId": "project-review",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "김민준",
|
||||
"position": "팀장",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-nested-1",
|
||||
"organizationId": "business-control",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "서지우",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-nested-2",
|
||||
"organizationId": "business-control",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "서지우",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-nested-3",
|
||||
"organizationId": "finance",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "서지우",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-nested-4",
|
||||
"organizationId": "project-review",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "서지우",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "expanded-nested-5",
|
||||
"organizationId": "review-support",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "서지우",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
}
|
||||
],
|
||||
"affiliations": [
|
||||
{
|
||||
"id": "hanmac",
|
||||
"name": "(주)한맥기술"
|
||||
},
|
||||
{
|
||||
"id": "saman",
|
||||
"name": "(주)삼안"
|
||||
},
|
||||
{
|
||||
"id": "jangheon",
|
||||
"name": "(주)장헌"
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry",
|
||||
"name": "장헌산업"
|
||||
},
|
||||
{
|
||||
"id": "ptc",
|
||||
"name": "PTC"
|
||||
},
|
||||
{
|
||||
"id": "halla",
|
||||
"name": "한라산업개발"
|
||||
},
|
||||
{
|
||||
"id": "baron",
|
||||
"name": "바론컨설턴트"
|
||||
}
|
||||
],
|
||||
"relations": [],
|
||||
"presentation": {
|
||||
"title": "확장형 조직도",
|
||||
"description": "자동 계층 배치 · 가상 직원 정보",
|
||||
"hubText": null,
|
||||
"groups": {
|
||||
"showEmployees": true,
|
||||
"showSummary": false,
|
||||
"showDispatched": false,
|
||||
"showOnLeave": false,
|
||||
"showAffiliations": false
|
||||
},
|
||||
"organizationOrder": [
|
||||
"general-planning",
|
||||
"business-management",
|
||||
"business-planning",
|
||||
"talent-growth",
|
||||
"business-control",
|
||||
"project-support",
|
||||
"finance",
|
||||
"planning-support",
|
||||
"people-support",
|
||||
"project-review",
|
||||
"review-support"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"asOfDate": "2026-10-12",
|
||||
"dataVersion": "p5-1b-expanded-hierarchy-v1",
|
||||
"updateNote": "P5-1B 최소 재귀 계층 데이터 · 운영 HR 정보와 무관"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
{
|
||||
"schemaVersion": "0.1-draft",
|
||||
"organizations": [
|
||||
{
|
||||
"id": "general-planning",
|
||||
"parentId": null,
|
||||
"name": "총괄기획실",
|
||||
"type": "root",
|
||||
"leaderId": "mock-leader-00",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "business-management",
|
||||
"parentId": "general-planning",
|
||||
"name": "사업관리",
|
||||
"type": "office",
|
||||
"leaderId": "mock-leader-01",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "business-planning",
|
||||
"parentId": "general-planning",
|
||||
"name": "경영기획",
|
||||
"type": "office",
|
||||
"leaderId": "mock-leader-02",
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "business-planning-1",
|
||||
"parentId": "business-planning",
|
||||
"name": "경영기획 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "business-planning-2",
|
||||
"parentId": "business-planning",
|
||||
"name": "사업관리 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "talent-growth",
|
||||
"parentId": "general-planning",
|
||||
"name": "인재성장",
|
||||
"type": "office",
|
||||
"leaderId": "mock-leader-03",
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "talent-growth-1",
|
||||
"parentId": "talent-growth",
|
||||
"name": "HR기획 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "talent-growth-2",
|
||||
"parentId": "talent-growth",
|
||||
"name": "자산관리 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "integrated-system",
|
||||
"parentId": "general-planning",
|
||||
"name": "통합시스템 Div",
|
||||
"type": "div",
|
||||
"leaderId": "mock-leader-04",
|
||||
"displayOrder": 3
|
||||
},
|
||||
{
|
||||
"id": "integrated-system-1",
|
||||
"parentId": "integrated-system",
|
||||
"name": "IS 1팀",
|
||||
"type": "team",
|
||||
"leaderId": "mock-employee-14",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "integrated-system-2",
|
||||
"parentId": "integrated-system",
|
||||
"name": "IS 2팀",
|
||||
"type": "team",
|
||||
"leaderId": "mock-employee-17",
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "integrated-system-3",
|
||||
"parentId": "integrated-system",
|
||||
"name": "IS 3팀",
|
||||
"type": "team",
|
||||
"leaderId": "mock-employee-20",
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "design-planning",
|
||||
"parentId": "general-planning",
|
||||
"name": "디자인기획",
|
||||
"type": "office",
|
||||
"leaderId": "mock-leader-05",
|
||||
"displayOrder": 4
|
||||
},
|
||||
{
|
||||
"id": "design-planning-1",
|
||||
"parentId": "design-planning",
|
||||
"name": "UX 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "design-planning-2",
|
||||
"parentId": "design-planning",
|
||||
"name": "영상 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "design-planning-3",
|
||||
"parentId": "design-planning",
|
||||
"name": "편집 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "solution-sales",
|
||||
"parentId": "general-planning",
|
||||
"name": "솔루션영업",
|
||||
"type": "office",
|
||||
"leaderId": "mock-leader-06",
|
||||
"displayOrder": 5
|
||||
},
|
||||
{
|
||||
"id": "solution-sales-1",
|
||||
"parentId": "solution-sales",
|
||||
"name": "S/W 셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
"displayOrder": 0
|
||||
}
|
||||
],
|
||||
"employees": [
|
||||
{
|
||||
"id": "mock-leader-00",
|
||||
"organizationId": "general-planning",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "김솔",
|
||||
"position": "경영진",
|
||||
"jobTitle": "실장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-01",
|
||||
"organizationId": "business-management",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "이도윤",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-01",
|
||||
"organizationId": "business-management",
|
||||
"affiliationId": "saman",
|
||||
"name": "박하준",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-02",
|
||||
"organizationId": "business-planning",
|
||||
"affiliationId": "saman",
|
||||
"name": "정지우",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-02",
|
||||
"organizationId": "business-planning-1",
|
||||
"affiliationId": "ptc",
|
||||
"name": "최예린",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-03",
|
||||
"organizationId": "business-planning-1",
|
||||
"affiliationId": "baron",
|
||||
"name": "한준서",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "dispatched"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-04",
|
||||
"organizationId": "business-planning-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "오수아",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-05",
|
||||
"organizationId": "business-planning-2",
|
||||
"affiliationId": "saman",
|
||||
"name": "강민서",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-06",
|
||||
"organizationId": "business-planning-2",
|
||||
"affiliationId": "ptc",
|
||||
"name": "윤시우",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-07",
|
||||
"organizationId": "business-planning-2",
|
||||
"affiliationId": "baron",
|
||||
"name": "서다은",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-03",
|
||||
"organizationId": "talent-growth",
|
||||
"affiliationId": "baron",
|
||||
"name": "배지안",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-08",
|
||||
"organizationId": "talent-growth-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "임서준",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-09",
|
||||
"organizationId": "talent-growth-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "송하린",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-10",
|
||||
"organizationId": "talent-growth-1",
|
||||
"affiliationId": "ptc",
|
||||
"name": "조유찬",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-11",
|
||||
"organizationId": "talent-growth-2",
|
||||
"affiliationId": "baron",
|
||||
"name": "신도하",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-12",
|
||||
"organizationId": "talent-growth-2",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "문예은",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-13",
|
||||
"organizationId": "talent-growth-2",
|
||||
"affiliationId": "saman",
|
||||
"name": "권지호",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-04",
|
||||
"organizationId": "integrated-system",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "안채원",
|
||||
"position": "수석",
|
||||
"jobTitle": "디비전장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-14",
|
||||
"organizationId": "integrated-system-1",
|
||||
"affiliationId": "ptc",
|
||||
"name": "유시온",
|
||||
"position": "책임",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-15",
|
||||
"organizationId": "integrated-system-1",
|
||||
"affiliationId": "baron",
|
||||
"name": "홍서아",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-16",
|
||||
"organizationId": "integrated-system-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "고은우",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-17",
|
||||
"organizationId": "integrated-system-2",
|
||||
"affiliationId": "saman",
|
||||
"name": "남하윤",
|
||||
"position": "책임",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-18",
|
||||
"organizationId": "integrated-system-2",
|
||||
"affiliationId": "ptc",
|
||||
"name": "백도현",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-19",
|
||||
"organizationId": "integrated-system-2",
|
||||
"affiliationId": "baron",
|
||||
"name": "노유나",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-20",
|
||||
"organizationId": "integrated-system-3",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "장태오",
|
||||
"position": "책임",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-21",
|
||||
"organizationId": "integrated-system-3",
|
||||
"affiliationId": "saman",
|
||||
"name": "양서진",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-22",
|
||||
"organizationId": "integrated-system-3",
|
||||
"affiliationId": "ptc",
|
||||
"name": "진서율",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-05",
|
||||
"organizationId": "design-planning",
|
||||
"affiliationId": "saman",
|
||||
"name": "구민재",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-23",
|
||||
"organizationId": "design-planning-1",
|
||||
"affiliationId": "baron",
|
||||
"name": "전하은",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-24",
|
||||
"organizationId": "design-planning-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "심이준",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-25",
|
||||
"organizationId": "design-planning-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "차소율",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "onLeave"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-26",
|
||||
"organizationId": "design-planning-2",
|
||||
"affiliationId": "ptc",
|
||||
"name": "우지율",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-27",
|
||||
"organizationId": "design-planning-2",
|
||||
"affiliationId": "baron",
|
||||
"name": "손예준",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-28",
|
||||
"organizationId": "design-planning-2",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "함다온",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-29",
|
||||
"organizationId": "design-planning-3",
|
||||
"affiliationId": "saman",
|
||||
"name": "허지유",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-30",
|
||||
"organizationId": "design-planning-3",
|
||||
"affiliationId": "ptc",
|
||||
"name": "황서우",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-31",
|
||||
"organizationId": "design-planning-3",
|
||||
"affiliationId": "baron",
|
||||
"name": "성유빈",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-leader-06",
|
||||
"organizationId": "solution-sales",
|
||||
"affiliationId": "baron",
|
||||
"name": "곽지후",
|
||||
"position": "수석",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-32",
|
||||
"organizationId": "solution-sales-1",
|
||||
"affiliationId": "hanmac",
|
||||
"name": "류다인",
|
||||
"position": "책임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-33",
|
||||
"organizationId": "solution-sales-1",
|
||||
"affiliationId": "saman",
|
||||
"name": "하주원",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "mock-employee-34",
|
||||
"organizationId": "solution-sales-1",
|
||||
"affiliationId": "ptc",
|
||||
"name": "이봄",
|
||||
"position": "연구",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
}
|
||||
],
|
||||
"affiliations": [
|
||||
{
|
||||
"id": "hanmac",
|
||||
"name": "(주)한맥기술"
|
||||
},
|
||||
{
|
||||
"id": "saman",
|
||||
"name": "(주)삼안"
|
||||
},
|
||||
{
|
||||
"id": "jangheon",
|
||||
"name": "(주)장헌"
|
||||
},
|
||||
{
|
||||
"id": "jangheon-industry",
|
||||
"name": "장헌산업"
|
||||
},
|
||||
{
|
||||
"id": "ptc",
|
||||
"name": "PTC"
|
||||
},
|
||||
{
|
||||
"id": "halla",
|
||||
"name": "한라산업개발"
|
||||
},
|
||||
{
|
||||
"id": "baron",
|
||||
"name": "바론컨설턴트"
|
||||
}
|
||||
],
|
||||
"relations": [],
|
||||
"presentation": {
|
||||
"title": "총괄기획실 조직도",
|
||||
"description": "실제 조직명 · 가상 직원 정보 및 인원 통계 · 운영 인원수가 아닙니다.",
|
||||
"hubText": "협업 · 소통 허브",
|
||||
"groups": {
|
||||
"showEmployees": true,
|
||||
"showSummary": true,
|
||||
"showDispatched": true,
|
||||
"showOnLeave": true,
|
||||
"showAffiliations": true
|
||||
},
|
||||
"organizationOrder": [
|
||||
"general-planning",
|
||||
"business-management",
|
||||
"business-planning",
|
||||
"talent-growth",
|
||||
"integrated-system",
|
||||
"design-planning",
|
||||
"solution-sales"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"asOfDate": "2026-10-12",
|
||||
"dataVersion": "p1-virtual-employees-v1",
|
||||
"updateNote": "운영 HR 정보와 무관한 가상 구성"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"organizations": [
|
||||
{
|
||||
"id": "org-root",
|
||||
"parentId": null,
|
||||
"name": "가상 사업본부",
|
||||
"type": "root",
|
||||
"leaderId": "emp-a",
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "team-a",
|
||||
"parentId": "org-root",
|
||||
"name": "가상 기획팀",
|
||||
"type": "team",
|
||||
"leaderId": "emp-b",
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "team-b",
|
||||
"parentId": "org-root",
|
||||
"name": "가상 운영팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
"displayOrder": 2
|
||||
}
|
||||
],
|
||||
"employees": [
|
||||
{
|
||||
"id": "emp-a",
|
||||
"organizationId": "org-root",
|
||||
"affiliationId": "aff-a",
|
||||
"name": "가상직원 가",
|
||||
"position": "대표",
|
||||
"jobTitle": "본부장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "emp-b",
|
||||
"organizationId": "team-a",
|
||||
"affiliationId": "aff-a",
|
||||
"name": "가상직원 나",
|
||||
"position": "책임",
|
||||
"jobTitle": "팀장",
|
||||
"profileImage": null,
|
||||
"workStatus": "active"
|
||||
},
|
||||
{
|
||||
"id": "emp-c",
|
||||
"organizationId": "team-a",
|
||||
"affiliationId": "aff-b",
|
||||
"name": "가상직원 다",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "dispatched"
|
||||
},
|
||||
{
|
||||
"id": "emp-d",
|
||||
"organizationId": "team-b",
|
||||
"affiliationId": "aff-b",
|
||||
"name": "가상직원 라",
|
||||
"position": "선임",
|
||||
"jobTitle": null,
|
||||
"profileImage": null,
|
||||
"workStatus": "onLeave"
|
||||
}
|
||||
],
|
||||
"affiliations": [
|
||||
{
|
||||
"id": "aff-a",
|
||||
"name": "가상 관계사 A"
|
||||
},
|
||||
{
|
||||
"id": "aff-b",
|
||||
"name": "가상 관계사 B"
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"id": "rel-a",
|
||||
"sourceOrganizationId": "team-a",
|
||||
"targetOrganizationId": "team-b",
|
||||
"type": "cooperation"
|
||||
}
|
||||
],
|
||||
"presentation": {
|
||||
"title": "가상 조직도",
|
||||
"description": "가상 데이터 · 원본 집계와 업무상 총원은 구분됩니다.",
|
||||
"hubText": "협업 중심",
|
||||
"groups": {
|
||||
"showEmployees": true,
|
||||
"showSummary": true,
|
||||
"showDispatched": true,
|
||||
"showOnLeave": true,
|
||||
"showAffiliations": true
|
||||
},
|
||||
"organizationOrder": [
|
||||
"org-root",
|
||||
"team-a",
|
||||
"team-b"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"schemaVersion": "0.1-draft",
|
||||
"asOfDate": "2026-10-12",
|
||||
"dataVersion": "mock-1",
|
||||
"updatedAt": null,
|
||||
"updateNote": "초안 예시"
|
||||
},
|
||||
"schemaVersion": "0.1-draft"
|
||||
}
|
||||
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user