Skip to content

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Nov 25, 2025

#️⃣연관된 이슈

ex) #888

📝작업 내용

Storybook 빌더 변경 (Webpack → Vite)

  • 기존 Webpack 기반이었던 Storybook 빌더를 Vite로 변경했습니다.

Vite 설정 오버라이딩

  • vite-tsconfig-paths 플러그인을 적용하기 위해 viteFinal 설정을 추가했습니다.
  • Storybook 내에서도 tsconfig.json에 정의된 절대 경로로 모듈을 불러올 수 있도록 했습니다.
viteFinal: async (config) => {
  const { mergeConfig } = await import('vite');
  const { default: tsconfigPaths } = await import('vite-tsconfig-paths');

  // Storybook의 Vite 설정에 tsconfig-paths 플러그인 병합
  return mergeConfig(config, {
    plugins: [tsconfigPaths()],
  });
},

공통 컴포넌트 스토리 추가

스토리북 배포링크

추가된 컴포넌트: CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner

🚀 트러블 슈팅

vercel preview에서 배포가 실패했습니다. (커밋 전 npm run build:dev 습관 중요..)

원인은 현재 vite는 7.2.2 이고 storybook은 8.6인데, 8.6 버전에서는 vite 7버전을 지원하지 않습니다.

그래서 vite 6.0.0으로 다운 그레이드하여 해결했습니다.

중점적으로 리뷰받고 싶은 부분(선택)

vite 6버전으로 내렸을 때 문제가 생길만한 부분이 있을지 찾아봐야 할 것 같네요.

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 여러 UI 컴포넌트에 대한 Storybook 스토리 추가 (CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner)
    • 빌드 도구를 Webpack 기반에서 Vite 기반으로 마이그레이션하여 개발 환경 성능 향상
  • 버그 수정

    • Spinner 컴포넌트의 높이 속성이 제대로 적용되도록 수정
  • Chores

    • Storybook 관련 의존성 업데이트

✏️ Tip: You can customize this high-level summary in your review settings.

- @storybook/react-webpack5를 @storybook/react-vite로 교체
- .storybook/main.ts 설정 업데이트
- 경로 별칭(alias) 해결을 위해 vite-tsconfig-paths 추가
- 컴포넌트에 React를 임포트하여 ReferenceError 수정
- Modal.stories.tsx 파일 생성하여 다양한 케이스(기본, 설명 없음, 긴 내용) 추가
- Spinner.stories.tsx 파일 생성 (기본, 커스텀 높이 예시)
- height prop이 스타일 컴포넌트로 전달되지 않던 버그 수정
- InputField.stories.tsx 파일 생성 (기본, 라벨 포함, 비밀번호, 에러 상태 등 다양한 케이스 추가)
- 기본 상태, 초기값 설정, 커스텀 플레이스홀더 등 다양한 사용 예시 추가
- 인터랙티브한 테스트를 위해 useState를 활용한 상태 관리 로직 포함
- Compound Component 패턴을 활용한 드롭다운 스토리 구현
- 기본 드롭다운 및 메뉴 위치 조정(Top) 예시 추가
- 제네릭 타입 호환성을 위한 타입 캐스팅 적용
- CustomTextArea.stories.tsx 파일 생성 (기본, 라벨 포함, 에러 상태, 글자수 제한 등)
@seongwon030 seongwon030 self-assigned this Nov 25, 2025
@seongwon030 seongwon030 added 🎨 Design 마크업 & 스타일링 💻 FE Frontend labels Nov 25, 2025
@vercel
Copy link

vercel bot commented Nov 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Dec 8, 2025 3:07pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Storybook 설정을 Webpack5 기반에서 Vite 기반으로 마이그레이션하고, 여러 공통 컴포넌트(CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner)에 대한 Storybook 스토리 파일을 신규로 추가합니다. 의존성을 업데이트하고 tsconfig-paths 플러그인을 통한 경로 해석을 구성합니다.

