이호영 이호영 07-29
상태 표시를 각진 태그 하나로 통일
색만 달랐던 알약 배지는 흑백 출력과 색약 상황에서 "운영 중"과 "개방불가"가
똑같이 보였다. 모양이 상태를 함께 담도록 바꾼다.

- ui/StatusGlyph: 90도와 45도만 쓴 10px 글리프 8종
- ui/StatusChip: 오른쪽 위 모서리를 잘라낸 태그 + 왼쪽 2px 컬러 바.
  표에서 바가 세로로 정렬돼 목록을 훑기 쉬워진다
- codes/tone: tone에서 칩 클래스와 기본 글리프를 파생

적용: 채널 준비 상태, 권리확인/권리처리 판정, 담당자 구분, 단계 태그,
타임라인(이모지 제거), 업무단계 스트립 완료 표시(글꼴 체크 제거 - OS마다
굵기와 곡률이 달랐다)

필터 칩과 아바타는 둥근 모양 그대로 뒀다. 둥글면 누를 수 있는 것,
각지면 사실을 알려주는 것으로 구분된다.

Co-Authored-By: Claude Opus 5 (1M context) 
@ecde204b49e2a916965e0f986e3ed2f97b842ca0
frontend/src/codes/tone.ts
--- frontend/src/codes/tone.ts
+++ frontend/src/codes/tone.ts
@@ -8,7 +8,9 @@
  * 못 찾아 지워버리기 때문이다. 반드시 문자열 전체가 소스에 보여야 한다.
  */
 
-/** 뱃지: 옅은 배경 + 진한 글씨. 처리결과·담당자 구분이 쓴다. */
+import type { GlyphKind } from '../ui/StatusGlyph'
+
+/** 뱃지: 옅은 배경 + 진한 글씨. 예전 표기. 새 화면은 StatusChip을 쓴다. */
 const BADGE: Record<string, string> = {
   slate: 'bg-slate-100 text-slate-700',
   gray: 'bg-gray-100 text-gray-700',
@@ -50,3 +52,49 @@
 export function barClass(tone: string | null | undefined): string {
   return (tone && BAR[tone]) || BAR_FALLBACK
 }
+
+/**
+ * 칩: 왼쪽 컬러 바 + 옅은 배경 + 진한 글씨. StatusChip이 쓴다.
+ *
+ * 배경만 칠하던 예전 뱃지와 달리 왼쪽에 2px 바를 세우는 이유는, 표에서 상태 칸이
+ * 세로로 늘어설 때 그 바들이 한 줄로 정렬돼 눈이 빠르게 훑고 내려갈 수 있어서다.
+ */
+const CHIP: Record<string, string> = {
+  slate: 'border-slate-400 bg-slate-50 text-slate-700',
+  gray: 'border-gray-400 bg-gray-50 text-gray-700',
+  blue: 'border-blue-500 bg-blue-50 text-blue-800',
+  sky: 'border-sky-500 bg-sky-50 text-sky-800',
+  indigo: 'border-indigo-500 bg-indigo-50 text-indigo-800',
+  violet: 'border-violet-500 bg-violet-50 text-violet-800',
+  emerald: 'border-emerald-600 bg-emerald-50 text-emerald-800',
+  amber: 'border-amber-500 bg-amber-50 text-amber-800',
+  orange: 'border-orange-500 bg-orange-50 text-orange-800',
+  red: 'border-red-500 bg-red-50 text-red-800',
+}
+
+const CHIP_FALLBACK = 'border-gray-300 bg-gray-50 text-gray-600'
+
+/**
+ * tone이 정해지면 글리프 모양도 따라 정해진다. 코드표에 색만 지정해도 알맞은 모양이
+ * 붙게 하려는 것이다 - 코드 한 줄 추가할 때마다 모양까지 고르게 하면 금방 어긋난다.
+ */
+const GLYPH: Record<string, GlyphKind> = {
+  emerald: 'done',
+  blue: 'progress',
+  sky: 'progress',
+  indigo: 'progress',
+  violet: 'progress',
+  amber: 'caution',
+  orange: 'caution',
+  red: 'blocked',
+  slate: 'idle',
+  gray: 'idle',
+}
+
+export function chipClass(tone: string | null | undefined): string {
+  return (tone && CHIP[tone]) || CHIP_FALLBACK
+}
+
+export function glyphOf(tone: string | null | undefined): GlyphKind {
+  return (tone && GLYPH[tone]) || 'idle'
+}
frontend/src/components/ContactsPage.tsx
--- frontend/src/components/ContactsPage.tsx
+++ frontend/src/components/ContactsPage.tsx
@@ -11,7 +11,7 @@
   type MemberImportReport,
 } from '../api/client'
 import { useCodes } from '../codes/useCodes'
