# Statistic 값 하나와 그것을 부르는 이름. 타이틀과 강조된 숫자 두 부품으로 현재 수치를 인쇄합니다. > 원본: `03_ASSET_GUIDE/catalog/statistic.js` · CSS: `03_ASSET_GUIDE/styles/elements/statistic.css` > kind `component` · category `display` · status `stable` · specSource `measured` > 형제 블록: `atom-statistic-group` (KPI 행) --- ## 1. 구성 부품은 둘입니다. ``` ┌──────────────────────┐ │ 진행 중 작업 │ ② │ 132 │ ③ └──────────────────────┘ ① ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | ① | Container | 필수 | `atom-statistic` | 자기 바닥을 가진 타일입니다. 흰 표면 · 8px 모서리 · 20px 여백. | | ② | Label | 필수 | `atom-statistic__label` | 무엇을 센 수인지 말합니다. 문장 부호로 끝내지 않습니다. | | ③ | Value | 필수 | `atom-statistic__value` | 그 수입니다. 라벨보다 크고 굵게 표시합니다. | | — | Group | 선택 | `atom-statistic-group` | 여러 개를 나란히 놓는 KPI 행. 형제 블록입니다. | 부품 둘은 **내용**인 Label 과 Value 를 말합니다. Container 는 둘을 담기만 하고 별도 정보를 갖지 않습니다. > [!NOTE] > **아이콘 · 증감 · 스파크라인은 이 엘리먼트에 없습니다** > 참고한 화면에는 카드마다 아이콘과 하단 보조 문구가 함께 있었지만, 이 엘리먼트는 > **타이틀과 숫자 두 부품**으로 정의했습니다. 부품을 추가하면 이미 쓰인 곳이 스펙과 > 어긋나므로, 증감이나 추이가 필요하면 별도 항목으로 정의합니다. --- ## 2. 종류 / 크기 ### Variant | variant | 표면 | 쓰는 곳 | |---|---|---| | `surface` | 흰 배경 · 8px 모서리 · 20px 여백 · 1px 경계 | 페이지에 바로 놓일 때 (기본) | | `plain` | 없음 | 이미 표면 위일 때 — [Grid](./grid.md) 셀, [Chart](./chart.md) 헤더, 다른 카드 안 | 경계는 그림자 대신 **1px 선**으로 그립니다. [Shadow](../foundation/shadow.md) 규칙에 따라 상시 노출되는 표면은 `--atom-border-subtle` 로 경계를 그립니다. 그림자를 쓰면 떠 있는 요소처럼 보이고, 다크 모드에서는 그림자만으로 그린 경계가 잘 보이지 않습니다. 배경은 `--atom-bg-layer` 입니다. 라이트에서 `#ffffff`, 다크에서 `#26272e` 로 풀립니다. `#fff` 로 고정하면 다크 모드에서도 흰 카드로 남습니다. > [!WARNING] > **표면 위에 표면을 겹치지 않습니다** > 카드 안이나 표 셀 안에 `surface` 를 두면 바닥이 두 겹이 됩니다. 그런 자리에는 > `plain` 을 씁니다. ### Size | size | Value | Label | 여백 | 쓰는 곳 | |---|---|---|---|---| | `small` | 20px (`--atom-t8-size`) | 12px | 16px | 좁은 패널. 표 안이면 `--variant_plain` 과 함께 | | `medium` | 24px (`--atom-t9-size`) | 13px | 20px | KPI 행 (기본) | | `large` | 32px (`--atom-t10-size`) | 14px | 24px | 화면이 이 수 하나를 위한 것일 때 | Value 는 **600** 입니다. [Typography](../foundation/typography.md) 의 제목 굵기는 500 이지만, 숫자는 흐린 라벨과 구분되도록 더 굵게 둡니다. ### Align 기본은 `start` 입니다. `end` 는 숫자를 오른쪽 정렬한 열에 세로로 쌓을 때 씁니다. 이때는 [숫자 폭](#숫자-폭)도 함께 바꿉니다. ### KPI 행 여러 개를 나란히 놓을 때는 `atom-statistic-group` 을 씁니다. Statistic 자신은 두 부품으로 남고, 배치는 형제 블록이 맡습니다. [Chip](./chip.md) 의 `atom-chip-group` 과 같은 구조입니다. **등폭 열**입니다. flex 행으로 두면 가장 긴 숫자가 폭을 가져가서, 네 개의 수가 서로 다른 위치에서 시작합니다. 640px 아래에서는 2열로 접힙니다. 4열이면 한 칸이 70px 이 되어 라벨이 들어가지 않습니다. 타일 사이 간격은 **12px** 입니다. 타일이 이미 20px 여백을 갖고 있어서, 간격을 여백과 같은 폭으로 두면 타일들이 한 행으로 묶여 보이지 않습니다. ### 숫자 폭 Value 는 **프로포셔널 숫자**가 기본이고 `--tabular` 가 옵트인입니다. 이 저장소의 다른 숫자 슬롯([Grid](./grid.md) 셀, 축 눈금, [Time Chip](./time-chip.md) 칩)과 **반대**입니다. 세로로 정렬되는 숫자는 자릿수가 맞아야 하므로 등폭을 씁니다. 단독으로 놓인 24px 숫자에 등폭을 적용하면 `1` 이 `0` 과 같은 폭을 차지해 `121` 의 간격이 벌어집니다. | 상황 | 설정 | |---|---| | KPI 행처럼 나란히 놓임 | 기본 (프로포셔널) | | 세로로 쌓아 자릿수를 맞춰야 함 | `--tabular` | ### 클래스 계약 `styles/elements/statistic.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | Prop | 값 | 클래스 | |---|---|---| | `size` | `small` · `medium` · `large` | `--size_small` · — · `--size_large` | | `variant` | `surface` · `plain` | — · `--variant_plain` | | `align` | `start` · `center` · `end` | — · `--align_center` · `--align_end` | | `tabular` | `true` | `--tabular` | | `state` | `ready` · `loading` · `empty` | — · `[data-state="loading"]` · `[data-state="empty"]` | | Group `cols` | `2` · `3` · `4` | `atom-statistic-group--cols_2` · `--cols_3` · `--cols_4` | 상태 선택자: `[data-state="loading"]` · `[data-state="empty"]` 로컬 커스텀 프로퍼티: `--atom-statistic-cols` (Group 의 열 수. `--cols_*` 클래스가 설정하고, 640px 아래 미디어 쿼리가 2 로 덮어씁니다) --- ## 3. 상태 | 상태 | 표시 | 선택자 | |---|---|---| | 기본 | 값 그대로 | — | | `loading` | 값 자리에 스켈레톤. **숫자 폭**으로 잡습니다. 문구 폭으로 잡으면 값이 들어올 때 타일 크기가 바뀝니다 | `[data-state="loading"]` | | `empty` | 대시 `—`. **`0` 을 쓰지 않습니다** | `[data-state="empty"]` | `0` 은 측정 결과이고, 대시는 데이터가 없음을 뜻합니다. 데이터가 없을 때 `0` 을 쓰면 측정값으로 오해됩니다. --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `label` | `string` | | ● | 무엇을 센 수인지입니다. 문장 부호로 끝내지 않습니다. | | `value` | `string \| number` | | ● | 그 수입니다. 서식(천 단위 구분·단위·줄임)은 넘기기 전에 끝냅니다. | | `size` | `"small" \| "medium" \| "large"` | `"medium"` | | 값과 라벨의 크기입니다. | | `variant` | `"surface" \| "plain"` | `"surface"` | | 자기 바닥을 가질지입니다. 이미 표면 위면 `plain`. | | `align` | `"start" \| "center" \| "end"` | `"start"` | | 두 줄의 정렬입니다. | | `tabular` | `boolean` | `false` | | 값에 등폭 숫자를 씁니다. 세로로 쌓아 자릿수를 맞출 때만. | | `state` | `"ready" \| "loading" \| "empty"` | `"ready"` | | 값 자리의 표시입니다. | | `valueLabel` | `string` | | | 줄인 값의 온전한 수입니다. `aria-label` 로 붙습니다. | `atom-statistic-group` 은 `cols` (`2 \| 3 \| 4`, 기본 `4`) 하나를 받습니다. > [!WARNING] > **원본 `registry.json` 의 기본값 충돌 경고 (`props-default-conflicts-css`)** > registry `problems` 는 "align: Props says `center`, the stylesheet header says `start`" · > "variant: Props says `plain`, the stylesheet header says `surface`" 라고 보고합니다. > 그러나 `catalog/statistic.js` 의 Props 표 원문은 위와 같이 `variant` 기본 `"surface"`, `align` 기본 `"start"` 로 > CSS 헤더(`surface is the default` · `start is the default`)와 일치합니다. > registry `api.props` 가 Type 열의 `\|` 를 열 구분자로 잘라 읽어 Default 열이 한 칸씩 밀린 결과입니다 > (`size` 의 Default 가 `"medium"`, Description 이 `` `"medium"` `` 으로 들어간 것도 같은 원인). ### Parts (compound) | Part | 요소 | 설명 | |---|---|---| | `Statistic` | `
` | Container. 위 Props 를 받고 Label · Value 를 순서대로 렌더합니다. | | `Statistic.Group` | `
` | KPI 행. `cols` (`2 \| 3 \| 4`, 기본 `4`). | --- ## 5. Component tokens | 대상 | 속성 | Token | Value | |---|---|---|---| | Container | background | `{color.bg.layer}` | 라이트 `#ffffff` · 다크 `#26272e` | | Container | radius | `{radius.r2}` | 8px | | Container | 경계 | `{color.border.subtle}` | `inset 0 0 0 1px` — 그림자 아님 | | Container | padding | `{space.x5}` | 20px (small `{space.x4}` 16 · large `{space.x6}` 24) | | Container | gap | — | 2px — 두 줄이 한 덩어리로 붙게 | | Label | font | — | 13px / 1.5 (small 12 · large 14) | | Label | color | `{color.fg.neutral-muted}` | 6.99:1 | | Value | font | — | 24px / 600 · `-0.01em` (small 20 · large 32 `-0.02em`) | | Value | color | `{color.fg.neutral}` | 17.19:1 | | Value | 숫자 폭 | — | `normal` (프로포셔널) · `--tabular` 로 전환 | | Value · loading | background | `{color.bg.neutral-weak}` | 폭 `3.5ch` · radius `{radius.r1}` | | Value · empty | color | `{color.fg.neutral-subtle}` | 대시 전용 · 굵기 500 | | Group | gap | `{space.x3}` | 12px — 타일이 자기 여백을 가지므로 | | Group | 열 | — | `--atom-statistic-cols` · 기본 4 · 640px 아래 2 (토큰 아님) | > [!NOTE] > **`--atom-statistic-cols` 는 토큰이 아닙니다** > `tokens.css` 에 없습니다. 이 엘리먼트 안에서만 쓰이는 로컬 커스텀 프로퍼티이고, > [Tree](./tree.md) 의 `--atom-tree-indent` 와 같은 성격입니다. > 크기 20 · 24 · 32px 도 [Typography](../foundation/typography.md) 스케일에서 고른 값을 > 리터럴로 적은 것입니다. 그 스케일은 아직 토큰이 아닙니다. ```json { "statistic": { "background": { "$type": "color", "$value": "{color.bg.layer}" }, "radius": { "$type": "dimension", "$value": "{radius.r2}" }, "border": { "$type": "color", "$value": "{color.border.subtle}" }, "padding": { "$type": "dimension", "default": { "$value": "{space.x5}" }, "small": { "$value": "{space.x4}" }, "large": { "$value": "{space.x6}" } }, "gap": { "$type": "dimension", "$value": "2px" }, "label": { "color": { "$type": "color", "$value": "{color.fg.neutral-muted}" }, "font-size": { "$type": "dimension", "default": { "$value": "13px" }, "small": { "$value": "12px" }, "large": { "$value": "14px" } } }, "value": { "color": { "$type": "color", "$value": "{color.fg.neutral}" }, "font-weight": { "$type": "fontWeight", "$value": 600 }, "font-size": { "$type": "dimension", "default": { "$value": "24px" }, "small": { "$value": "20px" }, "large": { "$value": "32px" } }, "loading-bg": { "$type": "color", "$value": "{color.bg.neutral-weak}" }, "loading-radius": { "$type": "dimension", "$value": "{radius.r1}" }, "empty-color": { "$type": "color", "$value": "{color.fg.neutral-subtle}" } }, "group": { "gap": { "$type": "dimension", "$value": "{space.x3}" } } } } ``` --- ## 6. 접근성 - **읽는 순서** — 라벨이 DOM 에서 값보다 앞입니다. 스크린리더가 "진행 중 작업, 132" 로 읽습니다. 값을 먼저 두면 "132, 진행 중 작업" 이 되어 값의 의미가 나중에 전달됩니다. - **줄임말** — `12.9K` · `4.2M` 처럼 줄인 값은 `aria-label` 로 온전한 수를 함께 답니다. 스크린리더가 "12.9 케이" 로 읽으면 값이 전달되지 않습니다. - **갱신 알림** — 실시간으로 바뀌는 값은 `aria-live="polite"` 로 묶습니다. 초점은 그대로인데 숫자만 바뀌면 그 변화가 전달되지 않습니다. - **loading** — 스켈레톤 상태에서는 `aria-busy="true"` 를 답니다. 투명한 글자가 읽히면 "000" 같은 가짜 값이 전달됩니다. - **대비** — Value 는 `--atom-fg-neutral`(17.19:1), Label 은 `--atom-fg-neutral-muted`(6.99:1) 로 둘 다 본문 기준 4.5:1 을 넘습니다. Label 을 `--atom-fg-neutral-subtle`(2.57:1) 로 내리지 마세요. 라벨이 읽히지 않으면 값의 의미를 알 수 없습니다. - **타일 경계** — `--atom-border-subtle` 는 흰 배경에서 1.18:1 이고 WCAG 1.4.11 의 3:1 에 못 미칩니다. 이 선은 표면을 나누는 장식이고 상태를 전달하지 않으므로 이 기준의 대상이 아닙니다. 타일의 정보는 라벨과 값이 전달합니다. --- ## 7. 사용 가이드 ### 숫자 하나에 차트를 쓰지 않습니다 값이 하나면 차트의 축·기준선·격자가 정보를 더하지 않습니다. 수를 그대로 표시합니다. - ✅ **Do** 현재 값 하나는 Statistic 으로 인쇄합니다. - ❌ **Don't** 막대 하나짜리 차트를 그리지 않습니다. 조각 두 개짜리 원형도 같습니다. - ⚠️ **Caution** 값이 시간에 따라 변한 추이를 보여줘야 하면 [Chart](./chart.md) 의 선 차트를 씁니다. ### 라벨은 문장 부호로 끝내지 않습니다 라벨과 값은 위아래로 붙어 있어 한 덩어리로 읽힙니다. 라벨 끝에 콜론을 붙이지 않습니다. - ✅ **Do** "진행 중 작업" — 무엇을 센 수인지 명사로 끝냅니다. - ❌ **Don't** "진행 중 작업 수 :" 처럼 콜론이나 "수" 를 덧붙이지 않습니다. ### 한 화면에 너무 많이 두지 않습니다 KPI 행에는 네 개까지 둡니다. 그보다 많으면 [Grid](./grid.md) 로 표를 만듭니다. 한 화면의 핵심 수는 **하나**입니다. 그 수만 `large` 로 두고 나머지는 `medium` 으로 둡니다. ### 관련 컴포넌트 - [Chart](./chart.md) — 값이 여러 개이거나 시간에 따른 변화를 보여줄 때. - [Grid](./grid.md) — 정확한 수를 여러 개 보여줄 때. - [Badge](./badge.md) — 수 대신 상태를 표시할 때. - [Section Title](./section-title.md) — KPI 행 위의 제목. --- ## 8. 예시 ```tsx ``` ```html
진행 중 작업 132
누적 처리 12.9K
반려 —
합계 1,284
프로젝트 18
```