Changes

Cohort / File(s) Summary
Storybook 마이그레이션 (Webpack → Vite)
frontend/.storybook/main.ts, frontend/package.json
프레임워크를 @storybook/react-webpack5에서 @storybook/react-vite로 변경, webpackFinal 제거, viteFinal 추가, Webpack 관련 의존성 제거 및 react-vite 의존성 추가
공통 컴포넌트 스토리 파일 추가
frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx, frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx, frontend/src/components/common/InputField/InputField.stories.tsx, frontend/src/components/common/Modal/Modal.stories.tsx, frontend/src/components/common/SearchField/SearchField.stories.tsx, frontend/src/components/common/Spinner/Spinner.stories.tsx
각 컴포넌트별 기본 및 변형 스토리(Default, WithLabel, ErrorState 등) 추가, 상호작용 가능한 상태 관리 및 argTypes 정의
기존 스토리 및 컴포넌트 정리
frontend/src/components/common/Header/Header.stories.tsx, frontend/src/components/common/Modal/Modal.tsx, frontend/src/components/common/Spinner/Spinner.tsx
Header 스토리에서 React import 제거, Modal 포맷팅 조정, Spinner에 height 속성 전달

Sequence Diagram(s)

sequenceDiagram
    participant Storybook as Storybook (main.ts)
    participant Vite as Vite Config
    participant TsConfigPaths as vite-tsconfig-paths
    participant Component as Component

    Storybook->>Vite: viteFinal(config) 실행
    Vite->>TsConfigPaths: tsconfigPaths() 플러그인 로드
    TsConfigPaths->>Vite: tsconfig 경로 설정 적용
    Vite->>Component: 모듈 해석 및 로드
    Component->>Storybook: 스토리 렌더링
Loading
sequenceDiagram
    participant User as 사용자
    participant Story as Story (예: InputField.stories.tsx)
    participant Component as InputField 컴포넌트
    participant State as 로컬 상태

    User->>Story: 값 입력 또는 이벤트 발생
    Story->>State: useState 상태 업데이트
    State->>Component: 최신 props 전달
    Component->>Story: onChange/onClear 콜백 트리거
    Story->>State: 상태 동기화
    Story->>User: UI 업데이트
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Storybook 설정 변경: Vite 마이그레이션 로직 검증 필요
  • 스토리 파일 추가: 동일한 패턴이 여러 파일에 반복되므로 일관성 확인 위주
  • 의존성 변경: package.json의 버전 호환성 확인

Possibly related issues

Possibly related PRs

Suggested labels

✨ Feature

Suggested reviewers

  • lepitaaar
  • suhyun113
  • Zepelown

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 사항의 주요 내용을 명확하게 요약하고 있습니다. 공통 컴포넌트 스토리 파일 제작이라는 주요 변경 사항을 직접적으로 설명합니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경 사항이 MOA-390의 요구 사항을 충족합니다. common 폴더의 컴포넌트들(CustomDropDown, CustomTextArea, InputField, Modal, SearchField, Spinner)에 대한 스토리북 파일이 제작되었고, Storybook 설정이 Vite로 마이그레이션되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 범위 내에 있습니다. Storybook 설정 변경(Webpack에서 Vite로의 마이그레이션)과 스토리 파일 추가는 공통 컴포넌트 스토리 파일 제작의 필수 작업입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#888-common-components-storybook-MOA-390

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
frontend/src/components/common/Modal/Modal.stories.tsx (1)

