Files
HMDesign/packages/org-chart/ai/COMPOSITION_PATTERNS.md
T
shkim4 7621934f69 docs(ai): split SW usage into general / viewer mode in packaged AI guides
- Usage types are now Web / SW (general mode, viewer mode) / OrgChart / mixed.
  Shared AI_GUIDE step 1 asks for the SW mode only when the request or the
  project setting (HMDesign 사용 유형: SW 일반 | SW 뷰어) leaves it open;
  examples B0/B1/B2 cover "HMDesign SW로 화면을 만들어 주세요", general and viewer.
- Missing-component handling in three steps (reuse in the chosen type/mode ->
  build with that mode's tokens -> HMDesign-consistent style, reported as
  "AI 구성 값 — 디자인 확인 필요"), plus SW general/viewer separation rules.
- sw/ai: AI_USAGE §0 mode selection, catalog §8 lists all 33 SWViewer* exports,
  DESIGN_RULES §11 viewer tokens, PATTERNS 7 viewer composition; examples B/F
  marked as general mode. ui/ai and org-chart/ai aligned with the new types.
- AGENTS_SNIPPET, AGENTS.md and MAINTAINING updated; copies synced.
- scripts/ai-docs.mjs: shared-copy check ignores CRLF/LF (core.autocrlf=true
  checks out packages/sw as LF and the rest as CRLF).
- Verification record updated (code samples typechecked, built and rendered).
2026-10-19 17:34:15 +09:00

187 lines
9.3 KiB
Markdown

