---
name: react-component-gen
description: |
  트리거: "react 컴포넌트 만들어줘", "component 생성", "리액트 컴포넌트", "react component gen"
  수행: 재사용 가능한 React 컴포넌트를 TypeScript + Props 타입 + 접근성(a11y) + Storybook 스텁과 함께 생성
  출력: 컴포넌트 파일(.tsx), 스타일 파일(.module.css 또는 Tailwind), Storybook 스텁(.stories.tsx)
---

# React Component Generator

## 목적

사용자가 요청한 UI 컴포넌트를 TypeScript 기반의 재사용 가능한 React 컴포넌트로 즉시 생성한다.
Props 인터페이스, 접근성 속성, Storybook 스텁까지 한 번에 제공하여 개발 생산성을 극대화한다.

## 실행 절차

1. **요구사항 파악**: 컴포넌트 이름, 용도, 필요한 Props, 스타일 방식(Tailwind / CSS Modules / styled-components) 확인
2. **Props 인터페이스 설계**: 필수/선택 Props 구분, 이벤트 핸들러 타입 정의
3. **컴포넌트 구현**: 함수형 컴포넌트 + forwardRef 적용(필요 시), 조건부 렌더링 처리
4. **접근성 적용**: aria-* 속성, role, tabIndex, keyboard 이벤트 처리
5. **Storybook 스텁 생성**: Default, 각 주요 variant별 Story 작성
6. **사용 예시 제공**: 실제 사용 코드 스니펫 포함

## 출력 형식

### 파일 구조
```
components/
  ComponentName/
    ComponentName.tsx       ← 메인 컴포넌트
    ComponentName.module.css ← 스타일 (CSS Modules 사용 시)
    ComponentName.stories.tsx ← Storybook 스텁
    index.ts                ← re-export
```

### 컴포넌트 예시 (Button)

```tsx
// Button.tsx
import React, { forwardRef } from 'react';

export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** 버튼 스타일 변형 */
  variant?: ButtonVariant;
  /** 버튼 크기 */
  size?: ButtonSize;
  /** 로딩 상태 */
  isLoading?: boolean;
  /** 좌측 아이콘 */
  leftIcon?: React.ReactNode;
  /** 우측 아이콘 */
  rightIcon?: React.ReactNode;
  /** 전체 너비 사용 */
  fullWidth?: boolean;
}

const variantClasses: Record<ButtonVariant, string> = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
  secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400',
  danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
};

const sizeClasses: Record<ButtonSize, string> = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg',
};

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = 'primary',
      size = 'md',
      isLoading = false,
      leftIcon,
      rightIcon,
      fullWidth = false,
      children,
      disabled,
      className = '',
      ...props
    },
    ref
  ) => {
    const isDisabled = disabled || isLoading;

    return (
      <button
        ref={ref}
        disabled={isDisabled}
        aria-busy={isLoading}
        aria-disabled={isDisabled}
        className={[
          'inline-flex items-center justify-center gap-2 rounded-md font-medium',
          'transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2',
          variantClasses[variant],
          sizeClasses[size],
          fullWidth ? 'w-full' : '',
          isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
          className,
        ]
          .filter(Boolean)
          .join(' ')}
        {...props}
      >
        {isLoading ? (
          <span
            className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
            aria-hidden="true"
          />
        ) : (
          leftIcon && <span aria-hidden="true">{leftIcon}</span>
        )}
        {children}
        {!isLoading && rightIcon && <span aria-hidden="true">{rightIcon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';
```

```tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: { control: 'select' },
    size: { control: 'select' },
    isLoading: { control: 'boolean' },
    fullWidth: { control: 'boolean' },
  },
};

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

export const Default: Story = {
  args: { children: '버튼', variant: 'primary', size: 'md' },
};

export const Secondary: Story = {
  args: { children: '취소', variant: 'secondary' },
};

export const Loading: Story = {
  args: { children: '저장 중...', isLoading: true },
};

export const Danger: Story = {
  args: { children: '삭제', variant: 'danger' },
};

export const FullWidth: Story = {
  args: { children: '전체 너비 버튼', fullWidth: true },
};
```

```ts
// index.ts
export { Button } from './Button';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
```

## 사용 예시

**입력:**
> "검색 입력창 컴포넌트 만들어줘. placeholder, onSearch 콜백, 로딩 상태 지원해야 해."

**출력:**
- `SearchInput.tsx` - debounce 적용, Ctrl+K 단축키, aria-label 포함
- `SearchInput.stories.tsx` - Default, Loading, WithValue, Disabled 스토리
- `index.ts` - re-export

## 주의사항

- `React.FC` 대신 `forwardRef` + 명시적 반환 타입 사용
- 모든 이벤트 핸들러는 `React.MouseEvent<HTMLElement>` 등 구체적 타입 지정
- 색상·간격은 하드코딩 금지 — Tailwind 유틸리티 클래스 또는 CSS 변수 사용
- `aria-label` 없는 아이콘 전용 버튼 생성 금지
- 컴포넌트 displayName 반드시 설정 (디버깅 편의)
- 스타일 방식을 명시하지 않으면 Tailwind CSS 기본 사용
