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:
2
backend/app/templates/report.css
Normal file
2
backend/app/templates/report.css
Normal file
@ -0,0 +1,2 @@
|
||||
/* Additional styles for PDF report - can be extended */
|
||||
/* Main styles are inline in report.html for WeasyPrint compatibility */
|
||||
307
backend/app/templates/report.html
Normal file
307
backend/app/templates/report.html
Normal 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>
|
||||
Reference in New Issue
Block a user