6-134: 모달 스토리 동작은 적절하지만, Controls 연동은 약간 개선 여지가 있습니다.

  • 세 스토리 모두 useState(args.isOpen)으로 한 번만 초기화하기 때문에, Storybook Controls에서 isOpen 값을 변경해도 이후 렌더에서는 로컬 state가 우선되어 arg 변경이 곧바로 반영되지는 않습니다. 큰 문제는 아니지만, args 기반으로 상태를 제어하고 싶다면 로컬 state를 제거하거나, 별도 상호작용 전용 스토리/데코레이터로 분리하는 방법도 있습니다.
  • childrencontrol: 'text' 설정은 LongContent처럼 JSX 노드를 args로 넘기는 케이스와 다소 어울리지 않습니다. 이 스토리는 control을 끄거나, children을 string 기반 예제로만 둘지 한 번 고민해 보셔도 좋겠습니다.

현재 구현만으로도 기능 확인에는 충분해 보여 선택 사항으로 남겨두셔도 됩니다.

frontend/package.json (1)

47-58: 버전 호환성 정보를 확인하겠습니다.

Storybook 8.5~8.6 혼용은 호환성 문제가 없습니다.

Storybook 8.6은 주요 breaking changes가 없으므로, @storybook/react-vite (^8.6.14)과 핵심 패키지 (^8.5.0)를 혼용해도 안정성 측면에서 기술적 문제는 없습니다. Caret range(^)에 의해 npm은 8.5.0 이상 9.0.0 미만의 모든 버전을 허용하므로 버전 정렬은 필수가 아닙니다.

다만 일관성을 위해 모든 Storybook 패키지를 동일 major·minor 버전으로 맞추는 것은 좋은 관행입니다. 현재 상태에서 즉시 수정이 필요하지는 않습니다.

frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (1)

86-86: 타입 단언을 고려해보세요.

options prop에 타입 단언(as readonly { label: string; value: string }[])을 사용하고 있습니다. 이는 args.options의 타입이 CustomDropDown이 기대하는 타입과 정확히 일치하지 않음을 시사합니다.

