Groo Design

Table

줄과 열로 된 데이터를 견주어 읽는 표 — HR 의 사용자 · 휴가 · 회비 · 신청 내역, Desk 의 가져오기 미리보기 · 일별 시세처럼 같은 모양의 줄을 열마다 맞춰 본다. 768 이상에서 표로 그리고, 768 미만에서는 같은 줄을 List 의 줄로 바꾼다. 표 아래 쪽 넘김은 Table Pagination, 한 줄짜리 항목을 늘어놓는 목록은 처음부터 List 다.

SEED 에는 표 컴포넌트가 없다. SEED 팀이 코드로 그린 표는 문서 사이트의 표 하나다 — "SEED 테이블 디자인의 단일 소스" 라고 적은 문서 사이트 부품(TableRoot, 제품 컴포넌트가 아니다)으로, 머리 14 / 20 · 500 · 짙은 글자 · 바탕 없음 · 높이 41, 본문 14 / 20 · 400 · 높이 45, 줄 선은 마지막 줄까지, 세로 선 · 줄무늬가 없다. 디자인 문서의 그림(Table Pagination 문서의 데이터 표)이 선택 칸 · 썸네일과 두 줄 칸 · 정렬 표시 ↑↓ · 숫자 오른쪽 · ⋮ 열 · 줄 72 를 보인다. 이 둘이 groo 표의 바탕이고(당근 SEED, Apache-2.0), 정렬 동작 · 선택 · 일괄 작업 바 · 머리 고정 · 좁은 화면 · 빈 · 실패처럼 SEED 가 적지 않은 자리는 groo 가 정했다(2026-10-08 사용자 결정). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다. 옛 Table 스펙(작은 대문자 회색 머리 · 회색 머리 바탕 · 칸 8 · 금액 고정폭 글꼴)을 대신한다.

수치 원본은 table.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.

라이트
hr.groo.app
그루 이음
근무
관리
사용자 관리

사용자 12명

사용자
부서
상태
동작
최민준
영업팀
15일
재직
김하늘
개발팀
12.5일
재직
한지우
재무팀
9.5일
재직
정예린
개발팀
7일
초대 대기
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
9:41
사용자 관리
남은 휴가 많은 순
  • 최민준영업팀 · 재직15일
  • 김하늘개발팀 · 재직12.5일
  • 오태윤마케팅팀 · 재직11일
  • 한지우재무팀 · 재직9.5일
  • 정예린개발팀 · 초대 대기7일
  • 이도윤디자인팀 · 휴직3일
  • 박서연인사팀 · 재직0.5일
다크
hr.groo.app
그루 이음
근무
관리
사용자 관리

사용자 12명

사용자
부서
상태
동작
최민준
영업팀
15일
재직
김하늘
개발팀
12.5일
재직
한지우
재무팀
9.5일
재직
정예린
개발팀
7일
초대 대기
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
9:41
사용자 관리
남은 휴가 많은 순
  • 최민준영업팀 · 재직15일
  • 김하늘개발팀 · 재직12.5일
  • 오태윤마케팅팀 · 재직11일
  • 한지우재무팀 · 재직9.5일
  • 정예린개발팀 · 초대 대기7일
  • 이도윤디자인팀 · 휴직3일
  • 박서연인사팀 · 재직0.5일
HR 사용자 표(데스크톱) · 같은 줄을 List 로(폰) — 라이트 · 다크

직접 골라 보기

줄 종류(한 줄 · 썸네일) · 정렬할 열과 방향 · 고르기 · 폭(데스크톱 · 폰)을 고르면 스펙대로 그린 표와 그 코드가 바뀐다. 머리를 누르면 정렬이 바뀌고, 체크를 누르면 일괄 작업 바가 뜬다. 폰 폭을 고르면 같은 줄이 List 줄로 바뀐다.

사용자 12명

사용자
부서
상태
동작
영업팀
15일
재직
개발팀
12.5일
재직
재무팀
9.5일
재직
개발팀
7일
초대 대기
디자인팀
3일
휴직
인사팀
0.5일
재직
읽는 글—
머리를 누를 때마다 내림 ↔ 오름(두 단계 — 숫자 열은 처음 내림, 글 열은 오름). 고른 줄은 바탕 없이 체크로만, 고른 줄이 있으면 일괄 작업 바. 768 미만은 같은 줄을 List 로.
줄 종류
정렬
고르기
머리 고정
폭
모드
import { Table, TableBody, TableCell, TableHead, TableHeader, TableMoreCell, TableMoreHead, TableRow, TableRowHeader } from "@/components/ui/table"

<Table caption="사용자">
  <TableHeader>
    <TableRow>
      <TableHead sort={sortBy === "name" ? dir : "none"} onSortChange={(d) => sort("name", d)}>이름</TableHead>
      <TableHead>부서</TableHead>
      <TableHead align="end" sort={sortBy === "days" ? dir : "none"} onSortChange={(d) => sort("days", d)}>남은 휴가</TableHead>
      <TableHead>상태</TableHead>
      <TableMoreHead />
    </TableRow>
  </TableHeader>
  <TableBody>…</TableBody>
</Table>

Anatomy

사용자 12명

2개 선택됨
사용자
부서
상태
동작
최민준
영업팀
15일
재직
김하늘
개발팀
12.5일
재직
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
ⓐ Rootⓑ Header Rowⓒ Header Cellⓓ Sort Iconⓔ Rowⓕ Cellⓖ Badgeⓗ More Buttonⓘ Checkboxⓙ Bulk Bar
표는 머리 줄 · 본문 줄 · 칸으로 이뤄지고, 정렬 표시 · 상태 배지 · 줄 끝 ⋮ · 선택 칸 · 일괄 작업 바를 붙인다
부위설명
ⓐ Root표 상자 — 카드 안에 가장자리까지 붙는다. 표와 넘김 줄을 담고, 넘치면 함께 가로로 민다.
ⓑ Header Row머리 줄 — 41, 바탕 없음, 아래 선.
ⓒ Header Cell머리 칸 — 열 이름 14 / 20 · 500. 정렬할 수 있는 열은 칸 전체가 버튼이다.
ⓓ Sort Icon정렬 표시 ↑↓ — 정렬할 수 있는 열에 늘 있고, 지금 방향만 짙다.
ⓔ Row본문 줄 — 45(썸네일 · 두 줄 칸이 있으면 72), 아래 선은 마지막 줄까지.
ⓕ Cell본문 칸 — 14 / 20 · 400. 숫자는 오른쪽.
ⓖ Badge상태 칸 — Badge medium weak.
ⓗ More Button줄 끝 ⋮ — 보이는 40 · 누르는 44, 이름 "{줄 이름} 더보기".
ⓘ Checkbox선택 칸 — Checkbox large 24. 머리는 전체 · 일부.
ⓙ Bulk Bar일괄 작업 바 — 고른 줄이 있을 때만 표 위에.

부위

부위설명
root표 상자 — 카드 안에 가장자리까지 붙는다. 표와 넘김 줄(Table Pagination)을 담고, 넘치면 함께 가로로 민다
caption표 이름 <caption> — 보이는 제목은 카드 머리, 여기는 같은 글을 숨겨 둔다
headerRow머리 줄 <thead><tr> — 바탕 없음, 아래 선
headerCell머리 칸 <th scope="col"> — 열 이름. ⋮ 열은 글 없이 숨긴 "동작"
sortButton정렬 버튼 — 정렬할 수 있는 열의 머리 칸을 채운다
sortIcon정렬 표시 ↑↓ — 정렬할 수 있는 열에 늘 있다
row본문 줄 <tr> — 아래 선(마지막 줄까지)
cell본문 칸 <td> — 첫 열(이름)은 <th scope="row">
detail칸의 둘째 줄 · 보조 글(ID · 부서 · 단위) — 옅은 글
thumbnail칸 앞 그림 — Image Frame 1:1 48
badge상태 칸 — Badge medium weak
checkbox선택 칸 — Checkbox large 24(머리는 전체 · 일부)
moreButton줄 끝 ⋮ — Button ghost iconOnly medium, 이름 "{줄 이름} 더보기"
bulkBar일괄 작업 바 — 고른 줄이 있을 때만, 표 위
focusRing키보드 포커스 링(웹)

Properties

머리 · 줄

머리 줄은 41(위아래 10 + 글 20 + 선 1), 본문 줄은 45(위아래 12 + 글 20 + 선 1)다 — SEED 문서 표 그대로. 칸의 좌우는 16 이고, 표가 카드 안에 가장자리까지 붙으므로 첫 칸 앞 · 끝 칸 뒤만 카드 여백과 같은 24 다(카드 머리 제목과 첫 열 글자가 한 줄에 선다). 머리 글자는 본문과 같은 14 · 짙은 fg-neutral 에 굵기만 500 이다 — 작은 대문자 · 회색 · 머리 바탕을 두지 않는다. 글은 t4(14 / 19)에 줄 높이만 20 으로 둔다 — 같은 부위에 따로 적은 줄 높이 · 굵기가 typography 의 값을 덮는다(스펙 JSON 은 둘을 접어 14 / 20 · 500 하나로 싣는다 — Q-011-2).

줄 선은 1px stroke-neutral-subtle 이고 머리 아래 · 줄마다 · 마지막 줄 아래까지 긋는다 — 마지막 선이 표 끝을 마감한다. 세로 선 · 줄무늬 · 바깥 테두리는 없다.

사용자

사용자
부서
상태
김하늘
개발팀
12.5일
재직
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
머리 41 — 위아래 10 · 14 / 20 · 500 · fg-neutral, 바탕 없음줄 45 — 위아래 12 · 14 / 20 · 400칸 좌우 16 · 첫 칸 앞 · 끝 칸 뒤 24(카드 여백)선 1px stroke-neutral-subtle — 마지막 줄 아래까지(분홍 점선). 세로 선 · 줄무늬 없음
머리 41 · 줄 45 · 첫 칸 24 · 칸 16 · 줄 선은 마지막 줄까지

표 상자

속성값비고
배경transparent카드 면(bg-layer-default)이 바탕 — 표는 카드(card.md) 안에 가장자리까지 붙는다. 바깥 테두리 · 모서리 · 세로 선 · 줄무늬 없음
넘칠 때auto열이 상자보다 넓으면 표와 넘김 줄이 함께 가로로 밀린다(Table Pagination). 768 미만은 표 대신 List 줄이라 밀지 않는다
좌우 여백spacing-x6 24px첫 칸 앞 · 끝 칸 뒤만 — 카드 여백 24 와 같아 카드 머리와 첫 열 글자가 한 줄에 선다. 줄 선은 상자 끝까지 긋는다