-import { badgeClass } from '../codes/tone'
+import StatusChip from '../ui/StatusChip'
 import HelpButton from '../help/HelpButton'
 
 // 구분 5종(표시명·뱃지색·필터·드롭다운)은 예전에 이 파일 안에서만 네 번 따로 적혀 있었다.
@@ -391,13 +391,11 @@
               visible.map((contact) => (
                 <tr key={contact.id} className="border-b border-gray-100 last:border-b-0">
                   <td className="px-3 py-2">
-                    <span
-                      className={`rounded-full px-2 py-0.5 text-xs font-medium ${badgeClass(
-                        categoryOf(contact.category)?.attrs.tone as string | undefined,
-                      )}`}
-                    >
-                      {categoryOf(contact.category)?.label ?? contact.category}
-                    </span>
+                    <StatusChip
+                      label={categoryOf(contact.category)?.label ?? contact.category}
+                      tone={categoryOf(contact.category)?.attrs.tone as string | undefined}
+                      glyph="note"
+                    />
                   </td>
                   <td className="px-3 py-2 font-medium text-gray-900">{contact.name}</td>
                   <td className="px-3 py-2 text-gray-600">{contact.affiliation ?? '-'}</td>
frontend/src/components/Dashboard.tsx
--- frontend/src/components/Dashboard.tsx
+++ frontend/src/components/Dashboard.tsx
@@ -7,6 +7,7 @@
 } from '../api/client'
 import { useStages } from '../codes/stage'
 import { barClass } from '../codes/tone'
+import StatusChip from '../ui/StatusChip'
 import HelpButton from '../help/HelpButton'
 
 /**
@@ -163,9 +164,7 @@
   return (
     <div className="space-y-1 text-xs text-gray-500">
       <p>{lawyer}</p>
-      <span className="inline-block rounded-full bg-violet-50 px-2 py-0.5 text-[11px] text-violet-700">
-        {stages.symbol(stage)} {stages.label(stage)}
-      </span>
+      <StatusChip label={stages.label(stage)} tone="violet" prefix={stages.symbol(stage)} />
     </div>
   )
 }
@@ -614,9 +613,15 @@
                     <td className="px-2 py-2 text-gray-500 tabular-nums">{row.orgNo}</td>
                     <td className="px-2 py-2 font-medium">{row.orgName}</td>
                     <td className="px-2 py-2">
-                      <span className="inline-block rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">
-                        {stage === null ? '단계 미지정' : `${stages.symbol(stage)} ${stages.label(stage)}`}
-                      </span>
+                      {stage === null ? (
+                        <StatusChip label="단계 미지정" tone="gray" glyph="idle" />
+                      ) : (
+                        <StatusChip
+                          label={stages.label(stage)}
+                          tone="violet"
+                          prefix={stages.symbol(stage)}
+                        />
+                      )}
                     </td>
                     <td className="px-2 py-2 text-gray-600">{row.mjName ?? '-'}</td>
                     <td className="px-2 py-2 text-gray-600">{row.lawyerName ?? '-'}</td>
frontend/src/components/OrgOverview.test.tsx
--- frontend/src/components/OrgOverview.test.tsx
+++ frontend/src/components/OrgOverview.test.tsx
@@ -402,7 +402,9 @@
       const currentButton = screen.getByText('예비검토').closest('button') as HTMLElement
       const futureButton = screen.getByText('변호사 배당').closest('button') as HTMLElement
 
-      expect(within(doneButton).getByText('✓')).toBeTruthy()
+      // 완료 단계는 단계 번호 대신 체크 글리프(svg)를 보여준다.
+      expect(doneButton.querySelector('svg')).toBeTruthy()
+      expect(within(doneButton).queryByText('1')).toBeNull()
       expect(within(currentButton).getByText('진행중')).toBeTruthy()
       expect(within(futureButton).getByText('-')).toBeTruthy()
       expect(futureButton).toBeDisabled()
frontend/src/components/OrgOverview.tsx
--- frontend/src/components/OrgOverview.tsx
+++ frontend/src/components/OrgOverview.tsx
@@ -12,6 +12,7 @@
   type PostView,
 } from '../api/client'
 import { useStages } from '../codes/stage'
+import StatusGlyph from '../ui/StatusGlyph'
 import HelpButton from '../help/HelpButton'
 import ChannelFiles from './ChannelFiles'
 import ChannelPosts from './ChannelPosts'
@@ -467,7 +468,7 @@
                     } ${clickable ? 'cursor-pointer hover:bg-blue-100' : 'cursor-default disabled:opacity-100'}`}
                   >
                     <span
-                      className={`flex h-6 w-6 items-center justify-center rounded-full border text-xs ${
+                      className={`flex h-6 w-6 items-center justify-center border text-xs tabular-nums ${
                         isDone
                           ? 'border-emerald-500 bg-emerald-500 text-white'
                           : isCurrent
@@ -475,7 +476,8 @@
                             : 'border-gray-300 text-gray-500'
                       }`}
                     >
-                      {isDone ? '✓' : n}
+                      {/* 완료 표시는 글꼴의 ✓ 대신 각진 글리프를 쓴다. ✓는 OS마다 굵기와 곡률이 달라진다. */}
+                      {isDone ? <StatusGlyph kind="check" size={12} /> : n}
                     </span>
                     <span
                       className={`text-[11px] leading-tight ${
frontend/src/components/ProcessBoard.tsx
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.tsx
@@ -15,6 +15,7 @@
   type ProcessStatusFilter,
   type ProcessingRequest,
 } from '../api/client'
+import StatusChip from '../ui/StatusChip'
 import { useCodes, useSplitByFirstRow } from '../codes/useCodes'
 
 const PAGE_SIZE = 30
@@ -173,13 +174,11 @@
   const done = !!doneCode && value === doneCode.code
   const pendingLabel = statuses.find((status) => status.attrs.done !== true)?.label ?? ''
   return (
-    <span
-      className={`rounded-full px-2 py-0.5 text-xs font-medium ${
-        done ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-500'
-      }`}
-    >
-      {done ? doneCode.label : pendingLabel}
-    </span>
+    <StatusChip
+      label={done ? doneCode.label : pendingLabel}
+      tone={done ? 'emerald' : 'gray'}
+      glyph={done ? 'done' : 'idle'}
+    />
   )
 }
 
frontend/src/components/ProjectsPage.test.tsx
--- frontend/src/components/ProjectsPage.test.tsx
+++ frontend/src/components/ProjectsPage.test.tsx
@@ -100,7 +100,9 @@
 
     const table = within(await screen.findByTestId('project-table'))
     expect(table.getByText('국제방송교류재단')).toBeTruthy()
-    expect(table.getByText('5. 법률검토(권리확인)')).toBeTruthy()
+    // 단계 태그는 번호와 이름이 각각 다른 span이다(번호는 tabular-nums로 세로 정렬된다).
+    expect(table.getByText('5.')).toBeTruthy()
+    expect(table.getByText('법률검토(권리확인)')).toBeTruthy()
     expect(table.getByText('2026-07-20')).toBeTruthy()
     expect(table.getByText('(3)')).toBeTruthy()
   })
frontend/src/components/ProjectsPage.tsx
--- frontend/src/components/ProjectsPage.tsx
+++ frontend/src/components/ProjectsPage.tsx
@@ -15,6 +15,7 @@
 } from '../api/client'
 import { useStages } from '../codes/stage'
 import { useCodes } from '../codes/useCodes'
+import StatusChip from '../ui/StatusChip'
 import HelpButton from '../help/HelpButton'
 
 /**
@@ -300,9 +301,15 @@
                       </button>
                     </td>
                     <td className="px-2 py-2 text-gray-600">
-                      {row.org.stage === null
-                        ? '단계 미지정'
-                        : `${row.org.stage}. ${stages.label(row.org.stage)}`}
+                      {row.org.stage === null ? (
+                        <StatusChip label="단계 미지정" tone="gray" glyph="idle" />
+                      ) : (
+                        <StatusChip
+                          label={stages.label(row.org.stage)}
+                          tone="violet"
+                          prefix={`${row.org.stage}.`}
+                        />
+                      )}
                     </td>
                     <td className="px-2 py-2 text-gray-600">{row.org.mjName ?? '-'}</td>
                     <td className="px-2 py-2 text-gray-600">{row.org.lawyerName ?? '-'}</td>
frontend/src/components/ReviewBoard.tsx
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
@@ -14,7 +14,7 @@
   type ReviewItem,
 } from '../api/client'
 import { NOTE_NO_PROCESSING } from '../codes/messages'
-import { badgeClass } from '../codes/tone'
+import StatusChip from '../ui/StatusChip'
 import { attr, useCodes, useScopedCodes, useSplitByFirstRow } from '../codes/useCodes'
 
 const PAGE_SIZE = 30
@@ -160,17 +160,13 @@
 }
 
 function ReviewResultBadge({ value }: { value: string | null }) {
-  // 뱃지 색은 코드의 tone에서 온다. 코드표에 없는 값이면 badgeClass가 회색으로 떨어뜨린다.
+  // 색과 모양이 모두 코드의 tone에서 파생된다. 코드표에 없는 값이면 회색 빈 사각으로 떨어진다.
   const results = useCodes('REVIEW_RESULT')
   if (!value) {
-    return (
-      <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-500">미판정</span>
-    )
+    return <StatusChip label="미판정" tone="gray" glyph="idle" />
   }
   const tone = attr(results.find((candidate) => candidate.code === value), 'tone')
-  return (
-    <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${badgeClass(tone)}`}>{value}</span>
-  )
+  return <StatusChip label={value} tone={tone} />
 }
 
 /** 기관관리 상세의 권리확인 탭. 목록↔상세 두 화면을 이 컴포넌트 하나에서 전환한다. */
