Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions packages/design-system/src/components/button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,108 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import Button from './Button';

// Storybook에 컴포넌트를 어떻게 표시할지 정의합니다.
const meta: Meta<typeof Button> = {
title: 'UI/Button',
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: {
description: {
component:
'**애플리케이션 전반에서 사용될 기본 버튼입니다.** `variant`로 주/보조 스타일을, `size`로 크기를, `isDisabled`로 비활성화 상태를 제어합니다.',
},
},
},
argTypes: {
variant: {
control: 'inline-radio',
options: ['primary', 'secondary'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
isDisabled: {
control: 'boolean',
},
children: {
control: 'text',
},
onClick: {
action: 'clicked',
description: '버튼 클릭 시 호출되는 이벤트 핸들러입니다.',
},
className: { table: { disable: true } },
},

args: {
variant: 'primary',
size: 'medium',
isDisabled: false,
children: 'Button',
},
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Primary: Story = {
name: 'variant: primary',
args: {
variant: 'primary',
children: 'Primary Button',
},
};

export const Secondary: Story = {
name: 'variant: secondary',
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};

export const Large: Story = {
name: 'size: large',
args: {
size: 'large',
children: 'Large Button',
},
};

export const Small: Story = {
name: 'size: small',
args: {
size: 'small',
children: 'Small Button',
},
};

export const Disabled: Story = {
name: 'state: disabled',
args: {
isDisabled: true,
},
render: (args) => (
<div style={{ display: 'flex', gap: '16px' }}>
<Button {...args} variant="primary">
Primary
</Button>
<Button {...args} variant="secondary">
Secondary
</Button>
</div>
),
};

export const WithAlertOnClick: Story = {
name: 'onClick with Alert',
args: {
children: 'onClick test',
onClick: () => {
alert('버튼이 클릭되었습니다.');
},
},
};
46 changes: 43 additions & 3 deletions packages/design-system/src/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
import { cn } from '@/lib';
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

cn import 경로(바렐) 확인 필요

@/libcn을 재노출(re-export)하지 않으면 빌드가 실패합니다. 현재 구현대로라면 @/lib/utils에서 직접 가져오거나, @/lib/index.ts에서 cn을 내보내도록 보장해야 합니다.

아래 스크립트로 재노출 여부를 점검해 주세요. 기대값: lib 폴더 어딘가에서 cn이 export 되거나, index.*에서 utils를 재노출.

만약 재노출이 없다면 다음 중 하나로 수정해 주세요.

- import { cn } from '@/lib';
+ import { cn } from '@/lib/utils';

또는 packages/design-system/src/lib/index.ts에서:

+ export * from './utils';

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# cn 선언/재노출 위치 확인
rg -n -C1 -S --type ts --type tsx '\bexport\b.*\bcn\b' packages/design-system/src/lib || true
# index에서 utils 재노출 확인
rg -n -S --type ts --type tsx 'export\s+\*\s+from\s+["'\'']\.\/utils["'\'']' packages/design-system/src/lib || true

Length of output: 391


아래 스크립트로 다시 한 번 cn의 선언/재노출 위치를 점검해주세요.
– 이번에는 파일 타입 제한을 제거해 범용 검색을 수행합니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) packages/design-system/src/lib 내부 전체에서 'export ... cn' 검색
rg -n -C1 '\bexport\b.*\bcn\b' packages/design-system/src/lib || echo "cn export 없음"

# 2) packages/design-system/src/lib/index.* 에서 utils 재노출 확인
rg -n 'export \* from [\'"]\.\/utils[\'"]' packages/design-system/src/lib || echo "utils 재노출 없음"

Length of output: 414


아래 스크립트로 index.ts(x)에서 utils 재노출 여부를 다시 확인합니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) lib 디렉터리에서 인덱스 파일 찾기
INDEX_FILES=$(find packages/design-system/src/lib -maxdepth 1 -type f \( -name 'index.ts' -o -name 'index.tsx' \))

if [ -z "$INDEX_FILES" ]; then
  echo "index 파일이 없습니다."
  exit 0
fi

echo "찾은 인덱스 파일:"
echo "$INDEX_FILES"

# 2) 각 인덱스 파일에서 utils 재노출 확인
for f in $INDEX_FILES; do
  echo "파일: $f"
  grep -n -E "export \* from ['\"]/utils['\"]" "$f" || echo "utils 재노출 없음"
done

Length of output: 592


cn 재노출(바렐) 누락 확인 – 조치 필요