머리 줄

속성값비고
머리 줄 최소 높이41px선 1 포함 — 위아래 10 + 글 20(SEED 문서 표)
머리 줄 배경transparent머리 바탕 없음. 상자 안에서 고정할 때만 카드 면과 같은 bg-layer-default 로 칠해 지나가는 줄을 가린다 — 바탕이 달라 보이지 않는다
머리 줄 아래 선 두께1px
머리 줄 아래 선color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D
머리 줄 자리sticky표 상자가 스스로 세로로 스크롤할 때만(위 0) — 페이지 스크롤에는 붙이지 않는다
머리 줄 z-index1표 상자 안의 겹침 — 층이 아니다(specs/z-index.md)

머리 칸

속성값비고
머리 칸 상하 여백spacing-x2_5 10px
머리 칸 좌우 여백spacing-x4 16px
머리 칸 글자text-t4 14px / 19px
머리 칸 줄 높이20pxSEED 문서 표 14 / 20 — t4(14 / 19)보다 1 높여 머리 41 · 줄 45 를 SEED 와 맞춘다(사용자 결정 1A). 같은 부위의 lineHeight · fontWeight 가 typography 의 값을 덮는다 — 스펙 JSON 은 typography 에 접어 14 / 20 · 500 하나로 싣는다(Q-011-2)
머리 칸 굵기500
머리 칸 글자색color-fg-neutral #1A1F2E · 다크 #F5F6FA
머리 칸 맞춤start

본문 줄

속성값비고
줄 최소 높이45px선 1 포함 — 위아래 12 + 글 20(SEED 문서 표). 한 표 안에서 줄 높이를 섞지 않는다 — 썸네일 · 두 줄 칸이 하나라도 있으면 모든 줄이 72
줄 아래 선 두께1px
줄 아래 선color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D마지막 줄까지 — 표 끝을 마감한다(SEED 문서 표). 줄무늬 · 세로 선은 두지 않는다
줄 배경transparent
줄 커서pointer누르는 줄만 — 줄을 누르면 상세가 열린다. 보기만 하는 표는 default
줄 전환 대상background-color
줄 전환 시간motion-duration-color-transition 150ms
줄 전환 곡선motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1)

본문 칸

속성값비고
칸 상하 여백spacing-x3 12px
칸 좌우 여백spacing-x4 16px
칸 글자text-t4 14px / 19px
칸 줄 높이20px머리와 같은 14 / 20 — typography 의 19 를 덮는다(Q-011-2)
칸 굵기400
칸 글자색color-fg-neutral #1A1F2E · 다크 #F5F6FA
칸 세로 맞춤center칸의 내용은 세로 가운데

줄 종류 — 45 · 72

줄 높이는 하나다 — 45. 칸 하나라도 썸네일(Image Frame 1:1 48)이나 두 줄 글(윗줄 14 + 둘째 줄 13 · fg-neutral-subtle)을 두면 그 표의 모든 줄이 72 다(SEED 그림). 72 줄의 칸은 위아래 여백을 두지 않고 내용을 세로 가운데에 둔다 — 썸네일 48 에 위아래 12 를 더하면 선까지 73 이라 높이가 먼저다(Q-011-1). 한 표 안에서 줄 높이를 섞지 않고, 촘촘한 · 넉넉한 두 벌(밀도 변형)을 두지 않는다. 사람은 Avatar 42, 은행 · 카드 같은 물건은 Logo Tile 40 을 썸네일 자리에 둔다.

사용자

사용자
부서
상태
김하늘
개발팀
12.5일
재직
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
한 줄 표 — 45글만 있는 칸

사용자

사용자
부서
상태
김하늘haneul.kim
개발팀팀원
12.5일
재직
이도윤doyun.lee
디자인팀팀장
3일
휴직
박서연seoyeon.park
인사팀팀원
0.5일
재직
썸네일 · 두 줄 표 — 72Avatar 42 + 둘째 줄 13 · fg-neutral-subtle — 한 칸이라도 있으면 모든 줄이 72
한 줄 표 45 · 썸네일 · 두 줄 표 72

줄 종류

row줄 최소 높이칸 상하 여백설명
text (기본)——한 줄 칸만 — 45(기본)
rich72px — 선 1 포함(SEED 그림) — 썸네일 48 이 세로 가운데. 칸 여백 12 보다 높이가 먼저다0px — 72 안 세로 가운데 — 48 + 위아래 12 + 선 1 = 73 이라 위아래 여백을 두지 않는다(높이가 먼저, 사용자 결정 1A 의 72 — Q-011-1). 좌우 16 은 그대로썸네일 · 두 줄 칸이 있는 줄 — 72

둘째 줄

속성값비고
둘째 줄 글자text-t3 13px / 400 / 18px
둘째 줄 글자색color-fg-neutral-subtle #62697A · 다크 #A2A8B7
윗줄 ↔ 둘째 줄spacing-x0_5 2px윗줄 ↔ 둘째 줄

썸네일

속성값비고
썸네일 크기48pxImage Frame 1:1 — 모서리는 폭 48 이하라 6(image-frame.yaml). 사람은 Avatar 42, 기관은 Logo Tile 40
썸네일 ↔ 글spacing-x3 12px그림 ↔ 글

숫자 열

금액 · 개수 · 비율은 오른쪽에 맞추고 고정폭 숫자(tabular-nums)로 둔다 — 자릿수가 위아래로 맞아 크기를 견준다. 머리도 오른쪽이다. 고정폭 글꼴(mono)은 쓰지 않는다. 돈은 줄이지 않고 원까지("1,240,000원"), 빼기는 U+2212(−) 하나다(International Design). 날짜는 왼쪽이다 — 같은 형식이라 자리가 맞는다. 오르내림을 적는 칸(등락률)은 증감 표기대로 ▲ fg-critical · ▼ fg-informative + 값이다.

회비

회비
날짜
내용
금액
2026. 10. 5.
10월 회비
1,240,000원
2026. 10. 2.
간식 구입
−35,000원
2026. 9. 28.
워크숍 장소
−480,000원
2026. 9. 5.
9월 회비
1,180,000원
오른쪽 · 고정폭 숫자 · 머리도 오른쪽자릿수가 위아래로 맞아 큰 값이 한눈에 — 돈은 원까지, 빼기 U+2212

회비

회비
날짜
내용
금액
2026. 10. 5.
10월 회비
1,240,000원
2026. 10. 2.
간식 구입
−35,000원
2026. 9. 28.
워크숍 장소
−480,000원
2026. 9. 5.
9월 회비
1,180,000원
왼쪽에 둔 숫자자릿수가 어긋나 크기를 견주기 어렵다
숫자 열 — 오른쪽 · 고정폭 숫자 · 머리도 오른쪽, 왼쪽에 둔 숫자

맞춤

align머리 칸 맞춤칸 맞춤칸 숫자 폭설명
start (기본)———글 · 이름 · 날짜 · 상태 — 왼쪽(기본)
endend — 머리도 오른쪽 — 글 + ↑↓ 묶음이 열 오른쪽 끝에 붙는다endtabular-nums — 고정폭 숫자 — 자릿수가 위아래로 맞는다. 고정폭 글꼴(mono)은 쓰지 않는다. 돈은 줄이지 않고 원까지, 빼기는 U+2212(−)금액 · 개수 · 비율 — 오른쪽 + 고정폭 숫자, 머리도 오른쪽

정렬

정렬할 수 있는 열은 머리 칸 전체가 버튼이고, 열 이름 오른쪽 6 에 ↑↓ 표시(16)를 늘 둔다 — 흐린 fg-neutral-muted 둘에서 지금 정렬된 방향 하나만 fg-neutral 로 짙어진다(SEED 그림). 어느 열을 정렬할 수 있는지 처음부터 보이므로, 마우스를 올려야 화살표가 나타나는 열은 없다. 정렬하지 않는 열(이미지 · ⋮ · 긴 설명)은 글만이다.

정렬은 두 단계다 — 누를 때마다 내림 ↔ 오름이 바뀌고, 정렬 없음으로 돌아가지 않는다(사용자 결정). 처음 누르면 숫자 · 날짜 열은 내림(큰 값 · 최근 것 먼저), 글 열은 오름(가나다)이다. 다른 열을 누르면 그 열의 처음 방향으로 옮긴다.

사용자

사용자
부서
상태
김하늘
개발팀
12.5일
재직
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
늘 보이는 ↑↓ — 지금 방향만 짙게정렬할 수 있는 열(이름 · 남은 휴가)에 늘 ↑↓(16) — 흐린 fg-neutral-muted 둘에서 지금 방향만 fg-neutral. 머리 칸 전체가 버튼, aria-sort 는 정렬된 열에만
처음
남은 휴가
한 번
남은 휴가
두 번
남은 휴가
세 번
남은 휴가
숫자 · 날짜 열 — 처음 내림누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다)
처음
이름
한 번
이름
두 번
이름
세 번
이름
글 열 — 처음 오름(가나다)다른 열을 누르면 그 열의 처음 방향으로 옮긴다
정렬 — 늘 보이는 ↑↓, 지금 방향만 짙게(남은 휴가 ↓)

정렬

sort정렬 버튼 너비글 ↔ 정렬 표시정렬 버튼 커서정렬 표시 크기정렬 표시 색설명
none (기본)—————정렬하지 않는 열 — 머리는 글만(기본)
sortable칸 전체 — 머리 칸의 여백을 버튼이 가진다 — 칸 어디를 눌러도 정렬. 누르는 영역 칸 폭 × 41(위아래로 44 까지 넓힌다). 누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다), 처음 누르면 숫자 · 날짜 열은 내림 · 글 열은 오름(사용자 결정 18A)spacing-x1_5 6px — 글 ↔ ↑↓pointer16px — ↑ 와 ↓ 를 나란히 한 표시 — 두 화살표를 따로 칠한다color-fg-neutral-muted #535866 · 다크 #B7BDCC — 정렬할 수 있는 열에 늘 보인다 — 둘 다 흐린 색정렬할 수 있고 지금은 아님 — ↑↓ 둘 다 흐리게
ascending칸 전체spacing-x1_5 6pxpointer16pxcolor-fg-neutral #1A1F2E · 다크 #F5F6FA — ↑ 만 짙게 — ↓ 는 fg-neutral-muted 그대로. 머리 칸에 aria-sort="ascending"오름차순 — ↑ 를 짙게
descending칸 전체spacing-x1_5 6pxpointer16pxcolor-fg-neutral #1A1F2E · 다크 #F5F6FA — ↓ 만 짙게 — ↑ 는 fg-neutral-muted 그대로. 머리 칸에 aria-sort="descending"내림차순 — ↓ 를 짙게