frontend/src/components/StatusBadge.tsx
--- frontend/src/components/StatusBadge.tsx
+++ frontend/src/components/StatusBadge.tsx
@@ -1,4 +1,6 @@
 import type { OrgStatus } from '../api/client'
+import StatusChip from '../ui/StatusChip'
+import type { GlyphKind } from '../ui/StatusGlyph'
 
 export const STATUS_LABELS: Record<OrgStatus, string> = {
   INFO_PENDING: '정보 대기',
@@ -7,7 +9,7 @@
   ACTIVE: '운영 중',
 }
 
-/** 접힌 목록 등 좁은 자리에서 뱃지 대신 쓰는 상태 점 색 */
+/** 접힌 목록 등 뱃지를 놓을 자리가 없을 때 쓰는 상태 점 색 */
 export const STATUS_DOT: Record<OrgStatus, string> = {
   INFO_PENDING: 'bg-gray-300',
   READY: 'bg-blue-500',
@@ -15,19 +17,24 @@
   ACTIVE: 'bg-emerald-500',
 }
 
-const LABELS = STATUS_LABELS
+/**
+ * 채널 준비 상태는 네 단계가 순서대로 이어진다. 그래서 색뿐 아니라 모양도 그 순서를 담는다.
+ * 빈 사각(담당자 없음) → 절반(생성 가능) → 삼각(한쪽만 생성돼 손봐야 함) → 채운 사각(운영 중).
+ */
+const TONE: Record<OrgStatus, string> = {
+  INFO_PENDING: 'gray',
+  READY: 'blue',
+  PARTIAL: 'amber',
+  ACTIVE: 'emerald',
+}
 
