feat(org-chart): implement expandable layout and multi-company navigation

Integrate P5-1B through P5-4 shared Core layout, collapse, zoom/pan/fit, company roots and keyboard accessibility. Include React/HTML examples, virtual 20/100/300 employee fixtures, focused checks and the additive data contract.
This commit is contained in:
shkim4 committed 2026-10-12 21:59:09 +09:00
1 parent a92913d6bc
commit 064517ec46
30 files changed
+7532 -183

No files matched your search

+31 -16
View File
@@ -1,35 +1,50 @@
import { StrictMode, useId, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hm-design/ui';
import { Button, Checkbox, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hm-design/ui';
import { OrgChart } from '@hm-design/org-chart-react';
import { companyColorPresets, getCompanyColorPreset, JsonOrgChartDataSource } from '@hm-design/org-chart';
import { companyColorPresets, JsonOrgChartDataSource, type OrgChartConnectionDirection } 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 expandedFixture from '../../examples/org-chart/data-expanded.json';
import small from '../../examples/org-chart/data-expanded-small.json';
import medium from '../../examples/org-chart/data-expanded-medium.json';
import large from '../../examples/org-chart/data-expanded-large.json';
import './org-chart-stories.css';
const expandedSource = new JsonOrgChartDataSource(expandedFixture);
const fixtures = { small, medium, large };
const sources = { small: new JsonOrgChartDataSource(small), medium: new JsonOrgChartDataSource(medium), large: new JsonOrgChartDataSource(large) };
const compositionStylesheets = { engine: engineUrl, core: coreUrl };
const topNodes = { ptc: { title: '기술총괄', name: '가상책임자 5' } };
const connections: Record<string, OrgChartConnectionDirection> = Object.fromEntries(large.organizations.map((org, index) => [org.id, (['top', 'left', 'right'] as const)[index % 3]! ]));
/** P5-1A establishes the Core rendering boundary and initial 1Depth groups.
* Automatic hierarchy placement belongs to P5-1B; this host owns no layout. */
/** Company state is passed to the shared Core without filtering/mutating JSON. */
function ExpandedPreview() {
const id = useId();
const [companyId, setCompanyId] = useState('hanmac');
const preset = getCompanyColorPreset(companyId);
return <section className="hm-orgchart-story" aria-label="Expanded OrgChart · P5-1A">
<p className="hm-orgchart-story-note"><strong>P5-1A · 확장형 조직도 초기 화면</strong> — 대표자와 예시 1Depth 그룹, 기존 카드 및 회사 컬러 프리셋을 표시합니다. 자동 계층 배치·재계산은 P5-1B, 접기·펼치기와 Zoom/Pan은 후속 작업입니다. 모든 직원 정보는 가상 데이터입니다.</p>
const [size, setSize] = useState<keyof typeof fixtures>('small');
const [selected, setSelected] = useState<string[] | undefined>();
const companies = fixtures[size].affiliations;
const visible = selected ?? companies.map(company => company.id);
return <section className="hm-orgchart-story" aria-label="Expanded OrgChart · P5-4">
<p className="hm-orgchart-story-note"><strong>확장형 조직도</strong> — 회사별 독립 조직 · 타이틀 클릭으로 접기·펼치기 · 확대 및 드래그 이동. 모든 직원은 가상 데이터입니다.</p>
<div className="hm-orgchart-story-controls">
<div className="hm-orgchart-story-instance">
<Label id={`${id}-company-label`} htmlFor={`${id}-company`}>회사별 컬러 테마</Label>
<Select value={companyId} onValueChange={setCompanyId}>
<SelectTrigger id={`${id}-company`} aria-labelledby={`${id}-company-label`} size="medium"><SelectValue /></SelectTrigger>
<SelectContent>{Object.values(companyColorPresets).map(company => <SelectItem key={company.companyId} value={company.companyId}>{company.companyName}</SelectItem>)}</SelectContent>
<Label id={`${id}-data-label`} htmlFor={`${id}-data`}>가상 데이터</Label>
<Select value={size} onValueChange={value => { setSize(value as keyof typeof fixtures); setSelected(undefined); }}>
<SelectTrigger id={`${id}-data`} aria-labelledby={`${id}-data-label`} size="medium"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="small">Small · 20명</SelectItem><SelectItem value="medium">Medium · 100명</SelectItem><SelectItem value="large">Large · 300명</SelectItem></SelectContent>
</Select>
</div>
<Button variant="secondary" appearance="outline" onClick={() => setSelected(undefined)}>전체 회사</Button>
<Button variant="secondary" appearance="outline" onClick={() => setSelected([])}>선택 해제</Button>
</div>
{preset && <p className="hm-orgchart-story-note" aria-live="polite">{preset.companyName} · Main {preset.primary} · 보조 컬러 {preset.secondary.length ? preset.secondary.join(', ') : '등록 없음'}</p>}
<OrgChart dataSource={expandedSource} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" companyId={companyId} className="hm-orgchart-story-chart" />
<fieldset className="hm-orgchart-story-companies">
<legend>표시할 회사 · 복수 선택 가능</legend>
{companies.map(company => <div key={company.id} className="hm-orgchart-story-company">
<Checkbox id={`${id}-${company.id}`} checked={visible.includes(company.id)} onCheckedChange={checked => setSelected(checked === true ? [...visible, company.id] : visible.filter(value => value !== company.id))} />
<Label htmlFor={`${id}-${company.id}`}>{company.name}</Label>
<span className="hm-orgchart-story-note">{companyColorPresets[company.id as keyof typeof companyColorPresets].primary}</span>
</div>)}
</fieldset>
<OrgChart dataSource={sources[size]} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" selectedCompanyIds={selected} companyTopNodes={topNodes} employeeConnections={connections} className="hm-orgchart-story-chart" />
</section>;
}
+4
View File
@@ -37,6 +37,10 @@ body:has(.hm-orgchart-story-full) { background: #F5F8FB; }
max-width: 100%;
}
.hm-orgchart-story-companies { display: flex; flex-wrap: wrap; gap: 1rem; border: 0; padding: 0; min-width: 0; }
.hm-orgchart-story-companies legend { margin-bottom: 0.5rem; }
.hm-orgchart-story-company { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.hm-orgchart-story-chart {
width: 100%;
}