상태 칸

상태는 Badge medium(20) · weak 이고 톤이 뜻을 말한다(재직 positive · 휴직 warning · 초대 대기 neutral). 한 열은 한 변형이고, 글 열처럼 왼쪽에 맞춘다.

상태 배지

속성값비고
상태 배지Badge medium20 · weak — 톤은 상태의 뜻(badge.md). 한 열은 한 변형, 왼쪽 맞춤

줄 끝 ⋮

줄의 동작(수정 · 삭제 …)은 줄 끝 ⋮ 하나로 연다 — Button ghost · iconOnly · medium(보이는 40 · 누르는 44), 이름 "{줄 이름} 더보기". 1280 이상은 Menu, 미만은 Menu Sheet 다. 열 폭은 80(앞 16 + 40 + 끝 24 — 끝 열이라 카드 여백)이고 머리 칸에는 글을 쓰지 않는다 — 보조 기술에는 숨긴 "동작" 이 읽힌다. 줄을 누르면 상세가 열린다.

사용자

사용자
이름
남은 휴가
동작
김하늘
12.5일
이도윤
3일
보조 기술이 읽는 글: 머리 칸: 동작(숨긴 글)보조 기술이 읽는 글: ⋮: 김하늘 더보기, 메뉴 버튼
열 폭 80(앞 16 + 40 + 끝 24) · Button ghost iconOnly medium · 1280 이상 Menu, 미만 Menu Sheet. 머리 칸에는 글을 쓰지 않는다
줄 끝 ⋮ — 40 · 누르는 44 · 머리 글 없음

줄 끝 ⋮

속성값비고
⋮ 크기40pxButton ghost · iconOnly · medium — 아이콘 18(lucide ellipsis-vertical). 열 폭 80(앞 16 + 40 + 끝 24 — 끝 열이라 카드 여백), 머리 칸은 글 없이 숨긴 "동작"
⋮ 누르는 영역44 × 44보이는 40 바깥으로 2 씩 — 줄 45 안에 든다. AA ✓ · AAA ✓

선택 · 일괄 작업 바

여럿을 골라 한 번에 다루는 표만 첫 열에 Checkbox large(24)를 둔다. 머리 칸의 체크는 지금 쪽의 줄 전부를 고르고 풀며, 일부만 골랐으면 일부(indeterminate)다. 한 줄 이상 고르면 표 위에 일괄 작업 바가 뜬다 — 바탕 bg-brand-weak 한 값, 높이 48 · 모서리 12, 왼쪽에 "3개 선택됨", 오른쪽에 동작(Button ghost small)과 선택 해제 ✕. 바 안 동작은 모두 ghost neutral 이다 — "삭제" 도 회색 글(아이콘 · 글이 위험을 말한다)이고, 빨강은 누른 뒤 뜨는 확인창(Alert Dialog — "3개를 지울까요?" + criticalSolid)에서 쓴다. 옅은 브랜드 바탕 위 빨강 글은 라이트 4.44 : 1 로 4.5 에 못 미친다(2026-10-17 사용자 결정 — 01 기초 앱 · 글자 웹 비교 페이지 01T-033 A, SEED Action Button 의 ghost 도 fg.neutral 하나다). 고른 줄 자체에는 바탕을 칠하지 않는다 — 체크가 고른 것을 말한다(List 의 여럿 고르기와 같다, 사용자 결정). 브랜드 옅은 바탕은 일괄 작업 바에만 있다.

업무 보고

2개 선택됨
업무 보고
제목
작성자
상태
9월 4주 주간 보고
박서연
2026. 9. 28.
승인
채용 면접 정리
최민준
2026. 9. 25.
반려
9월 3주 주간 보고
한지우
2026. 9. 21.
승인
10월 1주 주간 보고
김하늘
2026. 10. 5.
제출
결제 화면 개편 회고
이도윤
2026. 10. 2.
제출
고른 줄은 바탕 없이 체크(Checkbox large 24)로만 — 머리 체크는 일부(indeterminate). 일괄 작업 바 48 · 모서리 12 · bg-brand-weak, 표 위 8
선택 열 · 머리 체크 일부 · 일괄 작업 바 — 고른 줄은 바탕 없이 체크로만

고르기

selection선택 칸일괄 작업 바설명
none (기본)——고르지 않는 표(기본)
empty보임 — 머리 체크는 꺼짐 — 누르면 지금 쪽의 줄을 모두 고른다—고를 수 있다 · 고른 줄 없음 — 체크 칸만, 바 없음
some머리 체크 일부 — indeterminate — 누르면 모두 고른다보임 — "{n}개 선택됨" — 보조 기술에 고른 수를 알린다(role="status")일부 고름 — 머리 체크는 일부 · 바 보임
all머리 체크 켬 — 누르면 모두 푼다 — 지금 쪽의 줄만보임 — "{n}개 선택됨"모두 고름 — 머리 체크 켬 · 바 보임

선택 칸

속성값비고
선택 칸none고르는 표만 — 첫 열(selection empty · some · all)
선택 칸 크기24pxCheckbox large — 열 폭은 24 + 여백(첫 칸이라 앞 24 · 뒤 16). 줄을 누르면 상세라 체크만 고른다
선택 칸 누르는 영역44 × 44체크 가운데 — 줄 45 안에 든다. AA ✓ · AAA ✓

일괄 작업 바

속성값비고
일괄 작업 바none
일괄 작업 바 최소 높이48px표 위 — 고른 수 글(t4 14 · 500 "3개 선택됨") + 동작 버튼(Button ghost small) + 선택 해제 ✕
actionsButton ghost small · neutral바 안 동작은 모두 ghost neutral — "삭제" 도 회색 글. 빨강은 누른 뒤의 확인창(Alert Dialog criticalSolid)에서. 옅은 브랜드 바탕 위 빨강 글은 라이트 4.44 : 1(2026-10-17 사용자 결정 01T-033 A — SEED ghost 는 fg.neutral 하나)
일괄 작업 바 좌우 여백spacing-x4 16px왼쪽 16 · 오른쪽은 끝 버튼이 붙도록 8
일괄 작업 바 모서리radius-r3 12px
일괄 작업 바 배경color-bg-brand-weak HR #E8F3EC · Desk #E8F1FE한 값 — 지금 표 10% · DESIGN 8% 두 벌을 걷는다. 고른 줄에는 바탕을 칠하지 않는다(체크로만)
일괄 작업 바 간격spacing-x2 8px
바 ↔ 표 머리spacing-x2 8px바 ↔ 표 머리

머리 고정

줄이 많은 표(25 · 50줄 보기)나 위젯 · 대화상자 안의 표처럼 표 상자에 높이를 정해 상자 안에서 스크롤하면 머리 줄을 상자 맨 위에 붙인다. 붙은 머리는 카드 면과 같은 bg-layer-default 로 칠해 지나가는 줄을 가린다 — 바탕이 달라 보이지 않는다. 페이지 전체가 스크롤할 때는 붙이지 않는다(상단 바와 겹친다).

휴가 현황

사용자
부서
상태
최민준
영업팀
15일
재직
장민호
개발팀
13일
재직
김하늘
개발팀
12.5일
재직
오태윤
마케팅팀
11일
재직
한지우
재무팀
9.5일
재직
배수빈
재무팀
8.5일
재직
정예린
개발팀
7일
초대 대기
서하은
인사팀
6일
재직
윤서아
디자인팀
4.5일
재직
이도윤
디자인팀
3일
휴직
문재희
영업팀
2일
휴직
박서연
인사팀
0.5일
재직
상자 안에서 스크롤하면 머리 줄은 맨 위에 붙고 카드 면과 같은 bg-layer-default 로 지나가는 줄을 가린다 — 페이지 스크롤에는 붙이지 않는다
상자 안에서 스크롤 — 머리는 맨 위에, 줄은 그 아래로

State

줄을 누르면 상세가 열리는 표만 상태가 있다. 보기만 하는 표의 줄은 바뀌지 않는다.

상태모습
enabled줄 바탕 없음
hovered웹 · 누르는 줄 — 줄 전체가 bg-layer-default-pressed(누름과 같은 색 — v106). 정렬 버튼도 같다
pressed같은 바탕 — 축소는 없다(표의 줄 · 칸은 이웃과 붙어 있어 줄이면 흔들린다)
focused웹 — 키보드 포커스가 간 링크 · 버튼 · 정렬 버튼에 안쪽 2px 링

사용자

사용자
부서
상태
최민준 — 포커스(웹)
영업팀
15일
재직
김하늘 — 기본
개발팀
12.5일
재직
이도윤 — 호버(웹)
디자인팀
3일
휴직
박서연 — 누름
인사팀
0.5일
재직
누르는 줄호버 · 누름은 같은 bg-layer-default-pressed — 축소는 없다. 포커스는 이름 링크 안쪽 2px 링
남은 휴가
기본
남은 휴가
호버(웹)
남은 휴가
누름
남은 휴가
포커스(웹)
정렬 버튼
상태 — 기본 · 호버 · 누름 · 포커스

상태 — 줄

상태줄 배경
enabled 기본 — 보기만 하는 줄은 이것 하나transparent
hovered 웹 · 누르는 줄 · 정렬 버튼 — 누름과 같은 바탕(v106)color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — 누르는 줄만 — 줄 전체(줄 선 위까지). List 의 누름 바탕과 같은 색
pressed 누르는 줄 · 정렬 버튼 — 바탕만(축소 없음)color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — 바탕만 — 표의 줄 · 칸은 이웃과 붙어 있어 줄이지 않는다

상태 — 정렬 버튼

