diff --git a/.github/workflows/fe-ci.yml b/.github/workflows/fe-ci.yml new file mode 100644 index 00000000..72da9bf8 --- /dev/null +++ b/.github/workflows/fe-ci.yml @@ -0,0 +1,121 @@ +name: FRONTEND-CI + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - dev + - main + paths: + - 'src/frontend/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + changes: + if: ${{ github.actor != 'l10nbot' }} # Avoid running for 'l10nbot' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: pnpm/action-setup@v3 + with: + version: 8 + + - uses: actions/cache@v4 + id: cache + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: | + cd src/frontend + pnpm install --strict-peer-dependencies=false --frozen-lockfile + pnpm add turbo --save-dev -w + + type-check: + needs: [changes] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Typescript Check + run: | + cd src/frontend + pnpm install + pnpm typescript + env: + NODE_OPTIONS: --max-old-space-size=4096 + + lint: + needs: [changes] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Prettier check + run: | + cd src/frontend + pnpm install + pnpm format --check + + - name: Lint + run: | + cd src/frontend + pnpm lint + + build: + needs: [changes, lint, type-check] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Build project + run: | + cd src/frontend + pnpm install + pnpm build + env: + NODE_OPTIONS: --max_old_space_size=4096 + + - name: Build Storybook + run: | + cd src/frontend + pnpm install + pnpm build-storybook + + required: + needs: [changes, lint, type-check, build] + if: always() + runs-on: ubuntu-latest + steps: + - name: fail if conditional jobs failed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/src/frontend/.eslintignore b/src/frontend/.eslintignore new file mode 100644 index 00000000..6b697bc8 --- /dev/null +++ b/src/frontend/.eslintignore @@ -0,0 +1,47 @@ +# Node modules 디렉토리 제외 +node_modules/ + +# 빌드 결과물 제외 +dist/ +build/ + +# 환경설정 파일 제외 +package.json +turbo.json +.eslintrc.json + +# 로그 파일 +npm-debug.log + +# 프로젝트 루트에서 제외된 다른 파일들 +public/ + +# 기타 설정 파일 제외 (필요시) +*.config.js +*.config.cjs +*.config.mjs + +# GitHub 워크플로우 및 VSCode 설정 파일 제외 +.github/ +.vscode/ +.DS_Store +.idea/ + +# Jest 관련 파일 제외 (필요시) +jest.config.js +jest.setup.js + +# TurboRepo 캐시 제외 +.turbo/ + +# ESLint 설정 파일 제외 +.eslintrc.json +.eslintignore + +# 압축된 자바스크립트 및 CSS 파일 제외 +*.min.js +*.min.css + +# 포맷팅을 제외할 파일들 +.prettierrc +storybook-static diff --git a/src/frontend/.eslintrc.js b/src/frontend/.eslintrc.js index 781274d0..6d5c64ff 100644 --- a/src/frontend/.eslintrc.js +++ b/src/frontend/.eslintrc.js @@ -1,10 +1,9 @@ // This configuration only applies to the package manager root. /** @type {import("eslint").Linter.Config} */ module.exports = { - ignorePatterns: ["apps/**", "packages/**"], - extends: ["@workspace/eslint-config/library.js"], - parser: "@typescript-eslint/parser", + ignorePatterns: ['apps/**', 'packages/**'], + parser: '@typescript-eslint/parser', parserOptions: { project: true, }, -} +}; diff --git a/src/frontend/.gitignore b/src/frontend/.gitignore index e4248147..f3550986 100644 --- a/src/frontend/.gitignore +++ b/src/frontend/.gitignore @@ -36,3 +36,4 @@ npm-debug.log* *.pem *storybook.log +*storybook-static \ No newline at end of file diff --git a/src/frontend/.prettierignore b/src/frontend/.prettierignore new file mode 100644 index 00000000..2c5a20ff --- /dev/null +++ b/src/frontend/.prettierignore @@ -0,0 +1,49 @@ +# Node modules 디렉토리 제외 +node_modules/ + +# 빌드 결과물 제외 +dist/ +build/ + +# 환경설정 파일 제외 +package.json +turbo.json +.eslintrc.json + +# 로그 파일 +npm-debug.log + +# 프로젝트 루트에서 제외된 다른 파일들 +public/ + +# 기타 설정 파일 제외 (필요시) +*.config.js +*.config.cjs +*.config.mjs + +# GitHub 워크플로우 및 VSCode 설정 파일 제외 +.github/ +.vscode/ +.DS_Store +.idea/ + +# Jest 관련 파일 제외 (필요시) +jest.config.js +jest.setup.js + +# TurboRepo 캐시 제외 +.turbo/ + +# ESLint 설정 파일 제외 +.eslintrc.json +.eslintignore + +# 압축된 자바스크립트 및 CSS 파일 제외 +*.min.js +*.min.css + +# 포맷팅을 제외할 파일들 +.prettierrc +storybook-static + +README.md \ No newline at end of file diff --git a/src/frontend/.prettierrc b/src/frontend/.prettierrc new file mode 100644 index 00000000..f69ca308 --- /dev/null +++ b/src/frontend/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 180, + "tabWidth": 2, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "singleAttributePerLine": true +} diff --git a/src/frontend/README.md b/src/frontend/README.md index b907dd6a..6f8df71a 100644 --- a/src/frontend/README.md +++ b/src/frontend/README.md @@ -27,5 +27,5 @@ Your `tailwind.config.ts` and `globals.css` are already set up to use the compon To use the components in your app, import them from the `ui` package. ```tsx -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from '@workspace/ui/components/ui/button'; ``` diff --git a/src/frontend/apps/web/app/layout.tsx b/src/frontend/apps/web/app/layout.tsx index 42b1cbb0..7375d14e 100644 --- a/src/frontend/apps/web/app/layout.tsx +++ b/src/frontend/apps/web/app/layout.tsx @@ -18,10 +18,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + {children} diff --git a/src/frontend/apps/web/src/shared/components/react-query/RQProvider.tsx b/src/frontend/apps/web/src/shared/components/react-query/RQProvider.tsx index 2e7f5722..ad5544b6 100644 --- a/src/frontend/apps/web/src/shared/components/react-query/RQProvider.tsx +++ b/src/frontend/apps/web/src/shared/components/react-query/RQProvider.tsx @@ -20,15 +20,13 @@ function RQProvider({ children }: Props) { retry: false, }, }, - }) + }), ); return ( {children} - + ); } diff --git a/src/frontend/apps/web/tailwind.config.ts b/src/frontend/apps/web/tailwind.config.ts index f119228a..4e1acc9c 100644 --- a/src/frontend/apps/web/tailwind.config.ts +++ b/src/frontend/apps/web/tailwind.config.ts @@ -1 +1 @@ -export * from "@workspace/ui/tailwind.config"; \ No newline at end of file +export * from '@workspace/ui/tailwind.config'; diff --git a/src/frontend/dangerfile.ts b/src/frontend/dangerfile.ts new file mode 100644 index 00000000..ce9345b0 --- /dev/null +++ b/src/frontend/dangerfile.ts @@ -0,0 +1,19 @@ +import { danger, fail, warn, message } from 'danger'; + +// PR에서 변경된 파일 목록 가져오기 +const changedFiles = danger.git.modified_files; + +// 예시: 변경된 파일에 대해 경고 추가 +if (changedFiles.some((file) => file.includes('src/frontend'))) { + warn('Frontend files were modified. Please ensure that UI tests are run.'); +} + +// 예시: 특정 파일이 포함된 경우 실패를 표시 +if (changedFiles.some((file) => file.includes('README.md'))) { + fail('The README.md file was modified. Please check for outdated documentation.'); +} + +// 예시: 특정 파일 변경을 알림으로 표시 +if (changedFiles.length === 0) { + message('No files were modified in this PR.'); +} diff --git a/src/frontend/package.json b/src/frontend/package.json index 6cd35cd2..2de03536 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -8,11 +8,14 @@ "lint": "turbo lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "storybook": "pnpm --filter @workspace/ui run storybook", - "build-storybook": "pnpm --filter @workspace/ui run build-storybook" + "build-storybook": "pnpm --filter @workspace/ui run build-storybook", + "danger": "danger ci --config tsconfig.danger.json", + "typescript": "tsc --noEmit" }, "devDependencies": { "@workspace/eslint-config": "workspace:*", "@workspace/typescript-config": "workspace:*", + "danger": "^12.3.3", "prettier": "^3.4.2", "turbo": "^2.3.3", "typescript": "5.7.3" diff --git a/src/frontend/packages/eslint-config/next.js b/src/frontend/packages/eslint-config/next.js index e40fffcd..3b737014 100644 --- a/src/frontend/packages/eslint-config/next.js +++ b/src/frontend/packages/eslint-config/next.js @@ -1,12 +1,12 @@ -import js from "@eslint/js" -import pluginNext from "@next/eslint-plugin-next" -import eslintConfigPrettier from "eslint-config-prettier" -import pluginReact from "eslint-plugin-react" -import pluginReactHooks from "eslint-plugin-react-hooks" -import globals from "globals" -import tseslint from "typescript-eslint" +import js from '@eslint/js'; +import pluginNext from '@next/eslint-plugin-next'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import pluginReact from 'eslint-plugin-react'; +import pluginReactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; -import { config as baseConfig } from "./base.js" +import { config as baseConfig } from './base.js'; /** * A custom ESLint configuration for libraries that use Next.js. @@ -29,23 +29,25 @@ export const nextJsConfig = [ }, { plugins: { - "@next/next": pluginNext, + '@next/next': pluginNext, }, rules: { ...pluginNext.configs.recommended.rules, - ...pluginNext.configs["core-web-vitals"].rules, + ...pluginNext.configs['core-web-vitals'].rules, }, }, { plugins: { - "react-hooks": pluginReactHooks, + 'react-hooks': pluginReactHooks, }, - settings: { react: { version: "detect" } }, + settings: { react: { version: 'detect' } }, rules: { ...pluginReactHooks.configs.recommended.rules, // React scope no longer necessary with new JSX transform. - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', }, }, -] +]; diff --git a/src/frontend/packages/eslint-config/package.json b/src/frontend/packages/eslint-config/package.json index 118db4a9..b7a1fb63 100644 --- a/src/frontend/packages/eslint-config/package.json +++ b/src/frontend/packages/eslint-config/package.json @@ -12,7 +12,7 @@ "@next/eslint-plugin-next": "^15.1.0", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", - "eslint": "^9.15.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-react": "^7.37.2", diff --git a/src/frontend/packages/typescript-config/base.json b/src/frontend/packages/typescript-config/base.json index 3f441c66..b1549eb0 100644 --- a/src/frontend/packages/typescript-config/base.json +++ b/src/frontend/packages/typescript-config/base.json @@ -16,5 +16,6 @@ "skipLibCheck": true, "strict": false, "target": "ES2022" - } + }, + "include": ["../../dangerfile.ts"] } diff --git a/src/frontend/packages/ui/.storybook/main.ts b/src/frontend/packages/ui/.storybook/main.ts index 714d8f0a..b4e374e3 100644 --- a/src/frontend/packages/ui/.storybook/main.ts +++ b/src/frontend/packages/ui/.storybook/main.ts @@ -1,24 +1,24 @@ -import type { StorybookConfig } from "@storybook/react-vite"; +import type { StorybookConfig } from '@storybook/react-vite'; -import { join, dirname } from "path"; +import { join, dirname } from 'path'; /** * This function is used to resolve the absolute path of a package. * It is needed in projects that use Yarn PnP or are set up within a monorepo. */ function getAbsolutePath(value: string): any { - return dirname(require.resolve(join(value, "package.json"))); + return dirname(require.resolve(join(value, 'package.json'))); } const config: StorybookConfig = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - getAbsolutePath("@storybook/addon-onboarding"), - getAbsolutePath("@storybook/addon-essentials"), - getAbsolutePath("@chromatic-com/storybook"), - getAbsolutePath("@storybook/addon-interactions"), + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@chromatic-com/storybook'), + getAbsolutePath('@storybook/addon-interactions'), ], framework: { - name: getAbsolutePath("@storybook/react-vite"), + name: getAbsolutePath('@storybook/react-vite'), options: {}, }, }; diff --git a/src/frontend/packages/ui/package.json b/src/frontend/packages/ui/package.json index 4e91aff8..cd081dbe 100644 --- a/src/frontend/packages/ui/package.json +++ b/src/frontend/packages/ui/package.json @@ -4,7 +4,7 @@ "type": "module", "private": false, "scripts": { - "lint": "eslint . --max-warnings 0", + "lint": "eslint . --max-warnings 50", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, diff --git a/src/frontend/packages/ui/src/components/Avatar/avatar.tsx b/src/frontend/packages/ui/src/components/Avatar/avatar.tsx index eae16f03..c0006117 100644 --- a/src/frontend/packages/ui/src/components/Avatar/avatar.tsx +++ b/src/frontend/packages/ui/src/components/Avatar/avatar.tsx @@ -17,14 +17,9 @@ const avatarVariants = cva('relative flex h-10 w-10 shrink-0 overflow-hidden', { variant: 'default', }, }); -interface AvatarProps - extends React.ComponentPropsWithoutRef, - VariantProps {} +interface AvatarProps extends React.ComponentPropsWithoutRef, VariantProps {} -const Avatar = React.forwardRef< - React.ElementRef, - AvatarProps ->(({ className, variant, ...props }, ref) => ( +const Avatar = React.forwardRef, AvatarProps>(({ className, variant, ...props }, ref) => ( , - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +const AvatarImage = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +); AvatarImage.displayName = AvatarPrimitive.Image.displayName; -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +const AvatarFallback = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/frontend/packages/ui/src/components/Badge/badge.tsx b/src/frontend/packages/ui/src/components/Badge/badge.tsx index fac31475..7a222809 100644 --- a/src/frontend/packages/ui/src/components/Badge/badge.tsx +++ b/src/frontend/packages/ui/src/components/Badge/badge.tsx @@ -21,12 +21,10 @@ const badgeVariants = cva( variant: 'default', size: 'sm', }, - } + }, ); -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, size, ...props }: BadgeProps) { return ( diff --git a/src/frontend/packages/ui/src/components/Button/button.stories.tsx b/src/frontend/packages/ui/src/components/Button/button.stories.tsx index 9b2e3952..0af88810 100644 --- a/src/frontend/packages/ui/src/components/Button/button.stories.tsx +++ b/src/frontend/packages/ui/src/components/Button/button.stories.tsx @@ -14,14 +14,7 @@ const meta: Meta = { children: { control: 'text', defaultValue: 'Button' }, variant: { control: 'select', - options: [ - 'primary', - 'destructive', - 'outline', - 'secondary', - 'ghost', - 'kakao', - ], + options: ['primary', 'destructive', 'outline', 'secondary', 'ghost', 'kakao'], }, size: { control: 'radio', options: ['sm', 'lg', 'icon'] }, asChild: { control: 'boolean' }, @@ -76,11 +69,17 @@ export const Kakao: Story = { export const Icon: Story = { render: () => (
- -
diff --git a/src/frontend/packages/ui/src/components/Button/button.tsx b/src/frontend/packages/ui/src/components/Button/button.tsx index e82a53ce..b42a4eeb 100644 --- a/src/frontend/packages/ui/src/components/Button/button.tsx +++ b/src/frontend/packages/ui/src/components/Button/button.tsx @@ -9,14 +9,10 @@ const buttonVariants = cva( { variants: { variant: { - default: - 'bg-primary text-primary-foreground shadow hover:bg-primary/90', - destructive: - 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', - outline: - 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', - secondary: - 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', kakao: 'bg-kakao text-black font-bold', }, @@ -31,27 +27,23 @@ const buttonVariants = cva( variant: 'default', size: 'default', }, - } + }, ); -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - return ( - - ); - } -); +const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); +}); Button.displayName = 'Button'; export { Button, buttonVariants }; diff --git a/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.stories.tsx b/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.stories.tsx index 431f5e19..53ecff41 100644 --- a/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.stories.tsx +++ b/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.stories.tsx @@ -26,7 +26,10 @@ export const Default: Story = { }, render: () => (
- +
), }; diff --git a/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.tsx b/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.tsx index 993e62ea..f31b97ed 100644 --- a/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.tsx +++ b/src/frontend/packages/ui/src/components/ChatTextarea/chat-textarea.tsx @@ -7,17 +7,20 @@ import { cn } from '@workspace/ui/lib/utils'; const ChatTextarea = ({ onSend, onAdd }) => { return ( -
+