feat: 웹사이트 표준화 검사 도구 구현

- 4개 검사 엔진: HTML/CSS, 접근성(WCAG), SEO, 성능/보안 (총 50개 항목)
- FastAPI 백엔드 (9개 API, SSE 실시간 진행, PDF/JSON 리포트)
- Next.js 15 프론트엔드 (6개 페이지, 29개 컴포넌트, 반원 게이지 차트)
- Docker Compose 배포 (Backend:8011, Frontend:3011, MongoDB:27022, Redis:6392)
- 전체 테스트 32/32 PASS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-13 13:57:27 +09:00
parent c37cda5b13
commit b5fa5d96b9
93 changed files with 18735 additions and 22 deletions

View File

@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Web Inspector Report - {{ inspection.url }}</title>
<link rel="stylesheet" href="report.css">
<style>
@page {
size: A4;
margin: 2cm;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
color: #1F2937;
line-height: 1.6;
font-size: 10pt;
}
/* Cover */
.cover {
text-align: center;
padding: 60px 0 40px;
border-bottom: 3px solid #6366F1;
margin-bottom: 30px;
}
.cover h1 {
font-size: 24pt;
color: #6366F1;
margin-bottom: 10px;
}
.cover .subtitle {
font-size: 12pt;
color: #6B7280;
}
.cover .url {
font-size: 11pt;
color: #374151;
margin-top: 20px;
word-break: break-all;
}
.cover .date {
font-size: 9pt;
color: #9CA3AF;
margin-top: 10px;
}
/* Score Section */
.overall-score {
text-align: center;
padding: 20px;
margin-bottom: 30px;
}
.score-circle {
display: inline-block;
width: 120px;
height: 120px;
border-radius: 50%;
line-height: 120px;
text-align: center;
font-size: 36pt;
font-weight: bold;
color: white;
margin-bottom: 10px;
}
.score-grade {
font-size: 14pt;
font-weight: bold;
margin-top: 5px;
}
/* Category Table */
.category-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.category-table th, .category-table td {
border: 1px solid #E5E7EB;
padding: 8px 12px;
text-align: center;
}
.category-table th {
background: #F9FAFB;
font-weight: 600;
font-size: 9pt;
}
.category-table td {
font-size: 9pt;
}
/* Issues Section */
.issues-section {
margin-bottom: 30px;
page-break-inside: avoid;
}
.issues-section h2 {
font-size: 14pt;
color: #374151;
border-bottom: 2px solid #E5E7EB;
padding-bottom: 5px;
margin-bottom: 15px;
}
.issue-card {
border: 1px solid #E5E7EB;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 8px;
page-break-inside: avoid;
}
.issue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.issue-code {
font-weight: 600;
font-size: 9pt;
color: #6B7280;
}
.severity-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
color: white;
font-size: 8pt;
font-weight: 600;
}
.severity-critical { background: #EF4444; }
.severity-major { background: #F97316; }
.severity-minor { background: #EAB308; color: #1F2937; }
.severity-info { background: #3B82F6; }
.issue-message {
font-size: 9pt;
color: #374151;
margin-bottom: 5px;
}
.issue-suggestion {
font-size: 8pt;
color: #6366F1;
background: #EEF2FF;
padding: 4px 8px;
border-radius: 3px;
}
.issue-element {
font-size: 7pt;
color: #6B7280;
background: #F9FAFB;
padding: 3px 6px;
border-radius: 3px;
font-family: monospace;
margin-bottom: 4px;
word-break: break-all;
}
/* Summary */
.summary-section {
margin-bottom: 30px;
}
.summary-bar {
display: flex;
gap: 10px;
margin-top: 10px;
}
.summary-item {
flex: 1;
text-align: center;
padding: 10px;
border-radius: 6px;
}
.summary-count {
font-size: 18pt;
font-weight: bold;
}
.summary-label {
font-size: 8pt;
color: #6B7280;
}
/* Footer */
.footer {
text-align: center;
font-size: 8pt;
color: #9CA3AF;
border-top: 1px solid #E5E7EB;
padding-top: 10px;
margin-top: 30px;
}
h3 {
font-size: 12pt;
color: #374151;
margin-bottom: 10px;
}
</style>
</head>
<body>
<!-- Cover Page -->
<div class="cover">
<h1>Web Inspector</h1>
<div class="subtitle">웹 표준 검사 리포트</div>
<div class="url">{{ inspection.url }}</div>
<div class="date">
검사일시: {{ inspection.created_at }}
{% if inspection.duration_seconds %} | 소요시간: {{ inspection.duration_seconds }}초{% endif %}
</div>
<div class="date">리포트 생성: {{ generated_at }}</div>
</div>
<!-- Overall Score -->
<div class="overall-score">
<div class="score-circle" style="background: {{ inspection.grade | grade_color }};">
{{ inspection.overall_score }}
</div>
<div class="score-grade" style="color: {{ inspection.grade | grade_color }};">
등급: {{ inspection.grade }}
</div>
</div>
<!-- Category Summary Table -->
<table class="category-table">
<thead>
<tr>
<th>카테고리</th>
<th>점수</th>
<th>등급</th>
<th>이슈 수</th>
<th>Critical</th>
<th>Major</th>
<th>Minor</th>
<th>Info</th>
</tr>
</thead>
<tbody>
{% for cat_name, cat_data in inspection.categories.items() %}
<tr>
<td style="text-align: left; font-weight: 600;">{{ cat_name | category_label }}</td>
<td style="font-weight: bold; color: {{ cat_data.grade | grade_color }};">{{ cat_data.score }}</td>
<td>{{ cat_data.grade }}</td>
<td>{{ cat_data.total_issues }}</td>
<td style="color: #EF4444;">{{ cat_data.critical }}</td>
<td style="color: #F97316;">{{ cat_data.major }}</td>
<td style="color: #EAB308;">{{ cat_data.minor }}</td>
<td style="color: #3B82F6;">{{ cat_data.info }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Issue Summary -->
<div class="summary-section">
<h3>이슈 요약</h3>
<div class="summary-bar">
<div class="summary-item" style="background: #FEE2E2;">
<div class="summary-count" style="color: #EF4444;">{{ inspection.summary.critical }}</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-item" style="background: #FFEDD5;">
<div class="summary-count" style="color: #F97316;">{{ inspection.summary.major }}</div>
<div class="summary-label">Major</div>
</div>
<div class="summary-item" style="background: #FEF9C3;">
<div class="summary-count" style="color: #EAB308;">{{ inspection.summary.minor }}</div>
<div class="summary-label">Minor</div>
</div>
<div class="summary-item" style="background: #DBEAFE;">
<div class="summary-count" style="color: #3B82F6;">{{ inspection.summary.info }}</div>
<div class="summary-label">Info</div>
</div>
</div>
</div>
<!-- Issues by Category -->
{% for cat_name, cat_data in inspection.categories.items() %}
{% if cat_data.issues %}
<div class="issues-section">
<h2>{{ cat_name | category_label }} ({{ cat_data.score }}점)</h2>
{% for issue in cat_data.issues %}
<div class="issue-card">
<div class="issue-header">
<span class="issue-code">{{ issue.code }}</span>
<span class="severity-badge severity-{{ issue.severity }}">{{ issue.severity | upper }}</span>
</div>
<div class="issue-message">{{ issue.message }}</div>
{% if issue.element %}
<div class="issue-element">{{ issue.element }}</div>
{% endif %}
{% if issue.suggestion %}
<div class="issue-suggestion">{{ issue.suggestion }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
<!-- Footer -->
<div class="footer">
Web Inspector Report | Generated by Web Inspector v1.0
</div>
</body>
</html>