diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 41d76e1..7176775 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,3 +1,5 @@ +import '../src/app/globals.css'; + import type { Preview } from '@storybook/nextjs-vite'; const preview: Preview = { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..d0fb224 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,4 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +module.exports = { + plugins: [require('@tailwindcss/postcss')], +}; diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/src/shared/assets/icons/ArrowDown.tsx b/src/shared/assets/icons/ArrowDown.tsx new file mode 100644 index 0000000..dd02094 --- /dev/null +++ b/src/shared/assets/icons/ArrowDown.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function ArrowDown(props: HTMLAttributes) { + return ( +
+
+
+
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/shared/assets/icons/ArrowNext.tsx b/src/shared/assets/icons/ArrowNext.tsx new file mode 100644 index 0000000..135e047 --- /dev/null +++ b/src/shared/assets/icons/ArrowNext.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function ArrowNext(props: HTMLAttributes) { + return ( +
+
+
+
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/shared/assets/icons/ArrowPrev.tsx b/src/shared/assets/icons/ArrowPrev.tsx new file mode 100644 index 0000000..3e0c3e4 --- /dev/null +++ b/src/shared/assets/icons/ArrowPrev.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function ArrowPrev(props: HTMLAttributes) { + return ( +
+
+
+
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/shared/assets/icons/ArrowUp.tsx b/src/shared/assets/icons/ArrowUp.tsx new file mode 100644 index 0000000..624dafd --- /dev/null +++ b/src/shared/assets/icons/ArrowUp.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function ArrowUp(props: HTMLAttributes) { + return ( +
+
+
+
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcCircleCheckFilled.tsx b/src/shared/assets/icons/IcCircleCheckFilled.tsx new file mode 100644 index 0000000..5ca3e88 --- /dev/null +++ b/src/shared/assets/icons/IcCircleCheckFilled.tsx @@ -0,0 +1,38 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcCircleCheckFilled( + props: HTMLAttributes, +) { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcCircleCheckOutline.tsx b/src/shared/assets/icons/IcCircleCheckOutline.tsx new file mode 100644 index 0000000..5e17cbc --- /dev/null +++ b/src/shared/assets/icons/IcCircleCheckOutline.tsx @@ -0,0 +1,41 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcCircleCheckOutline( + props: HTMLAttributes, +) { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcCircleXFilled.tsx b/src/shared/assets/icons/IcCircleXFilled.tsx new file mode 100644 index 0000000..00cac0d --- /dev/null +++ b/src/shared/assets/icons/IcCircleXFilled.tsx @@ -0,0 +1,36 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcCircleXFilled(props: HTMLAttributes) { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcCircleXOutline.tsx b/src/shared/assets/icons/IcCircleXOutline.tsx new file mode 100644 index 0000000..c3989bc --- /dev/null +++ b/src/shared/assets/icons/IcCircleXOutline.tsx @@ -0,0 +1,34 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcCircleXOutline( + props: HTMLAttributes, +) { + return ( +
+
+ + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcHamburger.tsx b/src/shared/assets/icons/IcHamburger.tsx new file mode 100644 index 0000000..5e393ce --- /dev/null +++ b/src/shared/assets/icons/IcHamburger.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcHamburger(props: HTMLAttributes) { + return ( +
+
+
+ + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcInfoFilled.tsx b/src/shared/assets/icons/IcInfoFilled.tsx new file mode 100644 index 0000000..f1976cf --- /dev/null +++ b/src/shared/assets/icons/IcInfoFilled.tsx @@ -0,0 +1,36 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcInfoFilled(props: HTMLAttributes) { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcInfoOutline.tsx b/src/shared/assets/icons/IcInfoOutline.tsx new file mode 100644 index 0000000..cbe7c9c --- /dev/null +++ b/src/shared/assets/icons/IcInfoOutline.tsx @@ -0,0 +1,39 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcInfoOutline(props: HTMLAttributes) { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/shared/assets/icons/IcMagic.tsx b/src/shared/assets/icons/IcMagic.tsx new file mode 100644 index 0000000..76c0ef5 --- /dev/null +++ b/src/shared/assets/icons/IcMagic.tsx @@ -0,0 +1,43 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcMagic(props: HTMLAttributes) { + return ( +
+
+
+ + + + + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcMenuClose.tsx b/src/shared/assets/icons/IcMenuClose.tsx new file mode 100644 index 0000000..0ac99a0 --- /dev/null +++ b/src/shared/assets/icons/IcMenuClose.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcMenuClose(props: HTMLAttributes) { + return ( +
+
+
+ + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcOtherShare.tsx b/src/shared/assets/icons/IcOtherShare.tsx new file mode 100644 index 0000000..ec93c34 --- /dev/null +++ b/src/shared/assets/icons/IcOtherShare.tsx @@ -0,0 +1,37 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcOtherShare(props: HTMLAttributes) { + return ( +
+
+
+ + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcPeople.tsx b/src/shared/assets/icons/IcPeople.tsx new file mode 100644 index 0000000..e1b514a --- /dev/null +++ b/src/shared/assets/icons/IcPeople.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcPeople(props: HTMLAttributes) { + return ( +
+
+
+ + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/IcRefresh.tsx b/src/shared/assets/icons/IcRefresh.tsx new file mode 100644 index 0000000..a42ed2e --- /dev/null +++ b/src/shared/assets/icons/IcRefresh.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from 'react'; + +export default function IcRefresh(props: HTMLAttributes) { + return ( +
+
+
+ + + +
+
+
+ ); +} diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts new file mode 100644 index 0000000..80442ff --- /dev/null +++ b/src/shared/assets/icons/index.ts @@ -0,0 +1,37 @@ +import ArrowDown from './ArrowDown'; +import ArrowNext from './ArrowNext'; +import ArrowPrev from './ArrowPrev'; +import ArrowUp from './ArrowUp'; +import IcCircleCheckFilled from './IcCircleCheckFilled'; +import IcCircleCheckOutline from './IcCircleCheckOutline'; +import IcCircleXFilled from './IcCircleXFilled'; +import IcCircleXOutline from './IcCircleXOutline'; +import IcHamburger from './IcHamburger'; +import IcInfoFilled from './IcInfoFilled'; +import IcInfoOutline from './IcInfoOutline'; +import IcMagic from './IcMagic'; +import IcMenuClose from './IcMenuClose'; +import IcOtherShare from './IcOtherShare'; +import IcPeople from './IcPeople'; +import IcRefresh from './IcRefresh'; + +export const icons = { + arrow_down: ArrowDown, + arrow_next: ArrowNext, + arrow_prev: ArrowPrev, + arrow_up: ArrowUp, + ic_circle_check_filled: IcCircleCheckFilled, + ic_circle_check_outline: IcCircleCheckOutline, + ic_circle_x_filled: IcCircleXFilled, + ic_circle_x_outline: IcCircleXOutline, + ic_hamburger: IcHamburger, + ic_info_filled: IcInfoFilled, + ic_info_outline: IcInfoOutline, + ic_magic: IcMagic, + ic_menu_close: IcMenuClose, + ic_other_share: IcOtherShare, + ic_people: IcPeople, + ic_refresh: IcRefresh, +} as const; + +export type IconName = keyof typeof icons; diff --git a/src/shared/ui/icon/Icon.stories.tsx b/src/shared/ui/icon/Icon.stories.tsx new file mode 100644 index 0000000..ed0016d --- /dev/null +++ b/src/shared/ui/icon/Icon.stories.tsx @@ -0,0 +1,117 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import { type IconName, icons } from '@/shared/assets/icons'; + +import { ICON_SIZES } from './constants'; +import Icon from './Icon'; + +const meta: Meta = { + title: 'Shared/UI/Icon', + component: Icon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + name: { + control: 'select', + options: Object.keys(icons), + description: 'Icon name', + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg', 'xl', 48, 64], + description: 'Icon size token or number', + }, + color: { + control: 'color', + description: 'Inline style color', + }, + className: { + control: 'text', + description: 'Tailwind CSS classes', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// 1. Default Playground +export const Default: Story = { + args: { + name: 'ic_hamburger', + size: 'md', + }, +}; + +// 2. All Sizes +export const Sizes: Story = { + render: () => ( +
+ {(Object.keys(ICON_SIZES) as Array).map( + (size) => ( +
+ + + {size} ({ICON_SIZES[size]}px) + +
+ ), + )} +
+ + custom (48px) +
+
+ ), +}; + +// 3. Colors +export const Colors: Story = { + render: () => ( +
+
+ + Tailwind (text-blue-500) +
+
+ + + Prop (color="#FF5733") + +
+
+ + + Inherited (text-green-600) + +
+
+ ), +}; + +// 4. Icon Gallery +export const Gallery: Story = { + render: () => { + const iconNames = Object.keys(icons).sort(); + return ( +
+ {iconNames.map((name) => ( +
+ + + {name} + +
+ ))} +
+ ); + }, + parameters: { + layout: 'fullscreen', + }, +}; diff --git a/src/shared/ui/icon/Icon.tsx b/src/shared/ui/icon/Icon.tsx new file mode 100644 index 0000000..04ad310 --- /dev/null +++ b/src/shared/ui/icon/Icon.tsx @@ -0,0 +1,53 @@ +import { HTMLAttributes } from 'react'; + +import { IconName, icons } from '../../assets/icons'; +import { ICON_SIZES } from './constants'; +import { IconSize } from './types'; +interface IconProps extends HTMLAttributes { + name: IconName; + size?: IconSize | number; + className?: string; + color?: string; + ref?: React.Ref; +} + +export default function Icon({ + name, + size = 'md', + className, + color, + ref, + ...props +}: IconProps) { + const sizeValue = + typeof size === 'number' + ? size + : (ICON_SIZES[size as IconSize] ?? + (typeof size === 'string' && !isNaN(Number(size)) + ? Number(size) + : ICON_SIZES['md'])); + + const IconComponent = icons[name]; + + if (!IconComponent) { + console.warn(`Icon "${name}" not found`); + return null; + } + + return ( +
+ +
+ ); +} diff --git a/src/shared/ui/icon/constants.ts b/src/shared/ui/icon/constants.ts new file mode 100644 index 0000000..159c7c7 --- /dev/null +++ b/src/shared/ui/icon/constants.ts @@ -0,0 +1,6 @@ +export const ICON_SIZES = { + sm: 16, + md: 24, + lg: 32, + xl: 40, +} as const; diff --git a/src/shared/ui/icon/index.ts b/src/shared/ui/icon/index.ts new file mode 100644 index 0000000..ce4eb8f --- /dev/null +++ b/src/shared/ui/icon/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export * from './Icon'; +export * from './types'; diff --git a/src/shared/ui/icon/types.ts b/src/shared/ui/icon/types.ts new file mode 100644 index 0000000..909371b --- /dev/null +++ b/src/shared/ui/icon/types.ts @@ -0,0 +1,3 @@ +import { ICON_SIZES } from './constants'; + +export type IconSize = keyof typeof ICON_SIZES; diff --git a/src/shared/ui/top-bar/TopBar.stories.tsx b/src/shared/ui/top-bar/TopBar.stories.tsx new file mode 100644 index 0000000..7bcfc83 --- /dev/null +++ b/src/shared/ui/top-bar/TopBar.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import TopBar from './TopBar'; + +const meta: Meta = { + title: 'Shared/UI/TopBar', + component: TopBar, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + argTypes: { + title: { control: 'text' }, + leftIcon: { + control: 'select', + options: [ + undefined, + 'arrow_prev', + 'arrow_next', + 'ic_hamburger', + 'ic_menu_close', + 'ic_refresh', + ], + }, + rightIcon: { + control: 'select', + options: [ + undefined, + 'arrow_prev', + 'arrow_next', + 'ic_hamburger', + 'ic_menu_close', + 'ic_refresh', + ], + }, + iconColor: { control: 'color' }, + }, + args: { + onLeftClick: () => console.log('Left clicked'), + onRightClick: () => console.log('Right clicked'), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '페이지 제목', + }, +}; + +export const WithBack: Story = { + args: { + title: '페이지 제목', + leftIcon: 'arrow_prev', + }, +}; + +export const WithBackAndAction: Story = { + args: { + title: '페이지 제목', + leftIcon: 'arrow_prev', + rightIcon: 'ic_menu_close', + }, +}; + +export const OnlyIcons: Story = { + args: { + leftIcon: 'ic_hamburger', + rightIcon: 'ic_refresh', + }, +}; + +export const LongTitle: Story = { + args: { + leftIcon: 'arrow_prev', + title: + '엄청나게 긴 페이지 제목입니다 엄청나게 긴 페이지 제목입니다 확인해보세요', + rightIcon: 'ic_menu_close', + }, +}; diff --git a/src/shared/ui/top-bar/TopBar.tsx b/src/shared/ui/top-bar/TopBar.tsx new file mode 100644 index 0000000..a50e9bc --- /dev/null +++ b/src/shared/ui/top-bar/TopBar.tsx @@ -0,0 +1,71 @@ +import { HTMLAttributes } from 'react'; + +import { IconName } from '../../assets/icons'; +import Icon from '../icon/Icon'; + +interface TopBarProps extends HTMLAttributes { + title?: string; + leftIcon?: IconName; + onLeftClick?: () => void; + rightIcon?: IconName; + onRightClick?: () => void; + className?: string; + /** + * 좌우 아이콘의 색상을 지정합니다. + * 현재는 일관된 스타일 유지를 위해 동일한 색상을 적용하지만, 추후 확장이 필요하면 개별 로직으로 분리할 수 있습니다. + */ + iconColor?: string; +} + +export default function TopBar({ + title, + leftIcon, + onLeftClick, + rightIcon, + onRightClick, + className, + iconColor, + ...props +}: TopBarProps) { + return ( +
+ {/* 좌측 아이콘 영역 - 터치 타겟 확보를 위해 고정 크기 사용 */} +
+ {leftIcon && ( + + )} +
+ + {/* 제목 영역 - 완벽한 중앙 정렬을 위해 absolute 포지셔닝 사용 */} +

+ {title} +

+ + {/* 우측 아이콘 영역 - 터치 타겟 확보를 위해 고정 크기 사용 */} +
+ {rightIcon && ( + + )} +
+
+ ); +} diff --git a/src/shared/ui/top-bar/index.ts b/src/shared/ui/top-bar/index.ts new file mode 100644 index 0000000..34e1aea --- /dev/null +++ b/src/shared/ui/top-bar/index.ts @@ -0,0 +1 @@ +export { default as TopBar } from './TopBar';