# OrgChart 조합 패턴
모든 패턴은 공개 Export만 씁니다([COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)). 예시 데이터는 가상이며 회사·분기·인원을 기본값으로 쓰지 않습니다.
## 0. 어떤 패턴을 고를까
| 요청 | 패턴 |
|---|---|
| 조직도만 기존 페이지에 삽입 | 1. Chart Only (Auto) |
| 조직도가 들어간 새 페이지 전체 | 2. Full Page (Web + OrgChart) |
| 직원 정보 팝업을 Web 디자인으로 | 3. HM Dialog 팝업 |
| 다른 디자인 시스템의 팝업 | 4. 팝업 어댑터 |
| 자연어·CSV·Excel → 데이터 | 5. 데이터 만들기 |
| Web·SW 등 다른 영역과 혼합 | 2. Full Page의 영역 분리 원칙. SW 화면 + 조직도는 NOT VERIFIED([AI_USAGE.md §0](./AI_USAGE.md)) |
| 기존 Workdesk(고정) 구성 | `layoutMode="fixed"`(기본), `displayMode="readOnly"` 등 — Chart Only 불가 |
## 1. Chart Only (Auto) — 조직도 영역만
**React**
```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 OrganizationChart({ data }: { data: OrgChartDataset }) {
const dataSource = useMemo(() => new JsonOrgChartDataSource(data), [data])
return (
<OrgChart
dataSource={dataSource}
stylesheetUrls={stylesheetUrls}
layoutMode="auto"
presentationMode="chart-only" // 초기 Fit·일반 휠 Zoom 기본
dragDropEnabled={false}
style={{ height: "100%" }} // 높이는 호스트 영역이 정함
onLifecycleError={error => console.error(error)}
/>
)
}
```
**Vanilla (React 없음)**
```ts
import { createOrgChart, 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"
export async function mountOrganizationChart(container: HTMLElement, data: OrgChartDataset): Promise<() => void> {
const chart = createOrgChart({
container, dataSource: new JsonOrgChartDataSource(data),
stylesheetUrls: { engine: engineUrl, core: coreUrl },
layoutMode: "auto", presentationMode: "chart-only", dragDropEnabled: false,
})
try {
await chart.mount()
if (chart.getState().status !== "ready") throw new Error("조직도를 표시할 수 없습니다.")
return () => chart.destroy()
} catch (error) {
chart.destroy()
throw error
}
}
```
- 이 컴포넌트에 제목·통계·버튼 바·설명·바깥 회색 영역을 **추가하지 않습니다.** 호스트 페이지의 기존 UI도 바꾸지 않습니다.
- 독립 미리보기가 필요하면 흰색 조직도 영역을 최상위로 두는 별도 entry만 만듭니다(`html, body, #root { height: 100%; margin: 0 }` 같은 페이지 설정은 **미리보기 entry의 index.html/CSS**에만).
- 레이아웃 방향 등 요청된 조직도 내부 옵션만 추가: `autoLayout={{ type: "lr" }}`, `selectedCompanyIds`, `employeePopup` …
## 2. Full Page (Web + OrgChart)
영역: 페이지·Navigation·제목·필터 = Web(`@hm-design/ui`, `node_modules/@hm-design/ui/ai/` 규칙), 조직도 영역 = OrgChart.
```tsx
import { useId, useMemo, useState } from "react"
import {
Card, CardContent, Label, PageHeader, PageHeaderContent, PageHeaderTitle,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@hm-design/ui"
import { OrgChart } from "@hm-design/org-chart-react"
import { JsonOrgChartDataSource, companyColorPresets, type CompanyId, 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 OrganizationPage({ data, companyIds }: { data: OrgChartDataset; companyIds: CompanyId[] }) {
const selectId = useId()
const [company, setCompany] = useState<"all" | CompanyId>("all")
const dataSource = useMemo(() => new JsonOrgChartDataSource(data), [data])
const selected = useMemo(() => (company === "all" ? undefined : [company]), [company])
return (
<main className="app-org-page">
<PageHeader><PageHeaderContent><PageHeaderTitle>조직도</PageHeaderTitle></PageHeaderContent></PageHeader>
<Card>
<CardContent>
<Label htmlFor={selectId}>회사</Label>
<Select value={company} onValueChange={value => setCompany(value as "all" | CompanyId)}>
<SelectTrigger id={selectId} size="medium"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">전체</SelectItem>
{companyIds.map(id => <SelectItem key={id} value={id}>{companyColorPresets[id].companyName}</SelectItem>)}
</SelectContent>
</Select>
<div className="app-org-canvas">
<OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} layoutMode="auto"
selectedCompanyIds={selected} style={{ height: "100%" }} />
</div>
</CardContent>
</Card>
</main>
)
}
```
```css
/* 소비 레이아웃만. 조직도 안쪽 디자인은 건드리지 않음 */
.app-org-page { display: flex; flex-direction: column; gap: 16px; }
.app-org-canvas { height: 640px; margin-top: 16px; }
```
- 위 회사 선택 Select는 **요청된 경우의 예**입니다. 요청에 없는 필터·통계·설명을 넣지 않습니다.
- Full Page의 Navigation(GNB/LNB/DASHBOARD 중 하나)과 Web 규칙은 Web 문서를 따릅니다.
- Web 토큰(`--hm-sem-*`)을 조직도에 넣지 않습니다. 조직도 색을 맞춰야 하면 [DESIGN_RULES.md](./DESIGN_RULES.md) §3의 `--hm-orgchart-*` 입력으로 따로 연결합니다(자동 동기화 없음).
- `presentationMode`를 생략하면 Auto 기본 화면(조직도 내부 제목·요약·툴바 포함)입니다. Full Page에서 그 내장 UI가 요청 UI와 겹치면 `presentationMode="chart-only"`로 조직도 영역만 쓰고 바깥 UI는 Web으로 만듭니다.
## 3. HM Dialog 팝업 (Web Dialog)
```tsx
import { OrgChart } from "@hm-design/org-chart-react/hm-dialog"
<OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} layoutMode="auto" employeePopup="hm-dialog" />
```
- `@hm-design/ui` 0.1.0 설치 + 앱 CSS 진입점에 Web `tailwind.css`·`theme.css` 필요(`/hm-dialog`가 CSS를 자동 import하지 않음).
- `@hm-design/ui` 설치만으로 팝업이 바뀌지 않습니다. Entry와 `employeePopup`을 모두 명시합니다. Fixed는 항상 Core 팝업입니다.
## 4. 팝업 어댑터 (다른 디자인 시스템)
```tsx
import { OrgChart, type OrgChartEmployeePopupProps } from "@hm-design/org-chart-react"
function MyEmployeePopup({ selection, onClose }: OrgChartEmployeePopupProps) {
if (!selection) return null
// selection.employee / selection.context를 읽기만 한다. 모달 접근성·Escape·context.trigger로 Focus 복귀는 어댑터 책임.
return <div role="dialog" aria-modal="true" aria-label={selection.employee.name}>…<button onClick={onClose}>닫기</button></div>
}
<OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} layoutMode="auto"
employeePopup="adapter" employeePopupAdapter={MyEmployeePopup} />
```
`onEmployeeClick`을 주면 어떤 팝업 설정보다 우선합니다.
## 5. 데이터 만들기
```ts
import { validateOrgChartDataset, type OrgChartDataset } from "@hm-design/org-chart"
const data: OrgChartDataset = {
schemaVersion: "0.1-draft",
organizations: [
{ id: "hanmac-root", companyId: "hanmac", parentId: null, name: "한맥기술", type: "root", leaderId: "e-ceo", displayOrder: 0 },
{ id: "hanmac-office-1", parentId: "hanmac-root", name: "1본부", type: "office", leaderId: "e-1", displayOrder: 0 },
{ id: "hanmac-team-1", parentId: "hanmac-office-1", name: "1팀", type: "team", leaderId: null, displayOrder: 0 },
],
employees: [
{ id: "e-ceo", organizationId: "hanmac-root", affiliationId: "hanmac", name: "가상 대표", position: "대표이사", jobTitle: null, profileImage: null, workStatus: "active" },
{ id: "e-1", organizationId: "hanmac-office-1", affiliationId: "hanmac", name: "가상 본부장", position: "본부장", jobTitle: null, profileImage: null, workStatus: "active" },
{ id: "e-2", organizationId: "hanmac-team-1", affiliationId: "hanmac", name: "가상 사원", position: "사원", jobTitle: null, profileImage: null, workStatus: "active" },
],
affiliations: [{ id: "hanmac", name: "한맥기술" }],
metadata: { asOfDate: "2026-10-19", dataVersion: "example-1" },
}
const checked = validateOrgChartDataset(data)
if (!checked.valid) throw new Error(JSON.stringify(checked.issues))
```
- 필드는 위가 전부입니다(알 수 없는 필드는 Validator가 거절). 이름 규칙: `displayOrder`, `jobTitle`, `profileImage`, `workStatus`.
- 업무 명칭 "본부"는 `name`에 두고 `type: "office"`로 대응했습니다 — 이런 대응은 결과 보고에 적습니다.
- 회사 추가 = `parentId: null`인 Root를 하나 더(+ 그 회사 `affiliations` 항목). 가짜 상위 조직 금지.
- 검증 후 보고: 요청 대비 회사 수, 조직 수, Depth, 고유 직원 수(대표·조직장 포함 여부), 조직별 직속/하위 포함 인원.