상태정렬 버튼 너비글 ↔ 정렬 표시정렬 버튼 배경정렬 버튼 커서
enabled 기본 — 보기만 하는 줄은 이것 하나칸 전체 — 머리 칸의 여백을 버튼이 가진다 — 칸 어디를 눌러도 정렬. 누르는 영역 칸 폭 × 41(위아래로 44 까지 넓힌다). 누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다), 처음 누르면 숫자 · 날짜 열은 내림 · 글 열은 오름(사용자 결정 18A)spacing-x1_5 6px — 글 ↔ ↑↓—pointer
hovered 웹 · 누르는 줄 · 정렬 버튼 — 누름과 같은 바탕(v106)칸 전체 — 머리 칸의 여백을 버튼이 가진다 — 칸 어디를 눌러도 정렬. 누르는 영역 칸 폭 × 41(위아래로 44 까지 넓힌다). 누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다), 처음 누르면 숫자 · 날짜 열은 내림 · 글 열은 오름(사용자 결정 18A)spacing-x1_5 6px — 글 ↔ ↑↓color-bg-layer-default-pressed #F7F8FD · 다크 #353B4Dpointer
pressed 누르는 줄 · 정렬 버튼 — 바탕만(축소 없음)칸 전체 — 머리 칸의 여백을 버튼이 가진다 — 칸 어디를 눌러도 정렬. 누르는 영역 칸 폭 × 41(위아래로 44 까지 넓힌다). 누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다), 처음 누르면 숫자 · 날짜 열은 내림 · 글 열은 오름(사용자 결정 18A)spacing-x1_5 6px — 글 ↔ ↑↓color-bg-layer-default-pressed #F7F8FD · 다크 #353B4Dpointer

모션

전환시간곡선대상비고
줄 바탕motion-duration-color-transition 150msmotion-ease-easing cubic-bezier(0.35, 0, 0.35, 1)background-color누르는 줄 · 정렬 버튼의 호버 · 누름 — 축소는 없다

Guidelines

768 미만은 List 줄로

768 미만에서는 표를 그리지 않고 줄마다 List 의 누르는 줄로 바꾼다 — 폰에서 옆으로 밀거나 열을 숨겨 보지 않는다(사용자 결정). 표마다 줄에 무엇을 둘지 이 규칙으로 정한다.

표List 줄
첫 열(그 줄의 이름)제목(16 / 22)
다음으로 중요한 열 1 ~ 2개설명(13 · fg-neutral-subtle, " · " 로 잇는다)
핵심 숫자 하나 또는 상태오른쪽 값 — 숫자는 16 · 고정폭 숫자, 상태는 Badge
나머지 열줄을 눌러 여는 상세
썸네일 · 사람 · 기관앞 붙이개(Image Frame · Avatar · Logo Tile)
선택 칸앞 체크 24(List 의 여럿 고르기)
줄 끝 ⋮줄 끝 ⋮ → Menu Sheet — 폰 스와이프는 지름길(Swipe Actions)
정렬목록 위 정렬 고르기 하나("남은 휴가 많은 순") — Select
넘김 줄끝없이 불러오기(Pagination 의 "폰은 끝없이 불러오기")

사용자

사용자
부서
상태
동작
최민준
영업팀
15일
재직
김하늘
개발팀
12.5일
재직
정예린
개발팀
7일
초대 대기
이도윤
디자인팀
3일
휴직
박서연
인사팀
0.5일
재직
768 이상 — 표이름 · 부서 · 남은 휴가 · 상태 · ⋮
9:41
사용자
남은 휴가 많은 순
  • 최민준영업팀 · 재직15일
  • 김하늘개발팀 · 재직12.5일
  • 정예린개발팀 · 초대 대기7일
  • 이도윤디자인팀 · 휴직3일
  • 박서연인사팀 · 재직0.5일
List 줄제목 이름 · 설명 "부서 · 상태" · 오른쪽 남은 휴가(16 · 고정폭 숫자). 나머지는 줄을 눌러 상세
HR 사용자 표 → 폰 List — 이름 · 부서 · 상태 · 남은 휴가

좁은 화면

width넘칠 때그리는 모양칸 → 줄 자리선택 칸줄 끝 ⋮설명
wide (기본)—————768 이상 — 표로 그린다(기본)
narrowvisible — 가로로 밀지 않는다List 줄 — 768 미만은 표를 그리지 않는다 — 줄마다 List 의 누르는 줄(list.yaml). 넘김 줄 대신 끝없이 불러오기(infinite-list.yaml)제목 · 설명 · 값 — 첫 열 → 제목(t5 16 / 22), 다음 열 1 ~ 2개 → 설명(t3 13 · " · " 로 잇는다), 핵심 숫자 하나 또는 상태 → 오른쪽 값(숫자는 t5 · 고정폭 숫자, 상태는 Badge). 나머지 열은 줄을 눌러 여는 상세에앞 체크 24 — 고르는 표는 List 의 여럿 고르기(ListCheckItem) — 체크를 앞에줄 끝 ⋮ — 1280 미만이라 Menu Sheet 를 연다(menu.md). 폰 스와이프는 같은 동작의 지름길(swipe-actions.md)768 미만 — List 줄로 바꾼다

선은 줄 사이에만, 마지막 줄까지

줄 사이 1px 선 하나로 줄을 가른다 — 마지막 줄 아래까지. 세로 선 · 줄무늬(짝수 줄 바탕) · 머리 바탕 · 작은 대문자 머리를 두지 않는다. 줄이 길어 따라 읽기 어려우면 줄무늬 대신 열을 줄인다.

사용자
부서
김하늘
개발팀
12.5일
이도윤
디자인팀
3일
박서연
인사팀
0.5일
이렇게 — 줄 사이 선 하나 — 마지막 줄 아래까지
사용자
부서
김하늘
개발팀
12.5일
이도윤
디자인팀
3일
박서연
인사팀
0.5일
이렇게 하지 않는다 — 세로 선 — 칸이 갇혀 줄을 따라 읽기 어렵다
사용자
부서
김하늘
개발팀
12.5일
이도윤
디자인팀
3일
박서연
인사팀
0.5일
이렇게 하지 않는다 — 줄무늬(짝수 줄 바탕) — 줄이 길면 열을 줄인다
사용자
부서
김하늘
개발팀
12.5일
이도윤
디자인팀
3일
박서연
인사팀
0.5일
이렇게 하지 않는다 — 작은 대문자 회색 머리 · 머리 바탕
줄 선만 · 세로 선 · 줄무늬 · 회색 머리 바탕

숫자는 오른쪽, 돈은 원까지

숫자 열은 머리까지 오른쪽이다 — 왼쪽에 두면 자릿수가 어긋나 큰 값을 한눈에 못 찾는다. 돈은 줄이지 않는다("1.2만원" 이 아니라 "12,000원"). 표는 넓은 화면의 자리라 줄일 이유가 없다.

회비
날짜
내용
금액
2026. 10. 5.
10월 회비
1,240,000원
2026. 10. 2.
간식 구입
−35,000원
2026. 9. 28.
워크숍 장소
−480,000원
2026. 9. 5.
9월 회비
1,180,000원
이렇게 — 숫자는 머리까지 오른쪽 · 고정폭 숫자, 돈은 원까지
회비
날짜
내용
금액
2026. 10. 5.
10월 회비
1,240,000원
2026. 10. 2.
간식 구입
−35,000원
2026. 9. 28.
워크숍 장소
−480,000원
2026. 9. 5.
9월 회비
1,180,000원
이렇게 하지 않는다 — 왼쪽 · 고정폭 글꼴(mono) — 자릿수가 어긋나고 글꼴이 섞인다
숫자 열 — 오른쪽 · 왼쪽

줄의 동작은 ⋮ 하나

수정 · 삭제 아이콘을 줄마다 늘어놓지 않는다 — 줄 끝 ⋮ 하나에 모으고, 줄 자체를 누르면 상세가 열린다(Menu 의 "줄의 동작"). 줄 안의 누르는 것(체크 · 줄 · ⋮)은 셋까지 — 넷 이상은 피한다(List 의 누르는 자리와 같은 세기).

사용자
동작
김하늘
12.5일
이도윤
3일
박서연
0.5일
이렇게 — 줄 끝 ⋮ 하나 — 이름 "{줄 이름} 더보기", 줄을 누르면 상세
사용자
김하늘
12.5일
이도윤
3일
박서연
0.5일
이렇게 하지 않는다 — 줄마다 늘어놓은 수정 · 삭제 아이콘 — 누르는 것이 줄마다 늘고 이름이 없다
⋮ 하나 · 늘 보이는 아이콘 묶음

표처럼 보이면 table 로

열을 맞춰 줄을 견주는 자리는 div 격자가 아니라 <table> 로 짠다 — 일별 시세 · 호가 · 체결도 표다. div 로 짜면 보조 기술이 열 이름을 읽지 못한다. 반대로 줄 하나에 한 항목을 보이는 목록(거래 · 할 일)은 표가 아니라 List 다.

삼성전자 일별 시세
날짜
종가
등락률
거래량
10. 8.
71,200원
1.28% 올랐어요
12,480,310
10. 7.
70,300원
0.85% 내렸어요
9,812,004
10. 6.
70,900원
2.16% 올랐어요
15,230,998
보조 기술이 읽는 글: 10. 8., 등락률, 1.28% 올랐어요
이렇게 — <table> — 칸마다 열 이름 · 줄 이름이 읽힌다
날짜종가등락률거래량
10. 8.71,2001.28%1248만
10. 7.70,300-0.85%981만
10. 6.70,9002.16%1523만
보조 기술이 읽는 글: 10. 8. 71,200 1.28% 1248만
이렇게 하지 않는다 — div 격자 — 보이는 것은 같지만 보조 기술이 열 이름을 모른다
일별 시세 — table · div 격자

불러오는 동안 · 비었을 때 · 실패했을 때

머리 줄은 처음부터 그린다 — 열 이름은 서버에서 오지 않는다. 불러오는 동안은 줄 자리만 Skeleton(줄 높이 그대로 · 글 자리 t4 19)이다. 비었으면 본문 자리에 Result Section medium("등록된 사용자가 없어요" · 거르기 때문이면 "조건에 맞는 사용자가 없어요" + "필터 초기화"), 불러오지 못했으면 failure + "다시 시도" 다 — 실패를 빈 표로 보이지 않는다. 시간표(1 · 5 · 10초)는 Skeleton 의 "기다리는 동안" 이다.

사용자

사용자
부서
불러오는 동안머리는 처음부터, 줄 자리만 Skeleton(줄 높이 그대로 · 글 자리 t4 19)