가능하다면 argTypes 정의나 OPTIONS 상수의 타입을 조정하여 타입 단언을 제거하는 것이 타입 안전성 측면에서 더 좋습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ae126f and 4bc9f10.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • frontend/.storybook/main.ts (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (1 hunks)
  • frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx (1 hunks)
  • frontend/src/components/common/Header/Header.stories.tsx (0 hunks)
  • frontend/src/components/common/InputField/InputField.stories.tsx (1 hunks)
  • frontend/src/components/common/Modal/Modal.stories.tsx (1 hunks)
  • frontend/src/components/common/Modal/Modal.tsx (2 hunks)
  • frontend/src/components/common/SearchField/SearchField.stories.tsx (1 hunks)
  • frontend/src/components/common/Spinner/Spinner.stories.tsx (1 hunks)
  • frontend/src/components/common/Spinner/Spinner.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/components/common/Header/Header.stories.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-09T08:11:51.820Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 388
File: frontend/src/components/common/Spinner/Spinner.tsx:1-31
Timestamp: 2025-05-09T08:11:51.820Z
Learning: 로딩 스피너(Spinner) 컴포넌트에는 웹 접근성을 위해 `role="status"` 및 `aria-label` 같은 ARIA 속성을 추가해야 합니다. 이는 스크린 리더와 같은 보조 기술이 로딩 상태를 사용자에게 적절히 전달할 수 있게 합니다.

Applied to files:

  • frontend/src/components/common/Spinner/Spinner.tsx
🧬 Code graph analysis (1)
frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (1)
frontend/src/components/common/CustomDropDown/CustomDropDown.tsx (1)
  • CustomDropDown (94-115)
🔇 Additional comments (11)
frontend/src/components/common/Modal/Modal.tsx (1)

15-31: 기능 변화 없음 – 포맷팅 정리만 확인했습니다.

useEffect 블록과 헤더 영역 JSX 들여쓰기만 변경되었고, 모달의 열림/닫힘·백드롭 동작에는 영향이 없습니다. 그대로 머지해도 무방해 보입니다.

frontend/src/components/common/Spinner/Spinner.tsx (1)

4-40: height prop 전달 보완 및 접근성 유지 모두 적절합니다.

SpinnerWrapperheight를 받아 스타일에 반영하고, Spinner에서 이를 실제로 전달하도록 수정된 부분이 스토리(CustomHeight)와도 잘 맞습니다. 기존에 유지하던 role="status", aria-label="로딩 중"도 그대로라 접근성 요구사항도 충족합니다.

Based on learnings, 로딩 스피너에 ARIA 속성을 포함해야 한다는 이전 합의와도 일치합니다.

frontend/src/components/common/Spinner/Spinner.stories.tsx (1)

4-44: Spinner 높이 제어 스토리가 컴포넌트 변경과 잘 맞습니다.

기본 스토리와 CustomHeight 스토리가 height prop 사용법을 명확히 보여주고 있고, 테두리 박스 안에서 렌더링하는 예시도 실제 사용 시나리오를 잘 설명해 줍니다. 현재 구조로 충분히 직관적이라 추가 수정 없이 사용 가능해 보입니다.

frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx (5)

1-4: 임포트가 적절합니다.

Storybook 스토리 파일에 필요한 모든 임포트가 올바르게 구성되어 있습니다.


6-35: 메타 설정이 잘 구성되어 있습니다.

argTypes에 한글 설명이 명확하게 작성되어 있고, autodocs 태그와 layout 설정이 적절합니다.


40-50: 스토리용 스타일 컴포넌트가 적절합니다.

스토리 파일 내에서 데모용 스타일 컴포넌트를 정의하는 것은 일반적인 패턴입니다.


52-56: 샘플 데이터가 적절합니다.

드롭다운 테스트를 위한 옵션 데이터가 명확하게 정의되어 있습니다.


1-108: React 19와 Storybook 8.5.0의 호환성은 공식적으로 검증되었습니다.

검증 결과, Storybook 8.5.0은 2025년 1월 21일 릴리스에서 React 19 지원을 공식 추가했습니다. CustomDropDown 컴포넌트는 React 19의 알려진 호환성 문제(element.ref 제거)와 관련된 패턴(element.ref, forwardRef 등)을 사용하지 않으므로 현재 구성에서 안전합니다.

frontend/.storybook/main.ts (3)

1-1: Vite 프레임워크로의 마이그레이션이 올바릅니다.

@storybook/react-webpack5에서 @storybook/react-vite로 임포트를 변경한 것이 적절합니다.


13-13: 프레임워크 설정이 올바릅니다.

Vite 기반 Storybook으로 프레임워크를 변경한 것이 적절합니다.


16-23: viteFinal 구성이 올바르게 구현되어 있으며 모든 의존성이 확인되었습니다.

vite-tsconfig-paths (^5.1.4)와 vite (^7.2.2)가 모두 devDependencies에 정상적으로 설치되어 있습니다. 동적 임포트와 mergeConfig를 사용한 플러그인 추가 방식은 Vite 기반 Storybook의 권장 패턴이며, 이를 통해 tsconfig.json에 정의된 경로 별칭(예: @/...)이 Storybook에서 올바르게 작동합니다.

Comment on lines +5 to +161
const meta = {
title: 'Components/Common/CustomTextArea',
component: CustomTextArea,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
value: {
control: 'text',
description: '텍스트 영역의 값입니다.',
},
onChange: {
action: 'changed',
description: '값이 변경될 때 호출되는 함수입니다.',
},
placeholder: {
control: 'text',
description: '텍스트 영역의 플레이스홀더입니다.',
},
label: {
control: 'text',
description: '텍스트 영역 상단에 표시되는 라벨입니다.',
},
width: {
control: 'text',
description: '텍스트 영역의 너비입니다.',
},
disabled: {
control: 'boolean',
description: '비활성화 여부입니다.',
},
isError: {
control: 'boolean',
description: '에러 상태 여부입니다.',
},
helperText: {
control: 'text',
description: '하단에 표시되는 도움말 텍스트입니다 (에러 시 표시).',
},
showMaxChar: {
control: 'boolean',
description: '최대 글자수 표시 여부입니다.',
},
maxLength: {
control: 'number',
description: '최대 글자수 제한입니다.',
},
},
} satisfies Meta<typeof CustomTextArea>;

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

