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:
108
backend/app/engines/base.py
Normal file
108
backend/app/engines/base.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user