diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
new file mode 100644
index 0000000..2773522
--- /dev/null
+++ b/.github/workflows/chromatic.yml
@@ -0,0 +1,61 @@
+name: 'Chromatic Deployment'
+
+run-name: Storybook deployment by ${{ github.actor }}
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ paths:
+ - '**/*.stories.tsx'
+ - '**/*.stories.ts'
+ - '**/*.stories.jsx'
+ - '**/*.stories.js'
+ - 'src/components/**'
+
+jobs:
+ chromatic:
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-build') }}
+ name: Run Chromatic Deployment
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ outputs:
+ status: ${{ job.status }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ run_install: false
+
+ - name: Set up Node.js version
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+ cache: 'pnpm'
+ cache-dependency-path: '**/pnpm-lock.yaml'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run Chromatic
+ id: chromatic
+ uses: chromaui/action@latest
+ with:
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ buildScriptName: build-storybook
+ onlyChanged: true
+
+ - name: Comment PR
+ uses: thollander/actions-comment-pull-request@v1
+ if: ${{ github.event_name == 'pull_request' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ message: '스토리북 URL 확인하기 - ${{ steps.chromatic.outputs.storybookUrl }}'
diff --git a/.storybook/main.ts b/.storybook/main.ts
index f8109ce..f7e58ae 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -2,17 +2,35 @@ import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
- addons: [
- '@chromatic-com/storybook',
- '@storybook/addon-docs',
- '@storybook/addon-onboarding',
- '@storybook/addon-a11y',
- '@storybook/addon-vitest',
- ],
+ addons: ['@storybook/addon-onboarding', '@chromatic-com/storybook', '@storybook/addon-a11y'],
framework: {
name: '@storybook/nextjs',
options: {},
},
- staticDirs: ['../public'],
+ webpackFinal: async (config) => {
+ if (!config.module || !config.module.rules) {
+ return config;
+ }
+
+ config.module.rules = [
+ ...config.module.rules.map((rule) => {
+ if (!rule || rule === '...') {
+ return rule;
+ }
+
+ if (rule.test && /svg/.test(String(rule.test))) {
+ return { ...rule, exclude: /\.svg$/i };
+ }
+
+ return rule;
+ }),
+ {
+ test: /\.svg$/,
+ use: ['@svgr/webpack'],
+ },
+ ];
+
+ return config;
+ },
};
export default config;
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index a9e11e2..858e3d8 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/nextjs';
+import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
@@ -8,13 +9,6 @@ const preview: Preview = {
date: /Date$/i,
},
},
-
- a11y: {
- // 'todo' - show a11y violations in the test UI only
- // 'error' - fail CI on a11y violations
- // 'off' - skip a11y checks entirely
- test: 'todo',
- },
},
};
diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts
deleted file mode 100644
index e8c7ed3..0000000
--- a/.storybook/vitest.setup.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
-import { setProjectAnnotations } from '@storybook/nextjs-vite';
-import * as projectAnnotations from './preview';
-
-// This is an important step to apply the right configuration when testing your stories.
-// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
-setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f5458bf..30ae42d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,6 @@
"files.eol": "\n",
"files.insertFinalNewline": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
- "javascript.updateImportsOnFileMove.enabled": "always"
+ "javascript.updateImportsOnFileMove.enabled": "always",
+ "css.lint.unknownAtRules": "ignore"
}
diff --git a/next.config.ts b/next.config.ts
index c9157e8..5399318 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -8,25 +8,14 @@ const nextConfig: NextConfig = {
typeof rule.test === 'object' && rule.test instanceof RegExp && rule.test?.test?.('.svg')
);
- if (!fileLoaderRule) {
- throw new Error('File loader rule not found');
- }
-
- config.module.rules.push(
- {
- ...fileLoaderRule,
- test: /\.svg$/i,
- resourceQuery: /url/,
- },
- {
+ if (fileLoaderRule) {
+ config.module.rules.push({
test: /\.svg$/i,
- issuer: fileLoaderRule.issuer,
- resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: ['@svgr/webpack'],
- }
- );
+ });
- fileLoaderRule.exclude = /\.svg$/i;
+ fileLoaderRule.exclude = /\.svg$/i;
+ }
return config;
},
diff --git a/package.json b/package.json
index 2de735a..6dafc23 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"prettier": "prettier --write .",
"prepare": "husky",
"storybook": "storybook dev -p 6006",
- "build-storybook": "storybook build"
+ "build-storybook": "storybook build",
+ "chromatic": "npx chromatic"
},
"lint-staged": {
"*.{js,ts,tsx,jsx}": [
@@ -43,6 +44,7 @@
"@types/webpack": "^5.28.5",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
+ "chromatic": "^13.3.3",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"eslint-config-prettier": "^10.1.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 78bcd76..9793af1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -75,6 +75,9 @@ importers:
'@vitest/coverage-v8':
specifier: ^3.2.4
version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)
+ chromatic:
+ specifier: ^13.3.3
+ version: 13.3.3
eslint:
specifier: ^9
version: 9.37.0(jiti@2.6.1)
@@ -2416,6 +2419,18 @@ packages:
'@chromatic-com/playwright':
optional: true
+ chromatic@13.3.3:
+ resolution: {integrity: sha512-89w0hiFzIRqLbwGSkqSQzhbpuqaWpXYZuevSIF+570Wb+T/apeAkp3px8nMJcFw+zEdqw/i6soofkJtfirET1Q==}
+ hasBin: true
+ peerDependencies:
+ '@chromatic-com/cypress': ^0.*.* || ^1.0.0
+ '@chromatic-com/playwright': ^0.*.* || ^1.0.0
+ peerDependenciesMeta:
+ '@chromatic-com/cypress':
+ optional: true
+ '@chromatic-com/playwright':
+ optional: true
+
chrome-trace-event@1.0.4:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
@@ -7777,6 +7792,8 @@ snapshots:
chromatic@12.2.0: {}
+ chromatic@13.3.3: {}
+
chrome-trace-event@1.0.4: {}
cipher-base@1.0.7:
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index c517ba7..9ec184e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,10 +1,9 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
-import './globals.css';
+import '@/styles/globals.css';
const pretendard = localFont({
src: [{ path: '../assets/fonts/PretendardVariable.woff2', style: 'normal', weight: '100 900' }],
- variable: '--font-family-sans',
display: 'swap',
fallback: ['system-ui', 'Arial', 'sans-serif'],
});
@@ -14,13 +13,9 @@ export const metadata: Metadata = {
description: '함께 모으는 우리만의 기록, 공유 가계부 MOA',
};
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
+export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
diff --git a/src/components/Button/constants.ts b/src/components/Button/constants.ts
new file mode 100644
index 0000000..46d45a3
--- /dev/null
+++ b/src/components/Button/constants.ts
@@ -0,0 +1,36 @@
+export const BUTTON_BASE_STYLES =
+ 'inline-flex items-center justify-center font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
+
+export const BUTTON_BORDER_RADIUS = {
+ xs: 'rounded-[var(--rounded-sm)]',
+ sm: 'rounded-[var(--rounded-sm)]',
+ md: 'rounded-[var(--rounded-md)]',
+ lg: 'rounded-[var(--rounded-md)]',
+ xl: 'rounded-[var(--rounded-md)]',
+} as const;
+
+export const BUTTON_VARIANTS = {
+ primary:
+ 'bg-[var(--color-green-normal)] text-white hover:bg-[var(--color-green-normal-hover)] active:bg-[var(--color-green-normal-active)]',
+ back: 'bg-[var(--color-green-dark)] text-white hover:bg-[var(--color-green-dark-hover)] active:bg-[var(--color-green-dark-active)]',
+ cancel:
+ 'bg-[var(--color-grey-light)] text-white hover:bg-[var(--color-grey-light-hover)] active:bg-[var(--color-grey-light-active)]',
+ outline:
+ 'bg-[var(--color-green-light)] !text-[var(--color-green-normal)] border border-[var(--color-green-normal)] hover:bg-[var(--color-green-light-hover)] active:bg-[var(--color-green-light-active)]',
+ text: 'text-[var(--color-green-darker)] hover:text-[var(--color-green-darker-hover)] active:text-[var(--color-green-darker-active)] font-medium',
+ underline:
+ 'text-[var(--color-grey-normal)] hover:text-[var(--color-grey-normal-hover)] active:text-[var(--color-grey-normal-active)] font-medium underline decoration-[var(--color-grey-normal)] underline-offset-[4px]',
+} as const;
+
+export const BUTTON_TEXT_SIZES = {
+ text: 'text-xs-custom',
+ underline: 'text-sm-custom',
+} as const;
+
+export const BUTTON_SIZES = {
+ xs: 'h-8 w-[100px] max-w-full text-base-custom',
+ sm: 'h-8 w-[200px] max-w-full text-base-custom',
+ md: 'h-10 w-[72px] max-w-full text-lg-custom',
+ lg: 'h-10 w-[228px] max-w-full text-lg-custom',
+ xl: 'h-10 w-[300px] max-w-full text-lg-custom',
+} as const;
diff --git a/src/components/Button/index.stories.tsx b/src/components/Button/index.stories.tsx
new file mode 100644
index 0000000..191e00d
--- /dev/null
+++ b/src/components/Button/index.stories.tsx
@@ -0,0 +1,105 @@
+import type { Meta, StoryObj } from '@storybook/nextjs';
+import { Button } from './index';
+
+const meta = {
+ title: 'Components/Button',
+ component: Button,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta
;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ children: '입력 완료',
+ variant: 'primary',
+ },
+};
+
+export const Back: Story = {
+ args: {
+ children: '이전',
+ variant: 'back',
+ },
+};
+
+export const Cancel: Story = {
+ args: {
+ children: '취소',
+ variant: 'cancel',
+ },
+};
+
+export const Outline: Story = {
+ args: {
+ children: '새 가계부 만들기',
+ variant: 'outline',
+ },
+};
+
+export const Text: Story = {
+ args: {
+ children: '건너뛰기',
+ variant: 'text',
+ },
+};
+
+export const Underline: Story = {
+ args: {
+ children: '건너뛰기',
+ variant: 'underline',
+ },
+};
+
+export const SizeXS: Story = {
+ args: {
+ children: 'XS',
+ size: 'xs',
+ },
+};
+
+export const SizeSM: Story = {
+ args: {
+ children: '다음',
+ size: 'sm',
+ },
+};
+
+export const SizeMD: Story = {
+ args: {
+ children: '확인',
+ size: 'md',
+ },
+};
+
+export const SizeLG: Story = {
+ args: {
+ children: '확인',
+ size: 'lg',
+ },
+};
+
+export const SizeXL: Story = {
+ args: {
+ children: '입력 완료',
+ size: 'xl',
+ },
+};
+
+export const Loading: Story = {
+ args: {
+ children: '입력 완료',
+ isLoading: true,
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ children: '입력 완료',
+ disabled: true,
+ },
+};
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
new file mode 100644
index 0000000..6ed07d5
--- /dev/null
+++ b/src/components/Button/index.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import { FC, ButtonHTMLAttributes, ReactNode } from 'react';
+import {
+ BUTTON_BASE_STYLES,
+ BUTTON_BORDER_RADIUS,
+ BUTTON_SIZES,
+ BUTTON_TEXT_SIZES,
+ BUTTON_VARIANTS,
+} from './constants';
+
+export type ButtonVariant = 'primary' | 'back' | 'cancel' | 'outline' | 'text' | 'underline';
+export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+export interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ isLoading?: boolean;
+ children: ReactNode;
+}
+
+const getSizeClasses = (variant: ButtonVariant, size: ButtonSize): string => {
+ const isTextVariant = variant === 'text' || variant === 'underline';
+ return isTextVariant
+ ? BUTTON_TEXT_SIZES[variant]
+ : `${BUTTON_SIZES[size]} ${BUTTON_BORDER_RADIUS[size]}`;
+};
+
+const getButtonClasses = (
+ variant: ButtonVariant,
+ size: ButtonSize,
+ additionalClasses: string
+): string => {
+ const sizeClasses = getSizeClasses(variant, size);
+ return `${BUTTON_BASE_STYLES} ${BUTTON_VARIANTS[variant]} ${sizeClasses} ${additionalClasses}`;
+};
+
+export const Button: FC = ({
+ variant = 'primary',
+ size = 'md',
+ isLoading = false,
+ children,
+ className = '',
+ disabled,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/src/app/globals.css b/src/styles/globals.css
similarity index 51%
rename from src/app/globals.css
rename to src/styles/globals.css
index 693bfb2..cc86aca 100644
--- a/src/app/globals.css
+++ b/src/styles/globals.css
@@ -1,18 +1,18 @@
@import 'tailwindcss';
-:root {
+@theme {
--background: #ffffff;
--foreground: #000000;
--font-family-sans: 'Pretendard', sans-serif;
--font-family-mono: 'Menlo', monospace;
- --font-size-xs: 8px;
- --font-size-sm: 10px;
- --font-size-base: 12px;
- --font-size-lg: 14px;
- --font-size-xl: 16px;
- --font-size-xxl: 24px;
+ --text-size-xs: 0.5rem; /* 8px */
+ --text-size-sm: 0.625rem; /* 10px */
+ --text-size-base: 0.75rem; /* 12px */
+ --text-size-lg: 0.875rem; /* 14px */
+ --text-size-xl: 1rem; /* 16px */
+ --text-size-xxl: 1.5rem; /* 24px */
--font-weight-regular: 400;
--font-weight-medium: 500;
@@ -35,16 +35,18 @@
--color-green-dark-active: #004c41;
--color-green-darker: #003b33;
- --color-grey-light: #f3f3f3;
- --color-grey-light-hover: #ededed;
- --color-grey-light-active: #d9d9d9;
+ --color-grey-light: #c8c8c8;
+ --color-grey-light-hover: #bdbdbd;
+ --color-grey-light-active: #afafaf;
--color-grey-normal: #848484;
- --color-grey-normal-hover: #777777;
- --color-grey-normal-active: #6a6a6a;
- --color-grey-dark: #636363;
- --color-grey-dark-hover: #4f4f4f;
- --color-grey-dark-active: #3b3b3b;
- --color-grey-darker: #2e2e2e;
+ --color-grey-normal-hover: #707070;
+ --color-grey-normal-active: #606060;
+ --color-grey-dark: #2e2e2e;
+ --color-grey-dark-hover: #212121;
+ --color-grey-dark-active: #0f0f0f;
+
+ --color-grey-lighter: #fdfdfd;
+ --color-grey-lighter-hover: #f2f2f2;
--color-red: #ed4242;
@@ -52,13 +54,38 @@
--layout-width: 440px;
- --radius-sm: 4px;
- --radius-md: 8px;
+ --rounded-sm: 4px;
+ --rounded-md: 8px;
--color-background: var(--background);
--color-foreground: var(--foreground);
}
+/* Custom text size utilities */
+.text-xs-custom {
+ font-size: var(--text-size-xs);
+}
+
+.text-sm-custom {
+ font-size: var(--text-size-sm);
+}
+
+.text-base-custom {
+ font-size: var(--text-size-base);
+}
+
+.text-lg-custom {
+ font-size: var(--text-size-lg);
+}
+
+.text-xl-custom {
+ font-size: var(--text-size-xl);
+}
+
+.text-xxl-custom {
+ font-size: var(--text-size-xxl);
+}
+
body {
background: var(--background);
color: var(--foreground);
diff --git a/svgr.d.ts b/svgr.d.ts
index bd3438f..100cfb5 100644
--- a/svgr.d.ts
+++ b/svgr.d.ts
@@ -3,8 +3,3 @@ declare module '*.svg' {
const content: FC>;
export default content;
}
-
-declare module '*.svg?url' {
- const content: string;
- export default content;
-}
diff --git a/vitest.config.ts b/vitest.config.ts
deleted file mode 100644
index e4644ff..0000000
--- a/vitest.config.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-import { defineConfig } from 'vitest/config';
-
-import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
-
-const dirname =
- typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
-// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
-export default defineConfig({
- test: {
- projects: [
- {
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({ configDir: path.join(dirname, '.storybook') }),
- ],
- test: {
- name: 'storybook',
- browser: {
- enabled: true,
- headless: true,
- provider: 'playwright',
- instances: [{ browser: 'chromium' }],
- },
- setupFiles: ['.storybook/vitest.setup.ts'],
- },
- },
- ],
- },
-});
diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts
deleted file mode 100644
index a1d31e5..0000000
--- a/vitest.shims.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///