export const Default: Story = {
args: {
placeholder: '내용을 입력하세요',
width: '300px',
value: '',
onChange: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<CustomTextArea
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
/>
);
},
};

export const WithLabel: Story = {
args: {
label: '자기소개',
placeholder: '자기소개를 입력하세요',
width: '300px',
value: '',
onChange: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<CustomTextArea
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
/>
);
},
};

export const ErrorState: Story = {
args: {
label: '지원동기',
value: '너무 짧습니다.',
isError: true,
helperText: '10자 이상 입력해주세요.',
width: '300px',
onChange: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<CustomTextArea
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
/>
);
},
};

export const WithMaxLength: Story = {
args: {
label: '문의 내용',
placeholder: '100자 이내로 입력해주세요',
maxLength: 100,
showMaxChar: true,
width: '300px',
value: '',
onChange: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<CustomTextArea
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
/>
);
},
};

export const Disabled: Story = {
args: {
label: '피드백',
value: '이미 제출된 피드백입니다.',
disabled: true,
width: '300px',
onChange: () => { },
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onChange Action이 args의 no-op 핸들러로 가려질 수 있습니다.

argTypes.onChangeaction: 'changed'를 설정해 두었지만, 각 스토리의 args에서 onChange: () => { }를 지정해 기본 action 핸들러를 덮어쓰고 있습니다. 이 경우 args.onChange?.(e) 호출이 실제로는 아무 일도 하지 않아서, Storybook Actions 패널에 이벤트가 찍히지 않을 수 있습니다.

각 스토리의 args에서 onChange를 제거하고, render 함수에서만 args.onChange?.(e)를 호출하는 쪽이 더 자연스러워 보입니다.

중복된 상태 관리/렌더링 로직은 공통화 여지가 있습니다.

Default, WithLabel, ErrorState, WithMaxLength 스토리가 모두 useState(args.value || '') + 공통 onChange 구현을 공유하므로, 헬퍼 함수나 데코레이터 추출로 중복을 줄일 수 있습니다. 필수는 아니고 가독성·유지보수 관점에서의 선택 사항입니다.

🤖 Prompt for AI Agents
frontend/src/components/common/CustomTextArea/CustomTextArea.stories.tsx lines
5-161: several stories set onChange: () => {} in their args which overrides the
argTypes action and prevents Storybook Actions from firing; remove onChange from
each story's args (Default, WithLabel, ErrorState, WithMaxLength, Disabled) so
the argTypes action is used, keep the render implementations calling
args.onChange?.(e) to forward the event, and optionally extract the repeated
useState + onChange render pattern into a small helper or decorator to eliminate
duplication.

Comment on lines +5 to +231
const meta = {
title: 'Components/Common/InputField',
component: InputField,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
value: {
control: 'text',
description: '입력 필드의 값입니다.',
},
onChange: {
action: 'changed',
description: '값이 변경될 때 호출되는 함수입니다.',
},
onClear: {
action: 'cleared',
description: '삭제 버튼 클릭 시 호출되는 함수입니다.',
},
placeholder: {
control: 'text',
description: '입력 필드의 플레이스홀더입니다.',
},
label: {
control: 'text',
description: '입력 필드 상단에 표시되는 라벨입니다.',
},
type: {
control: 'radio',
options: ['text', 'password'],
description: '입력 필드의 타입입니다.',
},
width: {
control: 'text',
description: '입력 필드의 너비입니다.',
},
disabled: {
control: 'boolean',
description: '비활성화 여부입니다.',
},
isError: {
control: 'boolean',
description: '에러 상태 여부입니다.',
},
isSuccess: {
control: 'boolean',
description: '성공 상태 여부입니다.',
},
helperText: {
control: 'text',
description: '하단에 표시되는 도움말 텍스트입니다 (에러 시 표시).',
},
showClearButton: {
control: 'boolean',
description: '삭제 버튼 표시 여부입니다.',
},
showMaxChar: {
control: 'boolean',
description: '최대 글자수 표시 여부입니다.',
},
maxLength: {
control: 'number',
description: '최대 글자수 제한입니다.',
},
readOnly: {
control: 'boolean',
description: '읽기 전용 여부입니다.',
},
},
} satisfies Meta<typeof InputField>;

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

export const Default: Story = {
args: {
placeholder: '텍스트를 입력하세요',
width: '300px',
value: '',
onChange: () => { },
onClear: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<InputField
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
onClear={() => {
setValue('');
args.onClear?.();
}}
/>
);
},
};

