Skip to content

[Setting/#33] 스토리북 도입 및 세팅#48

Merged
jjjsun merged 8 commits intodevelopfrom
setting/#33
Feb 19, 2026
Merged

[Setting/#33] 스토리북 도입 및 세팅#48
jjjsun merged 8 commits intodevelopfrom
setting/#33

Conversation

@jjjsun
Copy link
Collaborator

@jjjsun jjjsun commented Feb 18, 2026

🚨 관련 이슈

Closes #33

✨ 변경사항

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

✏️ 작업 내용

  • Storybook(스토리북) 초기화 및 설치(storybook@latest init)
  • .storybook/preview.ts에 프로젝트 전역 스타일 주입하여 Tailwind v4 스타일 적용
  • React Router 의존 컴포넌트 테스트를 위한 MemoryRouter 데코레이터 추가
  • 공통 UI 컴포넌트 Storybook 스토리 작성 및 랜더링 테스트
    • Button/Input/DropdownMenu/Modal/Badge/Alert
  • 디자인 보완: index.cssstatus-yellow, statuw-green 컬러 추가

😅 미완성 작업

N/A

📢 논의 사항 및 참고 사항

N/A

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • 새로운 기능

    • Alert, Badge, Button, DropdownMenu, Input, Modal 컴포넌트 추가 — 다양한 상태(정보/성공/경고/오류 등)와 사용자 인터랙션 지원
    • Badge는 여러 변형 및 크기 지원, Button은 다양한 스타일·상태(비활성/로딩/풀너비 등) 제공
    • DropdownMenu는 트리거로 열리는 항목 목록 및 키보드/외부 클릭 제어 지원
  • 문서

    • 모든 컴포넌트에 대해 Storybook 스토리(여러 상태 및 사용 예시) 추가
  • 스타일

    • 상태 색상용 CSS 커스텀 변수(초록, 노랑) 추가

@jjjsun jjjsun requested review from Seojegyeong and YermIm February 18, 2026 20:54
@jjjsun jjjsun self-assigned this Feb 18, 2026
@jjjsun jjjsun added ⚙ Setting 개발 환경 세팅 ✨ Feature 기능 개발 labels Feb 18, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

스토리북용으로 여러 공통 UI 컴포넌트(Alert, Badge, DropdownMenu)를 추가하고, Button/Input/Modal에 대한 스토리를 작성했으며 글로벌 CSS에 상태 색상 커스텀 프로퍼티 두 개를 추가했습니다.

Changes

Cohort / File(s) Summary
Alert 컴포넌트
src/components/common/alert/Alert.tsx, src/components/common/alert/Alert.stories.tsx
새로운 Alert 컴포넌트 추가(variant: `info
Badge 컴포넌트
src/components/common/badge/Badge.tsx, src/components/common/badge/Badge.stories.tsx
새로운 Badge 컴포넌트 추가(variant: `syncing
DropdownMenu 컴포넌트
src/components/common/dropdownmenu/DropdownMenu.tsx, src/components/common/dropdownmenu/DropdownMenu.stories.tsx
새로운 DropdownMenu 컴포넌트 추가: trigger로 토글, 외부 클릭 시 닫기(mousedown 리스너), 키보드 토글(Enter/Space), 아이템 onClick 처리 및 active 스타일링. Default 스토리 추가.
Button / Input / Modal 스토리
src/components/common/button/Button.stories.tsx, src/components/common/input/Input.stories.tsx, src/components/common/modal/Modal.stories.tsx
기존 컴포넌트들의 스토리 추가: Button(Primary, Secondary, Outline, Gradient, Disabled, Loading, Sizes, FullWidth), Input(Default, HelperText, Error, Success, Disabled, RightElement), Modal(Default — EnsureModalRoot 포함, 열기/닫기 플로우).
글로벌 스타일
src/index.css
@theme에 상태 색상 커스텀 프로퍼티 추가: --color-status-green: #22c55e, `--color-status-yellow: `#facc15.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • YermIm
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '[Setting/#33] 스토리북 도입 및 세팅'으로, Storybook 도입이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿 구조를 잘 따르고 있으며, 관련 이슈 링크, 체크된 변경사항, 상세한 작업 내용이 모두 기록되어 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 #33 이슈의 모든 요구사항을 충족합니다: Storybook 초기화, Tailwind CSS v4 스타일 적용, MemoryRouter 데코레이터 설정, 공통 컴포넌트 스토리 작성.
Out of Scope Changes check ✅ Passed 모든 변경사항이 Storybook 도입 및 세팅과 관련된 범위 내에 있으며, 불필요한 외부 변경은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 setting/#33

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

@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: 4

🧹 Nitpick comments (8)
src/components/common/alert/Alert.tsx (1)

29-42: Alert 컴포넌트에 ARIA role 속성 추가를 권장합니다.

스크린 리더 사용자를 위해 Alert 컴포넌트에 적절한 ARIA role을 설정해 주세요. variant에 따라 role="alert" (danger, warning) 또는 role="status" (info, success)를 적용하면 접근성이 크게 향상됩니다.

🔧 variant별 role 매핑 예시
+  const roleMap: Record<TAlertVariant, "alert" | "status"> = {
+    info: "status",
+    success: "status",
+    warning: "alert",
+    danger: "alert",
+  };
+
   return (
     <div
+      role={roleMap[variant]}
       className={twMerge(base, variantClasses[variant], className)}
       {...rest}
     >

As per coding guidelines, src/**: "7. 접근성: 시맨틱 HTML, ARIA 속성 사용 확인."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/alert/Alert.tsx` around lines 29 - 42, The Alert
component should set an ARIA role on the outer div based on the variant prop to
improve screen-reader accessibility: in the Alert function/component, compute a
role string (e.g., if variant is "danger" or "warning" use "alert", if "info" or
"success" use "status", default to undefined or "status") and add it to the
returned root div alongside className/ {...rest}; reference the component return
(the outer <div> that uses twMerge(base, variantClasses[variant], className))
and the prop name variant to locate where to implement this mapping.
src/components/common/modal/Modal.stories.tsx (1)

32-78: 다양한 사이즈/패딩 조합에 대한 스토리 추가를 고려해 주세요.

현재 Default 스토리 하나만 있는데, Modal의 IModalProps 인터페이스(src/components/common/modal/Modal.tsx:13-23)에 정의된 size (sm | md | lg | xl), padding, hideCloseButton, disableOverlayClick 등 다양한 props 조합을 보여주는 스토리가 있으면 컴포넌트 문서화에 더 효과적입니다. 초기 세팅이니 추후 추가해도 무방합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/modal/Modal.stories.tsx` around lines 32 - 78, Add
additional Storybook stories in Modal.stories.tsx that demonstrate the different
IModalProps combinations: create separate exported stories (e.g., Small, Large,
XLarge, NoPadding, HiddenCloseButton, NoOverlayClose) that reuse the existing
Default render pattern (EnsureModalRoot, button to open, Modal component) but
pass size="sm"|"lg"|"xl", different padding values, hideCloseButton={true}, and
disableOverlayClick={true} to the Modal component so users can see each variant;
keep each story self-contained and named to reflect the prop combination.
src/components/common/dropdownmenu/DropdownMenu.tsx (2)

33-70: 접근성: 드롭다운 메뉴에 WAI-ARIA 속성이 누락되어 있습니다.

코딩 가이드라인에 따라 시맨틱 HTML과 ARIA 속성 사용을 확인해야 합니다. 현재 드롭다운 패턴에 필요한 핵심 접근성 속성이 빠져 있습니다:

  • 트리거 버튼에 aria-expanded, aria-haspopup="menu" 누락
  • 메뉴 패널에 role="menu" 누락
  • 각 메뉴 아이템에 role="menuitem" 누락
  • Escape 키로 메뉴 닫기, 화살표 키 탐색 등 키보드 지원 부재
🔧 최소한의 ARIA 속성 추가 예시
-      <button type="button" onClick={() => setOpen((v) => !v)}>
+      <button
+        type="button"
+        aria-haspopup="menu"
+        aria-expanded={open}
+        onClick={() => setOpen((v) => !v)}
+      >
         {trigger}
       </button>
       {open && (
-        <div className="absolute left-0 mt-2 w-72 rounding-15 bg-brand-200 py-3 px-1 shadow-Medium">
-          <div className="space-y-1">
+        <div role="menu" className="absolute left-0 mt-2 w-72 rounding-15 bg-brand-200 py-3 px-1 shadow-Medium">
+          <div className="space-y-1">
             {items.map((it, idx) => (
               <div key={idx} className="px-2">
                 <button
                   type="button"
+                  role="menuitem"
                   onClick={() => {

As per coding guidelines, src/**: "7. 접근성: 시맨틱 HTML, ARIA 속성 사용 확인."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx` around lines 33 - 70,
The DropdownMenu component is missing accessibility semantics and keyboard
support; update the trigger button (the element rendering {trigger}) to include
aria-haspopup="menu", aria-expanded={open}, and aria-controls pointing to the
menu id, add role="menu" and an id to the menu panel (the div rendered when
open) and set role="menuitem" on each menu item button (the elements inside
items.map), implement keyboard handlers on the trigger and menu to close on
Escape and navigate items with ArrowUp/ArrowDown (manage focus via refs for each
item and ensure setOpen(false) still runs on item activation), and ensure focus
is moved into the menu when opened and returned to the trigger when closed.

22-31: useEffect 리스너를 open 상태에 따라 조건부로 등록하면 불필요한 이벤트 처리를 줄일 수 있습니다.

메뉴가 닫혀 있을 때도 mousedown 리스너가 항상 등록되어 있습니다. open을 의존성으로 추가하고 열린 상태에서만 리스너를 등록하면 성능상 약간 이점이 있습니다.

♻️ 조건부 리스너 등록
   useEffect(() => {
+    if (!open) return;
     const onDocClick = (e: MouseEvent) => {
       if (!ref.current) return;
       if (!ref.current.contains(e.target as Node)) setOpen(false);
     };
     document.addEventListener("mousedown", onDocClick);
     return () => {
       document.removeEventListener("mousedown", onDocClick);
     };
-  }, []);
+  }, [open]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx` around lines 22 - 31,
The effect currently always registers the document "mousedown" listener; update
the useEffect that defines onDocClick to depend on the open state and only
attach the listener when open is true: add open to the dependency array, wrap
the document.addEventListener call in a conditional (if (open) { ... }), and
ensure the cleanup still removes the listener (or is a no-op when not attached).
This change touches the useEffect block that references ref, onDocClick, and
setOpen so the listener is only active while the dropdown is open.
src/components/common/badge/Badge.tsx (2)

4-12: TBadgeVariant 타입 export 권장

TBadgeVariantTBadgeSize를 export하지 않으면, 사용 측에서 API 상태 값을 badge variant로 매핑할 때 직접 string 리터럴을 하드코딩하게 됩니다. 외부에서 타입 안전하게 참조할 수 있도록 export를 추가하는 것을 권장합니다.

♻️ 제안
-type TBadgeVariant = "syncing" | "success" | "inactive" | "running" | "stopped";
+export type TBadgeVariant = "syncing" | "success" | "inactive" | "running" | "stopped";

-type TBadgeSize = "sm" | "md";
+export type TBadgeSize = "sm" | "md";

-interface IBadgeProps extends HTMLAttributes<HTMLSpanElement> {
+export interface IBadgeProps extends HTMLAttributes<HTMLSpanElement> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/badge/Badge.tsx` around lines 4 - 12, Export the badge
type aliases so consumers can reference them for type-safe mappings: add export
to TBadgeVariant and TBadgeSize (e.g., export type TBadgeVariant = ... and
export type TBadgeSize = ...); optionally export IBadgeProps if external
components need the prop shape. Update the declarations for TBadgeVariant,
TBadgeSize (and IBadgeProps if desired) in Badge.tsx so they are exported for
external use.

22-34: sizeClasses, variantClasses를 컴포넌트 외부로 이동 권장

두 객체는 props나 state에 의존하지 않는 순수 상수인데 함수 본문 내에 정의되어 있어, 렌더링마다 새 객체가 생성됩니다. 컴포넌트 외부로 이동하면 불필요한 메모리 할당을 피할 수 있습니다.

♻️ 제안
+const SIZE_CLASSES: Record<TBadgeSize, string> = {
+  sm: "h-7 px-3 py-1 font-caption",
+  md: "h-9 px-4 py-1 font-body2",
+};
+
+const VARIANT_CLASSES: Record<TBadgeVariant, string> = {
+  syncing: "bg-status-yellow/30 text-status-yellow border border-status-yellow",
+  success: "bg-status-green/30 text-status-green border border-status-green",
+  inactive: "bg-status-red/30 text-status-red border border-status-red/40",
+  running: "bg-status-blue/30 text-status-blue border border-status-blue/40",
+  stopped: "bg-brand-300 text-text-sub border border-text-placeholder/40",
+};
+
 export default function Badge({ variant = "success", size = "md", leftIcon, className, children, ...rest }: IBadgeProps) {
-  const sizeClasses: Record<TBadgeSize, string> = {
-    sm: "h-7 px-3 py-1 font-caption",
-    md: "h-9 px-4 py-1 font-body2",
-  };
-
-  const variantClasses: Record<TBadgeVariant, string> = {
-    syncing: "bg-status-yellow/30 text-status-yellow border border-status-yellow",
-    success: "bg-status-green/30 text-status-green border border-status-green",
-    inactive: "bg-status-red/30 text-status-red border border-status-red/40",
-    running: "bg-status-blue/30 text-status-blue border border-status-blue/40",
-    stopped: "bg-brand-300 text-text-sub border border-text-placeholder/40",
-  };
   return (
     <span
       className={twMerge(
         "inline-flex items-center gap-2 rounded-full whitespace-nowrap",
-        sizeClasses[size],
-        variantClasses[variant],
+        SIZE_CLASSES[size],
+        VARIANT_CLASSES[variant],
         className,
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/badge/Badge.tsx` around lines 22 - 34, Move the pure
constant maps sizeClasses and variantClasses out of the Badge component so they
are defined once at module scope (keeping their types Record<TBadgeSize, string>
and Record<TBadgeVariant, string>), then update the component to reference these
top-level constants instead of recreating them on each render; locate the
definitions of sizeClasses and variantClasses in Badge.tsx and cut them to the
file top-level (above the Badge component) and ensure any exported type names
TBadgeSize and TBadgeVariant remain available for the record typings.
src/components/common/badge/Badge.stories.tsx (2)

1-11: import from "@storybook/react-vite" 임포트 방식 확인

@storybook/react-vite에서 MetaStoryObj를 임포트하는 것은 Storybook 8의 공식 마이그레이션 가이드에서 권장하는 방식입니다. 현재 코드는 올바른 임포트를 사용하고 있습니다.

다만, meta 선언 시 satisfies 연산자를 활용하면 타입 추론이 더 엄격해집니다. Storybook 공식 문서에서는 satisfies Meta<typeof Component> 패턴과 StoryObj<typeof meta>를 함께 사용하는 것을 권장합니다.

♻️ 제안 (타입 안전성 강화)
-const meta: Meta<typeof Badge> = {
+const meta = {
   title: "Common/Badge",
   component: Badge,
   parameters: { layout: "centered" },
-};
+} satisfies Meta<typeof Badge>;

 export default meta;
-type TBadgeStory = StoryObj<typeof Badge>;
+type TBadgeStory = StoryObj<typeof meta>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/badge/Badge.stories.tsx` around lines 1 - 11, The meta
declaration should use the TypeScript satisfies pattern to improve type safety:
change the meta object (symbol: meta) so it is declared with the satisfies
Meta<typeof Badge> operator, and adjust story typings to use StoryObj<typeof
meta> (symbol: StoryObj) instead of StoryObj<typeof Badge>; keep exporting meta
as the default export (export default meta) so Storybook picks it up. Ensure
imports (Meta, StoryObj) remain from "@storybook/react-vite" and update any
existing story consts to reference StoryObj<typeof meta>.

14-40: leftIcon prop을 보여주는 스토리 누락

Badge 컴포넌트의 주요 기능인 leftIcon prop이 어떤 스토리에서도 시연되지 않습니다. 아이콘을 함께 사용하는 케이스는 실무에서 흔하게 사용될 수 있으므로, 스토리에 예시를 추가하면 컴포넌트 문서로서의 가치가 높아집니다.

♻️ 제안
+import { CircleIcon } from "lucide-react"; // 프로젝트에서 사용 중인 아이콘 라이브러리로 교체
+
 export const Syncing: TBadgeStory = {
   args: { variant: "syncing", children: "동기화중" },
 };
+
+export const WithIcon: TBadgeStory = {
+  args: {
+    variant: "running",
+    leftIcon: <CircleIcon size={12} />,
+    children: "운영중",
+  },
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/badge/Badge.stories.tsx` around lines 14 - 40, The
stories currently (Syncing, Success, Inactive, Running, Stopped, SizeSm, SizeMd)
never demonstrate the Badge component's leftIcon prop; add a new story (e.g.,
WithLeftIcon or LeftIconExample) of type TBadgeStory that supplies a leftIcon
arg and children to showcase the icon+label combination, import a small icon
(from your icon set used elsewhere) at the top of Badge.stories.tsx, and set
args like { variant: "running", leftIcon: <IconName />, children: "아이콘 포함" } so
the storybook renders the leftIcon usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/common/alert/Alert.tsx`:
- Around line 22-27: The alert variant mapping in Alert.tsx is incorrect: update
the variantClasses Record<TAlertVariant, string> so the "success" key maps to
"text-status-green" (not "text-status-blue") to use the newly added
--color-status-green; ensure no other places in the Alert component (or
TAlertVariant usages) still expect blue for success and adjust tests/snapshots
if any reference the old class.

In `@src/components/common/badge/Badge.tsx`:
- Around line 36-49: The Badge component currently renders a plain <span> (root
span with {...rest}) which can omit ARIA semantics; set a sensible default
accessible role by adding role="status" (and optionally aria-live="polite") on
the root span while still allowing consumers to override via {...rest}, and
update any docs/stories to tell consumers to pass aria-label when using the
badge as a simple label; locate the root span in Badge.tsx (the element applying
twMerge, sizeClasses[size], variantClasses[variant], className, and {...rest})
and add the default role there.

In `@src/components/common/dropdownmenu/DropdownMenu.stories.tsx`:
- Around line 14-24: The Default story for DropdownMenu passes a nested <button>
via the trigger prop causing nested buttons; update the Default story's render
so trigger uses a non-interactive element (e.g., <span> or <div>) instead of
<button>—keep the same inner text ("...") and handlers if any—so the
DropdownMenu component (which already wraps the trigger in a button) does not
produce nested button elements.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 35-37: The DropdownMenu currently wraps the passed-in trigger
ReactNode with a <button> (see the onClick using setOpen), which can cause
nested button elements if trigger is itself a button; update DropdownMenu to
either render the trigger node directly and attach the click handler to it
(e.g., cloneElement to inject onClick) or require/validate that trigger is a
non-button element; modify the component around the trigger rendering (where
setOpen is used) to avoid always wrapping in a <button>—use
React.cloneElement(trigger, { onClick: () => setOpen(v => !v) }) or render
trigger directly and attach a wrapper non-button (e.g., <span>) to toggle, and
update DropdownMenu.stories.tsx if needed to use a non-interactive trigger.

---

Nitpick comments:
In `@src/components/common/alert/Alert.tsx`:
- Around line 29-42: The Alert component should set an ARIA role on the outer
div based on the variant prop to improve screen-reader accessibility: in the
Alert function/component, compute a role string (e.g., if variant is "danger" or
"warning" use "alert", if "info" or "success" use "status", default to undefined
or "status") and add it to the returned root div alongside className/ {...rest};
reference the component return (the outer <div> that uses twMerge(base,
variantClasses[variant], className)) and the prop name variant to locate where
to implement this mapping.

In `@src/components/common/badge/Badge.stories.tsx`:
- Around line 1-11: The meta declaration should use the TypeScript satisfies
pattern to improve type safety: change the meta object (symbol: meta) so it is
declared with the satisfies Meta<typeof Badge> operator, and adjust story
typings to use StoryObj<typeof meta> (symbol: StoryObj) instead of
StoryObj<typeof Badge>; keep exporting meta as the default export (export
default meta) so Storybook picks it up. Ensure imports (Meta, StoryObj) remain
from "@storybook/react-vite" and update any existing story consts to reference
StoryObj<typeof meta>.
- Around line 14-40: The stories currently (Syncing, Success, Inactive, Running,
Stopped, SizeSm, SizeMd) never demonstrate the Badge component's leftIcon prop;
add a new story (e.g., WithLeftIcon or LeftIconExample) of type TBadgeStory that
supplies a leftIcon arg and children to showcase the icon+label combination,
import a small icon (from your icon set used elsewhere) at the top of
Badge.stories.tsx, and set args like { variant: "running", leftIcon: <IconName
/>, children: "아이콘 포함" } so the storybook renders the leftIcon usage.

In `@src/components/common/badge/Badge.tsx`:
- Around line 4-12: Export the badge type aliases so consumers can reference
them for type-safe mappings: add export to TBadgeVariant and TBadgeSize (e.g.,
export type TBadgeVariant = ... and export type TBadgeSize = ...); optionally
export IBadgeProps if external components need the prop shape. Update the
declarations for TBadgeVariant, TBadgeSize (and IBadgeProps if desired) in
Badge.tsx so they are exported for external use.
- Around line 22-34: Move the pure constant maps sizeClasses and variantClasses
out of the Badge component so they are defined once at module scope (keeping
their types Record<TBadgeSize, string> and Record<TBadgeVariant, string>), then
update the component to reference these top-level constants instead of
recreating them on each render; locate the definitions of sizeClasses and
variantClasses in Badge.tsx and cut them to the file top-level (above the Badge
component) and ensure any exported type names TBadgeSize and TBadgeVariant
remain available for the record typings.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 33-70: The DropdownMenu component is missing accessibility
semantics and keyboard support; update the trigger button (the element rendering
{trigger}) to include aria-haspopup="menu", aria-expanded={open}, and
aria-controls pointing to the menu id, add role="menu" and an id to the menu
panel (the div rendered when open) and set role="menuitem" on each menu item
button (the elements inside items.map), implement keyboard handlers on the
trigger and menu to close on Escape and navigate items with ArrowUp/ArrowDown
(manage focus via refs for each item and ensure setOpen(false) still runs on
item activation), and ensure focus is moved into the menu when opened and
returned to the trigger when closed.
- Around line 22-31: The effect currently always registers the document
"mousedown" listener; update the useEffect that defines onDocClick to depend on
the open state and only attach the listener when open is true: add open to the
dependency array, wrap the document.addEventListener call in a conditional (if
(open) { ... }), and ensure the cleanup still removes the listener (or is a
no-op when not attached). This change touches the useEffect block that
references ref, onDocClick, and setOpen so the listener is only active while the
dropdown is open.

In `@src/components/common/modal/Modal.stories.tsx`:
- Around line 32-78: Add additional Storybook stories in Modal.stories.tsx that
demonstrate the different IModalProps combinations: create separate exported
stories (e.g., Small, Large, XLarge, NoPadding, HiddenCloseButton,
NoOverlayClose) that reuse the existing Default render pattern (EnsureModalRoot,
button to open, Modal component) but pass size="sm"|"lg"|"xl", different padding
values, hideCloseButton={true}, and disableOverlayClick={true} to the Modal
component so users can see each variant; keep each story self-contained and
named to reflect the prop combination.

Copy link

@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.

🧹 Nitpick comments (2)
src/components/common/dropdownmenu/DropdownMenu.stories.tsx (2)

14-24: 이전 리뷰 이슈 해결 확인 및 추가 스토리 변형 제안

trigger<button> 대신 <span>을 사용한 것이 확인되었습니다. DropdownMenu.tsx에서 트리거를 <div role="button">으로 감싸기 때문에 현재처럼 <span> 사용이 접근성 측면에서 올바릅니다.

다만 Default 스토리 하나만 존재하기 때문에, 아이콘이 있는 메뉴 항목이나 전체 비활성 상태 등 실제 사용 사례를 추가 스토리로 커버하면 Storybook의 시각적 테스트 효용이 높아집니다.

✨ 추가 스토리 변형 예시
+import { Home, Settings } from "lucide-react"; // 또는 프로젝트의 아이콘 라이브러리
+
+export const WithIcons: TDropdownMenuStory = {
+  render: () => (
+    <DropdownMenu
+      trigger={<span>⚙</span>}
+      items={[
+        { label: "홈", icon: <Home size={16} />, active: true, onClick: () => alert("홈") },
+        { label: "설정", icon: <Settings size={16} />, onClick: () => alert("설정") },
+      ]}
+    />
+  ),
+};
+
+export const AllInactive: TDropdownMenuStory = {
+  render: () => (
+    <DropdownMenu
+      trigger={<span>···</span>}
+      items={[
+        { label: "항목 A", onClick: () => alert("A") },
+        { label: "항목 B", onClick: () => alert("B") },
+      ]}
+    />
+  ),
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.stories.tsx` around lines 14
- 24, Add additional Storybook variations to cover more real-world cases: create
new exports (e.g., WithIcons and AllDisabled) alongside the existing Default
TDropdownMenuStory and reuse the DropdownMenu component in each; for WithIcons
provide items that include an icon property or visually distinct labels to
demonstrate icon rendering, and for AllDisabled set all items' disabled (or
equivalent) prop to true to show the fully-disabled state; keep the existing
Default as-is (trigger as <span>) and ensure each new story follows the same
shape as Default so Storybook picks them up.

5-12: satisfies 연산자와 StoryObj<typeof meta> 패턴으로 전환하는 것을 권장합니다.

현재 const meta: Meta<typeof DropdownMenu> = {...} 형태의 명시적 타입 어노테이션 방식을 사용하고 있습니다. Storybook 공식 문서와 Storybook 10에서 도입된 권장 패턴은 더 나은 타입 안정성과 자동완성을 제공합니다.

Storybook 10 마이그레이션 가이드에서는 @storybook/react가 아닌 @storybook/react-vite에서 Meta, StoryObj를 직접 임포트하는 방식을 명시적으로 권장하고 있습니다. 임포트 자체는 올바르지만, meta 정의 방식과 TDropdownMenuStory 타입을 아래와 같이 개선하면 story 레벨의 args 기본값까지 타입 추론에 반영됩니다.

♻️ 수정 제안: `satisfies` 패턴 적용
-const meta: Meta<typeof DropdownMenu> = {
+const meta = {
   title: "Common/DropdownMenu",
   component: DropdownMenu,
   parameters: { layout: "centered" },
-};
+} satisfies Meta<typeof DropdownMenu>;
 
 export default meta;
-type TDropdownMenuStory = StoryObj<typeof DropdownMenu>;
+type TDropdownMenuStory = StoryObj<typeof meta>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.stories.tsx` around lines 5 -
12, Change the explicit type annotation on meta to use the TypeScript satisfies
operator and switch the story type to StoryObj<typeof meta>: replace the current
"const meta: Meta<typeof DropdownMenu> = {...}" with a plain const meta = {...}
satisfies Meta<typeof DropdownMenu> so story-level args are inferred, and update
"type TDropdownMenuStory = StoryObj<typeof DropdownMenu>;" to "type
TDropdownMenuStory = StoryObj<typeof meta>;" (keep existing imports of Meta and
StoryObj from `@storybook/react-vite`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/common/dropdownmenu/DropdownMenu.stories.tsx`:
- Around line 14-24: Add additional Storybook variations to cover more
real-world cases: create new exports (e.g., WithIcons and AllDisabled) alongside
the existing Default TDropdownMenuStory and reuse the DropdownMenu component in
each; for WithIcons provide items that include an icon property or visually
distinct labels to demonstrate icon rendering, and for AllDisabled set all
items' disabled (or equivalent) prop to true to show the fully-disabled state;
keep the existing Default as-is (trigger as <span>) and ensure each new story
follows the same shape as Default so Storybook picks them up.
- Around line 5-12: Change the explicit type annotation on meta to use the
TypeScript satisfies operator and switch the story type to StoryObj<typeof
meta>: replace the current "const meta: Meta<typeof DropdownMenu> = {...}" with
a plain const meta = {...} satisfies Meta<typeof DropdownMenu> so story-level
args are inferred, and update "type TDropdownMenuStory = StoryObj<typeof
DropdownMenu>;" to "type TDropdownMenuStory = StoryObj<typeof meta>;" (keep
existing imports of Meta and StoryObj from `@storybook/react-vite`).

Copy link

@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)
src/components/common/dropdownmenu/DropdownMenu.tsx (2)

54-55: key={idx} 대신 안정적인 식별자 사용을 권장합니다.

현재 items 배열이 정적이라면 큰 문제는 없지만, 향후 items가 동적으로 변경되거나 재정렬될 경우 React가 잘못된 DOM 업데이트를 할 수 있습니다. TMenuItem에 고유 id 필드를 추가하거나, 최소한 key={it.label}처럼 의미 있는 값을 사용하는 것을 권장합니다.

♻️ 안정적인 key 사용 제안 (label 기반 또는 id 추가)

TMenuItem 타입에 id 추가 (선호):

 export type TMenuItem = {
+  id: string;
   label: string;
   icon?: React.ReactNode;
   onClick: () => void;
   active?: boolean;
 };
-            {items.map((it, idx) => (
-              <div key={idx} className="px-2">
+            {items.map((it) => (
+              <div key={it.id} className="px-2">

또는 id 추가 없이 label을 key로 사용 (label이 unique하다고 보장될 때만):

-            {items.map((it, idx) => (
-              <div key={idx} className="px-2">
+            {items.map((it) => (
+              <div key={it.label} className="px-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx` around lines 54 - 55,
The map in DropdownMenu.tsx currently uses key={idx} which can cause incorrect
DOM updates; update the TMenuItem type to include a unique id (preferred) and
change the map to use key={it.id}, or if you cannot add an id ensure you use a
stable unique value such as key={it.label} (only if labels are guaranteed
unique). Locate the items.map(...) usage inside DropdownMenu and replace the
numeric index key with the chosen stable identifier and adjust TMenuItem where
necessary.

28-31: 터치 기기 지원을 위해 mousedown 대신 pointerdown 사용을 고려하세요.

mousedown 이벤트는 터치 전용 기기에서 발생하지 않아 외부 클릭으로 메뉴가 닫히지 않을 수 있습니다. pointerdown은 마우스와 터치를 모두 커버합니다.

♻️ pointerdown으로 변경
-    document.addEventListener("mousedown", onDocClick);
     return () => {
-      document.removeEventListener("mousedown", onDocClick);
+    document.addEventListener("pointerdown", onDocClick);
+    return () => {
+      document.removeEventListener("pointerdown", onDocClick);
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/dropdownmenu/DropdownMenu.tsx` around lines 28 - 31,
The document event listener currently uses "mousedown" which doesn't fire on
touch-only devices; update the listener registration in DropdownMenu (where
document.addEventListener("mousedown", onDocClick) and
document.removeEventListener("mousedown", onDocClick) are used) to use
"pointerdown" instead so both mouse and touch inputs are handled—replace both
addEventListener and removeEventListener event names to "pointerdown" while
keeping the same onDocClick handler.
src/components/common/modal/Modal.stories.tsx (1)

65-72: alert() 대신 Storybook fn() 사용 권장

PR 커밋 메시지에서 "fn 기반 액션 확인 환경 구성"을 명시하고 있는데, 확인 버튼의 핸들러에 브라우저 기본 alert()를 사용하면 Storybook의 Actions 패널에서 이벤트를 추적할 수 없습니다.

♻️ 수정 제안
+import { fn } from "@storybook/test";
 ...
 const meta: Meta<typeof Modal> = {
   title: "Common/Modal",
   component: Modal,
   parameters: { layout: "centered" },
+  args: {
+    onClose: fn(),
+  },
 };
 ...
             <button
               type="button"
-              onClick={() => alert("확인")}
+              onClick={fn()}
               className="..."
               aria-label="모달 확인"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/modal/Modal.stories.tsx` around lines 65 - 72, Replace
the browser alert used in the Modal story's confirm button with Storybook's
action function so the click is tracked in the Actions panel: in
Modal.stories.tsx import the action helper (e.g., import { action } from
'@storybook/addon-actions') and change the button's onClick from () =>
alert("확인") to action('modal-confirm') (or use the story arg fn() if this story
uses args) on the button element to emit a named action instead of calling
alert().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 51-82: The dropdown popup currently rendered when open (the div in
DropdownMenu that contains items.map) is missing ARIA roles and an id; add
role="menu" to that popup div and give it a stable id, then update the trigger
(the element that has aria-haspopup="menu") to include aria-controls pointing to
that id; also add role="menuitem" to each item button inside the map (the button
that calls it.onClick and setOpen(false)) and ensure focus/keyboard behavior
remains unchanged.

In `@src/components/common/input/Input.stories.tsx`:
- Around line 25-30: The exported story named Error shadows the global Error
constructor; rename the export (e.g., ErrorStory or ErrorState) and update any
references to that export to avoid colliding with the built-in Error. Locate the
TInputStory export object currently declared as export const Error and change
its identifier, keeping its args (error: true, helperText) intact and updating
any story imports/usages or Storybook references to the new name.
- Around line 32-38: The Success story currently passes value: "정상 입력" without
an onChange handler, causing React's controlled-input warning; update the
Success story (export const Success: TInputStory) to either replace value with
defaultValue: "정상 입력" or add an onChange noop plus readOnly if you want it
immutable (e.g., provide onChange: () => {} and/or readOnly: true), ensuring the
Input component is not treated as a controlled input without a change handler.

---

Duplicate comments:
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 42-47: The onKeyDown handler currently toggles the menu for
Enter/Space but omits Escape handling; update the onKeyDown in DropdownMenu to
also handle e.key === "Escape" by calling e.preventDefault(), calling
setOpen(false) to close the menu, and restoring focus to the menu trigger
element (use the existing trigger/button ref, e.g.,
triggerRef.current?.focus()); ensure the handler does not stop other key
behavior and that triggerRef is available/forwarded if not already.

---

Nitpick comments:
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 54-55: The map in DropdownMenu.tsx currently uses key={idx} which
can cause incorrect DOM updates; update the TMenuItem type to include a unique
id (preferred) and change the map to use key={it.id}, or if you cannot add an id
ensure you use a stable unique value such as key={it.label} (only if labels are
guaranteed unique). Locate the items.map(...) usage inside DropdownMenu and
replace the numeric index key with the chosen stable identifier and adjust
TMenuItem where necessary.
- Around line 28-31: The document event listener currently uses "mousedown"
which doesn't fire on touch-only devices; update the listener registration in
DropdownMenu (where document.addEventListener("mousedown", onDocClick) and
document.removeEventListener("mousedown", onDocClick) are used) to use
"pointerdown" instead so both mouse and touch inputs are handled—replace both
addEventListener and removeEventListener event names to "pointerdown" while
keeping the same onDocClick handler.

In `@src/components/common/modal/Modal.stories.tsx`:
- Around line 65-72: Replace the browser alert used in the Modal story's confirm
button with Storybook's action function so the click is tracked in the Actions
panel: in Modal.stories.tsx import the action helper (e.g., import { action }
from '@storybook/addon-actions') and change the button's onClick from () =>
alert("확인") to action('modal-confirm') (or use the story arg fn() if this story
uses args) on the button element to emit a named action instead of calling
alert().

Copy link

@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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/common/input/Input.stories.tsx`:
- Around line 40-45: The Disabled story object (Disabled: TInputStory) passes a
controlled prop value without an onChange handler which triggers React's
controlled/uncontrolled warning; update the story to either replace value: "비활성"
with defaultValue: "비활성" to show an initial read-only value, or keep value but
add a noop onChange handler in the Disabled story args so the input is a
controlled component (adjust the Disabled story args accordingly).

@YermIm
Copy link
Collaborator

YermIm commented Feb 19, 2026

P4: 확인했습니다! 수고하셨어요!!

@jjjsun jjjsun merged commit a8cbc1c into develop Feb 19, 2026
2 checks passed
@jjjsun jjjsun deleted the setting/#33 branch February 19, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 ⚙ Setting 개발 환경 세팅

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚙️ [Setting] 스토리북(Storybook) 도입 및 세팅

3 participants

Comments