diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..ab17bac --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,90 @@ +name: PR Check + +permissions: + contents: read + pull-requests: write + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + id: install + run: npm ci + + - name: Lint check + id: lint + continue-on-error: true + run: npm run lint:fix + + - name: Format check + id: format + continue-on-error: true + run: npm run format + + - name: Type check + id: typecheck + continue-on-error: true + run: npx tsc --noEmit + + - name: Run tests + id: test + continue-on-error: true + run: npm run test + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Build check + id: build + continue-on-error: true + run: npm run build + + - name: Report Status + if: always() + uses: actions/github-script@v6 + with: + script: | + const steps = { + lint: '${{ steps.lint.outcome }}', + format: '${{ steps.format.outcome }}', + typecheck: '${{ steps.typecheck.outcome }}', + test: '${{ steps.test.outcome }}', + build: '${{ steps.build.outcome }}' + }; + + const emoji = (status) => status === 'success' ? '✅' : '❌'; + + const body = `## CI Status Report\n\n` + + `### 검사 결과\n` + + `- Lint: ${emoji(steps.lint)} ${steps.lint}\n` + + `- Format: ${emoji(steps.format)} ${steps.format}\n` + + `- Type Check: ${emoji(steps.typecheck)} ${steps.typecheck}\n` + + `- Tests: ${emoji(steps.test)} ${steps.test}\n` + + `- Build: ${emoji(steps.build)} ${steps.build}\n\n` + + `${Object.values(steps).every(s => s === 'success') ? '✅ 모든 검사가 통과되었습니다.' : '❌ 일부 검사가 실패했습니다.'}`; + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: body + }); + + if (Object.values(steps).some(s => s === 'failure')) { + core.setFailed('Some checks failed'); + } diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..fa9f857 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,73 @@ +name: PR Labeler + +permissions: + contents: read + pull-requests: write + +on: + pull_request: + types: [opened, edited] + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Label PR based on commit messages + uses: actions/github-script@v6 + with: + script: | + try { + const { data: commits } = await github.rest.pulls.listCommits({ + ...context.repo, + pull_number: context.issue.number + }); + + const labels = new Set(); + commits.forEach(commit => { + const msg = commit.commit.message; + // Using regex to match your commit convention: type(issue) message + const typeMatch = msg.match(/^(feat|fix|style|refactor|test|docs|chore|setting)\(/i); + + if (typeMatch) { + const type = typeMatch[1].toLowerCase(); + switch (type) { + case 'feat': + labels.add('✨ feat'); + break; + case 'fix': + labels.add('🐛 fix'); + break; + case 'style': + labels.add('💄 style'); + break; + case 'refactor': + labels.add('♻️ refactor'); + break; + case 'test': + labels.add('✅ test'); + break; + case 'docs': + labels.add('📝 docs'); + break; + case 'chore': + labels.add('🔧 chore'); + break; + case 'setting': + labels.add('⚙️ setting'); + break; + } + } + }); + + if (labels.size > 0) { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: context.issue.number, + labels: Array.from(labels) + }); + } + } catch (error) { + core.setFailed(`Action failed with error: ${error}`); + } diff --git a/.prettierrc b/.prettierrc index 6a82526..b8a2ec2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,7 +7,10 @@ "bracketSpacing": true, "arrowParens": "always", "endOfLine": "lf", - "plugins": ["prettier-plugin-tailwindcss"], + "plugins": [ + "prettier-plugin-tailwindcss", + "@trivago/prettier-plugin-sort-imports" + ], "importOrder": [ "^@utils/(.*)$", "^@apis/(.*)$", diff --git a/__tests__/page.test.tsx b/__tests__/page.test.tsx deleted file mode 100644 index 4ffb821..0000000 --- a/__tests__/page.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import Home from '@/app/page'; - -describe('Page', () => { - it('renders a heading', () => { - render(); - - const heading = screen.getByRole('heading'); - - expect(heading).toBeInTheDocument(); - }); -}); diff --git a/eslint.config.mjs b/eslint.config.mjs index 0287374..aa5bc63 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,9 +16,17 @@ const compat = new FlatCompat({ recommendedConfig: {}, }); +/** @type {import('eslint').Linter.Config[]} */ const eslintConfig = [ { - ignores: ['node_modules/', 'dist/', 'public/'], + ignores: [ + 'node_modules/', + 'dist/', + 'public/', + '.next/', + '**/*.js', + 'next-env.d.ts', + ], }, ...compat.extends( 'eslint:recommended', @@ -36,9 +44,7 @@ const eslintConfig = [ parserOptions: { ecmaVersion: 2023, sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, + ecmaFeatures: { jsx: true }, project: './tsconfig.json', }, }, @@ -57,33 +63,22 @@ const eslintConfig = [ ], '@typescript-eslint/no-unused-vars': [ 'warn', - { - args: 'after-used', - varsIgnorePattern: '^_', - }, + { args: 'after-used', varsIgnorePattern: '^_' }, ], 'jsx-a11y/label-has-associated-control': [ 'error', - { - required: { some: ['nesting', 'id'] }, - }, + { required: { some: ['nesting', 'id'] } }, ], 'no-console': 'warn', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'react/jsx-no-useless-fragment': 'warn', }, - settings: { - react: { - version: 'detect', - }, - }, + settings: { react: { version: 'detect' } }, }, { files: ['*.tsx', '*.jsx'], - rules: { - '@typescript-eslint/no-use-before-define': 'off', - }, + rules: { '@typescript-eslint/no-use-before-define': 'off' }, }, ]; diff --git a/jest.config.ts b/jest.config.ts index 230058c..7e136fd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,4 @@ // jest.config.ts - import type { Config } from 'jest'; import nextJest from 'next/jest.js'; diff --git a/package-lock.json b/package-lock.json index 53d9a64..7c90008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^18", @@ -40,7 +41,7 @@ "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.1", "ts-node": "^10.9.2", - "typescript": "^5" + "typescript": "~5.5.0" } }, "node_modules/@adobe/css-tools": { @@ -1566,6 +1567,40 @@ "node": ">= 10" } }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -5713,6 +5748,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -9802,9 +9843,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index dccab4e..264e2ab 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "lint:fix": "eslint --fix \"**/*.{js,jsx,ts,tsx}\"", + "lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx}\"", "format": "prettier --write \"**/*.{js,jsx,ts,tsx}\"", "prepare": "husky install", "test": "jest" @@ -23,6 +23,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^18", @@ -45,7 +46,7 @@ "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.1", "ts-node": "^10.9.2", - "typescript": "^5" + "typescript": "~5.5.0" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/postcss.config.js b/postcss.config.js index 12a703d..cce4985 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f3d4978..634143e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; + import './globals.css'; export const metadata: Metadata = { diff --git a/tailwind.config.ts b/tailwind.config.ts index 109807b..5b60d94 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,16 +1,16 @@ -import type { Config } from "tailwindcss"; +import type { Config } from 'tailwindcss'; export default { content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { - background: "var(--background)", - foreground: "var(--foreground)", + background: 'var(--background)', + foreground: 'var(--foreground)', }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 048b3fc..f14a137 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,9 +19,9 @@ "name": "next" } ], - "baseUrl": "src", + "baseUrl": "./src", "paths": { - "@/*": ["src/*"], + "@/*": ["/*"], "@/app/*": ["app/*"], "@/components/*": ["components/*"], "@/hooks/*": ["hooks/*"],