사용자

사용자
부서
조건에 맞는 사용자가 없어요
비었음거르기 때문이면 "조건에 맞는 사용자가 없어요" + 필터 초기화

사용자

사용자
부서
사용자를 불러오지 못했어요

잠시 뒤 다시 시도해주세요.

실패실패를 빈 표로 보이지 않는다 — Result Section failure + 다시 시도
불러오는 동안 줄 스켈레톤 · 비었음 · 실패 + 다시 시도

표 아래는 Table Pagination

줄이 한 쪽(10줄)보다 많을 수 있으면 표 아래 12 에 Table Pagination 을 둔다 — 표와 같은 상자 안이라 열이 넘쳐 가로로 밀 때 함께 밀린다. 넘기면 표 맨 위로 스크롤한다. 12 는 넘김 줄의 위 여백(table-pagination.yaml root.marginTop)이다 — 표의 pagination 자리는 좌우 24 만 두고 위 여백을 더하지 않는다. 그래서 자리에 넣어도, 표 바로 뒤에 두어도 12 다(2026-10-17 사용자 결정 — 03-D 비교 페이지 결정 5 B. 전에는 두 스펙이 같은 12 를 각자 적어 자리에 넣으면 24 였다).

글

열 이름은 짧은 명사("이름" · "남은 휴가" · "상태") — 영어("Email") · 마침표 없이, 단위는 칸에("12.5일"). 표 이름은 카드 머리의 제목이고 <caption> 에 같은 글을 둔다. 고른 수는 "3개 선택됨", 선택 칸의 이름은 "{줄 이름} 선택" · 머리는 "모두 선택", ⋮ 는 "{줄 이름} 더보기" 다.

코드

레시피 recipes/shadcn/components/ui/table.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.

  • Table — 표 상자(가로 스크롤) + <table>. caption(필수 — 숨긴 표 이름, 보이는 제목은 카드 머리) · rowHeight("text" 기본 45 · "rich" 72) · stickyHeader(상자가 스스로 스크롤할 때 머리를 붙인다 — 높이는 className 의 max-h-*) · pagination(표 아래 Table Pagination 자리 — 표와 같은 가로 스크롤 상자 안, 좌우 24. 위 12 는 Table Pagination 이 갖는다) · tableClassName(안쪽 <table> 에 붙는 클래스).
  • TableHeader · TableBody · TableRow — <thead> · <tbody> · <tr>. TableRow 에 onClick 을 주면 누르는 줄이다(호버 · 누름 바탕) — 키보드로는 첫 열의 이름 링크(TableRowHeader 의 href 또는 onClick)로 같은 곳에 간다.
  • TableHead — 머리 칸(<th scope="col">). align("start" 기본 · "end") · sort("none" · "ascending" · "descending" — 주면 정렬할 수 있는 열, 칸 전체가 버튼이고 ↑↓ 를 늘 그린다 · aria-sort 를 단다) · onSortChange(next)(누를 때마다 내림 ↔ 오름, 두 단계 — "none" 으로 돌아가지 않는다) · defaultSortDirection(그 열을 처음 누를 때의 방향 — 기본은 align="end"(숫자)면 "descending", 아니면 "ascending". 날짜 열은 "descending" 을 준다).
  • TableRowHeader — 첫 열(<th scope="row">, 그 줄의 이름). href · onClick 을 주면 이름이 링크 · 버튼이 된다.
  • TableCell — 본문 칸(<td>). align. 숫자는 align="end" 가 고정폭 숫자를 건다.
  • TableCellContent — 썸네일 · 두 줄 칸. media(Image Frame 48 · Avatar 42 · Logo Tile 40) · title · detail.
  • TableSelectHead · TableSelectCell — 선택 칸. 머리는 checked(true · false · "indeterminate") · onCheckedChange(이름 "모두 선택"), 줄은 checked · onCheckedChange · label(이름 "{label} 선택"). 줄을 누르는 것과 따로다.
  • TableMoreHead · TableMoreCell — ⋮ 열. 머리는 숨긴 "동작", 칸은 label(이름 "{label} 더보기") · open · onOpenChange + 자식에 ResponsiveMenuContent(1280 이상 Menu · 미만 Menu Sheet — menu.md).
  • TableBulkBar — 일괄 작업 바. count(0 이면 그리지 않는다 · "{count}개 선택됨" 을 알린다) · onClear(✕ "선택 해제") · 자식은 동작 버튼(Button ghost small).
  • TableStatusRow — 본문 자리 한 칸(colSpan 전체). 자식은 ResultSection size="medium"(비었음 · 실패) — 표가 카드 안이라 Result Section 의 좌우 여백은 0, 칸의 24 만 둔다(card.md).
  • TableSkeletonRows — 불러오는 동안의 줄. rows(기본 10 — 줄 수 보기) · columns(열마다 맞춤 · 폭).
  • useTableLayout() — "table"(768 이상) · "list"(미만). 768 미만이면 쓰는 쪽이 같은 데이터를 List 줄로 그린다(위 "768 미만은 List 줄로").

HR 사용자 표

사용자

사용자
부서
상태
동작
영업팀
15일
재직
개발팀
12.5일
재직
개발팀
7일
초대 대기
디자인팀
3일
휴직
인사팀
0.5일
재직
읽는 글—
사용자 — 이름 · 부서 · 남은 휴가(오른쪽) · 상태 · ⋮
import { Badge } from "@/components/ui/badge"
import { ResponsiveMenuContent, ResponsiveMenuItem } from "@/components/ui/menu"
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableMoreCell, TableMoreHead, TableRow, TableRowHeader,
} from "@/components/ui/table"

<Table caption="사용자">
  <TableHeader>
    <TableRow>
      <TableHead sort={sortBy === "name" ? dir : "none"} onSortChange={(d) => sort("name", d)}>이름</TableHead>
      <TableHead>부서</TableHead>
      <TableHead align="end" sort={sortBy === "days" ? dir : "none"} onSortChange={(d) => sort("days", d)}>남은 휴가</TableHead>
      <TableHead>상태</TableHead>
      <TableMoreHead />
    </TableRow>
  </TableHeader>
  <TableBody>
    {users.map((u) => (
      <TableRow key={u.id} onClick={() => openUser(u.id)}>
        <TableRowHeader href={`/users/${u.id}`}>{u.name}</TableRowHeader>
        <TableCell>{u.department}</TableCell>
        <TableCell align="end">{formatDays(u.remainingDays)}</TableCell>
        <TableCell><Badge tone={u.active ? "positive" : "warning"}>{u.statusLabel}</Badge></TableCell>
        <TableMoreCell label={u.name}>
          <ResponsiveMenuContent title={u.name}>
            <ResponsiveMenuItem label="수정" onSelect={() => edit(u)} />
            <ResponsiveMenuItem label="삭제" tone="critical" onSelect={() => askDelete(u)} />
          </ResponsiveMenuContent>
        </TableMoreCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

고르기 · 일괄 작업

업무 보고

2개 선택됨
업무 보고
제목
작성자
시간
상태
9월 4주 주간 보고
박서연
2026. 9. 28.
40시간
승인
채용 면접 정리
최민준
2026. 9. 25.
6.5시간
반려
9월 3주 주간 보고
한지우
2026. 9. 21.
39시간
승인
10월 1주 주간 보고
김하늘
2026. 10. 5.
38시간
제출
결제 화면 개편 회고
이도윤
2026. 10. 2.
12시간
제출
2개 선택됨
읽는 글—
업무 보고 — 두 줄 고름(바탕 없이 체크로만) · 일괄 작업 바
import { Button } from "@/components/ui/button"
import { Table, TableBulkBar, TableSelectCell, TableSelectHead } from "@/components/ui/table"

<TableBulkBar count={selected.size} onClear={clearSelection}>
  <Button variant="ghost" size="small" onClick={exportSelected}>내보내기</Button>
  <Button variant="ghost" size="small" onClick={askDeleteSelected}>삭제</Button>
</TableBulkBar>
<Table caption="업무 보고">
  <TableHeader>
    <TableRow>
      <TableSelectHead checked={allChecked ? true : someChecked ? "indeterminate" : false} onCheckedChange={toggleAll} />
      …
    </TableRow>
  </TableHeader>
  <TableBody>
    {reports.map((r) => (
      <TableRow key={r.id}>
        <TableSelectCell label={r.title} checked={selected.has(r.id)} onCheckedChange={() => toggle(r.id)} />
        …
      </TableRow>
    ))}
  </TableBody>
</Table>

768 미만 — List 줄

  • 김하늘개발팀 · 재직12.5일
  • 이도윤디자인팀 · 휴직3일
  • 박서연인사팀 · 재직0.5일
  • 최민준영업팀 · 재직15일
  • 정예린개발팀 · 초대 대기7일
읽는 글—
폰 — 이름 · "개발팀 · 재직" · 12.5일
import { List, ListButtonItem } from "@/components/ui/list"
import { useTableLayout } from "@/components/ui/table"

const layout = useTableLayout()

{layout === "list" ? (
  <List aria-label="사용자">
    {users.map((u) => (
      <ListButtonItem
        key={u.id}
        title={u.name}
        detail={[u.department, u.statusLabel].join(" · ")}
        suffix={<span className="tabular-nums">{formatDays(u.remainingDays)}</span>}
        onClick={() => openUser(u.id)}
      />
    ))}
  </List>
) : (
  <UserTable users={users} />
)}

불러오는 동안 · 비었음 · 실패

사용자

사용자
부서
상태
읽는 글—
줄 스켈레톤 · 비었음 · 실패 + 다시 시도
import { SearchX } from "lucide-react"
import { ResultSection } from "@/components/ui/result-section"
import { TableBody, TableSkeletonRows, TableStatusRow } from "@/components/ui/table"

<TableBody>
  {query.isPending ? (
    <TableSkeletonRows rows={pageSize} columns={[{ width: "40%" }, { width: "30%" }, { align: "end", width: "20%" }]} />
  ) : query.isError ? (
    <TableStatusRow>
      <ResultSection kind="failure" size="medium" title="사용자를 불러오지 못했어요" description="잠시 뒤 다시 시도해주세요."
        primaryAction={{ label: "다시 시도", onClick: () => query.refetch() }} />
    </TableStatusRow>
  ) : rows.length === 0 ? (
    <TableStatusRow>
      <ResultSection kind="empty" size="medium" icon={<SearchX />} title="조건에 맞는 사용자가 없어요" secondaryAction={{ label: "필터 초기화", onClick: resetFilters }} />
    </TableStatusRow>
  ) : (
    rows.map(renderRow)
  )}
</TableBody>

Behavior

인터랙션동작
정렬할 수 있는 머리 누르기그 열로 정렬 — 처음은 숫자 · 날짜 열 내림, 글 열 오름. 다시 누르면 반대로, 또 누르면 다시 반대로(두 단계 — 정렬 없음으로 돌아가지 않는다). 다른 열을 누르면 그 열의 처음 방향으로 옮긴다. 정렬을 바꾸면 첫 쪽으로 돌아간다(Table Pagination)
줄 누르기(누르는 표)상세를 연다. 체크 · ⋮ 를 누른 것은 줄 누르기가 아니다
체크 누르기그 줄을 고르거나 푼다 — 고른 수가 1 이상이면 일괄 작업 바가 뜨고 "{n}개 선택됨" 을 알린다
머리 체크 누르기지금 쪽의 줄을 모두 고르거나 푼다 — 일부 고른 상태면 모두 고른다
일괄 작업 바 ✕고른 것을 모두 푼다 — 초점은 머리 체크로
⋮ 누르기1280 이상 Menu · 미만 Menu Sheet — 동작을 고르면 닫고 실행(menu.md)
Tab문서 차례 — 선택 칸이 첫 열이면 머리 체크 → 정렬 버튼, 그다음 줄마다 체크 · 이름 링크 · ⋮. 칸 사이를 화살표로 옮기지 않는다(표는 grid 가 아니다)
상자 안 스크롤머리는 맨 위에 붙는다
768 미만표 대신 List 줄 — 넘김 대신 끝없이 불러오기

Accessibility

기준검증
WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1)머리 · 칸 fg-neutral 카드 면 위 16.41 · 다크 13.42, 누름 바탕 위 15.48 · 10.32 ✓. 둘째 줄 fg-neutral-subtle 5.50 · 6.09 ✓. 일괄 작업 바 글 · 동작 fg-neutral bg-brand-weak 위 Desk 14.42 · 다크 12.07 · HR 14.43 · 다크 12.09 ✓ — 바 안 동작은 모두 ghost neutral(빨강 글은 라이트 4.44 라 쓰지 않는다)
WCAG 1.4.11 Non-text contrast(≥ 3:1)↑↓ 흐린 쪽 fg-neutral-muted 7.11 · 다크 7.70, 짙은 쪽 fg-neutral ✓. 줄 선은 장식(1.15) — 줄은 글의 자리로 읽힌다. 키보드 포커스 링 Desk 8.38 · 6.10 · HR 8.17 · 6.07 ✓
WCAG 1.4.1 Use of color정렬 방향은 짙은 화살표의 모양(↑ · ↓)과 aria-sort 로도 알린다. 고른 줄은 체크로 ✓
WCAG 1.3.1 Info and relationships<table> · <caption> · <th scope="col"> · 첫 열 <th scope="row"> — 보조 기술이 칸마다 열 이름과 줄 이름을 읽는다. 표처럼 보이는 div 격자를 두지 않는다
WCAG 4.1.3 Status messages고른 수("3개 선택됨") · 정렬 결과("남은 휴가 내림차순으로 정렬했어요.")를 role="status" 로 ✓ — 고른 수의 status 는 바가 없을 때도 두어 처음 고를 때 읽힌다
WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24)정렬 버튼 칸 폭 × 41 · 체크 · ⋮ 44 ✓
WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44)체크 · ⋮ 44 × 44 ✓. 정렬 버튼은 위아래로 44 까지 넓힌다 ✓. 768 미만은 List 줄(46 이상) ✓
ARIA정렬된 열의 머리 칸에 aria-sort="ascending" · "descending"(다른 열에는 달지 않는다). 정렬 버튼의 이름은 열 이름. ⋮ 열 머리는 숨긴 "동작", ⋮ 는 "{줄 이름} 더보기" · aria-haspopup 은 여는 것대로 — 1280 이상 "menu"(Menu) · 미만 "dialog"(Menu Sheet — SEED Menu Sheet 트리거와 같다, Q-011-4). 체크 이름 "{줄 이름} 선택" · 머리 "모두 선택"(일부면 aria-checked="mixed"). 일괄 작업 바는 role="region" · aria-label="선택한 항목"

