Skip to content

Commit 68fe304

Browse files
SOIVclaude
andcommitted
feat(controls): Storybook 세팅 및 P0/P0.5 컴포넌트 스토리 추가
- @storybook/react-vite@8 + @storybook/addon-essentials@8 설치 - .storybook/main.ts, preview.tsx 설정 (라이트/다크 테마 토글 포함) - 18개 컴포넌트 스토리 파일 생성 (Button ~ Skeleton) - 루트 pnpm storybook 편의 스크립트 추가 (port 6007) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4142f1e commit 68fe304

23 files changed

Lines changed: 510 additions & 3 deletions

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
## Project Status
88

9-
Phase 1.5 진행 중 (2026-04-13 기준). Core UI Shell은 mock으로 동작하며 실제 백엔드 API 엔드포인트는 미구현. `packages/controls`에 React 컴포넌트 미구현 (`ready: false` 상태).
9+
Phase 1.5 진행 중 (2026-04-13 기준). Core UI Shell은 mock으로 동작하며 실제 백엔드 API 엔드포인트는 미구현. `packages/controls` P0/P0.5 컴포넌트 구현 완료 (`ready: true`). Storybook 세팅 완료 (port 6007).
1010

1111
---
1212

@@ -29,6 +29,9 @@ pnpm --filter api test # api만
2929
pnpm --filter core test # core만
3030
pnpm exec vitest run apps/api/src/loader/index.test.ts # 단일 파일
3131

32+
# Storybook (controls 컴포넌트 확인)
33+
pnpm storybook # http://localhost:6007 (port 6006 충돌 시 6007 사용)
34+
3235
# 타입 체크
3336
pnpm typecheck
3437
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test": "pnpm --recursive test",
2424
"lint": "pnpm --recursive lint",
2525
"typecheck": "pnpm --recursive typecheck",
26-
"format": "pnpm --recursive format"
26+
"format": "pnpm --recursive format",
27+
"storybook": "pnpm --filter @fieldstack/controls storybook"
2728
}
2829
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { StorybookConfig } from '@storybook/react-vite';
2+
3+
const config: StorybookConfig = {
4+
stories: ['../src/stories/**/*.stories.@(ts|tsx)'],
5+
addons: ['@storybook/addon-essentials'],
6+
framework: {
7+
name: '@storybook/react-vite',
8+
options: {},
9+
},
10+
};
11+
12+
export default config;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Preview, Decorator } from '@storybook/react';
2+
import '../src/styles/controls.css';
3+
4+
const withTheme: Decorator = (Story, context) => {
5+
const theme = (context.globals.theme as string) ?? 'light';
6+
document.documentElement.setAttribute('data-theme', theme);
7+
return <Story />;
8+
};
9+
10+
const preview: Preview = {
11+
globalTypes: {
12+
theme: {
13+
description: 'Color theme',
14+
defaultValue: 'light',
15+
toolbar: {
16+
title: 'Theme',
17+
icon: 'circlehollow',
18+
items: [
19+
{ value: 'light', icon: 'sun', title: 'Light' },
20+
{ value: 'dark', icon: 'moon', title: 'Dark' },
21+
],
22+
dynamicTitle: true,
23+
},
24+
},
25+
},
26+
decorators: [withTheme],
27+
parameters: {
28+
layout: 'centered',
29+
backgrounds: { disable: true },
30+
},
31+
};
32+
33+
export default preview;

