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

9.3 KiB

OrgChart 조합 패턴

모든 패턴은 공개 Export만 씁니다(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)
기존 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, 고유 직원 수(대표·조직장 포함 여부), 조직별 직속/하위 포함 인원.