Do / Don't

✅ Do

  • 머리 41 · 줄 45(썸네일 · 두 줄이면 72), 머리는 본문과 같은 14 · 짙은 글자 · 500.
  • 줄 선은 마지막 줄까지 — 세로 선 · 줄무늬 없이.
  • 숫자 열은 머리까지 오른쪽 · 고정폭 숫자, 돈은 원까지 · 빼기는 U+2212.
  • 정렬할 수 있는 열에 ↑↓ 를 늘, 정렬된 열에 aria-sort.
  • 줄의 동작은 끝 ⋮ 하나, 줄을 누르면 상세.
  • 768 미만은 List 줄 — 표마다 제목 · 설명 · 오른쪽 값을 정해 둔다.
  • 실패는 Result Section failure + 다시 시도.

❌ Don't

  • 작은 대문자 · 회색 머리, 머리 바탕.
  • 정렬된 열에만 화살표 · 마우스를 올려야 보이는 화살표.
  • 폰에서 가로로 밀어 보는 표 · 열을 숨긴 표.
  • 줄마다 늘어놓은 수정 · 삭제 아이콘, 이름 없는 ⋮.
  • 숫자를 왼쪽에 · 고정폭 글꼴로, 돈을 "1.2만" 으로.
  • 실패를 빈 표("데이터가 없어요")로 · 줄을 불투명도로 흐리기.
  • 고른 줄에 바탕을 칠하기(브랜드 옅은 바탕은 일괄 작업 바만) · 정렬 없음으로 돌아가는 세 단계 정렬.
  • div 로 짠 표.

Specification

table.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Table 을 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.

Base

부위상태속성값
rootenabled좌우 여백paddingXspacing-x624px첫 칸 앞 · 끝 칸 뒤만 — 카드 여백 24 와 같아 카드 머리와 첫 열 글자가 한 줄에 선다. 줄 선은 상자 끝까지 긋는다
배경backgroundtransparent카드 면(bg-layer-default)이 바탕 — 표는 카드(card.md) 안에 가장자리까지 붙는다. 바깥 테두리 · 모서리 · 세로 선 · 줄무늬 없음
넘칠 때overflowXauto열이 상자보다 넓으면 표와 넘김 줄이 함께 가로로 밀린다(Table Pagination). 768 미만은 표 대신 List 줄이라 밀지 않는다
captionenabled표 이름 보이기display숨긴 글보이는 제목은 카드 머리(16 / 22 · 700). caption 은 같은 글을 화면 밖에 둔다 — 보조 기술이 표 이름으로 읽는다
headerRowenabled머리 줄 최소 높이minHeight41px선 1 포함 — 위아래 10 + 글 20(SEED 문서 표)
머리 줄 배경backgroundtransparent머리 바탕 없음. 상자 안에서 고정할 때만 카드 면과 같은 bg-layer-default 로 칠해 지나가는 줄을 가린다 — 바탕이 달라 보이지 않는다
머리 줄 아래 선borderBottomColorstroke-neutral-subtle#EDEFF3다크 #353B4D
머리 줄 아래 선 두께borderBottomWidth1px
머리 줄 z-indexzIndex1표 상자 안의 겹침 — 층이 아니다(specs/z-index.md)
머리 줄 자리positionsticky표 상자가 스스로 세로로 스크롤할 때만(위 0) — 페이지 스크롤에는 붙이지 않는다
headerCellenabled머리 칸 상하 여백paddingYspacing-x2_510px
머리 칸 좌우 여백paddingXspacing-x416px
머리 칸 글자색foregroundfg-neutral#1A1F2E다크 #F5F6FA
머리 칸 글자typographytext-t414px / 19px
머리 칸 굵기fontWeight500
머리 칸 줄 높이lineHeight20pxSEED 문서 표 14 / 20 — t4(14 / 19)보다 1 높여 머리 41 · 줄 45 를 SEED 와 맞춘다(사용자 결정 1A). 같은 부위의 lineHeight · fontWeight 가 typography 의 값을 덮는다 — 스펙 JSON 은 typography 에 접어 14 / 20 · 500 하나로 싣는다(Q-011-2)
머리 칸 맞춤textAlignstart
rowenabled줄 최소 높이minHeight45px선 1 포함 — 위아래 12 + 글 20(SEED 문서 표). 한 표 안에서 줄 높이를 섞지 않는다 — 썸네일 · 두 줄 칸이 하나라도 있으면 모든 줄이 72
줄 배경backgroundtransparent
줄 아래 선borderBottomColorstroke-neutral-subtle#EDEFF3다크 #353B4D마지막 줄까지 — 표 끝을 마감한다(SEED 문서 표). 줄무늬 · 세로 선은 두지 않는다
줄 아래 선 두께borderBottomWidth1px
줄 커서cursorpointer누르는 줄만 — 줄을 누르면 상세가 열린다. 보기만 하는 표는 default
줄 전환 대상transitionPropertybackground-color
줄 전환 시간transitionDurationmotion-duration-color-transition150ms
줄 전환 곡선transitionEasingmotion-ease-easingcubic-bezier(0.35, 0, 0.35, 1)
cellenabled칸 상하 여백paddingYspacing-x312px
칸 좌우 여백paddingXspacing-x416px
칸 세로 맞춤alignItemscenter칸의 내용은 세로 가운데
칸 글자색foregroundfg-neutral#1A1F2E다크 #F5F6FA
칸 글자typographytext-t414px / 19px
칸 굵기fontWeight400
칸 줄 높이lineHeight20px머리와 같은 14 / 20 — typography 의 19 를 덮는다(Q-011-2)
detailenabled윗줄 ↔ 둘째 줄gapspacing-x0_52px윗줄 ↔ 둘째 줄
둘째 줄 글자색foregroundfg-neutral-subtle#62697A다크 #A2A8B7
둘째 줄 글자typographytext-t313px / 18px / 400
thumbnailenabled썸네일 크기size48pxImage Frame 1:1 — 모서리는 폭 48 이하라 6(image-frame.yaml). 사람은 Avatar 42, 기관은 Logo Tile 40
썸네일 ↔ 글gapspacing-x312px그림 ↔ 글
badgeenabled상태 배지sizeBadge medium20 · weak — 톤은 상태의 뜻(badge.md). 한 열은 한 변형, 왼쪽 맞춤
checkboxenabled선택 칸 크기size24pxCheckbox large — 열 폭은 24 + 여백(첫 칸이라 앞 24 · 뒤 16). 줄을 누르면 상세라 체크만 고른다
선택 칸 누르는 영역touchTarget44 × 44체크 가운데 — 줄 45 안에 든다. AA ✓ · AAA ✓
선택 칸displaynone고르는 표만 — 첫 열(selection empty · some · all)
moreButtonenabled⋮ 크기size40pxButton ghost · iconOnly · medium — 아이콘 18(lucide ellipsis-vertical). 열 폭 80(앞 16 + 40 + 끝 24 — 끝 열이라 카드 여백), 머리 칸은 글 없이 숨긴 "동작"
⋮ 누르는 영역touchTarget44 × 44보이는 40 바깥으로 2 씩 — 줄 45 안에 든다. AA ✓ · AAA ✓
bulkBarenabled일괄 작업 바 최소 높이minHeight48px표 위 — 고른 수 글(t4 14 · 500 "3개 선택됨") + 동작 버튼(Button ghost small) + 선택 해제 ✕
일괄 작업 바 좌우 여백paddingXspacing-x416px왼쪽 16 · 오른쪽은 끝 버튼이 붙도록 8
바 ↔ 표 머리marginBottomspacing-x28px바 ↔ 표 머리
일괄 작업 바 간격gapspacing-x28px
일괄 작업 바 모서리radiusradius-r312px
일괄 작업 바 배경backgroundbg-brand-weakDesk#E8F1FE다크 #20314EHR#E8F3EC다크 #21352B한 값 — 지금 표 10% · DESIGN 8% 두 벌을 걷는다. 고른 줄에는 바탕을 칠하지 않는다(체크로만)
일괄 작업 바displaynone
actionsactionsButton ghost small · neutral바 안 동작은 모두 ghost neutral — "삭제" 도 회색 글. 빨강은 누른 뒤의 확인창(Alert Dialog criticalSolid)에서. 옅은 브랜드 바탕 위 빨강 글은 라이트 4.44 : 1(2026-10-17 사용자 결정 01T-033 A — SEED ghost 는 fg.neutral 하나)
sortButtonhovered정렬 버튼 배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D
rowhovered줄 배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D누르는 줄만 — 줄 전체(줄 선 위까지). List 의 누름 바탕과 같은 색
sortButtonpressed정렬 버튼 배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D
rowpressed줄 배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D바탕만 — 표의 줄 · 칸은 이웃과 붙어 있어 줄이지 않는다
focusRingfocused링 두께width2px
링 색colorstroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84
링 간격offset-2px안쪽 — 줄 안의 링크 · 버튼 · 정렬 버튼에. 표 상자 가장자리에서 잘리지 않게

