# Component Spec Agent Evaluation

## Quality Score
4/5

## Tested With
- claude

## Recommended For
- Writing component states, variants, behavior, and responsive rules for implementation
- Teams with the required inputs ready: Current context, Goal, Audience or stakeholder
- Project work where a human owner will review assumptions, risks, and final output

## Not Recommended For
- The product team has not decided the component purpose or user actions
- Fully autonomous production changes without human review
- Work requiring legal, financial, medical, compliance, or security certification

## Known Weaknesses
- Code snippets contain small defects a reviewer must fix: the strong run's DOM sketch gives the 'show more' button aria-controls="{uid}-toolbar" but the toolbar div has no such id, and nextMultipleValue uses a bare ternary expression statement (set.has(x) ? set.delete(x) : set.add(x)) that typical no-unused-expressions lint rules flag.
- Usage examples invent dictionary paths and state shapes (t.filters.level, t.filterChips, filters.tool, setFilters) without marking them as placeholders, so they look like existing project APIs.
- The strong output runs about 19KB with many tables. That fits the long list of requested sections, but some design-review notes (hoverOnlyWhenSupported, layout-shift discussion) could be trimmed for a ticket.
- The boundary draft stays generic: hideBelow drops columns on narrow screens without saying how users reach the hidden data, onSortChange is left untyped ((sort) => void), and row-click keyboard behavior is only a caution, not a spec.

## Evaluation Criteria
- Defines props, states, interactions, accessibility notes, and responsive constraints
- Produces component spec in a format that can be pasted into a ticket, PR, document, or handoff
- States assumptions, missing context, risks, and concrete next actions separately

## Sample Runs
### Korean spec for a reusable FilterChipGroup component

Input:

````
디자인 시스템에 필터 칩 그룹 컴포넌트를 추가하려고 합니다. 개발/디자인이 같이 보는 컴포넌트 스펙을 써 주세요.

프로젝트: Agent Archive (Next.js 16, React 19, TypeScript, Tailwind 3). 다크 테마 토큰: canvas, panel, elevated, line, primary, secondary, muted, accent(sky-400). 클래스 병합은 `cn()`(clsx). 영어/한국어 UI, 문자열은 src/i18n/dictionaries.ts에서 주입.

현재 상태: src/components/agents/agent-search-panel.tsx 안에 내부 함수 `FilterRow`가 있습니다.
```tsx
function FilterRow({ title, values, active, onSelect, label = titleCase }: {
  title: string; values: string[]; active?: string;
  onSelect: (value: string) => void; label?: (value: string) => string;
}) {
  return (
    <div>
      <p className="mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted">{title}</p>
      <div className="flex flex-wrap gap-2">
        {values.map((value) => (
          <button key={value} type="button" onClick={() => onSelect(value)}
            className={active === value
              ? "rounded-md border border-accent/40 bg-accent/15 px-2.5 py-1 text-xs font-medium text-sky-200"
              : "rounded-md border border-line bg-elevated px-2.5 py-1 text-xs font-medium text-secondary transition hover:border-accent/35 hover:text-primary"}>
            {label(value)}
          </button>
        ))}
      </div>
    </div>
  );
}
```
- 단일 선택, 같은 값 다시 누르면 해제(부모의 setFilter가 토글).
- 사용처: /agents 필터 6개 그룹(역할 9개, 카테고리 11개, 난이도 3개, 자동화 1~5, 도구 49개, 검증 상태 4개). 곧 /workflows와 /cases 페이지에도 쓸 예정.
- 알려진 문제: 선택 상태가 색으로만 구분됨, aria-pressed 없음, 그룹 제목이 <p>라 버튼 그룹과 연결 안 됨, 도구 49개가 모바일에서 10줄 넘게 wrap, 칩 높이 약 24px라 터치 타깃 작음.

요구사항:
- src/components/common/filter-chip-group.tsx로 추출, 단일/다중 선택 둘 다 지원(다중은 /cases에서 필요).
- 칩 개수가 많을 때 "더 보기"로 접기(기준 개수는 prop).
- 개수 배지 선택적 표시(예: "리뷰 12").
- 키보드: Tab으로 그룹 진입, 화살표로 이동하는 방식이 나은지 각 칩이 Tab 대상인지 결정 필요.
- 375px부터 1440px까지.
- 새 의존성 금지.

원하는 결과: props(타입 포함), 상태(기본/hover/focus-visible/선택/비활성/접힘), 인터랙션, 접근성(역할, aria, 키보드), 반응형 규칙, 사용 예시, 마이그레이션 메모, 가정, 리스크, 다음 액션.
````

Expected output:
A Korean FilterChipGroup spec for src/components/common/filter-chip-group.tsx with typed single/multi-select props, all listed states, a reasoned keyboard model (roving vs per-chip Tab), fixes for each known a11y issue (color-only selection, missing aria-pressed, unlinked <p> title, 24px targets, 49-tool wrap), 375-1440px rules, usage, migration from FilterRow without new deps, plus separate assumptions, risks, and next actions.

