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

View File

@ -0,0 +1,33 @@
"""
MongoDB document models and helper functions.
"""
from datetime import datetime, timezone
from typing import Optional
def create_inspection_document(
inspection_id: str,
url: str,
status: str,
overall_score: int,
grade: str,
categories: dict,
summary: dict,
created_at: datetime,
completed_at: Optional[datetime] = None,
duration_seconds: Optional[float] = None,
) -> dict:
"""Create a MongoDB document for the inspections collection."""
return {
"inspection_id": inspection_id,
"url": url,
"status": status,
"created_at": created_at,
"completed_at": completed_at,
"duration_seconds": duration_seconds,
"overall_score": overall_score,
"grade": grade,
"categories": categories,
"summary": summary,
}

View File

@ -0,0 +1,179 @@
"""
Pydantic models for request/response validation and serialization.
"""
from pydantic import BaseModel, Field, HttpUrl
from typing import Optional
from datetime import datetime
from enum import Enum
# --- Enums ---
class InspectionStatus(str, Enum):
RUNNING = "running"
COMPLETED = "completed"
ERROR = "error"
class Severity(str, Enum):
CRITICAL = "critical"
MAJOR = "major"
MINOR = "minor"
INFO = "info"
class CategoryName(str, Enum):
HTML_CSS = "html_css"
ACCESSIBILITY = "accessibility"
SEO = "seo"
PERFORMANCE_SECURITY = "performance_security"
# --- Request ---
class StartInspectionRequest(BaseModel):
url: HttpUrl
# --- Core Data Models ---
class Issue(BaseModel):
code: str
category: str
severity: Severity
message: str
element: Optional[str] = None
line: Optional[int] = None
suggestion: str
wcag_criterion: Optional[str] = None
class CategoryResult(BaseModel):
score: int = Field(ge=0, le=100)
grade: str
total_issues: int
critical: int = 0
major: int = 0
minor: int = 0
info: int = 0
issues: list[Issue] = []
# Category-specific fields
wcag_level: Optional[str] = None
meta_info: Optional[dict] = None
sub_scores: Optional[dict] = None
metrics: Optional[dict] = None
class IssueSummary(BaseModel):
total_issues: int
critical: int
major: int
minor: int
info: int
# --- Response Models ---
class StartInspectionResponse(BaseModel):
inspection_id: str
status: str = "running"
url: str
stream_url: str
class InspectionResult(BaseModel):
inspection_id: str
url: str
status: InspectionStatus
created_at: datetime
completed_at: Optional[datetime] = None
duration_seconds: Optional[float] = None
overall_score: int = Field(ge=0, le=100)
grade: str
categories: dict[str, CategoryResult]
summary: IssueSummary
class InspectionResultResponse(BaseModel):
"""Response model for GET /api/inspections/{id} (without nested issues)."""
inspection_id: str
url: str
status: InspectionStatus
created_at: datetime
completed_at: Optional[datetime] = None
duration_seconds: Optional[float] = None
overall_score: int = Field(ge=0, le=100)
grade: str
categories: dict[str, CategoryResult]
summary: IssueSummary
class IssueListResponse(BaseModel):
inspection_id: str
total: int
filters: dict
issues: list[Issue]
class InspectionListItem(BaseModel):
inspection_id: str
url: str
created_at: datetime
overall_score: int
grade: str
total_issues: int
class PaginatedResponse(BaseModel):
items: list[InspectionListItem]
total: int
page: int
limit: int
total_pages: int
class TrendDataPoint(BaseModel):
inspection_id: str
created_at: datetime
overall_score: int
html_css: int
accessibility: int
seo: int
performance_security: int
class TrendResponse(BaseModel):
url: str
data_points: list[TrendDataPoint]
class HealthResponse(BaseModel):
status: str
timestamp: str
services: dict[str, str]
# --- Utility functions ---
def calculate_grade(score: int) -> str:
"""Calculate letter grade from numeric score."""
if score >= 90:
return "A+"
if score >= 80:
return "A"
if score >= 70:
return "B"
if score >= 60:
return "C"
if score >= 50:
return "D"
return "F"
def calculate_overall_score(categories: dict[str, CategoryResult]) -> int:
"""Calculate overall score as simple average of category scores."""
scores = [cat.score for cat in categories.values()]
if not scores:
return 0
return round(sum(scores) / len(scores))