Data Table
로그·이벤트·정책 목록처럼 대량의 행 데이터를 다룹니다. 정렬·선택·행 액션을 갖춘 콘솔의 핵심 컴포넌트입니다. 단순 표는 Table 을 씁니다.
1. Default (Sortable)
헤더 클릭 정렬. 정렬 중인 열은 .sorted(.desc). 숫자 열은 .num.
| 시간 | 심각도 | 이벤트 | 대상 | 횟수 | 상태 |
|---|---|---|---|---|---|
| 09:41:12 | Critical | 랜섬웨어 행위 탐지 | PC-2041 · 김민준 | 3 | 차단 |
| 09:40:58 | Medium | 미승인 USB 연결 | PC-1187 · 이서연 | 1 | 검토 필요 |
| 09:38:03 | Low | 정책 동기화 완료 | Server-03 | 12 | 정상 |
| 09:36:47 | High | 알려진 악성코드 차단 | PC-0932 · 박지훈 | 2 | 차단 |
HTML
<div class="tbl-wrap">
<table class="tbl">
<thead><tr><th class="sortable sorted desc" aria-sort="descending">시간</th><th class="sortable">심각도</th><th>이벤트</th><th class="sortable">대상</th><th class="num sortable">횟수</th><th>상태</th></tr></thead>
<tbody>
<tr><td>09:41:12</td><td><span class="tag critical">Critical</span></td><td><a href="#" class="link">랜섬웨어 행위 탐지</a></td><td>PC-2041 · 김민준</td><td class="num">3</td><td><span class="indicator danger">차단</span></td></tr>
<tr><td>09:40:58</td><td><span class="tag medium">Medium</span></td><td><a href="#" class="link">미승인 USB 연결</a></td><td>PC-1187 · 이서연</td><td class="num">1</td><td><span class="indicator warn">검토 필요</span></td></tr>
<tr><td>09:38:03</td><td><span class="tag low">Low</span></td><td><a href="#" class="link">정책 동기화 완료</a></td><td>Server-03</td><td class="num">12</td><td><span class="indicator ok">정상</span></td></tr>
<tr><td>09:36:47</td><td><span class="tag high">High</span></td><td><a href="#" class="link">알려진 악성코드 차단</a></td><td>PC-0932 · 박지훈</td><td class="num">2</td><td><span class="indicator danger">차단</span></td></tr>
</tbody>
</table>
</div>CSS · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator
/* JS Design System v0.6 · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator · 토큰(var(--*))은 assets/style.css :root 참고 */
.checkbox{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;cursor:pointer;line-height:1.4}
.checkbox input{appearance:none;-webkit-appearance:none;flex:none;width:16px;height:16px;margin:2px 0 0;border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--bg-surface);display:grid;place-items:center;cursor:inherit;transition:background var(--motion-fast),border-color var(--motion-fast)}
.checkbox input:hover{border-color:var(--border-strong)}
.checkbox input:checked,.checkbox input:indeterminate,.checkbox input.mixed{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:indeterminate::after,.checkbox input.mixed::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:disabled{opacity:.4;cursor:not-allowed}
.checkbox input:disabled+span{color:var(--text-disabled)}
.checkbox small{display:block;color:var(--text-tertiary);font-size:12px;font-weight:400}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.tbl-wrap{overflow-x:auto;border:1px solid var(--border-default);border-radius:var(--radius-lg);background:var(--bg-surface);width:100%}
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin:0}
.tbl th{text-align:left;font-weight:600;color:var(--text-tertiary);font-size:12px;height:44px;padding:0 16px;border-bottom:1px solid var(--border-default);white-space:nowrap;background:var(--bg-subtle)}
.tbl td{height:52px;padding:12px 16px;border-bottom:1px solid var(--border-default);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover td{background:var(--bg-subtle)}
.tbl tr.on td{background:var(--accent-subtle)}
.tbl tr.disabled td{color:var(--text-disabled)}
.tbl.compact td{height:44px;padding:8px 16px;font-size:14px}
.tbl.compact th{height:40px}
.tbl.zebra tbody tr:nth-child(even) td{background:var(--bg-subtle)}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:"";display:inline-block;width:14px;height:14px;margin-left:6px;vertical-align:-3px;opacity:.45;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sortable:hover{color:var(--text-primary)}
.tbl th.sorted{color:var(--accent-text)}
.tbl th.sorted::after{opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sorted.desc::after{-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl .num{text-align:right;}
.tbl td svg{vertical-align:middle}
.tbl .check{width:44px;padding-right:0}
.tbl .check .checkbox{vertical-align:middle}
.tbl .actions{text-align:right;white-space:nowrap}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.tbl.kv th{width:200px;height:auto;padding:12px 16px;font-size:14px;color:var(--text-secondary);font-weight:500;border-right:1px solid var(--border-default);white-space:normal;vertical-align:top}
.tbl.kv td{height:auto}
.tbl td.empty{display:table-cell;text-align:center;color:var(--text-tertiary);height:auto;padding:24px 16px;font-size:14px;border:none;border-radius:0;background:transparent}
.tbl .link{color:var(--accent-text);text-decoration:none;font-weight:500}
.tbl .link:hover{text-decoration:underline}
.action-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--accent-subtle);border-bottom:1px solid var(--border-default);font-size:14px}
.action-bar b{color:var(--accent-text)}
.action-bar .spacer{flex:1}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile .tag{position:absolute;top:12px;right:12px}
.status-item .indicator{font-weight:600}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { DataTable, Indicator, Tag, type DataTableColumn } from "@jiran/ds-react";
type Row = { id: string; time: string; sev: "critical" | "high" | "medium" | "low"; event: string; target: string; count: number; status: "blocked" | "review" | "ok" };
const ROWS: Row[] = [
{ id: "1", time: "09:41:12", sev: "critical", event: "랜섬웨어 행위 탐지", target: "PC-2041 · 김민준", count: 3, status: "blocked" },
{ id: "2", time: "09:40:58", sev: "medium", event: "미승인 USB 연결", target: "PC-1187 · 이서연", count: 1, status: "review" },
{ id: "3", time: "09:38:03", sev: "low", event: "정책 동기화 완료", target: "Server-03", count: 12, status: "ok" },
{ id: "4", time: "09:36:47", sev: "high", event: "알려진 악성코드 차단", target: "PC-0932 · 박지훈", count: 2, status: "blocked" },
];
const SEV_LABEL = { critical: "Critical", high: "High", medium: "Medium", low: "Low" };
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
const STATUS = { blocked: ["차단", "danger"], review: ["검토 필요", "warn"], ok: ["정상", "ok"] } as const;
const COLUMNS: DataTableColumn<Row>[] = [
{ key: "time", header: "시간", sortable: true },
{ key: "sev", header: "심각도", sortable: true, sortValue: (r) => SEV_RANK[r.sev], render: (r) => <Tag tone={r.sev}>{SEV_LABEL[r.sev]}</Tag> },
{ key: "event", header: "이벤트", render: (r) => <a href="#" className="link">{r.event}</a> },
{ key: "target", header: "대상", sortable: true },
{ key: "count", header: "횟수", num: true, sortable: true },
{ key: "status", header: "상태", render: (r) => <Indicator tone={STATUS[r.status][1]}>{STATUS[r.status][0]}</Indicator> },
];
export default function Example() {
return <DataTable columns={COLUMNS} rows={ROWS} rowKey={(r) => r.id} defaultSort={{ key: "time", dir: "desc" }} />;
}2. Selectable + Action Bar
체크박스 열(.check) + 선택 시 상단 액션 바. 선택 행은 .on.
| 정책 | 적용 그룹 | 대상 | 수정 | 사용 | |
|---|---|---|---|---|---|
| 외부 USB 차단 | 전체 | 1,284 | 2026-09-06 | ||
| 스크립트 실행 제한 | 개발팀 제외 | 1,102 | 2026-09-02 | ||
| 원격 접속 허용 목록 | 인프라팀 | 48 | 2026-08-21 |
HTML
<div class="tbl-wrap">
<div class="action-bar"><b>2개 선택</b><span class="spacer"></span><button type="button" class="btn sm secondary">허용 처리</button><button type="button" class="btn sm secondary"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"/><path d="M7 11l5 5l5 -5"/><path d="M12 4l0 12"/></svg>내보내기</button><button type="button" class="btn sm danger secondary">삭제</button></div>
<table class="tbl">
<thead><tr><th class="check"><label class="checkbox"><input type="checkbox" class="mixed" aria-label="전체 선택"></label></th><th>정책</th><th>적용 그룹</th><th class="num">대상</th><th>수정</th><th>사용</th></tr></thead>
<tbody>
<tr class="on"><td class="check"><label class="checkbox"><input type="checkbox" checked aria-label="선택"></label></td><td>외부 USB 차단</td><td>전체</td><td class="num">1,284</td><td>2026-09-06</td><td><label class="switch sm"><input type="checkbox" role="switch" checked aria-label="사용"></label></td></tr>
<tr class="on"><td class="check"><label class="checkbox"><input type="checkbox" checked aria-label="선택"></label></td><td>스크립트 실행 제한</td><td>개발팀 제외</td><td class="num">1,102</td><td>2026-09-02</td><td><label class="switch sm"><input type="checkbox" role="switch" checked aria-label="사용"></label></td></tr>
<tr><td class="check"><label class="checkbox"><input type="checkbox" aria-label="선택"></label></td><td>원격 접속 허용 목록</td><td>인프라팀</td><td class="num">48</td><td>2026-08-21</td><td><label class="switch sm"><input type="checkbox" role="switch" aria-label="사용"></label></td></tr>
</tbody>
</table>
</div>CSS · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator
/* JS Design System v0.6 · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator · 토큰(var(--*))은 assets/style.css :root 참고 */
.checkbox{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;cursor:pointer;line-height:1.4}
.checkbox input{appearance:none;-webkit-appearance:none;flex:none;width:16px;height:16px;margin:2px 0 0;border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--bg-surface);display:grid;place-items:center;cursor:inherit;transition:background var(--motion-fast),border-color var(--motion-fast)}
.checkbox input:hover{border-color:var(--border-strong)}
.checkbox input:checked,.checkbox input:indeterminate,.checkbox input.mixed{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:indeterminate::after,.checkbox input.mixed::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:disabled{opacity:.4;cursor:not-allowed}
.checkbox input:disabled+span{color:var(--text-disabled)}
.checkbox small{display:block;color:var(--text-tertiary);font-size:12px;font-weight:400}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.tbl-wrap{overflow-x:auto;border:1px solid var(--border-default);border-radius:var(--radius-lg);background:var(--bg-surface);width:100%}
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin:0}
.tbl th{text-align:left;font-weight:600;color:var(--text-tertiary);font-size:12px;height:44px;padding:0 16px;border-bottom:1px solid var(--border-default);white-space:nowrap;background:var(--bg-subtle)}
.tbl td{height:52px;padding:12px 16px;border-bottom:1px solid var(--border-default);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover td{background:var(--bg-subtle)}
.tbl tr.on td{background:var(--accent-subtle)}
.tbl tr.disabled td{color:var(--text-disabled)}
.tbl.compact td{height:44px;padding:8px 16px;font-size:14px}
.tbl.compact th{height:40px}
.tbl.zebra tbody tr:nth-child(even) td{background:var(--bg-subtle)}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:"";display:inline-block;width:14px;height:14px;margin-left:6px;vertical-align:-3px;opacity:.45;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sortable:hover{color:var(--text-primary)}
.tbl th.sorted{color:var(--accent-text)}
.tbl th.sorted::after{opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sorted.desc::after{-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl .num{text-align:right;}
.tbl td svg{vertical-align:middle}
.tbl .check{width:44px;padding-right:0}
.tbl .check .checkbox{vertical-align:middle}
.tbl .actions{text-align:right;white-space:nowrap}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.tbl.kv th{width:200px;height:auto;padding:12px 16px;font-size:14px;color:var(--text-secondary);font-weight:500;border-right:1px solid var(--border-default);white-space:normal;vertical-align:top}
.tbl.kv td{height:auto}
.tbl td.empty{display:table-cell;text-align:center;color:var(--text-tertiary);height:auto;padding:24px 16px;font-size:14px;border:none;border-radius:0;background:transparent}
.tbl .link{color:var(--accent-text);text-decoration:none;font-weight:500}
.tbl .link:hover{text-decoration:underline}
.action-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--accent-subtle);border-bottom:1px solid var(--border-default);font-size:14px}
.action-bar b{color:var(--accent-text)}
.action-bar .spacer{flex:1}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile .tag{position:absolute;top:12px;right:12px}
.status-item .indicator{font-weight:600}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { useState } from "react";
import { ActionBar, Button, DataTable, Icon, Switch, type DataTableColumn } from "@jiran/ds-react";
type Policy = { id: string; name: string; group: string; targets: number; updated: string; enabled: boolean };
const ROWS: Policy[] = [
{ id: "p1", name: "외부 USB 차단", group: "전체", targets: 1284, updated: "2026-09-06", enabled: true },
{ id: "p2", name: "스크립트 실행 제한", group: "개발팀 제외", targets: 1102, updated: "2026-09-02", enabled: true },
{ id: "p3", name: "원격 접속 허용 목록", group: "인프라팀", targets: 48, updated: "2026-08-21", enabled: false },
];
const COLUMNS: DataTableColumn<Policy>[] = [
{ key: "name", header: "정책" },
{ key: "group", header: "적용 그룹" },
{ key: "targets", header: "대상", num: true, render: (r) => r.targets.toLocaleString() },
{ key: "updated", header: "수정" },
{ key: "enabled", header: "사용", render: (r) => <Switch size="sm" defaultChecked={r.enabled} aria-label="사용" /> },
];
export default function Example() {
const [selected, setSelected] = useState(new Set(["p1", "p2"]));
return (
<DataTable
columns={COLUMNS}
rows={ROWS}
rowKey={(r) => r.id}
selectable
selected={selected}
onSelectedChange={setSelected}
actionBar={(sel) => (
<ActionBar count={sel.size}>
<Button size="sm" variant="secondary">허용 처리</Button>
<Button size="sm" variant="secondary" leading={<Icon name="download" />}>내보내기</Button>
<Button size="sm" variant="danger-secondary">삭제</Button>
</ActionBar>
)}
/>
);
}3. Compact
로그 뷰어처럼 행이 많을 때 행 높이 44. 시간·IP 는 모노스페이스(.log-row).
| 시간 | 레벨 | 소스 | 메시지 |
|---|---|---|---|
| 2026-09-07 09:41:12.031 | CRIT | 10.0.4.21 | process.create blocked: rundll32.exe → cryptor.dll |
| 2026-09-07 09:41:11.870 | WARN | 10.0.4.21 | usb.attach vendor=0x0781 product=0x5583 |
| 2026-09-07 09:41:09.114 | INFO | 10.0.0.3 | policy.sync ok (rev 2041) |
| 2026-09-07 09:41:08.552 | INFO | 10.0.0.3 | agent.checkin 1284/1284 |
HTML
<div class="tbl-wrap">
<table class="tbl compact">
<thead><tr><th>시간</th><th>레벨</th><th>소스</th><th>메시지</th></tr></thead>
<tbody>
<tr class="log-row"><td>2026-09-07 09:41:12.031</td><td><span class="tag critical sm">CRIT</span></td><td>10.0.4.21</td><td>process.create blocked: rundll32.exe → cryptor.dll</td></tr>
<tr class="log-row"><td>2026-09-07 09:41:11.870</td><td><span class="tag medium sm">WARN</span></td><td>10.0.4.21</td><td>usb.attach vendor=0x0781 product=0x5583</td></tr>
<tr class="log-row"><td>2026-09-07 09:41:09.114</td><td><span class="tag info sm">INFO</span></td><td>10.0.0.3</td><td>policy.sync ok (rev 2041)</td></tr>
<tr class="log-row"><td>2026-09-07 09:41:08.552</td><td><span class="tag info sm">INFO</span></td><td>10.0.0.3</td><td>agent.checkin 1284/1284</td></tr>
</tbody>
</table>
</div>CSS · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator
/* JS Design System v0.6 · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator · 토큰(var(--*))은 assets/style.css :root 참고 */
.checkbox{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;cursor:pointer;line-height:1.4}
.checkbox input{appearance:none;-webkit-appearance:none;flex:none;width:16px;height:16px;margin:2px 0 0;border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--bg-surface);display:grid;place-items:center;cursor:inherit;transition:background var(--motion-fast),border-color var(--motion-fast)}
.checkbox input:hover{border-color:var(--border-strong)}
.checkbox input:checked,.checkbox input:indeterminate,.checkbox input.mixed{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:indeterminate::after,.checkbox input.mixed::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:disabled{opacity:.4;cursor:not-allowed}
.checkbox input:disabled+span{color:var(--text-disabled)}
.checkbox small{display:block;color:var(--text-tertiary);font-size:12px;font-weight:400}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.tbl-wrap{overflow-x:auto;border:1px solid var(--border-default);border-radius:var(--radius-lg);background:var(--bg-surface);width:100%}
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin:0}
.tbl th{text-align:left;font-weight:600;color:var(--text-tertiary);font-size:12px;height:44px;padding:0 16px;border-bottom:1px solid var(--border-default);white-space:nowrap;background:var(--bg-subtle)}
.tbl td{height:52px;padding:12px 16px;border-bottom:1px solid var(--border-default);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover td{background:var(--bg-subtle)}
.tbl tr.on td{background:var(--accent-subtle)}
.tbl tr.disabled td{color:var(--text-disabled)}
.tbl.compact td{height:44px;padding:8px 16px;font-size:14px}
.tbl.compact th{height:40px}
.tbl.zebra tbody tr:nth-child(even) td{background:var(--bg-subtle)}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:"";display:inline-block;width:14px;height:14px;margin-left:6px;vertical-align:-3px;opacity:.45;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sortable:hover{color:var(--text-primary)}
.tbl th.sorted{color:var(--accent-text)}
.tbl th.sorted::after{opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sorted.desc::after{-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl .num{text-align:right;}
.tbl td svg{vertical-align:middle}
.tbl .check{width:44px;padding-right:0}
.tbl .check .checkbox{vertical-align:middle}
.tbl .actions{text-align:right;white-space:nowrap}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.tbl.kv th{width:200px;height:auto;padding:12px 16px;font-size:14px;color:var(--text-secondary);font-weight:500;border-right:1px solid var(--border-default);white-space:normal;vertical-align:top}
.tbl.kv td{height:auto}
.tbl td.empty{display:table-cell;text-align:center;color:var(--text-tertiary);height:auto;padding:24px 16px;font-size:14px;border:none;border-radius:0;background:transparent}
.tbl .link{color:var(--accent-text);text-decoration:none;font-weight:500}
.tbl .link:hover{text-decoration:underline}
.action-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--accent-subtle);border-bottom:1px solid var(--border-default);font-size:14px}
.action-bar b{color:var(--accent-text)}
.action-bar .spacer{flex:1}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile .tag{position:absolute;top:12px;right:12px}
.status-item .indicator{font-weight:600}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { DataTable, Tag, type DataTableColumn } from "@jiran/ds-react";
type Log = { id: number; time: string; level: "critical" | "medium" | "info"; src: string; msg: string };
const LEVEL = { critical: "CRIT", medium: "WARN", info: "INFO" };
const ROWS: Log[] = [
{ id: 1, time: "2026-09-07 09:41:12.031", level: "critical", src: "10.0.4.21", msg: "process.create blocked: rundll32.exe → cryptor.dll" },
{ id: 2, time: "2026-09-07 09:41:11.870", level: "medium", src: "10.0.4.21", msg: "usb.attach vendor=0x0781 product=0x5583" },
{ id: 3, time: "2026-09-07 09:41:09.114", level: "info", src: "10.0.0.3", msg: "policy.sync ok (rev 2041)" },
{ id: 4, time: "2026-09-07 09:41:08.552", level: "info", src: "10.0.0.3", msg: "agent.checkin 1284/1284" },
];
const COLUMNS: DataTableColumn<Log>[] = [
{ key: "time", header: "시간" },
{ key: "level", header: "레벨", render: (r) => <Tag tone={r.level} size="sm">{LEVEL[r.level]}</Tag> },
{ key: "src", header: "소스" },
{ key: "msg", header: "메시지" },
];
export default function Example() {
return <DataTable compact columns={COLUMNS} rows={ROWS} rowKey={(r) => String(r.id)} rowProps={() => ({ log: true })} />;
}4. Empty
결과 없음. 조건 초기화 등 다음 행동을 제시.
| 시간 | 심각도 | 이벤트 | 대상 |
|---|---|---|---|
조건에 맞는 이벤트가 없습니다. 필터 초기화 | |||
HTML
<div class="tbl-wrap">
<table class="tbl">
<thead><tr><th>시간</th><th>심각도</th><th>이벤트</th><th>대상</th></tr></thead>
<tbody><tr><td colspan="4" class="empty"><div class="empty">조건에 맞는 이벤트가 없습니다. <a href="#" class="link">필터 초기화</a></div></td></tr></tbody>
</table>
</div>CSS · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator
/* JS Design System v0.6 · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator · 토큰(var(--*))은 assets/style.css :root 참고 */
.checkbox{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;cursor:pointer;line-height:1.4}
.checkbox input{appearance:none;-webkit-appearance:none;flex:none;width:16px;height:16px;margin:2px 0 0;border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--bg-surface);display:grid;place-items:center;cursor:inherit;transition:background var(--motion-fast),border-color var(--motion-fast)}
.checkbox input:hover{border-color:var(--border-strong)}
.checkbox input:checked,.checkbox input:indeterminate,.checkbox input.mixed{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:indeterminate::after,.checkbox input.mixed::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:disabled{opacity:.4;cursor:not-allowed}
.checkbox input:disabled+span{color:var(--text-disabled)}
.checkbox small{display:block;color:var(--text-tertiary);font-size:12px;font-weight:400}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.tbl-wrap{overflow-x:auto;border:1px solid var(--border-default);border-radius:var(--radius-lg);background:var(--bg-surface);width:100%}
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin:0}
.tbl th{text-align:left;font-weight:600;color:var(--text-tertiary);font-size:12px;height:44px;padding:0 16px;border-bottom:1px solid var(--border-default);white-space:nowrap;background:var(--bg-subtle)}
.tbl td{height:52px;padding:12px 16px;border-bottom:1px solid var(--border-default);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover td{background:var(--bg-subtle)}
.tbl tr.on td{background:var(--accent-subtle)}
.tbl tr.disabled td{color:var(--text-disabled)}
.tbl.compact td{height:44px;padding:8px 16px;font-size:14px}
.tbl.compact th{height:40px}
.tbl.zebra tbody tr:nth-child(even) td{background:var(--bg-subtle)}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:"";display:inline-block;width:14px;height:14px;margin-left:6px;vertical-align:-3px;opacity:.45;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sortable:hover{color:var(--text-primary)}
.tbl th.sorted{color:var(--accent-text)}
.tbl th.sorted::after{opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sorted.desc::after{-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl .num{text-align:right;}
.tbl td svg{vertical-align:middle}
.tbl .check{width:44px;padding-right:0}
.tbl .check .checkbox{vertical-align:middle}
.tbl .actions{text-align:right;white-space:nowrap}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.tbl.kv th{width:200px;height:auto;padding:12px 16px;font-size:14px;color:var(--text-secondary);font-weight:500;border-right:1px solid var(--border-default);white-space:normal;vertical-align:top}
.tbl.kv td{height:auto}
.tbl td.empty{display:table-cell;text-align:center;color:var(--text-tertiary);height:auto;padding:24px 16px;font-size:14px;border:none;border-radius:0;background:transparent}
.tbl .link{color:var(--accent-text);text-decoration:none;font-weight:500}
.tbl .link:hover{text-decoration:underline}
.action-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--accent-subtle);border-bottom:1px solid var(--border-default);font-size:14px}
.action-bar b{color:var(--accent-text)}
.action-bar .spacer{flex:1}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile .tag{position:absolute;top:12px;right:12px}
.status-item .indicator{font-weight:600}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { DataTable } from "@jiran/ds-react";
type Row = { id: string; time: string; sev: string; event: string; target: string };
export default function Example() {
return (
<DataTable<Row>
columns={[{ key: "time", header: "시간" }, { key: "sev", header: "심각도" }, { key: "event", header: "이벤트" }, { key: "target", header: "대상" }]}
rows={[]}
rowKey={(r) => r.id}
empty={<div className="empty">조건에 맞는 이벤트가 없습니다. <a href="#" className="link">필터 초기화</a></div>}
/>
);
}Props
HTML 에서는 클래스와 속성으로, React(@jiran/ds-react)에서는 같은 이름의 prop(variant · size …)으로 변형·상태를 지정합니다.
| Prop / Class | Type · Values | Default | Description |
|---|---|---|---|
| .tbl-wrap | container | — | 보더 + radius/lg + 가로 스크롤. 안에 .action-bar 와 table.tbl |
| .tbl | table | — | 기본 행 52. .compact 44 · .zebra 줄무늬 |
| th.sortable / .sorted(.desc) | header | — | 정렬 가능 / 정렬 중. aria-sort |
| tr.on / .disabled | row | — | 선택 / 비활성 |
| td.num / .check / .actions / .empty | cell | — | 숫자 우측 정렬 / 체크박스 열 / 행 액션 / 빈 상태 |
| .action-bar | div | — | 선택 건수 + 일괄 액션 |
Usage
React: @jiran/ds-react 패키지를 설치하고 style.css 를 한 번 import 하면 위 React 탭 코드를 그대로 쓸 수 있습니다(React Package). HTML: assets/style.css 를 연결하고(일부만 필요하면 아래 컴포넌트 CSS 복사), 동작이 필요하면 assets/ds.js 를 함께 넣습니다 — 이 페이지의 프리뷰가 그 스크립트로 동작합니다. 색은 전부 토큰(var(--accent) 등)을 참조하므로 --accent-* 만 제품 컬러로 바꾸면 됩니다.
React @jiran/ds-react
npm i @jiran/ds-react
// main.tsx — 앱 진입점에서 한 번
import "@jiran/ds-react/style.css";
// 컴포넌트
import { DataTable, ActionBar } from "@jiran/ds-react";HTML
<!-- 1) 토큰 + 전체 컴포넌트 스타일 한 번에 연결 -->
<link rel="stylesheet" href="assets/style.css">
<!-- 동작(드롭다운·탭·달력·표 정렬/선택·토스트 등) — 순수 JS 한 파일. React 제품은 불필요 -->
<script src="assets/ds.js" defer></script>
<!-- 2) 또는 아래 "Component CSS" 만 프로젝트 CSS 에 붙이고,
:root 토큰은 Foundations › Design Token 에서 복사 -->Component CSS .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator
/* JS Design System v0.6 · .tbl-wrap, .tbl, .action-bar, .tag, .checkbox, .indicator · 토큰(var(--*))은 assets/style.css :root 참고 */
.checkbox{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;cursor:pointer;line-height:1.4}
.checkbox input{appearance:none;-webkit-appearance:none;flex:none;width:16px;height:16px;margin:2px 0 0;border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--bg-surface);display:grid;place-items:center;cursor:inherit;transition:background var(--motion-fast),border-color var(--motion-fast)}
.checkbox input:hover{border-color:var(--border-strong)}
.checkbox input:checked,.checkbox input:indeterminate,.checkbox input.mixed{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l5 5l10 -10%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:indeterminate::after,.checkbox input.mixed::after{content:"";width:12px;height:12px;background:var(--accent-on);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E") center/contain no-repeat}
.checkbox input:disabled{opacity:.4;cursor:not-allowed}
.checkbox input:disabled+span{color:var(--text-disabled)}
.checkbox small{display:block;color:var(--text-tertiary);font-size:12px;font-weight:400}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.tbl-wrap{overflow-x:auto;border:1px solid var(--border-default);border-radius:var(--radius-lg);background:var(--bg-surface);width:100%}
.tbl{width:100%;border-collapse:collapse;font-size:14px;margin:0}
.tbl th{text-align:left;font-weight:600;color:var(--text-tertiary);font-size:12px;height:44px;padding:0 16px;border-bottom:1px solid var(--border-default);white-space:nowrap;background:var(--bg-subtle)}
.tbl td{height:52px;padding:12px 16px;border-bottom:1px solid var(--border-default);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover td{background:var(--bg-subtle)}
.tbl tr.on td{background:var(--accent-subtle)}
.tbl tr.disabled td{color:var(--text-disabled)}
.tbl.compact td{height:44px;padding:8px 16px;font-size:14px}
.tbl.compact th{height:40px}
.tbl.zebra tbody tr:nth-child(even) td{background:var(--bg-subtle)}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:"";display:inline-block;width:14px;height:14px;margin-left:6px;vertical-align:-3px;opacity:.45;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 9l4 -4l4 4m-4 -4v14%22 /%3E%3Cpath d=%22M21 15l-4 4l-4 -4m4 4v-14%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sortable:hover{color:var(--text-primary)}
.tbl th.sorted{color:var(--accent-text)}
.tbl th.sorted::after{opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 11l-6 -6%22 /%3E%3Cpath d=%22M6 11l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl th.sorted.desc::after{-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M18 13l-6 6%22 /%3E%3Cpath d=%22M6 13l6 6%22 /%3E%3C/svg%3E") center/contain no-repeat}
.tbl .num{text-align:right;}
.tbl td svg{vertical-align:middle}
.tbl .check{width:44px;padding-right:0}
.tbl .check .checkbox{vertical-align:middle}
.tbl .actions{text-align:right;white-space:nowrap}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.tbl.kv th{width:200px;height:auto;padding:12px 16px;font-size:14px;color:var(--text-secondary);font-weight:500;border-right:1px solid var(--border-default);white-space:normal;vertical-align:top}
.tbl.kv td{height:auto}
.tbl td.empty{display:table-cell;text-align:center;color:var(--text-tertiary);height:auto;padding:24px 16px;font-size:14px;border:none;border-radius:0;background:transparent}
.tbl .link{color:var(--accent-text);text-decoration:none;font-weight:500}
.tbl .link:hover{text-decoration:underline}
.action-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--accent-subtle);border-bottom:1px solid var(--border-default);font-size:14px}
.action-bar b{color:var(--accent-text)}
.action-bar .spacer{flex:1}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile .tag{position:absolute;top:12px;right:12px}
.status-item .indicator{font-weight:600}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}