Sample output:

````markdown
# FilterChipGroup 컴포넌트 스펙

> 대상: 디자인 + 프론트엔드 공용
> 위치: `src/components/common/filter-chip-group.tsx`
> 대체 대상: `agent-search-panel.tsx` 내부의 `FilterRow`

주신 정보로 스펙을 쓰는 데는 충분해서 추가 질문 없이 진행했습니다. 판단이 필요했던 부분은 **가정**에 적었고, 컴포넌트가 아니라 페이지 쪽에서 정해야 하는 두 가지는 맨 끝 **열린 질문**에 모았습니다. 작업을 막는 질문은 아닙니다.

---

## 1. 요약: 핵심 결정

| 항목 | 결정 |
|---|---|
| 선택 모델 | **제어 컴포넌트**. `selectionMode: "single" \| "multiple"`. 토글(같은 값 다시 누르면 해제)은 컴포넌트가 처리하고, 부모는 결과값만 받습니다. |
| 시맨틱 | 칩은 `<button aria-pressed>` 토글 버튼입니다. 단일 선택이어도 `radiogroup`은 쓰지 않습니다. 라디오는 해제가 불가능하고, 화살표를 누르면 곧바로 선택되어 필터가 연달아 바뀌기 때문입니다. |
| 키보드 | **그룹당 Tab 정지점 1개와 화살표 이동(roving tabindex)**. 컨테이너는 `role="toolbar"`와 `aria-labelledby`로 둡니다. 도구 49개와 6개 그룹이면 Tab 정지점이 약 80개가 되므로 칩마다 Tab을 받는 방식은 제외했습니다. |
| 선택 표시 | 색 외에 **체크 아이콘과 테두리 강조**를 추가해서 색에만 의존하지 않게 합니다. |
| 터치 타깃 | 칩 높이를 모바일 36px(`h-9`), `sm` 이상 32px(`h-8`)로 하고, 간격 8px를 유지합니다. WCAG 2.5.8(AA, 24px)을 여유 있게 충족합니다. |
| 긴 목록 | `collapseAfter` prop으로 접습니다. **선택된 칩은 접혀도 항상 보입니다.** 숨길 칩이 2개 이하이면 접지 않습니다. |
| 문자열 | 컴포넌트는 dictionaries를 직접 import하지 않고, `messages` prop으로 주입받습니다. |
| 의존성 | 추가하지 않습니다. 체크 아이콘은 인라인 SVG, roving tabindex는 직접 구현합니다. |

---

## 2. Props (TypeScript)

```ts
"use client";

export type FilterChipOption<V extends string = string> = {
  value: V;
  /** 이미 번역된 표시 문자열 */
  label: string;
  /** showCounts가 true일 때 배지로 표시 */
  count?: number;
  /** 선택 불가. 포커스는 가능(aria-disabled) */
  disabled?: boolean;
};

export type FilterChipGroupMessages = {
  /** 예: "{count}개 더 보기" / "Show {count} more" */
  showMore: string;
  /** 예: "접기" / "Show less" */
  showLess: string;
  /** 스크린 리더용 개수 문구. 예: "{count}개" / "{count} results" */
  countLabel?: string;
};

type FilterChipGroupBaseProps<V extends string> = {
  /** 그룹 제목 (번역된 문자열). 버튼 그룹의 접근 가능한 이름이 됨 */
  title: string;
  options: readonly FilterChipOption<V>[];
  /** 이 개수를 넘으면 접기. 미지정 시 접지 않음 */
  collapseAfter?: number;
  /** 개수 배지 표시 여부. 기본 false */
  showCounts?: boolean;
  /** count === 0이고 선택되지 않은 옵션을 비활성 처리. 기본 false */
  disableEmpty?: boolean;
  messages: FilterChipGroupMessages;
  /** 제목 요소 태그. 페이지의 헤딩 구조에 맞춰 선택. 기본 "p" */
  titleAs?: "p" | "h2" | "h3" | "h4";
  /** 제목을 시각적으로 숨김(접근성 이름은 유지) */
  hideTitle?: boolean;
  className?: string;
};

type SingleSelectProps<V extends string> = {
  selectionMode?: "single";
  value: V | undefined;
  onValueChange: (next: V | undefined) => void;
};

type MultipleSelectProps<V extends string> = {
  selectionMode: "multiple";
  value: readonly V[];
  onValueChange: (next: V[]) => void;
};

export type FilterChipGroupProps<V extends string = string> =
  FilterChipGroupBaseProps<V> & (SingleSelectProps<V> | MultipleSelectProps<V>);
```

**설계 메모**

