feat: 접근성 검사 표준 선택 기능 — WCAG/KWCAG 버전별 선택 지원

3가지 검사 모드(한 페이지, 사이트 크롤링, 목록 업로드) 모두에서 접근성 표준을
선택할 수 있도록 추가. WCAG 2.0 A/AA, 2.1 AA, 2.2 AA와 KWCAG 2.1, 2.2를
지원하며, KWCAG 선택 시 axe-core 결과를 KWCAG 검사항목으로 자동 매핑.

- KWCAG 2.2 (33항목) / 2.1 (24항목) ↔ WCAG 매핑 테이블 (kwcag_mapping.py)
- AccessibilityChecker에 표준 파싱 및 KWCAG 변환 로직 추가
- 전체 API 파이프라인에 accessibility_standard 파라미터 전파
- 프론트엔드 3개 폼에 공용 표준 선택 드롭다운 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-14 08:36:14 +09:00
parent 21259eb40a
commit bffce65aca
19 changed files with 857 additions and 59 deletions

View File

@ -1,8 +1,9 @@
"""
Accessibility (WCAG) Checker Engine (F-003).
Accessibility (WCAG/KWCAG) Checker Engine (F-003).
Uses Playwright + axe-core for comprehensive accessibility testing.
Falls back to BeautifulSoup-based checks if Playwright is unavailable.
Supports WCAG version selection (2.0/2.1/2.2) via rules/accessibility.yaml.
Supports KWCAG (Korean) standard selection (2.1/2.2) via kwcag_mapping module.
"""
import json
@ -14,6 +15,11 @@ from typing import Optional
from bs4 import BeautifulSoup
from app.engines.base import BaseChecker
from app.engines.kwcag_mapping import (
convert_wcag_issue_to_kwcag,
get_kwcag_axe_tags,
get_kwcag_label,
)
from app.models.schemas import CategoryResult, Issue
from app.rules import get_rules
@ -115,13 +121,55 @@ def _get_wcag_level_label(wcag_version: str) -> str:
return labels.get(wcag_version, "WCAG 2.1 Level AA")
class AccessibilityChecker(BaseChecker):
"""Accessibility (WCAG) checker engine with version selection."""
# Standard parameter to internal version mapping
# Maps API-facing standard values to (is_kwcag, internal_version)
_STANDARD_MAP: dict[str, tuple[bool, str]] = {
"wcag_2.0_a": (False, "2.0_A"),
"wcag_2.0_aa": (False, "2.0_AA"),
"wcag_2.1_aa": (False, "2.1_AA"),
"wcag_2.2_aa": (False, "2.2_AA"),
"kwcag_2.1": (True, "kwcag_2.1"),
"kwcag_2.2": (True, "kwcag_2.2"),
}
def __init__(self, progress_callback=None, wcag_version: str = "2.1_AA"):
def _parse_standard(standard: str) -> tuple[bool, str]:
"""
Parse the accessibility standard parameter.
Args:
standard: API-facing standard string, e.g. "wcag_2.1_aa" or "kwcag_2.2"
Returns:
(is_kwcag, version_key) where version_key is the internal key
for axe tag selection and label generation.
"""
return _STANDARD_MAP.get(standard, (False, "2.1_AA"))
class AccessibilityChecker(BaseChecker):
"""Accessibility (WCAG/KWCAG) checker engine with standard selection."""
def __init__(
self,
progress_callback=None,
wcag_version: str = "2.1_AA",
standard: str = "wcag_2.1_aa",
):
super().__init__(progress_callback)
self.wcag_version = wcag_version
self.axe_tags = _get_axe_tags_for_version(wcag_version)
# Parse the standard parameter to determine mode
self.is_kwcag, self._version_key = _parse_standard(standard)
self.standard = standard
if self.is_kwcag:
# KWCAG mode: use KWCAG-specific axe tags
self.wcag_version = wcag_version # Keep for internal compatibility
self.axe_tags = get_kwcag_axe_tags(self._version_key)
else:
# WCAG mode: use the internal version key for axe tags
self.wcag_version = self._version_key
self.axe_tags = _get_axe_tags_for_version(self._version_key)
@property
def category_name(self) -> str:
@ -172,7 +220,12 @@ class AccessibilityChecker(BaseChecker):
}
""")
await self.update_progress(60, f"접근성 검사 실행 중 ({_get_wcag_level_label(self.wcag_version)})...")
level_label = (
get_kwcag_label(self._version_key)
if self.is_kwcag
else _get_wcag_level_label(self.wcag_version)
)
await self.update_progress(60, f"접근성 검사 실행 중 ({level_label})...")
# Build axe-core tag list including best-practice
axe_tag_list = self.axe_tags + ["best-practice"]
@ -203,11 +256,17 @@ class AccessibilityChecker(BaseChecker):
await browser.close()
await self.update_progress(100, "완료")
wcag_level = (
get_kwcag_label(self._version_key)
if self.is_kwcag
else _get_wcag_level_label(self.wcag_version)
)
return self._build_result(
category="accessibility",
score=score,
issues=issues,
wcag_level=_get_wcag_level_label(self.wcag_version),
wcag_level=wcag_level,
)
async def _check_with_beautifulsoup(self, url: str, html_content: str) -> CategoryResult:
@ -239,11 +298,16 @@ class AccessibilityChecker(BaseChecker):
score = self._calculate_score_by_deduction(issues)
await self.update_progress(100, "완료")
wcag_level = (
get_kwcag_label(self._version_key)
if self.is_kwcag
else _get_wcag_level_label(self.wcag_version)
)
return self._build_result(
category="accessibility",
score=score,
issues=issues,
wcag_level=_get_wcag_level_label(self.wcag_version),
wcag_level=wcag_level,
)
def _parse_axe_results(self, axe_results: dict) -> list[Issue]:
@ -287,14 +351,22 @@ class AccessibilityChecker(BaseChecker):
node_count = len(nodes)
count_info = f" ({node_count}개 요소)" if node_count > 1 else ""
issues.append(self._create_issue(
issue = self._create_issue(
code=code,
severity=severity,
message=f"{korean_msg}{detail}{count_info}",
element=element,
suggestion=violation.get("helpUrl", "해당 WCAG 기준을 확인하고 수정하세요"),
wcag_criterion=wcag,
))
)
# Convert to KWCAG criterion if in KWCAG mode
if self.is_kwcag:
issue_dict = issue.model_dump()
issue_dict = convert_wcag_issue_to_kwcag(issue_dict, self._version_key)
issue = Issue(**issue_dict)
issues.append(issue)
return issues