본문으로 건너뛰기

표 운영

표(Table) 위에 얹는 거르기 · 보기 · 일괄 작업 · 열 다루기입니다. 위에서부터 저장한 보기 → 툴바 → 필터 칩 → 표 순서로 둡니다.

표 엔진을 들이지 않습니다. 거르기 · 정렬 · 열 · 너비 · 고르기 상태는 앱이 가지고, 이 묶음은 그 상태를 보이고 바꾸는 부품입니다. 행을 고르면 툴바가 같은 자리 · 같은 높이(44)에서 일괄 바로 바뀌어 표가 흔들리지 않습니다. 좁은 화면(40rem 미만)에서는 툴바가 검색 + 필터 하나로 줄고, 칩은 가로로 스크롤되며, 일괄 바는 화면 바닥에 붙습니다. radix-ui 가 필요합니다.

import
import { TableToolbar, BulkBar, FilterChips, SavedViews, ColumnMenu, SortHeader, ColumnResizer, EditableCell } from "@uyoolabs/ui/table";
쓰는 곳
작업 표 · 결제 내역 · 멤버 관리 · 감사 로그처럼 거르고 정렬하고 여러 행을 한 번에 다루는 목록. 1,000행을 넘으면 VirtualTable(./virtual)과 함께 씁니다.
쓰지 않는 경우
행이 열 개 남짓이고 거를 일이 없는 표(설정 · 요금제 비교)에는 두지 않습니다. 저장한 보기를 탭(role=tablist)으로 만들지 않습니다 — 보기는 주소라 링크입니다. 되돌릴 수 없는 일괄 작업(지우기)을 일괄 바에서 바로 실행하지 않고 AlertDialog 를 거칩니다. 빨강은 셀 오류와 되돌릴 수 없는 일에만 씁니다.
상태 표시
바뀐 보기는 점 + ‘바뀜’, 셀 오류는 경계 + 아이콘 + 문장, 고른 행은 체크박스가 말하고 틴트가 거듭니다. 색만으로 말하지 않습니다.
알려진 한계
칩 편집기는 지금 기본 목록 · 입력칸입니다(Combobox · DatePicker · Slider 가 들어오면 바꿉니다). 툴바 끝 단추가 넘칠 때 ⋯ 메뉴로 접는 일은 아직 하지 않습니다 — 좁은 화면에서는 끝 단추를 숨기므로 필터 시트 안에 둡니다. 셀 이동이 켜진 표는 APG Data Grid 지만 Table 이 role=table 을 고정해 셀 포커스 · 화살표 이동만 제공합니다.

한 벌

저장한 보기 · 툴바 · 필터 칩 · 정렬 머리 · 열 메뉴 · 너비 · 셀 편집 · 일괄 바

결과 3개
환불 정책 문구 검토PAY-139검토 대기이도현2026-10-23
PG 웹훅 서명 검증PAY-142진행 중한서윤2026-10-24
결제 실패 재시도 큐PAY-137진행 중박지민2026-10-25
키동작
Enter머리: 그 열 하나로 정렬(오름 → 내림 → 풂)
Shift+Enter머리: 정렬을 더한다(순위 숫자)
Alt+↓머리: 열 메뉴를 연다
←·→너비 손잡이: 16px. Shift 와 함께면 64px. 최소 64
Enter·Enter너비 손잡이: 두 번 누르면 내용에 맞춤(두 번 누름도 같다)
Space열 메뉴: 열 보이기 · 숨기기(메뉴는 열어 둔다)
Alt+↑·Alt+↓열 메뉴: 순서 바꾸기(끌기의 대안). 고정 열 앞으로는 가지 않는다
Enter·Space거르기 칩: 값 편집 팝오버를 연다
Backspace·Delete거르기 칩: 그 거르기를 푼다. 포커스는 다음 칩으로
←·→·↑·↓편집 셀: 셀 사이를 옮긴다(Home · End 는 행의 처음 · 끝)
Enter·F2편집 셀: 편집 시작(읽기 전용이면 아무 일 없음)
Enter셀 편집 중: 저장. 조합 중(한 · 일 IME) Enter 는 확정만 한다
Esc셀 편집 중: 취소하고 셀로. 조합 중 Esc 는 조합만 취소한다
Tab·Shift+Tab셀 편집 중: 저장하고 오른쪽 · 왼쪽 편집 셀로