export const WithLabel: Story = {
args: {
label: '이메일',
placeholder: 'example@email.com',
width: '300px',
value: '',
onChange: () => { },
onClear: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<InputField
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
onClear={() => {
setValue('');
args.onClear?.();
}}
/>
);
},
};

export const Password: Story = {
args: {
type: 'password',
label: '비밀번호',
placeholder: '비밀번호를 입력하세요',
width: '300px',
value: '',
onChange: () => { },
onClear: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<InputField
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
onClear={() => {
setValue('');
args.onClear?.();
}}
/>
);
},
};

export const ErrorState: Story = {
args: {
label: '닉네임',
value: '이미 사용중인 닉네임입니다',
isError: true,
helperText: '이미 사용중인 닉네임입니다.',
width: '300px',
onChange: () => { },
onClear: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<InputField
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
onClear={() => {
setValue('');
args.onClear?.();
}}
/>
);
},
};
export const WithMaxLength: Story = {
args: {
label: '한줄 소개',
placeholder: '20자 이내로 입력해주세요',
maxLength: 20,
showMaxChar: true,
width: '300px',
value: '',
onChange: () => { },
onClear: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value || '');
return (
<InputField
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value);
args.onChange?.(e);
}}
onClear={() => {
setValue('');
args.onClear?.();
}}
/>
);
},
};

export const Disabled: Story = {
args: {
label: '아이디',
value: 'disabled_user',
disabled: true,
width: '300px',
onChange: () => { },
onClear: () => { },
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onChange/onClear Actions가 args에 의해 덮어쓰일 수 있습니다.

argTypes에서 onChange, onClearaction을 선언해 두셨는데, 각 스토리 args에서 onChange: () => { }, onClear: () => { }를 지정하면서 실제로는 no-op 함수가 들어가 Action 패널에 이벤트가 찍히지 않을 수 있습니다.

각 스토리의 args에서 onChange/onClear를 제거하고, render 안에서 그대로 args.onChange?.(e), args.onClear?.()를 호출하면 Storybook이 기본 action 핸들러를 주입해 줘서 더 편하게 이벤트를 확인할 수 있습니다.

중복된 render 로직은 공통 헬퍼로 정리할 수 있습니다.

여러 스토리에서 useState(args.value || '')onChange/onClear 구현이 거의 동일하니, 공통 renderInputField 함수나 데코레이터로 추출하면 코드가 조금 더 읽기 쉬워질 것 같습니다. 현재 상태도 동작에는 문제 없어서 선택 사항입니다.

🤖 Prompt for AI Agents
frontend/src/components/common/InputField/InputField.stories.tsx lines 5-231:
The stories currently set no-op onChange/onClear functions in each story's args
which override the argTypes actions; remove onChange and onClear from every
story's args so Storybook can inject the action handlers, keep the render
implementations calling args.onChange?.(e) and args.onClear?.() as-is;
optionally refactor the repeated render logic into a shared renderInputField
helper (or decorator) to avoid duplication.

Comment on lines +5 to +116
const meta = {
title: 'Components/Common/SearchField',
component: SearchField,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
value: {
control: 'text',
description: '검색어 입력값입니다.',
},
onChange: {
action: 'changed',
description: '입력값이 변경될 때 호출되는 함수입니다.',
},
onSubmit: {
action: 'submitted',
description: '검색 제출 시 호출되는 함수입니다.',
},
placeholder: {
control: 'text',
description: '입력창의 플레이스홀더 텍스트입니다.',
},
ariaLabel: {
control: 'text',
description: '접근성을 위한 aria-label 속성입니다.',
},
autoBlur: {
control: 'boolean',
description: '제출 후 자동으로 포커스를 해제할지 여부입니다.',
},
},
} satisfies Meta<typeof SearchField>;

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

// 기본 검색창 스토리
export const Default: Story = {
args: {
value: '',
placeholder: '동아리 이름을 입력하세요',
autoBlur: true,
onChange: () => { },
onSubmit: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value);

return (
<SearchField
{...args}
value={value}
onChange={(newValue) => {
setValue(newValue);
args.onChange(newValue);
}}
/>
);
},
};

