20 KiB
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를 참고한다.
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
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에서 재정의한다.
/* 공개 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 보고서, P7 변경과 실측은 P7 보고서를 참고한다.
P7 데이터·레이아웃·독립 옵션
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 조합은 별도 미검증이다.
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는 유지한다. 표시 직원 수는 최초 예시 인원이 아닌 현재 조직의
회사 소속을 기준으로 계산하므로 회사 간 이동 직후와 회사 숨김·다시 표시에도 일치한다.
미리보기에는 선택 회사별 현재 직원 수도 표시하며 데이터 설정의 회사당 인원은 예시 생성 기준이다.