packages/controls/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,21 @@
1515
"scripts": {
1616
"build": "tsc",
1717
"test": "vitest run --passWithNoTests",
18-
"typecheck": "tsc --noEmit"
18+
"typecheck": "tsc --noEmit",
19+
"storybook": "storybook dev -p 6006",
20+
"storybook:build": "storybook build"
1921
},
2022
"peerDependencies": {
2123
"react": ">=18",
2224
"react-dom": ">=18"
2325
},
2426
"devDependencies": {
27+
"@storybook/addon-essentials": "^8.6.18",
28+
"@storybook/react-vite": "^8.6.18",
2529
"@types/react": "^19.2.14",
30+
"@vitejs/plugin-react": "^4",
31+
"storybook": "^8",
32+
"vite": "^5",
2633
"vitest": "^2.1.9"
2734
}
2835
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Alert } from '../components/Alert.js';
3+
4+
const meta: Meta<typeof Alert> = {
5+
title: 'Controls/Alert',
6+
component: Alert,
7+
parameters: { layout: 'padded' },
8+
};
9+
export default meta;
10+
11+
type Story = StoryObj<typeof Alert>;
12+
13+
export const Success: Story = { args: { variant: 'success', message: '저장되었습니다.' } };
14+
export const Warning: Story = { args: { variant: 'warning', message: '변경사항이 저장되지 않을 수 있습니다.' } };
15+
export const Error: Story = { args: { variant: 'error', message: '오류가 발생했습니다. 다시 시도해주세요.' } };
16+
export const Info: Story = { args: { variant: 'info', message: '시스템 점검이 예정되어 있습니다.' } };
17+
export const WithTitle: Story = {
18+
args: { variant: 'error', title: '인증 실패', message: '이메일 또는 비밀번호를 확인해주세요.' },
19+
};
20+
export const Closable: Story = {
21+
args: { variant: 'info', message: '닫기 버튼이 있는 알림입니다.', closable: true },
22+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Button } from '../components/Button.js';
3+
4+
const meta: Meta<typeof Button> = {
5+
title: 'Controls/Button',
6+
component: Button,
7+
args: { children: 'Button' },
8+
};
9+
export default meta;
10+
11+
type Story = StoryObj<typeof Button>;
12+
13+
export const Primary: Story = { args: { variant: 'primary', children: 'Primary' } };
14+
export const Secondary: Story = { args: { variant: 'secondary', children: 'Secondary' } };
15+
export const Danger: Story = { args: { variant: 'danger', children: 'Danger' } };
16+
export const Ghost: Story = { args: { variant: 'ghost', children: 'Ghost' } };
17+
export const Loading: Story = { args: { variant: 'primary', loading: true, children: 'Loading' } };
18+
export const Disabled: Story = { args: { variant: 'primary', disabled: true, children: 'Disabled' } };
19+
export const Sizes: Story = {
20+
render: () => (
21+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
22+
<Button size="sm" variant="primary">Small</Button>
23+
<Button size="md" variant="primary">Medium</Button>
24+
<Button size="lg" variant="primary">Large</Button>
25+
</div>
26+
),
27+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useState } from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import { Checkbox, CheckboxGroup } from '../components/Checkbox.js';
4+
5+
const meta: Meta = { title: 'Controls/Checkbox' };
6+
export default meta;
7+
8+
export const Single: StoryObj = {
9+
render: () => {
10+
const [checked, setChecked] = useState(false);
11+
return <Checkbox checked={checked} onChange={setChecked} label="동의합니다" />;
12+
},
13+
};
14+
15+
export const Indeterminate: StoryObj = {
16+
render: () => <Checkbox checked={false} indeterminate onChange={() => {}} label="일부 선택됨" />,
17+
};
18+
19+
export const Group: StoryObj = {
20+
render: () => {
21+
const [values, setValues] = useState<string[]>(['a']);
22+
return (
23+
<CheckboxGroup
24+
values={values}
25+
onChange={setValues}
26+
options={[
27+
{ label: '항목 A', value: 'a' },
28+
{ label: '항목 B', value: 'b' },
29+
{ label: '항목 C (비활성)', value: 'c', disabled: true },
30+
]}
31+
/>
32+
);
33+
},
34+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { EmptyState } from '../components/EmptyState.js';
3+
4+
const meta: Meta<typeof EmptyState> = {
5+
title: 'Controls/EmptyState',
6+
component: EmptyState,
7+
parameters: { layout: 'padded' },
8+
};
9+
export default meta;
10+
11+
type Story = StoryObj<typeof EmptyState>;
12+
13+
export const Default: Story = {
14+
args: { icon: '📭', title: '데이터가 없습니다', description: '항목을 추가하면 여기에 표시됩니다.' },
15+
};
16+
17+
export const WithAction: Story = {
18+
args: {
19+
icon: '📦',
20+
title: '모듈이 없습니다',
21+
description: '마켓플레이스에서 모듈을 설치해보세요.',
22+
action: { label: '마켓플레이스 보기', onClick: () => alert('navigate') },
23+
},
24+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { FormField } from '../components/FormField.js';
3+
import { Input } from '../components/Input.js';
4+
5+
const meta: Meta = { title: 'Controls/FormField' };
6+
export default meta;
7+
8+
export const Default: StoryObj = {
9+
render: () => (
10+
<FormField label="이메일" htmlFor="email">
11+
<Input id="email" type="email" placeholder="name@example.com" />
12+
</FormField>
13+
),
14+
};
15+
16+
export const Required: StoryObj = {
17+
render: () => (
18+
<FormField label="이름" htmlFor="name" required>
19+
<Input id="name" placeholder="홍길동" />
20+
</FormField>
21+
),
22+
};
23+
24+
export const WithError: StoryObj = {
25+
render: () => (
26+
<FormField label="이메일" htmlFor="email-err" required error="올바른 이메일 형식이 아닙니다.">
27+
<Input id="email-err" type="email" defaultValue="invalid" />
28+
</FormField>
29+
),
30+
};
31+
32+
export const WithHelpText: StoryObj = {
33+
render: () => (
34+
<FormField label="비밀번호" htmlFor="pw" helpText="영문·숫자·특수문자 포함 8자 이상">
35+
<Input id="pw" type="password" placeholder="비밀번호 입력" />
36+
</FormField>
37+
),
38+
};

0 commit comments

Comments
 (0)