Conversation
📝 Walkthrough개요Storybook 통합을 추가하고 여러 UI 컴포넌트를 대대적으로 리팩토링하는 종합적인 풀 리퀘스트입니다. Button 컴포넌트의 스타일 시스템을 재설계하고, Label 컴포넌트를 분리하며, Callout과 Card 컴포넌트를 재구성합니다. PromptDTO를 mapper 모듈로 이동하고, 모든 주요 컴포넌트에 대한 Storybook 스토리를 추가합니다. 변경 사항
예상 코드 리뷰 노력🎯 4 (복잡) | ⏱️ ~60분 관련 가능성이 있는 풀 리퀘스트
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Card/Card.types.ts (1)
4-8:⚠️ Potential issue | 🟡 Minor
name필드 타입 확인 필요
name: string[]로 정의되어 있지만,Card.tsx에서는badge.name을 Badge의 children으로 직접 전달합니다. 실제 사용 패턴에 맞게name: string으로 변경하거나, 배열을 조인하는 로직이 필요합니다.
🤖 Fix all issues with AI agents
In `@src/components/Badge/Badge.stories.tsx`:
- Around line 1-42: The argTypes in Badge.stories.tsx currently use string
controls (e.g., control: 'radio') which is not CSF3-compliant; update the
argTypes for size and variant inside the meta object (argTypes -> size and
argTypes -> variant) to use object-form controls (control: { type: 'radio' })
instead of the string form, keep existing options, descriptions, and table
settings unchanged, and ensure the children argType remains as control: 'text'
converted similarly to control: { type: 'text' } if you want consistent
object-form usage.
In `@src/components/Callout/Callout.styles.ts`:
- Around line 12-15: calloutBodyStyles is missing an "as const" assertion which
breaks parity with calloutVariantMap and calloutTitleStyles; add "as const" to
calloutBodyStyles to ensure literal type inference. Locate the export named
calloutBodyStyles in Callout.styles.ts and append the as const assertion to the
object (matching the style used for calloutVariantMap and calloutTitleStyles) so
the keys/values are treated as readonly literals for proper typing.
In `@src/components/Card/Card.stories.tsx`:
- Around line 1-61: The argTypes block on the meta object incorrectly uses
"object: 'content'" and "object: 'stats'"; update those entries to use
Storybook's control syntax (e.g., change content and stats to use control:
'object' or control: { type: 'object' }) so Storybook recognizes them; locate
the argTypes inside the meta declaration (the "argTypes" property) and replace
the wrong object keys for content and stats with the proper control
configuration while keeping the descriptions.
In `@src/components/Card/Card.tsx`:
- Around line 15-18: CardBadges.name is typed as string[] in Card.types.ts but
the Card component maps badges and passes badge.name directly as Badge children
(badges.map -> <Badge ...>{badge.name}</Badge>), which can render an array
incorrectly; fix by either changing CardBadges.name to string in Card.types.ts
if each badge has a single name, or update the render to join the array into a
string (e.g., badge.name.join(', ') or appropriate separator) before passing it
to Badge, ensuring Badge receives a string child and preserving component props
size/variant.
In `@src/components/Dialog/Dialog.tsx`:
- Line 24: The close icon-only Button in Dialog.tsx lacks an accessible name, so
add an accessible label (e.g., aria-label or aria-labelledby) to the Button
instance (the <Button icon="close" variant="ghost" className="absolute top-4
right-4 p-2" />) so screen readers can announce its purpose; use a localized
string like "Close" and prefer aria-label for a one-off label, or reference a
visually-hidden element via aria-labelledby if you already have a label node.
In `@src/components/Label/Label.styles.ts`:
- Around line 3-7: The textLabelSizeMap constant uses a non-standard Tailwind
class for the md key; update the md mapping in textLabelSizeMap to use
'text-base' instead of 'text-md' so it aligns with Tailwind defaults (e.g.,
change the md entry in textLabelSizeMap to 'text-base').
In `@src/components/Logo/Logo.tsx`:
- Around line 6-8: The IconLogo component declares IconLogoProps (which
currently extends HTMLAttributes<HTMLDivElement>) but never forwards additional
props; either remove the HTMLAttributes extension from the IconLogoProps type to
match current behavior, or update the IconLogo implementation to accept and
forward rest props (e.g., change signature to IconLogo = ({ size = 'xl', ...rest
}: IconLogoProps) and spread {...rest} onto the Icon component) so props
declared in IconLogoProps are actually passed through; update the type or the
IconLogo function accordingly (referencing IconLogoProps, IconLogo, and Icon).
In `@src/components/Pagination/Pagination.tsx`:
- Around line 24-25: The Pagination component is concatenating className into
the container string which can insert "undefined" into the DOM; set a safe
default for className (e.g., empty string) so it never becomes undefined. Update
the Pagination component's props handling (or function parameter default) for
the className prop used in the JSX container div (the expression
`className={`flex items-center justify-center gap-1 ${className}`}`) to use a
fallback like an empty string (or null-coalescing) so the template never
includes "undefined".
In `@src/mappers/promptMapper.ts`:
- Around line 56-58: The category default fallback is broken because
tags.categories.map(...) returns an array (truthy even when empty) and you’re
assigning a string '기타', causing type mismatch; change the assignment for
category to produce an array of strings and use a length check on
tags.categories (e.g., tags.categories && tags.categories.length ?
tags.categories.map(c => c.name) : ['기타']) so that category is always string[];
adjust the platform mapping similarly if needed and update any usage expecting
string[] to match the new shape (look for tags.categories, tags.platforms, and
the tags.category property in promptMapper.ts).
- Around line 51-55: In promptMapper (the block constructing the stats object),
likeCount and copyCount are incorrectly using stats.viewCount; update the
mapping so likeCount maps to stats.likeCount and copyCount maps to
stats.copyCount, using handleFormattedCount for each (leave viewCount mapping as
handleFormattedCount(stats.viewCount)); ensure you update the object keys in the
promptMapper function that builds PromptStats so the three fields use their
corresponding source properties.
In `@src/pages/PromptDetail/PromptDetailPage.tsx`:
- Line 43: The PromptDetailPage currently passes content.description into the
PromptContentBox but the PromptContentBox should display the actual prompt text;
update the render in PromptDetailPage to pass content.prompt (not
content.description) to PromptContentBox while keeping the promptId argument
(i.e., replace the prop currently using content.description with
content.prompt), ensuring PromptContentBox receives the correct prompt string
per the PromptContent interface.
In `@vite.config.ts`:
- Around line 1-41: Add the recommended storybookScript option to the
storybookTest plugin call so Storybook launch is explicit: update the
storybookTest({ configDir: path.join(dirname, '.storybook') }) invocation to
include a storybookScript property (for example storybookScript: 'npm run
storybook') so the test project (defined in test.projects) uses that script to
start Storybook; modify the call to storybookTest in the test.projects.plugins
array accordingly.
🧹 Nitpick comments (19)
src/pages/PromptDetail/_components/BackToListButton.tsx (1)
9-13: 리팩토링된 Button 스타일 적용이 잘 되었습니다.
variant="ghost"와 추가된 스타일 클래스들이 깔끔하게 적용되었습니다.다만,
navigate(-1)사용 시 사용자가 직접 URL로 접근하거나 공유 링크를 통해 들어온 경우 예상치 못한 페이지로 이동할 수 있습니다. 버튼 텍스트가 "목록으로"이므로, 명시적인 목록 경로(예:/)로 이동하는 것이 더 안정적일 수 있습니다.💡 명시적 경로 사용 제안
- onClick={() => navigate(-1)} + onClick={() => navigate('/')}또는 history가 있을 때만 뒤로 가고, 없으면 목록으로 이동하는 방식도 고려해볼 수 있습니다.
src/global.css (1)
41-48: 색상 포맷 일관성 검토 권장기존 브랜드 색상들은
oklch()포맷을 사용하는데, 새로 추가된brand-purple계열 색상들은 hex 포맷(#6d5bd0)을 사용합니다.brand-blue계열은oklch()를 사용하고 있어 일관성이 떨어집니다.유지보수를 위해 동일한 포맷으로 통일하는 것을 고려해 주세요.
.storybook/preview.ts (1)
12-14: 접근성 테스트 후속 작업 필요
a11y.test가'todo'로 설정되어 있습니다. 추후 접근성 테스트 구현 시 이 부분을 업데이트해 주세요.접근성 테스트 설정을 위한 이슈를 생성할까요?
src/components/NavigationBar/_components/Dialog.tsx (2)
76-81: 불필요한 주석 처리된 코드를 제거하세요.리팩토링이 완료되었으므로 주석 처리된 이전 버튼 코드를 삭제해야 합니다. 코드베이스의 가독성을 위해 사용하지 않는 코드는 제거하는 것이 좋습니다.
♻️ 제안된 수정 사항
secondaryAction={ - // <button - // type="button" - // onClick={handleWithdraw} - // className="flex-1 px-4 py-3 bg-red-600 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"> - // <span className="text-white">탈퇴하기</span> - // </button> <Button onClick={handleWithdraw} variant="destructive" className="flex-1" size="lg"> 탈퇴하기 </Button> }
53-54: 동일한 분기 로직을 단순화하세요.
result가 truthy이든 falsy이든 상관없이logout()을 호출하고 있습니다. 이것이 의도된 동작이라면 조건문을 제거하고logout()만 호출하는 것이 명확합니다.♻️ 제안된 수정 사항
const result = await deleteAccount(); - - if (result) logout(); - else logout(); + logout();src/components/Divider/Divider.tsx (1)
11-11:clsx또는tailwind-merge를 사용한 클래스 합성을 고려하세요.이 PR에서
clsx와tailwind-merge가 dependencies로 추가되었으므로, 템플릿 리터럴 대신 이러한 유틸리티를 사용하면 클래스 충돌을 안전하게 처리하고 일관성을 유지할 수 있습니다.♻️ 제안된 수정 사항
+import { twMerge } from 'tailwind-merge'; +import clsx from 'clsx'; import type { DividerProps } from './Divider.types'; const Divider = ({ orientation = 'horizontal', className = '' }: DividerProps) => { const baseStyle = orientation === 'horizontal' ? 'w-full h-px' : 'h-auto w-px min-h-[1rem] self-stretch'; return ( <div role="separator" aria-orientation={orientation} - className={`bg-gray-100 ${baseStyle} ${className}`} + className={twMerge(clsx('bg-gray-100', baseStyle, className))} /> ); };src/components/Logo/Logo.stories.tsx (1)
35-37:Basic스토리의 타입 정의가 meta와 다릅니다.
Basic스토리는BasicLogo컴포넌트를 렌더링하지만 meta는IconLogo를 기준으로 설정되어 있습니다. 이 방식은 동작하지만, autodocs에서 argTypes가 올바르게 추론되지 않을 수 있습니다.BasicLogo에 대한 별도의 argTypes를 추가하거나 별도 스토리 파일로 분리하는 것을 고려해 보세요.src/components/Badge/Badge.styles.ts (2)
1-4:variantStyleMap에as const단언이 누락되었습니다.
variantIconMap과badgeSizeMap은as const를 사용하고 있지만,variantStyleMap에는 적용되어 있지 않습니다. 일관성과 타입 추론 개선을 위해as const를 추가하는 것을 권장합니다.♻️ 제안된 수정
export const variantStyleMap = { category: 'bg-brand-purple-light text-brand-purple', platform: 'bg-brand-blue-light text-brand-blue', -}; +} as const;
11-14:badgeSizeMap에도as const단언 추가를 권장합니다.타입 추론의 일관성을 위해
variantIconMap과 동일하게as const를 추가하세요.♻️ 제안된 수정
export const badgeSizeMap = { sm: 'gap-1.5 px-2.5 py-1 text-xs', md: 'gap-1.5 px-3 py-1.5 text-sm', -}; +} as const;src/components/Label/Label.stories.tsx (1)
4-12:meta.component가InputLabel만 지정되어 있어TextLabel스토리의 argTypes가 정확하지 않을 수 있습니다.현재
meta.component가InputLabel로 설정되어 있지만,TextLabelExample스토리는TextLabel컴포넌트를 사용합니다. 이로 인해 Storybook의 자동 생성 문서(autodocs)에서TextLabel의 props가 정확하게 표시되지 않을 수 있습니다.두 컴포넌트를 별도의 스토리 파일로 분리하거나,
subcomponents옵션을 활용하는 것을 고려해 보세요.♻️ subcomponents 사용 예시
const meta: Meta = { title: 'Components/Label', component: InputLabel, + subcomponents: { TextLabel }, tags: ['autodocs'], argTypes: { children: { control: 'text', description: '라벨 텍스트' }, className: { control: 'text', description: '추가 스타일 (Tailwind Classes)' }, }, };src/components/Card/Card.tsx (1)
77-79:keyprop이 불필요합니다
key={id}는 리스트 렌더링 시 React가 요소를 식별하는 데 사용됩니다. 단일 컴포넌트 렌더링에서는 불필요하며, 리스트에서 Card를 렌더링하는 부모 컴포넌트에서 key를 지정해야 합니다.♻️ 제안된 수정
<article - key={id} className="flex flex-col h-58 gap-3 px-5 pt-5 pb-4 group bg-white rounded-2xl border border-gray-200 overflow-hidden cursor-pointer transition-all shadow-[0_1px_3px_0_rgba(0,0,0,0.04)] hover:shadow-[0_8px_24px_-4px_rgba(109,91,208,0.12),0_4px_8px_-2px_rgba(109,91,208,0.08)] hover:-translate-y-0.5 hover:border-brand-purple-border" {...restProps}>src/components/Button/Button.tsx (1)
22-27: 아이콘 전용 버튼의 사이즈 클래스 누락
isIconOnly일 때buttonSizeMap[size]가undefined로 전달되어 패딩/갭 클래스가 적용되지 않습니다. 아이콘 전용 버튼에 대한 별도 사이징이 필요할 수 있습니다.♻️ 제안된 수정
+const iconOnlySizeMap = { + xs: 'p-1.5', + sm: 'p-2', + md: 'p-2.5', + lg: 'p-3', +} as const; // Button 컴포넌트 내부 className={cn( 'inline-flex items-center justify-center rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-purple ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed', - isIconOnly ? undefined : buttonSizeMap[size], + isIconOnly ? iconOnlySizeMap[size] : buttonSizeMap[size], buttonThemeMap[variant], className )}src/components/Card/Card.types.ts (2)
10-25: 내부 인터페이스 export 여부 검토
CardContent,CardStats,ButtonActions가 export 되지 않았습니다. 다른 파일에서 이 타입들을 사용해야 한다면 export가 필요합니다. 의도적인 캡슐화라면 현재 상태로 유지해도 됩니다.
16-20: 통계 값 타입 일관성
viewCount,copyCount,likeCount가string | number로 정의되어 있습니다. 유연하지만, API 응답과 표시 형식을 명확히 분리하면 타입 안정성이 높아집니다 (예: 항상number로 저장하고 표시 시 포맷팅).src/components/NavigationBar/NavigationBar.tsx (4)
5-5: 주석 처리된 import 제거사용하지 않는 주석 처리된 import는 코드 정리 시 제거하는 것이 좋습니다.
♻️ 제안된 수정
-// import { Icon } from '../Icon/Icon';
9-9: AddPromptButton과 직접 Button 사용 간 불일치
AddPromptButton컴포넌트가 정의되어 있지만, 로그인 상태에서는 직접Button을 사용합니다. 일관성을 위해 하나의 방식으로 통일하거나,AddPromptButton에onClickprop을 추가하여 재사용하세요.♻️ 제안된 수정
-const AddPromptButton = () => <Button icon="addLine">프롬프트 등록</Button>; +const AddPromptButton = ({ onClick }: { onClick?: () => void }) => ( + <Button icon="addLine" onClick={onClick}> + 프롬프트 등록 + </Button> +); // 사용처 {isLoggedIn && user ? ( - <Button icon="addLine" onClick={() => navigate('/write')}> - 프롬프트 등록 - </Button> + <AddPromptButton onClick={() => navigate('/write')} /> ) : ( <Dialog.Login trigger={<AddPromptButton />} /> )}Also applies to: 62-64
15-17: UserMenuButton 내 불필요한 span 래핑Button 컴포넌트가 이미 children을 span으로 감싸고 있어서, 여기서 다시 span으로 감싸면 이중 래핑이 됩니다.
♻️ 제안된 수정
return ( <Button variant="tertiary" onClick={onClick}> - <span>{name}</span> + {name} </Button> );
28-29: UserDropdown 내 주석 처리된 Icon 사용로그아웃/회원 탈퇴 버튼에 Icon이 주석 처리되어 있습니다. 아이콘을 사용할 계획이 없다면 주석을 제거하고, 사용할 계획이라면 TODO 주석을 추가하여 추적하세요.
Also applies to: 36-37
src/components/Button/Button.styles.ts (1)
4-6:cn유틸리티 추출은 현재 필요 없음
cn함수가 현재 Button 컴포넌트에서만 사용되고 있습니다. 다른 컴포넌트에서clsx나twMerge를 사용하는 경우가 없으므로, 지금 당장 공유 모듈로 추출할 필요는 없습니다. 향후 다른 컴포넌트에서 동일한 className 유틸리티가 필요하게 되면 그때src/utils/cn.ts로 이동하는 것을 검토하세요.
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Badge from './Badge'; | ||
|
|
||
| const meta = { | ||
| title: 'Components/Badge', | ||
| component: Badge, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| size: { | ||
| control: 'radio', | ||
| options: ['sm', 'md'], | ||
| description: '배지 크기 설정', | ||
| table: { | ||
| defaultValue: { summary: 'md' }, | ||
| }, | ||
| }, | ||
| variant: { | ||
| control: 'radio', | ||
| options: ['category', 'platform'], | ||
| description: '배지의 의미론적 스타일 설정', | ||
| }, | ||
| children: { | ||
| control: 'text', | ||
| description: '배지에 표시될 텍스트', | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof Badge>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| variant: 'category', | ||
| size: 'md', | ||
| children: 'Badge', | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Storybook Meta StoryObj argTypes control radio options React Vite 2024 2025 documentation
💡 Result:
In Storybook CSF3 (React + Vite), define radio controls via argTypes on the meta object, then type stories with StoryObj<typeof meta>:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta = {
component: Button,
argTypes: {
variant: {
options: ['primary', 'secondary'],
control: { type: 'radio' }, // or 'inline-radio'
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: { variant: 'primary' },
};options supplies the allowed values, and control.type can be 'radio' / 'inline-radio' (or other control types). If options is present, Storybook otherwise defaults the control to 'select' unless you override it. [1], [2]
argTypes의 control 속성 형식을 수정해야 합니다.
Storybook CSF3 표준에 따르면 control은 객체 형식({ type: 'radio' })으로 지정해야 합니다. 현재 코드의 control: 'radio'는 문자열 형식으로, 올바른 형식이 아닙니다.
argTypes: {
size: {
control: { type: 'radio' }, // 'radio' → { type: 'radio' }
options: ['sm', 'md'],
description: '배지 크기 설정',
table: {
defaultValue: { summary: 'md' },
},
},
variant: {
control: { type: 'radio' }, // 'radio' → { type: 'radio' }
options: ['category', 'platform'],
description: '배지의 의미론적 스타일 설정',
},
// ...
}
🤖 Prompt for AI Agents
In `@src/components/Badge/Badge.stories.tsx` around lines 1 - 42, The argTypes in
Badge.stories.tsx currently use string controls (e.g., control: 'radio') which
is not CSF3-compliant; update the argTypes for size and variant inside the meta
object (argTypes -> size and argTypes -> variant) to use object-form controls
(control: { type: 'radio' }) instead of the string form, keep existing options,
descriptions, and table settings unchanged, and ensure the children argType
remains as control: 'text' converted similarly to control: { type: 'text' } if
you want consistent object-form usage.
| export const calloutBodyStyles = { | ||
| attentive: 'text-gray-700', | ||
| destructive: 'text-red-700', | ||
| }; |
There was a problem hiding this comment.
calloutBodyStyles에 as const 단언이 누락되었습니다.
calloutVariantMap과 calloutTitleStyles에는 as const가 적용되어 있지만 calloutBodyStyles에는 누락되어 있습니다. 일관성과 정확한 타입 추론을 위해 추가하세요.
🔧 제안된 수정 사항
export const calloutBodyStyles = {
attentive: 'text-gray-700',
destructive: 'text-red-700',
-};
+} as const;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const calloutBodyStyles = { | |
| attentive: 'text-gray-700', | |
| destructive: 'text-red-700', | |
| }; | |
| export const calloutBodyStyles = { | |
| attentive: 'text-gray-700', | |
| destructive: 'text-red-700', | |
| } as const; |
🤖 Prompt for AI Agents
In `@src/components/Callout/Callout.styles.ts` around lines 12 - 15,
calloutBodyStyles is missing an "as const" assertion which breaks parity with
calloutVariantMap and calloutTitleStyles; add "as const" to calloutBodyStyles to
ensure literal type inference. Locate the export named calloutBodyStyles in
Callout.styles.ts and append the as const assertion to the object (matching the
style used for calloutVariantMap and calloutTitleStyles) so the keys/values are
treated as readonly literals for proper typing.
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Card from './Card'; | ||
|
|
||
| const MOCK_CONTENT = { | ||
| title: 'ChatGPT로 블로그 글 작성하기', | ||
| createdAt: '2024.03.15', | ||
| description: | ||
| 'SEO에 최적화된 블로그 글을 작성하기 위한 프롬프트입니다. 검색 엔진 노출을 극대화하면서도 독자가 읽기 쉬운 자연스러운 글을 작성할 수 있도록 구조화되어 있습니다.', | ||
| }; | ||
|
|
||
| const MOCK_STATS = { | ||
| viewCount: '1.2k', | ||
| copyCount: 342, | ||
| likeCount: 128, | ||
| }; | ||
|
|
||
| const MOCK_BADGES = [ | ||
| { id: '1', name: ['글쓰기'], variant: 'platform' as const }, | ||
| { id: '2', name: ['chatGPT'], variant: 'category' as const }, | ||
| ]; | ||
|
|
||
| const MOCK_ACTIONS = { | ||
| likeAction: () => alert('좋아요 클릭'), | ||
| bookmarkAction: () => alert('북마크 클릭'), | ||
| }; | ||
|
|
||
| const meta: Meta<typeof Card> = { | ||
| title: 'Components/Card', | ||
| component: Card, | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| decorators: [ | ||
| (Story) => ( | ||
| <div className="max-w-90 p-4 bg-gray-50/50"> | ||
| <Story /> | ||
| </div> | ||
| ), | ||
| ], | ||
| argTypes: { | ||
| writer: { control: 'text', description: '작성자 이름' }, | ||
| content: { object: 'content', description: '제목, 날짜, 설명 객체' }, | ||
| stats: { object: 'stats', description: '조회수, 복사수, 좋아요수' }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Card>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| id: 1, | ||
| writer: 'DevMaster', | ||
| badges: MOCK_BADGES, | ||
| content: MOCK_CONTENT, | ||
| stats: MOCK_STATS, | ||
| actions: MOCK_ACTIONS, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Storybook argTypes controls configuration React Vite 2024 2025 documentation
💡 Result:
Storybook (React + Vite) argTypes / Controls configuration (2024–2025 docs)
1) Set (or override) a control for a prop via argTypes
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta = {
component: Button,
argTypes: {
variant: {
options: ['primary', 'secondary'],
control: { type: 'radio' },
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = { args: { variant: 'primary' } };Storybook infers controls from component + args, and you customize with argTypes.*.control / options. [1]
2) Common control types + extra config
Examples you can apply per-arg:
- Number/range with constraints:
control: { type: 'number'|'range', min, max, step } - Color presets:
control: { type: 'color', presetColors: [...] } - File picker:
control: { type: 'file', accept: '.png' }
[1]
The ArgTypes API also documents control behavior like defaults (e.g., select when options is provided) and other argType annotations. [2]
3) Filter/sort controls shown in the Controls panel (per-story or per-component)
export const Example = {
parameters: {
controls: {
include: ['foo', 'bar'], // or a RegExp
// exclude: ['baz'], // or a RegExp
sort: 'requiredFirst', // 'none' | 'alpha' | 'requiredFirst'
expanded: true,
},
},
};[1]
4) Disable a specific prop in Controls
- Remove from the table entirely:
argTypes: { foo: { table: { disable: true } } }- Keep docs row but no UI control:
argTypes: { foo: { control: false } }[1]
5) Docs pages: dynamic Controls table vs static ArgTypes table
- Use the Controls doc block for an interactive args table. [3]
- Use the ArgTypes doc block for a static (non-interactive) interface table. [4]
Sources: Storybook Controls (Essentials) docs [1], ArgTypes API docs [2], Controls doc block API [3], ArgTypes doc block API [4].
argTypes 설정에 오류가 있습니다.
content와 stats 속성의 argTypes에서 object: 'content'와 object: 'stats' 문법은 Storybook 표준이 아닙니다. 다음과 같이 수정해야 합니다:
수정 필요한 부분 (lines 41-43)
argTypes: {
writer: { control: 'text', description: '작성자 이름' },
- content: { object: 'content', description: '제목, 날짜, 설명 객체' },
- stats: { object: 'stats', description: '조회수, 복사수, 좋아요수' },
+ content: { control: 'object', description: '제목, 날짜, 설명 객체' },
+ stats: { control: 'object', description: '조회수, 복사수, 좋아요수' },
},표준 Storybook 문법은 control: '...' 또는 control: { type: '...' } 형식입니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { Meta, StoryObj } from '@storybook/react-vite'; | |
| import Card from './Card'; | |
| const MOCK_CONTENT = { | |
| title: 'ChatGPT로 블로그 글 작성하기', | |
| createdAt: '2024.03.15', | |
| description: | |
| 'SEO에 최적화된 블로그 글을 작성하기 위한 프롬프트입니다. 검색 엔진 노출을 극대화하면서도 독자가 읽기 쉬운 자연스러운 글을 작성할 수 있도록 구조화되어 있습니다.', | |
| }; | |
| const MOCK_STATS = { | |
| viewCount: '1.2k', | |
| copyCount: 342, | |
| likeCount: 128, | |
| }; | |
| const MOCK_BADGES = [ | |
| { id: '1', name: ['글쓰기'], variant: 'platform' as const }, | |
| { id: '2', name: ['chatGPT'], variant: 'category' as const }, | |
| ]; | |
| const MOCK_ACTIONS = { | |
| likeAction: () => alert('좋아요 클릭'), | |
| bookmarkAction: () => alert('북마크 클릭'), | |
| }; | |
| const meta: Meta<typeof Card> = { | |
| title: 'Components/Card', | |
| component: Card, | |
| tags: ['autodocs'], | |
| parameters: { | |
| layout: 'centered', | |
| }, | |
| decorators: [ | |
| (Story) => ( | |
| <div className="max-w-90 p-4 bg-gray-50/50"> | |
| <Story /> | |
| </div> | |
| ), | |
| ], | |
| argTypes: { | |
| writer: { control: 'text', description: '작성자 이름' }, | |
| content: { object: 'content', description: '제목, 날짜, 설명 객체' }, | |
| stats: { object: 'stats', description: '조회수, 복사수, 좋아요수' }, | |
| }, | |
| }; | |
| export default meta; | |
| type Story = StoryObj<typeof Card>; | |
| export const Default: Story = { | |
| args: { | |
| id: 1, | |
| writer: 'DevMaster', | |
| badges: MOCK_BADGES, | |
| content: MOCK_CONTENT, | |
| stats: MOCK_STATS, | |
| actions: MOCK_ACTIONS, | |
| }, | |
| }; | |
| import type { Meta, StoryObj } from '@storybook/react-vite'; | |
| import Card from './Card'; | |
| const MOCK_CONTENT = { | |
| title: 'ChatGPT로 블로그 글 작성하기', | |
| createdAt: '2024.03.15', | |
| description: | |
| 'SEO에 최적화된 블로그 글을 작성하기 위한 프롬프트입니다. 검색 엔진 노출을 극대화하면서도 독자가 읽기 쉬운 자연스러운 글을 작성할 수 있도록 구조화되어 있습니다.', | |
| }; | |
| const MOCK_STATS = { | |
| viewCount: '1.2k', | |
| copyCount: 342, | |
| likeCount: 128, | |
| }; | |
| const MOCK_BADGES = [ | |
| { id: '1', name: ['글쓰기'], variant: 'platform' as const }, | |
| { id: '2', name: ['chatGPT'], variant: 'category' as const }, | |
| ]; | |
| const MOCK_ACTIONS = { | |
| likeAction: () => alert('좋아요 클릭'), | |
| bookmarkAction: () => alert('북마크 클릭'), | |
| }; | |
| const meta: Meta<typeof Card> = { | |
| title: 'Components/Card', | |
| component: Card, | |
| tags: ['autodocs'], | |
| parameters: { | |
| layout: 'centered', | |
| }, | |
| decorators: [ | |
| (Story) => ( | |
| <div className="max-w-90 p-4 bg-gray-50/50"> | |
| <Story /> | |
| </div> | |
| ), | |
| ], | |
| argTypes: { | |
| writer: { control: 'text', description: '작성자 이름' }, | |
| content: { control: 'object', description: '제목, 날짜, 설명 객체' }, | |
| stats: { control: 'object', description: '조회수, 복사수, 좋아요수' }, | |
| }, | |
| }; | |
| export default meta; | |
| type Story = StoryObj<typeof Card>; | |
| export const Default: Story = { | |
| args: { | |
| id: 1, | |
| writer: 'DevMaster', | |
| badges: MOCK_BADGES, | |
| content: MOCK_CONTENT, | |
| stats: MOCK_STATS, | |
| actions: MOCK_ACTIONS, | |
| }, | |
| }; |
🤖 Prompt for AI Agents
In `@src/components/Card/Card.stories.tsx` around lines 1 - 61, The argTypes block
on the meta object incorrectly uses "object: 'content'" and "object: 'stats'";
update those entries to use Storybook's control syntax (e.g., change content and
stats to use control: 'object' or control: { type: 'object' }) so Storybook
recognizes them; locate the argTypes inside the meta declaration (the "argTypes"
property) and replace the wrong object keys for content and stats with the
proper control configuration while keeping the descriptions.
| {badges.map((badge) => ( | ||
| <Badge key={badge.id} size="sm" variant={badge.variant}> | ||
| {badge.name} | ||
| </Badge> |
There was a problem hiding this comment.
badge.name 타입 불일치 가능성
Card.types.ts에서 CardBadges.name이 string[]로 정의되어 있지만, 여기서는 badge.name을 Badge의 children으로 직접 전달하고 있습니다. 배열을 직접 렌더링하면 예상치 못한 결과가 발생할 수 있습니다.
🛠️ 제안된 수정
Card.types.ts에서 name을 string으로 변경하거나, 렌더링 로직을 수정해야 합니다:
// Card.types.ts
export interface CardBadges {
id: string;
- name: string[];
+ name: string;
variant: BadgeVariants;
}🤖 Prompt for AI Agents
In `@src/components/Card/Card.tsx` around lines 15 - 18, CardBadges.name is typed
as string[] in Card.types.ts but the Card component maps badges and passes
badge.name directly as Badge children (badges.map -> <Badge
...>{badge.name}</Badge>), which can render an array incorrectly; fix by either
changing CardBadges.name to string in Card.types.ts if each badge has a single
name, or update the render to join the array into a string (e.g.,
badge.name.join(', ') or appropriate separator) before passing it to Badge,
ensuring Badge receives a string child and preserving component props
size/variant.
| aria-label="닫기"> | ||
| <Icon name="close" size="md" aria-hidden /> | ||
| </button> | ||
| <Button icon="close" variant="ghost" className="absolute top-4 right-4 p-2" /> |
There was a problem hiding this comment.
아이콘-only 버튼에 접근성 라벨이 필요합니다.
스크린리더가 버튼 의미를 전달하지 못해 접근성 문제가 됩니다.
🛠️ 수정 제안
- <Button icon="close" variant="ghost" className="absolute top-4 right-4 p-2" />
+ <Button
+ icon="close"
+ variant="ghost"
+ className="absolute top-4 right-4 p-2"
+ aria-label="닫기"
+ />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Button icon="close" variant="ghost" className="absolute top-4 right-4 p-2" /> | |
| <Button | |
| icon="close" | |
| variant="ghost" | |
| className="absolute top-4 right-4 p-2" | |
| aria-label="닫기" | |
| /> |
🤖 Prompt for AI Agents
In `@src/components/Dialog/Dialog.tsx` at line 24, The close icon-only Button in
Dialog.tsx lacks an accessible name, so add an accessible label (e.g.,
aria-label or aria-labelledby) to the Button instance (the <Button icon="close"
variant="ghost" className="absolute top-4 right-4 p-2" />) so screen readers can
announce its purpose; use a localized string like "Close" and prefer aria-label
for a one-off label, or reference a visually-hidden element via aria-labelledby
if you already have a label node.
| <div className={`flex items-center justify-center gap-1 ${className}`}> | ||
| <button | ||
| <Button |
There was a problem hiding this comment.
옵션 className이 없으면 DOM에 undefined 클래스가 들어갈 수 있습니다.
Line 24에서 className이 undefined일 때 문자열로 합쳐집니다. 기본값을 지정해 주세요.
🛠️ 제안 수정안
- className,
+ className = '',🤖 Prompt for AI Agents
In `@src/components/Pagination/Pagination.tsx` around lines 24 - 25, The
Pagination component is concatenating className into the container string which
can insert "undefined" into the DOM; set a safe default for className (e.g.,
empty string) so it never becomes undefined. Update the Pagination component's
props handling (or function parameter default) for the className prop used in
the JSX container div (the expression `className={`flex items-center
justify-center gap-1 ${className}`}`) to use a fallback like an empty string (or
null-coalescing) so the template never includes "undefined".
| stats: { | ||
| likeCount: handleFormattedCount(stats.viewCount), | ||
| copyCount: handleFormattedCount(stats.viewCount), | ||
| viewCount: handleFormattedCount(stats.viewCount), | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check PromptStats interface definition
rg -n -C5 'interface PromptStats' src/Repository: promlog/promlog-client
Length of output: 636
🏁 Script executed:
#!/bin/bash
# Check the actual code in promptMapper.ts at lines 51-55
cat -n src/mappers/promptMapper.ts | sed -n '45,60p'Repository: promlog/promlog-client
Length of output: 708
🏁 Script executed:
#!/bin/bash
# Search for all usages of PromptStats to understand the data structure
rg -n 'PromptStats' src/ -A 3Repository: promlog/promlog-client
Length of output: 503
likeCount/copyCount가 viewCount로 고정되는 버그
PromptStats 인터페이스에는 likeCount, viewCount, copyCount 세 개의 필드가 정의되어 있으나, 현재 매퍼에서 likeCount와 copyCount가 모두 stats.viewCount로 매핑되어 실제 값이 손실됩니다. 각 필드를 올바르게 매핑해 주세요.
수정 제안
stats: {
- likeCount: handleFormattedCount(stats.viewCount),
- copyCount: handleFormattedCount(stats.viewCount),
+ likeCount: handleFormattedCount(stats.likeCount),
+ copyCount: handleFormattedCount(stats.copyCount),
viewCount: handleFormattedCount(stats.viewCount),
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| stats: { | |
| likeCount: handleFormattedCount(stats.viewCount), | |
| copyCount: handleFormattedCount(stats.viewCount), | |
| viewCount: handleFormattedCount(stats.viewCount), | |
| }, | |
| stats: { | |
| likeCount: handleFormattedCount(stats.likeCount), | |
| copyCount: handleFormattedCount(stats.copyCount), | |
| viewCount: handleFormattedCount(stats.viewCount), | |
| }, |
🤖 Prompt for AI Agents
In `@src/mappers/promptMapper.ts` around lines 51 - 55, In promptMapper (the block
constructing the stats object), likeCount and copyCount are incorrectly using
stats.viewCount; update the mapping so likeCount maps to stats.likeCount and
copyCount maps to stats.copyCount, using handleFormattedCount for each (leave
viewCount mapping as handleFormattedCount(stats.viewCount)); ensure you update
the object keys in the promptMapper function that builds PromptStats so the
three fields use their corresponding source properties.
| tags: { | ||
| category: tags.categories.map((category) => category.name) || '기타', | ||
| platform: tags.platforms.map((platform) => platform.name), |
There was a problem hiding this comment.
카테고리 기본값이 동작하지 않습니다.
map() 결과는 빈 배열이어도 truthy라 '기타'가 절대 적용되지 않고, 타입도 string이 되어 불일치합니다.
🛠️ 수정 제안
tags: {
- category: tags.categories.map((category) => category.name) || '기타',
+ category: tags.categories.length
+ ? tags.categories.map((category) => category.name)
+ : ['기타'],
platform: tags.platforms.map((platform) => platform.name),
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| tags: { | |
| category: tags.categories.map((category) => category.name) || '기타', | |
| platform: tags.platforms.map((platform) => platform.name), | |
| tags: { | |
| category: tags.categories.length | |
| ? tags.categories.map((category) => category.name) | |
| : ['기타'], | |
| platform: tags.platforms.map((platform) => platform.name), |
🤖 Prompt for AI Agents
In `@src/mappers/promptMapper.ts` around lines 56 - 58, The category default
fallback is broken because tags.categories.map(...) returns an array (truthy
even when empty) and you’re assigning a string '기타', causing type mismatch;
change the assignment for category to produce an array of strings and use a
length check on tags.categories (e.g., tags.categories && tags.categories.length
? tags.categories.map(c => c.name) : ['기타']) so that category is always
string[]; adjust the platform mapping similarly if needed and update any usage
expecting string[] to match the new shape (look for tags.categories,
tags.platforms, and the tags.category property in promptMapper.ts).
| <p className="text-gray-700 leading-relaxed">{content.description}</p> | ||
| </div> | ||
| <PromptContentBox promptId={promptId!} description={prompt} /> | ||
| <PromptContentBox promptId={promptId!} description={content.description} /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check PromptContent interface structure
echo "=== PromptContent Interface ==="
rg -A 15 "interface PromptContent" src/apis/prompts/prompts.types.ts
# Check PromptContentBox component and its props
echo -e "\n=== PromptContentBox Component ==="
rg -B 2 -A 10 "PromptContentBox" --type tsx | head -40Repository: promlog/promlog-client
Length of output: 460
🏁 Script executed:
#!/bin/bash
# Find PromptContentBox component file
echo "=== Searching for PromptContentBox ==="
rg "PromptContentBox" --type-list | head
rg -l "PromptContentBox"
# Search for the component definition
echo -e "\n=== PromptContentBox Implementation ==="
rg -B 5 -A 20 "export.*PromptContentBox\|function PromptContentBox\|const PromptContentBox"
# Also check the PromptDetailPage to see the context
echo -e "\n=== PromptDetailPage usage context ==="
rg -B 5 -A 5 "PromptContentBox" src/pages/PromptDetail/PromptDetailPage.tsxRepository: promlog/promlog-client
Length of output: 1698
🏁 Script executed:
#!/bin/bash
# Read PromptContentBox component
cat -n src/pages/PromptDetail/_components/PromptContentBox.tsxRepository: promlog/promlog-client
Length of output: 868
content.prompt를 사용해야 합니다
PromptContent 인터페이스에는 description과 prompt가 별도의 필드로 존재합니다. 현재 코드에서 PromptDetailPage 41번 줄은 "💬 프롬프트 설명"으로 content.description을 표시하고, 43번 줄의 PromptContentBox는 "프롬프트"를 표시합니다. PromptContentBox는 설명이 아닌 실제 프롬프트 내용(content.prompt)을 표시해야 합니다.
🤖 Prompt for AI Agents
In `@src/pages/PromptDetail/PromptDetailPage.tsx` at line 43, The PromptDetailPage
currently passes content.description into the PromptContentBox but the
PromptContentBox should display the actual prompt text; update the render in
PromptDetailPage to pass content.prompt (not content.description) to
PromptContentBox while keeping the promptId argument (i.e., replace the prop
currently using content.description with content.prompt), ensuring
PromptContentBox receives the correct prompt string per the PromptContent
interface.
| /// <reference types="vitest/config" /> | ||
| import { defineConfig } from 'vite'; | ||
| import react from '@vitejs/plugin-react'; | ||
| import tailwindcss from '@tailwindcss/vite'; | ||
|
|
||
| // https://vite.dev/config/ | ||
| import path from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; | ||
| import { playwright } from '@vitest/browser-playwright'; | ||
| const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon | ||
| export default defineConfig({ | ||
| plugins: [react(), tailwindcss()], | ||
| server: { | ||
| port: 3000, | ||
| port: 3000 | ||
| }, | ||
| }); | ||
| test: { | ||
| projects: [{ | ||
| extends: true, | ||
| plugins: [ | ||
| // The plugin will run tests for the stories defined in your Storybook config | ||
| // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest | ||
| storybookTest({ | ||
| configDir: path.join(dirname, '.storybook') | ||
| })], | ||
| test: { | ||
| name: 'storybook', | ||
| browser: { | ||
| enabled: true, | ||
| headless: true, | ||
| provider: playwright({}), | ||
| instances: [{ | ||
| browser: 'chromium' | ||
| }] | ||
| }, | ||
| setupFiles: ['.storybook/vitest.setup.ts'] | ||
| } | ||
| }] | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Storybook addon-vitest storybookTest configuration projects browser playwright
💡 Result:
To run Storybook stories as Vitest tests in a separate Vitest “project” (so they don’t conflict with your normal unit tests) and execute them in a real browser via Playwright, configure storybookTest() inside a dedicated project/workspace config.
Vitest config using projects (recommended when available)
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import viteConfig from './vite.config';
const dirname = path.dirname(fileURLToPath(import.meta.url));
export default mergeConfig(
viteConfig,
defineConfig({
test: {
projects: [
{
extends: true,
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
// must match how you run SB in package.json
storybookScript: 'yarn storybook --no-open',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
provider: playwright({}),
headless: true,
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
],
},
})
);Source (Storybook Vitest addon docs): [1]
Vitest 3.x style workspace (separate Storybook project)
// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineWorkspace([
'./vitest.config.ts', // your existing unit tests
{
extends: './vite.config.ts',
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'yarn storybook --ci',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
provider: 'playwright',
headless: true,
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
]);Source: [1]
Required setup file
Create .storybook/vitest.setup.ts to apply your Storybook preview annotations:
import { setProjectAnnotations } from '@storybook/your-framework';
import * as previewAnnotations from './preview';
setProjectAnnotations([previewAnnotations]);Source: [1]
Notes:
- The addon’s default/recommended execution is Vitest browser mode + Playwright (Chromium). [1]
- Storybook’s older test-runner (Jest + Playwright) is considered superseded by the Vitest addon for Vite-based Storybooks. [2]
Sources:
[1] Storybook Docs — “Vitest addon” (configuration examples incl. storybookTest, projects, browser+Playwright): (storybook.js.org)
[2] Storybook Docs — “Test runner” (superseded by Vitest addon): (storybook.js.org)
Citations:
- 1: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon/index
- 2: https://storybook.js.org/docs/9/writing-tests/integrations/test-runner?utm_source=openai
🌐 Web query:
@storybook/addon-vitest vitest-plugin projects array configuration
💡 Result:
To use @storybook/addon-vitest/vitest-plugin with Vitest’s test.projects array, you define a dedicated “storybook” project inside your main vitest.config.ts, and attach the storybookTest() plugin only to that project. (storybook.js.org)
// vitest.config.ts (Vitest >= 3.2)
import { defineConfig, mergeConfig } from 'vitest/config';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import viteConfig from './vite.config';
const dirname = path.dirname(fileURLToPath(import.meta.url));
export default mergeConfig(
viteConfig,
defineConfig({
test: {
projects: [
{
// inherit base Vitest config (so you don't duplicate common settings)
extends: true,
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'yarn storybook --no-open',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
// (provider config omitted here—use your preferred Vitest browser provider setup)
},
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
],
},
})
);If you’re on Vitest < 3.2, Storybook recommends using a workspace file instead (because projects wasn’t the preferred mechanism there). (storybook.js.org)
Citations:
- 1: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon/index
- 2: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon/index
🏁 Script executed:
# Check if there are any other config files or package.json that might provide version info
find . -maxdepth 2 -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" 2>/dev/null | head -5Repository: promlog/promlog-client
Length of output: 100
🏁 Script executed:
# Check package.json for version information
cat ./package.json | grep -A 50 '"dependencies"'Repository: promlog/promlog-client
Length of output: 1431
Storybook/Vitest 프로젝트 구성이 올바르게 설정되었습니다.
공식 Storybook 문서와 비교하여 구성을 검증했습니다. 사용 중인 버전들(Vitest 4.0.16, @storybook/addon-vitest 10.2.2, @vitest/browser-playwright 4.0.16)은 모두 이 구성을 완벽히 지원합니다. projects 배열 구조, extends: true, storybookTest() 플러그인, playwright 프로바이더 설정이 모두 올바릅니다.
한 가지 권장사항: storybookTest() 설정에 storybookScript 옵션을 추가하면 Storybook 실행 방식을 명시적으로 지정할 수 있습니다(예: storybookScript: 'npm run storybook'). 이는 선택사항이지만 공식 문서에서 권장됩니다.
🤖 Prompt for AI Agents
In `@vite.config.ts` around lines 1 - 41, Add the recommended storybookScript
option to the storybookTest plugin call so Storybook launch is explicit: update
the storybookTest({ configDir: path.join(dirname, '.storybook') }) invocation to
include a storybookScript property (for example storybookScript: 'npm run
storybook') so the test project (defined in test.projects) uses that script to
start Storybook; modify the call to storybookTest in the test.projects.plugins
array accordingly.
✅ 체크리스트
📝 작업 상세 내용
🔎 후속 작업 (선택 사항)
📚 참고 자료 (선택 사항)
📸 스크린샷 (선택 사항)
✅ 셀프 체크리스트
이슈 번호: #31
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일
Chores