-const STYLES: Record<OrgStatus, string> = {
-  INFO_PENDING: 'bg-gray-100 text-gray-600',
-  READY: 'bg-blue-50 text-blue-700',
-  PARTIAL: 'bg-amber-50 text-amber-700',
-  ACTIVE: 'bg-emerald-50 text-emerald-700',
+const GLYPH: Record<OrgStatus, GlyphKind> = {
+  INFO_PENDING: 'idle',
+  READY: 'progress',
+  PARTIAL: 'caution',
+  ACTIVE: 'done',
 }
 
 export default function StatusBadge({ status }: { status: OrgStatus }) {
-  return (
-    <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${STYLES[status]}`}>
-      {LABELS[status]}
-    </span>
-  )
+  return <StatusChip label={STATUS_LABELS[status]} tone={TONE[status]} glyph={GLYPH[status]} />
 }
frontend/src/components/Timeline.tsx
--- frontend/src/components/Timeline.tsx
+++ frontend/src/components/Timeline.tsx
@@ -1,6 +1,8 @@
 import { useEffect, useState } from 'react'
 import { getTimeline, type Org, type TimelineEvent } from '../api/client'
 import { useStages, type Stages } from '../codes/stage'
+import StatusChip from '../ui/StatusChip'
+import StatusGlyph, { type GlyphKind } from '../ui/StatusGlyph'
 
 const dateFormatter = new Intl.DateTimeFormat('ko-KR', {
   year: 'numeric',
@@ -55,10 +57,18 @@
   return groups
 }
 
-const EVENT_META: Record<TimelineEvent['type'], { dot: string; icon: string; badge: string; badgeClass: string }> = {
-  STAGE: { dot: 'bg-blue-600', icon: '🚩', badge: '단계 변경', badgeClass: 'bg-blue-50 text-blue-700' },
-  FILE: { dot: 'bg-amber-500', icon: '📎', badge: '자료 등록', badgeClass: 'bg-amber-50 text-amber-700' },
-  MEMO: { dot: 'bg-emerald-600', icon: '✏️', badge: '업무메모', badgeClass: 'bg-emerald-50 text-emerald-700' },
+/**
+ * 이벤트 종류별 색·글리프. 이모지를 쓰지 않는 이유는 두 가지다. 하나는 OS마다 그림이 달라
+ * 같은 화면이 사람마다 다르게 보인다는 것, 다른 하나는 둥근 이모지가 이 화면의 각진 상태
+ * 태그와 어울리지 않는다는 것이다. 대신 같은 글리프 세트를 쓴다.
+ */
+const EVENT_META: Record<
+  TimelineEvent['type'],
+  { dot: string; glyph: GlyphKind; badge: string; tone: string }
+> = {
+  STAGE: { dot: 'bg-blue-600', glyph: 'progress', badge: '단계 변경', tone: 'blue' },
+  FILE: { dot: 'bg-amber-500', glyph: 'document', badge: '자료 등록', tone: 'amber' },
+  MEMO: { dot: 'bg-emerald-600', glyph: 'note', badge: '업무메모', tone: 'emerald' },
 }
 
 /** STAGE 이벤트의 title(단계 번호 문자열)을 "5. 법률검토(권리확인) 진행" 형태로 바꾼다. */
@@ -160,15 +170,13 @@
                     <li key={`${group.key}-${i}`} className="relative">
                       <span
                         aria-hidden="true"
-                        className={`absolute -left-[1.65rem] flex h-5 w-5 items-center justify-center rounded-full text-[10px] leading-none text-white ${meta.dot}`}
+                        className={`absolute -left-[1.6rem] flex h-[18px] w-[18px] items-center justify-center text-white ${meta.dot}`}
                       >
-                        {meta.icon}
+                        <StatusGlyph kind={meta.glyph} />
                       </span>
                       <div className="flex items-baseline gap-2">
                         <span className="text-xs tabular-nums text-gray-400">{formatTime(event.at)}</span>
-                        <span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${meta.badgeClass}`}>
-                          {meta.badge}
-                        </span>
+                        <StatusChip label={meta.badge} tone={meta.tone} glyph={meta.glyph} />
                       </div>
                       <div className="mt-1">
                         <EventBody event={event} />
 
frontend/src/ui/StatusChip.tsx (added)
+++ frontend/src/ui/StatusChip.tsx
@@ -0,0 +1,45 @@
+import StatusGlyph, { type GlyphKind } from './StatusGlyph'
+import { chipClass, glyphOf } from '../codes/tone'
+
+/**
+ * 상태를 나타내는 태그. 화면 전체가 이 하나만 쓴다.
+ *
+ * 생김새의 근거: 오른쪽 위 모서리를 잘라낸 각진 태그 + 왼쪽 컬러 바. 서류 모서리와
+ * 서식의 항목 표시에서 가져왔다. 알약(rounded-full)을 쓰지 않는 이유는 두 가지다.
+ *   1) 표 안에서 왼쪽 바가 세로로 정렬돼 눈이 한 줄로 훑고 내려갈 수 있다.
+ *   2) 이 화면의 모든 상태가 같은 모양이면 색이 유일한 단서가 된다. 모양(글리프)이
+ *      상태를 함께 담아야 색약 사용자와 흑백 출력에서도 구분된다.
+ */
+
+interface Props {
+  /** 화면에 보이는 글자. 접근성 이름도 이 값이다. */
+  label: string
+  /** 코드표의 tone. 색과 글리프가 여기서 파생된다. */
+  tone?: string | null
+  /** tone에서 파생된 글리프를 쓰지 않고 직접 지정할 때. */
+  glyph?: GlyphKind
+  /** 글리프를 빼고 글자만 보여준다(자리가 아주 좁은 칸). */
+  bare?: boolean
+  /** 앞에 붙는 작은 식별자. 단계 번호(①) 같은 것. */
+  prefix?: string
+  className?: string
+}
+
+/** 오른쪽 위 5px를 잘라낸 모서리. 이 태그의 표식이다. */
+const NOTCH = { clipPath: 'polygon(0 0, calc(100% - 5px) 0, 100% 5px, 100% 100%, 0 100%)' }
+
+export default function StatusChip({ label, tone, glyph, bare, prefix, className = '' }: Props) {
+  const kind = glyph ?? glyphOf(tone)
+  return (
+    <span
+      style={NOTCH}
+      className={`inline-flex items-center gap-1.5 border-l-2 py-[3px] pl-1.5 pr-2 align-middle text-[11px] font-semibold leading-none tracking-tight ${chipClass(
+        tone,
+      )} ${className}`}
+    >
+      {!bare && <StatusGlyph kind={kind} />}
+      {prefix && <span className="tabular-nums opacity-70">{prefix}</span>}
+      {label}
+    </span>
+  )
+}
 
frontend/src/ui/StatusGlyph.tsx (added)
+++ frontend/src/ui/StatusGlyph.tsx
@@ -0,0 +1,128 @@
+/**
+ * 상태 글리프. 10×10 정사각 격자 위에 90°와 45°만 써서 그린다 - 곡선을 하나도 넣지 않은 것은
+ * 이 화면들이 법률 서식(체크칸·도장·서류 모서리)의 어휘를 따르기 때문이다.
+ *
+ * 모양 자체가 상태를 담는다는 점이 핵심이다. 예전에는 색만 달랐던 탓에, 색약 사용자나
+ * 흑백 출력에서는 "완료"와 "개방불가"가 똑같이 보였다.
+ *
+ * 색은 넣지 않는다. currentColor를 그대로 물려받아 감싼 칩의 글자색을 따른다.
+ */
+
+export type GlyphKind =
+  /** 아직 손대지 않음 - 빈 사각 */
+  | 'idle'
+  /** 진행 중 - 왼쪽 절반만 채운 사각 */
+  | 'progress'
+  /** 끝남 - 채운 사각에 각진 체크 */
+  | 'done'
+  /** 확인이 필요함 - 삼각 */
+  | 'caution'
+  /** 막힘·불가 - 사각을 가로지르는 사선 */
+  | 'blocked'
+  /** 자료·문서 - 모서리를 접은 사각 */
+  | 'document'
+  /** 메모 - 가로줄 세 개 */
+  | 'note'
+  /** 끝난 항목 표시 - 상자 없는 각진 체크. 이미 색을 채운 자리 위에 얹을 때 쓴다. */
+  | 'check'
+
+interface Props {
+  kind: GlyphKind
+  /** 픽셀 크기. 표 안에서는 10, 제목 옆에서는 12를 쓴다. */
+  size?: number
+  className?: string
+}
+
+export default function StatusGlyph({ kind, size = 10, className = '' }: Props) {
+  const common = {
+    width: size,
+    height: size,
+    viewBox: '0 0 10 10',
+    // 상태 이름은 옆의 글자가 이미 말해 준다. 보조기술이 같은 말을 두 번 읽지 않게 감춘다.
+    'aria-hidden': true,
+    focusable: false,
+    className: `shrink-0 ${className}`,
+  } as const
+
+  const stroke = {
+    stroke: 'currentColor',
+    strokeWidth: 1.6,
+    strokeLinecap: 'square',
+    strokeLinejoin: 'miter',
+    fill: 'none',
+  } as const
+
+  switch (kind) {
+    case 'idle':
+      return (
+        <svg {...common}>
+          <rect x="1.6" y="1.6" width="6.8" height="6.8" {...stroke} />
+        </svg>
+      )
+
+    case 'progress':
+      return (
+        <svg {...common}>
+          <rect x="1.6" y="1.6" width="6.8" height="6.8" {...stroke} />
+          <rect x="1.6" y="1.6" width="3.4" height="6.8" fill="currentColor" />
+        </svg>
+      )
+
+    case 'done':
+      return (
+        <svg {...common}>
+          <rect x="1" y="1" width="8" height="8" fill="currentColor" />
+          <polyline
+            points="2.9,5.1 4.3,6.5 7.1,3.4"
+            stroke="#fff"
+            strokeWidth="1.5"
+            strokeLinecap="square"
+            strokeLinejoin="miter"
+            fill="none"
+          />
+        </svg>
+      )
+
+    case 'caution':
+      return (
+        <svg {...common}>
+          <polygon points="5,1 9.2,8.6 0.8,8.6" {...stroke} />
+          <line x1="5" y1="4" x2="5" y2="6.2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="square" />
+          <line x1="5" y1="7.2" x2="5" y2="7.3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="square" />
+        </svg>
+      )
+
+    case 'blocked':
+      return (
+        <svg {...common}>
+          <rect x="1.6" y="1.6" width="6.8" height="6.8" {...stroke} />
+          <line x1="2.4" y1="7.6" x2="7.6" y2="2.4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="square" />
+        </svg>
+      )
+
+    case 'document':
+      return (
+        <svg {...common}>
+          {/* 오른쪽 위 모서리를 접은 종이 */}
+          <polygon points="1.8,1 6.4,1 8.4,3 8.4,9 1.8,9" {...stroke} />
+          <polyline points="6.4,1 6.4,3 8.4,3" {...stroke} />
+        </svg>
+      )
+
+    case 'note':
+      return (
+        <svg {...common}>
+          <line x1="1.4" y1="2.6" x2="8.6" y2="2.6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="square" />
+          <line x1="1.4" y1="5" x2="8.6" y2="5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="square" />
+          <line x1="1.4" y1="7.4" x2="5.8" y2="7.4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="square" />
+        </svg>
+      )
+
+    case 'check':
+      return (
+        <svg {...common}>
+          <polyline points="1.4,5 3.9,7.6 8.6,2.4" {...stroke} strokeWidth={1.8} />
+        </svg>
+      )
+  }
+}
Add a comment
List