- `values: string[]`와 `label()` 대신 `options` 객체 배열을 받습니다. count와 disabled를 옵션 단위로 붙여야 하고, 라벨 번역은 호출하는 쪽에서 끝내는 편이 i18n 흐름에 맞습니다.
- 단일 선택에서 `value`는 기존 `active?: string`과 맞춰 `undefined`로 "선택 없음"을 표현합니다.
- 토글 로직은 순수 함수로 export해서 단위 테스트를 붙입니다.

```ts
export function nextSingleValue<V>(current: V | undefined, clicked: V) {
  return current === clicked ? undefined : clicked;
}
export function nextMultipleValue<V>(current: readonly V[], clicked: V, order: readonly V[]) {
  const set = new Set(current);
  set.has(clicked) ? set.delete(clicked) : set.add(clicked);
  return order.filter((v) => set.has(v)); // 옵션 순서로 정렬해 URL/상태를 안정적으로 유지
}
```

---

## 3. 구조 (DOM)

```html
<div class="...">                                  <!-- 루트, className 병합 -->
  <p id="{uid}-title">Tools</p>                    <!-- titleAs -->
  <div role="toolbar" aria-labelledby="{uid}-title" class="flex flex-wrap gap-2">
    <button type="button" aria-pressed="true" tabindex="0">
      <svg aria-hidden="true">✓</svg> Claude Code
      <span aria-hidden="true">12</span><span class="sr-only">, 12개</span>
    </button>
    <button type="button" aria-pressed="false" tabindex="-1">…</button>
    …
    <button type="button" aria-expanded="false" aria-controls="{uid}-toolbar" tabindex="-1">
      37개 더 보기
    </button>
  </div>
</div>
```

- id는 `useId()`로 만듭니다. 정적 export에서도 SSR과 클라이언트의 id가 일치합니다.
- "더 보기" 버튼은 툴바 **안의 마지막 항목**으로 두어 칩과 같은 줄에 이어서 wrap되게 합니다. roving 순서에도 포함되고, `End` 키로 바로 이동할 수 있습니다.
- `display: contents`로 툴바를 감싸는 방식은 쓰지 않습니다. 일부 브라우저에서 role이 사라지는 문제가 있습니다.

---

## 4. 상태 (디자인 토큰 / Tailwind)

공통 칩 베이스:

```
inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border
h-9 px-3 sm:h-8 sm:px-2.5 text-xs font-medium
transition-colors
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent
focus-visible:ring-offset-2 focus-visible:ring-offset-panel
```

| 상태 | 시각 | 클래스(제안) | ARIA |
|---|---|---|---|
| 기본 | 중립 칩 | `border-line bg-elevated text-secondary` | `aria-pressed="false"` |
| hover | 테두리와 텍스트 강조 | `hover:border-accent/35 hover:text-primary` | – |
| focus-visible | 2px accent 링과 offset | 위 베이스의 focus-visible 링 | – |
| 선택 | accent 배경, **체크 아이콘**, 진한 테두리 | `border-accent/60 bg-accent/15 text-sky-200` + 체크 SVG(`size-3.5`) | `aria-pressed="true"` |
| 선택+hover | 배경을 한 단계 진하게 | `hover:bg-accent/20` | – |
| 비활성 | 흐리게, hover 없음 | `opacity-50 cursor-not-allowed` (hover 클래스 제거) | `aria-disabled="true"`. 포커스는 가능하고 클릭은 무시 |
| 비활성+선택 | 선택 표시는 유지하고 해제만 가능 | 선택 스타일 유지 | 선택된 칩은 count가 0이어도 **비활성으로 만들지 않습니다** (해제할 수 있어야 함) |
| 개수 배지 | 라벨 옆 작은 숫자 | `rounded px-1 tabular-nums text-[11px] text-muted`, 선택 시 `text-sky-200/80` | 숫자는 `aria-hidden`, sr-only로 "12개" 제공 |
| 접힘 | `collapseAfter`개와 선택된 칩만 보이고, 끝에 "+N개 더 보기" | 더 보기 버튼: `border-dashed border-line bg-transparent text-accent hover:border-accent/35` | `aria-expanded="false"` |
| 펼침 | 전체 표시, 끝에 "접기" | 동일 | `aria-expanded="true"` |

**디자인 쪽 확인 사항**

- 선택 칩의 체크 아이콘은 **선행 배치**합니다. 선택할 때 칩 폭이 약 18px 늘어나는 레이아웃 시프트는 허용하는 것으로 가정했습니다. 허용하지 않는다면 모든 칩에 아이콘 자리를 비워 두어야 해서 칩 밀도가 떨어집니다.
- `text-sky-200`은 토큰이 아닌 하드코딩 값입니다. 유지하거나 `accent-foreground` 같은 토큰을 추가할지 결정해 주세요.
- 대비 확인이 필요합니다. `text-muted` 배지가 `bg-elevated` 위에서 4.5:1을 넘는지, 선택 칩의 `text-sky-200`이 `accent/15` + panel 조합 위에서 기준을 넘는지 봐야 합니다.
- 칩이 `panel`이 아닌 배경 위에 놓이면 `ring-offset` 색을 그 배경에 맞춰야 합니다. 필요하면 prop이나 className으로 대응합니다.

