# Web(@hm-design/ui) 조합 패턴 모든 패턴은 공개 Export만 씁니다([COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)). 코드는 형태를 보여 주는 예이며, 실제 화면에서는 **요청된 메뉴·조건·버튼만** 넣습니다(예시의 항목을 요구사항으로 복사하지 않음). ## 0. 어떤 패턴을 고를까 | 요청 | 먼저 고를 것 | 조합 | |---|---|---| | 목록 검색·필터 | `FilterBar`(표가 없어도 먼저 검토), 표가 필요하면 `DataTable` | `PageHeader` 아래 `Card` 하나 안에 FilterBar + DataTable. 조건 → 조회 → 초기화, draft/applied 분리(§3) | | 업무 Dashboard | `DashboardLayout` + `DashboardHeader` + `DashboardMain` + `DashboardContent` | Header 48px, 최소 폭 960px·내부 가로 스크롤, Sidebar 없음. 상태는 Dashboard Status 계약(§6) | | Form(등록·수정) | 공개 Form Pattern 없음 | `Card` 안 `Label` + `Input`/`Textarea`/`Select`/`Checkbox`/`RadioGroup`/`Switch`, `useId`·`htmlFor`, 오류는 `aria-invalid` + 메시지, 첫 오류 Focus(§4) | | 서비스 상단 메뉴 | `GNB` + `GNBMobileMenu` | 요청된 경로만 Desktop·Mobile에 같게. Sidebar 없음(§1) | | 계층 좌측 메뉴 | `LNB` + `LNBMobileHeader`/`LNBMobileMenu` | 3뎁스까지 `LNBGroup` 중첩(§1) | | 작업 확인 | `AlertDialog`(또는 `Dialog`) | Title/Description, 취소/진행, Escape·Trigger Focus 복귀. 취소·삭제 모두 Solid(§5) | | 측면 상세 | `Sheet` | 메뉴 대체로 쓰지 않음 | | 하단 작업 | `Drawer` | desktop 취소→실행, mobile 실행 위·취소 아래 | | 초기·부분 로딩 | `LoadingScreen` 또는 `Skeleton` / `DataTable loading` | 기존 콘텐츠 유지 여부는 요구사항으로 결정 | | 빈 목록·오류 | `DataTable emptyMessage` / 콘텐츠 HTML / `Alert` | 별도 Empty 컴포넌트 만들지 않음 | | 패키지에 없는 블록 | 소비 프로젝트 신규 | `Card`·`Button` 등 재사용 + [DESIGN_RULES.md](./DESIGN_RULES.md) 토큰(§7) | ## 1. Navigation 단일 모드 (필수) 한 페이지 = 하나의 Navigation: **GNB ONLY** · **LNB ONLY** · **DASHBOARD ONLY** 중 하나. | 모드 | 구조 | 금지 | |---|---|---| | GNB | `GNB`(+ `GNBMobileMenu`) + Content | Sidebar·LNB·Dashboard와 혼합 | | LNB | `LNB`(Desktop) + `LNBMobileHeader`/`LNBMobileMenu`(모바일 <1024px) + Content | GNB·Dashboard와 혼합 | | DASHBOARD | `DashboardLayout header={}` + Content | Sidebar·LNB·GNB(`sidebar` prop·`DashboardSidebar`는 호환성 전용) | - 요청이 모드를 말하지 않으면 화면 목적(서비스 상단 메뉴 = GNB, 계층 업무 메뉴 = LNB, 운영 현황 = DASHBOARD)으로 고르고 **선택 근거를 보고**합니다. 임의로 Sidebar를 추가하지 않습니다. - 혼합 요청(GNB + LNB 등)은 정책 충돌로 안내하고 하나를 고르게 합니다. Drawer·Hamburger 등 대체 메뉴를 추측해 만들지 않습니다. - Breadcrumb·FilterBar·사용자 Dropdown은 Navigation 모드가 아닙니다. ```tsx // LNB ONLY 골격
업무 시스템서비스명 목록 사용자
목록 사용자 서비스명
…
``` ```css /* 소비 레이아웃(높이·스크롤)만. HM 면·간격은 그대로. --hm-lnb-width는 패키지 LNB 변수 */ .app-content-stack { display: flex; flex-direction: column; min-height: 0; min-width: 0; } .app-page-content { position: relative; flex: 1; min-height: 0; min-width: 0; overflow: auto; } ``` ```tsx // GNB ONLY 골격 — NavigationMenu는 viewport={false} 필수 대시보드 업무 시스템서비스명 대시보드 홍 로그아웃 ``` ## 2. 페이지 제목 ```tsx 사용자 목록 등록된 사용자를 조회합니다. {/* 페이지 대표 실행 1개만 brand */} ``` ## 3. 목록 화면 — FilterBar + DataTable (조회 버튼 적용) `draft` = 입력 중 조건, `applied` = 목록에 적용한 조건. 조회 클릭에만 복사하고, 초기화는 두 상태와 페이지를 함께 비웁니다. ```tsx import { useMemo, useState } from "react" import { Button, Card, CardContent, DataTable, FilterBar, FilterBarActions, FilterBarSearch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useDataTable, type DataTableColumnDef, } from "@hm-design/ui" type User = { id: string; name: string; status: "active" | "inactive" } type Conditions = { query: string; status: string } const empty: Conditions = { query: "", status: "all" } const rows: User[] = [ { id: "U-1", name: "김한맥", status: "active" }, { id: "U-2", name: "이한맥", status: "inactive" }, ] const columns: DataTableColumnDef[] = [ // 모듈 상수(참조 안정) { accessorKey: "name", header: "이름" }, { accessorKey: "status", header: "상태", cell: ({ row }) => (row.original.status === "active" ? "활성" : "비활성") }, ] export function UserList() { const [draft, setDraft] = useState({ ...empty }) const [applied, setApplied] = useState({ ...empty }) const data = useMemo(() => rows.filter(row => row.name.toLocaleLowerCase().includes(applied.query.trim().toLocaleLowerCase()) && (applied.status === "all" || row.status === applied.status) ), [applied]) const table = useDataTable({ columns, data, getRowId: row => row.id }) const apply = () => { setApplied({ ...draft }); table.setPageIndex(0) } const reset = () => { setDraft({ ...empty }); setApplied({ ...empty }); table.setPageIndex(0) } return ( setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />

