pf-storybook

SKILL.md

PF Storybook 스토리 생성기

$ARGUMENTS 컴포넌트에 대한 Storybook 스토리를 생성합니다.


스토리 파일 위치

packages/ui/src/atoms/Button/
├── Button.tsx
├── Button.stories.tsx  ← 생성
├── types.ts
└── variants.ts

기본 스토리 구조

import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Atoms/Button",
  component: Button,
  tags: ["autodocs"],
  parameters: {
    layout: "centered",
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["default", "secondary", "outline", "ghost", "destructive"],
      description: "버튼 스타일 변형",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "버튼 크기",
    },
    disabled: {
      control: "boolean",
      description: "비활성화 상태",
    },
    onClick: {
      action: "clicked",
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// 기본 스토리
export const Default: Story = {
  args: {
    children: "버튼",
    variant: "default",
    size: "md",
  },
};

// Variants
export const Secondary: Story = {
  args: {
    children: "Secondary",
    variant: "secondary",
  },
};

export const Outline: Story = {
  args: {
    children: "Outline",
    variant: "outline",
  },
};

export const Ghost: Story = {
  args: {
    children: "Ghost",
    variant: "ghost",
  },
};

export const Destructive: Story = {
  args: {
    children: "삭제",
    variant: "destructive",
  },
};

// Sizes
export const Small: Story = {
  args: {
    children: "Small",
    size: "sm",
  },
};

export const Large: Story = {
  args: {
    children: "Large",
    size: "lg",
  },
};

// States
export const Disabled: Story = {
  args: {
    children: "Disabled",
    disabled: true,
  },
};

export const Loading: Story = {
  args: {
    children: "Loading...",
    disabled: true,
  },
  render: (args) => (
    <Button {...args}>
      <Spinner className="mr-2 h-4 w-4 animate-spin" />
      Loading...
    </Button>
  ),
};

// With Icon
export const WithIcon: Story = {
  args: {
    children: "설정",
  },
  render: (args) => (
    <Button {...args}>
      <SettingsIcon className="mr-2 h-4 w-4" />
      설정
    </Button>
  ),
};

복합 컴포넌트 스토리 (Composition)

import type { Meta, StoryObj } from "@storybook/react";
import { Sidebar } from "./Sidebar";
import { Home, Settings, Users, LogOut } from "lucide-react";

const meta: Meta<typeof Sidebar> = {
  title: "Organisms/Sidebar",
  component: Sidebar,
  tags: ["autodocs"],
  parameters: {
    layout: "fullscreen",
  },
  decorators: [
    (Story) => (
      <div className="h-screen">
        <Story />
      </div>
    ),
  ],
};

export default meta;
type Story = StoryObj<typeof Sidebar>;

export const Default: Story = {
  render: () => (
    <Sidebar defaultCollapsed={false}>
      <Sidebar.Header title="Dashboard">
        <Sidebar.CollapseButton iconOnly />
      </Sidebar.Header>

      <Sidebar.Content>
        <Sidebar.Section label="메인">
          <Sidebar.Item icon={<Home />} active>
          </Sidebar.Item>
          <Sidebar.Item icon={<Users />}>사용자</Sidebar.Item>
          <Sidebar.Item icon={<Settings />}>설정</Sidebar.Item>
        </Sidebar.Section>
      </Sidebar.Content>

      <Sidebar.Footer>
        <Sidebar.Item icon={<LogOut />}>로그아웃</Sidebar.Item>
      </Sidebar.Footer>
    </Sidebar>
  ),
};

export const Collapsed: Story = {
  render: () => <Sidebar defaultCollapsed>{/* ... */}</Sidebar>,
};

export const WithBadge: Story = {
  render: () => (
    <Sidebar>
      <Sidebar.Content>
        <Sidebar.Item icon={<Bell />} badge={5}>
          알림
        </Sidebar.Item>
      </Sidebar.Content>
    </Sidebar>
  ),
};

폼 컴포넌트 스토리

import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./Input";

const meta: Meta<typeof Input> = {
  title: "Atoms/Input",
  component: Input,
  tags: ["autodocs"],
  argTypes: {
    type: {
      control: "select",
      options: ["text", "email", "password", "number"],
    },
    disabled: { control: "boolean" },
    error: { control: "text" },
  },
};

export default meta;
type Story = StoryObj<typeof Input>;

export const Default: Story = {
  args: {
    placeholder: "입력하세요",
  },
};

export const WithLabel: Story = {
  render: () => (
    <div className="space-y-2">
      <label htmlFor="email" className="text-sm font-medium">
        이메일
      </label>
      <Input id="email" type="email" placeholder="email@example.com" />
    </div>
  ),
};

export const WithError: Story = {
  args: {
    placeholder: "이메일",
    error: "유효한 이메일을 입력하세요",
    defaultValue: "invalid-email",
  },
  render: (args) => (
    <div className="space-y-2">
      <Input {...args} aria-invalid="true" />
      <p className="text-sm text-red-500">{args.error}</p>
    </div>
  ),
};

export const Password: Story = {
  args: {
    type: "password",
    placeholder: "비밀번호",
  },
};

인터랙티브 스토리

import { useState } from "react";

export const Controlled: Story = {
  render: function ControlledStory() {
    const [value, setValue] = useState("");

    return (
      <div className="space-y-4">
        <Input value={value} onChange={(e) => setValue(e.target.value)} placeholder="입력하세요" />
        <p className="text-sm text-gray-500">입력값: {value || "(없음)"}</p>
      </div>
    );
  },
};

export const WithValidation: Story = {
  render: function ValidationStory() {
    const [value, setValue] = useState("");
    const [error, setError] = useState("");

    const validate = (v: string) => {
      if (!v) setError("필수 입력입니다");
      else if (v.length < 3) setError("3자 이상 입력하세요");
      else setError("");
    };

    return (
      <div className="space-y-2">
        <Input
          value={value}
          onChange={(e) => {
            setValue(e.target.value);
            validate(e.target.value);
          }}
          aria-invalid={!!error}
        />
        {error && <p className="text-sm text-red-500">{error}</p>}
      </div>
    );
  },
};

실행

# Storybook 실행
pnpm storybook

# 빌드
pnpm build-storybook

스토리 체크리스트

  • 모든 variants가 스토리로 존재
  • 모든 sizes가 스토리로 존재
  • disabled/loading 상태 스토리
  • 에러 상태 스토리 (폼 컴포넌트)
  • 인터랙티브 예제 (필요시)
  • autodocs 태그 추가
  • argTypes 정의 (controls)
Weekly Installs
2
First Seen
12 days ago
Installed on
opencode2
gemini-cli2
codebuddy2
github-copilot2
codex2
kimi-cli2