feat: storybook 생성
This commit is contained in:
113
web-app/app/shared/components/button/Button.stories.tsx
Normal file
113
web-app/app/shared/components/button/Button.stories.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Button } from './Button'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['primary', 'light', 'green', 'lightGreen', 'black', 'gray', 'orange', 'navy', 'lightNavy'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
isDisabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
isPending: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Button>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: '버튼',
|
||||
color: 'primary',
|
||||
size: 'medium',
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
children: '작은 버튼',
|
||||
size: 'small',
|
||||
},
|
||||
}
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
children: '중간 버튼',
|
||||
size: 'medium',
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: '큰 버튼',
|
||||
size: 'large',
|
||||
},
|
||||
}
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: 'Primary',
|
||||
color: 'primary',
|
||||
},
|
||||
}
|
||||
|
||||
export const Light: Story = {
|
||||
args: {
|
||||
children: 'Light',
|
||||
color: 'light',
|
||||
},
|
||||
}
|
||||
|
||||
export const Green: Story = {
|
||||
args: {
|
||||
children: 'Green',
|
||||
color: 'green',
|
||||
},
|
||||
}
|
||||
|
||||
export const Navy: Story = {
|
||||
args: {
|
||||
children: 'Navy',
|
||||
color: 'navy',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: '비활성화',
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: '로딩 중',
|
||||
isPending: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllColors: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button color="primary">Primary</Button>
|
||||
<Button color="light">Light</Button>
|
||||
<Button color="green">Green</Button>
|
||||
<Button color="lightGreen">Light Green</Button>
|
||||
<Button color="black">Black</Button>
|
||||
<Button color="gray">Gray</Button>
|
||||
<Button color="orange">Orange</Button>
|
||||
<Button color="navy">Navy</Button>
|
||||
<Button color="lightNavy">Light Navy</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import type { InputProps as AriaInputProps } from 'react-aria-components';
|
||||
import { Input as AriaInput } from 'react-aria-components';
|
||||
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import style from './style';
|
||||
|
||||
export interface InputProps extends AriaInputProps {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import Input from './Input'
|
||||
import { InputGroup } from './InputGroup'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/InputGroup',
|
||||
component: InputGroup,
|
||||
argTypes: {
|
||||
isReadOnly: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof InputGroup>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => (
|
||||
<InputGroup {...args}>
|
||||
<Input placeholder="입력해주세요" />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithValue: Story = {
|
||||
render: (args) => (
|
||||
<InputGroup {...args}>
|
||||
<Input defaultValue="입력된 값" />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<InputGroup>
|
||||
<Input placeholder="비활성화" disabled />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
isReadOnly: true,
|
||||
},
|
||||
render: (args) => (
|
||||
<InputGroup {...args}>
|
||||
<Input defaultValue="읽기 전용" readOnly />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const NumberInput: Story = {
|
||||
render: () => (
|
||||
<InputGroup>
|
||||
<Input type="number" placeholder="숫자 입력" />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithPrefix: Story = {
|
||||
render: () => (
|
||||
<InputGroup>
|
||||
<span className="pl-3 text-dabeeo-gray-99">@</span>
|
||||
<Input placeholder="사용자명" />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithSuffix: Story = {
|
||||
render: () => (
|
||||
<InputGroup>
|
||||
<Input placeholder="금액" type="number" />
|
||||
<span className="pr-3 text-dabeeo-gray-99">원</span>
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
|
||||
export const CustomWidth: Story = {
|
||||
render: () => (
|
||||
<InputGroup className="w-60">
|
||||
<Input placeholder="너비 240px" />
|
||||
</InputGroup>
|
||||
),
|
||||
}
|
||||
35
web-app/app/shared/components/inputGroup/style.ts
Normal file
35
web-app/app/shared/components/inputGroup/style.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
export default tv({
|
||||
slots: {
|
||||
container: 'relative flex flex-col justify-center items-start bg-white',
|
||||
base: [
|
||||
'h-9',
|
||||
'flex',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'text-dabeeo-black-34',
|
||||
'leading-[18px]',
|
||||
'border',
|
||||
'border-dabeeo-gray-be',
|
||||
'has-data-focused:border-dabeeo-black-34',
|
||||
'has-data-hovered:border-dabeeo-black-34',
|
||||
'has-data-disabled:bg-dabeeo-gray-eb',
|
||||
'has-data-disabled:text-dabeeo-gray-99',
|
||||
'has-data-disabled:border-dabeeo-gray-be',
|
||||
'has-data-invalid:border-dabeeo-red',
|
||||
'has-data-invalid:has-data-focused:border-dabeeo-red',
|
||||
'has-data-invalid:has-data-hovered:border-dabeeo-red',
|
||||
'w-full',
|
||||
'has-[[type=number]]:leading-6',
|
||||
],
|
||||
input: ['w-full', 'px-3', 'outline-0', '[&[type=number]]:pr-1.5'],
|
||||
},
|
||||
variants: {
|
||||
isReadOnly: {
|
||||
true: {
|
||||
base: 'bg-dabeeo-yellow-secondary has-data-hovered:border-dabeeo-gray-be has-data-focused:border-dabeeo-gray-be',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
142
web-app/app/shared/components/modal/Modal.stories.tsx
Normal file
142
web-app/app/shared/components/modal/Modal.stories.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '../button/Button'
|
||||
import { ModalBody, ModalFooter, ModalHeader, ModalRoot } from './Modal'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Modal',
|
||||
component: ModalRoot,
|
||||
argTypes: {
|
||||
isDismissable: { control: 'boolean' },
|
||||
isKeyboardDismissDisabled: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof ModalRoot>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: function Render() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>모달 열기</Button>
|
||||
<ModalRoot isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ModalHeader>모달 제목</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-sm">모달 내용입니다.</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="gray" size="large" onClick={() => setIsOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button color="primary" size="large" onClick={() => setIsOpen(false)}>
|
||||
확인
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutCloseButton: Story = {
|
||||
render: function Render() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>모달 열기</Button>
|
||||
<ModalRoot isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ModalHeader hasCloseButton={false}>닫기 버튼 없음</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-sm">헤더에 닫기 버튼이 없습니다.</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" size="large" onClick={() => setIsOpen(false)}>
|
||||
확인
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const LongContent: Story = {
|
||||
render: function Render() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>모달 열기</Button>
|
||||
<ModalRoot isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ModalHeader>긴 내용</ModalHeader>
|
||||
<ModalBody className="max-h-60 overflow-y-auto">
|
||||
{Array.from({ length: 20 }, (_, i) => (
|
||||
<p key={i} className="text-sm mb-2">
|
||||
내용 {i + 1}: Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</p>
|
||||
))}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" size="large" onClick={() => setIsOpen(false)}>
|
||||
확인
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const NotDismissable: Story = {
|
||||
render: function Render() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>모달 열기</Button>
|
||||
<ModalRoot
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={true}
|
||||
>
|
||||
<ModalHeader hasCloseButton={false}>닫을 수 없음</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-sm">배경 클릭이나 ESC로 닫을 수 없습니다.</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" size="large" onClick={() => setIsOpen(false)}>
|
||||
확인
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const ConfirmDialog: Story = {
|
||||
render: function Render() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Button color="primary" onClick={() => setIsOpen(true)}>삭제하기</Button>
|
||||
<ModalRoot isOpen={isOpen} onOpenChange={setIsOpen} className="w-75">
|
||||
<ModalBody className="px-7.5 py-8">
|
||||
<p className="text-sm text-dabeeo-black-34 font-medium text-center">
|
||||
정말 삭제하시겠습니까?
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="gray" size="large" onClick={() => setIsOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button color="primary" size="large" onClick={() => setIsOpen(false)}>
|
||||
삭제
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
import { Pagination } from './Pagination'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Pagination',
|
||||
component: Pagination,
|
||||
argTypes: {
|
||||
totalPages: { control: 'number' },
|
||||
currentPage: { control: 'number' },
|
||||
pageCount: { control: 'number' },
|
||||
},
|
||||
} satisfies Meta<typeof Pagination>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
totalPages: 100,
|
||||
currentPage: 0,
|
||||
pageCount: 10,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
|
||||
export const MiddlePage: Story = {
|
||||
args: {
|
||||
totalPages: 100,
|
||||
currentPage: 45,
|
||||
pageCount: 10,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
|
||||
export const LastPage: Story = {
|
||||
args: {
|
||||
totalPages: 100,
|
||||
currentPage: 99,
|
||||
pageCount: 10,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
|
||||
export const FewPages: Story = {
|
||||
args: {
|
||||
totalPages: 5,
|
||||
currentPage: 0,
|
||||
pageCount: 10,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
|
||||
export const SinglePage: Story = {
|
||||
args: {
|
||||
totalPages: 1,
|
||||
currentPage: 0,
|
||||
pageCount: 10,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: () => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<Pagination
|
||||
totalPages={50}
|
||||
currentPage={currentPage}
|
||||
pageCount={10}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
<p className="text-sm text-dabeeo-gray-44">
|
||||
현재 페이지: {currentPage + 1} / 50
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomPageCount: Story = {
|
||||
args: {
|
||||
totalPages: 100,
|
||||
currentPage: 0,
|
||||
pageCount: 5,
|
||||
onPageChange: () => {},
|
||||
},
|
||||
}
|
||||
98
web-app/app/shared/components/select/Select.stories.tsx
Normal file
98
web-app/app/shared/components/select/Select.stories.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
import { Select, type SelectOption } from './Select'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Select',
|
||||
component: Select,
|
||||
argTypes: {
|
||||
isDisabled: { control: 'boolean' },
|
||||
isReadOnly: { control: 'boolean' },
|
||||
placement: {
|
||||
control: 'select',
|
||||
options: ['bottom', 'top'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Select>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const sampleItems: SelectOption[] = [
|
||||
{ label: '옵션 1', value: '1' },
|
||||
{ label: '옵션 2', value: '2' },
|
||||
{ label: '옵션 3', value: '3' },
|
||||
{ label: '비활성화 옵션', value: '4', isDisabled: true },
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: sampleItems,
|
||||
placeholder: '선택해주세요',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithSelectedValue: Story = {
|
||||
args: {
|
||||
items: sampleItems,
|
||||
defaultSelectedKey: '2',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
items: sampleItems,
|
||||
defaultSelectedKey: '1',
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
items: sampleItems,
|
||||
defaultSelectedKey: '1',
|
||||
isReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const PlacementTop: Story = {
|
||||
args: {
|
||||
items: sampleItems,
|
||||
placeholder: '위로 열림',
|
||||
placement: 'top',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="pt-40">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: function Render() {
|
||||
const [value, setValue] = useState<string | number>('1')
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Select
|
||||
items={sampleItems}
|
||||
selectedKey={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
<p className="text-sm">선택된 값: {value}</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
items: Array.from({ length: 20 }, (_, i) => ({
|
||||
label: `옵션 ${i + 1}`,
|
||||
value: String(i + 1),
|
||||
})),
|
||||
placeholder: '많은 옵션',
|
||||
maxHeight: 200,
|
||||
},
|
||||
}
|
||||
265
web-app/app/shared/components/table/Table.stories.tsx
Normal file
265
web-app/app/shared/components/table/Table.stories.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
import { Table } from './Table'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Table',
|
||||
component: Table,
|
||||
args: {
|
||||
children: null
|
||||
},
|
||||
argTypes: {
|
||||
isLayoutFixed: { control: 'boolean' },
|
||||
isFullHeight: { control: 'boolean' },
|
||||
isHoverable: { control: 'boolean' },
|
||||
isClickable: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof Table>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const sampleData = [
|
||||
{ id: 1, name: '홍길동', email: 'hong@example.com', role: '관리자' },
|
||||
{ id: 2, name: '김철수', email: 'kim@example.com', role: '사용자' },
|
||||
{ id: 3, name: '이영희', email: 'lee@example.com', role: '사용자' },
|
||||
{ id: 4, name: '박민수', email: 'park@example.com', role: '편집자' },
|
||||
{ id: 5, name: '최지은', email: 'choi@example.com', role: '사용자' },
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="h-80">
|
||||
<Table>
|
||||
<Table.Caption>
|
||||
<Table.CaptionLeft>
|
||||
<Table.Total count={sampleData.length} />
|
||||
</Table.CaptionLeft>
|
||||
</Table.Caption>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col width={100} />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center">ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center">역할</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{sampleData.map((row) => (
|
||||
<Table.Row key={row.id}>
|
||||
<Table.Cell align="center">{row.id}</Table.Cell>
|
||||
<Table.Cell>{row.name}</Table.Cell>
|
||||
<Table.Cell>{row.email}</Table.Cell>
|
||||
<Table.Cell align="center">{row.role}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
render: () => (
|
||||
<div className="h-60">
|
||||
<Table>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col width={100} />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center">ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center">역할</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Empty colSpan={4} />
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => (
|
||||
<div className="h-60">
|
||||
<Table>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col width={100} />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center">ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center">역할</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Loading colSpan={4} />
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Selectable: Story = {
|
||||
render: function Render() {
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
return (
|
||||
<div className="h-80">
|
||||
<Table>
|
||||
<Table.Caption>
|
||||
<Table.CaptionLeft>
|
||||
<Table.Total count={sampleData.length} />
|
||||
{selectedId && (
|
||||
<span className="text-sm text-primary">선택: {selectedId}</span>
|
||||
)}
|
||||
</Table.CaptionLeft>
|
||||
</Table.Caption>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col width={100} />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center">ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center">역할</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{sampleData.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
isSelected={selectedId === row.id}
|
||||
onClick={() => setSelectedId(row.id)}
|
||||
>
|
||||
<Table.Cell align="center">{row.id}</Table.Cell>
|
||||
<Table.Cell>{row.name}</Table.Cell>
|
||||
<Table.Cell>{row.email}</Table.Cell>
|
||||
<Table.Cell align="center">{row.role}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const GroupedHeader: Story = {
|
||||
render: () => (
|
||||
<div className="h-80">
|
||||
<Table>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center" rowSpan={2}>ID</Table.HeaderCell>
|
||||
<Table.HeaderCell rowSpan={2}>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center" colSpan={2} isGroupTitle>연락처</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell isGroupChild>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell isGroupChild>전화번호</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell align="center">1</Table.Cell>
|
||||
<Table.Cell>홍길동</Table.Cell>
|
||||
<Table.Cell>hong@example.com</Table.Cell>
|
||||
<Table.Cell>010-1234-5678</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell align="center">2</Table.Cell>
|
||||
<Table.Cell>김철수</Table.Cell>
|
||||
<Table.Cell>kim@example.com</Table.Cell>
|
||||
<Table.Cell>010-9876-5432</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ManyRows: Story = {
|
||||
render: () => {
|
||||
const manyData = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `사용자 ${i + 1}`,
|
||||
email: `user${i + 1}@example.com`,
|
||||
role: i % 3 === 0 ? '관리자' : '사용자',
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="h-96">
|
||||
<Table>
|
||||
<Table.Caption>
|
||||
<Table.CaptionLeft>
|
||||
<Table.Total count={manyData.length} />
|
||||
</Table.CaptionLeft>
|
||||
</Table.Caption>
|
||||
<Table.Container>
|
||||
<Table.Colgroup>
|
||||
<Table.Col width={60} />
|
||||
<Table.Col width={120} />
|
||||
<Table.Col />
|
||||
<Table.Col width={100} />
|
||||
</Table.Colgroup>
|
||||
<Table.Header>
|
||||
<Table.HeaderRow>
|
||||
<Table.HeaderCell align="center">ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>이름</Table.HeaderCell>
|
||||
<Table.HeaderCell>이메일</Table.HeaderCell>
|
||||
<Table.HeaderCell align="center">역할</Table.HeaderCell>
|
||||
</Table.HeaderRow>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{manyData.map((row) => (
|
||||
<Table.Row key={row.id}>
|
||||
<Table.Cell align="center">{row.id}</Table.Cell>
|
||||
<Table.Cell>{row.name}</Table.Cell>
|
||||
<Table.Cell>{row.email}</Table.Cell>
|
||||
<Table.Cell align="center">{row.role}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Container>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user