늦게 온 응답 버리기

거르기를 빠르게 바꾸면 앞 조건의 응답이 뒤에 올 수 있습니다. useLatestRequest 가 앞 요청을 끊고 늦은 응답을 버립니다.

화면 없는 훅입니다. 요청마다 begin() 을 부르고, 응답을 쓰기 전에 isLatest() 를 봅니다.

props

소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.

TableToolbar

prop타입기본값설명
searchReactNode—보통 <Input size="sm" type="search"> 240px
filtersReactNode—‘필터 3’ secondary sm(개수 칩)
resultCountnumber—‘결과 9개’. polite 라이브 영역이 입력이 멈추고 500ms 뒤 한 번 읽는다
actionsReactNode—끝 자리 ghost sm(정렬 · 열 · 밀도 · 내보내기). 좁은 화면(40rem 미만)에서는 숨는다 — 필터 시트 안에 둔다
selectionBulkBarProps—count > 0 이면 툴바 대신 일괄 바를 같은 자리에 그린다
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—

이 밖에 <div> props(children 제외)를 그대로 받습니다.

BulkBar

prop타입기본값설명
count필수number—고른 수
pageCountnumber—이 페이지의 행 수. count 와 같으면 ‘전체 모두 고르기’ 안내가 나온다
totalnumber—전체 수(‘작업 240개 모두 고르기’). pageCount 보다 클 때만 안내한다
nounstring—무엇을 골랐는지(‘작업’). 일괄 바 이름 · 모두 고르기 문구에 들어간다
allSelectedbooleanfalse다른 페이지 행까지 모두 고른 상태. 앱이 onSelectAll 뒤에 켠다
onSelectAll() => void—‘전체 t개 모두 고르기’. 없으면 안내 글만 보인다
onClear필수() => void—‘고르기 풀기’
actions필수ReactNode—secondary sm 단추들. 되돌릴 수 없는 일(지우기)은 AlertDialog 를 거친다
returnFocusRefRefObject<HTMLElement | null>—고르기를 푼 뒤 포커스를 돌려줄 곳(마지막으로 고른 행). 없으면 포커스가 body 로 떨어진다
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
classNamestring—

FilterChips

prop타입기본값설명
fields필수FilterField[]—
value필수FilterValue—제어. 값이 없는 키(undefined · 빈 글자 · 빈 배열)는 칩을 그리지 않는다
onChange필수(next: FilterValue) => void—
lockedstring[][]저장한 보기에 묶인 키 → 읽기 전용 칩(× 없음, ‘모두 지우기’ 에도 남는다)
invalidstring[][]값이 사라진 키(탈퇴한 멤버) → 오류 색 칩 + sr 꼬리
maxValuesnumber1값 이름을 몇 개까지 잇는가. 기본 1 — ‘한서윤 외 2개’
openKeystring | null—열어 둘 칩(제어). 표 머리의 ‘이 열로 거르기’ 가 그 칩의 편집기를 연다
onOpenKeyChange(key: string | null) => void—
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
classNamestring—

SavedViews

prop타입기본값설명
views필수SavedView[]—
current필수string—지금 보기의 id
dirtybooleanfalse지금 거르기 · 정렬 · 열이 보기와 다르다 → ‘바뀜’ 점 + 되돌리기 · 저장
onSave() => void—지금 상태로 이 보기를 덮어쓴다
onSaveAs(name: string) => void—지금 상태를 새 보기로 저장한다. 이름은 팝오버에서 받는다
onRevert() => void—보기의 저장된 상태로 되돌린다
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
classNamestring—

ColumnMenu

prop타입기본값설명
columns필수ColumnDef[]—
visible필수string[]—보이는 열 id
order필수string[]—열 순서(모든 열 id)
onChange필수(next: { visible: string[]; order: string[] }) => void—
onReset() => void—‘기본 열로 되돌리기’
openboolean—메뉴 열림(제어). 보통은 비제어로 둔다
defaultOpenboolean—
onOpenChange(open: boolean) => void—
modalboolean—기본 true — 열린 동안 메뉴 밖을 읽기 프로그램에서 가린다. 연 채로 고정해 보이는 자리(문서 · 픽스처)는 false
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—

SortHeader

