Files
HMDesign/src/patterns/data-table.stories.tsx
T
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

172 lines
7.6 KiB
TypeScript

import { useMemo, useState } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
Card, CardContent, CardHeader, CardTitle,
Badge,
Button,
Combobox,
DataTable,
DataTableColumnHeader,
DataTableViewOptions,
DatePicker,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
FilterBar,
FilterBarActions,
FilterBarSearch,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
dataTableSelectColumn,
toast,
useDataTable,
type DataTableColumnDef,
} from "@hm-design/ui"
import "../list-screen.css"
import { MoreHorizontalIcon } from "lucide-react"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { formatDate, sameDay, sampleUsers, statusLabel, type SampleUser } from "./sample-users"
// Data Table is a pattern: the HM Table primitive driven by TanStack Table v9 (useDataTable). Columns and data stay referentially stable
// (module scope / useMemo). Row actions use the HM Dropdown Menu and report through Sonner `toast` (Story composition only).
const statusBadge = (status: SampleUser["status"]) => (
<Badge variant={status === "active" ? "default" : status === "pending" ? "secondary" : "outline"}>{statusLabel[status]}</Badge>
)
const columnLabels = { name: "이름", department: "부서", status: "상태", createdAt: "등록일" }
const columns: DataTableColumnDef<SampleUser>[] = [
dataTableSelectColumn<SampleUser>(),
{ accessorKey: "name", header: ({ column }) => <DataTableColumnHeader column={column} title="이름" /> },
{ accessorKey: "department", header: "부서" },
{
accessorKey: "status",
header: "상태",
cell: ({ row }) => statusBadge(row.original.status),
filterFn: "equalsString",
enableSorting: false,
},
{
accessorKey: "createdAt",
header: ({ column }) => <DataTableColumnHeader column={column} title="등록일" />,
cell: ({ row }) => formatDate(row.original.createdAt),
filterFn: (row, columnId, value: Date) => sameDay(row.getValue<string>(columnId), value),
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="primary" appearance="outline" size="icon-sm" aria-label={`${row.original.name} 작업`}>
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 보기`)}>보기</DropdownMenuItem>
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 수정`)}>수정</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={() => toast.error(`${row.original.name} 삭제 요청`)}>
삭제
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]
function Basic({ data = sampleUsers, loading }: { data?: SampleUser[]; loading?: boolean }) {
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
return (
<div className="grid w-full gap-3">
<div className="flex justify-end">
<DataTableViewOptions table={table} labels={columnLabels} />
</div>
<DataTable table={table} loading={loading} />
</div>
)
}
const statusOptions = [
{ value: "active", label: "활성" },
{ value: "pending", label: "대기" },
{ value: "inactive", label: "비활성" },
]
const departmentOptions = [...new Set(sampleUsers.map((u) => u.department))].map((d) => ({ value: d, label: d }))
// Filter Bar → filter state → Data Table: search is the TanStack global filter, status / date are column filters.
function WithFilterBar({ extended = true }: { extended?: boolean }) {
const data = useMemo(() => sampleUsers, [])
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
const [search, setSearch] = useState("")
const [status, setStatus] = useState("all")
const [date, setDate] = useState<Date | undefined>()
const [department, setDepartment] = useState<string | undefined>()
const apply = () => {
table.setGlobalFilter(search || undefined)
table.getColumn("status")?.setFilterValue(status === "all" ? undefined : status)
table.getColumn("department")?.setFilterValue(department)
table.getColumn("createdAt")?.setFilterValue(date)
}
const reset = () => {
setSearch(""); setStatus("all"); setDate(undefined); setDepartment(undefined)
table.resetGlobalFilter(true); table.resetColumnFilters(true)
}
// The list title belongs to the list: Card title → search → table inside the one white Card.
return <Card className="min-w-0">
<CardHeader><CardTitle>사용자 목록</CardTitle></CardHeader>
<CardContent>
<div className="grid min-w-0 gap-4">
<div className="hm-data-table-tools">
<form className="hm-data-table-search" onSubmit={event => { event.preventDefault(); apply() }}>
<FilterBar className={extended ? "hm-list-search-extended" : "hm-list-search-basic"}>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger size="medium" aria-label="상태"><SelectValue placeholder="상태" /></SelectTrigger>
<SelectContent><SelectItem value="all">전체 상태</SelectItem>{statusOptions.map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectContent>
</Select>
{extended && <><Combobox controlled size="medium" aria-label="부서" options={departmentOptions} value={department} placeholder="부서" onValueChange={setDepartment} />
<DatePicker controlled size="medium" aria-label="등록일" placeholder="등록일" value={date} onValueChange={setDate} /></>}
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="이름·부서 검색" />
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button></FilterBarActions>
</FilterBar>
</form>
<div className="hm-data-table-view-options"><DataTableViewOptions table={table} labels={columnLabels} /></div>
</div>
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
</div>
</CardContent>
</Card>
}
const meta = { title: "Patterns/Data/Data Table", parameters: { layout: "padded", controls: { disable: true } } } satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
// Representative Story: selection (mixed header), sorting, column visibility, row actions and pagination with page size.
export const All: Story = {
name: "Data Table",
parameters: { layout: "fullscreen" },
render: () => (
<StoryPage>
<StoryPageHeader title="Data Table" />
<StorySection title="Overview">
<Basic />
</StorySection>
<StorySection title="기본 목록 검색"><WithFilterBar extended={false} /></StorySection>
<StorySection title="추가 조건 목록 검색"><WithFilterBar /></StorySection>
</StoryPage>
),
}
// Combination Story: Filter Bar (search, status, department, date, reset) filtering the Data Table rows.
export const WithFilterBarStory: Story = { tags: ["verification"], name: "With Filter Bar", render: () => <WithFilterBar /> }
export const Loading: Story = { tags: ["verification"], render: () => <Basic loading /> }
export const Empty: Story = { tags: ["verification"], render: () => <Basic data={[]} /> }