Files
shkim4 4c29894e56 refactor(storybook): simplify component and preview examples
Drop page / section descriptions, state notes and repeated captions from
the representative Stories of 44 components, the Patterns, Center
Graphic and OrgChart pages. Value readouts, spec lists and captions hide
only on the representative page; hidden verification Stories keep them.
Tabs Default API and the Filter Bar draft / applied counts move to the
hidden DefaultApi and DraftApplied Stories; Center Overview is hidden
with its direct URL kept. Story IDs are unchanged.

Align the component, pattern and OrgChart docs with the pages, point the
quality reference check at the DraftApplied Story, and make the P8
styles check expect no section header on the preview-only Expanded page.
2026-10-23 00:28:05 +09:00

13 KiB

Filter Bar (Pattern)

2026-10-10 추가 수정 요청 기준: 공통 FilterBar 기본 배치는 유지. 목록 소비자는 공개 layout="list-basic" | "list-extended" prop으로 가운데·왼쪽 정렬과 actions auto margin 제거를 선택한다. 기존 src/list-screen.css 소비자는 호환성을 유지한다. PageHeader 밖의 흰 Card 하나에 조건→검색어→조회→초기화 순서, 간격 8px, 조회 Solid Green·초기화 Outline Gray를 적용한다. readOnly·disabled Search에는 Clear 버튼을 노출하지 않는다. 과거 검증 기록은 당시 결과이며 현재 검증은 추가 수정 보고서를 따릅니다.

현재 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. 목적 / 사용 범위

목록·업무 화면의 검색 조건 행. Table·Card 목록·모바일 목록의 검색에서 우선 검토하며 Data Table 없이도 쓸 수 있고, 값은 소비자가 소유한다. 맞는 부분만 조합할 수 있다. 요구사항이 chips/복수 검색 등 공개 API·구조로 표현되지 않으면 구체적인 미사용 이유를 기록하고 HM Component로 조합한다. ui-viz의 Search Field(선택값 칩 + 후보)는 별개의 복합 컨트롤이라 이 Pattern에 넣지 않았다.

3. 구성 요소

FilterBar(flex wrap 레이아웃), FilterBarSearch(검색 아이콘 + HM Input + 지우기), FilterBarActions(행 끝 정렬: 초기화·조회 등). 나머지는 HM Select / Combobox / Date Picker / Button을 children으로 직접 둔다. schema 기반 필터 생성은 없다.

4. 구조

<FilterBar layout="list-extended">
  <Select>…</Select>  <Combobox … />  <DatePicker … />
  <FilterBarSearch value={q} onValueChange={setQ} placeholder="이름·부서 검색" />
  <FilterBarActions>
    <Button type="button" variant="primary" appearance="solid" onClick={apply}>조회</Button>
    <Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button>
  </FilterBarActions>
</FilterBar>

5. Public API

  • FilterBar: div props + 선택적 layout: "default" | "list-basic" | "list-extended". 기본은 기존 배치 유지. FilterBarProps 공개 type 추가. 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는 조회/초기화 callback을 실행하고, 입력 조건 수와 조회 적용 조건 수의 구분은 숨김 verification DraftApplied가 표시한다. DataTable 연동 verification의 WithFilterBarStory는 조회 버튼(form submit)에서 table 필터를 적용하고 초기화로 비운다. 이 둘은 소비 조합 예시이며 FilterBar 자체의 조회 시점 계약으로 해석하지 않는다.

DatePicker / Combobox는 대표 Story에서 controlled={true}로 사용한다. 부모가 각 value를 소유하고 초기화 시 setDate(undefined) / setCategory(undefined)로 비우므로, 선택값 초기화 목적의 key remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API는 유지하고 목록의 조회/초기화 버튼은 위 최신 소비 계약을 따른다. 외부 값 초기화는 자식 onValueChange를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.

6.1 조회 버튼으로 적용하는 소비 예제

draft는 입력·선택 중인 조건, applied는 실제 목록에 적용한 조건이다. 아래 예는 조회 클릭에만 draft를 복사하고, 초기화 클릭에는 두 상태와 페이지를 함께 초기화해 전체 목록을 즉시 복원한다. Pattern의 강제 동작이 아닌 소비 정책이다. Theme/Tailwind CSS는 React 연결 순서에 따라 앱 entry에서 로드한다.

import { useMemo, useState } from "react"
import {
  Button, Card, CardContent, 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<User>[] = [
  { accessorKey: "name", header: "이름" },
  { accessorKey: "status", header: "상태", cell: ({ row }) => row.original.status === "active" ? "활성" : "비활성" },
]

export function QueryList() {
  const [draft, setDraft] = useState<Conditions>({ ...empty })
  const [applied, setApplied] = useState<Conditions>({ ...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 <>
    <Card><CardContent>
    <FilterBar layout="list-basic">
      <Select value={draft.status} onValueChange={status => setDraft(s => ({ ...s, status }))}>
        <SelectTrigger size="medium" aria-label="사용자 상태"><SelectValue /></SelectTrigger>
        <SelectContent>
          <SelectItem value="all">전체 상태</SelectItem>
          <SelectItem value="active">활성</SelectItem>
          <SelectItem value="inactive">비활성</SelectItem>
        </SelectContent>
      </Select>
      <FilterBarSearch value={draft.query} onValueChange={query => setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />
      <FilterBarActions>
        <Button type="button" variant="primary" appearance="solid" size="default" onClick={apply}>조회</Button>
        <Button type="button" variant="cancel" appearance="outline" size="default" onClick={reset}>초기화</Button>
      </FilterBarActions>
    </FilterBar>
    <p role="status">조회 결과 {data.length}건</p>
    <DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
    </CardContent></Card>
  </>
}

이 예의 사용자 상태는 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, 조회 / 초기화. 입력·적용 조건 수 표시는 숨김 verification DraftApplied(patterns-data-filter-bar--draft-applied)에만 있다. Usage / Do & Don't 규칙은 Story가 아니라 이 문서(§6.1, §9)에 둔다.

현재 대표 Story는 조건→검색어→조회→초기화, 조회 variant="primary" appearance="solid"·초기화 variant="cancel" appearance="outline"를 사용한다. 이는 최신 목록 검색 계약에 따른 Button 세트 예외다. FilterBar 자체가 Button appearance나 조회 시점을 강제하지 않는다. 숨김 verification DraftApplied의 입력 조건 수는 즉시 변하지만 조회 적용 조건 수는 조회 클릭에만 반영되며 초기화는 둘 다 비운다(대표 Story는 상태값을 표시하지 않는다). 업무 화면은 §6.1처럼 요청된 동작을 구현하며 Story의 부가 기능을 필수로 복사하지 않는다.

10. Accessibility

검색은 type="search" + aria-label, 아이콘은 aria-hidden. 컨트롤마다 aria-label을 준다(Story 기준). Reset·Clear는 button. wrapper는 시맨틱 변경 없음. 지우기 버튼이 사라질 때 검색 입력에 focus를 돌려주며 소비자 ref 전달을 유지한다. 브라우저 기본 검색 지우기 버튼은 숨기고 자체 버튼을 쓴다.

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 없음.