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

108
backend/app/engines/base.py Normal file
View File

@ -0,0 +1,108 @@
"""
BaseChecker abstract class - foundation for all inspection engines.
"""
from abc import ABC, abstractmethod
from typing import Callable, Optional
from app.models.schemas import CategoryResult, Issue, Severity, calculate_grade
class BaseChecker(ABC):
"""
Abstract base class for all inspection engines.
Provides progress callback mechanism and common utility methods.
"""
def __init__(self, progress_callback: Optional[Callable] = None):
self.progress_callback = progress_callback
async def update_progress(self, progress: int, current_step: str) -> None:
"""Update progress via Redis callback."""
if self.progress_callback:
await self.progress_callback(
category=self.category_name,
progress=progress,
current_step=current_step,
)
@property
@abstractmethod
def category_name(self) -> str:
"""Category identifier (e.g., 'html_css')."""
pass
@abstractmethod
async def check(self, url: str, html_content: str, headers: dict) -> CategoryResult:
"""Execute inspection and return results."""
pass
def _create_issue(
self,
code: str,
severity: str,
message: str,
suggestion: str,
element: Optional[str] = None,
line: Optional[int] = None,
wcag_criterion: Optional[str] = None,
) -> Issue:
"""Helper to create a standardized Issue object."""
return Issue(
code=code,
category=self.category_name,
severity=Severity(severity),
message=message,
element=element,
line=line,
suggestion=suggestion,
wcag_criterion=wcag_criterion,
)
def _calculate_score_by_deduction(self, issues: list[Issue]) -> int:
"""
Calculate score by deduction:
score = 100 - (Critical*15 + Major*8 + Minor*3 + Info*1)
Minimum 0, Maximum 100
"""
severity_weights = {
"critical": 15,
"major": 8,
"minor": 3,
"info": 1,
}
deduction = sum(
severity_weights.get(issue.severity.value, 0) for issue in issues
)
return max(0, 100 - deduction)
def _build_result(
self,
category: str,
score: int,
issues: list[Issue],
wcag_level: Optional[str] = None,
meta_info: Optional[dict] = None,
sub_scores: Optional[dict] = None,
metrics: Optional[dict] = None,
) -> CategoryResult:
"""Build a CategoryResult with computed severity counts."""
critical = sum(1 for i in issues if i.severity == Severity.CRITICAL)
major = sum(1 for i in issues if i.severity == Severity.MAJOR)
minor = sum(1 for i in issues if i.severity == Severity.MINOR)
info = sum(1 for i in issues if i.severity == Severity.INFO)
return CategoryResult(
score=score,
grade=calculate_grade(score),
total_issues=len(issues),
critical=critical,
major=major,
minor=minor,
info=info,
issues=issues,
wcag_level=wcag_level,
meta_info=meta_info,
sub_scores=sub_scores,
metrics=metrics,
)