diff --git a/.gitignore b/.gitignore index 307c7a7..3d19163 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /api-app/app/src/main/resources/application-local.yml +api-app/.gradle +api-app/build \ No newline at end of file diff --git a/web-app/.storybook/main.ts b/web-app/.storybook/main.ts new file mode 100644 index 0000000..dcaa523 --- /dev/null +++ b/web-app/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from '@storybook/react-vite' + +const config: StorybookConfig = { + stories: ['../app/**/*.stories.@(ts|tsx)'], + + framework: '@storybook/react-vite', + + // Storybook 전용 Vite 설정 파일 사용 (React Router 플러그인 제외) + core: { + builder: { + name: '@storybook/builder-vite', + options: { + viteConfigPath: '.storybook/vite.config.ts', + }, + }, + }, +} + +export default config diff --git a/web-app/.storybook/preview.ts b/web-app/.storybook/preview.ts new file mode 100644 index 0000000..099f3e4 --- /dev/null +++ b/web-app/.storybook/preview.ts @@ -0,0 +1,16 @@ +import type { Preview } from '@storybook/react' + +import '../app/app.css' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +} + +export default preview diff --git a/web-app/.storybook/vite.config.ts b/web-app/.storybook/vite.config.ts new file mode 100644 index 0000000..50503a5 --- /dev/null +++ b/web-app/.storybook/vite.config.ts @@ -0,0 +1,12 @@ +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'vite' + +// Storybook 전용 Vite 설정 (React Router 플러그인 제외) +export default defineConfig({ + plugins: [tailwindcss()], + resolve: { + alias: { + '~': new URL('../app', import.meta.url).pathname, + }, + }, +}) diff --git a/web-app/app/app.css b/web-app/app/app.css index 55af49e..25ff1d6 100644 --- a/web-app/app/app.css +++ b/web-app/app/app.css @@ -54,6 +54,13 @@ --color-dabeeo-navy-tertiary01: #d4dde9; --color-dabeeo-navy-tertiary02: #f0f3f7; + /* Green */ + --color-dabeeo-green-main: #1b8466; + --color-dabeeo-green-secondary: #125a46; + --color-dabeeo-green-tertiary: #89bea7; + --color-dabeeo-green-tertiary01: #d1e6e1; + --color-dabeeo-green-tertiary02: #ebf2f0; + /* Orange */ --color-dabeeo-orange-main: #ff7937; --color-dabeeo-orange-secondary: #c14b11; diff --git a/web-app/app/shared/components/button/Button.stories.tsx b/web-app/app/shared/components/button/Button.stories.tsx new file mode 100644 index 0000000..ce0fcec --- /dev/null +++ b/web-app/app/shared/components/button/Button.stories.tsx @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Button } from './Button' + +const meta = { + title: 'Components/Button', + component: Button, + argTypes: { + color: { + control: 'select', + options: ['primary', 'light', 'green', 'lightGreen', 'black', 'gray', 'orange', 'navy', 'lightNavy'], + }, + size: { + control: 'select', + options: ['small', 'medium', 'large'], + }, + isDisabled: { + control: 'boolean', + }, + isPending: { + control: 'boolean', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children: '버튼', + color: 'primary', + size: 'medium', + }, +} + +export const Small: Story = { + args: { + children: '작은 버튼', + size: 'small', + }, +} + +export const Medium: Story = { + args: { + children: '중간 버튼', + size: 'medium', + }, +} + +export const Large: Story = { + args: { + children: '큰 버튼', + size: 'large', + }, +} + +export const Primary: Story = { + args: { + children: 'Primary', + color: 'primary', + }, +} + +export const Light: Story = { + args: { + children: 'Light', + color: 'light', + }, +} + +export const Green: Story = { + args: { + children: 'Green', + color: 'green', + }, +} + +export const Navy: Story = { + args: { + children: 'Navy', + color: 'navy', + }, +} + +export const Disabled: Story = { + args: { + children: '비활성화', + isDisabled: true, + }, +} + +export const Loading: Story = { + args: { + children: '로딩 중', + isPending: true, + }, +} + +export const AllColors: Story = { + render: () => ( +
+ + + + + + + + + +
+ ), +} diff --git a/web-app/app/shared/components/inputGroup/Input.tsx b/web-app/app/shared/components/inputGroup/Input.tsx index 785b164..6fe53b8 100644 --- a/web-app/app/shared/components/inputGroup/Input.tsx +++ b/web-app/app/shared/components/inputGroup/Input.tsx @@ -3,7 +3,6 @@ import type { InputProps as AriaInputProps } from 'react-aria-components'; import { Input as AriaInput } from 'react-aria-components'; import { twMerge } from 'tailwind-merge'; - import style from './style'; export interface InputProps extends AriaInputProps { diff --git a/web-app/app/shared/components/inputGroup/InputGroup.stories.tsx b/web-app/app/shared/components/inputGroup/InputGroup.stories.tsx new file mode 100644 index 0000000..e55cfea --- /dev/null +++ b/web-app/app/shared/components/inputGroup/InputGroup.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/react' +import Input from './Input' +import { InputGroup } from './InputGroup' + +const meta = { + title: 'Components/InputGroup', + component: InputGroup, + argTypes: { + isReadOnly: { control: 'boolean' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ( + + + + ), +} + +export const WithValue: Story = { + render: (args) => ( + + + + ), +} + +export const Disabled: Story = { + render: () => ( + + + + ), +} + +export const ReadOnly: Story = { + args: { + isReadOnly: true, + }, + render: (args) => ( + + + + ), +} + +export const NumberInput: Story = { + render: () => ( + + + + ), +} + +export const WithPrefix: Story = { + render: () => ( + + @ + + + ), +} + +export const WithSuffix: Story = { + render: () => ( + + + + + ), +} + +export const CustomWidth: Story = { + render: () => ( + + + + ), +} diff --git a/web-app/app/shared/components/inputGroup/style.ts b/web-app/app/shared/components/inputGroup/style.ts new file mode 100644 index 0000000..52ba032 --- /dev/null +++ b/web-app/app/shared/components/inputGroup/style.ts @@ -0,0 +1,35 @@ +import { tv } from 'tailwind-variants'; + +export default tv({ + slots: { + container: 'relative flex flex-col justify-center items-start bg-white', + base: [ + 'h-9', + 'flex', + 'items-center', + 'text-sm', + 'text-dabeeo-black-34', + 'leading-[18px]', + 'border', + 'border-dabeeo-gray-be', + 'has-data-focused:border-dabeeo-black-34', + 'has-data-hovered:border-dabeeo-black-34', + 'has-data-disabled:bg-dabeeo-gray-eb', + 'has-data-disabled:text-dabeeo-gray-99', + 'has-data-disabled:border-dabeeo-gray-be', + 'has-data-invalid:border-dabeeo-red', + 'has-data-invalid:has-data-focused:border-dabeeo-red', + 'has-data-invalid:has-data-hovered:border-dabeeo-red', + 'w-full', + 'has-[[type=number]]:leading-6', + ], + input: ['w-full', 'px-3', 'outline-0', '[&[type=number]]:pr-1.5'], + }, + variants: { + isReadOnly: { + true: { + base: 'bg-dabeeo-yellow-secondary has-data-hovered:border-dabeeo-gray-be has-data-focused:border-dabeeo-gray-be', + }, + }, + }, +}); diff --git a/web-app/app/shared/components/modal/Modal.stories.tsx b/web-app/app/shared/components/modal/Modal.stories.tsx new file mode 100644 index 0000000..268662d --- /dev/null +++ b/web-app/app/shared/components/modal/Modal.stories.tsx @@ -0,0 +1,142 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { Button } from '../button/Button' +import { ModalBody, ModalFooter, ModalHeader, ModalRoot } from './Modal' + +const meta = { + title: 'Components/Modal', + component: ModalRoot, + argTypes: { + isDismissable: { control: 'boolean' }, + isKeyboardDismissDisabled: { control: 'boolean' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false) + return ( + <> + + + 모달 제목 + +

모달 내용입니다.

+
+ + + + +
+ + ) + }, +} + +export const WithoutCloseButton: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false) + return ( + <> + + + 닫기 버튼 없음 + +

헤더에 닫기 버튼이 없습니다.

+
+ + + +
+ + ) + }, +} + +export const LongContent: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false) + return ( + <> + + + 긴 내용 + + {Array.from({ length: 20 }, (_, i) => ( +

+ 내용 {i + 1}: Lorem ipsum dolor sit amet consectetur adipisicing elit. +

+ ))} +
+ + + +
+ + ) + }, +} + +export const NotDismissable: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false) + return ( + <> + + + 닫을 수 없음 + +

배경 클릭이나 ESC로 닫을 수 없습니다.

+
+ + + +
+ + ) + }, +} + +export const ConfirmDialog: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false) + return ( + <> + + + +

+ 정말 삭제하시겠습니까? +

+
+ + + + +
+ + ) + }, +} diff --git a/web-app/app/shared/components/pagination/Pagination.stories.tsx b/web-app/app/shared/components/pagination/Pagination.stories.tsx new file mode 100644 index 0000000..4b7e7d4 --- /dev/null +++ b/web-app/app/shared/components/pagination/Pagination.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { Pagination } from './Pagination' + +const meta = { + title: 'Components/Pagination', + component: Pagination, + argTypes: { + totalPages: { control: 'number' }, + currentPage: { control: 'number' }, + pageCount: { control: 'number' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + totalPages: 100, + currentPage: 0, + pageCount: 10, + onPageChange: () => {}, + }, +} + +export const MiddlePage: Story = { + args: { + totalPages: 100, + currentPage: 45, + pageCount: 10, + onPageChange: () => {}, + }, +} + +export const LastPage: Story = { + args: { + totalPages: 100, + currentPage: 99, + pageCount: 10, + onPageChange: () => {}, + }, +} + +export const FewPages: Story = { + args: { + totalPages: 5, + currentPage: 0, + pageCount: 10, + onPageChange: () => {}, + }, +} + +export const SinglePage: Story = { + args: { + totalPages: 1, + currentPage: 0, + pageCount: 10, + onPageChange: () => {}, + }, +} + +export const Interactive: Story = { + render: () => { + const [currentPage, setCurrentPage] = useState(0) + return ( +
+ +

+ 현재 페이지: {currentPage + 1} / 50 +

+
+ ) + }, +} + +export const CustomPageCount: Story = { + args: { + totalPages: 100, + currentPage: 0, + pageCount: 5, + onPageChange: () => {}, + }, +} diff --git a/web-app/app/shared/components/select/Select.stories.tsx b/web-app/app/shared/components/select/Select.stories.tsx new file mode 100644 index 0000000..6c6dea7 --- /dev/null +++ b/web-app/app/shared/components/select/Select.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { Select, type SelectOption } from './Select' + +const meta = { + title: 'Components/Select', + component: Select, + argTypes: { + isDisabled: { control: 'boolean' }, + isReadOnly: { control: 'boolean' }, + placement: { + control: 'select', + options: ['bottom', 'top'], + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const sampleItems: SelectOption[] = [ + { label: '옵션 1', value: '1' }, + { label: '옵션 2', value: '2' }, + { label: '옵션 3', value: '3' }, + { label: '비활성화 옵션', value: '4', isDisabled: true }, +] + +export const Default: Story = { + args: { + items: sampleItems, + placeholder: '선택해주세요', + }, +} + +export const WithSelectedValue: Story = { + args: { + items: sampleItems, + defaultSelectedKey: '2', + }, +} + +export const Disabled: Story = { + args: { + items: sampleItems, + defaultSelectedKey: '1', + isDisabled: true, + }, +} + +export const ReadOnly: Story = { + args: { + items: sampleItems, + defaultSelectedKey: '1', + isReadOnly: true, + }, +} + +export const PlacementTop: Story = { + args: { + items: sampleItems, + placeholder: '위로 열림', + placement: 'top', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export const Controlled: Story = { + render: function Render() { + const [value, setValue] = useState('1') + return ( +
+