---

## 5. 인터랙션

**선택**

- 단일: 클릭하면 선택하고, 이미 선택된 칩을 다시 누르면 해제(`undefined`)합니다. 다른 칩을 누르면 선택이 교체됩니다.
- 다중: 클릭할 때마다 해당 값을 토글합니다. 결과 배열은 옵션 순서로 정렬합니다.
- `aria-disabled` 칩을 클릭하거나 Space/Enter를 눌러도 아무 일도 일어나지 않습니다.

**접기/펼치기**

- `options.length > collapseAfter`이고 숨길 개수가 **3개 이상**일 때만 접습니다. "1개 더 보기" 버튼은 칩 하나와 공간이 같아서 의미가 없습니다.
- 접힌 상태에서 보이는 칩은 앞에서부터 `collapseAfter`개에 **선택된 칩 전부**를 더한 것입니다. 원래 옵션 순서를 유지합니다.
- 버튼 문구의 N은 실제로 숨겨진 개수입니다(선택되어 노출된 칩은 제외).
- 펼치면 포커스가 **새로 드러난 첫 칩**으로 이동합니다. 접으면 포커스는 토글 버튼에 남습니다.
- 펼침 상태는 내부 `useState`로 관리합니다. 필터가 바뀌어도 유지되고, 페이지를 이동하면 초기화됩니다.

**hover**

- Tailwind 3의 `hover:`는 터치에서 잔상이 남습니다. `tailwind.config.ts`에 `future: { hoverOnlyWhenSupported: true }`를 켜는 것을 권장합니다. 전역에 영향이 있으므로 별도 PR로 진행합니다.

---

## 6. 접근성

**역할과 ARIA**

- 그룹: `role="toolbar"`와 `aria-labelledby={titleId}`. 제목을 `hideTitle`로 숨겨도 sr-only로 남깁니다.
- 칩: 네이티브 `<button type="button">`에 `aria-pressed`를 붙입니다. 다중 선택에서도 같은 패턴을 씁니다.
- 개수: 시각 숫자는 `aria-hidden`로 가리고, sr-only로 `messages.countLabel`을 붙입니다. 읽히는 예: "리뷰, 12개, 전환 버튼, 눌림".
- 체크 아이콘: `aria-hidden="true"`. 선택 상태는 `aria-pressed`로 전달합니다.
- 더 보기: `aria-expanded`와 `aria-controls`(툴바 id).

**키보드 (roving tabindex)**

| 키 | 동작 |
|---|---|
| Tab / Shift+Tab | 그룹 진입과 이탈. 그룹당 정지점은 1개입니다. |
| → / ↓ | 다음 칩(마지막에서 처음으로 순환) |
| ← / ↑ | 이전 칩(처음에서 마지막으로 순환) |
| Home / End | 처음 / 마지막 항목(더 보기 버튼 포함) |
| Space / Enter | 토글 (네이티브 버튼 동작) |

- 진입할 때 포커스를 받는 칩은 **마지막으로 포커스했던 칩**입니다. 기록이 없으면 첫 번째 선택된 칩, 선택이 없으면 첫 칩입니다.
- 화살표는 **포커스만 옮기고 선택하지는 않습니다**. 선택할 때마다 결과가 바뀌기 때문입니다.
- 줄바꿈된 칩 사이에서 ↑/↓를 누르면 윗줄/아랫줄이 아니라 이전/다음 칩으로 이동합니다(DOM 순서 기준). 2D 이동은 구현 비용 대비 이점이 작습니다.
- 접힐 때 tabindex=0이던 칩이 숨겨지면 tabindex=0을 첫 칩으로 넘깁니다.
- 결과 개수 변화 안내(`aria-live`)는 **페이지 책임**입니다. 컴포넌트는 안내하지 않습니다.

**Tab 방식 대비 선택 근거**

| | 칩마다 Tab | roving (채택) |
|---|---|---|
| /agents에서 Tab 횟수 | 약 80회 이상 | 약 6회 |
| 학습 비용 | 없음 | 낮음 (toolbar 역할을 스크린 리더가 안내) |
| 구현 | 없음 | 약 40줄 (keydown과 ref 배열) |

---

## 7. 반응형 규칙 (375 ~ 1440px)

| 구간 | 규칙 |
|---|---|
| < 640px (375 기준) | 칩 `h-9 px-3`, 간격 `gap-2`. 한 줄에 보통 3~4개. 라벨은 `whitespace-nowrap`에 `max-w-full truncate`를 적용하고, 잘리면 `title`에 전체 라벨을 넣습니다. |
| ≥ 640px | 칩 `h-8 px-2.5`. 간격은 같습니다. |
| 공통 | 가로 스크롤은 쓰지 않습니다. 숨겨진 옵션을 발견하기 어렵고 roving 포커스와도 충돌합니다. 줄 수는 `collapseAfter`로 조절합니다. |
| 권장 `collapseAfter` | 도구 **12** (375px에서 약 4줄). 역할 9개, 카테고리 11개, 난이도, 자동화, 검증은 접지 않습니다. |

