122 lines
11 KiB
Markdown
122 lines
11 KiB
Markdown
# Data Table (Pattern)
|
|
|
|
> 2026-10-10 추가 수정 요청 기준: 기본 검색 가운데 / 추가 조건 검색 왼쪽, 조건→검색어→조회→초기화·8px. Header는 Card 밖, 검색·목록 도구·결과는 한 Card. 페이지 크기 label useId로 인스턴스 분리. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
|
|
|
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다. 이 문서는 Data Table Pattern 고유의 API·상태·결정만 기록한다.
|
|
|
|
## 1. 상태
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
|
| 기준일 | 2026-10-03 |
|
|
| 기반 | `@tanstack/react-table` 9.2.4 + HM Table primitive |
|
|
| 대표 Story | `Patterns/Data/Data Table` → `Data Table` (`With Filter Bar`는 숨김 verification) |
|
|
| 보류 | §12 |
|
|
|
|
## 2. 목적 / 사용 범위
|
|
|
|
업무 목록 화면의 표. 기존 `Table`(presentation primitive)에 정렬·행 선택·페이지·열 표시·로딩/빈 상태·행 액션을 조합한다. `Table`에는 어떤 로직도 넣지 않았다. 서버 데이터 호출·서버 사이드 페이지/정렬·가상 스크롤은 범위 밖이다.
|
|
|
|
## 3. 구성 요소
|
|
|
|
`DataTable`, `useDataTable`, `DataTableColumnHeader`, `DataTableViewOptions`, `DataTablePagination`, `dataTableSelectColumn`. (`DataTable`이 `DataTablePagination`을 기본으로 포함한다.)
|
|
|
|
## 4. 구조
|
|
|
|
```
|
|
Table primitive (정적 표시, 변경 없음)
|
|
↑ 조합
|
|
DataTable pattern ── useDataTable (TanStack useTable + 기능 등록)
|
|
├ DataTableColumnHeader 정렬 버튼
|
|
├ dataTableSelectColumn HM Checkbox 선택 열
|
|
├ DataTableViewOptions HM Dropdown Menu + Button
|
|
└ DataTablePagination HM Pagination + Select(페이지 크기) + 선택 수 요약
|
|
```
|
|
|
|
## 5. Public API
|
|
|
|
```tsx
|
|
const table = useDataTable({ columns, data, getRowId, initialState }) // TanStack TableOptions에서 features만 뺀 것
|
|
<DataTableViewOptions table={table} labels={{ name: "이름" }} />
|
|
<DataTable table={table} loading emptyMessage="..." pagination pageSizeOptions={[10, 20, 50]} />
|
|
```
|
|
|
|
- `columns`: `DataTableColumnDef<TData>[]`(TanStack `ColumnDef`, 등록된 기능 포함). 정렬 header는 `({ column }) => <DataTableColumnHeader column={column} title="이름" />`.
|
|
- `DataTable`: `table`, `loading`, `emptyMessage`, `pagination`, `pageSizeOptions`, `className`. 그 외 TanStack 옵션은 `useDataTable`에 그대로 전달한다(복제 prop 없음).
|
|
- `data`와 `columns`는 참조가 안정적이어야 한다(모듈 상수, `useMemo`, state).
|
|
- 타입: `DataTableColumnDef`, `DataTableInstance`, `DataTableOptions`, `DataTableProps`.
|
|
|
|
## 6. 상태 관리
|
|
|
|
TanStack Table v9의 state model 그대로. v9는 기능을 명시 등록하는 방식(`tableFeatures`)이라 v8 예제(`useReactTable`, `getCoreRowModel`)와 다르다. 등록한 기능: column filtering, global filtering, row sorting, row selection, row pagination, column visibility와 각 row model(`filteredRowModel`, `sortedRowModel`, `paginatedRowModel`), `includesString`·`equalsString` filter, `alphanumeric`·`basic`·`datetime` sort.
|
|
|
|
- 상태는 TanStack 내부가 소유한다(필요하면 `useDataTable`에 `state` / `atoms`로 제어). 검색은 `table.setGlobalFilter`, 열 필터는 `table.getColumn(id)?.setFilterValue`, 초기화는 `resetGlobalFilter(true)` / `resetColumnFilters(true)`로 한다. 필터 변경 시 TanStack이 page index를 0으로 되돌린다.
|
|
- error는 Data Table이 소유하지 않는다(소비자가 Alert / Toast로 처리).
|
|
|
|
조회 버튼을 사용하는 목록은 [FilterBar의 draft / applied 소비 예제](filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 따른다. 입력 중에는 draft만 바꾸고 조회 시 table 필터 또는 `useMemo`로 만든 `data`를 갱신한다. 초기화는 입력값과 적용값, 사용 중인 table 필터와 페이지를 소비자가 함께 처리한다. 대표 Data Table Story는 표 기능의 운영 Reference이며, `WithFilterBarStory`(`patterns-data-data-table--with-filter-bar-story`)는 **입력 즉시 적용**을 확인하는 숨김 verification이다. 조회 버튼으로 조건을 확정하는 공개 API나 서버 호출 기능은 제공하지 않는다.
|
|
|
|
## 7. 조합 컴포넌트
|
|
|
|
Table, Checkbox(header mixed), Pagination, Select(페이지 크기), Dropdown Menu(열 표시), Button(열 표시 trigger), Skeleton(loading). 행 액션은 소비자가 열 정의에서 Dropdown Menu(또는 Context Menu)로 구성한다. Story는 행 액션 결과를 Sonner `toast`로 보이지만 Pattern은 Sonner에 의존하지 않는다.
|
|
|
|
## 8. Styling / Token
|
|
|
|
Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table-*`)가 소유하고 Pattern은 다시 정의하지 않는다. 컬러 역할(2026-10-06): 선택 행 면 = `--hm-sem-accent` 20%(hover·selected+hover와 같은 Soft Surface, Table에만 적용하는 20% 정책; Select 강조는 기존 45% 유지), 선택 Checkbox = `--hm-sem-state-active`(Light 2). 면은 hover와 같으므로 선택 여부는 Checkbox와 선택 수 요약으로 구분한다. Pattern 전용: `packages/ui/src/theme/data-table.css`의 `--hm-data-table-*`(footer gap·padding, summary 색·크기, 정렬 버튼 간격·아이콘 크기·활성 색, empty padding·색, skeleton 높이). 신규 Primitive·Semantic 없음.
|
|
|
|
- Loading: header 유지, body는 Skeleton 행(최대 5행), `aria-busy`.
|
|
- Sort Direction Accent: `--hm-data-table-sort-active-color`는 현재 `--hm-sem-palette-green-light-3`(Green Light 3 palette)를 참조한다. 별도 State Semantic으로 승격하지 않았으며 Primary/Active/Accent 역할에 자동 연결되지 않는다. Token 이름의 `active`는 Control Active Semantic 계약을 뜻하지 않는다.
|
|
- Empty: 한 행을 열 수만큼 `colSpan`, 문구는 `emptyMessage`(원본 0건과 필터 결과 0건 동일 UI).
|
|
|
|
## 9. Storybook
|
|
|
|
- `Patterns/Data/Data Table` → `Data Table`(대표: 선택·정렬·열 표시·행 액션·페이지 크기·페이지), `With Filter Bar`(숨김 Filter Bar 연동 검증). verification: `WithFilterBar`, `Loading`, `Empty`.
|
|
- 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음).
|
|
|
|
## 10. Accessibility
|
|
|
|
semantic `<table>` 유지. 정렬 열 `<th aria-sort>`(ascending / descending), 정렬 버튼 `aria-label`("이름 정렬, 오름차순"), 버튼이라 Enter / Space 동작. 선택 Checkbox는 `aria-label`("현재 페이지 전체 선택", "행 선택"), header는 `aria-checked="mixed"`. 선택 수 요약은 `role="status"`. Pagination은 기존 `nav` + `aria-current` / `aria-disabled`.
|
|
|
|
## 11. Responsive
|
|
|
|
Table primitive의 container가 가로 스크롤한다. Pattern은 `[data-slot="data-table"] { min-width: 0 }`로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
|
|
|
|
### 11.1 식별자 줄바꿈과 열 폭
|
|
|
|
Table의 `overflow-wrap: anywhere`는 긴 일반 콘텐츠를 보호하지만 좁은 ID 열에서 `JOB-001`을 여러 줄로 분절할 수 있다. ID·날짜 등 의미상 한 줄이어야 하는 값은 해당 cell의 콘텐츠만 보호한다. 제목 등 장문은 기존 줄바꿈을 유지한다.
|
|
|
|
```tsx
|
|
const columns: DataTableColumnDef<Job>[] = [
|
|
{ accessorKey: 'id', header: 'ID', enableSorting: false,
|
|
cell: ({ row }) => <span className="whitespace-nowrap">{row.original.id}</span> },
|
|
// 제목·그 외 열은 요구사항에 맞는 기존 cell composition을 유지한다.
|
|
]
|
|
```
|
|
|
|
현재 DataTable은 `ColumnDef.size/minSize`를 DOM의 th/td width 또는 min-width로 연결하지 않는다. 해당 값만 지정하고 폭이 확보됐다고 보고하지 않는다. `nowrap` 콘텐츠의 실제 폭·cell padding·표 내부 스크롤을 측정한다. 긴 식별자의 별도 최소 폭이 필요하면 소비 콘텐츠의 min-width composition과 실제 DOM 폭을 확인한다. Typography·Color·공통 Table CSS는 변경하지 않는다. Ellipsis를 쓰는 요구사항이라면 키보드/터치로 전체 값을 확인할 경로를 함께 제공하며 ID를 잘라서 식별 가능하다고 판단하지 않는다.
|
|
|
|
네 폭에서 ID 전체 텍스트·Range 줄 수·cell 내용 폭·table/container scrollWidth/clientWidth와 문서 가로 넘침을 기록한다. 긴 제목 줄바꿈, 표 내부 가로 스크롤과 마지막 열 접근을 함께 확인한다. `min-width: 0`만으로 ID 가독성을 보장하지 않는다.
|
|
|
|
## 12. 보류 / 비범위
|
|
|
|
- column resizing·pinning, grouping, tree, virtualization, infinite scroll, drag reorder, 서버 사이드 페이지/정렬, 편집 가능한 표.
|
|
- 선택 action toolbar(선택 수 요약만 제공), 행 클릭 이동, 다중 정렬 UI.
|
|
- 정렬 header와 열 표시 메뉴의 열 이름: header가 컴포넌트일 수 있어 메뉴 문구는 `labels`로 받는다.
|
|
- 확인하지 못한 것: 스크린 리더 낭독, 키보드 Tab 순서 전체, 페이지 크기 Select 조작, 행 선택 + 페이지 이동 후 선택 유지.
|
|
- `With Filter Bar` verification Story의 DatePicker / Combobox는 `controlled={true}`를 사용한다. 초기화는 부모의 검색·상태·날짜·부서 state와 `table.resetGlobalFilter(true)` / `table.resetColumnFilters(true)`를 함께 비운다. 날짜·부서의 undefined 초기화는 선택값을 비우며, 해당 목적의 `key` remount를 제거했다. 외부 값 변경은 자식 콜백을 호출하지 않으므로 테이블 필터는 소비자가 명시적으로 초기화해야 한다. 팝업 닫기·검색어·달력 월 초기화는 별도 계약이며, 이 Story는 일반 운영 Reference로 승격하지 않는다.
|
|
- **Legacy 호환 범위**: `controlled`를 생략한 DatePicker / Combobox는 기존 `value !== undefined` 판정을 유지한다. 외부 undefined 초기화 시 내부 값이 다시 표시될 수 있으므로, 과거 `key` remount 우회 설명은 이 Legacy 호출에 한정한다. 신규 연동은 명시적 Controlled 모드를 사용한다.
|
|
|
|
## 13. 관련 파일 / 검증
|
|
|
|
- 2026-10-08: `With Filter Bar` verification Story 소스를 격리 Headless Edge 환경에서 실행했다. 부서(검색/global filter) + 상태/등록일(column filter)로 1행 조회 후 초기화하면 전체 47개와 기존 표 표시를 복원하고 날짜·부서 Trigger DOM을 유지했다. 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
|
|
|
```
|
|
packages/ui/src/patterns/data-table/{data-table.tsx,index.ts}
|
|
packages/ui/src/theme/data-table.css
|
|
packages/ui/src/index.ts, theme.css, tailwind.css(@source patterns)
|
|
src/patterns/data-table.stories.tsx, sample-users.ts
|
|
```
|
|
|
|
- `npm run typecheck`, `npm run build` 통과(2026-10-03).
|
|
- Browser 확인: 정렬 none → asc → desc → none과 `aria-sort`, header mixed / 전체 / 해제와 선택 수 요약·`data-state="selected"`, 페이지 이동, 열 숨김(메뉴 유지), 행 액션이 행 데이터를 받음(toast), Loading(skeleton 30개, `aria-busy`), Empty(`colSpan`), 390px overflow 없음. 연동은 `filter-bar.md` §13.
|