prop타입기본값설명
align"right" | "center"—글 정렬. 숫자 열은 num 을 쓴다
numboolean—숫자 · 금액 · 날짜: 오른쪽 정렬 · 고정폭 숫자
column필수ColumnDef—
sortColumnSort—이 열의 정렬. 없으면 정렬하지 않은 열
sortCountnumbersort ? 1 : 0정렬된 열 수. 둘 이상이면 순위 숫자를 보이고 이름에 ‘2순위’ 를 더한다
onSort(dir: SortDirection | null, additive: boolean) => void—없으면 정렬할 수 없는 머리(글자만)
menu| Omit< ColumnHeaderMenuProps, "column" | "sort" | "sortCount" | "onSort" | "lang" | "labels" | "open" | "onOpenChange" > | false—⋯ 메뉴의 항목. 하나라도 주면(또는 onSort 가 있으면) 메뉴가 생긴다
widthnumber—열 너비(px). onResize 와 함께 주면 오른쪽 경계에 너비 손잡이가 생긴다
minWidthnumber—
maxWidthnumber—
onResize(width: number) => void—
onAutoFit() => void—
resizingboolean—너비 손잡이를 끄는 중으로 그린다(스펙 · 픽스처에서 상태를 고정해 보일 때)
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
extraReactNode—머리 글자 뒤에 붙일 것(도움말 아이콘 등)

이 밖에 <th> props(children · onResize 제외)를 그대로 받습니다.

ColumnHeaderMenu

prop타입기본값설명
column필수ColumnDef—
sortColumnSort—
sortCountnumbersort ? 1 : 0정렬된 열 수. 둘 이상이면 메뉴에 ‘지금 n순위’ 를 적는다
onSort필수(dir: SortDirection | null, additive: boolean) => void—dir 가 null 이면 이 열의 정렬을 뺀다. additive 는 다른 열의 정렬을 남긴다
onFilter() => void—
onPin() => void—
onHide() => void—
openboolean—
onOpenChange(open: boolean) => void—
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—

ColumnResizer

prop타입기본값설명
label필수string—열 이름. 손잡이 이름이 ‘작업 열 너비’ 가 된다
width필수number—지금 너비(px)
minnumber64기본 64
maxnumber—
onResize필수(width: number) => void—끄는 동안 · 키를 누를 때마다 부른다. 앱이 너비를 가진다
onAutoFit() => void—두 번 누름(또는 Enter 두 번) — 내용에 맞춤
resizingboolean—끄는 중으로 그린다(스펙 · 픽스처에서 상태를 고정해 보일 때)
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
classNamestring—

EditableCell

prop타입기본값설명
align"right" | "center"—글 정렬. 숫자 열은 num 을 쓴다
numboolean—숫자 · 금액 · 날짜: 오른쪽 정렬 · 고정폭 숫자
monoboolean—ID · 해시 같은 값. 고정폭 12.5 · 보조 색
value필수T—
editor"text" | "select" | "date" | ((props: CellEditorProps<T>) => ReactNode)"text"text · date 는 셀 자리의 sm 입력칸, select 는 기본 목록이다. 고르는 값(담당 · 상태)을 Combobox 로 열려면 함수로 편집기를 넘긴다
optionsTableOption[][]select 의 값들
validate(value: T) => string | undefined—오류 문장을 돌려주면 저장하지 않고 편집을 유지한다
onSave필수(next: T) => void | Promise<void>—한 번만 부른다(Enter 직후의 blur 로 두 번 저장하지 않는다). 거절하면 편집을 유지하고 오류를 보인다
readOnlybooleanfalse권한 없음 — 셀로 옮겨 다닐 수는 있고 편집만 안 된다
label필수string—편집칸의 이름(‘PAY-142 작업 이름’)
editingboolean—편집 중(제어)
defaultEditingbooleanfalse처음부터 편집 중(비제어)
onEditingChange(editing: boolean) => void—
errorstring—앱이 아는 오류(서버 검증). 주면 편집칸 아래 뜬 판에 보인다
savingboolean—앱이 저장을 들고 있을 때(제어). 셀 끝 스피너 + aria-busy
lang"ko" | "en" | "ja""ko"
labelsPartial<TableOpsLabels>—
children필수ReactNode—보기

이 밖에 <td> props(children · onChange 제외)를 그대로 받습니다.