- 화면 폭에 따라 기준 개수를 다르게 하는 기능은 넣지 않습니다. `matchMedia`에 의존하면 정적 export의 hydration 결과가 달라지고, 12개 기준이면 1440px에서도 2줄 정도라 과하지 않습니다.
- 49개를 펼친 상태의 모바일 스크롤 길이는 허용합니다. 사용자가 직접 펼친 것이기 때문입니다.

---

## 8. 사용 예시

**/agents: 단일 선택과 개수 배지**

```tsx
<FilterChipGroup
  title={t.filters.tools}
  options={tools.map((tool) => ({
    value: tool,
    label: toolLabel(tool),
    count: facetCounts.tools[tool] ?? 0,
  }))}
  value={filters.tool}
  onValueChange={(tool) => setFilters((f) => ({ ...f, tool }))}
  collapseAfter={12}
  showCounts
  disableEmpty
  messages={t.filterChips}
/>
```

**/agents: 자동화 레벨 (숫자 값은 문자열로)**

```tsx
<FilterChipGroup
  title={t.filters.automation}
  options={["1", "2", "3", "4", "5"].map((v) => ({ value: v, label: `${t.filters.level} ${v}` }))}
  value={filters.automation}
  onValueChange={(automation) => setFilters((f) => ({ ...f, automation }))}
  messages={t.filterChips}
/>
```

**/cases: 다중 선택**

```tsx
<FilterChipGroup
  selectionMode="multiple"
  title={t.filters.categories}
  options={categoryOptions}
  value={filters.categories}            // string[]
  onValueChange={(categories) => setFilters((f) => ({ ...f, categories }))}
  messages={t.filterChips}
/>
```

**dictionaries 추가 키 (en과 ko 모두)**

```ts
// en
filterChips: { showMore: "Show {count} more", showLess: "Show less", countLabel: "{count} results" },
// ko
filterChips: { showMore: "{count}개 더 보기", showLess: "접기", countLabel: "{count}개" },
```

`{count}` 치환은 컴포넌트 안의 작은 헬퍼가 처리합니다. 사전이 이미 함수형 메시지를 지원한다면 그 방식을 따릅니다.

---

## 9. 마이그레이션 메모

1. `filter-chip-group.tsx`를 만들고 `nextSingleValue`와 `nextMultipleValue`의 단위 테스트를 추가합니다(기존 unit test 구성을 사용).
2. **부모의 토글 로직을 제거합니다.** 지금은 `setFilter`가 토글을 하는데, 컴포넌트도 토글하면 **두 번 토글되어 선택이 되지 않습니다**. `onValueChange`에는 받은 값을 그대로 저장하는 setter만 넘겨야 합니다. 가장 놓치기 쉬운 부분입니다.
3. `values`와 `label` 조합을 `options`로 바꿉니다. 기존 `titleCase` 기본 라벨은 호출하는 쪽으로 옮기고, 가능하면 dictionaries 번역으로 대체합니다.
4. 개수 배지를 쓰는 그룹은 facet count 계산을 페이지 쪽에 추가합니다(열린 질문 2 참고).
5. `filterChips` 키를 `en`과 `ko`에 모두 추가합니다.
6. `FilterRow`를 삭제하고 6개 그룹을 교체합니다. 도구 그룹에만 `collapseAfter={12}`를 적용합니다.
7. URL 쿼리 동기화(`useSearchParams`)는 그대로 둡니다. 다중 선택용 직렬화 규칙은 /cases 작업에서 정합니다(예: `?category=a,b`).
8. 확인: `check:data`, `lint`, `typecheck`, `build`. 키보드만으로 6개 그룹을 순회하고, VoiceOver로 "눌림" 상태가 읽히는지, 375px 레이아웃을 수동으로 점검합니다.

시각 변경이 함께 들어갑니다(칩 높이 증가, 체크 아이콘). 스크린샷 비교를 PR에 첨부하는 것을 권장합니다.

---

## 10. 가정

- 필터 패널은 `panel` 배경 위에 놓입니다(ring-offset 색 기준).
- 옵션 라벨은 짧은 단어입니다(대부분 20자 이하). 긴 라벨은 truncate로 처리합니다.
- 옵션 수는 최대 약 50개입니다. 수백 개가 되면 검색 입력이 필요해지며 이 스펙의 범위를 벗어납니다.
- 선택 시 체크 아이콘 때문에 칩 폭이 변하는 것은 허용합니다.
- 기존에 아이콘 라이브러리가 있어도 체크 하나는 인라인 SVG로 충분합니다(의존성 금지 조건 충족).
- 펼침 상태를 URL에 저장할 필요는 없습니다.

