diff --git a/src/assets/icons/state/stateEmptyLarge.svg b/src/assets/icons/state/stateEmptyLarge.svg new file mode 100644 index 0000000..c96c95e --- /dev/null +++ b/src/assets/icons/state/stateEmptyLarge.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/state/stateEmptySmall.svg b/src/assets/icons/state/stateEmptySmall.svg new file mode 100644 index 0000000..250e4bb --- /dev/null +++ b/src/assets/icons/state/stateEmptySmall.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/badge/Badge.stories.tsx b/src/components/badge/Badge.stories.tsx new file mode 100644 index 0000000..3be9eda --- /dev/null +++ b/src/components/badge/Badge.stories.tsx @@ -0,0 +1,78 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import Badge from './Badge'; + +const meta = { + title: 'Components/Badge', + component: Badge, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + args: { + state: 'done', + label: '5/5', + }, + argTypes: { + state: { + control: 'inline-radio', + options: ['done', 'ongoing', 'empty'], + }, + size: { + control: 'inline-radio', + options: ['small', 'large'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Done: Story = { + args: { + state: 'done', + label: '5/5', + }, +}; + +export const Ongoing: Story = { + args: { + state: 'ongoing', + label: '3/5', + }, +}; + +export const Empty: Story = { + args: { + state: 'empty', + label: '0/5', + }, +}; + +export const Large: Story = { + args: { + state: 'done', + size: 'large', + label: '5/5', + }, +}; + +export const Overview: Story = { + render: () => ( +
+
+ + + +
+
+ + + +
+
+ ), + parameters: { + controls: { disable: true }, + }, +}; diff --git a/src/components/badge/Badge.tsx b/src/components/badge/Badge.tsx new file mode 100644 index 0000000..d228073 --- /dev/null +++ b/src/components/badge/Badge.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx'; +import Image from 'next/image'; + +import styles from './styles/Badge.module.css'; +import { BADGE_STATE_LABEL, BADGE_STYLE } from './constants/badgeConstants'; +import type { BadgeProps } from './types/types'; + +/** + * 상태 표시 뱃지 컴포넌트. + * state에 따라 색상과 아이콘이 자동 결정되며 (done=초록, ongoing=파랑, empty=회색), + * 스크린리더용 aria-label도 자동으로 "완료: 라벨", "진행 중: 라벨" 형태로 생성됩니다. + */ +export default function Badge({ state, size = 'small', label }: BadgeProps) { + const iconSrc = BADGE_STYLE.icons[state][size]; + const iconSize = BADGE_STYLE.iconSize[size]; + + return ( + + + + + ); +} diff --git a/src/components/badge/constants/badgeConstants.ts b/src/components/badge/constants/badgeConstants.ts new file mode 100644 index 0000000..46c8373 --- /dev/null +++ b/src/components/badge/constants/badgeConstants.ts @@ -0,0 +1,23 @@ +import type { BadgeState } from '../types/types'; + +import stateDoneLarge from '@/assets/icons/state/stateDoneLarge.svg'; +import stateDoneSmall from '@/assets/icons/state/stateDoneSmall.svg'; +import stateOngoingLarge from '@/assets/icons/state/stateOngoingLarge.svg'; +import stateOngoingSmall from '@/assets/icons/state/stateOngoingSmall.svg'; +import stateEmptyLarge from '@/assets/icons/state/stateEmptyLarge.svg'; +import stateEmptySmall from '@/assets/icons/state/stateEmptySmall.svg'; + +export const BADGE_STATE_LABEL: Record = { + done: '완료', + ongoing: '진행 중', + empty: '시작 전', +} as const; + +export const BADGE_STYLE = { + icons: { + done: { large: stateDoneLarge, small: stateDoneSmall }, + ongoing: { large: stateOngoingLarge, small: stateOngoingSmall }, + empty: { large: stateEmptyLarge, small: stateEmptySmall }, + }, + iconSize: { large: 20, small: 16 }, +} as const; diff --git a/src/components/badge/index.ts b/src/components/badge/index.ts new file mode 100644 index 0000000..bcd3195 --- /dev/null +++ b/src/components/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from './Badge'; +export type { BadgeProps, BadgeState, BadgeSize } from './types/types'; diff --git a/src/components/badge/styles/Badge.module.css b/src/components/badge/styles/Badge.module.css new file mode 100644 index 0000000..aef54e0 --- /dev/null +++ b/src/components/badge/styles/Badge.module.css @@ -0,0 +1,34 @@ +.badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + background-color: var(--color-background-inverse); +} + +.large { + padding: 4px 10px 4px 6px; + gap: 4px; + font-size: 14px; + font-weight: 600; +} + +.small { + padding: 2px 8px 2px 4px; + gap: 3px; + font-size: 12px; + font-weight: 600; +} + +.done, +.ongoing { + color: var(--color-icon-brand); +} + +.empty { + color: var(--color-text-disabled); +} + +.icon { + display: block; + flex-shrink: 0; +} diff --git a/src/components/badge/types/types.ts b/src/components/badge/types/types.ts new file mode 100644 index 0000000..9e69a10 --- /dev/null +++ b/src/components/badge/types/types.ts @@ -0,0 +1,14 @@ +/** 뱃지 상태. `done`(완료) | `ongoing`(진행 중) | `empty`(시작 전) */ +export type BadgeState = 'done' | 'ongoing' | 'empty'; + +/** 뱃지 크기. `large`(아이콘 20px) | `small`(아이콘 16px) */ +export type BadgeSize = 'large' | 'small'; + +export type BadgeProps = { + /** 뱃지 상태 (색상·아이콘이 자동 결정됨) */ + state: BadgeState; + /** 뱃지 크기 (기본값: `'small'`) */ + size?: BadgeSize; + /** 뱃지에 표시할 텍스트 (예: "3개" , "마감 완료") */ + label: string; +};