@t3q-ui/design-system 1.56.1 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/patterns/listPageRules.ts","../../src/patterns/states.tsx","../../src/patterns/listPage.tsx","../../src/patterns/detailPage.tsx","../../src/patterns/formShell.tsx","../../src/patterns/formPage.tsx","../../src/patterns/formDrawer.tsx","../../src/patterns/formDialog.tsx","../../src/patterns/dashboardLayout.tsx"],"names":["jsxs","jsx","useRef","Fragment","useId"],"mappings":"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","file":"index.js","sourcesContent":["import type { ColDef, RowClassParams, RowClassRules } from 'ag-grid-community'\n\nimport { TwoLineCellRenderer } from '../components/agGridTable'\n\n// ListPage 의 기본값 규칙 — 패턴 내부용 (공개 API 아님). listPage.tsx 와 listPage.test.tsx 가 쓴다.\n\n/** primaryField 기본값: 명시값(null = 끔)이 우선, 없으면 행 클릭이 있을 때 'name' */\nexport function resolvePrimaryField(primaryField: string | null | undefined, hasRowClick: boolean) {\n if (primaryField !== undefined) return primaryField\n return hasRowClick ? 'name' : undefined\n}\n\n/** `primaryField` 와 field 가 같은 열에 `type: 'primary'` 를 붙인다 (이미 type 이 있으면 건드리지 않는다) */\nexport function withPrimaryColumn<TRow>(\n columns: ColDef<TRow>[],\n primaryField?: string | null,\n): ColDef<TRow>[] {\n if (!primaryField) return columns\n return columns.map((c) =>\n c.field === primaryField && c.type == null ? { ...c, type: 'primary' } : c,\n )\n}\n\n/**\n * `secondaryField` 가 있으면 주 식별자 열에 두 줄 렌더러(TwoLineCellRenderer)를 붙인다 — 이름 아래 12px 회색 보조 줄(2026-10-18 13차 ⑥).\n * 그 열이 이미 `cellRenderer` 를 가졌으면 건드리지 않는다(소비 코드의 렌더러가 우선)\n */\nexport function withSecondaryLine<TRow>(\n columns: ColDef<TRow>[],\n primaryField: string | null | undefined,\n secondaryField: string | undefined,\n): ColDef<TRow>[] {\n if (!primaryField || !secondaryField) return columns\n return columns.map((c) =>\n c.field === primaryField && c.cellRenderer == null\n ? {\n ...c,\n cellRenderer: TwoLineCellRenderer,\n cellRendererParams: { ...c.cellRendererParams, secondaryField },\n }\n : c,\n )\n}\n\n/**\n * 상세가 열린 행(`row.id === activeRowId`)에 `ag-row-active` 클래스를 붙이는 규칙. 사용자 rowClassRules 는 보존한다.\n * ag-grid 는 rowClassRules 옵션이 바뀌면 행 클래스를 다시 계산하므로, activeRowId 가 바뀔 때 새 객체를 만들어 넘긴다.\n */\nexport function activeRowClassRules<TRow>(\n activeRowId: string | number | null | undefined,\n userRules?: RowClassRules<TRow>,\n): RowClassRules<TRow> | undefined {\n if (activeRowId == null) return userRules\n return {\n ...userRules,\n 'ag-row-active': (p: RowClassParams<TRow>) =>\n (p.data as { id?: unknown } | undefined)?.id === activeRowId,\n }\n}\n\n/** 빈 결과에 \"필터 초기화\" 버튼을 둘지: 초기화 핸들러가 있고 필터가 걸려 있을 때(filtersActive 가 false 가 아닐 때) */\nexport const showEmptyReset = (onResetFilters: unknown, filtersActive: boolean | undefined) =>\n Boolean(onResetFilters) && filtersActive !== false\n\n/**\n * 빈 결과 문자열을 제목·설명으로 나눈다. 첫 문장 끝(마침표·물음표·느낌표 뒤 공백)에서 한 번만 나누고, 제목 끝의 마침표는 뗀다.\n * \"검색 조건에 맞는 모델이 없습니다. 조건을 바꾸거나 필터를 초기화하세요.\" → 제목 \"검색 조건에 맞는 모델이 없습니다\" + 설명 \"조건을 …\".\n * 문장 하나면 제목만. 소비 코드가 문장만 넘겨도(2026-10-08 실험 36 run 전부) 패턴이 두 줄 구성으로 그리기 위한 규칙.\n */\nexport function splitEmptyText(text: string): { title: string; description?: string } {\n const t = text.trim()\n const m = /^(.*?[.!?。])\\s+(\\S[\\s\\S]*)$/.exec(t)\n const strip = (s: string) => s.replace(/[.。]$/, '')\n if (!m) return { title: strip(t) }\n return { title: strip(m[1]), description: m[2] }\n}\n","import type { ReactNode } from 'react'\n\nimport { Button } from '../components/button'\nimport { EmptyBoxIllustration } from '../components/emptyBoxIllustration'\nimport { LucideIcon } from '../components/lucideIcon'\nimport { Skeleton } from '../components/skeleton'\nimport { cn } from '../utils/cn'\n\n/**\n * 패턴 공통 로딩 자리표시. 스크린리더에는 \"불러오는 중\" 으로 읽힌다.\n *\n * 언제 쓰나: 패턴 밖에서 본문 하나를 통째로 기다릴 때(패턴 안에서는 `loading` prop 이 이것을 그린다).\n * 언제 안 쓰나: 표 안의 행 로딩(→ AgGridTable 의 로딩 오버레이), 버튼 안의 진행(→ Button `loading`).\n * 대신/함께: 줄 수는 `lines` 로. 자리 크기를 실제 내용과 비슷하게 맞추면 로딩 끝에 화면이 덜 튄다.\n */\nexport function LoadingState({ className, lines = 3 }: { className?: string; lines?: number }) {\n return (\n <div\n role='status'\n aria-label='불러오는 중'\n className={cn('flex flex-col gap-[12px]', className)}\n >\n <Skeleton className='h-[24px] w-[240px]' />\n {Array.from({ length: lines }, (_, i) => (\n <Skeleton key={i} className={cn('h-[16px]', i % 2 ? 'w-3/5' : 'w-full')} />\n ))}\n </div>\n )\n}\n\ntype StateBoxProps = {\n role: 'status' | 'alert'\n className?: string\n icon: ReactNode\n iconClassName?: string\n title: ReactNode\n description?: ReactNode\n action?: ReactNode\n}\n\n/** 빈 결과·오류가 같이 쓰는 가운데 상자. 아이콘 → 제목 → 설명 → 행동 버튼 */\nfunction StateBox({\n role,\n className,\n icon,\n iconClassName,\n title,\n description,\n action,\n}: StateBoxProps) {\n return (\n <div\n role={role}\n className={cn(\n 'flex flex-col items-center justify-center gap-[4px] text-center text-sys-text-subtler',\n className,\n )}\n >\n <span aria-hidden className={cn('mb-[4px] flex text-sys-text-subtler', iconClassName)}>\n {icon}\n </span>\n <span className='text-h5 text-sys-text-main'>{title}</span>\n {description ? (\n <span className='break-keep text-base-regular text-sys-text-subtle wrap-break-word'>\n {description}\n </span>\n ) : null}\n {action ? <div className='pointer-events-auto mt-[12px] flex gap-[8px]'>{action}</div> : null}\n </div>\n )\n}\n\n/**\n * 패턴 공통 빈 결과·없음 안내. 가운데 정렬. 아이콘 → 제목 → 설명 → 행동 버튼 순.\n * 제목만 있으면 한 줄 안내, 설명까지 있으면 \"무엇이 없고 다음에 뭘 하나\" 를 두 줄로 — 2026-10-10 진단에서 두 평가자가 모두 더 낫다고 본 구성.\n *\n * 언제 쓰나: 목록이 0건, 선택지가 없음, 항목이 삭제됨처럼 \"보여 줄 것이 없다\" 를 알릴 때. 패턴(ListPage·DetailPage·DashboardLayout·DashboardPanel)은 `emptyState`·`notFound`·`empty` prop 으로 이것을 그린다.\n * 언제 안 쓰나: 불러오기 실패(→ ErrorState), 아직 불러오는 중(→ LoadingState), 입력 검증 실패(→ FormMessage).\n * 대신/함께: `<div>` 에 문장만 넣어 직접 만들지 않는다. 다음 행동이 있으면 `action` 에 버튼 하나(예: 필터 초기화·새로 만들기).\n */\nexport function EmptyState({\n className,\n children,\n icon,\n title,\n description,\n action,\n}: {\n className?: string\n /** 제목 자리(`title` 과 같다). 기존 호출부 호환용 */\n children?: ReactNode\n /** 제목 위 그림. 기본은 상자 일러스트 `EmptyBoxIllustration`(라이트·다크 토큰, 2026-10-13 — dl-ui-v3 사용자 정보 화면의 빈 패널과 같은 그림). 장식이라 스크린리더에는 읽히지 않는다 */\n icon?: ReactNode\n /** 무엇이 없는지 한 줄 (예: \"조건에 맞는 모델이 없습니다\") */\n title?: ReactNode\n /** 다음에 할 수 있는 일 (예: \"검색어나 필터를 바꾸거나 초기화해 보세요\") */\n description?: ReactNode\n /** 설명 아래 행동 버튼 (예: 필터 초기화). ag-grid 오버레이 안에서도 눌리게 pointer-events 를 연다 */\n action?: ReactNode\n}) {\n return (\n <StateBox\n role='status'\n className={className}\n icon={icon ?? <EmptyBoxIllustration />}\n title={title ?? children}\n description={description}\n action={action}\n />\n )\n}\n\n/**\n * 패턴 공통 오류 안내. 빈 결과 상자와 같은 배치(아이콘 → 제목 → 설명 → 다시 시도)에 오류 색 아이콘. `role='alert'`.\n *\n * 언제 쓰나: 화면·패널 본문을 통째로 못 불러왔을 때(패턴은 `error` prop 으로 이것을 그린다).\n * 언제 안 쓰나: 폼 섹션 안의 부분 실패(선택지 목록 로딩 → Alert(danger) + AlertActions 의 다시 시도 — 상자가 섹션을 밀어내지 않게, 2026-10-13), 제출 실패·서버 응답 오류처럼 화면은 그대로 두고 알려야 할 때(→ Alert(danger)·Toast), 입력 검증 실패(→ FormMessage).\n * 대신/함께: 빨간 Alert 줄로 본문을 대체하지 않는다 — 2026-10-12 블라인드 평가에서 가운데 상자가 일관되게 선호됐다. `onRetry` 를 주면 \"다시 시도\" 버튼이 붙는다. 원인·코드가 있으면 `description` 에 한 줄로.\n */\nexport function ErrorState({\n className,\n title = '불러오지 못했습니다',\n description,\n onRetry,\n retryLabel = '다시 시도',\n}: {\n className?: string\n /** 무엇이 실패했는지 한 줄. 기본 \"불러오지 못했습니다\" */\n title?: ReactNode\n /** 원인·안내 (예: \"서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.\") */\n description?: ReactNode\n /** 주면 \"다시 시도\" 버튼이 붙는다 */\n onRetry?: () => void\n /** 다시 시도 버튼 라벨. 기본 \"다시 시도\" */\n retryLabel?: ReactNode\n}) {\n return (\n <StateBox\n role='alert'\n className={className}\n icon={<LucideIcon iconComponentName='CircleAlert' size={36} />}\n iconClassName='text-danger30'\n title={title}\n description={description}\n action={\n onRetry ? (\n <Button type='button' variant='primary' onClick={onRetry}>\n {retryLabel}\n </Button>\n ) : null\n }\n />\n )\n}\n","'use client'\n\nimport { isValidElement, type ReactNode, useEffect, useMemo, useRef } from 'react'\nimport type { ColDef, RowClickedEvent, SortChangedEvent } from 'ag-grid-community'\nimport { RotateCcw, X } from 'lucide-react'\n\nimport { AgGridTable, type AgGridTableType, getSortedColumnState } from '../components/agGridTable'\nimport { Alert, AlertDescription } from '../components/alert'\nimport type { BadgeProps } from '../components/badge'\nimport { Button } from '../components/button'\nimport { IconButton } from '../components/iconButton'\nimport { PageHeader } from '../components/pageHeader'\nimport { Pagination } from '../components/pagination'\nimport { Panel } from '../components/panel'\nimport { TablePagination } from '../components/table'\nimport { ToggleGroup, ToggleGroupItem } from '../components/toggleGroup'\nimport { cn } from '../utils/cn'\nimport {\n activeRowClassRules,\n resolvePrimaryField,\n showEmptyReset,\n splitEmptyText,\n withPrimaryColumn,\n withSecondaryLine,\n} from './listPageRules'\nimport { EmptyState } from './states'\n\n/** 행을 눌러 보는 상세 — 목록 **오른쪽에 나란히** 그리는 패널. 표가 좁아지고 목록은 계속 보인다(덮는 Drawer 와 다름) */\nexport type ListPageDetail = {\n /** 패널 제목 (h2). 패널의 접근성 이름 */\n title: ReactNode\n /** 제목 아래 한 줄 (id·이미지 등) */\n description?: ReactNode\n /** 닫기 — 오른쪽 위 닫기 버튼과 Escape 가 부른다. 포커스는 옮기지 않는다(행에 남음) */\n onClose: () => void\n /** 본문 (보통 MetaList + MetaRow) */\n children: ReactNode\n /** 버튼 줄 위 안내 한 줄 — 비활성 버튼의 사유 자리 (예: \"실행 중인 환경만 중지할 수 있습니다\"). 비활성 버튼에는 tooltip 이 뜨지 않는다 */\n note?: ReactNode\n /** 버튼 줄 (오른쪽 정렬, 간격 8px) */\n footer?: ReactNode\n /** 패널 폭. small 360 · medium 480(기본) — Drawer size 와 같은 값 */\n width?: 'small' | 'medium'\n}\nconst DETAIL_WIDTH: Record<NonNullable<ListPageDetail['width']>, string> = {\n small: 'w-[360px]',\n medium: 'w-[480px]',\n}\n\n/** 빈 결과의 제목·설명을 직접 줄 때의 형태 */\nexport type ListPageEmptyContent = {\n /** 무엇이 없는지 (예: \"조건에 맞는 모델이 없습니다\") */\n title: ReactNode\n /** 다음에 할 수 있는 일 (예: \"검색어나 필터를 바꾸거나 초기화해 보세요\") */\n description?: ReactNode\n}\n\n/** 표 왼쪽 위의 빠른 필터(상태처럼 선택지 3~6개). 드롭다운과 달리 선택지·선택 상태·건수가 항상 보인다 */\nexport type ListPageQuickFilter = {\n /** 필터 대상 이름 (예: '상태'). 세그먼트 앞에 글자로 보이고 그룹의 접근성 이름이 된다 */\n label: string\n /** 선택지. `count` 를 주면 항목 오른쪽에 건수 Badge — \"지금 몇 건이 어떤 상태인가\" (2026-10-11 진단에서 건수가 있을 때 두 평가자 모두 종합 선호가 올랐다) */\n options: {\n value: string\n label: string\n count?: number\n /** 건수 pill 뱃지의 색. 기본 secondary. **표의 상태 뱃지와 같은 tone**(`success`·`danger`·`warning`·`info`·`secondary`)을 주면 칩과 상태 열이 같은 색 언어가 된다(예제 list-page.tsx 의 `STATUS[s].tone`). \"전체\" 는 패턴이 primary 로 그린다 (2026-10-18 13차 D, 사용자 결정) */\n countVariant?: NonNullable<BadgeProps['variant']>\n }[]\n /** 선택된 값들 (다중 선택). 빈 배열 = 전체 */\n value: string[]\n /** 선택이 바뀔 때. 소비 앱은 `pagination.page` 를 1로 되돌리고 `filtersActive` 에 이 필터도 포함한다 */\n onChange: (next: string[]) => void\n}\n\n/** quickFilter 의 \"전체\" 세그먼트 값 — 소비 앱의 `value` 배열에는 절대 들어가지 않는다(onChange 전에 걷어낸다) */\nconst QUICK_ALL = '__all__'\n\nconst isEmptyContent = (v: unknown): v is ListPageEmptyContent =>\n typeof v === 'object' && v !== null && !Array.isArray(v) && !isValidElement(v) && 'title' in v\n\n/** emptyState prop → 한 줄로 이어 붙일 제목·설명·(그 밖의 노드) */\nfunction resolveEmptyContent(emptyState: ReactNode | ListPageEmptyContent | undefined): {\n title?: ReactNode\n description?: ReactNode\n children?: ReactNode\n} {\n if (emptyState == null) return splitEmptyText('데이터가 없습니다.')\n if (typeof emptyState === 'string') return splitEmptyText(emptyState)\n if (isEmptyContent(emptyState)) return emptyState\n return { children: emptyState }\n}\n\nexport type ListPageProps<TRow> = {\n /** 페이지 제목 (h1) */\n title: string\n /** 제목 아래 설명 */\n description?: ReactNode\n /** 제목 오른쪽 슬롯 (예: 새로고침 버튼·마지막 갱신 시각) */\n titleRight?: ReactNode\n /** 주요 행동 버튼 (보통 1개). 제목 줄 오른쪽(PageHeader `actions`) — 2026-10-19 14차: 페이지 수준 행동은 제목 줄(Polaris 인덱스 페이지·GitHub Issues·기준선과 같은 자리). 1.22.0~1.47.4 의 필터 줄 왼쪽(SharePoint 계열)은 2026-10-13 UIClip(폐기된 자) 근거였다 */\n actions?: ReactNode\n /** 검색·필터 컨트롤. 헤더 오른쪽 아래 (TableFilter*, ColumnFilter 등) */\n filters?: ReactNode\n /** 필터를 되돌리는 핸들러. 주면 빠른 필터 줄(표 바로 위) 오른쪽 끝에 \"필터 초기화\" 외곽선 버튼이 **항상** 자리를 잡는다(빠른 필터·건수가 없으면 filters 줄 끝). 검색창은 그래서 표 오른쪽 끝까지 닿는다(2026-10-18 10차). 필터가 걸렸을 때만 조건부로 넘기지 말고(빈 결과에서 버튼이 튀어나와 배치가 바뀐다) 항상 넘기고 `filtersActive` 로 보일지를 정한다 */\n onResetFilters?: () => void\n /** 필터가 하나라도 걸려 있는가. false 면 \"필터 초기화\" 버튼이 자리만 지킨 채 보이지 않는다(invisible) */\n filtersActive?: boolean\n /** 주 식별자 열의 field. 그 열에 `type: 'primary'`(굵게, 행 hover 시 밑줄)를 붙인다. 기본값: `onRowClick` 이 있으면 `'name'`, 없으면 없음. 끄려면 `null` */\n primaryField?: string | null\n /** 주 식별자 열 아래 **보조 줄**에 보일 행 필드(id·이미지·경로·설명, 점 표기 가능). 열을 하나 더 늘리는 대신 이름 아래 12px 회색 한 줄로 — dl-ui-v3 목록(이름 / 설명)과 같은 모양이고 열이 적어 잘림도 준다(2026-10-18 13차 ⑥). 주 열(`primaryField`, 기본 `name`)에 `cellRenderer` 가 이미 있으면 적용하지 않는다. 주면 행 높이 56(`tableProps.rowHeight` 가 우선) */\n secondaryField?: string\n /** 표 위 제목. **쓰지 않는다** — 헤더 제목(h1)과 중복이고 빠른 필터 옆에서 \"tightly packed\" 로 감점(2026-10-18 10차, 사용자 결정). 남겨 둔 건 호환용. `pagination.total` 이 있으면 같은 줄 오른쪽에 \"총 N개\" 가 붙는다(제목·빠른 필터 둘 다 없으면 윗줄 자체를 안 그린다 — 2026-10-27) — 단 `quickFilter` 가 항목마다 `count` 를 보여 주면 \"전체 N\" 과 같은 숫자라 생략한다 */\n tableTitle?: ReactNode\n /** 표 왼쪽 위(제목 줄)의 빠른 필터 — 상태처럼 선택지 3~6개의 다중 선택. ToggleGroup(붙어있는 세그먼트)으로 그려 드롭다운을 열지 않아도 선택지가 보인다. 선택지가 많거나 검색이 필요한 축은 `filters` 의 TableFilterMultiSelect 로. `tableTitle` 이 없으면 이 줄을 세그먼트가 대신한다(줄이 늘지 않음) */\n quickFilter?: ListPageQuickFilter\n /** 상세가 열린 행의 id (`row.id` 와 비교). 그 행을 표의 선택 색으로 칠해 \"지금 어느 행을 보고 있나\" 를 보인다. 상세를 닫으면 null */\n activeRowId?: string | number | null\n /** 행을 눌러 보는 상세 패널. 주면 목록 **오른쪽에 나란히** 그린다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape). 닫으면 undefined 로. 2026-10-16(이슈 45) — 덮는 `<Drawer modal={false}>` 는 표를 좁히면 안 될 때의 대안 */\n detail?: ListPageDetail\n /** 표 컬럼 정의 (ag-grid ColDef) */\n columns: ColDef<TRow>[]\n /** 현재 페이지 행. undefined 면 표가 로딩 오버레이를 보인다 */\n rows?: TRow[]\n /** true 면 표 자리에 TableSkeleton */\n loading?: boolean\n /** 주면 표 대신 Alert(danger) 로 표시한다 */\n error?: ReactNode\n /** 0건일 때 표 안에 그릴 안내. 패턴이 **한 줄 문장**(+ 필터가 걸려 있으면 링크형 \"필터 초기화\")으로 그린다 — 아이콘·상자는 그리지 않는다(1.29.0 의 상자는 2026-10-13 되돌림). 문자열 또는 `{ title, description }`(이어 붙여 한 줄). 버튼·아이콘은 넣지 않는다. 기본 \"데이터가 없습니다\" */\n emptyState?: ReactNode | ListPageEmptyContent\n /** 페이지네이션. 없으면 그리지 않는다 */\n pagination?: {\n /** 전체 행 수 */\n total: number\n /** 현재 페이지 (1부터) */\n page: number\n /** 페이지당 행 수 */\n pageSize: number\n /** 페이지 변경 */\n onChange: (page: number) => void\n }\n /** 정렬 변경. 값은 getSortedColumnState 규칙 ('name' | '-name' | '') */\n onSortChange?: (sort: string) => void\n /** 행 클릭 */\n onRowClick?: (row: TRow) => void\n /** AgGridTable 에 그대로 넘길 나머지 props (rowHeight, rowSelection 등). 표 높이는 기본 `domLayout='autoHeight'`(행 수만큼) — 고정 높이는 `domLayout='normal'` + `containerStyle` */\n tableProps?: Omit<\n AgGridTableType,\n | 'rowData'\n | 'columnDefs'\n | 'loading'\n | 'noRowsOverlayComponent'\n | 'onSortChanged'\n | 'onRowClicked'\n >\n}\n\n/**\n * 목록 화면 패턴. PageHeader(제목·설명·주요 버튼·필터 줄) → AgGridTable → Pagination 을 한 번에 배치한다.\n *\n * 언제 쓰나: 실행환경·모델·파이프라인·데이터셋처럼 \"검색/필터 → 표 → 상세 진입\" 흐름이 있는 리소스 목록. 컬럼·행·필터 컨트롤·데이터 조회는 소비 앱이 넘기고, 패턴은 배치와 로딩·빈 결과·오류 상태만 책임진다.\n * 언제 안 쓰나: 차트·요약 카드 중심의 대시보드, 한 항목의 상세 화면, 표가 없는 설정 화면.\n * 대신/함께: 행 클릭 뒤 상세는 `onRowClick` 에서 상세 페이지로 이동하는 것이 기본이고, 목록 맥락을 유지해야 할 때는 `detail` 슬롯으로 목록 오른쪽에 **나란히** 연다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape, 비활성 사유는 `detail.note`). 표를 좁히면 안 될 때만 `<Drawer modal={false}>`(덮는 옆 패널)로(어느 쪽인지는 화면 설계가 정한다). 필터가 바뀌면 소비 앱이 `pagination.page` 를 1로 되돌린다. `onResetFilters` 는 항상 넘기고 `filtersActive` 로 보일지를 정한다(필터가 걸린 채 0건이면 빈 결과 가운데에도 \"필터 초기화\" 가 나온다). 상세를 Drawer 로 열면 `activeRowId` 로 그 행을 표시한다. 주 식별자 열은 `primaryField`(행 클릭이 있으면 기본 `'name'`), 표 위 제목·건수는 `tableTitle`·`pagination.total`. 상태처럼 선택지가 3~6개인 다중 선택 필터는 `quickFilter` 로 표 왼쪽 위에 세그먼트로 둔다(드롭다운에 숨기지 않는다). 본문은 제목을 이름으로 가진 `section` landmark 다(소비 앱의 `<main>` 안에 둔다). 표는 한 페이지의 행을 다 보이도록 행 수만큼 자란다(`domLayout='autoHeight'` 기본) — 화면 높이에 맞춘 안쪽 스크롤이 필요하면 `tableProps` 로 `domLayout='normal'` 과 `containerStyle` 을 넘긴다.\n */\nexport function ListPage<TRow>({\n title,\n description,\n titleRight,\n actions,\n filters,\n onResetFilters,\n columns,\n rows,\n loading,\n error,\n emptyState,\n pagination,\n filtersActive,\n primaryField,\n secondaryField,\n tableTitle,\n quickFilter,\n activeRowId,\n detail,\n onSortChange,\n onRowClick,\n tableProps,\n}: ListPageProps<TRow>) {\n // detail 이 열린 동안 Escape 로 닫는다(목록 키보드 규약: Escape 가 상세를 닫고 포커스는 행에 남는다). 열린 dialog 안에서 누른 Escape 는\n // 그쪽 몫이라 무시(Radix 가 preventDefault 하거나 target 이 dialog 안). 핸들러는 ref 로 읽어 리스너를 매 렌더 다시 달지 않는다\n const onCloseRef = useRef(detail?.onClose)\n useEffect(() => {\n onCloseRef.current = detail?.onClose\n }, [detail?.onClose])\n const detailOpen = detail != null\n useEffect(() => {\n if (!detailOpen) return\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape' || e.defaultPrevented) return\n const target = e.target as HTMLElement | null\n if (target?.closest?.('[role=\"dialog\"],[role=\"alertdialog\"]')) return\n onCloseRef.current?.()\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [detailOpen])\n // \"필터 초기화\" 는 빠른 필터 줄(표 바로 위)의 **오른쪽 끝** — 검색 줄에 두면 보이지 않을 때도 자리를 차지해 검색창이 표 오른쪽 끝에\n // 못 닿는다(9차 심판 \"search field's right edge doesn't line up with the table\"). 자리는 항상 유지(filtersActive 로 비활성).\n // 2026-10-16 이슈 47: 검색창 앞에 두었을 때 \"검색창 앞에 어색하게\" 감점 → 2026-10-18 10차 ④: 빠른 필터 줄로. 그 줄이 없을 때만 검색 줄 끝\n const resetButton = onResetFilters ? (\n <Button\n type='button'\n // outline: 옆의 외곽선 칩·검색창과 같은 꼴(2026-10-17 2차에서 회색 채움 링크 버튼이 \"외곽선 검색창과 안 맞음\" 감점).\n // 필터가 없으면 자리만 두고 보이지 않는다(invisible) — 비활성 회색 채움도 감점됐다(4차). 자리를 비우지 않으니 배치는 안 바뀐다\n variant='secondaryOutline'\n onClick={onResetFilters}\n className={cn(filtersActive === false && 'invisible')}\n aria-hidden={filtersActive === false || undefined}\n >\n <RotateCcw size={16} aria-hidden />\n 필터 초기화\n </Button>\n ) : null\n // 제목도 빠른 필터도 없으면 윗줄을 그리지 않는다 — \"총 N개\" 만 남은 줄이 표 위 여백이 되고(2026-10-27 홀드아웃 11, 심판 2건) 같은 숫자는 아래 페이지네이션 라벨(\"전체 N개 중 X-Y번째\")에 있다\n const hasCaption = tableTitle != null || quickFilter != null\n const filterRow =\n filters || (resetButton && !hasCaption) ? (\n <div className='flex w-full items-start gap-[8px]'>\n {filters}\n {!hasCaption ? resetButton : null}\n </div>\n ) : undefined\n\n const columnDefs = useMemo(() => {\n // 행을 눌러 상세로 가는 목록이면 name 열이 기본 주 식별자 (2026-10-09 실험 36/36 이 이 구성). 보조 줄(secondaryField)을 주면 그 열이 주 식별자다\n const primary = resolvePrimaryField(\n primaryField,\n Boolean(onRowClick) || Boolean(secondaryField),\n )\n return withSecondaryLine(withPrimaryColumn(columns, primary), primary, secondaryField)\n }, [columns, primaryField, onRowClick, secondaryField])\n const total = pagination?.total\n // 빠른 필터: 표 왼쪽 위 세그먼트. 2026-10-11 진단(docs/results/diagnostic-status-toggle.md) — 드롭다운 대비 Ease of Use·Intuitive 가\n // 두 평가자 모두 올랐고 건수가 있을 때 종합 선호도 올랐다. 제목 줄에 두어 tableTitle 이 없으면 줄이 늘지 않는다\n // \"전체\" 세그먼트: 선택이 비었을 때 켜져 보여 \"지금 전체를 보고 있다\" 가 드러난다(2026-10-16 이슈 47, 원문 심판 6회 감점). 누르면 선택을 비운다.\n // 건수는 모든 선택지에 count 가 있을 때만 합계\n const quickAllCount = quickFilter?.options.every((o) => o.count !== undefined)\n ? quickFilter.options.reduce((sum, o) => sum + (o.count ?? 0), 0)\n : undefined\n const quickIsAll = (quickFilter?.value.length ?? 0) === 0\n const quick = quickFilter ? (\n <span className='flex items-center gap-[8px]'>\n <span className='text-small-regular font-medium text-sys-text-subtle'>\n {quickFilter.label}\n </span>\n <ToggleGroup\n type='multiple'\n variant='outline'\n // 떨어진 외곽선 pill(spacing): 한 테두리로 묶인 그룹은 옆의 외곽선 버튼·검색창과 다른 꼴로 읽혔다 (2026-10-17 이슈 47 4차)\n spacing\n aria-label={quickFilter.label}\n value={quickIsAll ? [QUICK_ALL] : quickFilter.value}\n onValueChange={(next) => {\n // 전체가 꺼진 상태에서 전체를 누름 → 선택을 비움. 그 외에는 전체 표식만 걷어낸 선택을 넘김(비어지면 다시 전체)\n const pressedAll = next.includes(QUICK_ALL) && !quickIsAll\n quickFilter.onChange(pressedAll ? [] : next.filter((v) => v !== QUICK_ALL))\n }}\n >\n <ToggleGroupItem value={QUICK_ALL} count={quickAllCount} countVariant='primary'>\n 전체\n </ToggleGroupItem>\n {quickFilter.options.map((o) => (\n <ToggleGroupItem\n key={o.value}\n value={o.value}\n count={o.count}\n countVariant={o.countVariant}\n >\n {o.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </span>\n ) : null\n // 표 제목 줄과 표 사이 8(붙어 있는 요소), 페이지네이션 위 32, 헤더→본문 32 — 2026-10-17 8차 D2 간격 세 단계\n const caption = hasCaption ? (\n <div className='mb-[8px] flex items-center justify-between gap-[8px]'>\n {tableTitle != null || quick ? (\n <div className='flex flex-wrap items-center gap-[16px]'>\n {tableTitle != null ? <h2 className='text-h6 text-sys-text-main'>{tableTitle}</h2> : null}\n {quick}\n </div>\n ) : (\n <span />\n )}\n <div className='flex shrink-0 items-center gap-[16px]'>\n {/* 빠른 필터가 \"전체 N\" 건수를 그리면 \"총 N개\" 는 같은 숫자가 한 줄에 두 번 — 원문 심판 copy 감점(2026-10-17 이슈 47 6차) → 생략 */}\n {total != null && quickAllCount === undefined ? (\n <span className='text-small-regular text-sys-text-subtle'>\n 총 {total.toLocaleString()}개\n </span>\n ) : null}\n {resetButton}\n </div>\n </div>\n ) : null\n\n // 빈 결과의 \"필터 초기화\": 필터가 걸린 채 0건이면 표 가운데에도 복구 버튼을 둔다 (2026-10-09 Q1 평가 1순위 지적).\n // 핸들러는 ref 로 읽어 오버레이 컴포넌트가 매 렌더 새로 만들어지지 않게 한다\n const resetRef = useRef(onResetFilters)\n useEffect(() => {\n resetRef.current = onResetFilters\n }, [onResetFilters])\n const withReset = showEmptyReset(onResetFilters, filtersActive)\n\n // ag-grid 는 컴포넌트 참조가 바뀌면 오버레이를 다시 만들므로 내용이 바뀔 때만 새로 만든다.\n // 2026-10-13 07:30(사용자 결정): 빈 결과도 EmptyState 상자(상자 일러스트 + 제목 + 설명 + 링크형 \"필터 초기화\").\n // 1.29.0 의 Lucide 아이콘 상자는 UIClip 에서 떨어져 한 줄 문장으로 되돌렸었는데(9-B), 그림이 dl-ui-v3 의 빈 상태 일러스트로 바뀌어 다시 상자로 — 정본 측정은 README \"패턴 정본\" 절\n const EmptyOverlay = useMemo(\n () =>\n function ListPageEmptyOverlay() {\n const content = resolveEmptyContent(emptyState)\n return (\n <EmptyState\n className='pt-[32px] pb-[16px]'\n title={content.title}\n // 소비 코드가 emptyState 를 노드로 직접 그렸으면 그 노드를 설명 자리에 그대로 둔다\n description={content.description ?? content.children}\n // 소비 노드에 자기 버튼이 있을 수 있으니 그때는 패턴 링크를 겹쳐 그리지 않는다 — 1.32.1 재촬영에서 \"필터 초기화\" 가 두 번 보였다\n action={\n withReset && content.children == null ? (\n <Button type='button' variant='secondaryLink' onClick={() => resetRef.current?.()}>\n 필터 초기화\n </Button>\n ) : null\n }\n />\n )\n },\n [emptyState, withReset],\n )\n const rowClassRules = useMemo(\n () => activeRowClassRules<TRow>(activeRowId, tableProps?.rowClassRules),\n [activeRowId, tableProps?.rowClassRules],\n )\n\n return (\n <div className='w-full'>\n <PageHeader\n title={title}\n description={description}\n titleRight={titleRight}\n // 주요 버튼은 제목 줄 오른쪽(2026-10-19 14차, 사용자 결정 L-A — DetailPage·DashboardLayout 과 한 규칙). 필터 줄(bottomRight)은 검색·필터·초기화만.\n // 이전: 1.22.0 필터 줄 왼쪽 → 1.25.0 제목 오른쪽 → 1.32.2 필터 줄 왼쪽(13-B, UIClip 근거) → 14차 제목 줄\n actions={actions}\n bottomRight={filterRow}\n />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark — 표 칸이 landmark 밖으로 찍히지 않게. <main> 은 소비 앱 레이아웃 몫 */}\n {/* 헤더→본문 32(묶음 사이) — 2026-10-17 8차 D2 */}\n <section aria-label={title} className='px-[24px] py-[32px]'>\n {error ? (\n <Alert variant='danger'>\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : (\n // 상세 패널(detail)이 있으면 표 쪽이 줄고 패널이 오른쪽에 나란히 선다 — 목록 맥락을 유지한 채 상세를 본다\n <div className='flex items-start gap-[24px]'>\n <div className='min-w-0 flex-1'>\n {caption}\n <AgGridTable\n rowData={rows}\n columnDefs={columnDefs}\n loading={loading}\n noRowsOverlayComponent={EmptyOverlay}\n onSortChanged={\n onSortChange\n ? (e: SortChangedEvent<TRow>) => onSortChange(getSortedColumnState(e).value)\n : undefined\n }\n onRowClicked={\n onRowClick\n ? (e: RowClickedEvent<TRow>) => {\n if (e.data) onRowClick(e.data)\n }\n : undefined\n }\n // 표는 행 수만큼 자란다(2026-10-15 이슈 38) — AgGridTable 의 기본 300px 고정 높이는 한 페이지 20건 중 5건만 보여\n // 나머지가 안쪽 스크롤에 숨었다. 고정 높이가 필요하면 tableProps 로 domLayout='normal' + containerStyle 을 넘긴다\n domLayout='autoHeight'\n // 두 줄 셀(secondaryField)이면 행 56 — dl-ui-v3 목록과 같은 높이. undefined 를 넘기면 AgGridTable 기본(50)을 덮어쓰므로 있을 때만\n {...(secondaryField ? { rowHeight: 56 } : {})}\n {...tableProps}\n rowClassRules={rowClassRules}\n />\n {pagination ? (\n <TablePagination>\n <Pagination\n total={pagination.total}\n rowsPerPage={pagination.pageSize}\n page={pagination.page}\n onChange={pagination.onChange}\n showLabel\n />\n </TablePagination>\n ) : null}\n </div>\n {detail ? (\n // 나란한 상세 패널: 제목·설명·닫기 → 본문 → (안내) → 버튼 줄. 스크롤해도 따라오게 sticky. Card 와 같은 테두리·라운드(그림자 없음).\n // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section — DetailPage 의 aside 와 같다\n <Panel\n title={detail.title}\n description={detail.description}\n padding='large'\n titleRight={\n <IconButton\n type='button'\n variant='secondary'\n aria-label='닫기'\n onClick={detail.onClose}\n >\n <X size={20} />\n </IconButton>\n }\n note={detail.note}\n footer={detail.footer}\n className={cn('sticky top-[24px] shrink-0', DETAIL_WIDTH[detail.width ?? 'medium'])}\n >\n {detail.children}\n </Panel>\n ) : null}\n </div>\n )}\n </section>\n </div>\n )\n}\n","'use client'\n\nimport { type ReactNode, useRef } from 'react'\nimport { Ellipsis } from 'lucide-react'\n\nimport { Button, type ButtonProps } from '../components/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/dropdownMenu'\nimport { IconButton } from '../components/iconButton'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Panel } from '../components/panel'\nimport { cn } from '../utils/cn'\nimport { EmptyState, ErrorState, LoadingState } from './states'\n\n/** 헤더 행동 버튼 하나. `children` 이 라벨(아이콘 포함), variant 는 슬롯이 정한다 */\nexport type DetailPageAction = Omit<ButtonProps, 'variant' | 'onClick'> & {\n /** 클릭. 좁은 헤더에서 \"더보기\" 메뉴 항목으로 접혔을 때는 항목 선택 */\n onClick?: () => void\n}\n\nexport type DetailPageProps = Pick<\n PageHeaderProps,\n | 'title'\n | 'description'\n | 'status'\n | 'meta'\n | 'titleRight'\n | 'breadcrumb'\n | 'tabs'\n | 'activeTab'\n | 'onTabChange'\n> & {\n /** 주 행동 1개 (예: 배포·새 추론 서비스). `primaryBold` 로 맨 오른쪽에 그린다 */\n primaryAction?: DetailPageAction\n /** 보조 행동들 (예: 다운로드·복제·새 버전). `secondary` 로 주 행동 왼쪽에 준 순서대로 — 2개까지만 보이고 3번째부터는 \"더보기\" 메뉴로 접힌다(2026-10-19 14차) */\n secondaryActions?: DetailPageAction[]\n /** 삭제. 항상 \"더보기\" 메뉴 안(구분선 아래 danger 항목)에 둔다 — 빨간 버튼을 첫 화면에 펼치지 않는다(2026-10-19 14차). 확인 Dialog 는 소비 앱이 onClick 에서 연다 */\n deleteAction?: DetailPageAction\n /** 버튼 묶음을 직접 구성할 때만 (드롭다운·툴팁 등). 주면 위 세 슬롯 대신 이것을 그대로 그린다 */\n actions?: ReactNode\n /** **쓰지 않는다**(2026-10-18 11차, 사용자 결정) — 본문 위 안내 한 줄이 4라운드 연속 \"떠 있다\" 로 감점. 비활성 버튼은 비활성으로만 두고, 상태 자체의 안내가 필요하면 `banner`. 호환용으로 남긴 prop */\n actionsNote?: ReactNode\n /** 헤더 아래, 본문 위에 놓는 알림 슬롯 (예: 삭제 예정·권한 안내 Alert). 상태와 무관하게 항상 그린다 */\n banner?: ReactNode\n /** true 면 본문 자리에 Skeleton. error·notFound 보다 우선 */\n loading?: boolean\n /** 주면 본문 대신 오류 상자(ErrorState: 아이콘·\"불러오지 못했습니다\"·이 내용). `onRetry` 가 있으면 \"다시 시도\" 버튼을 함께 그린다 */\n error?: ReactNode\n /** error 상태의 \"다시 시도\" 클릭 */\n onRetry?: () => void\n /** 항목이 없을 때 본문 대신 없음 상자(EmptyState — 상자 일러스트, 2026-10-13 부터 목록·대시보드와 같은 그림). true 면 기본 문구 \"항목을 찾을 수 없습니다.\" + 안내 한 줄 */\n notFound?: ReactNode\n /** **호환용** — 새 화면은 핵심 정보를 헤더 `meta`(`MetaGroup columns={3}`)에 둔다(2026-10-27 사용자 결정 A: 오른쪽 패널은 내용이 짧아 첫 화면 아래가 비는 것이 홀드아웃 9~12 연속 지적, 2026-10-30 요약 상자 대신 메타 자리 하나로). 주면 넓은 화면(xl)에서 본문 2 : 패널 1, 좁은 화면에서는 본문 아래 */\n aside?: ReactNode\n /** 패널 제목(Panel 머리글, 기본 \"기본 정보\"). 패턴이 그리므로 `aside` 안에 같은 제목을 다시 쓰지 않는다(2026-10-27 홀드아웃 12: \"작업 정보\" 가 연달아 두 번) */\n asideTitle?: ReactNode\n /** 본문 폭. full = 가로 전체(정보 탭), panel = 가운데 900px (설정 탭). 기본 full */\n width?: 'full' | 'panel'\n /** 본문 (현재 탭 내용). 탭 상태와 라우팅은 소비 앱이 가진다 */\n children?: ReactNode\n}\n\n/**\n * 상세 화면 패턴. PageHeader(제목·상태·메타·행동·탭) → 알림 → 본문(전폭 또는 가운데 패널) 을 한 번에 배치하고 로딩·오류·없음 상태를 책임진다.\n *\n * 언제 쓰나: 실행환경·모델·파이프라인 하나의 상세 화면. 핵심 정보는 헤더 `meta` 한 곳(`MetaGroup columns={3}` + `MetaItem` — 탭을 바꿔도 보임, 2026-10-30 사용자 결정: 메타 줄과 요약 상자를 따로 두지 않는다)으로, 정보 탭 본문은 전폭 Panel 들, 설정 탭은 `width='panel'` 로 가운데 900px. 오른쪽 패널 `aside` 는 호환용. 데이터 조회·탭 전환·삭제 확인은 소비 앱이 가진다.\n * 언제 안 쓰나: 목록(→ ListPage), 입력이 주인 화면(→ FormPage), 요약 타일·차트 중심(→ DashboardLayout), 목록 옆에 잠깐 띄우는 상세(→ Drawer 에 본문만 넣는다).\n * 대신/함께: `tabs`·`activeTab`·`onTabChange` 는 PageHeader 와 같다. 상태 Badge 는 `status`, 메타는 `meta` 에 MetaGroup/MetaItem, 본문 Panel 의 속성 묶음은 MetaList/MetaRow. 본문 안의 묶음은 `Panel`(제목 + 1px 테두리 상자)로 나눈다 — 옆 패널도 같은 Panel 이라 본문과 패널이 한 규칙으로 보인다(2026-10-17). 행동 버튼은 `deleteAction`·`secondaryActions`·`primaryAction` 슬롯으로 주면 왼쪽부터 삭제(danger) → 보조(secondary) → 주 행동(primaryBold) 순으로 강조가 올라가게 패턴이 배치하고, 헤더 내용 폭이 768px 보다 좁아지면 주 행동만 남기고 나머지를 \"더보기\" DropdownMenu 로 접는다(dl-ui-v3 상세 관례). `error`·`notFound` 는 패턴이 가운데 상자(아이콘·제목·설명·버튼)로 그리므로 소비 앱이 Alert·`<div>` 로 직접 만들지 않는다. `error` 에 `onRetry` 를 주면 \"다시 시도\" 버튼이 붙는다.\n */\n/** \"더보기\" ⋯ 메뉴: 넘긴 보조 행동들 + (구분선) + 삭제.\n * 항목의 onClick 은 메뉴가 닫히는 `onCloseAutoFocus` 에서 ⋯ 트리거로 포커스를 되돌린 **다음** 부른다 — 선택과 같은 틱에 확인 대화상자를 열면\n * 대화상자가 \"이전 포커스\" 로 사라지는 메뉴 항목(또는 body)을 기억해, 닫힌 뒤 포커스가 ⋯ 로 돌아오지 않는다(키보드 검사 focusReturnedToTrigger 거짓, 2026-10-19 15차 실측.\n * 메뉴 닫힘 애니메이션 때문에 setTimeout(0) 으로도 모자랐다) */\nfunction MoreMenu({\n items,\n deleteAction,\n}: {\n items: DetailPageAction[]\n deleteAction?: DetailPageAction\n}) {\n const triggerRef = useRef<HTMLButtonElement>(null)\n const pendingRef = useRef<(() => void) | undefined>(undefined)\n const select = (fn?: () => void) => () => {\n pendingRef.current = fn\n }\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <IconButton ref={triggerRef} variant='secondary' aria-label='더보기'>\n <Ellipsis size={24} />\n </IconButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align='end'\n onCloseAutoFocus={(e) => {\n e.preventDefault()\n triggerRef.current?.focus()\n const fn = pendingRef.current\n pendingRef.current = undefined\n fn?.()\n }}\n >\n {items.map(({ children, onClick, disabled }, i) => (\n <DropdownMenuItem key={i} disabled={disabled} onSelect={select(onClick)}>\n {children}\n </DropdownMenuItem>\n ))}\n {deleteAction && items.length ? <DropdownMenuSeparator /> : null}\n {deleteAction ? (\n <DropdownMenuItem\n variant='danger'\n disabled={deleteAction.disabled}\n onSelect={select(deleteAction.onClick)}\n >\n {deleteAction.children}\n </DropdownMenuItem>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nexport function DetailPage({\n primaryAction,\n secondaryActions,\n deleteAction,\n actions,\n actionsNote,\n banner,\n loading,\n error,\n onRetry,\n notFound,\n aside,\n asideTitle = '기본 정보',\n width = 'full',\n children,\n ...header\n}: DetailPageProps) {\n let body: ReactNode\n if (loading) {\n body = <LoadingState />\n } else if (error) {\n body = <ErrorState className='py-[32px]' description={error} onRetry={onRetry} />\n } else if (notFound) {\n body = (\n <EmptyState\n className='py-[32px]'\n title={notFound === true ? '항목을 찾을 수 없습니다.' : notFound}\n description={notFound === true ? '삭제되었거나 주소가 잘못되었을 수 있습니다.' : undefined}\n />\n )\n } else if (aside) {\n // 패널 사이·본문 상하 24 — 8차 D2 에서 32 로 키웠다가 심판 \"right column leaves a large empty area\" 감점으로 24 복원(2026-10-18 9차)\n body = (\n // 넓은 화면: 본문 2 : 패널 1 + 세로선. 좁은 화면: 세로로 쌓이고 패널은 제목 + 가로선으로 시작한다\n <div className='grid grid-cols-1 gap-[24px] xl:grid-cols-[2fr_1fr] xl:items-start'>\n <div className='min-w-0'>{children}</div>\n {/* <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section */}\n {/* 테두리 상자(Panel — ListPage detail·FormPage aside·본문 구역과 같은 상자). 세로선은 \"다른 선\" 으로 읽혔다 (2026-10-17 이슈 47 4·5차) */}\n <Panel title={asideTitle}>{aside}</Panel>\n </div>\n )\n } else {\n body = children\n }\n\n // 강조 순서는 패턴이 정한다 (2026-10-19 14차, 사용자 결정 2-A): 넓은 헤더 = [더보기(⋯)][보조 최대 2개][주 행동(primaryBold)].\n // 삭제(danger)는 항상 \"더보기\" 메뉴 안(구분선 아래) — 펼친 빨간 버튼은 첫 화면에서 주의를 뺏고 \"destructive button placed first\" 로 감점됐다(5·13차).\n // 보조 3번째부터도 메뉴로(Polaris·Material 3 의 \"보이는 개수 제한 + 나머지는 overflow\"). actions 를 주면 그대로\n const VISIBLE_SECONDARY = 2\n const visibleSecondary = secondaryActions?.slice(0, VISIBLE_SECONDARY) ?? []\n const overflowSecondary = secondaryActions?.slice(VISIBLE_SECONDARY) ?? []\n const primaryButton = primaryAction ? (\n <Button type='button' variant='primaryBold' {...primaryAction} />\n ) : null\n const moreMenu = (items: DetailPageAction[]) =>\n items.length || deleteAction ? <MoreMenu items={items} deleteAction={deleteAction} /> : null\n const fullRow = (\n <>\n {moreMenu(overflowSecondary)}\n {visibleSecondary.map((action, i) => (\n <Button key={i} type='button' variant='secondary' {...action} />\n ))}\n {primaryButton}\n </>\n )\n // 좁은 헤더(내용 폭 < 768px = @3xl): 주 행동만 남기고 보조 전부·삭제는 \"더보기\" 메뉴로. 판정은 PageHeader <header> 의 @container 가 한다\n const collapsedRow = (\n <>\n {moreMenu(secondaryActions ?? [])}\n {primaryButton}\n </>\n )\n const collapsible = Boolean(deleteAction || secondaryActions?.length)\n // 비활성 사유 같은 안내는 버튼 줄 **윗줄**(오른쪽 정렬) — 같은 줄 왼쪽에 두면 긴 문장이 버튼을 밀어낸다 (2026-10-16 이슈 45 → 2026-10-17 4차 조정)\n // 비활성 사유 같은 안내는 탭 선 **아래**, 본문 첫 줄 오른쪽 정렬(버튼 바로 아래 같은 열) — 버튼 윗줄(4차)은 \"버튼과 안내가 탭 선 위에 떠 있다\",\n // 같은 줄 왼쪽(45-A)은 \"긴 문장이 버튼을 밀어낸다\" 로 읽혔다 (2026-10-17 이슈 47 6차)\n const note =\n actionsNote != null ? (\n <p\n data-slot='detail-actions-note'\n className='mb-[8px] break-keep text-right text-small-regular text-sys-text-subtle wrap-break-word'\n >\n {actionsNote}\n </p>\n ) : null\n const actionRow = actions ? (\n actions\n ) : collapsible ? (\n <>\n <div data-slot='detail-actions-full' className='hidden items-center gap-[8px] @3xl:flex'>\n {fullRow}\n </div>\n <div data-slot='detail-actions-collapsed' className='flex items-center gap-[8px] @3xl:hidden'>\n {collapsedRow}\n </div>\n </>\n ) : primaryButton ? (\n primaryButton\n ) : undefined\n\n return (\n <div className='w-full'>\n <PageHeader {...header} actions={actionRow} />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다) */}\n <section\n aria-label={header.title || '본문'}\n className='flex flex-col gap-[24px] px-[24px] py-[24px]'\n >\n {banner}\n <div className={cn('w-full', width === 'panel' && 'mx-auto max-w-[900px]')}>\n {note}\n {body}\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport type { ReactNode } from 'react'\nimport type { FieldValues, UseFormReturn } from 'react-hook-form'\n\nimport { Alert, AlertDescription } from '../components/alert'\nimport { Button } from '../components/button'\n\n/** FormPage · FormDrawer · FormDialog 가 공유하는 props */\nexport type FormShellProps<TValues extends FieldValues> = {\n /** 제목 */\n title: string\n /** 제목 아래 설명 */\n description?: ReactNode\n /** react-hook-form `useForm()` 반환값. 검증 스키마·기본값은 소비 앱이 소유한다 */\n form: UseFormReturn<TValues>\n /** 검증 통과 후 호출. Promise 를 돌려주면 끝날 때까지 제출 중 상태가 된다 */\n onSubmit: (values: TValues) => void | Promise<void>\n /** 제출 버튼 라벨. 기본 \"저장\" */\n submitLabel?: string\n /** 취소 버튼 라벨. 기본 \"취소\" */\n cancelLabel?: string\n /** 외부 저장 중 상태(뮤테이션 등). 버튼 loading·disabled, 취소·닫기 차단 */\n submitting?: boolean\n /** 서버 오류 등 폼 전체 오류. 버튼 위에 Alert(danger) 로 표시 */\n error?: ReactNode\n /**\n * 제출 버튼 활성 규칙. `always`(기본) = 제출 중이 아니면 항상 눌리고, 누르면 react-hook-form 이 검증해 틀린 항목마다 FormMessage 를 보인다.\n * `whenValid` = 모든 입력이 유효해질 때까지 잠근다(2026-10-12 전 기본). \"누르면 검증해 이유 표시\" 가 흔한 폼 관례라 기본을 바꿨다\n */\n submitMode?: 'always' | 'whenValid'\n /** FormSection 으로 묶은 FormField 들 */\n children: ReactNode\n}\n\n/** 제출 상태·버튼 활성 조건·submit 핸들러. 세 껍데기가 같은 규칙을 쓴다 */\nexport function useFormShell<TValues extends FieldValues>({\n form,\n onSubmit,\n submitting,\n submitMode = 'always',\n}: Pick<FormShellProps<TValues>, 'form' | 'onSubmit' | 'submitting' | 'submitMode'>) {\n // isValid 는 render 에서 읽어야 react-hook-form 이 재계산한다 (구독 기반 proxy)\n const { isValid, isSubmitting } = form.formState\n const busy = Boolean(submitting) || isSubmitting\n const handleSubmit = form.handleSubmit((values) => onSubmit(values))\n return { busy, canSubmit: !busy && (submitMode === 'always' || isValid), handleSubmit }\n}\n\n/** 폼 전체 오류. 없으면 아무것도 그리지 않는다 */\nexport function FormError({ error, className }: { error?: ReactNode; className?: string }) {\n if (!error) return null\n return (\n <Alert variant='danger' className={className}>\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )\n}\n\ntype FormActionsProps = {\n busy: boolean\n canSubmit: boolean\n /** 없으면 취소 버튼을 그리지 않는다 */\n onCancel?: () => void\n submitLabel?: string\n cancelLabel?: string\n /** 버튼이 `<form>` 밖(예: FormPage 의 검토 패널)에 놓일 때 그 form 의 id — HTML `form` 속성으로 제출을 연결한다 (2026-10-20 16차 D1) */\n formId?: string\n}\n\n/** 취소 + 제출 버튼. 제출 중에는 둘 다 비활성 */\nexport function FormActions({\n busy,\n canSubmit,\n onCancel,\n submitLabel = '저장',\n cancelLabel = '취소',\n formId,\n}: FormActionsProps) {\n return (\n <>\n {onCancel ? (\n <Button\n type='button'\n variant='secondaryOutline'\n onClick={onCancel}\n disabled={busy}\n form={formId}\n >\n {cancelLabel}\n </Button>\n ) : null}\n <Button\n type='submit'\n variant='primaryBold'\n loading={busy}\n disabled={!canSubmit}\n form={formId}\n >\n {submitLabel}\n </Button>\n </>\n )\n}\n","'use client'\n\nimport { type ReactNode, useId } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport { Card } from '../components/card'\nimport { Form, FormFooter, FormSectionLevelContext } from '../components/form'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Panel } from '../components/panel'\nimport { cn } from '../utils/cn'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormPageProps<TValues extends FieldValues> = FormShellProps<TValues> &\n Pick<PageHeaderProps, 'breadcrumb' | 'titleRight'> & {\n /** 취소 버튼 핸들러. 없으면 취소 버튼을 그리지 않는다 */\n onCancel?: () => void\n /** true 면 폼 맨 위에 \"* 표시는 필수 입력 항목입니다\" 안내 한 줄 */\n requiredLegend?: boolean\n /** 본문 바탕. 'plain'(기본)은 흰 캔버스에 섹션 구분선. 'subtle' 은 연한 회색 캔버스 위에 폼 전체를 흰 Card 하나로 — UIClip 폼 진단(evals/metrics-spec §16.7)용 옵트인이며 캔버스 정책(DESIGN.md)은 흰색 그대로다 */\n canvas?: 'plain' | 'subtle'\n /** 폼 열(720px)의 자리. 'start'(기본)는 헤더 제목과 같은 왼쪽 축에서 시작, 'center' 는 본문 가운데(2026-10-16 이슈 47 전의 모양 — 제목은 왼쪽, 폼은 가운데라 축이 어긋난다고 원문 심판 10회 감점). 2026-10-26 22차부터 헤더 내용과 본문이 같은 가운데 폭 컨테이너(패널 있으면 1152, 없으면 720) 안에 들어가므로 이 값의 효과는 거의 없다(호환용). `aside` 가 있으면 폼 열은 왼쪽 */\n align?: 'start' | 'center'\n /** 폼 오른쪽의 검토·요약 패널(입력값 요약, 비용 미리보기 등). 넓은 화면에서 폼 열 옆에 세로선으로 구분돼 화면을 따라 내려오고(sticky), 좁으면 폼 아래로. **취소·제출 버튼은 이 패널 바닥**에 놓인다(폼 끝에는 없음 — 2026-10-20 16차 D1, 긴 폼 옆에서 패널 아래가 비고 제출이 멀다는 두 심판 지적). FormPage 를 flex 로 감싸 옆에 패널을 두면 헤더가 화면 폭을 다 못 써 구분선이 끊긴다 — 반드시 이 슬롯으로 */\n aside?: ReactNode\n /** 검토 패널 제목(h2). 기본 '검토' */\n asideTitle?: ReactNode\n }\n\n/**\n * 독립 화면 폼 패턴. PageHeader(제목·설명) → FormSection 들(사이에 연한 구분선, 왼쪽 축 720px) → 오류 → 취소/저장(FormFooter; `aside` 가 있으면 패널 바닥). **헤더 내용과 본문은 같은 가운데 폭 컨테이너**에 둔다 — 검토 패널이 있으면 `--max-w-page-content`(1152 = 720 + 24 + 408), 없으면 폼 폭 720. 띠(헤더 배경·아래 선)는 전폭 그대로(2026-10-26 22차: 전폭 왼쪽 정렬에서는 긴 폼 옆 짧은 패널 아래가 \"unused space\" 로 감점됐고, 내용을 가운데 폭에 가둔 변형만 원문 심판 layout 3.5 → 4.0 — 기준선 폼들과 같은 구조). 검토·요약 패널(`aside`)은 **항상** 폼 옆에 둔다 — 입력값 요약·예상 비용처럼 제출 전에 확인할 것(2026-10-17 8차 C2, 예제 form-page.tsx).\n *\n * 언제 쓰나: 스크롤이 생기는 긴 폼 — 섹션 3개 이상, 한 화면을 넘기거나, 여러 단계. 전용 URL 을 가진 생성·수정 화면의 기본. 검증 스키마·기본값·payload 는 소비 앱의 `useForm` 이 소유한다.\n * 언제 안 쓰나: 목록 맥락을 유지해야 하고 Drawer 한 화면 안에 들어가는 폼(섹션 1~2개)은 FormDrawer, 필드 1~4개의 짧은 입력은 FormDialog. 긴 폼을 Drawer·Dialog 에 넣지 않는다.\n * 대신/함께: 머리는 목록·상세·대시보드와 같은 PageHeader(h1)라 FormSection 제목은 h2 가 된다. 바탕은 흰색이 기본이고 `canvas='subtle'` 은 측정용 옵트인(회색 캔버스 + 카드)이다. 저장 버튼은 기본(`submitMode='always'`)으로 항상 눌리고 누르면 검증해 틀린 항목마다 FormMessage 를 보인다 — 유효해질 때까지 잠그려면 `submitMode='whenValid'`. `onSubmit` 이 Promise 면 끝날 때까지 loading — 다만 제출 중 상태는 앱이 `submitting` 으로도 **코드에 드러내라**(접근성·심사 모두 코드에서 보여야 한다, 2026-10-16). 숫자 입력의 단위는 `TextField elemAfterInput='개'`. 선택지 목록을 못 불러왔으면 폼을 지우지 말고 그 필드 자리에 ErrorState 를 둔다.\n */\nexport function FormPage<TValues extends FieldValues>({\n title,\n description,\n breadcrumb,\n titleRight,\n form,\n onSubmit,\n onCancel,\n submitLabel,\n cancelLabel,\n submitting,\n submitMode,\n error,\n requiredLegend,\n canvas = 'plain',\n align = 'start',\n aside,\n asideTitle = '검토',\n children,\n}: FormPageProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const formId = useId()\n // 헤더 내용·본문을 같은 가운데 폭에 — 검토 패널이 있으면 폼 + 패널 폭(1152), 없으면 폼 폭(720). 띠는 전폭 (2026-10-26 22차)\n const contentMaxWidth = aside ? 'var(--max-w-page-content)' : 'var(--max-w-form-control)'\n // 취소·제출 묶음. aside 가 없으면 폼 끝(FormFooter), 있으면 검토 패널 바닥 — 패널 안 버튼은 form 밖이라 formId 로 연결한다\n const actions = (\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={onCancel}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n formId={formId}\n />\n )\n\n const body = (\n <>\n {/* 안내는 form 밖에 둔다 — form 의 첫 자식이 FormSection 이어야 첫 섹션 위에 구분선이 안 생긴다 */}\n {requiredLegend ? (\n <p className='text-small-regular text-sys-text-subtle'>\n <span aria-hidden className='text-danger30'>\n *\n </span>{' '}\n 표시는 필수 입력 항목입니다\n </p>\n ) : null}\n <Form {...form}>\n <form id={formId} noValidate onSubmit={handleSubmit}>\n <FormSectionLevelContext.Provider value={2}>{children}</FormSectionLevelContext.Provider>\n <FormError error={error} className='mt-[24px]' />\n {/* 검토 패널(aside)이 있으면 버튼은 패널 바닥에(아래) — 긴 폼 옆에서 패널이 짧게 끝나고 제출이 멀어 \"unused space beneath the review panel\" 감점 (2026-10-20 16차 D1) */}\n {aside ? null : (\n <>\n {/* FormFooter 는 세로 flex 라 버튼 두 개를 한 줄로 두려면 row 로 한 번 감싼다 (DrawerFooter 와 같은 gap) */}\n {/* 폼 열 폭의 선 위에 버튼 — 긴 폼 끝에 버튼만 떠 있지 않게 (2026-10-17 이슈 47 4차) */}\n <FormFooter\n align='end'\n className='mt-[24px] border-t border-sys-divider-subtle pt-[16px]'\n >\n <div className='flex gap-[8px]'>{actions}</div>\n </FormFooter>\n </>\n )}\n </form>\n </Form>\n </>\n )\n\n // 폼은 FormControl 기본 폭(720px). 기본은 헤더 제목과 같은 왼쪽 축(align='start'), 'center' 면 가운데 — 푸터 버튼은 어느 쪽이든 입력 칸 오른쪽 끝에 정렬된다\n const column =\n canvas === 'subtle' ? (\n // 폼 전체를 흰 Card 하나로(섹션 사이 구분선은 그대로). 위 8px 는 첫 FormSection 의 pt-[16px] 과 더해 24\n <Card\n className={cn(\n 'w-full max-w-[var(--max-w-form-control)] px-[24px] pb-[24px] pt-[8px]',\n align === 'center' && !aside && 'mx-auto',\n )}\n >\n {body}\n </Card>\n ) : (\n <div\n className={cn(\n 'w-full max-w-[var(--max-w-form-control)]',\n align === 'center' && !aside && 'mx-auto',\n )}\n >\n {body}\n </div>\n )\n\n return (\n // subtle 의 캔버스 회색은 기존 토큰(grayscale20, 진단 18-A 의 f7 변형)을 쓴다 — 채택되면 전용 sys 토큰으로 올린다\n <div className={cn('w-full', canvas === 'subtle' && 'bg-sys-background-hover')}>\n <PageHeader\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n titleRight={titleRight}\n contentMaxWidth={contentMaxWidth}\n />\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다). 안쪽 div 가 헤더 내용과 같은 가운데 폭 컨테이너 */}\n {/* 헤더→본문 32(묶음 사이) — 2026-10-17 8차 D2 */}\n <section aria-label={title || '본문'} className='px-[24px] py-[32px]'>\n <div className='mx-auto w-full' style={{ maxWidth: contentMaxWidth }}>\n {aside ? (\n // 폼 열(최대 720px) + 검토 패널. 넓은 화면: 옆에 세로선, sticky. 좁은 화면: 폼 아래에 가로선으로 시작.\n // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe) 이름 있는 section (DetailPage aside 와 같은 꼴)\n <div className='grid grid-cols-1 gap-[24px] xl:grid-cols-[minmax(0,var(--max-w-form-control))_minmax(280px,1fr)] xl:items-start'>\n {column}\n {/* 테두리 상자(Panel) — 넓은 화면에서 sticky 로 폼을 따라온다. 바닥에 취소·제출(HTML form 속성으로 위 <form> 에 연결) */}\n <Panel title={asideTitle} className='xl:sticky xl:top-[24px]'>\n {aside}\n <div\n data-slot='form-page-aside-actions'\n className='mt-[16px] flex justify-end gap-[8px] border-t border-sys-divider-subtle pt-[16px]'\n >\n {actions}\n </div>\n </Panel>\n </div>\n ) : (\n column\n )}\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport type { ComponentProps } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerTitle,\n} from '../components/drawer'\nimport { Form } from '../components/form'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormDrawerProps<TValues extends FieldValues> = FormShellProps<TValues> & {\n /** 열림 상태 (controlled) */\n open: boolean\n /** 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 */\n onOpenChange: (open: boolean) => void\n /** 패널이 나오는 방향. 기본 right */\n side?: ComponentProps<typeof DrawerContent>['side']\n /** 패널 너비. 기본 large */\n size?: ComponentProps<typeof DrawerContent>['size']\n}\n\n/**\n * 사이드 패널 폼 패턴. 목록 화면 위에 오른쪽 Drawer 로 생성·수정 폼을 띄운다. 제목 → FormSection 들(스크롤) → 오류 → 취소/저장.\n *\n * 언제 쓰나: 목록 맥락을 유지해야 하고 **Drawer 한 화면 안에 들어가는** 폼(섹션 1~2개 — 예: 태그·설명·담당자 수정, 짧은 생성).\n * 언제 안 쓰나: 필드 1~4개의 짧은 입력은 FormDialog. 스크롤이 생기거나 섹션이 셋 이상이거나 여러 단계인 긴 폼(실행환경·파이프라인·모델 생성)은 FormPage — 긴 폼을 Drawer 에 넣지 않는다(2026-10-13, Carbon·Shopify 관례).\n * 대신/함께: 제출 중에는 Esc·오버레이·취소·닫기 버튼으로 닫히지 않는다. 닫을 때 폼을 reset 할지는 소비 앱이 `onOpenChange` 에서 정한다(초안을 보존하려면 reset 하지 않는다).\n */\nexport function FormDrawer<TValues extends FieldValues>({\n open,\n onOpenChange,\n side,\n size,\n title,\n description,\n form,\n onSubmit,\n submitLabel,\n cancelLabel,\n submitting,\n error,\n submitMode,\n children,\n}: FormDrawerProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const guardedOpenChange = (next: boolean) => {\n if (!next && busy) return\n onOpenChange(next)\n }\n\n return (\n <Drawer open={open} onOpenChange={guardedOpenChange}>\n <DrawerContent\n side={side}\n size={size}\n // 설명이 없으면 Radix 가 aria-describedby 대상을 찾지 못해 경고하므로 속성을 뺀다\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n <DrawerHeader>\n <DrawerTitle>{title}</DrawerTitle>\n {description ? <DrawerDescription>{description}</DrawerDescription> : null}\n </DrawerHeader>\n <Form {...form}>\n <form noValidate onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>\n <DrawerBody className='px-[24px]'>\n {children}\n <FormError error={error} className='mt-[24px]' />\n </DrawerBody>\n <DrawerFooter>\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={() => guardedOpenChange(false)}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n />\n </DrawerFooter>\n </form>\n </Form>\n </DrawerContent>\n </Drawer>\n )\n}\n","'use client'\n\nimport type { ComponentProps } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../components/dialog'\nimport { Form } from '../components/form'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormDialogProps<TValues extends FieldValues> = FormShellProps<TValues> & {\n /** 열림 상태 (controlled) */\n open: boolean\n /** 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 */\n onOpenChange: (open: boolean) => void\n /** 모달 너비. 기본 small */\n width?: ComponentProps<typeof DialogContent>['width']\n}\n\n/**\n * 모달 폼 패턴. 제목 → 필드 → 오류 → 취소/저장.\n *\n * 언제 쓰나: 이름 변경, 태그 추가처럼 필드 1~4개로 끝나는 짧은 입력.\n * 언제 안 쓰나: 목록 맥락이 필요한 한 화면짜리 폼은 FormDrawer, 스크롤이 생기는 긴 폼은 FormPage, 되돌릴 수 없는 확인만 받을 때는 AlertDialog.\n * 대신/함께: 제출 중에는 닫기 버튼이 사라지고 Esc·오버레이·취소로도 닫히지 않는다.\n */\nexport function FormDialog<TValues extends FieldValues>({\n open,\n onOpenChange,\n width,\n title,\n description,\n form,\n onSubmit,\n submitLabel,\n cancelLabel,\n submitting,\n error,\n submitMode,\n children,\n}: FormDialogProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const guardedOpenChange = (next: boolean) => {\n if (!next && busy) return\n onOpenChange(next)\n }\n\n return (\n <Dialog open={open} onOpenChange={guardedOpenChange}>\n <DialogContent\n width={width}\n hideCloseButton={busy}\n // 설명이 없으면 Radix 가 aria-describedby 대상을 찾지 못해 경고하므로 속성을 뺀다\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n {description ? <DialogDescription>{description}</DialogDescription> : null}\n </DialogHeader>\n <Form {...form}>\n <form noValidate onSubmit={handleSubmit} className='flex flex-col gap-[16px]'>\n {children}\n <FormError error={error} />\n <DialogFooter>\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={() => guardedOpenChange(false)}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n />\n </DialogFooter>\n </form>\n </Form>\n </DialogContent>\n </Dialog>\n )\n}\n","'use client'\n\nimport { type ReactNode, useId } from 'react'\nimport { TriangleAlert } from 'lucide-react'\n\nimport { Card } from '../components/card'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Skeleton } from '../components/skeleton'\nimport { cn } from '../utils/cn'\nimport { EmptyState, ErrorState, LoadingState } from './states'\n\nexport type DashboardStat = {\n /** 타일 라벨 (예: 실행 중인 학습) */\n label: ReactNode\n /** 큰 숫자 */\n value: ReactNode\n /** 숫자 아래 보조 설명 (예: 어제보다 +2) */\n caption?: ReactNode\n /** 주면 타일 전체가 이 주소로 가는 링크가 된다 */\n href?: string\n /** 사용률 같은 비율(0~1). 주면 숫자 아래에 진행 막대를 그린다 (예: 172/192 코어 → 0.9) */\n ratio?: number\n /** 값의 색. warning·danger 면 경고 아이콘이 붙고 숫자·막대가 그 색이 된다 (예: 사용률 80% 초과). 기본 default */\n tone?: 'default' | 'warning' | 'danger'\n}\n\nexport type DashboardLayoutProps = Pick<\n PageHeaderProps,\n 'title' | 'description' | 'titleRight' | 'breadcrumb'\n> & {\n /** 주요 행동 버튼. 제목 줄 오른쪽 (ListPage·DetailPage 와 같은 자리, 2026-10-19 14차) */\n actions?: ReactNode\n /** 맨 위 요약 타일. 2열, 넓은 화면(xl)에서 4열, 간격 12px */\n stats?: DashboardStat[]\n /** true 면 타일 숫자 자리에 Skeleton. 패널 로딩은 각 DashboardPanel 의 `loading` */\n loading?: boolean\n /** 주면 타일·섹션 대신 오류 상자(ErrorState). `onRetry` 가 있으면 \"다시 시도\" 버튼 */\n error?: ReactNode\n /** error 상태의 \"다시 시도\" 클릭 */\n onRetry?: () => void\n /** 보여 줄 데이터가 전혀 없을 때 타일·섹션 대신 없음 상자. true 면 기본 문구 \"데이터가 없습니다.\" 패널 하나만 비었으면 DashboardPanel 의 `empty` */\n empty?: ReactNode\n /** DashboardSection 들. 세로로 32px 간격 */\n children?: ReactNode\n}\n\n// 글자는 40단계(흰 배경 대비 warning40 5.4:1·danger30 5.9:1). 30단계 warning 은 채움용이라 글자에 쓰면 1.75:1 로 axe 에 걸린다\nconst TONE_TEXT = {\n default: 'text-sys-text-main',\n warning: 'text-warning40',\n danger: 'text-danger30',\n}\nconst TONE_BAR = { default: 'bg-primary30', warning: 'bg-warning30', danger: 'bg-danger30' }\n\nfunction StatTile({\n label,\n value,\n caption,\n href,\n ratio,\n tone = 'default',\n loading,\n}: DashboardStat & { loading?: boolean }) {\n const labelId = useId()\n const percent = ratio == null ? null : Math.round(Math.min(Math.max(ratio, 0), 1) * 100)\n return (\n <Card className='relative gap-[4px] p-[16px]'>\n <span id={labelId} className='text-h6 text-sys-text-subtle'>\n {href ? (\n // 타일 전체를 클릭 영역으로: 링크의 ::after 를 Card 크기로 늘린다\n <a\n href={href}\n className='after:absolute after:inset-0 after:rounded-medium focus-visible:outline-[2px] focus-visible:outline-sys-focus-ring-main'\n >\n {label}\n </a>\n ) : (\n label\n )}\n </span>\n {loading ? (\n <Skeleton role='status' aria-label='불러오는 중' className='h-[32px] w-[80px]' />\n ) : (\n <span className={cn('flex items-center gap-[6px] text-h2', TONE_TEXT[tone])}>\n {tone !== 'default' ? <TriangleAlert aria-hidden size={20} className='shrink-0' /> : null}\n {value}\n </span>\n )}\n {percent != null && !loading ? (\n <div\n role='progressbar'\n aria-labelledby={labelId}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n className='mt-[4px] h-[4px] w-full overflow-hidden rounded-full bg-secondary-o10'\n >\n <div\n className={cn('h-full rounded-full', TONE_BAR[tone])}\n style={{ width: `${percent}%` }}\n />\n </div>\n ) : null}\n {caption ? <span className='text-small-regular text-sys-text-subtler'>{caption}</span> : null}\n </Card>\n )\n}\n\n/**\n * 대시보드 화면 패턴. PageHeader → 요약 타일(`stats`) → DashboardSection(제목 + 패널 그리드 또는 표) 을 세로로 배치한다.\n *\n * 언제 쓰나: 프로젝트 홈·리소스 모니터링처럼 \"지금 상태를 숫자·차트로 한눈에\" 보여 주고 각 섹션에서 세부 화면으로 들어가는 화면. 값·시계열·갱신 주기는 소비 앱이 가진다.\n * 언제 안 쓰나: 한 리소스의 목록(→ ListPage)·상세(→ DetailPage), 설정 입력(→ FormPage). 섹션 하나뿐이면 PageHeader + Card 로 충분하다.\n * 대신/함께: 패널 내용(차트·표)은 `DashboardPanel` 의 children 으로 넣고 차트 라이브러리는 소비 앱이 고른다. 폴링·갱신 시각 표시는 `titleRight` 슬롯에 `PageHeaderRefreshButton` 으로. 표 섹션은 DashboardSection 안에 Table 또는 AgGridTable 을 그대로 넣는다. 사용률 타일은 `ratio`(진행 막대)·`tone`(80% 초과면 warning) 을 준다. 전체를 못 불러왔으면 `error`(+`onRetry`), 데이터가 전혀 없으면 `empty` — 빨간 Alert·`<div>` 로 직접 만들지 않는다.\n */\nexport function DashboardLayout({\n actions,\n stats,\n loading,\n error,\n onRetry,\n empty,\n children,\n ...header\n}: DashboardLayoutProps) {\n let body: ReactNode\n if (error) body = <ErrorState className='py-[32px]' description={error} onRetry={onRetry} />\n else if (empty)\n body = (\n <EmptyState className='py-[32px]'>{empty === true ? '데이터가 없습니다.' : empty}</EmptyState>\n )\n else\n body = (\n <>\n {stats?.length ? (\n <div className='grid grid-cols-2 gap-[12px] xl:grid-cols-4'>\n {stats.map((stat, i) => (\n <StatTile key={i} {...stat} loading={loading} />\n ))}\n </div>\n ) : null}\n {children}\n </>\n )\n return (\n <div className='w-full'>\n <PageHeader {...header} actions={actions} />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다) */}\n <section\n aria-label={header.title || '본문'}\n className='flex flex-col gap-[32px] px-[24px] py-[24px]'\n >\n {body}\n </section>\n </div>\n )\n}\n\nexport type DashboardSectionProps = {\n /** 섹션 제목 (h2) */\n title: ReactNode\n /** 제목 오른쪽 슬롯 (예: \"전체 보기\" 버튼) */\n action?: ReactNode\n /** 주면 children(DashboardPanel 들)을 **하나의 Card** 안에 1px 구분선으로 나눈 그리드로 그린다. 2 면 넓은 화면(xl)에서 2열. 없으면 children 을 그대로(표 등) */\n columns?: 1 | 2\n /** columns 가 있으면 DashboardPanel 들, 없으면 표 등 아무 내용 */\n children?: ReactNode\n}\n\n/**\n * DashboardLayout 의 한 섹션. 제목(h2)·오른쪽 행동 → 패널 그리드(Card 하나 안에 구분선) 또는 표.\n *\n * 언제 쓰나: 대시보드에서 주제 하나(리소스 사용량, 최근 학습 …)를 묶을 때. 차트 패널 여러 개면 `columns={2}` — 패널마다 카드가 아니라 **큰 카드 하나를 구분선으로 나눈다**(dl-ui-v3 리소스 모니터링 관례).\n * 언제 안 쓰나: 대시보드 밖의 섹션 제목(→ FormSection·CardHeader).\n * 대신/함께: 표 섹션은 `columns` 없이 children 에 Table 을 바로 넣고 `action` 에 \"전체 보기\" 링크 버튼을 둔다.\n */\nexport function DashboardSection({ title, action, columns, children }: DashboardSectionProps) {\n const id = useId()\n return (\n <section aria-labelledby={id} className='flex flex-col gap-[16px]'>\n <div className='flex items-center justify-between gap-[12px]'>\n <h2 id={id} className='text-h4 text-sys-text-main'>\n {title}\n </h2>\n {action}\n </div>\n {columns ? (\n // 1px 간격 + 구분선 색 배경 = 칸 사이 가로·세로 구분선. 칸(DashboardPanel)은 자기 배경을 가진다\n // 2열에서 칸 수가 홀수면 마지막 칸이 두 열을 차지해 빈 칸이 생기지 않게 한다\n <Card\n className={cn(\n 'grid grid-cols-1 gap-px overflow-hidden bg-sys-divider-subtler',\n columns === 2 && 'xl:grid-cols-2 xl:[&>:last-child:nth-child(odd)]:col-span-2',\n )}\n >\n {children}\n </Card>\n ) : (\n children\n )}\n </section>\n )\n}\n\nexport type DashboardPanelProps = {\n /** 패널 제목 (h3) */\n title: ReactNode\n /** 제목 옆 현재값 (큰 글자) */\n value?: ReactNode\n /** 현재값 단위 (예: %, GB) */\n unit?: ReactNode\n /** 현재값 옆 보조 수치 (예: 할당 8 / 사용 3) */\n stats?: { label: ReactNode; value: ReactNode }[]\n /** true 면 내용 자리에 Skeleton. `empty` 보다 우선 */\n loading?: boolean\n /** 주면 내용 대신 표시. true 면 기본 문구 \"데이터가 없습니다.\" */\n empty?: ReactNode\n /** 내용 (차트·표). 차트 라이브러리는 소비 앱이 고른다 */\n children?: ReactNode\n}\n\n/**\n * DashboardSection(`columns`) 의 칸 하나. 제목·현재값·단위·보조 수치 머리 → 내용(차트·표). 자체 테두리는 없고 섹션 Card 가 칸 사이를 구분선으로 나눈다.\n *\n * 언제 쓰나: 시계열 차트나 작은 표 하나에 현재값을 붙여 보여 줄 때. 항상 `columns` 를 준 DashboardSection 안에 둔다.\n * 언제 안 쓰나: 숫자 하나만 보여 줄 때(→ DashboardLayout 의 `stats` 타일), 상세 화면의 정보 묶음(→ 제목 + Separator + MetaRow).\n * 대신/함께: 데이터가 없으면 `empty`, 불러오는 중이면 `loading` 을 준다. 내용 높이는 children 이 정한다.\n */\nexport function DashboardPanel({\n title,\n value,\n unit,\n stats,\n loading,\n empty,\n children,\n}: DashboardPanelProps) {\n let body = children\n if (loading) body = <LoadingState lines={2} />\n else if (empty)\n body = (\n <EmptyState className='py-[32px]'>{empty === true ? '데이터가 없습니다.' : empty}</EmptyState>\n )\n\n return (\n <div className='flex flex-col gap-[16px] bg-sys-background p-[24px]'>\n <div className='flex flex-wrap items-end gap-[12px]'>\n <h3 className='text-h6 text-sys-text-subtle'>{title}</h3>\n {value !== undefined && value !== null ? (\n <span className='flex items-end gap-[2px]'>\n <span className='text-h3 text-sys-text-main'>{value}</span>\n {unit ? <span className='text-base-regular text-sys-text-subtle'>{unit}</span> : null}\n </span>\n ) : null}\n {stats?.map((stat, i) => (\n <span\n key={i}\n className='flex items-end gap-[4px] border-l border-sys-divider-subtler pl-[12px]'\n >\n <span className='text-base-regular text-sys-text-subtle'>{stat.label}</span>\n <span className='text-h5 text-sys-text-main'>{stat.value}</span>\n </span>\n ))}\n </div>\n {body}\n </div>\n )\n}\n"]}
1
+ {"version":3,"sources":["../../src/patterns/listPageRules.ts","../../src/patterns/states.tsx","../../src/patterns/listPage.tsx","../../src/patterns/detailPage.tsx","../../src/patterns/formShell.tsx","../../src/patterns/formPage.tsx","../../src/patterns/formDrawer.tsx","../../src/patterns/formDialog.tsx","../../src/patterns/dashboardLayout.tsx"],"names":["jsxs","jsx","useRef","Fragment","useId"],"mappings":"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","file":"index.js","sourcesContent":["import type { ColDef, RowClassParams, RowClassRules } from 'ag-grid-community'\n\nimport { TwoLineCellRenderer } from '../components/agGridTable'\n\n// ListPage 의 기본값 규칙 — 패턴 내부용 (공개 API 아님). listPage.tsx 와 listPage.test.tsx 가 쓴다.\n\n/** primaryField 기본값: 명시값(null = 끔)이 우선, 없으면 행 클릭이 있을 때 'name' */\nexport function resolvePrimaryField(primaryField: string | null | undefined, hasRowClick: boolean) {\n if (primaryField !== undefined) return primaryField\n return hasRowClick ? 'name' : undefined\n}\n\n/** `primaryField` 와 field 가 같은 열에 `type: 'primary'` 를 붙인다 (이미 type 이 있으면 건드리지 않는다) */\nexport function withPrimaryColumn<TRow>(\n columns: ColDef<TRow>[],\n primaryField?: string | null,\n): ColDef<TRow>[] {\n if (!primaryField) return columns\n return columns.map((c) =>\n c.field === primaryField && c.type == null ? { ...c, type: 'primary' } : c,\n )\n}\n\n/**\n * `secondaryField` 가 있으면 주 식별자 열에 두 줄 렌더러(TwoLineCellRenderer)를 붙인다 — 이름 아래 12px 회색 보조 줄(2026-10-18 13차 ⑥).\n * 그 열이 이미 `cellRenderer` 를 가졌으면 건드리지 않는다(소비 코드의 렌더러가 우선)\n */\nexport function withSecondaryLine<TRow>(\n columns: ColDef<TRow>[],\n primaryField: string | null | undefined,\n secondaryField: string | undefined,\n): ColDef<TRow>[] {\n if (!primaryField || !secondaryField) return columns\n return columns.map((c) =>\n c.field === primaryField && c.cellRenderer == null\n ? {\n ...c,\n cellRenderer: TwoLineCellRenderer,\n cellRendererParams: { ...c.cellRendererParams, secondaryField },\n }\n : c,\n )\n}\n\n/**\n * 상세가 열린 행(`row.id === activeRowId`)에 `ag-row-active` 클래스를 붙이는 규칙. 사용자 rowClassRules 는 보존한다.\n * ag-grid 는 rowClassRules 옵션이 바뀌면 행 클래스를 다시 계산하므로, activeRowId 가 바뀔 때 새 객체를 만들어 넘긴다.\n */\nexport function activeRowClassRules<TRow>(\n activeRowId: string | number | null | undefined,\n userRules?: RowClassRules<TRow>,\n): RowClassRules<TRow> | undefined {\n if (activeRowId == null) return userRules\n return {\n ...userRules,\n 'ag-row-active': (p: RowClassParams<TRow>) =>\n (p.data as { id?: unknown } | undefined)?.id === activeRowId,\n }\n}\n\n/** 빈 결과에 \"필터 초기화\" 버튼을 둘지: 초기화 핸들러가 있고 필터가 걸려 있을 때(filtersActive 가 false 가 아닐 때) */\nexport const showEmptyReset = (onResetFilters: unknown, filtersActive: boolean | undefined) =>\n Boolean(onResetFilters) && filtersActive !== false\n\n/**\n * 빈 결과 문자열을 제목·설명으로 나눈다. 첫 문장 끝(마침표·물음표·느낌표 뒤 공백)에서 한 번만 나누고, 제목 끝의 마침표는 뗀다.\n * \"검색 조건에 맞는 모델이 없습니다. 조건을 바꾸거나 필터를 초기화하세요.\" → 제목 \"검색 조건에 맞는 모델이 없습니다\" + 설명 \"조건을 …\".\n * 문장 하나면 제목만. 소비 코드가 문장만 넘겨도(2026-10-08 실험 36 run 전부) 패턴이 두 줄 구성으로 그리기 위한 규칙.\n */\nexport function splitEmptyText(text: string): { title: string; description?: string } {\n const t = text.trim()\n const m = /^(.*?[.!?。])\\s+(\\S[\\s\\S]*)$/.exec(t)\n const strip = (s: string) => s.replace(/[.。]$/, '')\n if (!m) return { title: strip(t) }\n return { title: strip(m[1]), description: m[2] }\n}\n","import type { ReactNode } from 'react'\n\nimport { Button } from '../components/button'\nimport { EmptyBoxIllustration } from '../components/emptyBoxIllustration'\nimport { LucideIcon } from '../components/lucideIcon'\nimport { Skeleton } from '../components/skeleton'\nimport { cn } from '../utils/cn'\n\n/**\n * 패턴 공통 로딩 자리표시. 스크린리더에는 \"불러오는 중\" 으로 읽힌다.\n *\n * 언제 쓰나: 패턴 밖에서 본문 하나를 통째로 기다릴 때(패턴 안에서는 `loading` prop 이 이것을 그린다).\n * 언제 안 쓰나: 표 안의 행 로딩(→ AgGridTable 의 로딩 오버레이), 버튼 안의 진행(→ Button `loading`).\n * 대신/함께: 줄 수는 `lines` 로. 자리 크기를 실제 내용과 비슷하게 맞추면 로딩 끝에 화면이 덜 튄다.\n */\nexport function LoadingState({ className, lines = 3 }: { className?: string; lines?: number }) {\n return (\n <div\n role='status'\n aria-label='불러오는 중'\n className={cn('flex flex-col gap-[12px]', className)}\n >\n <Skeleton className='h-[24px] w-[240px]' />\n {Array.from({ length: lines }, (_, i) => (\n <Skeleton key={i} className={cn('h-[16px]', i % 2 ? 'w-3/5' : 'w-full')} />\n ))}\n </div>\n )\n}\n\ntype StateBoxProps = {\n role: 'status' | 'alert'\n className?: string\n icon: ReactNode\n iconClassName?: string\n title: ReactNode\n description?: ReactNode\n action?: ReactNode\n}\n\n/** 빈 결과·오류가 같이 쓰는 가운데 상자. 아이콘 → 제목 → 설명 → 행동 버튼 */\nfunction StateBox({\n role,\n className,\n icon,\n iconClassName,\n title,\n description,\n action,\n}: StateBoxProps) {\n return (\n <div\n role={role}\n className={cn(\n 'flex flex-col items-center justify-center gap-[4px] text-center text-sys-text-subtler',\n className,\n )}\n >\n <span aria-hidden className={cn('mb-[4px] flex text-sys-text-subtler', iconClassName)}>\n {icon}\n </span>\n <span className='text-h5 text-sys-text-main'>{title}</span>\n {description ? (\n <span className='break-keep text-base-regular text-sys-text-subtle wrap-break-word'>\n {description}\n </span>\n ) : null}\n {action ? <div className='pointer-events-auto mt-[12px] flex gap-[8px]'>{action}</div> : null}\n </div>\n )\n}\n\n/**\n * 패턴 공통 빈 결과·없음 안내. 가운데 정렬. 아이콘 → 제목 → 설명 → 행동 버튼 순.\n * 제목만 있으면 한 줄 안내, 설명까지 있으면 \"무엇이 없고 다음에 뭘 하나\" 를 두 줄로 — 2026-10-10 진단에서 두 평가자가 모두 더 낫다고 본 구성.\n *\n * 언제 쓰나: 목록이 0건, 선택지가 없음, 항목이 삭제됨처럼 \"보여 줄 것이 없다\" 를 알릴 때. 패턴(ListPage·DetailPage·DashboardLayout·DashboardPanel)은 `emptyState`·`notFound`·`empty` prop 으로 이것을 그린다.\n * 언제 안 쓰나: 불러오기 실패(→ ErrorState), 아직 불러오는 중(→ LoadingState), 입력 검증 실패(→ FormMessage).\n * 대신/함께: `<div>` 에 문장만 넣어 직접 만들지 않는다. 다음 행동이 있으면 `action` 에 버튼 하나(예: 필터 초기화·새로 만들기).\n */\nexport function EmptyState({\n className,\n children,\n icon,\n title,\n description,\n action,\n}: {\n className?: string\n /** 제목 자리(`title` 과 같다). 기존 호출부 호환용 */\n children?: ReactNode\n /** 제목 위 그림. 기본은 상자 일러스트 `EmptyBoxIllustration`(라이트·다크 토큰, 2026-10-13 — dl-ui-v3 사용자 정보 화면의 빈 패널과 같은 그림). 장식이라 스크린리더에는 읽히지 않는다 */\n icon?: ReactNode\n /** 무엇이 없는지 한 줄 (예: \"조건에 맞는 모델이 없습니다\") */\n title?: ReactNode\n /** 다음에 할 수 있는 일 (예: \"검색어나 필터를 바꾸거나 초기화해 보세요\") */\n description?: ReactNode\n /** 설명 아래 행동 버튼 (예: 필터 초기화). ag-grid 오버레이 안에서도 눌리게 pointer-events 를 연다 */\n action?: ReactNode\n}) {\n return (\n <StateBox\n role='status'\n className={className}\n icon={icon ?? <EmptyBoxIllustration />}\n title={title ?? children}\n description={description}\n action={action}\n />\n )\n}\n\n/**\n * 패턴 공통 오류 안내. 빈 결과 상자와 같은 배치(아이콘 → 제목 → 설명 → 다시 시도)에 오류 색 아이콘. `role='alert'`.\n *\n * 언제 쓰나: 화면·패널 본문을 통째로 못 불러왔을 때(패턴은 `error` prop 으로 이것을 그린다).\n * 언제 안 쓰나: 폼 섹션 안의 부분 실패(선택지 목록 로딩 → Alert(danger) + AlertActions 의 다시 시도 — 상자가 섹션을 밀어내지 않게, 2026-10-13), 제출 실패·서버 응답 오류처럼 화면은 그대로 두고 알려야 할 때(→ Alert(danger)·Toast), 입력 검증 실패(→ FormMessage).\n * 대신/함께: 빨간 Alert 줄로 본문을 대체하지 않는다 — 2026-10-12 블라인드 평가에서 가운데 상자가 일관되게 선호됐다. `onRetry` 를 주면 \"다시 시도\" 버튼이 붙는다. 원인·코드가 있으면 `description` 에 한 줄로.\n */\nexport function ErrorState({\n className,\n title = '불러오지 못했습니다',\n description,\n onRetry,\n retryLabel = '다시 시도',\n}: {\n className?: string\n /** 무엇이 실패했는지 한 줄. 기본 \"불러오지 못했습니다\" */\n title?: ReactNode\n /** 원인·안내 (예: \"서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.\") */\n description?: ReactNode\n /** 주면 \"다시 시도\" 버튼이 붙는다 */\n onRetry?: () => void\n /** 다시 시도 버튼 라벨. 기본 \"다시 시도\" */\n retryLabel?: ReactNode\n}) {\n return (\n <StateBox\n role='alert'\n className={className}\n icon={<LucideIcon iconComponentName='CircleAlert' size={36} />}\n iconClassName='text-danger30'\n title={title}\n description={description}\n action={\n onRetry ? (\n <Button type='button' variant='primary' onClick={onRetry}>\n {retryLabel}\n </Button>\n ) : null\n }\n />\n )\n}\n","'use client'\n\nimport { isValidElement, type ReactNode, useEffect, useMemo, useRef } from 'react'\nimport type { ColDef, RowClickedEvent, SortChangedEvent } from 'ag-grid-community'\nimport { RotateCcw, X } from 'lucide-react'\n\nimport { AgGridTable, type AgGridTableType, getSortedColumnState } from '../components/agGridTable'\nimport { Alert, AlertDescription } from '../components/alert'\nimport type { BadgeProps } from '../components/badge'\nimport { Button } from '../components/button'\nimport { IconButton } from '../components/iconButton'\nimport { PageHeader } from '../components/pageHeader'\nimport { Pagination } from '../components/pagination'\nimport { Panel } from '../components/panel'\nimport { TablePagination } from '../components/table'\nimport { ToggleGroup, ToggleGroupItem } from '../components/toggleGroup'\nimport { cn } from '../utils/cn'\nimport {\n activeRowClassRules,\n resolvePrimaryField,\n showEmptyReset,\n splitEmptyText,\n withPrimaryColumn,\n withSecondaryLine,\n} from './listPageRules'\nimport { EmptyState } from './states'\n\n/** 행을 눌러 보는 상세 — 목록 **오른쪽에 나란히** 그리는 패널. 표가 좁아지고 목록은 계속 보인다(덮는 Drawer 와 다름) */\nexport type ListPageDetail = {\n /** 패널 제목 (h2). 패널의 접근성 이름 */\n title: ReactNode\n /** 제목 아래 한 줄 (id·이미지 등) */\n description?: ReactNode\n /** 닫기 — 오른쪽 위 닫기 버튼과 Escape 가 부른다. 포커스는 옮기지 않는다(행에 남음) */\n onClose: () => void\n /** 본문 (보통 MetaList + MetaRow) */\n children: ReactNode\n /** 버튼 줄 위 안내 한 줄 — 비활성 버튼의 사유 자리 (예: \"실행 중인 환경만 중지할 수 있습니다\"). 비활성 버튼에는 tooltip 이 뜨지 않는다 */\n note?: ReactNode\n /** 버튼 줄 (오른쪽 정렬, 간격 8px) */\n footer?: ReactNode\n /** 패널 폭. small 360 · medium 480(기본) — Drawer size 와 같은 값 */\n width?: 'small' | 'medium'\n}\nconst DETAIL_WIDTH: Record<NonNullable<ListPageDetail['width']>, string> = {\n small: 'w-[360px]',\n medium: 'w-[480px]',\n}\n\n/** 빈 결과의 제목·설명을 직접 줄 때의 형태 */\nexport type ListPageEmptyContent = {\n /** 무엇이 없는지 (예: \"조건에 맞는 모델이 없습니다\") */\n title: ReactNode\n /** 다음에 할 수 있는 일 (예: \"검색어나 필터를 바꾸거나 초기화해 보세요\") */\n description?: ReactNode\n}\n\n/** 표 왼쪽 위의 빠른 필터(상태처럼 선택지 3~6개). 드롭다운과 달리 선택지·선택 상태·건수가 항상 보인다 */\nexport type ListPageQuickFilter = {\n /** 필터 대상 이름 (예: '상태'). 세그먼트 앞에 글자로 보이고 그룹의 접근성 이름이 된다 */\n label: string\n /** 선택지. `count` 를 주면 항목 오른쪽에 건수 Badge — \"지금 몇 건이 어떤 상태인가\" (2026-10-11 진단에서 건수가 있을 때 두 평가자 모두 종합 선호가 올랐다) */\n options: {\n value: string\n label: string\n count?: number\n /** 건수 pill 뱃지의 색. 기본 secondary. **표의 상태 뱃지와 같은 tone**(`success`·`danger`·`warning`·`info`·`secondary`)을 주면 칩과 상태 열이 같은 색 언어가 된다(예제 list-page.tsx 의 `STATUS[s].tone`). \"전체\" 는 패턴이 primary 로 그린다 (2026-10-18 13차 D, 사용자 결정) */\n countVariant?: NonNullable<BadgeProps['variant']>\n }[]\n /** 선택된 값들 (다중 선택). 빈 배열 = 전체 */\n value: string[]\n /** 선택이 바뀔 때. 소비 앱은 `pagination.page` 를 1로 되돌리고 `filtersActive` 에 이 필터도 포함한다 */\n onChange: (next: string[]) => void\n}\n\n/** quickFilter 의 \"전체\" 세그먼트 값 — 소비 앱의 `value` 배열에는 절대 들어가지 않는다(onChange 전에 걷어낸다) */\nconst QUICK_ALL = '__all__'\n\nconst isEmptyContent = (v: unknown): v is ListPageEmptyContent =>\n typeof v === 'object' && v !== null && !Array.isArray(v) && !isValidElement(v) && 'title' in v\n\n/** emptyState prop → 한 줄로 이어 붙일 제목·설명·(그 밖의 노드) */\nfunction resolveEmptyContent(emptyState: ReactNode | ListPageEmptyContent | undefined): {\n title?: ReactNode\n description?: ReactNode\n children?: ReactNode\n} {\n if (emptyState == null) return splitEmptyText('데이터가 없습니다.')\n if (typeof emptyState === 'string') return splitEmptyText(emptyState)\n if (isEmptyContent(emptyState)) return emptyState\n return { children: emptyState }\n}\n\nexport type ListPageProps<TRow> = {\n /** 페이지 제목 (h1) */\n title: string\n /** 제목 아래 설명 */\n description?: ReactNode\n /** 제목 오른쪽 슬롯 (예: 새로고침 버튼·마지막 갱신 시각) */\n titleRight?: ReactNode\n /** 주요 행동 버튼 (보통 1개). 제목 줄 오른쪽(PageHeader `actions`) — 2026-10-19 14차: 페이지 수준 행동은 제목 줄(Polaris 인덱스 페이지·GitHub Issues·기준선과 같은 자리). 1.22.0~1.47.4 의 필터 줄 왼쪽(SharePoint 계열)은 2026-10-13 UIClip(폐기된 자) 근거였다 */\n actions?: ReactNode\n /** 검색·필터 컨트롤. 헤더 오른쪽 아래 (TableFilter*, ColumnFilter 등) */\n filters?: ReactNode\n /** 필터를 되돌리는 핸들러. 주면 빠른 필터 줄(표 바로 위) 오른쪽 끝에 \"필터 초기화\" 외곽선 버튼이 **항상** 자리를 잡는다(빠른 필터·건수가 없으면 filters 줄 끝). 검색창은 그래서 표 오른쪽 끝까지 닿는다(2026-10-18 10차). 필터가 걸렸을 때만 조건부로 넘기지 말고(빈 결과에서 버튼이 튀어나와 배치가 바뀐다) 항상 넘기고 `filtersActive` 로 보일지를 정한다 */\n onResetFilters?: () => void\n /** 필터가 하나라도 걸려 있는가. false 면 \"필터 초기화\" 버튼이 자리만 지킨 채 보이지 않는다(invisible) */\n filtersActive?: boolean\n /** 주 식별자 열의 field. 그 열에 `type: 'primary'`(굵게, 행 hover 시 밑줄)를 붙인다. 기본값: `onRowClick` 이 있으면 `'name'`, 없으면 없음. 끄려면 `null` */\n primaryField?: string | null\n /** 주 식별자 열 아래 **보조 줄**에 보일 행 필드(id·이미지·경로·설명, 점 표기 가능). 열을 하나 더 늘리는 대신 이름 아래 12px 회색 한 줄로 — dl-ui-v3 목록(이름 / 설명)과 같은 모양이고 열이 적어 잘림도 준다(2026-10-18 13차 ⑥). 주 열(`primaryField`, 기본 `name`)에 `cellRenderer` 가 이미 있으면 적용하지 않는다. 주면 행 높이 56(`tableProps.rowHeight` 가 우선) */\n secondaryField?: string\n /** 표 위 제목. **쓰지 않는다** — 헤더 제목(h1)과 중복이고 빠른 필터 옆에서 \"tightly packed\" 로 감점(2026-10-18 10차, 사용자 결정). 남겨 둔 건 호환용. `pagination.total` 이 있으면 같은 줄 오른쪽에 \"총 N개\" 가 붙는다(제목·빠른 필터 둘 다 없으면 윗줄 자체를 안 그린다 — 2026-10-27) — 단 `quickFilter` 가 항목마다 `count` 를 보여 주면 \"전체 N\" 과 같은 숫자라 생략한다 */\n tableTitle?: ReactNode\n /** 표 왼쪽 위(제목 줄)의 빠른 필터 — 상태처럼 선택지 3~6개의 다중 선택. ToggleGroup(붙어있는 세그먼트)으로 그려 드롭다운을 열지 않아도 선택지가 보인다. 선택지가 많거나 검색이 필요한 축은 `filters` 의 TableFilterMultiSelect 로. `tableTitle` 이 없으면 이 줄을 세그먼트가 대신한다(줄이 늘지 않음) */\n quickFilter?: ListPageQuickFilter\n /** 상세가 열린 행의 id (`row.id` 와 비교). 그 행을 표의 선택 색으로 칠해 \"지금 어느 행을 보고 있나\" 를 보인다. 상세를 닫으면 null */\n activeRowId?: string | number | null\n /** 행을 눌러 보는 상세 패널. 주면 목록 **오른쪽에 나란히** 그린다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape). 닫으면 undefined 로. 2026-10-16(이슈 45) — 덮는 `<Drawer modal={false}>` 는 표를 좁히면 안 될 때의 대안 */\n detail?: ListPageDetail\n /** 표 컬럼 정의 (ag-grid ColDef) */\n columns: ColDef<TRow>[]\n /** 현재 페이지 행. undefined 면 표가 로딩 오버레이를 보인다 */\n rows?: TRow[]\n /** true 면 표 자리에 TableSkeleton */\n loading?: boolean\n /** 주면 표 대신 Alert(danger) 로 표시한다 */\n error?: ReactNode\n /** 0건일 때 표 안에 그릴 안내. 패턴이 **한 줄 문장**(+ 필터가 걸려 있으면 링크형 \"필터 초기화\")으로 그린다 — 아이콘·상자는 그리지 않는다(1.29.0 의 상자는 2026-10-13 되돌림). 문자열 또는 `{ title, description }`(이어 붙여 한 줄). 버튼·아이콘은 넣지 않는다. 기본 \"데이터가 없습니다\" */\n emptyState?: ReactNode | ListPageEmptyContent\n /** 페이지네이션. 없으면 그리지 않는다 */\n pagination?: {\n /** 전체 행 수 */\n total: number\n /** 현재 페이지 (1부터) */\n page: number\n /** 페이지당 행 수 */\n pageSize: number\n /** 페이지 변경 */\n onChange: (page: number) => void\n }\n /** 정렬 변경. 값은 getSortedColumnState 규칙 ('name' | '-name' | '') */\n onSortChange?: (sort: string) => void\n /** 행 클릭 */\n onRowClick?: (row: TRow) => void\n /** AgGridTable 에 그대로 넘길 나머지 props (rowHeight, rowSelection 등). 표 높이는 기본 `domLayout='autoHeight'`(행 수만큼) — 고정 높이는 `domLayout='normal'` + `containerStyle` */\n tableProps?: Omit<\n AgGridTableType,\n | 'rowData'\n | 'columnDefs'\n | 'loading'\n | 'noRowsOverlayComponent'\n | 'onSortChanged'\n | 'onRowClicked'\n >\n}\n\n/**\n * 목록 화면 패턴. PageHeader(제목·설명·주요 버튼·필터 줄) → AgGridTable → Pagination 을 한 번에 배치한다.\n *\n * 언제 쓰나: 실행환경·모델·파이프라인·데이터셋처럼 \"검색/필터 → 표 → 상세 진입\" 흐름이 있는 리소스 목록. 컬럼·행·필터 컨트롤·데이터 조회는 소비 앱이 넘기고, 패턴은 배치와 로딩·빈 결과·오류 상태만 책임진다.\n * 언제 안 쓰나: 차트·요약 카드 중심의 대시보드, 한 항목의 상세 화면, 표가 없는 설정 화면.\n * 대신/함께: 행 클릭 뒤 상세는 `onRowClick` 에서 상세 페이지로 이동하는 것이 기본이고, 목록 맥락을 유지해야 할 때는 `detail` 슬롯으로 목록 오른쪽에 **나란히** 연다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape, 비활성 사유는 `detail.note`). 표를 좁히면 안 될 때만 `<Drawer modal={false}>`(덮는 옆 패널)로(어느 쪽인지는 화면 설계가 정한다). 필터가 바뀌면 소비 앱이 `pagination.page` 를 1로 되돌린다. `onResetFilters` 는 항상 넘기고 `filtersActive` 로 보일지를 정한다(필터가 걸린 채 0건이면 빈 결과 가운데에도 \"필터 초기화\" 가 나온다). 상세를 Drawer 로 열면 `activeRowId` 로 그 행을 표시한다. 주 식별자 열은 `primaryField`(행 클릭이 있으면 기본 `'name'`), 표 위 제목·건수는 `tableTitle`·`pagination.total`. 상태처럼 선택지가 3~6개인 다중 선택 필터는 `quickFilter` 로 표 왼쪽 위에 세그먼트로 둔다(드롭다운에 숨기지 않는다). 본문은 제목을 이름으로 가진 `section` landmark 다(소비 앱의 `<main>` 안에 둔다). 표는 한 페이지의 행을 다 보이도록 행 수만큼 자란다(`domLayout='autoHeight'` 기본) — 화면 높이에 맞춘 안쪽 스크롤이 필요하면 `tableProps` 로 `domLayout='normal'` 과 `containerStyle` 을 넘긴다.\n */\nexport function ListPage<TRow>({\n title,\n description,\n titleRight,\n actions,\n filters,\n onResetFilters,\n columns,\n rows,\n loading,\n error,\n emptyState,\n pagination,\n filtersActive,\n primaryField,\n secondaryField,\n tableTitle,\n quickFilter,\n activeRowId,\n detail,\n onSortChange,\n onRowClick,\n tableProps,\n}: ListPageProps<TRow>) {\n // detail 이 열린 동안 Escape 로 닫는다(목록 키보드 규약: Escape 가 상세를 닫고 포커스는 행에 남는다). 열린 dialog 안에서 누른 Escape 는\n // 그쪽 몫이라 무시(Radix 가 preventDefault 하거나 target 이 dialog 안). 핸들러는 ref 로 읽어 리스너를 매 렌더 다시 달지 않는다\n const onCloseRef = useRef(detail?.onClose)\n useEffect(() => {\n onCloseRef.current = detail?.onClose\n }, [detail?.onClose])\n const detailOpen = detail != null\n useEffect(() => {\n if (!detailOpen) return\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape' || e.defaultPrevented) return\n const target = e.target as HTMLElement | null\n if (target?.closest?.('[role=\"dialog\"],[role=\"alertdialog\"]')) return\n onCloseRef.current?.()\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [detailOpen])\n // \"필터 초기화\" 는 빠른 필터 줄(표 바로 위)의 **오른쪽 끝** — 검색 줄에 두면 보이지 않을 때도 자리를 차지해 검색창이 표 오른쪽 끝에\n // 못 닿는다(9차 심판 \"search field's right edge doesn't line up with the table\"). 자리는 항상 유지(filtersActive 로 비활성).\n // 2026-10-16 이슈 47: 검색창 앞에 두었을 때 \"검색창 앞에 어색하게\" 감점 → 2026-10-18 10차 ④: 빠른 필터 줄로. 그 줄이 없을 때만 검색 줄 끝\n const resetButton = onResetFilters ? (\n <Button\n type='button'\n // outline: 옆의 외곽선 칩·검색창과 같은 꼴(2026-10-17 2차에서 회색 채움 링크 버튼이 \"외곽선 검색창과 안 맞음\" 감점).\n // 필터가 없으면 자리만 두고 보이지 않는다(invisible) — 비활성 회색 채움도 감점됐다(4차). 자리를 비우지 않으니 배치는 안 바뀐다\n variant='secondaryOutline'\n onClick={onResetFilters}\n className={cn(filtersActive === false && 'invisible')}\n aria-hidden={filtersActive === false || undefined}\n >\n <RotateCcw size={16} aria-hidden />\n 필터 초기화\n </Button>\n ) : null\n // 제목도 빠른 필터도 없으면 윗줄을 그리지 않는다 — \"총 N개\" 만 남은 줄이 표 위 여백이 되고(2026-10-27 홀드아웃 11, 심판 2건) 같은 숫자는 아래 페이지네이션 라벨(\"전체 N개 중 X-Y번째\")에 있다\n const hasCaption = tableTitle != null || quickFilter != null\n const filterRow =\n filters || (resetButton && !hasCaption) ? (\n <div className='flex w-full items-start gap-[8px]'>\n {filters}\n {!hasCaption ? resetButton : null}\n </div>\n ) : undefined\n\n const columnDefs = useMemo(() => {\n // 행을 눌러 상세로 가는 목록이면 name 열이 기본 주 식별자 (2026-10-09 실험 36/36 이 이 구성). 보조 줄(secondaryField)을 주면 그 열이 주 식별자다\n const primary = resolvePrimaryField(\n primaryField,\n Boolean(onRowClick) || Boolean(secondaryField),\n )\n return withSecondaryLine(withPrimaryColumn(columns, primary), primary, secondaryField)\n }, [columns, primaryField, onRowClick, secondaryField])\n const total = pagination?.total\n // 빠른 필터: 표 왼쪽 위 세그먼트. 2026-10-11 진단(docs/results/diagnostic-status-toggle.md) — 드롭다운 대비 Ease of Use·Intuitive 가\n // 두 평가자 모두 올랐고 건수가 있을 때 종합 선호도 올랐다. 제목 줄에 두어 tableTitle 이 없으면 줄이 늘지 않는다\n // \"전체\" 세그먼트: 선택이 비었을 때 켜져 보여 \"지금 전체를 보고 있다\" 가 드러난다(2026-10-16 이슈 47, 원문 심판 6회 감점). 누르면 선택을 비운다.\n // 건수는 모든 선택지에 count 가 있을 때만 합계\n const quickAllCount = quickFilter?.options.every((o) => o.count !== undefined)\n ? quickFilter.options.reduce((sum, o) => sum + (o.count ?? 0), 0)\n : undefined\n const quickIsAll = (quickFilter?.value.length ?? 0) === 0\n const quick = quickFilter ? (\n <span className='flex items-center gap-[8px]'>\n <span className='text-small-regular font-medium text-sys-text-subtle'>\n {quickFilter.label}\n </span>\n <ToggleGroup\n type='multiple'\n variant='outline'\n // 떨어진 외곽선 pill(spacing): 한 테두리로 묶인 그룹은 옆의 외곽선 버튼·검색창과 다른 꼴로 읽혔다 (2026-10-17 이슈 47 4차)\n spacing\n aria-label={quickFilter.label}\n value={quickIsAll ? [QUICK_ALL] : quickFilter.value}\n onValueChange={(next) => {\n // 전체가 꺼진 상태에서 전체를 누름 → 선택을 비움. 그 외에는 전체 표식만 걷어낸 선택을 넘김(비어지면 다시 전체)\n const pressedAll = next.includes(QUICK_ALL) && !quickIsAll\n quickFilter.onChange(pressedAll ? [] : next.filter((v) => v !== QUICK_ALL))\n }}\n >\n <ToggleGroupItem value={QUICK_ALL} count={quickAllCount} countVariant='primary'>\n 전체\n </ToggleGroupItem>\n {quickFilter.options.map((o) => (\n <ToggleGroupItem\n key={o.value}\n value={o.value}\n count={o.count}\n countVariant={o.countVariant}\n >\n {o.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </span>\n ) : null\n // 표 제목 줄과 표 사이 8(붙어 있는 요소), 페이지네이션 위 32, 헤더→본문 32 — 2026-10-17 8차 D2 간격 세 단계\n const caption = hasCaption ? (\n <div className='mb-[8px] flex items-center justify-between gap-[8px]'>\n {tableTitle != null || quick ? (\n <div className='flex flex-wrap items-center gap-[16px]'>\n {tableTitle != null ? <h2 className='text-h6 text-sys-text-main'>{tableTitle}</h2> : null}\n {quick}\n </div>\n ) : (\n <span />\n )}\n <div className='flex shrink-0 items-center gap-[16px]'>\n {/* 빠른 필터가 \"전체 N\" 건수를 그리면 \"총 N개\" 는 같은 숫자가 한 줄에 두 번 — 원문 심판 copy 감점(2026-10-17 이슈 47 6차) → 생략 */}\n {total != null && quickAllCount === undefined ? (\n <span className='text-small-regular text-sys-text-subtle'>\n 총 {total.toLocaleString()}개\n </span>\n ) : null}\n {resetButton}\n </div>\n </div>\n ) : null\n\n // 빈 결과의 \"필터 초기화\": 필터가 걸린 채 0건이면 표 가운데에도 복구 버튼을 둔다 (2026-10-09 Q1 평가 1순위 지적).\n // 핸들러는 ref 로 읽어 오버레이 컴포넌트가 매 렌더 새로 만들어지지 않게 한다\n const resetRef = useRef(onResetFilters)\n useEffect(() => {\n resetRef.current = onResetFilters\n }, [onResetFilters])\n const withReset = showEmptyReset(onResetFilters, filtersActive)\n\n // ag-grid 는 컴포넌트 참조가 바뀌면 오버레이를 다시 만들므로 내용이 바뀔 때만 새로 만든다.\n // 2026-10-13 07:30(사용자 결정): 빈 결과도 EmptyState 상자(상자 일러스트 + 제목 + 설명 + 링크형 \"필터 초기화\").\n // 1.29.0 의 Lucide 아이콘 상자는 UIClip 에서 떨어져 한 줄 문장으로 되돌렸었는데(9-B), 그림이 dl-ui-v3 의 빈 상태 일러스트로 바뀌어 다시 상자로 — 정본 측정은 README \"패턴 정본\" 절\n const EmptyOverlay = useMemo(\n () =>\n function ListPageEmptyOverlay() {\n const content = resolveEmptyContent(emptyState)\n return (\n <EmptyState\n className='pt-[32px] pb-[16px]'\n title={content.title}\n // 소비 코드가 emptyState 를 노드로 직접 그렸으면 그 노드를 설명 자리에 그대로 둔다\n description={content.description ?? content.children}\n // 소비 노드에 자기 버튼이 있을 수 있으니 그때는 패턴 링크를 겹쳐 그리지 않는다 — 1.32.1 재촬영에서 \"필터 초기화\" 가 두 번 보였다\n action={\n withReset && content.children == null ? (\n <Button type='button' variant='secondaryLink' onClick={() => resetRef.current?.()}>\n 필터 초기화\n </Button>\n ) : null\n }\n />\n )\n },\n [emptyState, withReset],\n )\n const rowClassRules = useMemo(\n () => activeRowClassRules<TRow>(activeRowId, tableProps?.rowClassRules),\n [activeRowId, tableProps?.rowClassRules],\n )\n\n return (\n <div className='w-full'>\n <PageHeader\n title={title}\n description={description}\n titleRight={titleRight}\n // 주요 버튼은 제목 줄 오른쪽(2026-10-19 14차, 사용자 결정 L-A — DetailPage·DashboardLayout 과 한 규칙). 필터 줄(bottomRight)은 검색·필터·초기화만.\n // 이전: 1.22.0 필터 줄 왼쪽 → 1.25.0 제목 오른쪽 → 1.32.2 필터 줄 왼쪽(13-B, UIClip 근거) → 14차 제목 줄\n actions={actions}\n bottomRight={filterRow}\n />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark — 표 칸이 landmark 밖으로 찍히지 않게. <main> 은 소비 앱 레이아웃 몫 */}\n {/* 헤더→본문 32(묶음 사이) — 2026-10-17 8차 D2 */}\n <section aria-label={title} className='px-[24px] py-[32px]'>\n {error ? (\n <Alert variant='danger'>\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : (\n // 상세 패널(detail)이 있으면 표 쪽이 줄고 패널이 오른쪽에 나란히 선다 — 목록 맥락을 유지한 채 상세를 본다\n <div className='flex items-start gap-[24px]'>\n <div className='min-w-0 flex-1'>\n {caption}\n <AgGridTable\n rowData={rows}\n columnDefs={columnDefs}\n loading={loading}\n noRowsOverlayComponent={EmptyOverlay}\n onSortChanged={\n onSortChange\n ? (e: SortChangedEvent<TRow>) => onSortChange(getSortedColumnState(e).value)\n : undefined\n }\n onRowClicked={\n onRowClick\n ? (e: RowClickedEvent<TRow>) => {\n if (e.data) onRowClick(e.data)\n }\n : undefined\n }\n // 표는 행 수만큼 자란다(2026-10-15 이슈 38) — AgGridTable 의 기본 300px 고정 높이는 한 페이지 20건 중 5건만 보여\n // 나머지가 안쪽 스크롤에 숨었다. 고정 높이가 필요하면 tableProps 로 domLayout='normal' + containerStyle 을 넘긴다\n domLayout='autoHeight'\n // 두 줄 셀(secondaryField)이면 행 56 — dl-ui-v3 목록과 같은 높이. undefined 를 넘기면 AgGridTable 기본(50)을 덮어쓰므로 있을 때만\n {...(secondaryField ? { rowHeight: 56 } : {})}\n {...tableProps}\n rowClassRules={rowClassRules}\n />\n {pagination ? (\n <TablePagination>\n <Pagination\n total={pagination.total}\n rowsPerPage={pagination.pageSize}\n page={pagination.page}\n onChange={pagination.onChange}\n showLabel\n />\n </TablePagination>\n ) : null}\n </div>\n {detail ? (\n // 나란한 상세 패널: 제목·설명·닫기 → 본문 → (안내) → 버튼 줄. 스크롤해도 따라오게 sticky. Card 와 같은 테두리·라운드(그림자 없음).\n // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section — DetailPage 의 aside 와 같다\n <Panel\n title={detail.title}\n description={detail.description}\n padding='large'\n titleRight={\n <IconButton\n type='button'\n variant='secondary'\n aria-label='닫기'\n onClick={detail.onClose}\n >\n <X size={20} />\n </IconButton>\n }\n note={detail.note}\n footer={detail.footer}\n className={cn('sticky top-[24px] shrink-0', DETAIL_WIDTH[detail.width ?? 'medium'])}\n >\n {detail.children}\n </Panel>\n ) : null}\n </div>\n )}\n </section>\n </div>\n )\n}\n","'use client'\n\nimport { type ReactNode, useRef } from 'react'\nimport { Ellipsis } from 'lucide-react'\n\nimport { Button, type ButtonProps } from '../components/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/dropdownMenu'\nimport { IconButton } from '../components/iconButton'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Panel } from '../components/panel'\nimport { cn } from '../utils/cn'\nimport { EmptyState, ErrorState, LoadingState } from './states'\n\n/** 헤더 행동 버튼 하나. `children` 이 라벨(아이콘 포함), variant 는 슬롯이 정한다 */\nexport type DetailPageAction = Omit<ButtonProps, 'variant' | 'onClick'> & {\n /** 클릭. 좁은 헤더에서 \"더보기\" 메뉴 항목으로 접혔을 때는 항목 선택 */\n onClick?: () => void\n}\n\nexport type DetailPageProps = Pick<\n PageHeaderProps,\n | 'title'\n | 'description'\n | 'status'\n | 'meta'\n | 'titleRight'\n | 'breadcrumb'\n | 'tabs'\n | 'activeTab'\n | 'onTabChange'\n> & {\n /** 주 행동 1개 (예: 배포·새 추론 서비스). `primaryBold` 로 맨 오른쪽에 그린다 */\n primaryAction?: DetailPageAction\n /** 보조 행동들 (예: 다운로드·복제·새 버전). `secondary` 로 주 행동 왼쪽에 준 순서대로 — 2개까지만 보이고 3번째부터는 \"더보기\" 메뉴로 접힌다(2026-10-19 14차) */\n secondaryActions?: DetailPageAction[]\n /** 삭제. 항상 \"더보기\" 메뉴 안(구분선 아래 danger 항목)에 둔다 — 빨간 버튼을 첫 화면에 펼치지 않는다(2026-10-19 14차). 확인 Dialog 는 소비 앱이 onClick 에서 연다 */\n deleteAction?: DetailPageAction\n /** 버튼 묶음을 직접 구성할 때만 (드롭다운·툴팁 등). 주면 위 세 슬롯 대신 이것을 그대로 그린다 */\n actions?: ReactNode\n /** **쓰지 않는다**(2026-10-18 11차, 사용자 결정) — 본문 위 안내 한 줄이 4라운드 연속 \"떠 있다\" 로 감점. 비활성 버튼은 비활성으로만 두고, 상태 자체의 안내가 필요하면 `banner`. 호환용으로 남긴 prop */\n actionsNote?: ReactNode\n /** 헤더 아래, 본문 위에 놓는 알림 슬롯 (예: 삭제 예정·권한 안내 Alert). 상태와 무관하게 항상 그린다 */\n banner?: ReactNode\n /** true 면 본문 자리에 Skeleton. error·notFound 보다 우선 */\n loading?: boolean\n /** 주면 본문 대신 오류 상자(ErrorState: 아이콘·\"불러오지 못했습니다\"·이 내용). `onRetry` 가 있으면 \"다시 시도\" 버튼을 함께 그린다 */\n error?: ReactNode\n /** error 상태의 \"다시 시도\" 클릭 */\n onRetry?: () => void\n /** 항목이 없을 때 본문 대신 없음 상자(EmptyState — 상자 일러스트, 2026-10-13 부터 목록·대시보드와 같은 그림). true 면 기본 문구 \"항목을 찾을 수 없습니다.\" + 안내 한 줄 */\n notFound?: ReactNode\n /** **호환용** — 새 화면은 핵심 정보를 헤더 `meta`(`MetaGroup` + `MetaItem`)에 둔다(2026-10-27 사용자 결정 A: 오른쪽 패널은 내용이 짧아 첫 화면 아래가 비는 것이 홀드아웃 9~12 연속 지적, 2026-10-30 요약 상자 대신 메타 자리 하나로). 주면 넓은 화면(xl)에서 본문 2 : 패널 1, 좁은 화면에서는 본문 아래 */\n aside?: ReactNode\n /** 패널 제목(Panel 머리글, 기본 \"기본 정보\"). 패턴이 그리므로 `aside` 안에 같은 제목을 다시 쓰지 않는다(2026-10-27 홀드아웃 12: \"작업 정보\" 가 연달아 두 번) */\n asideTitle?: ReactNode\n /** 본문 폭. full = 가로 전체(정보 탭), panel = 가운데 900px (설정 탭). 기본 full */\n width?: 'full' | 'panel'\n /** 본문 (현재 탭 내용). 탭 상태와 라우팅은 소비 앱이 가진다 */\n children?: ReactNode\n}\n\n/**\n * 상세 화면 패턴. PageHeader(제목·상태·메타·행동·탭) → 알림 → 본문(전폭 또는 가운데 패널) 을 한 번에 배치하고 로딩·오류·없음 상태를 책임진다.\n *\n * 언제 쓰나: 실행환경·모델·파이프라인 하나의 상세 화면. 핵심 정보는 헤더 `meta` 한 곳(`MetaGroup` + `MetaItem` 을 가로로 흘림 — 탭을 바꿔도 보임, 2026-10-30 사용자 결정: 메타 줄과 요약 상자를 따로 두지 않는다)으로, 정보 탭 본문은 전폭 Panel 들, 설정 탭은 `width='panel'` 로 가운데 900px. 오른쪽 패널 `aside` 는 호환용. 데이터 조회·탭 전환·삭제 확인은 소비 앱이 가진다.\n * 언제 안 쓰나: 목록(→ ListPage), 입력이 주인 화면(→ FormPage), 요약 타일·차트 중심(→ DashboardLayout), 목록 옆에 잠깐 띄우는 상세(→ Drawer 에 본문만 넣는다).\n * 대신/함께: `tabs`·`activeTab`·`onTabChange` 는 PageHeader 와 같다. 상태 Badge 는 `status`, 메타는 `meta` 에 MetaGroup/MetaItem, 본문 Panel 의 속성 묶음은 MetaList/MetaRow. 본문 안의 묶음은 `Panel`(제목 + 1px 테두리 상자)로 나눈다 — 옆 패널도 같은 Panel 이라 본문과 패널이 한 규칙으로 보인다(2026-10-17). 행동 버튼은 `deleteAction`·`secondaryActions`·`primaryAction` 슬롯으로 주면 왼쪽부터 삭제(danger) → 보조(secondary) → 주 행동(primaryBold) 순으로 강조가 올라가게 패턴이 배치하고, 헤더 내용 폭이 768px 보다 좁아지면 주 행동만 남기고 나머지를 \"더보기\" DropdownMenu 로 접는다(dl-ui-v3 상세 관례). `error`·`notFound` 는 패턴이 가운데 상자(아이콘·제목·설명·버튼)로 그리므로 소비 앱이 Alert·`<div>` 로 직접 만들지 않는다. `error` 에 `onRetry` 를 주면 \"다시 시도\" 버튼이 붙는다.\n */\n/** \"더보기\" ⋯ 메뉴: 넘긴 보조 행동들 + (구분선) + 삭제.\n * 항목의 onClick 은 메뉴가 닫히는 `onCloseAutoFocus` 에서 ⋯ 트리거로 포커스를 되돌린 **다음** 부른다 — 선택과 같은 틱에 확인 대화상자를 열면\n * 대화상자가 \"이전 포커스\" 로 사라지는 메뉴 항목(또는 body)을 기억해, 닫힌 뒤 포커스가 ⋯ 로 돌아오지 않는다(키보드 검사 focusReturnedToTrigger 거짓, 2026-10-19 15차 실측.\n * 메뉴 닫힘 애니메이션 때문에 setTimeout(0) 으로도 모자랐다) */\nfunction MoreMenu({\n items,\n deleteAction,\n}: {\n items: DetailPageAction[]\n deleteAction?: DetailPageAction\n}) {\n const triggerRef = useRef<HTMLButtonElement>(null)\n const pendingRef = useRef<(() => void) | undefined>(undefined)\n const select = (fn?: () => void) => () => {\n pendingRef.current = fn\n }\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <IconButton ref={triggerRef} variant='secondary' aria-label='더보기'>\n <Ellipsis size={24} />\n </IconButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align='end'\n onCloseAutoFocus={(e) => {\n e.preventDefault()\n triggerRef.current?.focus()\n const fn = pendingRef.current\n pendingRef.current = undefined\n fn?.()\n }}\n >\n {items.map(({ children, onClick, disabled }, i) => (\n <DropdownMenuItem key={i} disabled={disabled} onSelect={select(onClick)}>\n {children}\n </DropdownMenuItem>\n ))}\n {deleteAction && items.length ? <DropdownMenuSeparator /> : null}\n {deleteAction ? (\n <DropdownMenuItem\n variant='danger'\n disabled={deleteAction.disabled}\n onSelect={select(deleteAction.onClick)}\n >\n {deleteAction.children}\n </DropdownMenuItem>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nexport function DetailPage({\n primaryAction,\n secondaryActions,\n deleteAction,\n actions,\n actionsNote,\n banner,\n loading,\n error,\n onRetry,\n notFound,\n aside,\n asideTitle = '기본 정보',\n width = 'full',\n children,\n ...header\n}: DetailPageProps) {\n let body: ReactNode\n if (loading) {\n body = <LoadingState />\n } else if (error) {\n body = <ErrorState className='py-[32px]' description={error} onRetry={onRetry} />\n } else if (notFound) {\n body = (\n <EmptyState\n className='py-[32px]'\n title={notFound === true ? '항목을 찾을 수 없습니다.' : notFound}\n description={notFound === true ? '삭제되었거나 주소가 잘못되었을 수 있습니다.' : undefined}\n />\n )\n } else if (aside) {\n // 패널 사이·본문 상하 24 — 8차 D2 에서 32 로 키웠다가 심판 \"right column leaves a large empty area\" 감점으로 24 복원(2026-10-18 9차)\n body = (\n // 넓은 화면: 본문 2 : 패널 1 + 세로선. 좁은 화면: 세로로 쌓이고 패널은 제목 + 가로선으로 시작한다\n <div className='grid grid-cols-1 gap-[24px] xl:grid-cols-[2fr_1fr] xl:items-start'>\n <div className='min-w-0'>{children}</div>\n {/* <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section */}\n {/* 테두리 상자(Panel — ListPage detail·FormPage aside·본문 구역과 같은 상자). 세로선은 \"다른 선\" 으로 읽혔다 (2026-10-17 이슈 47 4·5차) */}\n <Panel title={asideTitle}>{aside}</Panel>\n </div>\n )\n } else {\n body = children\n }\n\n // 강조 순서는 패턴이 정한다 (2026-10-19 14차, 사용자 결정 2-A): 넓은 헤더 = [더보기(⋯)][보조 최대 2개][주 행동(primaryBold)].\n // 삭제(danger)는 항상 \"더보기\" 메뉴 안(구분선 아래) — 펼친 빨간 버튼은 첫 화면에서 주의를 뺏고 \"destructive button placed first\" 로 감점됐다(5·13차).\n // 보조 3번째부터도 메뉴로(Polaris·Material 3 의 \"보이는 개수 제한 + 나머지는 overflow\"). actions 를 주면 그대로\n const VISIBLE_SECONDARY = 2\n const visibleSecondary = secondaryActions?.slice(0, VISIBLE_SECONDARY) ?? []\n const overflowSecondary = secondaryActions?.slice(VISIBLE_SECONDARY) ?? []\n const primaryButton = primaryAction ? (\n <Button type='button' variant='primaryBold' {...primaryAction} />\n ) : null\n const moreMenu = (items: DetailPageAction[]) =>\n items.length || deleteAction ? <MoreMenu items={items} deleteAction={deleteAction} /> : null\n const fullRow = (\n <>\n {moreMenu(overflowSecondary)}\n {visibleSecondary.map((action, i) => (\n <Button key={i} type='button' variant='secondary' {...action} />\n ))}\n {primaryButton}\n </>\n )\n // 좁은 헤더(내용 폭 < 768px = @3xl): 주 행동만 남기고 보조 전부·삭제는 \"더보기\" 메뉴로. 판정은 PageHeader <header> 의 @container 가 한다\n const collapsedRow = (\n <>\n {moreMenu(secondaryActions ?? [])}\n {primaryButton}\n </>\n )\n const collapsible = Boolean(deleteAction || secondaryActions?.length)\n // 비활성 사유 같은 안내는 버튼 줄 **윗줄**(오른쪽 정렬) — 같은 줄 왼쪽에 두면 긴 문장이 버튼을 밀어낸다 (2026-10-16 이슈 45 → 2026-10-17 4차 조정)\n // 비활성 사유 같은 안내는 탭 선 **아래**, 본문 첫 줄 오른쪽 정렬(버튼 바로 아래 같은 열) — 버튼 윗줄(4차)은 \"버튼과 안내가 탭 선 위에 떠 있다\",\n // 같은 줄 왼쪽(45-A)은 \"긴 문장이 버튼을 밀어낸다\" 로 읽혔다 (2026-10-17 이슈 47 6차)\n const note =\n actionsNote != null ? (\n <p\n data-slot='detail-actions-note'\n className='mb-[8px] break-keep text-right text-small-regular text-sys-text-subtle wrap-break-word'\n >\n {actionsNote}\n </p>\n ) : null\n const actionRow = actions ? (\n actions\n ) : collapsible ? (\n <>\n <div data-slot='detail-actions-full' className='hidden items-center gap-[8px] @3xl:flex'>\n {fullRow}\n </div>\n <div data-slot='detail-actions-collapsed' className='flex items-center gap-[8px] @3xl:hidden'>\n {collapsedRow}\n </div>\n </>\n ) : primaryButton ? (\n primaryButton\n ) : undefined\n\n return (\n <div className='w-full'>\n <PageHeader {...header} actions={actionRow} />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다) */}\n <section\n aria-label={header.title || '본문'}\n className='flex flex-col gap-[24px] px-[24px] py-[24px]'\n >\n {banner}\n <div className={cn('w-full', width === 'panel' && 'mx-auto max-w-[900px]')}>\n {note}\n {body}\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport type { ReactNode } from 'react'\nimport type { FieldValues, UseFormReturn } from 'react-hook-form'\n\nimport { Alert, AlertDescription } from '../components/alert'\nimport { Button } from '../components/button'\n\n/** FormPage · FormDrawer · FormDialog 가 공유하는 props */\nexport type FormShellProps<TValues extends FieldValues> = {\n /** 제목 */\n title: string\n /** 제목 아래 설명 */\n description?: ReactNode\n /** react-hook-form `useForm()` 반환값. 검증 스키마·기본값은 소비 앱이 소유한다 */\n form: UseFormReturn<TValues>\n /** 검증 통과 후 호출. Promise 를 돌려주면 끝날 때까지 제출 중 상태가 된다 */\n onSubmit: (values: TValues) => void | Promise<void>\n /** 제출 버튼 라벨. 기본 \"저장\" */\n submitLabel?: string\n /** 취소 버튼 라벨. 기본 \"취소\" */\n cancelLabel?: string\n /** 외부 저장 중 상태(뮤테이션 등). 버튼 loading·disabled, 취소·닫기 차단 */\n submitting?: boolean\n /** 서버 오류 등 폼 전체 오류. 버튼 위에 Alert(danger) 로 표시 */\n error?: ReactNode\n /**\n * 제출 버튼 활성 규칙. `always`(기본) = 제출 중이 아니면 항상 눌리고, 누르면 react-hook-form 이 검증해 틀린 항목마다 FormMessage 를 보인다.\n * `whenValid` = 모든 입력이 유효해질 때까지 잠근다(2026-10-12 전 기본). \"누르면 검증해 이유 표시\" 가 흔한 폼 관례라 기본을 바꿨다\n */\n submitMode?: 'always' | 'whenValid'\n /** FormSection 으로 묶은 FormField 들 */\n children: ReactNode\n}\n\n/** 제출 상태·버튼 활성 조건·submit 핸들러. 세 껍데기가 같은 규칙을 쓴다 */\nexport function useFormShell<TValues extends FieldValues>({\n form,\n onSubmit,\n submitting,\n submitMode = 'always',\n}: Pick<FormShellProps<TValues>, 'form' | 'onSubmit' | 'submitting' | 'submitMode'>) {\n // isValid 는 render 에서 읽어야 react-hook-form 이 재계산한다 (구독 기반 proxy)\n const { isValid, isSubmitting } = form.formState\n const busy = Boolean(submitting) || isSubmitting\n const handleSubmit = form.handleSubmit((values) => onSubmit(values))\n return { busy, canSubmit: !busy && (submitMode === 'always' || isValid), handleSubmit }\n}\n\n/** 폼 전체 오류. 없으면 아무것도 그리지 않는다 */\nexport function FormError({ error, className }: { error?: ReactNode; className?: string }) {\n if (!error) return null\n return (\n <Alert variant='danger' className={className}>\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )\n}\n\ntype FormActionsProps = {\n busy: boolean\n canSubmit: boolean\n /** 없으면 취소 버튼을 그리지 않는다 */\n onCancel?: () => void\n submitLabel?: string\n cancelLabel?: string\n /** 버튼이 `<form>` 밖(예: FormPage 의 검토 패널)에 놓일 때 그 form 의 id — HTML `form` 속성으로 제출을 연결한다 (2026-10-20 16차 D1) */\n formId?: string\n}\n\n/** 취소 + 제출 버튼. 제출 중에는 둘 다 비활성 */\nexport function FormActions({\n busy,\n canSubmit,\n onCancel,\n submitLabel = '저장',\n cancelLabel = '취소',\n formId,\n}: FormActionsProps) {\n return (\n <>\n {onCancel ? (\n <Button\n type='button'\n variant='secondaryOutline'\n onClick={onCancel}\n disabled={busy}\n form={formId}\n >\n {cancelLabel}\n </Button>\n ) : null}\n <Button\n type='submit'\n variant='primaryBold'\n loading={busy}\n disabled={!canSubmit}\n form={formId}\n >\n {submitLabel}\n </Button>\n </>\n )\n}\n","'use client'\n\nimport { type ReactNode, useId } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport { Card } from '../components/card'\nimport { Form, FormFooter, FormSectionLevelContext } from '../components/form'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Panel } from '../components/panel'\nimport { cn } from '../utils/cn'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormPageProps<TValues extends FieldValues> = FormShellProps<TValues> &\n Pick<PageHeaderProps, 'breadcrumb' | 'titleRight'> & {\n /** 취소 버튼 핸들러. 없으면 취소 버튼을 그리지 않는다 */\n onCancel?: () => void\n /** true 면 폼 맨 위에 \"* 표시는 필수 입력 항목입니다\" 안내 한 줄 */\n requiredLegend?: boolean\n /** 본문 바탕. 'plain'(기본)은 흰 캔버스에 섹션 구분선. 'subtle' 은 연한 회색 캔버스 위에 폼 전체를 흰 Card 하나로 — UIClip 폼 진단(evals/metrics-spec §16.7)용 옵트인이며 캔버스 정책(DESIGN.md)은 흰색 그대로다 */\n canvas?: 'plain' | 'subtle'\n /** 폼 열(720px)의 자리. 'start'(기본)는 헤더 제목과 같은 왼쪽 축에서 시작, 'center' 는 본문 가운데(2026-10-16 이슈 47 전의 모양 — 제목은 왼쪽, 폼은 가운데라 축이 어긋난다고 원문 심판 10회 감점). 2026-10-26 22차부터 헤더 내용과 본문이 같은 가운데 폭 컨테이너(패널 있으면 1152, 없으면 720) 안에 들어가므로 이 값의 효과는 거의 없다(호환용). `aside` 가 있으면 폼 열은 왼쪽 */\n align?: 'start' | 'center'\n /** 폼 오른쪽의 검토·요약 패널(입력값 요약, 비용 미리보기 등). 넓은 화면에서 폼 열 옆에 세로선으로 구분돼 화면을 따라 내려오고(sticky), 좁으면 폼 아래로. **취소·제출 버튼은 이 패널 바닥**에 놓인다(폼 끝에는 없음 — 2026-10-20 16차 D1, 긴 폼 옆에서 패널 아래가 비고 제출이 멀다는 두 심판 지적). FormPage 를 flex 로 감싸 옆에 패널을 두면 헤더가 화면 폭을 다 못 써 구분선이 끊긴다 — 반드시 이 슬롯으로 */\n aside?: ReactNode\n /** 검토 패널 제목(h2). 기본 '검토' */\n asideTitle?: ReactNode\n }\n\n/**\n * 독립 화면 폼 패턴. PageHeader(제목·설명) → FormSection 들(사이에 연한 구분선, 왼쪽 축 720px) → 오류 → 취소/저장(FormFooter; `aside` 가 있으면 패널 바닥). **헤더 내용과 본문은 같은 가운데 폭 컨테이너**에 둔다 — 검토 패널이 있으면 `--max-w-page-content`(1152 = 720 + 24 + 408), 없으면 폼 폭 720. 띠(헤더 배경·아래 선)는 전폭 그대로(2026-10-26 22차: 전폭 왼쪽 정렬에서는 긴 폼 옆 짧은 패널 아래가 \"unused space\" 로 감점됐고, 내용을 가운데 폭에 가둔 변형만 원문 심판 layout 3.5 → 4.0 — 기준선 폼들과 같은 구조). 검토·요약 패널(`aside`)은 **항상** 폼 옆에 둔다 — 입력값 요약·예상 비용처럼 제출 전에 확인할 것(2026-10-17 8차 C2, 예제 form-page.tsx).\n *\n * 언제 쓰나: 스크롤이 생기는 긴 폼 — 섹션 3개 이상, 한 화면을 넘기거나, 여러 단계. 전용 URL 을 가진 생성·수정 화면의 기본. 검증 스키마·기본값·payload 는 소비 앱의 `useForm` 이 소유한다.\n * 언제 안 쓰나: 목록 맥락을 유지해야 하고 Drawer 한 화면 안에 들어가는 폼(섹션 1~2개)은 FormDrawer, 필드 1~4개의 짧은 입력은 FormDialog. 긴 폼을 Drawer·Dialog 에 넣지 않는다.\n * 대신/함께: 머리는 목록·상세·대시보드와 같은 PageHeader(h1)라 FormSection 제목은 h2 가 된다. 바탕은 흰색이 기본이고 `canvas='subtle'` 은 측정용 옵트인(회색 캔버스 + 카드)이다. 저장 버튼은 기본(`submitMode='always'`)으로 항상 눌리고 누르면 검증해 틀린 항목마다 FormMessage 를 보인다 — 유효해질 때까지 잠그려면 `submitMode='whenValid'`. `onSubmit` 이 Promise 면 끝날 때까지 loading — 다만 제출 중 상태는 앱이 `submitting` 으로도 **코드에 드러내라**(접근성·심사 모두 코드에서 보여야 한다, 2026-10-16). 숫자 입력의 단위는 `TextField elemAfterInput='개'`. 선택지 목록을 못 불러왔으면 폼을 지우지 말고 그 필드 자리에 ErrorState 를 둔다.\n */\nexport function FormPage<TValues extends FieldValues>({\n title,\n description,\n breadcrumb,\n titleRight,\n form,\n onSubmit,\n onCancel,\n submitLabel,\n cancelLabel,\n submitting,\n submitMode,\n error,\n requiredLegend,\n canvas = 'plain',\n align = 'start',\n aside,\n asideTitle = '검토',\n children,\n}: FormPageProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const formId = useId()\n // 헤더 내용·본문을 같은 가운데 폭에 — 검토 패널이 있으면 폼 + 패널 폭(1152), 없으면 폼 폭(720). 띠는 전폭 (2026-10-26 22차)\n const contentMaxWidth = aside ? 'var(--max-w-page-content)' : 'var(--max-w-form-control)'\n // 취소·제출 묶음. aside 가 없으면 폼 끝(FormFooter), 있으면 검토 패널 바닥 — 패널 안 버튼은 form 밖이라 formId 로 연결한다\n const actions = (\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={onCancel}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n formId={formId}\n />\n )\n\n const body = (\n <>\n {/* 안내는 form 밖에 둔다 — form 의 첫 자식이 FormSection 이어야 첫 섹션 위에 구분선이 안 생긴다 */}\n {requiredLegend ? (\n <p className='text-small-regular text-sys-text-subtle'>\n <span aria-hidden className='text-danger30'>\n *\n </span>{' '}\n 표시는 필수 입력 항목입니다\n </p>\n ) : null}\n <Form {...form}>\n <form id={formId} noValidate onSubmit={handleSubmit}>\n <FormSectionLevelContext.Provider value={2}>{children}</FormSectionLevelContext.Provider>\n <FormError error={error} className='mt-[24px]' />\n {/* 검토 패널(aside)이 있으면 버튼은 패널 바닥에(아래) — 긴 폼 옆에서 패널이 짧게 끝나고 제출이 멀어 \"unused space beneath the review panel\" 감점 (2026-10-20 16차 D1) */}\n {aside ? null : (\n <>\n {/* FormFooter 는 세로 flex 라 버튼 두 개를 한 줄로 두려면 row 로 한 번 감싼다 (DrawerFooter 와 같은 gap) */}\n {/* 폼 열 폭의 선 위에 버튼 — 긴 폼 끝에 버튼만 떠 있지 않게 (2026-10-17 이슈 47 4차) */}\n <FormFooter\n align='end'\n className='mt-[24px] border-t border-sys-divider-subtle pt-[16px]'\n >\n <div className='flex gap-[8px]'>{actions}</div>\n </FormFooter>\n </>\n )}\n </form>\n </Form>\n </>\n )\n\n // 폼은 FormControl 기본 폭(720px). 기본은 헤더 제목과 같은 왼쪽 축(align='start'), 'center' 면 가운데 — 푸터 버튼은 어느 쪽이든 입력 칸 오른쪽 끝에 정렬된다\n const column =\n canvas === 'subtle' ? (\n // 폼 전체를 흰 Card 하나로(섹션 사이 구분선은 그대로). 위 8px 는 첫 FormSection 의 pt-[16px] 과 더해 24\n <Card\n className={cn(\n 'w-full max-w-[var(--max-w-form-control)] px-[24px] pb-[24px] pt-[8px]',\n align === 'center' && !aside && 'mx-auto',\n )}\n >\n {body}\n </Card>\n ) : (\n <div\n className={cn(\n 'w-full max-w-[var(--max-w-form-control)]',\n align === 'center' && !aside && 'mx-auto',\n )}\n >\n {body}\n </div>\n )\n\n return (\n // subtle 의 캔버스 회색은 기존 토큰(grayscale20, 진단 18-A 의 f7 변형)을 쓴다 — 채택되면 전용 sys 토큰으로 올린다\n <div className={cn('w-full', canvas === 'subtle' && 'bg-sys-background-hover')}>\n <PageHeader\n title={title}\n description={description}\n breadcrumb={breadcrumb}\n titleRight={titleRight}\n contentMaxWidth={contentMaxWidth}\n />\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다). 안쪽 div 가 헤더 내용과 같은 가운데 폭 컨테이너 */}\n {/* 헤더→본문 32(묶음 사이) — 2026-10-17 8차 D2 */}\n <section aria-label={title || '본문'} className='px-[24px] py-[32px]'>\n <div className='mx-auto w-full' style={{ maxWidth: contentMaxWidth }}>\n {aside ? (\n // 폼 열(최대 720px) + 검토 패널. 넓은 화면: 옆에 세로선, sticky. 좁은 화면: 폼 아래에 가로선으로 시작.\n // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe) 이름 있는 section (DetailPage aside 와 같은 꼴)\n <div className='grid grid-cols-1 gap-[24px] xl:grid-cols-[minmax(0,var(--max-w-form-control))_minmax(280px,1fr)] xl:items-start'>\n {column}\n {/* 테두리 상자(Panel) — 넓은 화면에서 sticky 로 폼을 따라온다. 바닥에 취소·제출(HTML form 속성으로 위 <form> 에 연결) */}\n <Panel title={asideTitle} className='xl:sticky xl:top-[24px]'>\n {aside}\n <div\n data-slot='form-page-aside-actions'\n className='mt-[16px] flex justify-end gap-[8px] border-t border-sys-divider-subtle pt-[16px]'\n >\n {actions}\n </div>\n </Panel>\n </div>\n ) : (\n column\n )}\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport type { ComponentProps } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerTitle,\n} from '../components/drawer'\nimport { Form } from '../components/form'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormDrawerProps<TValues extends FieldValues> = FormShellProps<TValues> & {\n /** 열림 상태 (controlled) */\n open: boolean\n /** 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 */\n onOpenChange: (open: boolean) => void\n /** 패널이 나오는 방향. 기본 right */\n side?: ComponentProps<typeof DrawerContent>['side']\n /** 패널 너비. 기본 large */\n size?: ComponentProps<typeof DrawerContent>['size']\n}\n\n/**\n * 사이드 패널 폼 패턴. 목록 화면 위에 오른쪽 Drawer 로 생성·수정 폼을 띄운다. 제목 → FormSection 들(스크롤) → 오류 → 취소/저장.\n *\n * 언제 쓰나: 목록 맥락을 유지해야 하고 **Drawer 한 화면 안에 들어가는** 폼(섹션 1~2개 — 예: 태그·설명·담당자 수정, 짧은 생성).\n * 언제 안 쓰나: 필드 1~4개의 짧은 입력은 FormDialog. 스크롤이 생기거나 섹션이 셋 이상이거나 여러 단계인 긴 폼(실행환경·파이프라인·모델 생성)은 FormPage — 긴 폼을 Drawer 에 넣지 않는다(2026-10-13, Carbon·Shopify 관례).\n * 대신/함께: 제출 중에는 Esc·오버레이·취소·닫기 버튼으로 닫히지 않는다. 닫을 때 폼을 reset 할지는 소비 앱이 `onOpenChange` 에서 정한다(초안을 보존하려면 reset 하지 않는다).\n */\nexport function FormDrawer<TValues extends FieldValues>({\n open,\n onOpenChange,\n side,\n size,\n title,\n description,\n form,\n onSubmit,\n submitLabel,\n cancelLabel,\n submitting,\n error,\n submitMode,\n children,\n}: FormDrawerProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const guardedOpenChange = (next: boolean) => {\n if (!next && busy) return\n onOpenChange(next)\n }\n\n return (\n <Drawer open={open} onOpenChange={guardedOpenChange}>\n <DrawerContent\n side={side}\n size={size}\n // 설명이 없으면 Radix 가 aria-describedby 대상을 찾지 못해 경고하므로 속성을 뺀다\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n <DrawerHeader>\n <DrawerTitle>{title}</DrawerTitle>\n {description ? <DrawerDescription>{description}</DrawerDescription> : null}\n </DrawerHeader>\n <Form {...form}>\n <form noValidate onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>\n <DrawerBody className='px-[24px]'>\n {children}\n <FormError error={error} className='mt-[24px]' />\n </DrawerBody>\n <DrawerFooter>\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={() => guardedOpenChange(false)}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n />\n </DrawerFooter>\n </form>\n </Form>\n </DrawerContent>\n </Drawer>\n )\n}\n","'use client'\n\nimport type { ComponentProps } from 'react'\nimport type { FieldValues } from 'react-hook-form'\n\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../components/dialog'\nimport { Form } from '../components/form'\nimport { FormActions, FormError, type FormShellProps, useFormShell } from './formShell'\n\nexport type FormDialogProps<TValues extends FieldValues> = FormShellProps<TValues> & {\n /** 열림 상태 (controlled) */\n open: boolean\n /** 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 */\n onOpenChange: (open: boolean) => void\n /** 모달 너비. 기본 small */\n width?: ComponentProps<typeof DialogContent>['width']\n}\n\n/**\n * 모달 폼 패턴. 제목 → 필드 → 오류 → 취소/저장.\n *\n * 언제 쓰나: 이름 변경, 태그 추가처럼 필드 1~4개로 끝나는 짧은 입력.\n * 언제 안 쓰나: 목록 맥락이 필요한 한 화면짜리 폼은 FormDrawer, 스크롤이 생기는 긴 폼은 FormPage, 되돌릴 수 없는 확인만 받을 때는 AlertDialog.\n * 대신/함께: 제출 중에는 닫기 버튼이 사라지고 Esc·오버레이·취소로도 닫히지 않는다.\n */\nexport function FormDialog<TValues extends FieldValues>({\n open,\n onOpenChange,\n width,\n title,\n description,\n form,\n onSubmit,\n submitLabel,\n cancelLabel,\n submitting,\n error,\n submitMode,\n children,\n}: FormDialogProps<TValues>) {\n const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode })\n const guardedOpenChange = (next: boolean) => {\n if (!next && busy) return\n onOpenChange(next)\n }\n\n return (\n <Dialog open={open} onOpenChange={guardedOpenChange}>\n <DialogContent\n width={width}\n hideCloseButton={busy}\n // 설명이 없으면 Radix 가 aria-describedby 대상을 찾지 못해 경고하므로 속성을 뺀다\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n {description ? <DialogDescription>{description}</DialogDescription> : null}\n </DialogHeader>\n <Form {...form}>\n <form noValidate onSubmit={handleSubmit} className='flex flex-col gap-[16px]'>\n {children}\n <FormError error={error} />\n <DialogFooter>\n <FormActions\n busy={busy}\n canSubmit={canSubmit}\n onCancel={() => guardedOpenChange(false)}\n submitLabel={submitLabel}\n cancelLabel={cancelLabel}\n />\n </DialogFooter>\n </form>\n </Form>\n </DialogContent>\n </Dialog>\n )\n}\n","'use client'\n\nimport { type ReactNode, useId } from 'react'\nimport { TriangleAlert } from 'lucide-react'\n\nimport { Card } from '../components/card'\nimport { PageHeader, type PageHeaderProps } from '../components/pageHeader'\nimport { Skeleton } from '../components/skeleton'\nimport { cn } from '../utils/cn'\nimport { EmptyState, ErrorState, LoadingState } from './states'\n\nexport type DashboardStat = {\n /** 타일 라벨 (예: 실행 중인 학습) */\n label: ReactNode\n /** 큰 숫자 */\n value: ReactNode\n /** 숫자 아래 보조 설명 (예: 어제보다 +2) */\n caption?: ReactNode\n /** 주면 타일 전체가 이 주소로 가는 링크가 된다 */\n href?: string\n /** 사용률 같은 비율(0~1). 주면 숫자 아래에 진행 막대를 그린다 (예: 172/192 코어 → 0.9) */\n ratio?: number\n /** 값의 색. warning·danger 면 경고 아이콘이 붙고 숫자·막대가 그 색이 된다 (예: 사용률 80% 초과). 기본 default */\n tone?: 'default' | 'warning' | 'danger'\n}\n\nexport type DashboardLayoutProps = Pick<\n PageHeaderProps,\n 'title' | 'description' | 'titleRight' | 'breadcrumb'\n> & {\n /** 주요 행동 버튼. 제목 줄 오른쪽 (ListPage·DetailPage 와 같은 자리, 2026-10-19 14차) */\n actions?: ReactNode\n /** 맨 위 요약 타일. 2열, 넓은 화면(xl)에서 4열, 간격 12px */\n stats?: DashboardStat[]\n /** true 면 타일 숫자 자리에 Skeleton. 패널 로딩은 각 DashboardPanel 의 `loading` */\n loading?: boolean\n /** 주면 타일·섹션 대신 오류 상자(ErrorState). `onRetry` 가 있으면 \"다시 시도\" 버튼 */\n error?: ReactNode\n /** error 상태의 \"다시 시도\" 클릭 */\n onRetry?: () => void\n /** 보여 줄 데이터가 전혀 없을 때 타일·섹션 대신 없음 상자. true 면 기본 문구 \"데이터가 없습니다.\" 패널 하나만 비었으면 DashboardPanel 의 `empty` */\n empty?: ReactNode\n /** DashboardSection 들. 세로로 32px 간격 */\n children?: ReactNode\n}\n\n// 글자는 40단계(흰 배경 대비 warning40 5.4:1·danger30 5.9:1). 30단계 warning 은 채움용이라 글자에 쓰면 1.75:1 로 axe 에 걸린다\nconst TONE_TEXT = {\n default: 'text-sys-text-main',\n warning: 'text-warning40',\n danger: 'text-danger30',\n}\nconst TONE_BAR = { default: 'bg-primary30', warning: 'bg-warning30', danger: 'bg-danger30' }\n\nfunction StatTile({\n label,\n value,\n caption,\n href,\n ratio,\n tone = 'default',\n loading,\n}: DashboardStat & { loading?: boolean }) {\n const labelId = useId()\n const percent = ratio == null ? null : Math.round(Math.min(Math.max(ratio, 0), 1) * 100)\n return (\n <Card className='relative gap-[4px] p-[16px]'>\n <span id={labelId} className='text-h6 text-sys-text-subtle'>\n {href ? (\n // 타일 전체를 클릭 영역으로: 링크의 ::after 를 Card 크기로 늘린다\n <a\n href={href}\n className='after:absolute after:inset-0 after:rounded-medium focus-visible:outline-[2px] focus-visible:outline-sys-focus-ring-main'\n >\n {label}\n </a>\n ) : (\n label\n )}\n </span>\n {loading ? (\n <Skeleton role='status' aria-label='불러오는 중' className='h-[32px] w-[80px]' />\n ) : (\n <span className={cn('flex items-center gap-[6px] text-h2', TONE_TEXT[tone])}>\n {tone !== 'default' ? <TriangleAlert aria-hidden size={20} className='shrink-0' /> : null}\n {value}\n </span>\n )}\n {percent != null && !loading ? (\n <div\n role='progressbar'\n aria-labelledby={labelId}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n className='mt-[4px] h-[4px] w-full overflow-hidden rounded-full bg-secondary-o10'\n >\n <div\n className={cn('h-full rounded-full', TONE_BAR[tone])}\n style={{ width: `${percent}%` }}\n />\n </div>\n ) : null}\n {caption ? <span className='text-small-regular text-sys-text-subtler'>{caption}</span> : null}\n </Card>\n )\n}\n\n/**\n * 대시보드 화면 패턴. PageHeader → 요약 타일(`stats`) → DashboardSection(제목 + 패널 그리드 또는 표) 을 세로로 배치한다.\n *\n * 언제 쓰나: 프로젝트 홈·리소스 모니터링처럼 \"지금 상태를 숫자·차트로 한눈에\" 보여 주고 각 섹션에서 세부 화면으로 들어가는 화면. 값·시계열·갱신 주기는 소비 앱이 가진다.\n * 언제 안 쓰나: 한 리소스의 목록(→ ListPage)·상세(→ DetailPage), 설정 입력(→ FormPage). 섹션 하나뿐이면 PageHeader + Card 로 충분하다.\n * 대신/함께: 패널 내용(차트·표)은 `DashboardPanel` 의 children 으로 넣고 차트 라이브러리는 소비 앱이 고른다. 폴링·갱신 시각 표시는 `titleRight` 슬롯에 `PageHeaderRefreshButton` 으로. 표 섹션은 DashboardSection 안에 Table 또는 AgGridTable 을 그대로 넣는다. 사용률 타일은 `ratio`(진행 막대)·`tone`(80% 초과면 warning) 을 준다. 전체를 못 불러왔으면 `error`(+`onRetry`), 데이터가 전혀 없으면 `empty` — 빨간 Alert·`<div>` 로 직접 만들지 않는다.\n */\nexport function DashboardLayout({\n actions,\n stats,\n loading,\n error,\n onRetry,\n empty,\n children,\n ...header\n}: DashboardLayoutProps) {\n let body: ReactNode\n if (error) body = <ErrorState className='py-[32px]' description={error} onRetry={onRetry} />\n else if (empty)\n body = (\n <EmptyState className='py-[32px]'>{empty === true ? '데이터가 없습니다.' : empty}</EmptyState>\n )\n else\n body = (\n <>\n {stats?.length ? (\n <div className='grid grid-cols-2 gap-[12px] xl:grid-cols-4'>\n {stats.map((stat, i) => (\n <StatTile key={i} {...stat} loading={loading} />\n ))}\n </div>\n ) : null}\n {children}\n </>\n )\n return (\n <div className='w-full'>\n <PageHeader {...header} actions={actions} />\n {/* 64px 는 PageHeader 의 좌우 거터와 맞춘 값. audit 허용 간격 밖이지만 헤더와 본문 정렬이 우선이다 */}\n {/* 본문은 이름 있는 region landmark (소비 앱의 <main> 안에 중첩돼도 된다) */}\n <section\n aria-label={header.title || '본문'}\n className='flex flex-col gap-[32px] px-[24px] py-[24px]'\n >\n {body}\n </section>\n </div>\n )\n}\n\nexport type DashboardSectionProps = {\n /** 섹션 제목 (h2) */\n title: ReactNode\n /** 제목 오른쪽 슬롯 (예: \"전체 보기\" 버튼) */\n action?: ReactNode\n /** 주면 children(DashboardPanel 들)을 **하나의 Card** 안에 1px 구분선으로 나눈 그리드로 그린다. 2 면 넓은 화면(xl)에서 2열. 없으면 children 을 그대로(표 등) */\n columns?: 1 | 2\n /** columns 가 있으면 DashboardPanel 들, 없으면 표 등 아무 내용 */\n children?: ReactNode\n}\n\n/**\n * DashboardLayout 의 한 섹션. 제목(h2)·오른쪽 행동 → 패널 그리드(Card 하나 안에 구분선) 또는 표.\n *\n * 언제 쓰나: 대시보드에서 주제 하나(리소스 사용량, 최근 학습 …)를 묶을 때. 차트 패널 여러 개면 `columns={2}` — 패널마다 카드가 아니라 **큰 카드 하나를 구분선으로 나눈다**(dl-ui-v3 리소스 모니터링 관례).\n * 언제 안 쓰나: 대시보드 밖의 섹션 제목(→ FormSection·CardHeader).\n * 대신/함께: 표 섹션은 `columns` 없이 children 에 Table 을 바로 넣고 `action` 에 \"전체 보기\" 링크 버튼을 둔다.\n */\nexport function DashboardSection({ title, action, columns, children }: DashboardSectionProps) {\n const id = useId()\n return (\n <section aria-labelledby={id} className='flex flex-col gap-[16px]'>\n <div className='flex items-center justify-between gap-[12px]'>\n <h2 id={id} className='text-h4 text-sys-text-main'>\n {title}\n </h2>\n {action}\n </div>\n {columns ? (\n // 1px 간격 + 구분선 색 배경 = 칸 사이 가로·세로 구분선. 칸(DashboardPanel)은 자기 배경을 가진다\n // 2열에서 칸 수가 홀수면 마지막 칸이 두 열을 차지해 빈 칸이 생기지 않게 한다\n <Card\n className={cn(\n 'grid grid-cols-1 gap-px overflow-hidden bg-sys-divider-subtler',\n columns === 2 && 'xl:grid-cols-2 xl:[&>:last-child:nth-child(odd)]:col-span-2',\n )}\n >\n {children}\n </Card>\n ) : (\n children\n )}\n </section>\n )\n}\n\nexport type DashboardPanelProps = {\n /** 패널 제목 (h3) */\n title: ReactNode\n /** 제목 옆 현재값 (큰 글자) */\n value?: ReactNode\n /** 현재값 단위 (예: %, GB) */\n unit?: ReactNode\n /** 현재값 옆 보조 수치 (예: 할당 8 / 사용 3) */\n stats?: { label: ReactNode; value: ReactNode }[]\n /** true 면 내용 자리에 Skeleton. `empty` 보다 우선 */\n loading?: boolean\n /** 주면 내용 대신 표시. true 면 기본 문구 \"데이터가 없습니다.\" */\n empty?: ReactNode\n /** 내용 (차트·표). 차트 라이브러리는 소비 앱이 고른다 */\n children?: ReactNode\n}\n\n/**\n * DashboardSection(`columns`) 의 칸 하나. 제목·현재값·단위·보조 수치 머리 → 내용(차트·표). 자체 테두리는 없고 섹션 Card 가 칸 사이를 구분선으로 나눈다.\n *\n * 언제 쓰나: 시계열 차트나 작은 표 하나에 현재값을 붙여 보여 줄 때. 항상 `columns` 를 준 DashboardSection 안에 둔다.\n * 언제 안 쓰나: 숫자 하나만 보여 줄 때(→ DashboardLayout 의 `stats` 타일), 상세 화면의 정보 묶음(→ 제목 + Separator + MetaRow).\n * 대신/함께: 데이터가 없으면 `empty`, 불러오는 중이면 `loading` 을 준다. 내용 높이는 children 이 정한다.\n */\nexport function DashboardPanel({\n title,\n value,\n unit,\n stats,\n loading,\n empty,\n children,\n}: DashboardPanelProps) {\n let body = children\n if (loading) body = <LoadingState lines={2} />\n else if (empty)\n body = (\n <EmptyState className='py-[32px]'>{empty === true ? '데이터가 없습니다.' : empty}</EmptyState>\n )\n\n return (\n <div className='flex flex-col gap-[16px] bg-sys-background p-[24px]'>\n <div className='flex flex-wrap items-end gap-[12px]'>\n <h3 className='text-h6 text-sys-text-subtle'>{title}</h3>\n {value !== undefined && value !== null ? (\n <span className='flex items-end gap-[2px]'>\n <span className='text-h3 text-sys-text-main'>{value}</span>\n {unit ? <span className='text-base-regular text-sys-text-subtle'>{unit}</span> : null}\n </span>\n ) : null}\n {stats?.map((stat, i) => (\n <span\n key={i}\n className='flex items-end gap-[4px] border-l border-sys-divider-subtler pl-[12px]'\n >\n <span className='text-base-regular text-sys-text-subtle'>{stat.label}</span>\n <span className='text-h5 text-sys-text-main'>{stat.value}</span>\n </span>\n ))}\n </div>\n {body}\n </div>\n )\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3q-ui/design-system",
3
- "version": "1.56.1",
3
+ "version": "1.57.0",
4
4
  "description": "T3Q Design System - React Components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",