---

## 11. 리스크

| 리스크 | 영향 | 대응 |
|---|---|---|
| 부모와 컴포넌트가 모두 토글 | 선택 불가 버그 | 마이그레이션 2단계에서 처리하고, 교체 PR에서 전 그룹을 수동 테스트합니다. |
| 접힌 영역에 선택 값이 숨음 | 현재 필터 상태를 오인 | 선택된 칩은 항상 노출하도록 스펙에 명시했습니다. |
| roving tabindex 구현 오류(포커스 유실, 숨긴 칩에 tabindex=0) | 키보드 사용자가 그룹에 진입하지 못함 | 접기/펼치기, 옵션 변경, 선택 해제 시나리오로 테스트합니다. |
| count 변경 시 칩 폭 흔들림 | 레이아웃이 들썩임 | `tabular-nums`를 적용합니다. 자릿수 변화는 허용합니다. |
| `disableEmpty`로 선택된 칩까지 비활성화 | 해제 불가 | 선택된 칩은 비활성 예외로 둡니다(스펙 명시). |
| 칩 높이 증가(24→36px)로 필터 패널이 길어짐 | 모바일 첫 화면의 결과 노출 감소 | 도구 그룹 접기로 상쇄합니다. 여전히 길면 모바일에서 필터 패널을 접는 방식을 별도로 검토합니다. |
| `hoverOnlyWhenSupported` 전역 변경 | 다른 컴포넌트 hover 동작 변화 | 별도 PR로 분리해 영향 범위를 확인합니다. |
| 영어 복수형("1 more") | 문구 어색함 | 숨김이 3개 이상일 때만 접으므로 N은 항상 3 이상이고, 단수 문제가 생기지 않습니다. |

---

## 12. 다음 액션

| # | 담당 | 작업 |
|---|---|---|
| 1 | 디자인 | 선택 스타일(체크 아이콘, 테두리 강조) 확정, `text-sky-200` 토큰화 여부 결정, 대비 수치 확인 |
| 2 | 디자인 | 더 보기 버튼 스타일(점선 테두리 + accent 텍스트) 확정 |
| 3 | 개발 | 컴포넌트와 토글 헬퍼 단위 테스트 작성 (1 PR) |
| 4 | 개발 | /agents 교체와 부모 토글 제거, i18n 키 추가 (1 PR, 스크린샷 첨부) |
| 5 | 개발 | (선택) `hoverOnlyWhenSupported` 적용 (별도 PR) |
| 6 | 개발 | /workflows, /cases 적용. /cases는 다중 선택 URL 직렬화 포함 |
| 7 | 공동 | 키보드와 VoiceOver, 375/768/1440px QA 체크리스트 수행 |

---

## 열린 질문 (페이지 쪽 결정, 컴포넌트 작업을 막지 않음)

1. **/cases 다중 선택의 의미**: 같은 그룹 안에서 여러 값을 고르면 OR(하나라도 해당)인가요, AND(모두 해당)인가요? 필터 UI의 관례는 OR입니다. 컴포넌트는 어느 쪽이든 동일합니다.
2. **개수 배지 기준**: "다른 그룹 필터를 적용한 상태에서 이 칩을 선택하면 나오는 결과 수"(facet count, 권장)인가요, 아니면 전체 데이터 기준의 고정 개수인가요? 권장안으로 가면 0개 칩을 `disableEmpty`로 흐리게 처리할 수 있습니다.
````

