Qtvy 152 fix UI #34
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Lighthouse CI | |
on: | |
pull_request: | |
branches: | |
- develop # develop λΈλμΉλ‘ κ°λ PRλ§ μ€ν | |
types: [closed] # PRμ΄ λ«ν λ μ€ν | |
permissions: | |
issues: write | |
contents: read | |
jobs: | |
lighthouse-audit: | |
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true | |
name: Lighthouse Audit | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Use Node.js 20 | |
uses: actions/setup-node@v4 | |
with: | |
node-version: 20 | |
- name: Install pnpm | |
uses: pnpm/action-setup@v2 | |
with: | |
version: latest | |
- name: Install dependencies | |
run: | | |
pnpm add -g @lhci/cli@0.14.x | |
pnpm add -g http-server | |
- name: Start local server | |
run: http-server . -p 8080 & | |
- name: Run Lighthouse CI | |
id: lighthouse | |
continue-on-error: true | |
run: | | |
URL="${{ github.event.inputs.url || 'http://localhost:8080' }}" | |
lhci autorun --collect.url=$URL | |
- name: Create GitHub Issue with Results | |
if: always() | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ github.token }} | |
script: | | |
const fs = require('fs'); | |
const path = require('path'); | |
const lhciDir = '.lighthouseci'; | |
const jsonReports = fs.readdirSync(lhciDir).filter(f => f.endsWith('.json')); | |
const latestReport = jsonReports.sort().reverse()[0]; | |
const reportPath = path.join(lhciDir, latestReport); | |
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); | |
const formatAssertions = (report) => { | |
const assertions = []; | |
// λΈλΌμ°μ μ½μ μλ¬ | |
if (report.audits['errors-in-console']?.score === 0) { | |
assertions.push({ | |
category: 'λΈλΌμ°μ μ€λ₯', | |
issue: 'Console μλ¬κ° λ°κ²¬λμμ΅λλ€.', | |
fix: 'λΈλΌμ°μ μ½μμ κΈ°λ‘λ μλ¬λ₯Ό νμΈνκ³ μμ ν΄μ£ΌμΈμ.', | |
link: 'https://developer.chrome.com/docs/lighthouse/best-practices/errors-in-console/' | |
}); | |
} | |
// μ κ·Όμ± λ¬Έμ | |
if (report.audits['html-has-lang']?.score === 0) { | |
assertions.push({ | |
category: 'μ κ·Όμ±', | |
issue: 'HTML lang μμ±μ΄ μμ΅λλ€.', | |
fix: '<html> νκ·Έμ lang μμ±μ μΆκ°ν΄μ£ΌμΈμ. μ: <html lang="ko">', | |
link: 'https://dequeuniversity.com/rules/axe/4.9/html-has-lang' | |
}); | |
} | |
// SEO λ¬Έμ | |
if (report.audits['meta-description']?.score === 0) { | |
assertions.push({ | |
category: 'SEO', | |
issue: 'meta descriptionμ΄ μμ΅λλ€.', | |
fix: '<head> νκ·Έ λ΄μ λ©ν μ€λͺ μ μΆκ°ν΄μ£ΌμΈμ.', | |
link: 'https://developer.chrome.com/docs/lighthouse/seo/meta-description/' | |
}); | |
} | |
// μ±λ₯ λ¬Έμ | |
if (report.audits['unused-css-rules']?.details?.items?.length > 0) { | |
assertions.push({ | |
category: 'μ±λ₯', | |
issue: 'μ¬μ©νμ§ μλ CSSκ° μμ΅λλ€.', | |
fix: 'λΆνμν CSSλ₯Ό μ κ±°νκ±°λ νμν CSSλ§ λ‘λνλλ‘ μμ ν΄μ£ΌμΈμ.', | |
link: 'https://developer.chrome.com/docs/lighthouse/performance/unused-css-rules/' | |
}); | |
} | |
return assertions; | |
}; | |
const formatScore = (value = 0) => { | |
return Math.round(value * 100); | |
}; | |
const getEmoji = (value, metric) => { | |
const thresholds = { | |
LCP: { good: 2500, needsImprovement: 4000 }, | |
INP: { good: 200, needsImprovement: 500 }, | |
CLS: { good: 0.1, needsImprovement: 0.25 }, | |
}; | |
if (!thresholds[metric]) return value >= 90 ? 'π’' : value >= 50 ? 'π ' : 'π΄'; | |
const t = thresholds[metric]; | |
return value <= t.good ? 'π’' : value <= t.needsImprovement ? 'π ' : 'π΄'; | |
}; | |
const formatMetric = (value, metric) => { | |
if (!value) return 'N/A'; | |
if (metric === 'CLS') return value.toFixed(3); | |
return `${(value / 1000).toFixed(2)}s`; | |
}; | |
const getLighthouseResult = (report, category) => { | |
try { | |
return report.categories?.[category]?.score ?? 0; | |
} catch (e) { | |
return 0; | |
} | |
}; | |
const getMetricValue = (report, metric) => { | |
try { | |
return report.audits?.[metric]?.numericValue ?? 0; | |
} catch (e) { | |
return 0; | |
} | |
}; | |
const lighthouseScores = { | |
performance: getLighthouseResult(report, 'performance'), | |
accessibility: getLighthouseResult(report, 'accessibility'), | |
'best-practices': getLighthouseResult(report, 'best-practices'), | |
seo: getLighthouseResult(report, 'seo'), | |
pwa: getLighthouseResult(report, 'pwa') | |
}; | |
const webVitals = { | |
LCP: getMetricValue(report, 'largest-contentful-paint'), | |
INP: getMetricValue(report, 'experimental-interaction-to-next-paint'), | |
CLS: getMetricValue(report, 'cumulative-layout-shift') | |
}; | |
const reportUrl = `.lighthouseci/${latestReport.replace('.json', '.html')}`; | |
const body = `## π¨ μΉμ¬μ΄νΈ μ±λ₯ μΈ‘μ κ²°κ³Ό | |
### π μ€ν μ 보 | |
- PR: #${context.payload.pull_request.number} | |
- Title: ${context.payload.pull_request.title} | |
- Author: ${context.payload.pull_request.user.login} | |
- Branch: ${context.payload.pull_request.head.ref} β ${context.payload.pull_request.base.ref} | |
### β νμ§ κ²μ¦ μ€ν¨ νλͺ© | |
${formatAssertions(report).map(assertion => ` | |
#### ${assertion.category} | |
- **λ¬Έμ **: ${assertion.issue} | |
- **κ°μ λ°©μ**: ${assertion.fix} | |
- **μ°Έκ³ λ¬Έμ**: [κ°μ΄λ λ¬Έμ](${assertion.link}) | |
`).join('\n')} | |
### π― Lighthouse μ μ | |
| μΉ΄ν κ³ λ¦¬ | μ μ | μν | | |
|----------|------|------| | |
| Performance | ${formatScore(lighthouseScores.performance)}% | ${getEmoji(formatScore(lighthouseScores.performance))} | | |
| Accessibility | ${formatScore(lighthouseScores.accessibility)}% | ${getEmoji(formatScore(lighthouseScores.accessibility))} | | |
| Best Practices | ${formatScore(lighthouseScores['best-practices'])}% | ${getEmoji(formatScore(lighthouseScores['best-practices']))} | | |
| SEO | ${formatScore(lighthouseScores.seo)}% | ${getEmoji(formatScore(lighthouseScores.seo))} | | |
| PWA | ${formatScore(lighthouseScores.pwa)}% | ${getEmoji(formatScore(lighthouseScores.pwa))} | | |
### π Core Web Vitals (2024) | |
| λ©νΈλ¦ | μ€λͺ | μΈ‘μ κ° | μν | | |
|--------|------|--------|------| | |
| LCP | Largest Contentful Paint | ${formatMetric(webVitals.LCP, 'LCP')} | ${getEmoji(webVitals.LCP, 'LCP')} | | |
| INP | Interaction to Next Paint | ${formatMetric(webVitals.INP, 'INP')} | ${getEmoji(webVitals.INP, 'INP')} | | |
| CLS | Cumulative Layout Shift | ${formatMetric(webVitals.CLS, 'CLS')} | ${getEmoji(webVitals.CLS, 'CLS')} | | |
### π Core Web Vitals κΈ°μ€κ° | |
- **LCP (Largest Contentful Paint)**: κ°μ₯ ν° μ½ν μΈ κ° νλ©΄μ κ·Έλ €μ§λ μμ | |
- π’ Good: < 2.5s | |
- π Needs Improvement: < 4.0s | |
- π΄ Poor: β₯ 4.0s | |
- **INP (Interaction to Next Paint)**: μ¬μ©μ μνΈμμ©μ λν μ λ°μ μΈ μλ΅μ± | |
- π’ Good: < 200ms | |
- π Needs Improvement: < 500ms | |
- π΄ Poor: β₯ 500ms | |
- **CLS (Cumulative Layout Shift)**: νμ΄μ§ λ‘λ μ€ μκΈ°μΉ μμ λ μ΄μμ λ³κ²½μ μ λ | |
- π’ Good: < 0.1 | |
- π Needs Improvement: < 0.25 | |
- π΄ Poor: β₯ 0.25 | |
> π μΈ‘μ μκ°: ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}`; | |
await github.rest.issues.create({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
title: `π μΉμ¬μ΄νΈ μ±λ₯ μΈ‘μ κ²°κ³Ό - ${new Date().toLocaleString('ko-KR', { | |
timeZone: 'Asia/Seoul', | |
year: 'numeric', | |
month: '2-digit', | |
day: '2-digit', | |
hour: '2-digit', | |
minute: '2-digit', | |
hour12: false | |
})}`, | |
body: body, | |
labels: ['lighthouse-audit', 'web-vitals'] | |
}); |