Skip to content

Qtvy 152 fix UI

Qtvy 152 fix UI #34

Workflow file for this run

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']
});