// 값이 미리 채워진 상태
export const WithValue: Story = {
args: {
value: '밴드 동아리',
placeholder: '검색어를 입력하세요',
autoBlur: true,
onChange: () => { },
onSubmit: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value);

return (
<SearchField
{...args}
value={value}
onChange={(newValue) => {
setValue(newValue);
args.onChange(newValue);
}}
/>
);
},
};

// 커스텀 플레이스홀더
export const CustomPlaceholder: Story = {
args: {
value: '',
placeholder: '원하는 태그를 검색해보세요 (#음악, #운동)',
autoBlur: true,
onChange: () => { },
onSubmit: () => { },
},
render: (args) => {
const [value, setValue] = useState(args.value);

return (
<SearchField
{...args}
value={value}
onChange={(newValue) => {
setValue(newValue);
args.onChange(newValue);
}}
/>
);
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onSubmit prop이 스토리에서 실제로 전달되지 않습니다.

argTypes와 설명에서는 onSubmit을 지원한다고 되어 있지만, 세 스토리 모두 SearchFieldonSubmit={args.onSubmit}를 넘기지 않아 submit 동작·Action 로깅이 확인되지 않습니다. render 내 JSX에 onSubmit={args.onSubmit}를 추가하는 편이 좋습니다.

Actions 설정과 중복 로직은 간단히 정리할 수 있습니다.

  • argTypes.onChange/onSubmitaction을 이미 선언했기 때문에, 각 스토리 args에서 onChange: () => {}, onSubmit: () => {}를 굳이 지정하지 않으면 기본 action 핸들러가 연결됩니다.
  • 세 스토리의 render 구현이 거의 동일하므로, 공통 renderSearchField 헬퍼를 만들거나 Story-level decorator로 중복을 줄일 수 있습니다.
🤖 Prompt for AI Agents
frontend/src/components/common/SearchField/SearchField.stories.tsx lines 5-116:
The stories define argTypes.onSubmit but do not pass args.onSubmit into the
rendered SearchField, and each story redundantly supplies empty
onChange/onSubmit handlers and duplicated render logic; fix by (1) in each
story's render, add onSubmit={args.onSubmit} to the SearchField props (keep
onChange handling as-is so local state updates and action logging both occur),
(2) remove the explicit onChange: () => {} and onSubmit: () => {} from each
story's args so Storybook's action from argTypes is used, and (3) optionally DRY
the repeated render by extracting a shared renderSearchField helper or using a
story-level decorator that manages state and passes args through.

- Storybook ^8.6이 아직 Vite 7을 지원하지 않아 Vite를 ^6.0.0으로 다운그레이드
- 버전 일관성을 위해 모든 Storybook 관련 패키지를 ^8.6.14로 업그레이드
- 호환되지 않는 피어 의존성으로 인한 npm install 에러 해결
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎨 Design 마크업 & 스타일링 💻 FE Frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants