Files
HMDesign/packages/org-chart/ai/COMPOSITION_PATTERNS.md
T
shkim4 fb74cd756f docs(ai): add packaged AI guides for Web, SW and OrgChart
- Shared entry point docs/ai/AI_GUIDE.md (type selection Web / SW / OrgChart / mixed,
  steps 1-7, package style separation, report format, examples A-D) and
  AGENTS_SNIPPET.md, copied into every package ai/ folder
- New ai/ docs for @hm-design/ui and @hm-design/org-chart (+ org-chart-react pointer):
  AI_USAGE, COMPONENT_CATALOG, DESIGN_RULES, COMPOSITION_PATTERNS
- SW ai/ linked to the shared guide; catalog lists every Props type explicitly
- "ai" added to package files of ui, org-chart, org-chart-react
- scripts/ai-docs.mjs: sync:ai-docs / check:ai-docs (copies, files, catalog <-> exports, links)
- Maintenance guide and verification record under docs/ai
2026-10-19 17:13:36 +09:00

9.2 KiB

OrgChart 조합 패턴

모든 패턴은 공개 Export만 씁니다(COMPONENT_CATALOG.md). 예시 데이터는 가상이며 회사·분기·인원을 기본값으로 쓰지 않습니다.

0. 어떤 패턴을 고를까

요청 패턴
조직도만 기존 페이지에 삽입 1. Chart Only (Auto)
조직도가 들어간 새 페이지 전체 2. Full Page (Web + OrgChart)
직원 정보 팝업을 Web 디자인으로 3. HM Dialog 팝업
다른 디자인 시스템의 팝업 4. 팝업 어댑터
자연어·CSV·Excel → 데이터 5. 데이터 만들기
기존 Workdesk(고정) 구성 layoutMode="fixed"(기본), displayMode="readOnly" 등 — Chart Only 불가

1. Chart Only (Auto) — 조직도 영역만

React

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 없음)

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.

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>
  )
}
/* 소비 레이아웃만. 조직도 안쪽 디자인은 건드리지 않음 */
.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 §3의 --hm-orgchart-* 입력으로 따로 연결합니다(자동 동기화 없음).
  • presentationMode를 생략하면 Auto 기본 화면(조직도 내부 제목·요약·툴바 포함)입니다. Full Page에서 그 내장 UI가 요청 UI와 겹치면 presentationMode="chart-only"로 조직도 영역만 쓰고 바깥 UI는 Web으로 만듭니다.

3. HM Dialog 팝업 (Web Dialog)

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. 팝업 어댑터 (다른 디자인 시스템)

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. 데이터 만들기

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, 고유 직원 수(대표·조직장 포함 여부), 조직별 직속/하위 포함 인원.