+ {/* 댓글 입력 영역 */}
+
+
+
+ {/* 등록된 댓글 목록 */}
+
+
+ 등록된 댓글 ({comments.length})
+
+ {comments.length === 0 ? (
+
+ 아직 댓글이 없습니다
+
+ ) : (
+
+ {comments.map((comment, index) => (
+
+ {comment}
+
+ ))}
+
+ )}
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ '댓글 입력에서 EnterButton을 사용하는 실제 예시입니다. 텍스트가 입력되면 버튼이 활성화되고, Enter 키로도 전송할 수 있습니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 채팅 입력
+// ==============================
+
+export const ChatInputExample: Story = {
+ render: () => {
+ const [message, setMessage] = useState('');
+ const [messages, setMessages] = useState<{ text: string; time: string }[]>([
+ { text: '안녕하세요!', time: '10:30' },
+ { text: '반갑습니다 😊', time: '10:31' },
+ ]);
+
+ const handleSend = () => {
+ if (message.trim()) {
+ const now = new Date();
+ const time = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
+ setMessages([...messages, { text: message, time }]);
+ setMessage('');
+ }
+ };
+
+ return (
+
+ {/* 채팅 헤더 */}
+
+ 채팅방
+
+
+ {/* 메시지 목록 */}
+
+ {messages.map((msg, index) => (
+
+
+ {msg.text}
+
+
+ {msg.time}
+
+
+ ))}
+
+
+ {/* 입력 영역 */}
+
+ setMessage(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && message.trim()) {
+ handleSend();
+ }
+ }}
+ placeholder="메시지를 입력하세요"
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ border: '1px solid #e2e8f0',
+ borderRadius: '8px',
+ fontSize: '14px',
+ outline: 'none',
+ }}
+ />
+ 0} />
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '채팅 인터페이스에서 EnterButton을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 간단한 검색
+// ==============================
+
+export const SearchExample: Story = {
+ render: () => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+
+ const mockData = [
+ '프로젝트 기획서.pdf',
+ '회의록_2025.docx',
+ '디자인 시안 v1.0',
+ '개발 가이드.md',
+ '테스트 케이스 목록',
+ ];
+
+ const handleSearch = () => {
+ if (searchQuery.trim()) {
+ const results = mockData.filter((item) =>
+ item.toLowerCase().includes(searchQuery.toLowerCase()),
+ );
+ setSearchResults(results);
+ }
+ };
+
+ return (
+
+
+ setSearchQuery(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && searchQuery.trim()) {
+ handleSearch();
+ }
+ }}
+ placeholder="파일명을 검색하세요"
+ style={{
+ flex: 1,
+ border: 'none',
+ outline: 'none',
+ fontSize: '14px',
+ }}
+ />
+ 0} />
+
+
+ {searchResults.length > 0 && (
+
+
+ 검색 결과: {searchResults.length}개
+
+ {searchResults.map((result, index) => (
+
+ {result}
+
+ ))}
+
+ )}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '검색 인터페이스에서 EnterButton을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ Active 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
+ console.log('Inactive')} active={false} />
+ alert('전송!')} active={true} />
+
+
+ Inactive: 회색 아이콘, 클릭 불가
+ Active: 파란색 아이콘, 클릭 가능
+ Hover (Active만): opacity 0.8
+ Active: scale(0.95)
+ Focus: 2px outline
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
+ console.log('Should not fire')} active={false} />
+ alert('댓글 전송 완료!')} active={true} />
+
+
+ aria-label: "댓글 등록"
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭 (active일 때만)
+ Inactive 상태는 disabled로 처리되어 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
+
{}} active={true} />
+ 24px × 24px (고정 크기)
+
+
+ • 아이콘 크기: 24px × 24px
+
+ • Active: 파란색 화살표
+ • Inactive: 회색 화살표 (disabled)
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/EnterButton/EnterButton.tsx b/src/components/Button/domain/EnterButton/EnterButton.tsx
new file mode 100644
index 0000000..cda9e4e
--- /dev/null
+++ b/src/components/Button/domain/EnterButton/EnterButton.tsx
@@ -0,0 +1,40 @@
+import styles from './EnterButton.module.css';
+
+import arrowUpActive from '@/assets/buttons/arrow/arrowUpActivedButton.svg';
+import arrowUpNonActive from '@/assets/buttons/arrow/arrowUpNonActivedButton.svg';
+
+interface EnterButtonProps {
+ onClick: () => void;
+ active?: boolean;
+}
+
+/**
+ * EnterButton 컴포넌트
+ *
+ * @description
+ * 댓글 등록(전송) 액션을 위한 아이콘 버튼이다.
+ * `active` 값에 따라 아이콘이 전환되며, `active`가 false이면 버튼이 비활성화된다.
+ *
+ * @remarks
+ * - `disabled={!active}`로 상태를 강제하여, 비활성 상태에서는 클릭이 발생하지 않는다.
+ * - 비활성 시각 표현은 NonActive SVG 자체의 색상으로 처리하며, CSS로 opacity를 추가로 적용하지 않는다.
+ *
+ * @param props.onClick - 클릭 핸들러
+ * @param props.active - 활성화 여부(기본값: `false`)
+ * @returns 댓글 등록 아이콘 버튼
+ */
+export default function EnterButton({ onClick, active = false }: EnterButtonProps) {
+ const iconSrc = active ? arrowUpActive : arrowUpNonActive;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Button/domain/FilledRoundButton/FilledRoundButton.module.css b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.module.css
new file mode 100644
index 0000000..7b708c9
--- /dev/null
+++ b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.module.css
@@ -0,0 +1,76 @@
+/* FilledRoundButton.module.css */
+.root {
+ height: 40px;
+ border-radius: 40px;
+ padding: 14px 20px 14px 16px;
+ gap: 10px;
+ font-size: 16px;
+ font-weight: 600;
+ display: inline-flex;
+ width: max-content;
+}
+
+/* filled */
+.filled {
+ background-color: var(--color-brand-primary);
+ color: var(--color-text-inverse);
+}
+
+.filled:hover:not(:disabled) {
+ background-color: var(--color-interaction-hover);
+}
+
+.filled:active:not(:disabled) {
+ background-color: var(--color-interaction-pressed);
+}
+
+.filled:disabled {
+ background-color: var(--color-interaction-inactive);
+ color: var(--color-text-inverse);
+}
+
+/* inverse */
+.inverse {
+ background-color: var(--color-background-inverse);
+ color: var(--color-brand-primary);
+ border: 1.5px solid var(--color-brand-primary);
+}
+
+.inverse:hover:not(:disabled) {
+ color: var(--color-interaction-hover);
+ border-color: var(--color-interaction-hover);
+}
+
+.inverse:active:not(:disabled) {
+ color: var(--color-interaction-pressed);
+ border-color: var(--color-interaction-pressed);
+}
+
+.inverse:disabled {
+ color: var(--color-interaction-inactive);
+ border-color: var(--color-interaction-inactive);
+}
+
+/* shadow */
+.shadow {
+ box-shadow: 0 15px 50px -12px rgba(0, 0, 0, 0.05);
+}
+
+/* icon + label */
+.icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+}
+
+.icon img {
+ width: 100%;
+ height: 100%;
+}
+
+.label {
+ display: inline-block;
+ white-space: nowrap;
+}
diff --git a/src/components/Button/domain/FilledRoundButton/FilledRoundButton.stories.tsx b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.stories.tsx
new file mode 100644
index 0000000..c900e1b
--- /dev/null
+++ b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.stories.tsx
@@ -0,0 +1,595 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import FilledRoundButton from '@/components/Button/domain/FilledRoundButton/FilledRoundButton';
+
+/**
+ * FilledRoundButton 컴포넌트
+ *
+ * 체크 아이콘과 라벨을 함께 사용하는 둥근 형태의 버튼 컴포넌트입니다.
+ * CTA(Call-to-Action) 버튼이나 완료/확인 액션에 주로 사용됩니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/FilledRoundButton',
+ component: FilledRoundButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ appearance: {
+ control: 'select',
+ options: ['filled', 'inverse'],
+ description: '버튼 외형 스타일',
+ table: {
+ defaultValue: { summary: 'filled' },
+ },
+ },
+ shadow: {
+ control: 'boolean',
+ description: '그림자 적용 여부',
+ table: {
+ defaultValue: { summary: 'true' },
+ },
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ type: {
+ control: 'select',
+ options: ['button', 'submit', 'reset'],
+ description: '버튼 타입',
+ table: {
+ defaultValue: { summary: 'button' },
+ },
+ },
+ children: {
+ control: 'text',
+ description: '버튼 라벨 텍스트',
+ },
+ onClick: {
+ action: 'clicked',
+ description: '클릭 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const Filled: Story = {
+ args: {
+ children: 'Filled Button',
+ appearance: 'filled',
+ },
+};
+
+export const Inverse: Story = {
+ args: {
+ children: 'Inverse Button',
+ appearance: 'inverse',
+ },
+};
+
+export const FilledWithoutShadow: Story = {
+ args: {
+ children: 'No Shadow',
+ appearance: 'filled',
+ shadow: false,
+ },
+};
+
+export const InverseWithoutShadow: Story = {
+ args: {
+ children: 'No Shadow',
+ appearance: 'inverse',
+ shadow: false,
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ children: 'Disabled Button',
+ disabled: true,
+ },
+};
+
+export const DisabledInverse: Story = {
+ args: {
+ children: 'Disabled Inverse',
+ appearance: 'inverse',
+ disabled: true,
+ },
+};
+
+// ==============================
+// Appearance 비교 (한눈에 보기)
+// ==============================
+
+export const AllAppearances: Story = {
+ render: () => (
+
+
+
기본 상태
+
+ Filled
+ Inverse
+
+
+
+
+
Disabled 상태
+
+
+ Filled
+
+
+ Inverse
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 appearance 스타일을 한눈에 확인합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// Shadow 비교
+// ==============================
+
+export const ShadowComparison: Story = {
+ render: () => (
+
+
+
+ Filled - Shadow On / Off
+
+
+
+ Shadow On
+
+
+ Shadow Off
+
+
+
+
+
+
+ Inverse - Shadow On / Off
+
+
+
+ Shadow On
+
+
+ Shadow Off
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'shadow prop에 따른 그림자 효과를 비교합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - CTA 버튼
+// ==============================
+
+export const CTAExample: Story = {
+ render: () => (
+
+
+
+ 프로젝트가 완성되었습니다!
+
+
+ 작업한 내용을 확인하고 팀원들과 공유하세요.
+
+
+
+
+ alert('프로젝트 확인하기')}>
+ 프로젝트 확인하기
+
+ alert('나중에 하기')}>
+ 나중에 하기
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'CTA(Call-to-Action) 버튼으로 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 완료 확인
+// ==============================
+
+export const ConfirmationExample: Story = {
+ render: () => (
+
+
+
+ ✓
+
+
+ 작업이 완료되었습니다
+
+
변경사항이 저장되었습니다.
+
+
+
alert('확인')}
+ >
+ 확인
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '완료 확인 다이얼로그에서 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 모달 액션
+// ==============================
+
+export const ModalActionsExample: Story = {
+ render: () => (
+
+
+
+ 정말 삭제하시겠습니까?
+
+
+ 이 작업은 되돌릴 수 없습니다. 삭제된 데이터는 복구할 수 없습니다.
+
+
+
+
+ alert('취소')}>
+ 취소
+
+ alert('삭제 완료')}>
+ 삭제하기
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모달 다이얼로그의 액션 버튼으로 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 다단계 플로우
+// ==============================
+
+export const MultiStepFlowExample: Story = {
+ render: () => {
+ const steps = ['기본 정보', '상세 설정', '완료'];
+ const currentStep = 1;
+
+ return (
+
+ {/* 진행 단계 표시 */}
+
+ {steps.map((step, index) => (
+
+ ))}
+
+
+
+
+ {steps[currentStep]}
+
+
+ {currentStep + 1} / {steps.length}
+
+
+
+
+ alert('이전')}
+ disabled={currentStep <= 0}
+ >
+ 이전
+
+ alert('다음')}>
+ {currentStep === steps.length - 1 ? '완료' : '다음'}
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '다단계 플로우에서 네비게이션 버튼으로 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
+ Hover me
+ Click me
+
+
+ Filled Hover: 더 진한 파란색
+ Inverse Hover: 테두리와 텍스트 색상 변경
+ Active: 배경/테두리 pressed 색상
+ Focus: 2px 파란색 outline
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
+ alert('버튼 1')}>Tab me
+ alert('버튼 2')}>
+ Then me
+
+ alert('실행 안됨')}>
+ Skip me
+
+ alert('버튼 3')}>And me
+
+
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭
+ 체크 아이콘: aria-hidden="true"로 장식용 처리
+ disabled 버튼은 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
+
Sample Button
+
높이: 40px (고정)
+
+
+ • height: 40px
+
+ • border-radius: 40px (완전한 라운드)
+
+ • padding: 14px 20px 14px 16px
+
+ • font-size: 16px
+
+ • font-weight: 600
+
+ • 체크 아이콘: 16px × 16px
+ • width: 콘텐츠에 맞춰 자동 조정
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/FilledRoundButton/FilledRoundButton.tsx b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.tsx
new file mode 100644
index 0000000..6aa4b22
--- /dev/null
+++ b/src/components/Button/domain/FilledRoundButton/FilledRoundButton.tsx
@@ -0,0 +1,65 @@
+import type { ButtonHTMLAttributes } from 'react';
+
+import behavior from '@/components/Button/shared/ButtonBehavior.module.css';
+import styles from './FilledRoundButton.module.css';
+
+import checkWhite from '@/assets/icons/check/check-1.svg';
+import checkBlue from '@/assets/icons/check/check.svg';
+
+type Appearance = 'filled' | 'inverse';
+
+interface FilledRoundButtonProps extends ButtonHTMLAttributes {
+ appearance?: Appearance;
+ shadow?: boolean;
+}
+
+/**
+ * FilledRoundButton 컴포넌트
+ *
+ * @description
+ * 체크 아이콘과 라벨을 함께 사용하는 둥근 형태의 버튼 컴포넌트이다.
+ * BaseButton을 상속하지 않고, 공통 ButtonBehavior를 합성하여
+ * 버튼의 기본 동작과 접근성, 인터랙션을 공유한다.
+ *
+ * @remarks
+ * - `appearance` 값에 따라 배경 스타일과 체크 아이콘 색상이 전환된다.
+ * - `shadow` 옵션을 통해 시각적 깊이(그림자)를 선택적으로 적용할 수 있다.
+ * - BaseButton과 동일한 HTML button 속성을 지원한다.
+ *
+ * @param props.appearance - 버튼 외형 스타일(`filled` | `inverse`)
+ * @param props.shadow - 그림자 적용 여부(기본값: true)
+ * @param props.children - 버튼 라벨 콘텐츠
+ * @returns 체크 아이콘이 포함된 라운드 버튼
+ */
+export default function FilledRoundButton({
+ appearance = 'filled',
+ shadow = true,
+ type = 'button',
+ className,
+ children,
+ ...rest
+}: FilledRoundButtonProps) {
+ const checkIconSrc = appearance === 'inverse' ? checkBlue : checkWhite;
+
+ return (
+
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/Button/domain/FilledRoundButton/index.ts b/src/components/Button/domain/FilledRoundButton/index.ts
new file mode 100644
index 0000000..c3f7a7c
--- /dev/null
+++ b/src/components/Button/domain/FilledRoundButton/index.ts
@@ -0,0 +1 @@
+export { default as FilledRoundButton } from '@/components/Button/domain/FilledRoundButton/FilledRoundButton';
diff --git a/src/components/Button/domain/FloatingButton/FloatingButton.module.css b/src/components/Button/domain/FloatingButton/FloatingButton.module.css
new file mode 100644
index 0000000..915b531
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingButton.module.css
@@ -0,0 +1,50 @@
+.button {
+ /* Reset */
+ border: none;
+ background: none;
+ padding: 0;
+ appearance: none;
+ cursor: pointer;
+
+ /* Layout */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+ height: 56px;
+
+ /* Style */
+ background: var(--color-brand-primary);
+ border-radius: 50%;
+ box-shadow: 0px 5px 5px 0px rgba(49, 84, 153, 0.2);
+
+ /* Transition */
+ transition:
+ transform 0.15s ease,
+ background-color 0.2s ease;
+}
+
+.button:hover {
+ background: var(--color-interaction-hover);
+}
+
+.button:active {
+ transform: scale(0.95);
+ background: var(--color-interaction-pressed);
+}
+
+.button:focus-visible {
+ outline: 2px solid var(--color-brand-primary);
+ outline-offset: 2px;
+}
+
+.button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.icon {
+ width: 24px;
+ height: 24px;
+ display: block;
+}
diff --git a/src/components/Button/domain/FloatingButton/FloatingButton.stories.tsx b/src/components/Button/domain/FloatingButton/FloatingButton.stories.tsx
new file mode 100644
index 0000000..a343ed7
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingButton.stories.tsx
@@ -0,0 +1,549 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import { useState } from 'react';
+import FloatingButton from '@/components/Button/domain/FloatingButton/FloatingButton';
+
+/**
+ * FloatingButton 컴포넌트
+ *
+ * 화면에 고정되어 주요 액션을 제공하는 플로팅 버튼입니다.
+ * 새 항목 추가, 편집 등의 주요 액션에 사용됩니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/FloatingButton',
+ component: FloatingButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ icon: {
+ control: 'select',
+ options: ['plus', 'edit'],
+ description: '버튼에 표시할 아이콘 타입',
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ onClick: {
+ action: 'clicked',
+ description: '클릭 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const Plus: Story = {
+ args: {
+ icon: 'plus',
+ onClick: () => console.log('Plus button clicked'),
+ },
+};
+
+export const Edit: Story = {
+ args: {
+ icon: 'edit',
+ onClick: () => console.log('Edit button clicked'),
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ icon: 'plus',
+ disabled: true,
+ onClick: () => console.log('This should not fire'),
+ },
+};
+
+// ==============================
+// Icon 비교 (한눈에 보기)
+// ==============================
+
+export const AllIcons: Story = {
+ render: () => (
+
+
+
+ Plus Icon (새 항목 추가)
+
+
+ console.log('Plus')} />
+ console.log('Disabled')} disabled />
+
+
+
+
+
Edit Icon (편집)
+
+ console.log('Edit')} />
+ console.log('Disabled')} disabled />
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 아이콘 타입과 disabled 상태를 한눈에 확인합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 게시판 목록
+// ==============================
+
+export const BoardListExample: Story = {
+ render: () => {
+ const [posts] = useState([
+ { id: 1, title: '첫 번째 게시글', author: '홍길동' },
+ { id: 2, title: '두 번째 게시글', author: '김철수' },
+ { id: 3, title: '세 번째 게시글', author: '이영희' },
+ ]);
+
+ return (
+
+ {/* 헤더 */}
+
+
자유게시판
+
+
+ {/* 게시글 목록 */}
+
+ {posts.map((post) => (
+
+
+ {post.title}
+
+
작성자: {post.author}
+
+ ))}
+
+
+ {/* Floating Button */}
+
+ alert('새 글 작성')} />
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ '게시판 목록 페이지에서 FloatingButton을 사용하는 예시입니다. 우측 하단에 고정됩니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 할일 목록
+// ==============================
+
+export const TodoListExample: Story = {
+ render: () => {
+ const [todos, setTodos] = useState([
+ { id: 1, text: '프로젝트 기획서 작성', completed: false },
+ { id: 2, text: '디자인 시안 검토', completed: true },
+ { id: 3, text: '개발 일정 조율', completed: false },
+ ]);
+
+ const handleAddTodo = () => {
+ const newTodo = prompt('새 할일을 입력하세요');
+ if (newTodo) {
+ setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
+ }
+ };
+
+ return (
+
+ {/* 헤더 */}
+
+
오늘의 할일
+
+ {todos.filter((t) => !t.completed).length}개 남음
+
+
+
+ {/* 할일 목록 */}
+
+ {todos.map((todo) => (
+
+ {
+ setTodos(
+ todos.map((t) => (t.id === todo.id ? { ...t, completed: !t.completed } : t)),
+ );
+ }}
+ style={{ width: '18px', height: '18px' }}
+ />
+
+ {todo.text}
+
+
+ ))}
+
+
+ {/* Floating Button */}
+
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '할일 목록에서 새 항목을 추가하는 FloatingButton 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 편집 모드
+// ==============================
+
+export const EditModeExample: Story = {
+ render: () => {
+ const [isEditMode, setIsEditMode] = useState(false);
+
+ return (
+
+ {/* 헤더 */}
+
+
프로필
+ {isEditMode && (
+ setIsEditMode(false)}
+ style={{
+ padding: '6px 12px',
+ fontSize: '13px',
+ backgroundColor: '#5189fa',
+ color: '#ffffff',
+ border: 'none',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ }}
+ >
+ 완료
+
+ )}
+
+
+ {/* 프로필 내용 */}
+
+
+
+
홍길동
+
hong@example.com
+
+
+ {isEditMode && (
+
+ 편집 모드가 활성화되었습니다
+
+ )}
+
+
+ {/* Floating Button */}
+
+ setIsEditMode(!isEditMode)} />
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '편집 모드를 토글하는 FloatingButton 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
+ console.log('Plus clicked')} />
+ console.log('Edit clicked')} />
+
+
+ 기본: 파란색 원형 배경 + 그림자
+ Hover: 더 진한 파란색
+ Active: scale(0.95)
+ Focus: 2px 파란색 outline
+ Disabled: opacity 0.5
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
+ alert('새 항목 추가')} />
+ alert('편집')} />
+ alert('실행되지 않음')} disabled />
+
+
+ Plus: aria-label "새 항목 추가"
+ Edit: aria-label "편집"
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭
+ disabled 버튼은 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
+
{}} />
+ 56px × 56px (고정 크기)
+
+
+ • 크기: 56px × 56px
+
+ • border-radius: 50% (완전한 원형)
+
+ • box-shadow: 0px 5px 5px 0px rgba(49, 84, 153, 0.2)
+
+ • 아이콘: 24px × 24px
+ • background: var(--color-brand-primary)
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/FloatingButton/FloatingButton.tsx b/src/components/Button/domain/FloatingButton/FloatingButton.tsx
new file mode 100644
index 0000000..62b5883
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingButton.tsx
@@ -0,0 +1,43 @@
+import styles from './FloatingButton.module.css';
+
+import plusBigIcon from '@/assets/icons/plus/plusBig.svg';
+import pencilIcon from '@/assets/icons/pencil/pencil.svg';
+
+interface FloatingButtonProps {
+ icon: 'plus' | 'edit';
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+/**
+ * FloatingButton 컴포넌트
+ *
+ * @description
+ * 화면에 고정되어 주요 액션을 제공하는 플로팅 버튼이다.
+ * `icon` 값에 따라 추가(plus) 또는 편집(edit) 아이콘을 렌더링한다.
+ *
+ * @remarks
+ * - `icon` 타입에 따라 `aria-label`을 자동으로 설정한다.
+ * - `disabled`가 true이면 버튼 클릭이 비활성화된다.
+ *
+ * @param props.icon - 버튼에 표시할 아이콘 타입(`plus` | `edit`)
+ * @param props.onClick - 클릭 핸들러
+ * @param props.disabled - 비활성화 여부
+ * @returns 플로팅 액션 버튼
+ */
+export default function FloatingButton({ icon, onClick, disabled }: FloatingButtonProps) {
+ const iconSrc = icon === 'plus' ? plusBigIcon : pencilIcon;
+ const ariaLabel = icon === 'plus' ? '새 항목 추가' : '편집';
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Button/domain/FloatingButton/FloatingLikeButton.module.css b/src/components/Button/domain/FloatingButton/FloatingLikeButton.module.css
new file mode 100644
index 0000000..9f22be2
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingLikeButton.module.css
@@ -0,0 +1,69 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+}
+
+.button {
+ /* Reset */
+ border: none;
+ background: none;
+ padding: 0;
+ appearance: none;
+ cursor: pointer;
+
+ /* Layout */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+ height: 56px;
+
+ /* Style */
+ background: var(--color-background-primary);
+ border: 1px solid var(--color-border-primary);
+ border-radius: 50%;
+ box-shadow: 0px 15px 50px 0px rgba(0, 0, 0, 0.05);
+
+ /* Transition */
+ transition:
+ transform 0.15s ease,
+ border-color 0.2s ease;
+}
+
+.button:hover {
+ border-color: var(--color-border-secondary);
+}
+
+.button:active {
+ transform: scale(0.95);
+}
+
+.button:focus-visible {
+ outline: 2px solid var(--color-brand-primary);
+ outline-offset: 2px;
+}
+
+.button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.icon {
+ width: 24px;
+ height: 24px;
+ display: block;
+ transition: transform 0.2s ease;
+}
+
+.button:active .icon {
+ transform: scale(1.1);
+}
+
+.count {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 19px;
+ color: var(--color-text-tertiary);
+}
diff --git a/src/components/Button/domain/FloatingButton/FloatingLikeButton.stories.tsx b/src/components/Button/domain/FloatingButton/FloatingLikeButton.stories.tsx
new file mode 100644
index 0000000..cf818c2
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingLikeButton.stories.tsx
@@ -0,0 +1,643 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import { useState } from 'react';
+import FloatingLikeButton from '@/components/Button/domain/FloatingButton/FloatingLikeButton';
+
+/**
+ * FloatingLikeButton 컴포넌트
+ *
+ * 좋아요(토글) 액션을 제공하는 플로팅 버튼입니다.
+ * 게시글, 콘텐츠 등에 좋아요 기능을 추가할 때 사용됩니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/FloatingLikeButton',
+ component: FloatingLikeButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ isLiked: {
+ control: 'boolean',
+ description: '좋아요 활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ count: {
+ control: 'number',
+ description: '좋아요 개수 (999 초과 시 "999+" 표시)',
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ onToggle: {
+ action: 'toggled',
+ description: '좋아요 토글 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const NotLiked: Story = {
+ args: {
+ isLiked: false,
+ count: 0,
+ onToggle: () => console.log('Toggle'),
+ },
+};
+
+export const Liked: Story = {
+ args: {
+ isLiked: true,
+ count: 1,
+ onToggle: () => console.log('Toggle'),
+ },
+};
+
+export const WithCount: Story = {
+ args: {
+ isLiked: false,
+ count: 42,
+ onToggle: () => console.log('Toggle'),
+ },
+};
+
+export const WithLargeCount: Story = {
+ args: {
+ isLiked: true,
+ count: 999,
+ onToggle: () => console.log('Toggle'),
+ },
+};
+
+export const WithMaxCount: Story = {
+ args: {
+ isLiked: true,
+ count: 1234,
+ onToggle: () => console.log('Toggle'),
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ isLiked: false,
+ count: 10,
+ disabled: true,
+ onToggle: () => console.log('This should not fire'),
+ },
+};
+
+// ==============================
+// 상태 비교 (한눈에 보기)
+// ==============================
+
+export const AllStates: Story = {
+ render: () => (
+
+
+
기본 상태
+
+
+
+
+
+
+
{}} />
+
+ 선택, 1234 (999+)
+
+
+
+
+
+
+
Disabled 상태
+
+ {}} disabled />
+ {}} disabled />
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 상태와 count 표시를 한눈에 확인합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 인터랙티브 토글
+// ==============================
+
+export const InteractiveToggle: Story = {
+ render: () => {
+ const [isLiked, setIsLiked] = useState(false);
+ const [count, setCount] = useState(42);
+
+ const handleToggle = () => {
+ setIsLiked(!isLiked);
+ setCount(isLiked ? count - 1 : count + 1);
+ };
+
+ return (
+
+
+
+ 클릭하여 좋아요를 토글해보세요
+
+
+ 현재 상태: {isLiked ? '좋아요 ❤️' : '좋아요 안함 🤍'}
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '좋아요를 토글할 수 있는 인터랙티브 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 게시글
+// ==============================
+
+export const BlogPostExample: Story = {
+ render: () => {
+ const [isLiked, setIsLiked] = useState(false);
+ const [count, setCount] = useState(127);
+
+ const handleToggle = () => {
+ setIsLiked(!isLiked);
+ setCount(isLiked ? count - 1 : count + 1);
+ };
+
+ return (
+
+ {/* 헤더 이미지 */}
+
+ Featured Image
+
+
+ {/* 콘텐츠 */}
+
+
+ 블로그 포스트 제목
+
+
+
+ 홍길동
+ •
+ 2025년 2월 4일
+
+
+
+ 이것은 블로그 포스트의 본문 내용입니다. 여기에 긴 글이 들어가며, 사용자는 우측에 있는
+ FloatingLikeButton을 통해 이 글에 좋아요를 표시할 수 있습니다.
+
+
+
+ {/* Floating Like Button */}
+
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '블로그 포스트에서 FloatingLikeButton을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 이미지 갤러리
+// ==============================
+
+export const ImageGalleryExample: Story = {
+ render: () => {
+ const [likes, setLikes] = useState([
+ { id: 1, isLiked: false, count: 24 },
+ { id: 2, isLiked: true, count: 156 },
+ { id: 3, isLiked: false, count: 89 },
+ ]);
+
+ const handleToggle = (id: number) => {
+ setLikes(
+ likes.map((item) =>
+ item.id === id
+ ? {
+ ...item,
+ isLiked: !item.isLiked,
+ count: item.isLiked ? item.count - 1 : item.count + 1,
+ }
+ : item,
+ ),
+ );
+ };
+
+ return (
+
+ {likes.map((item) => (
+
+
+ Image {item.id}
+
+
+ handleToggle(item.id)}
+ />
+
+
+ ))}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '이미지 갤러리에서 각 이미지에 좋아요 버튼을 추가한 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 카드 리스트
+// ==============================
+
+export const CardListExample: Story = {
+ render: () => {
+ const [cards, setCards] = useState([
+ { id: 1, title: '첫 번째 카드', description: '카드 설명 1', isLiked: false, count: 12 },
+ { id: 2, title: '두 번째 카드', description: '카드 설명 2', isLiked: true, count: 45 },
+ { id: 3, title: '세 번째 카드', description: '카드 설명 3', isLiked: false, count: 999 },
+ ]);
+
+ const handleToggle = (id: number) => {
+ setCards(
+ cards.map((card) =>
+ card.id === id
+ ? {
+ ...card,
+ isLiked: !card.isLiked,
+ count: card.isLiked ? card.count - 1 : card.count + 1,
+ }
+ : card,
+ ),
+ );
+ };
+
+ return (
+
+ {cards.map((card) => (
+
+
+
+ {card.title}
+
+
{card.description}
+
+
+ handleToggle(card.id)}
+ />
+
+
+ ))}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '카드 리스트에서 각 카드에 좋아요 버튼을 추가한 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
+ {}} />
+ {}} />
+
+
+ 기본: 흰색 원형 배경 + 테두리 + 그림자
+ 미선택: 빈 하트 아이콘
+ 선택: 채워진 하트 아이콘
+ Hover: 테두리 색상 변경
+ Active: scale(0.95) + 아이콘 scale(1.1)
+ Focus: 2px 파란색 outline
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => {
+ const [isLiked, setIsLiked] = useState(false);
+ const [count, setCount] = useState(5);
+
+ const handleToggle = () => {
+ setIsLiked(!isLiked);
+ setCount(isLiked ? count - 1 : count + 1);
+ };
+
+ return (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 토글해보세요:
+
+
+
+ {}} disabled />
+
+
+ aria-label: "좋아요" 또는 "좋아요 취소"
+ aria-pressed: 선택 상태를 스크린 리더에 전달
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 토글
+ disabled 버튼은 포커스를 받지 않음
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
+
{}} />
+
+ 버튼: 56px × 56px
+
+ Count: 16px font-size
+
+
+
+ • 버튼 크기: 56px × 56px
+
+ • border-radius: 50% (완전한 원형)
+
+ • border: 1px solid
+
+ • box-shadow: 0px 15px 50px 0px rgba(0, 0, 0, 0.05)
+
+ • 아이콘: 24px × 24px
+
+ • Count: 16px, font-weight: 400
+ • Count 최대값: 999 (초과 시 "999+" 표시)
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/FloatingButton/FloatingLikeButton.tsx b/src/components/Button/domain/FloatingButton/FloatingLikeButton.tsx
new file mode 100644
index 0000000..042d7ff
--- /dev/null
+++ b/src/components/Button/domain/FloatingButton/FloatingLikeButton.tsx
@@ -0,0 +1,62 @@
+import styles from './FloatingLikeButton.module.css';
+
+import emptyHeartIcon from '@/assets/icons/heart/emptyHeartLarge.svg';
+import fullHeartIcon from '@/assets/icons/heart/fullHeartLarge.svg';
+
+interface FloatingLikeButtonProps {
+ isLiked: boolean;
+ count?: number;
+ onToggle: () => void;
+ disabled?: boolean;
+}
+
+function formatCount(n: number): string {
+ return n > 999 ? '999+' : String(n);
+}
+
+/**
+ * FloatingLikeButton 컴포넌트
+ *
+ * @description
+ * 좋아요(토글) 액션을 제공하는 플로팅 버튼이다.
+ * 좋아요 상태(`isLiked`)에 따라 아이콘이 전환되며,
+ * 선택 상태는 `aria-pressed`로 접근성 속성을 제공한다.
+ *
+ * @remarks
+ * - 좋아요 개수(`count`)가 전달되면 버튼 하단에 표시된다.
+ * - 개수는 999를 초과할 경우 `"999+"` 형식으로 축약된다.
+ *
+ * @param props.isLiked - 좋아요 활성화 여부
+ * @param props.count - 좋아요 개수(선택)
+ * @param props.onToggle - 좋아요 토글 핸들러
+ * @param props.disabled - 비활성화 여부
+ * @returns 좋아요 플로팅 버튼
+ */
+export default function FloatingLikeButton({
+ isLiked,
+ count,
+ onToggle,
+ disabled,
+}: FloatingLikeButtonProps) {
+ const iconSrc = isLiked ? fullHeartIcon : emptyHeartIcon;
+ const ariaLabel = isLiked ? '좋아요 취소' : '좋아요';
+
+ return (
+
+
+
+
+
+ {typeof count === 'number' && count > 0 && (
+
{formatCount(count)}
+ )}
+
+ );
+}
diff --git a/src/components/Button/domain/GnbAddButton/GnbAddButton.module.css b/src/components/Button/domain/GnbAddButton/GnbAddButton.module.css
new file mode 100644
index 0000000..1a287dc
--- /dev/null
+++ b/src/components/Button/domain/GnbAddButton/GnbAddButton.module.css
@@ -0,0 +1,65 @@
+.button {
+ /* Reset */
+ border: none;
+ background: none;
+ padding: 0;
+ appearance: none;
+ cursor: pointer;
+
+ /* Layout */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ height: 33px;
+ padding: 0 12px;
+
+ /* Style */
+ background: transparent;
+ border: 1px solid var(--color-brand-primary);
+ border-radius: 4px;
+
+ /* Transition */
+ transition:
+ background-color 0.2s ease,
+ border-color 0.2s ease,
+ transform 0.15s ease;
+}
+
+.button:hover:not(:disabled) {
+ background: var(--color-brand-secondary);
+ border-color: var(--color-interaction-hover);
+}
+
+.button:active:not(:disabled) {
+ transform: scale(0.98);
+ background: var(--color-interaction-pressed);
+}
+
+.button:focus-visible {
+ outline: 2px solid var(--color-brand-primary);
+ outline-offset: 2px;
+}
+
+.button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.icon {
+ width: 16px;
+ height: 16px;
+ display: block;
+}
+
+.text {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 1;
+ color: var(--color-brand-primary);
+}
+
+.button:hover:not(:disabled) .text {
+ color: var(--color-interaction-hover);
+}
diff --git a/src/components/Button/domain/GnbAddButton/GnbAddButton.stories.tsx b/src/components/Button/domain/GnbAddButton/GnbAddButton.stories.tsx
new file mode 100644
index 0000000..0d1b073
--- /dev/null
+++ b/src/components/Button/domain/GnbAddButton/GnbAddButton.stories.tsx
@@ -0,0 +1,546 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import GnbAddButton from '@/components/Button/domain/GnbAddButton/GnbAddButton';
+
+/**
+ * GnbAddButton 컴포넌트
+ *
+ * GNB(Global Navigation Bar) 영역에서 새 항목을 추가하기 위한 버튼입니다.
+ * 주로 사이드바에서 팀, 프로젝트 등을 추가할 때 사용됩니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/GnbAddButton',
+ component: GnbAddButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ label: {
+ control: 'text',
+ description: '버튼에 표시될 텍스트',
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ onClick: {
+ action: 'clicked',
+ description: '클릭 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const Default: Story = {
+ args: {
+ label: '팀 추가하기',
+ onClick: () => console.log('팀 추가 클릭'),
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ label: '팀 추가하기',
+ disabled: true,
+ onClick: () => console.log('This should not fire'),
+ },
+};
+
+// ==============================
+// 다양한 라벨
+// ==============================
+
+export const VariousLabels: Story = {
+ render: () => (
+
+ console.log('팀 추가')} />
+ console.log('프로젝트 추가')} />
+ console.log('워크스페이스 생성')} />
+ console.log('보드 생성')} />
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '다양한 라벨 텍스트로 버튼을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상태 비교
+// ==============================
+
+export const AllStates: Story = {
+ render: () => (
+
+
+
기본 상태
+
console.log('클릭')} />
+
+
+
+
Disabled 상태
+
{}} disabled />
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 상태를 한눈에 확인합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - GNB Sidebar
+// ==============================
+
+export const GnbSidebarExample: Story = {
+ render: () => (
+
+ {/* GNB Header */}
+
+
+ COWORKERS
+
+
+
+ {/* Team List */}
+
+
+ 팀 선택
+
+ {['경영관리팀', '프로덕트팀', '마케팅팀', '콘텐츠팀'].map((team, index) => (
+
+ {team}
+
+ ))}
+
+
+ {/* Add Button */}
+
+ alert('팀 생성 모달 열기')} />
+
+
+ {/* Footer */}
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'GNB 사이드바에서 GnbAddButton을 사용하는 실제 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 프로젝트 사이드바
+// ==============================
+
+export const ProjectSidebarExample: Story = {
+ render: () => (
+
+ {/* Header */}
+
+
+ 내 프로젝트
+
+
5개의 프로젝트
+
+
+ {/* Project List */}
+
+ {[
+ { name: '웹사이트 리뉴얼', status: '진행중' },
+ { name: '모바일 앱 개발', status: '계획중' },
+ { name: 'UI/UX 개선', status: '완료' },
+ { name: '마케팅 캠페인', status: '진행중' },
+ { name: 'CS 시스템 구축', status: '진행중' },
+ ].map((project) => (
+
+
+ {project.name}
+
+
{project.status}
+
+ ))}
+
+
+ {/* Add Button */}
+
+ alert('프로젝트 생성')} />
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '프로젝트 사이드바에서 새 프로젝트를 추가하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 워크스페이스 선택
+// ==============================
+
+export const WorkspaceExample: Story = {
+ render: () => (
+
+ {/* Header */}
+
+
+ 워크스페이스
+
+
+
+ {/* Workspace List */}
+
+ {['개인 워크스페이스', '팀 A 워크스페이스', '팀 B 워크스페이스'].map((workspace, index) => (
+
+ ))}
+
+
+ {/* Add Button */}
+
+ alert('워크스페이스 생성 모달')} />
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '워크스페이스 선택 UI에서 새 워크스페이스를 추가하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
console.log('클릭')} />
+
+ 기본: 투명 배경 + 파란색 테두리
+ Hover: 연한 파란색 배경 + 진한 테두리
+ Active: scale(0.98)
+ Focus: 2px 파란색 outline
+ width: 100% (부모 너비에 맞춤)
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
alert('팀 추가')} />
+ alert('프로젝트 추가')} />
+ {}} disabled />
+
+ aria-label: label prop 값과 동일
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭
+ disabled 버튼은 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
{}} />
+
+ • width: 100% (부모 컨테이너에 맞춤)
+
+ • height: 33px
+
+ • padding: 0 12px
+
+ • border-radius: 4px
+
+ • border: 1px solid
+
+ • icon: 16px × 16px
+
+ • font-size: 14px
+ • font-weight: 500
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/GnbAddButton/GnbAddButton.tsx b/src/components/Button/domain/GnbAddButton/GnbAddButton.tsx
new file mode 100644
index 0000000..e2064f6
--- /dev/null
+++ b/src/components/Button/domain/GnbAddButton/GnbAddButton.tsx
@@ -0,0 +1,40 @@
+import styles from './GnbAddButton.module.css';
+
+import plusIcon from '@/assets/buttons/plus/plusGnbAddButton.svg';
+
+interface GnbAddButtonProps {
+ label: string;
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+/**
+ * GnbAddButton 컴포넌트
+ *
+ * @description
+ * GNB(Global Navigation Bar) 영역에서 새 항목을 추가하기 위한 버튼이다.
+ * 플러스 아이콘과 텍스트 라벨을 함께 표시하며, 전체 너비를 차지하는 형태로 렌더링된다.
+ *
+ * @remarks
+ * - 접근성을 위해 `aria-label`에 `label` 값을 그대로 사용한다.
+ * - `disabled`가 true이면 버튼 클릭이 비활성화된다.
+ *
+ * @param props.label - 버튼에 표시될 텍스트 및 aria-label 값
+ * @param props.onClick - 클릭 핸들러
+ * @param props.disabled - 비활성화 여부
+ * @returns GNB 추가 버튼
+ */
+export default function GnbAddButton({ label, onClick, disabled = false }: GnbAddButtonProps) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.module.css b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.module.css
new file mode 100644
index 0000000..ad50dc5
--- /dev/null
+++ b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.module.css
@@ -0,0 +1,115 @@
+/* ==============================
+ Root
+ ============================== */
+
+.root {
+ display: inline-flex;
+ width: max-content;
+
+ /* height 33px를 padding+border 포함해서 정확히 맞추기 */
+ box-sizing: border-box;
+
+ height: 33px;
+ padding: 8px 12px;
+ gap: 4px;
+
+ border-radius: 8px;
+ border: 1px solid var(--color-brand-primary);
+
+ background-color: transparent;
+ color: var(--color-brand-primary);
+
+ font-size: 14px;
+ font-weight: 600;
+}
+
+/* ==============================
+ Text & Border states
+ ============================== */
+
+.root:hover:not(:disabled) {
+ color: var(--color-interaction-hover);
+ border-color: var(--color-interaction-hover);
+}
+
+.root:active:not(:disabled) {
+ color: var(--color-interaction-pressed);
+ border-color: var(--color-interaction-pressed);
+}
+
+.root:disabled {
+ color: var(--color-interaction-inactive);
+ border-color: var(--color-interaction-inactive);
+}
+
+/* ==============================
+ Icon wrapper
+ ============================== */
+
+.icon {
+ position: relative;
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+ flex: 0 0 16px;
+}
+
+/* 모든 아이콘을 겹쳐두고 기본은 숨김 */
+.icon img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+}
+
+/* ==============================
+ Icon states (중요: 선언 순서)
+ ============================== */
+
+/* default */
+.icon .iconPrimary {
+ opacity: 1;
+}
+
+/* hover */
+.root:hover:not(:disabled) .iconPrimary {
+ opacity: 0;
+}
+.root:hover:not(:disabled) .iconHover {
+ opacity: 1;
+}
+
+/* pressed (hover보다 우선) */
+.root:active:not(:disabled) .iconPrimary {
+ opacity: 0;
+}
+.root:active:not(:disabled) .iconHover {
+ opacity: 0;
+}
+.root:active:not(:disabled) .iconPressed {
+ opacity: 1;
+}
+
+/* disabled (최우선) */
+.root:disabled .iconPrimary {
+ opacity: 0;
+}
+.root:disabled .iconHover {
+ opacity: 0;
+}
+.root:disabled .iconPressed {
+ opacity: 0;
+}
+.root:disabled .iconInactive {
+ opacity: 1;
+}
+
+/* ==============================
+ Label
+ ============================== */
+
+.label {
+ display: inline-block;
+ white-space: nowrap;
+}
diff --git a/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.stories.tsx b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.stories.tsx
new file mode 100644
index 0000000..c3b6cde
--- /dev/null
+++ b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.stories.tsx
@@ -0,0 +1,556 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import OutlineIconTextButton from '@/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton';
+
+/**
+ * OutlineIconTextButton 컴포넌트
+ *
+ * 아이콘과 텍스트를 함께 사용하는 아웃라인 스타일 버튼입니다.
+ * 상태별로 체크 아이콘이 변경되어 시각적 피드백을 제공합니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/OutlineIconTextButton',
+ component: OutlineIconTextButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ type: {
+ control: 'select',
+ options: ['button', 'submit', 'reset'],
+ description: '버튼 타입',
+ table: {
+ defaultValue: { summary: 'button' },
+ },
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ children: {
+ control: 'text',
+ description: '버튼 라벨 텍스트',
+ },
+ onClick: {
+ action: 'clicked',
+ description: '클릭 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const Default: Story = {
+ args: {
+ children: '완료 하기',
+ onClick: () => console.log('Clicked'),
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ children: '완료 하기',
+ disabled: true,
+ onClick: () => console.log('This should not fire'),
+ },
+};
+
+// ==============================
+// 다양한 라벨
+// ==============================
+
+export const VariousLabels: Story = {
+ render: () => (
+
+ console.log('완료')}>완료 하기
+ console.log('취소')}>
+ 완료 취소하기
+
+ console.log('확인')}>확인 완료
+ console.log('작업')}>작업 완료
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '다양한 라벨 텍스트로 버튼을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상태 비교
+// ==============================
+
+export const AllStates: Story = {
+ render: () => (
+
+
+
기본 상태
+
+ 완료 하기
+ 완료 취소하기
+
+
+
+
+
Disabled 상태
+
+ 완료 하기
+ 완료 취소하기
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 상태를 한눈에 확인합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 할일 완료
+// ==============================
+
+export const TodoCompleteExample: Story = {
+ render: () => {
+ const todos = [
+ { id: 1, text: '프로젝트 기획서 작성', completed: false },
+ { id: 2, text: '디자인 시안 검토', completed: true },
+ { id: 3, text: '개발 일정 조율', completed: false },
+ ];
+
+ return (
+
+ {todos.map((todo) => (
+
+
+ {todo.text}
+
+ alert(`"${todo.text}" 완료 상태 변경`)}
+ disabled={todo.completed}
+ >
+ {todo.completed ? '완료됨' : '완료 하기'}
+
+
+ ))}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '할일 목록에서 완료 처리하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 작업 승인
+// ==============================
+
+export const ApprovalExample: Story = {
+ render: () => {
+ const tasks = [
+ { id: 1, title: '디자인 리뷰 요청', status: 'pending' },
+ { id: 2, title: '코드 리뷰 요청', status: 'pending' },
+ { id: 3, title: '배포 승인 요청', status: 'approved' },
+ ];
+
+ return (
+
+
승인 대기 목록
+ {tasks.map((task) => (
+
+
+
+
+ {task.title}
+
+
+ {task.status === 'approved' ? '승인 완료' : '승인 대기중'}
+
+
+
alert(`"${task.title}" 승인`)}
+ disabled={task.status === 'approved'}
+ >
+ {task.status === 'approved' ? '승인 완료' : '승인 하기'}
+
+
+
+ ))}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '작업 승인 프로세스에서 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 모달 액션
+// ==============================
+
+export const ModalActionsExample: Story = {
+ render: () => (
+
+
+
+ 작업을 완료하시겠습니까?
+
+
+ 완료한 작업은 목록에서 제거되며, 완료 기록에 저장됩니다.
+
+
+
+
+ alert('취소')}>취소
+ alert('완료')}>완료
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모달 다이얼로그의 확인/취소 액션으로 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 실제 사용 예시 - 폼 제출
+// ==============================
+
+export const FormExample: Story = {
+ render: () => (
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '폼 제출 시나리오에서 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요 (아이콘이 변경됩니다):
+
+
+ 완료 하기
+ 완료 취소하기
+
+
+ 기본: Primary 체크 아이콘 + 파란색 테두리
+ Hover: Hover 체크 아이콘 + 진한 파란색
+ Active (pressed): Pressed 체크 아이콘
+ Disabled: Inactive 체크 아이콘 + 회색
+ Focus: 2px 파란색 outline
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태와 아이콘 변경을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
+ alert('버튼 1')}>Tab me
+ alert('버튼 2')}>Then me
+ {}}>
+ Skip me
+
+ alert('버튼 3')}>And me
+
+
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭
+ 체크 아이콘: aria-hidden="true"로 장식용 처리
+ disabled 버튼은 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
완료 하기
+
+ • height: 33px
+
+ • padding: 8px 12px
+
+ • gap: 4px (아이콘과 텍스트 사이)
+
+ • border-radius: 8px
+
+ • border: 1px solid
+
+ • 체크 아이콘: 16px × 16px
+
+ • font-size: 14px
+
+ • font-weight: 600
+ • width: max-content (콘텐츠에 맞춰 자동)
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+};
diff --git a/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.tsx b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.tsx
new file mode 100644
index 0000000..4de544c
--- /dev/null
+++ b/src/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton.tsx
@@ -0,0 +1,53 @@
+import type { ButtonHTMLAttributes } from 'react';
+
+import behavior from '@/components/Button/shared/ButtonBehavior.module.css';
+import styles from './OutlineIconTextButton.module.css';
+
+import checkPrimary from '@/assets/icons/check/primarycheck.svg';
+import checkHover from '@/assets/icons/check/hovercheck.svg';
+import checkPressed from '@/assets/icons/check/pressedcheck.svg';
+import checkInactive from '@/assets/icons/check/inactivecheck.svg';
+
+type OutlineIconTextButtonProps = ButtonHTMLAttributes;
+
+/**
+ * OutlineIconTextButton 컴포넌트
+ *
+ * @description
+ * 아이콘과 텍스트를 함께 사용하는 아웃라인 스타일 버튼이다.
+ * ButtonBehavior를 합성하여 버튼의 공통 동작과 접근성을 공유하며,
+ * 상태별 아이콘(primary/hover/pressed/inactive)은 CSS를 통해 제어된다.
+ *
+ * @remarks
+ * - BaseButton을 상속하지 않고 ButtonBehavior를 합성하여 구현한다.
+ * - 아이콘은 여러 SVG를 중첩 렌더링한 뒤, CSS 상태에 따라 표시 여부를 제어한다.
+ * - 버튼 상태(disabled, hover, active)는 시각적 아이콘 변화로 명확히 구분된다.
+ *
+ * @param props.type - 버튼 타입(기본값: `button`)
+ * @param props.className - 외부에서 전달되는 추가 클래스
+ * @param props.children - 버튼 라벨 콘텐츠
+ * @returns 아이콘과 텍스트가 결합된 아웃라인 버튼
+ */
+export default function OutlineIconTextButton({
+ type = 'button',
+ className,
+ children,
+ ...rest
+}: OutlineIconTextButtonProps) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/Button/domain/OutlineIconTextButton/index.ts b/src/components/Button/domain/OutlineIconTextButton/index.ts
new file mode 100644
index 0000000..763b164
--- /dev/null
+++ b/src/components/Button/domain/OutlineIconTextButton/index.ts
@@ -0,0 +1 @@
+export { default as OutlineIconTextButton } from '@/components/Button/domain/OutlineIconTextButton/OutlineIconTextButton';
diff --git a/src/components/Button/domain/ProgressButton/ProgressButton.module.css b/src/components/Button/domain/ProgressButton/ProgressButton.module.css
new file mode 100644
index 0000000..7d277c1
--- /dev/null
+++ b/src/components/Button/domain/ProgressButton/ProgressButton.module.css
@@ -0,0 +1,63 @@
+.button {
+ /* Reset */
+ border: none;
+ background: none;
+ padding: 0;
+ appearance: none;
+ cursor: pointer;
+
+ /* Layout */
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ height: 38px;
+ padding: 0 8px 0 20px;
+ gap: 8px;
+
+ /* Style */
+ background: var(--color-background-tertiary);
+ border-radius: 12px;
+
+ /* Transition */
+ transition:
+ background-color 0.2s ease,
+ transform 0.15s ease;
+}
+
+.button:active:not(:disabled) {
+ transform: scale(0.98);
+}
+
+.button:focus-visible {
+ outline: 2px solid var(--color-brand-primary);
+ outline-offset: 2px;
+}
+
+.button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.text {
+ flex: 1;
+ text-align: left;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.icon {
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ display: block;
+ transition: opacity 0.2s ease;
+}
+
+.button:hover:not(:disabled) .icon {
+ opacity: 0.8;
+}
diff --git a/src/components/Button/domain/ProgressButton/ProgressButton.stories.tsx b/src/components/Button/domain/ProgressButton/ProgressButton.stories.tsx
new file mode 100644
index 0000000..6b0ca97
--- /dev/null
+++ b/src/components/Button/domain/ProgressButton/ProgressButton.stories.tsx
@@ -0,0 +1,480 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+import { useState } from 'react';
+import ProgressButton from '@/components/Button/domain/ProgressButton/ProgressButton';
+
+/**
+ * ProgressButton 컴포넌트
+ *
+ * 진행 영역(칸반 보드 컬럼 등)에서 새로운 항목을 추가하기 위한 버튼입니다.
+ * 전체 너비를 차지하며, 텍스트가 길 경우 말줄임 처리됩니다.
+ */
+const meta: Meta = {
+ title: 'Components/Button/ProgressButton',
+ component: ProgressButton,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ label: {
+ control: 'text',
+ description: '버튼에 표시될 텍스트 (카테고리명)',
+ },
+ disabled: {
+ control: 'boolean',
+ description: '버튼 비활성화 여부',
+ table: {
+ defaultValue: { summary: 'false' },
+ },
+ },
+ onClick: {
+ action: 'clicked',
+ description: '클릭 핸들러',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ==============================
+// 기본 스토리
+// ==============================
+
+export const Default: Story = {
+ args: {
+ label: '할 일',
+ onClick: () => console.log('할 일 추가'),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export const LongText: Story = {
+ args: {
+ label: '아주 긴 카테고리 이름이 있을 때 말줄임 처리가 되는지 확인',
+ onClick: () => console.log('추가'),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export const Disabled: Story = {
+ args: {
+ label: '완료',
+ disabled: true,
+ onClick: () => console.log('This should not fire'),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+// ==============================
+// 다양한 라벨
+// ==============================
+
+export const VariousLabels: Story = {
+ render: () => (
+
+
console.log('할 일')} />
+ console.log('진행중')} />
+ console.log('완료')} />
+ console.log('보류')} />
+ console.log('검토 중')} />
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '다양한 카테고리 라벨로 버튼을 사용하는 예시입니다.',
+ },
+ },
+ },
+};
+
+// ==============================
+// 상태 비교
+// ==============================
+
+export const AllStates: Story = {
+ render: () => (
+
+
+
기본 상태
+
console.log('클릭')} />
+
+
+
+
+ 긴 텍스트 (말줄임)
+
+
console.log('클릭')}
+ />
+
+
+
+
Disabled 상태
+
{}} disabled />
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '모든 상태를 한눈에 확인합니다.',
+ },
+ },
+ },
+} as Story;
+
+// ==============================
+// 실제 사용 예시 - 칸반 보드
+// ==============================
+
+export const KanbanBoardExample: Story = {
+ render: () => {
+ const [columns, setColumns] = useState({
+ todo: ['프로젝트 기획서 작성', 'UI 디자인 리뷰'],
+ inProgress: ['API 개발', '테스트 코드 작성'],
+ done: ['환경 설정', '레포지토리 생성', '팀 회의'],
+ });
+
+ const handleAddItem = (columnKey: keyof typeof columns) => {
+ const newItem = prompt('새 항목 이름을 입력하세요');
+ if (newItem) {
+ setColumns({
+ ...columns,
+ [columnKey]: [...columns[columnKey], newItem],
+ });
+ }
+ };
+
+ return (
+
+
+
+ 할 일 ({columns.todo.length})
+
+
handleAddItem('todo')} />
+
+ {columns.todo.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+ 진행중 ({columns.inProgress.length})
+
+
handleAddItem('inProgress')} />
+
+ {columns.inProgress.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+ 완료 ({columns.done.length})
+
+
handleAddItem('done')} />
+
+ {columns.done.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '칸반 보드에서 ProgressButton을 사용하는 실제 예시입니다.',
+ },
+ },
+ },
+} as Story;
+
+// ==============================
+// 실제 사용 예시 - 모바일 뷰
+// ==============================
+
+export const MobileViewExample: Story = {
+ render: () => {
+ const [items, setItems] = useState(['할 일 1', '할 일 2', '할 일 3']);
+
+ const handleAdd = () => {
+ const newItem = prompt('새 항목 이름을 입력하세요');
+ if (newItem) {
+ setItems([...items, newItem]);
+ }
+ };
+
+ return (
+
+
+
할 일 목록
+
+
+
+
+
+ {items.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: '모바일 화면에서 전체 너비로 사용하는 예시입니다.',
+ },
+ },
+ },
+} as Story;
+
+// ==============================
+// 상호작용 테스트
+// ==============================
+
+export const InteractionStates: Story = {
+ render: () => (
+
+
+ 버튼 위에 마우스를 올리거나 클릭해보세요:
+
+
console.log('클릭')} />
+
+ 기본: 회색 배경 + 플러스 박스 아이콘
+ Hover: 더 진한 회색 + 아이콘 opacity 0.8
+ Active: scale(0.98)
+ Focus: 2px 파란색 outline
+ width: 100% (부모 너비에 맞춤)
+ 긴 텍스트: 말줄임(...) 처리
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 hover, active, focus 상태를 테스트합니다.',
+ },
+ },
+ },
+} as Story;
+
+// ==============================
+// 접근성 (Accessibility) 테스트
+// ==============================
+
+export const AccessibilityTest: Story = {
+ render: () => (
+
+
+ Tab 키로 포커스를 이동하고 Enter/Space로 실행해보세요:
+
+
alert('할 일 추가')} />
+ alert('진행중 추가')} />
+ {}} disabled />
+
+ aria-label: "[라벨] 항목 추가"
+ 키보드 네비게이션: Tab으로 포커스 이동
+ 키보드 실행: Enter/Space로 클릭
+ disabled 버튼은 포커스를 받지 않음
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '키보드 네비게이션과 스크린 리더 지원을 테스트합니다.',
+ },
+ },
+ },
+} as Story;
+
+// ==============================
+// 크기 스펙
+// ==============================
+
+export const SizeReference: Story = {
+ render: () => (
+
+
{}} />
+
+ • width: 100% (부모 컨테이너에 맞춤)
+
+ • height: 38px
+
+ • padding: 0 8px 0 20px
+
+ • gap: 8px (텍스트와 아이콘 사이)
+
+ • border-radius: 12px
+
+ • 플러스 박스 아이콘: 24px × 24px
+
+ • font-size: 14px
+
+ • font-weight: 500
+ • 긴 텍스트: overflow hidden + ellipsis
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: '버튼의 크기와 스타일 스펙을 확인합니다.',
+ },
+ },
+ },
+} as Story;
diff --git a/src/components/Button/domain/ProgressButton/ProgressButton.tsx b/src/components/Button/domain/ProgressButton/ProgressButton.tsx
new file mode 100644
index 0000000..5c9c8f6
--- /dev/null
+++ b/src/components/Button/domain/ProgressButton/ProgressButton.tsx
@@ -0,0 +1,41 @@
+import styles from './ProgressButton.module.css';
+
+import plusBoxIcon from '@/assets/buttons/plus/plusBoxButton.svg';
+
+interface ProgressButtonProps {
+ label: string;
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+/**
+ * ProgressButton 컴포넌트
+ *
+ * @description
+ * 진행 영역(예: 컬럼/리스트 하단)에서 새로운 항목을 추가하기 위한 버튼이다.
+ * 텍스트 라벨과 플러스 아이콘을 함께 표시하며, 전체 너비를 차지하는 형태로 렌더링된다.
+ *
+ * @remarks
+ * - 버튼 텍스트는 길이가 길 경우 말줄임 처리된다.
+ * - 아이콘은 시각적 보조 요소로 사용되며, hover 시 투명도가 변경된다.
+ * - 접근성을 위해 `aria-label`에 라벨 기반 설명을 제공한다.
+ *
+ * @param props.label - 버튼에 표시될 텍스트
+ * @param props.onClick - 클릭 핸들러
+ * @param props.disabled - 비활성화 여부
+ * @returns 진행 영역용 추가 버튼
+ */
+export default function ProgressButton({ label, onClick, disabled = false }: ProgressButtonProps) {
+ return (
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/Button/shared/ButtonBehavior.module.css b/src/components/Button/shared/ButtonBehavior.module.css
new file mode 100644
index 0000000..eee145b
--- /dev/null
+++ b/src/components/Button/shared/ButtonBehavior.module.css
@@ -0,0 +1,29 @@
+/* shared/ButtonBehavior.module.css */
+.buttonBase {
+ /* layout: display는 각 컴포넌트가 결정 */
+ align-items: center;
+ justify-content: center;
+
+ /* reset */
+ border: none;
+ background: none;
+ padding: 0;
+ appearance: none;
+
+ font-family: var(--font-pretendard);
+ cursor: pointer;
+
+ transition:
+ background-color 0.15s ease,
+ color 0.15s ease,
+ border-color 0.15s ease;
+}
+
+.buttonBase:focus-visible {
+ outline: 2px solid var(--color-brand-primary);
+ outline-offset: 2px;
+}
+
+.buttonBase:disabled {
+ cursor: not-allowed;
+}
diff --git a/src/shared/styles/color.css b/src/shared/styles/color.css
index 8b38f8f..9d37af6 100644
--- a/src/shared/styles/color.css
+++ b/src/shared/styles/color.css
@@ -14,7 +14,7 @@
--color-point-yellow: #eab308;
/* Background */
- --color-background-primary: #0f172a;
+ --color-background-primary: #ffffff;
--color-background-secondary: #f1f5f9;
--color-background-tertiary: #e2e8f0;
--color-background-inverse: #ffffff;
@@ -37,6 +37,8 @@
/* Status */
--color-status-danger: #fc4b4b;
+ --color-status-danger-hover: #e53e3e;
+ --color-status-danger-pressed: #c53030;
/* Icon */
--color-icon-primary: #64748b;
diff --git a/src/stories/Configure.mdx b/src/stories/Configure.mdx
deleted file mode 100644
index 70fcc2a..0000000
--- a/src/stories/Configure.mdx
+++ /dev/null
@@ -1,446 +0,0 @@
-import { Meta } from "@storybook/addon-docs/blocks";
-import Image from "next/image";
-
-import Github from "./assets/github.svg";
-import Discord from "./assets/discord.svg";
-import Youtube from "./assets/youtube.svg";
-import Tutorials from "./assets/tutorials.svg";
-import Styling from "./assets/styling.png";
-import Context from "./assets/context.png";
-import Assets from "./assets/assets.png";
-import Docs from "./assets/docs.png";
-import Share from "./assets/share.png";
-import FigmaPlugin from "./assets/figma-plugin.png";
-import Testing from "./assets/testing.png";
-import Accessibility from "./assets/accessibility.png";
-import Theming from "./assets/theming.png";
-import AddonLibrary from "./assets/addon-library.png";
-
-export const RightArrow = () =>
-
-
-
-
-
-
-
- # Configure your project
-
- Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
-
-
-
-
-
Add styling and CSS
-
Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
-
Learn more
-
-
-
-
Provide context and mocking
-
Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.
-
Learn more
-
-
-
-
-
Load assets and resources
-
To link static files (like fonts) to your projects and stories, use the
- `staticDirs` configuration option to specify folders to load when
- starting Storybook.
-
Learn more
-
-
-
-
-
-
- # Do more with Storybook
-
- Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
-
-
-
-
-
-
-
Autodocs
-
Auto-generate living,
- interactive reference documentation from your components and stories.
-
Learn more
-
-
-
-
Publish to Chromatic
-
Publish your Storybook to review and collaborate with your entire team.
-
Learn more
-
-
-
-
Figma Plugin
-
Embed your stories into Figma to cross-reference the design and live
- implementation in one place.
-
Learn more
-
-
-
-
Testing
-
Use stories to test a component in all its variations, no matter how
- complex.
-
Learn more
-
-
-
-
Accessibility
-
Automatically test your components for a11y issues as you develop.
-
Learn more
-
-
-
-
Theming
-
Theme Storybook's UI to personalize it to your project.
-
Learn more
-
-
-
-
-
-
-
-
-
- Join our contributors building the future of UI development.
-
-
Star on GitHub
-
-
-
-
-
-
-