조회 결과 {data.length}건

) } ``` - 정렬 Header: `header: ({ column }) => `. 선택 열: `dataTableSelectColumn()`를 columns 맨 앞에. - TanStack Table **v9** API입니다(v8의 `useReactTable`·`getCoreRowModel` 예제를 쓰지 않음). 필터: `table.setGlobalFilter`, `table.getColumn(id)?.setFilterValue`, 초기화 `resetGlobalFilter(true)`·`resetColumnFilters(true)`. - `ColumnDef.size`는 th/td 폭으로 연결되지 않습니다. 긴 ID는 셀 콘텐츠에 `whitespace-nowrap` 등을 주고 실제 폭을 확인합니다. ## 4. Form ```tsx const nameId = useId() 사용자 등록 {error && } ``` - 결정 세트는 같은 `appearance`·`size`. 오류 메시지 색이 필요하면 `--hm-sem-status-error`(잠정 토큰) 또는 `Alert variant="destructive"`. ## 5. 확인창 (삭제) ```tsx 사용자를 삭제할까요? 삭제한 사용자는 복구할 수 없습니다. 취소{/* 기본 cancel + solid */} 삭제{/* 기본 solid */} ``` 일반 작성·수정 모달은 `Dialog` + `DialogHeader` + `DialogBody`(스크롤) + `DialogFooter`, 버튼은 취소 Solid + 저장 Solid(팝업에 `brand` 금지). ## 6. Dashboard ```tsx …요청된 제목·상태 필터만…
}> … ``` - 업무 상태: `dashboardStatusDefinitions`·`dashboardFilterDefinitions`·`matchesDashboardFilter`·`countDashboardFilter`로 필터·집계를 하고, 색은 `@hm-design/ui/dashboard-status.css`를 import한 뒤 `--hm-dashboard-status-active|hold|done|issue`를 소비 코드에서 Badge 등에 연결합니다(자동 스타일 없음). 새 상태색을 만들지 않습니다. ## 7. 패키지에 없는 블록 만들기 예: "통계 카드 4개". 1. 카탈로그 확인 → 통계 전용 컴포넌트 없음 → **조합**: `Card` + `CardHeader`/`CardTitle` + `CardContent`. 2. 숫자 강조 등 Card 밖 스타일이 필요하면 소비 프로젝트 class(`app-stat-value`)를 만들고 토큰만 읽습니다. ```css .app-stat-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .app-stat-value { color: var(--hm-sem-fg-primary); font-weight: var(--hm-font-weight-bold); } .app-stat-label { color: var(--hm-sem-fg-secondary); } ``` 3. 간격 값(16px 등)은 레이아웃용입니다. 컴포넌트 내부 padding·radius는 바꾸지 않습니다. 4. 결과 보고에 "신규: `StatCard`(소비 프로젝트) — Card 조합, 토큰 `--hm-sem-fg-primary`, `--hm-sem-fg-secondary`, `--hm-font-weight-bold`"처럼 남깁니다. ## 8. 혼합 사용과 함께 쓸 때 - **Web + OrgChart:** 페이지·제목·필터는 이 문서대로, 조직도 영역은 `@hm-design/org-chart/ai/COMPOSITION_PATTERNS.md` "Full Page". 조직도 안쪽에 Web 토큰을 넣지 않습니다. - **Web + SW:** SW Popup 안 하단 버튼 등 SW 문서가 허용한 지점에서만 Web 컴포넌트를 씁니다. Web CSS(Tailwind base 포함)는 문서 전체에 적용되므로 SW 영역을 브라우저에서 확인합니다([AI_GUIDE.md](./AI_GUIDE.md) §5).