검증 결과, packages/design-system/src/lib/utils.ts에서 export const cn 구현은 확인되었으나, packages/design-system/src/lib/index.tsutils 재노출이 빠져 있어
import { cn } from '@/lib' 구문이 빌드 오류를 일으킵니다. 아래 두 가지 중 한 가지 방법으로 반드시 수정해 주세요.

• 직접 utils 경로에서 가져오기
• 파일: packages/design-system/src/components/button/Button.tsx (라인 1)
• 변경 예시:
diff - import { cn } from '@/lib'; + import { cn } from '@/lib/utils';

• lib 인덱스 파일에 utils 재노출 추가
• 파일: packages/design-system/src/lib/index.ts
• 변경 예시:
diff + export * from './utils';

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

Suggested change
import { cn } from '@/lib';
++ b/packages/design-system/src/components/button/Button.tsx
@@ -1,1 +1,1
import { cn } from '@/lib/utils';
🤖 Prompt for AI Agents
In packages/design-system/src/components/button/Button.tsx around line 1, the
import "import { cn } from '@/lib'" fails because
packages/design-system/src/lib/index.ts does not re-export utils; fix by either
changing this file to import cn directly from the utils module (import from
'@/lib/utils' or the correct relative path to utils.ts) or add a re-export in
packages/design-system/src/lib/index.ts that exports cn (export { cn } from
'./utils') so that '@/lib' exposes cn.

import { cva } from 'class-variance-authority';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
isDisabled?: boolean;
}

