# Filter Bar (Pattern)
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다.
## 1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-03 |
| 기반 | HM primitives 조합 (의존성 추가 없음) |
| 대표 Story | `Patterns/Data/Filter Bar` → `Filter Bar`, 연동 검증은 `Patterns/Data/Data Table` → `With Filter Bar` (숨김 verification) |
| 보류 | §12 |
## 2. 목적 / 사용 범위
목록·업무 화면의 검색 조건 행. Data Table 없이도 쓸 수 있고, 값은 소비자가 소유한다. ui-viz의 Search Field(선택값 칩 + 후보)는 별개의 복합 컨트롤이라 이 Pattern에 넣지 않았다.
## 3. 구성 요소
`FilterBar`(flex wrap 레이아웃), `FilterBarSearch`(검색 아이콘 + HM Input + 지우기), `FilterBarActions`(행 끝 정렬: 초기화·조회 등). 나머지는 HM Select / Combobox / Date Picker / Button을 children으로 직접 둔다. schema 기반 필터 생성은 없다.
## 4. 구조
```tsx
```
## 5. Public API
- `FilterBar`, `FilterBarActions`: `div` props. wrapper에 role을 두지 않는다.
- `FilterBarSearch`: `value`, `onValueChange`(필수), `size`(Input size, 기본 `medium`), `placeholder`(기본 "검색"), 나머지 `input` props. `aria-label` 기본값은 placeholder. 값이 있으면 지우기 버튼(`aria-label="검색어 지우기"`).
- Reset은 소비자가 Button과 callback으로 구성한다(Pattern에 `onReset` prop 없음).
- Dashboard의 빠른 상태 전환(`all / active / hold / done / issue`)은 `FilterBar`가 소유하지 않는다. `FilterBar`는 검색·Select·Combobox·DatePicker 등의 조회 조건 layout으로 유지하고, 상태 필터는 Dashboard status contract를 소비하는 별도 조합으로 배치한다.
## 6. 상태 관리
Pattern은 상태가 없다. Data Table과 연결할 때는 검색 → `table.setGlobalFilter`, 열 선택 → `column.setFilterValue`로 소비자가 잇는다(`data-table.md` §6).
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)의 조회 버튼에는 실행 callback이 없고 조건 수는 입력값으로 계산한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 입력 즉시 table 필터를 변경하며 조회 버튼은 없다. 이 둘을 조회 버튼 적용 계약으로 해석하지 않는다.
DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다. 부모가 각 `value`를 소유하고 초기화 시 `setDate(undefined)` / `setCategory(undefined)`로 비우므로, 선택값 초기화 목적의 `key` remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API와 초기화 / 조회 Outline 버튼 세트는 유지한다. 외부 값 초기화는 자식 `onValueChange`를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.
### 6.1 조회 버튼으로 적용하는 소비 예제
`draft`는 입력·선택 중인 조건, `applied`는 실제 목록에 적용한 조건이다. 아래 예는 조회 클릭에만 `draft`를 복사하고, 초기화 클릭에는 두 상태와 페이지를 함께 초기화해 전체 목록을 즉시 복원한다. Pattern의 강제 동작이 아닌 소비 정책이다. Theme/Tailwind CSS는 [React 연결 순서](../guidelines/runtime-portability.md#2-실제-hm-계약과-파일-경로)에 따라 앱 entry에서 로드한다.
```tsx
import { useMemo, useState } from "react"
import {
Button, DataTable, FilterBar, FilterBarActions, FilterBarSearch,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useDataTable,
type DataTableColumnDef,
} from "@hm-design/ui"
type User = { id: string; name: string; status: "active" | "inactive" }
type Conditions = { query: string; status: string }
const empty: Conditions = { query: "", status: "all" }
const rows: User[] = [
{ id: "U-1", name: "김한맥", status: "active" },
{ id: "U-2", name: "이한맥", status: "inactive" },
]
const columns: DataTableColumnDef[] = [
{ accessorKey: "name", header: "이름" },
{ accessorKey: "status", header: "상태", cell: ({ row }) => row.original.status === "active" ? "활성" : "비활성" },
]
export function QueryList() {
const [draft, setDraft] = useState({ ...empty })
const [applied, setApplied] = useState({ ...empty })
const data = useMemo(() => rows.filter(row =>
row.name.toLocaleLowerCase().includes(applied.query.trim().toLocaleLowerCase()) &&
(applied.status === "all" || row.status === applied.status)
), [applied])
const table = useDataTable({ columns, data, getRowId: row => row.id })
const apply = () => { setApplied({ ...draft }); table.setPageIndex(0) }
const reset = () => {
setDraft({ ...empty })
setApplied({ ...empty })
table.setPageIndex(0)
}
return <>
setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />
조회 결과 {data.length}건
>
}
```
이 예의 사용자 상태는 Dashboard 업무 상태와 다른 도메인이다. `data`/`columns` 참조를 안정적으로 유지하고 적용 조건으로 만든 결과만 DataTable에 전달한다. table 내부 global/column 필터를 사용하는 방식이면 조회 callback에서 해당 필터를 갱신하고 초기화 callback에서 `resetGlobalFilter(true) / resetColumnFilters(true)`도 호출한다. 서버 조회라면 `applied`를 요청 조건으로 사용하고 결과·loading·error는 소비자가 관리한다. 코드·Props는 공개 소스와 대조했으며 이 문서 작업에서 실행 검증은 하지 않았다.
## 7. 조합 컴포넌트
Input, Select, Combobox, Date Picker, Button(그 외 Popover 등 자유). 각 컨트롤의 모양·상태·접근성은 해당 컴포넌트가 소유한다.
## 8. Styling / Token
레이아웃만: `packages/ui/src/theme/filter-bar.css`의 `--hm-filter-bar-*`(gap 8px, control 폭 180px, search 폭 280px, 아이콘 크기·위치·색). 검색 텍스트 시작점은 아이콘 inset 12px + 크기 16px + gap 8px이며, Input의 Focus / Error / Readonly 상태에서도 같은 위치를 유지한다(Input 자체 CSS는 변경하지 않음). 직접 자식인 Select / Combobox / Date Picker trigger와 Input에 컨트롤 폭을 준다. 640px 이하: search는 한 줄 전체, 컨트롤은 한 줄에 2개씩, actions는 한 줄에 우측 정렬. spacing 전역 scale이 없어 값은 Pattern 내부 값이다.
## 9. Storybook
`Patterns/Data/Filter Bar` → `Filter Bar`: 검색, 상태 Select, 분류 Combobox, 날짜 Date Picker, 초기화 / 조회, 적용된 조건 수.
대표 Story의 초기화 / 조회는 목록의 반복 조회·보조 작업 세트다. [Button 사용 위치와 버튼 세트 규칙](../components/button.md#5-사용-위치와-버튼-세트)에 따라 둘 다 `appearance="outline"`, `size="default"`를 사용하고 역할만 `cancel` / `primary`로 구분한다. FilterBar는 Button appearance를 강제하지 않으며 소비자가 같은 세트의 appearance와 size를 맞춘다. 대표 Story의 조회는 실행 callback이 없는 조합 예시이고, 적용된 조건 수는 입력 상태에 즉시 반영된다.
## 10. Accessibility
검색은 `type="search"` + `aria-label`, 아이콘은 `aria-hidden`. 컨트롤마다 `aria-label`을 준다(Story 기준). Reset·Clear는 `button`. wrapper는 시맨틱 변경 없음. 브라우저 기본 검색 지우기 버튼은 숨기고 자체 버튼을 쓴다.
## 11. Responsive
390px: search 전체 폭, 상태 / 분류 한 줄, 날짜 한 줄, actions 우측 정렬. 페이지 가로 overflow 없음. 모바일 Filter Drawer는 만들지 않았다.
## 12. 보류 / 비범위
active filter chip·필터 수 배지 시스템, schema-driven 필터, URL / localStorage 연동, 고급 필터 Popover, 날짜 범위.
**Legacy 호환 범위**: DatePicker / Combobox에서 `controlled`를 생략하면 기존처럼 `value={undefined}`를 Uncontrolled로 취급하므로, 외부 undefined 초기화 시 내부 값이 다시 표시될 수 있다. 기존 `key` remount 우회는 이 Legacy 방식에서 사용할 수 있으나, 대표 Story는 명시적 Controlled 모드로 전환하고 해당 우회를 제거했다. Combobox의 `""`는 유효한 옵션 값이므로 일반적인 선택 없음 계약으로 대체하지 않는다.
## 13. 관련 파일 / 검증
- 2026-10-08: 대표 Story 소스를 격리 Headless Edge 환경에서 실행해 검색·분류·날짜 초기화, 적용 조건 수 0, 두 Trigger DOM 동일성 유지를 확인했다. 선택값 초기화 목적의 remount가 없다. 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
```
packages/ui/src/patterns/filter-bar/{filter-bar.tsx,index.ts}
packages/ui/src/theme/filter-bar.css
src/patterns/filter-bar.stories.tsx
```
- `npm run typecheck`, `npm run build` 통과(2026-10-03).
- Browser 확인(`With Filter Bar`, 47행): 검색 "설계" → 10행, + 상태 "대기" → 2행, + 날짜 10/11 → 1행, 검색 "zzzz" → 빈 결과(문구, `colSpan`), 초기화 → 47행 복원. 390px에서 위 배치, overflow 없음.