row=rich

부위상태속성값
rowenabled줄 최소 높이minHeight72px선 1 포함(SEED 그림) — 썸네일 48 이 세로 가운데. 칸 여백 12 보다 높이가 먼저다
cellenabled칸 상하 여백paddingY0px72 안 세로 가운데 — 48 + 위아래 12 + 선 1 = 73 이라 위아래 여백을 두지 않는다(높이가 먼저, 사용자 결정 1A 의 72 — Q-011-1). 좌우 16 은 그대로

align=end

부위상태속성값
headerCellenabled머리 칸 맞춤textAlignend머리도 오른쪽 — 글 + ↑↓ 묶음이 열 오른쪽 끝에 붙는다
cellenabled칸 맞춤textAlignend
칸 숫자 폭numeralstabular-nums고정폭 숫자 — 자릿수가 위아래로 맞는다. 고정폭 글꼴(mono)은 쓰지 않는다. 돈은 줄이지 않고 원까지, 빼기는 U+2212(−)

sort=sortable

부위상태속성값
sortButtonenabled정렬 버튼 너비width칸 전체머리 칸의 여백을 버튼이 가진다 — 칸 어디를 눌러도 정렬. 누르는 영역 칸 폭 × 41(위아래로 44 까지 넓힌다). 누를 때마다 내림 ↔ 오름(두 단계 — 정렬 없음으로 돌아가지 않는다), 처음 누르면 숫자 · 날짜 열은 내림 · 글 열은 오름(사용자 결정 18A)
글 ↔ 정렬 표시gapspacing-x1_56px글 ↔ ↑↓
정렬 버튼 커서cursorpointer
sortIconenabled정렬 표시 크기size16px↑ 와 ↓ 를 나란히 한 표시 — 두 화살표를 따로 칠한다
정렬 표시 색colorfg-neutral-muted#535866다크 #B7BDCC정렬할 수 있는 열에 늘 보인다 — 둘 다 흐린 색

sort=ascending

부위상태속성값
sortButtonenabled정렬 버튼 너비width칸 전체
글 ↔ 정렬 표시gapspacing-x1_56px
정렬 버튼 커서cursorpointer
sortIconenabled정렬 표시 크기size16px
정렬 표시 색colorfg-neutral#1A1F2E다크 #F5F6FA↑ 만 짙게 — ↓ 는 fg-neutral-muted 그대로. 머리 칸에 aria-sort="ascending"

sort=descending

부위상태속성값
sortButtonenabled정렬 버튼 너비width칸 전체
글 ↔ 정렬 표시gapspacing-x1_56px
정렬 버튼 커서cursorpointer
sortIconenabled정렬 표시 크기size16px
정렬 표시 색colorfg-neutral#1A1F2E다크 #F5F6FA↓ 만 짙게 — ↑ 는 fg-neutral-muted 그대로. 머리 칸에 aria-sort="descending"

selection=empty

부위상태속성값
checkboxenabled선택 칸display보임머리 체크는 꺼짐 — 누르면 지금 쪽의 줄을 모두 고른다

selection=some

부위상태속성값
checkboxenabled선택 칸display머리 체크 일부indeterminate — 누르면 모두 고른다
bulkBarenabled일괄 작업 바display보임"{n}개 선택됨" — 보조 기술에 고른 수를 알린다(role="status")

selection=all

부위상태속성값
checkboxenabled선택 칸display머리 체크 켬누르면 모두 푼다 — 지금 쪽의 줄만
bulkBarenabled일괄 작업 바display보임"{n}개 선택됨"

width=narrow

부위상태속성값
rootenabled넘칠 때overflowXvisible가로로 밀지 않는다
그리는 모양displayList 줄768 미만은 표를 그리지 않는다 — 줄마다 List 의 누르는 줄(list.yaml). 넘김 줄 대신 끝없이 불러오기(infinite-list.yaml)
cellenabled칸 → 줄 자리display제목 · 설명 · 값첫 열 → 제목(t5 16 / 22), 다음 열 1 ~ 2개 → 설명(t3 13 · " · " 로 잇는다), 핵심 숫자 하나 또는 상태 → 오른쪽 값(숫자는 t5 · 고정폭 숫자, 상태는 Badge). 나머지 열은 줄을 눌러 여는 상세에
checkboxenabled선택 칸display앞 체크 24고르는 표는 List 의 여럿 고르기(ListCheckItem) — 체크를 앞에
moreButtonenabled줄 끝 ⋮display줄 끝 ⋮1280 미만이라 Menu Sheet 를 연다(menu.md). 폰 스와이프는 같은 동작의 지름길(swipe-actions.md)

SEED 와 다른 점

  • groo 가 정한 자리가 많다 — SEED 에 표 컴포넌트가 없어 정렬 동작 · aria-sort · 선택 · 일괄 작업 바 · 머리 고정 · 좁은 화면 · 빈 · 실패 · caption · scope 는 groo 규칙이다. 머리 · 줄 · 선 · 글은 문서 사이트 표, ↑↓ · 72 · 숫자 오른쪽 · ⋮ 열은 디자인 그림을 따랐다.
  • 줄 선은 불투명한 stroke-neutral-subtle — SEED 안에서 문서 표(stroke.neutral-muted, 마지막 줄까지)와 토큰 설명("테이블 row = stroke.neutral-subtle")이 갈리고 둘 다 투명도가 있다. groo 의 불투명 짝은 둘 다 stroke-neutral-subtle 이다(v102). 마지막 줄 아래 선은 문서 표를 따른다(Divider 문서의 "마지막 요소 하단에는 표시하지 않습니다" 와 갈리는 자리).
  • 머리 굵기 500 — 문서 표 값. SEED 그림은 굵게(700 안팎) 보이고 Layout 그림은 더 가늘다.
  • 첫 칸 앞 · 끝 칸 뒤 24 — 표가 카드(카드 여백 24) 안에 가장자리까지 붙는다. SEED 문서 표는 사방 16 이다.
  • 상태 배지 왼쪽 — SEED 그림은 가운데. 글 열과 같은 맞춤으로 둔다.
  • 줄 호버 바탕은 bg-layer-default-pressed — 누르는 줄만. SEED 문서 표는 모든 줄에 bg.neutral-weak(한 단계 진하다)다.
  • 768 미만은 List 줄 — SEED 문서 표 · Table Pagination 예제는 감싸개가 가로로 밀린다(사용자 결정 — 가로 스크롤 · 열 숨김은 고르지 않았다).

Migration notes

2026-10-22 — 줄 안의 누르는 것은 셋까지

"셋을 넘지 않는다"(6ca9937 — 10-08 데이터 비교 페이지에 없다)를 List 와 같은 세기 "셋까지 — 넷 이상은 피한다" 로 맞췄다(남은 규칙 R15 — 고르는 법 5B 가 List 의 같은 숫자를 "지양" 으로 맞췄다). 사용자 결정 — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L. 값 변경 없음.

