diff --git a/.github/actions/setup-node-dependencies/action.yml b/.github/actions/setup-node-dependencies/action.yml new file mode 100644 index 000000000..4b82457c0 --- /dev/null +++ b/.github/actions/setup-node-dependencies/action.yml @@ -0,0 +1,32 @@ +name: Setup Node.js and Dependencies +description: Sets up Node.js, pnpm, and installs dependencies + +runs: + using: "composite" + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Get pnpm cache directory path + run: echo "dir=$(pnpm store path)" >> $GITHUB_ENV + shell: bash + + - name: Cache pnpm dependencies + id: pnpm-cache + uses: actions/cache@v4 + with: + path: ${{ env.dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --prefer-frozen-lockfile + shell: bash diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 728f0f113..7029f3036 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,84 +1,11 @@ ## 체크포인트 -### 코드 작성 체크포인트 +### PR 올리기 전에 확인사항 -- 가독성 - - [ ] 변수와 함수의 이름이 용도와 기능을 명확히 나타내는가? - - [ ] 중복되는 로직을 별도의 함수로 추출했는가? - - [ ] 중첩된 if 문을 피하고 조건문을 단순화했는가? - - [ ] 매직 넘버와 반복되는 문자열을 상수로 정의했는가? - - [ ] 데이터 처리에 reduce, filter, map 등의 고차 함수를 사용했는가? - - [ ] 일관된 코딩 스타일, 적절한 들여쓰기, 논리적인 함수 배치로 가독성을 향상시켰는가? -- 단일 책임 원칙 - - [ ] 함수/훅이 하나의 명확한 목적만을 가지고 있는가? - - [ ] 함수/훅의 이름이 그 목적을 명확하게 나타내는가? -- 순수 함수 - - [ ] 동일한 입력에 대해 항상 동일한 출력을 반환하는가? - - [ ] 외부 상태를 변경하지 않는가? - - [ ] 입력 데이터를 직접 수정하지 않고 새로운 데이터를 반환하는가? -- 합성 가능성 - - [ ] 함수/훅을 다른 함수/훅과 쉽게 조합할 수 있는가? -- 관심사 분리 - - [ ] 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가? -- DRY (Don't Repeat Yourself) 원칙 - - [ ] 이 함수/훅이 중복 코드를 제거하는 데 도움이 되는가? -- 테스트 용이성 - - [ ] 함수/훅에 대한 단위 테스트를 쉽게 작성할 수 있는가? - (input에 대한 output을 정의하기 수월한가) +1. head barnch와 sync를 맞춰주세요. -### advanced 과제 체크포인트 +```bash +$ git pull https://github.com/hanghae-plus/front_2nd.git main +``` -- [ ] advanced.test.ts에 있는 시나리오 테스트를 모두 통과한다. -- [ ] 3개 이상의 custom hook을 통해서 리팩토링을 진행했고, 해당 hook에 대한 테스트 코드가 작성되었다. -- [ ] 3개 이상의 순수함수를 통해서 리팩토링을 진행했고, 해당 함수에 대한 테스트 코드가 작성되었다. - - -## 고민했던 부분과 새롭게 알게 된 점 - - -### 고민했던 부분 -- -- - -### 새롭게 알게 된 점 -- -- - -## 해결하지 못한 문제 - -- -- - -## 리뷰 요청 사항 - -- -- - -## 자기 평가 - - -### 성장 포인트 - -1. -2. -3. - -### 개선하고 싶은 부분 -- -- - -## 인사이트 - -현재 작성된 코드가 요구사항에 잘 대응할 수 있는지 자체적으로 점검해보세요. **내가 작성한 코드가 좋은 코드인지 판단해볼 수 있습니다.** - -1. start:advanced로 실행하면 초기 데이터를 localStorage에서 불러오고, 추가하고, 수정하고, 삭제할 수 있다. -2. start:api-mock로 실행하면 초기 데이터를 API를 통해서 불러오고, 추가하고, 수정하고, 삭제할 수 있다. - - API는 [msw](https://mswjs.io/)로 만든다. -3. 회원 등급별 할인 기능 - - 관리자는 회원 등급을 추가할 수 있다. - - 회원 등급에 따른 할인율이 존재한다. - - 예시) 일반: 3%, 실버: 5%, 골드: 7%, VIP: 10%, - - 장바구니 페이지에서 현재 회원 등급을 임의로 선택할 수 있다. - - 회원 등급에 따라 총 구매 가격에 할인율이 적용되어야 한다. - -이 외에도 다양한 요구사항이 추가될 수 있습니다. +2. base branch를 `hanghae-plus:main`이 아니라 `hanghae-plus:<본안아이디>` 로 수정해주세요 확인해주세요. diff --git a/.github/scripts/lighthouse.js b/.github/scripts/lighthouse.js new file mode 100644 index 000000000..2d726f340 --- /dev/null +++ b/.github/scripts/lighthouse.js @@ -0,0 +1,83 @@ +const formatScore = (score) => Math.round(score * 100); +const getScoreEmoji = (score) => + score >= 90 ? "🟢" : score >= 50 ? "🟠" : "🔴"; + +const createMarkdownTable = (headers, rows) => { + return [ + `| ${headers.join(" | ")} |`, + `| ${headers.map(() => "---").join(" | ")} |`, + ...rows.map((row) => `| ${row.join(" | ")} |`), + ].join("\n"); +}; + +const createLighthouseComment = (summary, audits) => { + const formattedSummary = Object.fromEntries( + Object.entries(summary).map(([key, value]) => [key, formatScore(value)]) + ); + + const summaryTable = createMarkdownTable( + ["Category", "Score"], + Object.entries(formattedSummary).map(([key, value]) => [ + `${getScoreEmoji(value)} ${key.charAt(0).toUpperCase() + key.slice(1)}`, + value, + ]) + ); + + const detailRows = [ + "first-contentful-paint", + "largest-contentful-paint", + "total-blocking-time", + "cumulative-layout-shift", + "speed-index", + ].map((metric) => { + const score = formatScore(audits[metric].score * 100); + return [ + `${getScoreEmoji(score)} ${metric + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ")}`, + audits[metric].displayValue, + ]; + }); + + const detailTable = createMarkdownTable(["Category", "Score"], detailRows); + + return ` +⚡️ Lighthouse report! +${summaryTable} + +📊 Lighthouse Detail +${detailTable} + `.trim(); +}; + +const reportLighthouseSuccess = async ({ github, context, results, fs }) => { + const comments = results + .map((result) => { + const { summary, jsonPath } = result; + const { audits } = JSON.parse(fs.readFileSync(jsonPath)); + return createLighthouseComment(summary, audits); + }) + .join("\n\n"); + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comments, + }); +}; + +const reportLighthouseFailure = async ({ github, context }) => { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "빌드가 실패하여 Lighthouse 결과를 측정할 수 없습니다.", + }); +}; + +module.exports = { + reportLighthouseSuccess, + reportLighthouseFailure, +}; diff --git a/.github/scripts/lint.js b/.github/scripts/lint.js new file mode 100644 index 000000000..4089d40d3 --- /dev/null +++ b/.github/scripts/lint.js @@ -0,0 +1,26 @@ +const reportLintSuccess = async ({ context, github }) => { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `Lint 성공 🎉`, + }); +}; + +const reportLintFailure = async ({ eslintReport, context, github }) => { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: ` +## ESLint Results + +${eslintReport} +`, + }); +}; + +module.exports = { + reportLintSuccess, + reportLintFailure, +}; diff --git a/.github/scripts/review.js b/.github/scripts/review.js new file mode 100644 index 000000000..71697c576 --- /dev/null +++ b/.github/scripts/review.js @@ -0,0 +1,49 @@ +const getRandomReviewer = () => { + const reviewers = ["yoosion030", "dhp94d", "lydiahjchung", "callipenguin"]; + const randomReviewer = + reviewers[Math.floor(Math.random() * reviewers.length)]; + + return randomReviewer; +}; + +const commentReviewer = async ({ github, context, reviewer }) => { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `@${reviewer}, 리뷰를 부탁드립니다.`, + }); +}; + +const assignReviewer = async ({ github, context, reviewer }) => { + const currentReviewers = await github.rest.pulls.listRequestedReviewers({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + if (currentReviewers.data.users.length > 0) { + const reviewersToRemove = currentReviewers.data.users.map( + (user) => user.login + ); + await github.rest.pulls.removeRequestedReviewers({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + reviewers: reviewersToRemove, + }); + } + + await github.rest.pulls.requestReviewers({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + reviewers: [reviewer], + }); +}; + +module.exports = { + commentReviewer, + assignReviewer, + getRandomReviewer, +}; diff --git a/.github/scripts/test-coverage.js b/.github/scripts/test-coverage.js new file mode 100644 index 000000000..41f163758 --- /dev/null +++ b/.github/scripts/test-coverage.js @@ -0,0 +1,37 @@ +const reportTestSuccess = async ({ totalCoverage, github, context }) => { + const coverageComment = ` +## 테스트 성공 ✅ +### 커버리지 결과 + +- 라인 커버리지: ${totalCoverage.lines.pct}% +- 브랜치 커버리지: ${totalCoverage.branches.pct}% +- 함수 커버리지: ${totalCoverage.functions.pct}% +- 구문 커버리지: ${totalCoverage.statements.pct}% +`; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: coverageComment, + }); +}; + +const reportTestFailure = async ({ github, context }) => { + const coverageComment = ` +## 테스트 실패 ❌ + +테스트 코드를 확인해주세요. +`; + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: coverageComment, + }); +}; + +module.exports = { + reportTestSuccess, + reportTestFailure, +}; diff --git a/.github/workflows/assignment-6-ci.yml b/.github/workflows/assignment-6-ci.yml index 433db70d1..3f7800584 100644 --- a/.github/workflows/assignment-6-ci.yml +++ b/.github/workflows/assignment-6-ci.yml @@ -4,36 +4,191 @@ on: pull_request: types: - opened + - reopened + - synchronize paths: - - 'packages/assignment-6/**' + - "packages/assignment-6/**" + - ".github/workflows/assignment-6-ci.yml" jobs: build: runs-on: ubuntu-latest steps: - - name: job1 구현 - run: echo "job1의 내용을 구현해주세요" + - uses: actions/checkout@v4 + + - name: Setup Node.js and Dependencies + uses: ./.github/actions/setup-node-dependencies + + - name: Build + run: | + pnpm -F assignment-6 build lint: runs-on: ubuntu-latest steps: - - name: job2 구현 - run: echo "job2의 내용을 구현해주세요" + - uses: actions/checkout@v4 + + - name: Setup Node.js and Dependencies + uses: ./.github/actions/setup-node-dependencies + + - name: Lint + run: | + pnpm -F assignment-6 lint > eslint_report.txt + + - name: Report Lint Success + if: success() + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.TOKEN_FOR_CI }} + script: | + const fs = require('fs') + const eslintReport = fs.readFileSync('eslint_report.txt', 'utf8') + + const lint = require('.github/scripts/lint.js'); + await lint.reportLintSuccess({ + context, + github + }); + + - name: Report Lint Failure + if: failure() + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.TOKEN_FOR_CI }} + script: | + const fs = require('fs') + const eslintReport = fs.readFileSync('eslint_report.txt', 'utf8') + + const lint = require('.github/scripts/lint.js'); + await lint.reportLintFailure({ + eslintReport, + context, + github + }); test: runs-on: ubuntu-latest steps: - - name: job3 구현 - run: echo "job3의 내용을 구현해주세요" + - uses: actions/checkout@v4 + + - name: Setup Node.js and Dependencies + uses: ./.github/actions/setup-node-dependencies + + - name: Testing and Coverage + run: | + pnpm -F assignment-6 test + pnpm -F assignment-6 coverage + + - name: Report Test Coverage + uses: actions/github-script@v6 + if: success() + with: + github-token: ${{ secrets.TOKEN_FOR_CI }} + script: | + const fs = require('fs'); + const coverage = JSON.parse(fs.readFileSync("packages/assignment-6/coverage/coverage-summary.json", 'utf8')); + + const testCoverage = require('.github/scripts/test-coverage.js'); + await testCoverage.reportTestSuccess({ + totalCoverage: coverage.total, + context, + github + }); + + - name: Report Test Coverage on Failure + if: failure() + uses: actions/github-script@v6 + with: + github-token: ${{secrets.TOKEN_FOR_CI}} + script: | + const testCoverage = require('.github/scripts/test-coverage.js'); + await testCoverage.reportTestFailure({ + context, + github + }); lighthouse: runs-on: ubuntu-latest + steps: - - name: job4 구현 - run: echo "job4의 내용을 구현해주세요" + - uses: actions/checkout@v4 + + - name: Setup Node.js and Dependencies + uses: ./.github/actions/setup-node-dependencies + + - name: Install Lighthouse CI + run: | + npm i -g @lhci/cli + + - name: Build Local Server + run: | + pnpm -F assignment-6 build + pnpm -F assignment-6 lighthouse + + - name: Report Lighthouse + if: success() + uses: actions/github-script@v6 + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + with: + github-token: ${{ secrets.TOKEN_FOR_CI }} + script: | + const fs = require('fs'); + const results = JSON.parse(fs.readFileSync("packages/assignment-6/lhci_reports/manifest.json", 'utf-8')) + + const lighthouse = require('.github/scripts/lighthouse.js'); + await lighthouse.reportLighthouseSuccess({ + results, + context, + github, + fs + }); + + - name: Report Lighthouse on Failure + if: failure() + uses: actions/github-script@v6 + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + with: + github-token: ${{ secrets.TOKEN_FOR_CI }} + script: | + const fs = require('fs'); + + const lighthouse = require('.github/scripts/lighthouse.js'); + await lighthouse.reportLighthouseFailure({ + context, + github + }); assign_reviewer: runs-on: ubuntu-latest steps: - - name: job5 구현 - run: echo "job5의 내용을 구현해주세요" + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Assign Reviewer + uses: actions/github-script@v6 + with: + github-token: ${{secrets.TOKEN_FOR_CI}} + script: | + const review = require('.github/scripts/review.js'); + const randomReviewer = review.getRandomReviewer(); + + await review.commentReviewer({ + context, + github, + reviewer: randomReviewer + }); + + await review.assignReviewer({ + context, + github, + reviewer: randomReviewer + }); diff --git a/packages/assignment-6/.lighthouserc.js b/packages/assignment-6/.lighthouserc.js new file mode 100644 index 000000000..1e4abbdb8 --- /dev/null +++ b/packages/assignment-6/.lighthouserc.js @@ -0,0 +1,14 @@ +module.exports = { + ci: { + collect: { + staticDistDir: "./dist", + url: ["http://localhost:5173"], + numberOfRuns: 1, + }, + upload: { + target: "filesystem", + outputDir: "./lhci_reports", + reportFilenamePattern: "%%PATHNAME%%-%%DATETIME%%-report.%%EXTENSION%%", + }, + }, +}; diff --git a/packages/assignment-6/package.json b/packages/assignment-6/package.json index 2ed9d0369..5923ead6e 100644 --- a/packages/assignment-6/package.json +++ b/packages/assignment-6/package.json @@ -2,13 +2,14 @@ "name": "assignment-6", "private": true, "version": "0.0.0", - "type": "module", "scripts": { "start": "vite", "test": "vitest", "test:ui": "vitest --ui", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "coverage": "vitest --coverage.enabled --reporter=junit --coverage.reporter=json-summary", + "lighthouse": "lhci autorun" }, "dependencies": { "react": "^18.3.1", @@ -23,6 +24,7 @@ "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react-swc": "^3.5.0", + "@vitest/coverage-v8": "^2.0.4", "@vitest/ui": "^1.6.0", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", diff --git a/packages/assignment-6/src/main.tsx b/packages/assignment-6/src/main.tsx index db8548ee8..6b23509aa 100644 --- a/packages/assignment-6/src/main.tsx +++ b/packages/assignment-6/src/main.tsx @@ -1,11 +1,10 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import { forLintError } from './forLintError.ts' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +// import { forLintError } from "./forLintError.ts"; - -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById("root")!).render( - , -) + +);