Review notes:
- Real run on 2026-09-29: Claude Opus 5.5 with the agent prompt as instructions and this input, single turn, no tools. Graded by a separate Claude reviewer against the evaluation criteria; not yet reviewed by a human maintainer.
- Addresses every known problem from the input: aria-pressed on native buttons, a check icon plus border so selection is not color-only, role=toolbar with aria-labelledby for the title, h-9/h-8 chips meeting WCAG 2.5.8, and collapseAfter=12 for the 49 tools.
- Makes the requested keyboard decision with evidence: roving tabindex because per-chip Tab means about 80 stops (9+11+3+5+49+4 = 81, which is correct). It also explains why radiogroup is rejected (radios can't be deselected, and arrows would trigger selection).
- Migration note 2 catches a real regression: the parent's setFilter already toggles, so a component that also toggles would double-toggle and never select. This is a specific, high-value insight drawn from the input.
- Uses the stated constraints correctly: the theme tokens (elevated, line, accent, panel ring-offset), cn/no new deps (inline SVG check), i18n via a messages prop with keys added to both en and ko, and useId for static-export-safe ids.
- Minor slips: the aria-controls target id is missing from the toolbar markup, the ternary-as-statement in nextMultipleValue would likely fail lint, and example dictionary keys are invented without being marked as placeholders.

### Spec for a 'universal table' with no requirements

Input:

```
테이블 컴포넌트 스펙 만들어줘. 모든 화면에서 다 쓸 수 있는 만능 테이블로. 디자인은 아직 없어.
```

Expected output:
Flag that a 'universal table for every screen' with no design is under-specified and risky, ask a few focused questions (stack, data scale/server vs client, actual screens and features), and still give a clearly labeled provisional scope/API/state/a11y draft with assumptions and next steps.

Sample output:

````markdown
디자인이 아직 없으니, 아래 스펙은 **가정 기반 초안**입니다. 먼저 한 가지를 짚고 가겠습니다.

> **"모든 화면에서 다 쓰는 만능 테이블"은 권장하지 않습니다.** 정렬·필터·편집·트리·가상 스크롤·드래그 등을 한 컴포넌트에 전부 넣으면 props가 수십 개로 늘어나고, 한 화면을 고치면 다른 화면이 깨지는 구조가 됩니다. 대신 **작은 코어 테이블 + 선택형 기능(opt-in) + 조합 가능한 하위 컴포넌트** 구조를 제안합니다. "모든 화면에서 쓸 수 있다"는 목표는 이 방식으로 더 안전하게 달성할 수 있습니다.

---

## 먼저 확인이 필요한 것 (필수 3가지)

아래 답에 따라 스펙 범위가 크게 달라집니다. 답을 주시면 초안을 확정본으로 다듬겠습니다.

1. **기술 스택**: React / Vue / 기타? 기존 디자인 시스템(MUI, Ant, shadcn 등)이나 테이블 라이브러리(TanStack Table 등)를 쓰나요, 직접 구현인가요?
2. **데이터 규모와 처리 위치**: 한 화면에 보통 몇 행인가요(수십 / 수천 / 수만)? 정렬·필터·페이지네이션을 **서버**에서 하나요, **클라이언트**에서 하나요?
3. **실제로 필요한 화면 목록**: 지금 테이블이 들어갈 화면 3~5개와, 각 화면에 필요한 기능(정렬, 선택, 인라인 편집 등)을 알려주세요. "모든 화면"의 실제 범위를 여기서 정합니다.

---

## 1. 목적과 범위

- **목적**: 여러 화면에서 반복되는 목록/표 UI를 일관된 동작과 접근성으로 제공.
- **범위 안 (v1)**: 데이터 표시, 컬럼 정의, 정렬, 페이지네이션, 행 선택, 로딩/빈/에러 상태, 반응형 기본 대응.
- **범위 밖 (v1에서는 제외, 필요 시 별도 확장)**: 인라인 편집, 트리/그룹 행, 컬럼 드래그 재정렬, 셀 병합, 엑셀형 스프레드시트 동작, 피벗.

## 2. 구조 (제안)

| 계층 | 역할 |
|---|---|
| `Table` (코어) | 순수 렌더링: 헤더, 행, 셀. 상태 없음에 가깝게 유지 |
| 기능 모듈 (opt-in) | 정렬, 선택, 페이지네이션, 컬럼 표시/숨김 등. 필요한 화면만 켬 |
| 상태 슬롯 | Loading / Empty / Error 표시 영역 |
| 툴바 (선택) | 검색, 필터, 일괄 작업 버튼 영역. 테이블 밖에 조합 |
| 셀 렌더러 | 텍스트, 숫자, 날짜, 상태 배지, 링크, 액션 메뉴 등 재사용 셀 |

## 3. API 초안 (React 가정)

```ts
type Column<T> = {
  id: string;
  header: string | ReactNode;
  accessor: (row: T) => unknown;
  cell?: (row: T) => ReactNode;      // 커스텀 렌더링
  sortable?: boolean;
  align?: "left" | "center" | "right"; // 숫자는 right 권장
  width?: number | string;
  minWidth?: number;
  hideBelow?: "sm" | "md" | "lg";     // 반응형 숨김
};

type TableProps<T> = {
  columns: Column<T>[];
  data: T[];
  rowKey: (row: T) => string;         // 필수: index를 key로 쓰지 않음
  loading?: boolean;
  error?: ReactNode;
  emptyState?: ReactNode;

  // 정렬 (controlled 우선)
  sort?: { columnId: string; direction: "asc" | "desc" } | null;
  onSortChange?: (sort) => void;

  // 선택
  selectable?: "none" | "single" | "multiple";
  selectedKeys?: Set<string>;
  onSelectionChange?: (keys: Set<string>) => void;

  // 페이지네이션
  pagination?: { page: number; pageSize: number; total: number };
  onPageChange?: (page: number) => void;

  onRowClick?: (row: T) => void;
  density?: "compact" | "default" | "comfortable";
  stickyHeader?: boolean;
  caption?: string;                   // 접근성용 표 설명
};
```

**원칙**: 정렬·선택·페이지 상태는 **controlled**(부모가 소유)로 설계해 서버/클라이언트 처리 방식 모두 지원합니다.

## 4. 상태 정의

| 상태 | 동작 |
|---|---|
| Loading (최초) | 스켈레톤 행 표시, 헤더는 유지 |
| Loading (재조회) | 기존 데이터 유지 + 상단 진행 표시 (깜빡임 방지) |
| Empty | "데이터 없음"과 "검색 결과 없음"을 구분해 문구/액션 다르게 |
| Error | 메시지 + 재시도 버튼 |
| Row hover / selected / disabled | 시각적 구분 필요 (디자인 단계에서 토큰 확정) |

## 5. 접근성

- 네이티브 `<table>`, `<thead>`, `<th scope="col">` 사용.
- 정렬 가능한 헤더는 `<button>` + `aria-sort`.
- 선택 체크박스에 행 단위 라벨 제공 ("○○ 선택").
- 행 클릭 동작이 있으면 키보드로도 동일하게 접근 가능해야 함 (행 전체를 클릭 영역으로 쓸 때 주의).
- `caption` 또는 `aria-label`로 표 목적 명시.

## 6. 반응형

- 기본: 가로 스크롤 + 첫 컬럼 고정(선택).
- 좁은 화면: `hideBelow`로 우선순위 낮은 컬럼 숨김.
- 모바일 카드형 전환은 **v1 제외** 권장 (별도 컴포넌트가 더 깔끔함).

## 7. 가정

- React + TypeScript 환경.
- 데이터는 수백 행 이하가 일반적이고, 대용량은 서버 페이지네이션으로 처리.
- 디자인 토큰(색상, 간격, 타이포)은 추후 디자인 시스템에서 정의.
- 한국어/영어 등 다국어 문구는 props로 주입.

## 8. 권장 사항

1. **v1은 작게**: 위 범위 안 기능만 구현하고, 실제 화면 2~3개에 먼저 적용해 검증.
2. **헤드리스 라이브러리 검토**: 정렬/선택/페이지 로직은 TanStack Table 같은 헤드리스 라이브러리를 쓰고 UI만 직접 만드는 쪽이 유지보수 비용이 낮습니다.
3. **기능 추가 기준 마련**: "2개 이상 화면에서 필요할 때만 코어에 추가, 아니면 해당 화면에서 조합"이라는 규칙을 둡니다.
4. **디자인과 병행**: 밀도(density), 상태 색상, 정렬 아이콘, 빈 상태 일러스트는 디자이너와 우선 합의할 항목입니다.

## 9. 리스크

| 리스크 | 영향 | 대응 |
|---|---|---|
| 만능화로 인한 props 폭증 | 사용법 복잡, 버그 증가 | opt-in 모듈 구조, 기능 추가 기준 |
| 서버/클라이언트 처리 혼재 | 정렬·페이지 동작 불일치 | controlled API로 통일 |
| 대용량 데이터 성능 | 스크롤 끊김 | 서버 페이지네이션 우선, 필요 시 가상 스크롤 별도 도입 |
| 디자인 부재 상태에서 구현 선행 | 재작업 | 구조/API 먼저 확정, 스타일은 토큰으로 분리 |
| 접근성 누락 | 키보드/스크린리더 사용 불가 | 체크리스트를 완료 기준에 포함 |

## 10. 다음 액션

1. 위 **필수 질문 3가지**에 답변 주기.
2. 테이블이 들어갈 화면 목록과 화면별 필요 기능을 표로 정리 (제가 템플릿을 드릴 수도 있습니다).
3. 디자이너와 density, 상태 스타일, 빈/에러 상태 문구 합의.
4. v1 범위 확정 후 스펙 확정본 + 완료 기준(Definition of Done) 작성.

답을 주시면 이 초안을 확정 스펙으로 정리해 드리겠습니다.
````

Review notes:
- Real run on 2026-09-29: Claude Opus 5.5 with the agent prompt as instructions and this input, single turn, no tools. Graded by a separate Claude reviewer against the evaluation criteria; not yet reviewed by a human maintainer.
- Opens by challenging the 'universal table' premise and proposes a small core plus opt-in modules, which is the right call and is backed by concrete risks (props explosion, cross-screen breakage).
- Asks exactly three blocking questions (stack/library, data scale and server vs client processing, real screen list with features). They are focused and actually decide the scope.
- Moves the work forward with a labeled provisional draft: v1 in/out scope, a React TS API with controlled sort/selection/pagination, loading/empty/error states split into sensible cases (initial vs refetch, no data vs no results), and native table semantics with aria-sort.
- Assumptions are stated separately (React+TS, hundreds of rows, tokens TBD, i18n via props), and nothing project-specific is invented, though it recommends TanStack Table before knowing whether dependencies are allowed.
- The draft is thin in places: hiding columns by breakpoint has no fallback for the hidden data, keyboard behavior for row click and selection is not specified, and the onSortChange param is untyped.
