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