const Button = ({ children, ...props }: ButtonProps) => {
const buttonVariants = cva(
'cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed w-full',
{
Comment on lines +10 to +12
Copy link
Collaborator

@jllee000 jllee000 Aug 25, 2025

Choose a reason for hiding this comment

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

w-full로 화면에 따라 그냥 자동 반응형으로 맞춰지니 레이아웃 맞추기 편리하겠네요!

혹시 그럼 따로 버튼 width 설정이 필요한 경우는 무조건 부모 컨테이너로 계속 감싸야하는거죠? 버튼 크기가 상황에 따라 계속 달라진다면,, width 값을 props로 줘서 제어하는 거랑 필요시에 계속 width 있는 컨테이너로 wrapping는 것 중 어느 것이 더 유연성 있는 방향일지 고민되는 포인트 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

맞습니다! 그렇기 때문에 width값도 className으로 주입 받을 수 있도록 설정했어요.
width 값을 props로 줘서 제어하는 거 vs 필요시에 계속 width 있는 컨테이너로 wrapping
말씀해주신 것 중에 저는 후자를 선택했는데 PR에 작성한 것처럼 className으로 width 값을 주면 전자 props 제어랑 똑같은 방법으로 커스텀이 돼서 선택을 했어요.

재림님은 어떻게 생각하시나요??

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 그럼, 사실 상 width 커스텀 시 후자(wrapping)랑 전자(className) 방안이 다 되는 식으로 열려있군용
버튼 컴포넌트는 앞으로 많이 여기저기 쓸 것 같아서! width 지정하는 방법을 통일하면 좋을 것 같아요! 저는 개인적으로 전자 방식이 좀더 width를 커스텀해준다는 유연한 느낌도 주고, 추가 컨테이너로 인한 구조가 필요없으니 코드가 복잡해지지 않을 것 같아요!

Copy link
Member Author

@constantly-dev constantly-dev Aug 25, 2025

Choose a reason for hiding this comment

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

만약 전자로 하게 되면 어떤 식으로 넘겨야 할까요?
'10rem' 이런 형태 혹은 숫자만 넘겨서 단위를 안에서 붙여서 쓰는 방법..!? props로 할 때 내부 처리가 조금 고민이네요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

개인적으로 어차피 props에 넣어서 커스텀 유연성을 높이는 의미를 유지하려면, 아예 단위 자체로 '10rem'으로 넘겨서 그자체로 스타일 먹여주는게 좋을 것 같아요!
(그럴리는 없지만 혹시 뭐 부모요소나 디바이스에 맞춘 스타일링 적용하면서 70%, 10dvh 등의 단위도 쓸 수 있으니..)

variants: {
variant: {
primary:
'bg-gray900 text-white-bg hover:bg-gray800 active:bg-gray900 disabled:bg-gray200 disabled:text-font-ltgray-4',
secondary:
'bg-white-bg text-font-black-1 hover:bg-gray200 active:bg-white-bg disabled:bg-gray200 disabled:text-font-ltgray-4 outline outline-gray200',
},
size: {
small: 'px-[1.2rem] py-[0.8rem] sub5-sb',
medium: 'px-[1.2rem] py-[1.2rem] sub5-sb',
large: 'px-[1.6rem] py-[1.5rem] sub3-sb',
},
},
defaultVariants: {
variant: 'primary',
size: 'medium',
},
}
);

const Button = ({
variant = 'primary',
size,
children,
className,
isDisabled = false,
...props
Comment on lines +35 to +39
Copy link
Collaborator

@jllee000 jllee000 Aug 25, 2025

Choose a reason for hiding this comment

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

질문있습니당
요거 props 형태로 받는 className는 외부에서 어떤 방식으로 주면 되는걸까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

일반 tailwind css 스타일 주듯이 똑같이 넘겨주시면 됩니다!! 기본 스타일은 거의 지정이 되어있어서 PR 내용처럼 width 정도 주면 될 것 같아요 👍

}: ButtonProps) => {
return (
<button type="button" {...props}>
<button
type="button"
className={cn(buttonVariants({ variant, size }), className)}
disabled={isDisabled}
{...props}
>
Comment on lines +33 to +47
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중요: disabled 우선순위가 {...props}에 의해 덮여 비활성화 상태가 틀어질 수 있습니다

현재 {...props}가 마지막에 배치되어 있어, 소비자가 disabled를 전달하면 isDisabled로 설정한 값이 덮여 버립니다. 예: <Button isDisabled onClick=... disabled={false} />인 경우 실제 DOM에는 disabled={false}가 적용될 수 있습니다. 또한 type="button"{...props}에 의해 임의로 덮일 수 있어 의도하지 않은 submit이 발생할 여지가 있습니다.

아래처럼 typedisabled를 구조분해 후 병합하고, 나머지를 rest로 분리해 충돌을 제거하세요. 이때 isDisabled가 최우선으로 적용되도록 하는 것을 권장합니다.

-const Button = ({
-  variant = 'primary',
-  size,
-  children,
-  className,
-  isDisabled = false,
-  ...props
-}: ButtonProps) => {
+const Button = ({
+  variant = 'primary',
+  size,
+  children,
+  className,
+  isDisabled,
+  type,
+  disabled,
+  ...rest
+}: ButtonProps) => {
   return (
     <button
-      type="button"
-      className={cn(buttonVariants({ variant, size }), className)}
-      disabled={isDisabled}
-      {...props}
+      type={type ?? 'button'}
+      className={cn(buttonVariants({ variant, size }), className)}
+      disabled={isDisabled ?? disabled}
+      {...rest}
     >
       {children}
     </button>
   );
 };

추가 제안(선택): ref 전달이 필요한 시나리오를 위해 forwardRef로 감싸는 것도 고려해 주세요.

+import React from 'react';
-const Button = ({ ... }: ButtonProps) => {
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+  ({ ...props }, ref) => {
+    const { /* 위 병합 로직 동일 */ } = props;
+    return (
+      <button ref={ref} /* ... */ />
+    );
+  }
+);
📝 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.

Suggested change
const Button = ({
variant = 'primary',
size,
children,
className,
isDisabled = false,
...props
}: ButtonProps) => {
return (
<button type="button" {...props}>
<button
type="button"
className={cn(buttonVariants({ variant, size }), className)}
disabled={isDisabled}
{...props}
>
const Button = ({
variant = 'primary',
size,
children,
className,
isDisabled,
type,
disabled,
...rest
}: ButtonProps) => {
return (
<button
type={type ?? 'button'}
className={cn(buttonVariants({ variant, size }), className)}
disabled={isDisabled ?? disabled}
{...rest}
>
{children}
</button>
);
};
🤖 Prompt for AI Agents
In packages/design-system/src/components/button/Button.tsx around lines 33 to
47, the spread {...props} placed last allows consumer-supplied type/disabled to
override component intent (so isDisabled or default type="button" can be
clobbered); fix by destructuring type and disabled (e.g. const { type, disabled,
...rest } = props) and then explicitly set type={type ?? "button"} and
disabled={isDisabled ?? disabled} (or disabled={Boolean(isDisabled) ||
Boolean(disabled)} with isDisabled taking precedence) and spread the remaining
rest props afterward; optionally wrap the component with forwardRef to support
ref forwarding.

{children}
</button>
);
Expand Down
Loading