2026-10-17 — 넘김 줄 위 12 · 바 안 동작(03-D 결정 5 · 01T-033)

  • 표 ↔ 넘김 줄 12 는 Table Pagination 이 갖는다(사용자 결정 — 03-D 비교 페이지 결정 5 B). table.md 의 "위 12 · 좌우 24"(10-08 데이터 표시 PR)와 table-pagination.yaml root.marginTop 12(10-08 화면 틀 PR)가 같은 12 를 각자 적어, 웹 ds(table.tsx:164 px-x6 pt-x3 + table-pagination-variants.ts:31 mt-component-default) · 레시피에서 자리에 넣으면 24 였다. 표의 자리는 좌우 24 만 두고(pt-x3 를 뺐다 — 레시피 table.tsx), 넘김 줄의 위 12 는 그대로다. 표 자리가 12 를 갖기(A) · 24 를 스펙으로(C)는 고르지 않았다.
  • 일괄 작업 바의 동작은 모두 ghost neutral(사용자 결정 — 01 기초 앱 · 글자 웹 비교 페이지 01T-033 A). 웹 ds 견본(table.demo.tsx:247-258)의 "삭제" ghost critical 은 bg-brand-weak 위 라이트 4.44 : 1 이었다. 빨강은 누른 뒤의 확인창(Alert Dialog criticalSolid)에서 쓴다. 4.44 를 받아들이기(B) · fg-critical-contrast(C)는 고르지 않았다.

옛 스펙은 table.history/v-pre-app-type-03d.* · table-pagination.history/v-pre-app-type-03d.*.

2026-10-15 — 구현 세션 질문 정리(Q-011-1 · 2 · 4)

사용자가 비교 페이지에서 정리 항목을 "추천대로" 정했다(2026-10-15). 보이는 값은 바뀌지 않는다 — 이미 정한 결정(1A 의 41 · 45 · 72 · 14 / 20)을 YAML 이 빠짐없이 적게 했다.

  • Q-011-1 72 줄의 칸 여백 — row: rich 규칙에 cell.paddingY 0 을 적었다. 레시피 · 웹(h-[72px] py-0)은 이미 그렇다 — 웹 측정 스크립트의 "rich 칸 여백은 재지 않음" 예외를 걷을 수 있다.
  • Q-011-2 t4 + 줄 높이 20 — 같은 부위에 lineHeight · fontWeight 가 따로 있으면 그 값이 typography 를 덮는다(specs/CLAUDE.md). 스펙 JSON 빌더가 그 둘을 typography 에 접어 머리 칸은 14 / 20 · 500, 본문 칸은 14 / 20 · 400 하나로 싣는다 — 따로 적은 값도 그대로 남는다.
  • Q-011-4 ⋮ 의 aria-haspopup — 여는 것대로 1280 이상 "menu" · 미만 "dialog" 로 고쳐 적었다. 레시피(ResponsiveMenuTrigger) · 웹은 이미 그렇다.

2026-10-08 — SEED 문서 표 · 그림으로 다시 쓴다

사용자가 데이터 표시 비교 페이지에서 정했다 — 머리 · 줄은 SEED 문서 표(1A — 머리 41 · 14 / 20 · 500 · 짙은 글자 · 바탕 없음, 줄 45, 줄 선 stroke-neutral-subtle 마지막 줄까지, 썸네일 · 두 줄 줄 72), 정렬 표시는 늘 보이는 ↑↓(2A — 지금 방향만 짙게, 머리는 버튼 + aria-sort), 768 미만은 List 줄(3B). 그리고 "따라오는 것" — 숫자 열 오른쪽 + 고정폭 숫자(머리도) · mono 걷음 · U+2212, 줄 끝 ⋮(보이는 40 · 누르는 44 · "{줄} 더보기", 머리 글 없음), 선택 체크 24, 일괄 작업 바 bg-brand-weak 하나, table · caption · scope · aria-sort, div 격자 표는 table 로, 머리 고정, 줄 스켈레톤 · 표 자리 Result Section + 다시 시도. 작은 대문자 회색 머리 · 머리 바탕(1B) · HR 그대로(1C) · 정렬된 열에만 화살표(2B) · 가로 스크롤 + 첫 열 고정(3A) · 열 숨김(3C)은 고르지 않았다. 스펙을 쓰다 나온 것(같은 비교 페이지 18 · 21) — 정렬은 두 단계(18A — 누를 때마다 내림 ↔ 오름, 숫자 · 날짜 열은 처음 내림, 정렬 없음까지 세 단계(18B)는 고르지 않았다), 고른 줄은 체크로만(21A — 바탕 + 체크(21B)는 고르지 않았다). 옛 스펙은 table.history/v-pre-seed-data.* 에 남겼고, DESIGN.md 의 Data Table 절(v71)은 이 스펙으로 합쳤다.

옛 Table새 Table
머리 40 · 12 / 600 · 대문자 · text-tertiary · 머리 바탕 bg-page41 · 14 / 20 · 500 · fg-neutral · 바탕 없음
칸 사방 8 · 글 15위아래 12 · 좌우 16(첫 칸 앞 · 끝 칸 뒤 24) · 14 / 20
줄 선 border-default · 호버 surface-input 50% · 고른 줄 바탕stroke-neutral-subtle 마지막 줄까지 · 누르는 줄만 bg-layer-default-pressed · 고른 줄은 체크로만
정렬 — 정렬된 열에만 ↑ / ↓, 나머지는 호버 때 ↕ · 오름 → 내림 → 없음정렬할 수 있는 열에 ↑↓ 늘, 지금 방향만 짙게 · 내림 ↔ 오름 두 단계(숫자 · 날짜는 처음 내림)
금액 font-mono tabular-nums고정폭 숫자만 — 고정폭 글꼴 걷음
일괄 작업 바 primary 10%(DESIGN 8%) · 모서리 4bg-brand-weak 한 값 · 48 · 모서리 12
좁은 화면 — 열 숨김 또는 카드768 미만 List 줄

제품은 앱 적용 단계에서 옮긴다(2026-10-08 조사 — HR · Desk 웹은 크로미움에 띄워 쟀고, Desk 앱은 코드로 봤다).

  • HR 표 10개가 shadcn 기본값 그대로 — 머리 48 · 14 / 500 · muted-foreground #787878(흰 바탕 4.42 · 다크 4.13 — 4.5 미달), 칸 16, 줄 65(두 줄 73), 줄 선 #EEEEEE · 마지막 줄 선 없음([&_tr:last-child]:border-0), 호버 바탕은 표 면과 1.03(shared/ui/shadcn/table.tsx:64 · 79 · 93). 사용자(features/admin-users-management/ui/UserTable.tsx:91-306) · 휴가 내역(features/vacation-history/ui/VacationHistoryContent.tsx:86-280) · 회비(features/culture-dues/ui/DuesTableContent.tsx:77-306) · 신청 내역(features/vacation-application/ui/ApplicationTableContent.tsx:47-184) · 업무 보고(features/work-report/ui/ReportTable.tsx:137-399) · 업무 코드(features/admin-work-code/ui/WorkCodeList.tsx:57 · WorkDivisionList.tsx:57) · 위젯 표(features/dashboard/ui/widgets/UserVacationStats/UserVacationStatsContent.tsx:21-50) · 대화상자 안 표(features/admin-holiday/ui/BulkGenerateHolidayDialog.tsx:327 · features/work-report/ui/ExcelImportDialog.tsx:332). 머리 41 · 줄 45 로.
  • HR 표 — 정렬 · aria-sort · scope · caption 0 — 열 머리 "Email" 이 영어다(UserTable.tsx). 정렬할 수 있는 열을 표마다 정하고 ↑↓ 를 단다.
  • HR 숫자가 왼쪽 — 회비 표의 금액 · 총액이 text-align: start · 기본 숫자이고 입금 파랑 / 출금 빨강 글자색이다(DuesTableContent.tsx). 오른쪽 + 고정폭 숫자로 — 입금 · 출금은 글("입금" · "출금")이나 부호로도 알린다.
  • HR 줄 ⋮ 에 이름이 없다 — 32 × 32 · 아이콘 16 · 이름 ""(Tab 순서 "초대" → "" → "" …, UserTable.tsx · DuesTableContent.tsx:200 · ApplicationTableContent.tsx:142 · VacationHistoryContent.tsx:131 · ReportTable.tsx:354-363). Button ghost iconOnly medium 40(누르는 44) · "{줄 이름} 더보기" 로.
  • HR 360 폭은 가로 스크롤만 — 표 최소 폭 800 ~ 1500(min-w-[…])이 246 ~ 296 상자 안에서 밀린다. 768 미만 대체 레이아웃 0(useIsMobile + Table 파일 0). 표마다 List 줄 대응을 정해 바꾼다.
  • HR 빈 표 세 가지 — antd Empty(사용자 표 UserTable.tsx:20 · 254 — 기본 영어 문구로 보인다) · shadcn Empty(회비) · 표 안 높이 250 Empty(신청 내역 ApplicationTableContent.tsx:62-75), 실패는 QueryAsyncBoundary 의 "오류가 발생했습니다."(다시 시도 0). Result Section medium + 다시 시도로.
  • HR 위젯에서만 머리 고정 — 휴가 내역 위젯(VacationHistoryContent.tsx:94 · 176)만 stickyHeader. 상자가 스스로 스크롤하는 표는 모두 머리를 붙인다.
  • Desk 웹 <table> 2 — 가져오기 미리보기(widgets/data-transfer/ui/DataImportSection.tsx:495-591 — 머리 12 · 700, 금액만 오른쪽, 오류 줄 불투명도 .4 · 중복 줄 .5 :530-537 — v106 "불투명도로 흐리지 않는다")와 내보내기 미리보기(DataExportSection.tsx:593-634 · 738-755). 오류 · 중복 줄은 흐리지 않고 상태 배지("오류" · "중복")로 가른다.
  • Desk 웹 div 격자 표 3 — 일별 시세(features/stock/ui/daily-quote-table.tsx:70-212 — 머리 11 / 600 · 줄 35.8 · 하락률 "-1.00%" 가 ASCII 빼기 :190) · 호가(pages/stocks/ui/TossStocksPage.tsx:115-336) · 체결(:338-445 — 머리 10.5 / 600 · 줄 25). 표 의미(table · role=row)가 0 이다. <table> 로, 등락률은 ▲ · ▼ + 값으로.
  • Desk 앱 Material DataTable 2 — 가져오기 · 내보내기(features/import/presentation/import_view.dart:510- · features/export/presentation/export_screen.dart:679-) — 머리 36 · 줄 32 ~ 44 · 가로 스크롤 · 숫자 맞춤 지정 0(DataColumn(numeric:)). 폰이라 List 줄로.
  • 넘김 — HR 표 아래 넘김 줄은 Table Pagination 의 Migration notes 에 있다.

이 페이지