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>
393 lines
12 KiB
Python
393 lines
12 KiB
Python
"""
|
|
KWCAG (Korean Web Content Accessibility Guidelines) mapping module.
|
|
|
|
Provides bidirectional mapping between KWCAG 2.1/2.2 and WCAG criteria,
|
|
enabling axe-core results to be re-interpreted under Korean accessibility standards.
|
|
|
|
KWCAG 2.2 contains 33 inspection items across 4 principles.
|
|
KWCAG 2.1 contains 24 inspection items (a subset of 2.2).
|
|
KWCAG 2.2 aligns with WCAG 2.1 Level AA and includes additional items.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# KWCAG 2.2 Full Mapping (33 items)
|
|
# =============================================================================
|
|
# Key: KWCAG criterion number
|
|
# Value: {
|
|
# "wcag_criteria": [list of corresponding WCAG criterion numbers],
|
|
# "name": Korean name of the KWCAG item,
|
|
# "principle": KWCAG principle name,
|
|
# "kwcag_21": True if this item also exists in KWCAG 2.1,
|
|
# }
|
|
|
|
KWCAG_TO_WCAG: dict[str, dict] = {
|
|
# ========================================
|
|
# 원칙 1. 인식의 용이성
|
|
# ========================================
|
|
"1.1.1": {
|
|
"wcag_criteria": ["1.1.1"],
|
|
"name": "적절한 대체 텍스트",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.2.1": {
|
|
"wcag_criteria": ["1.2.2", "1.2.4"],
|
|
"name": "자막 제공",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.3.1": {
|
|
"wcag_criteria": ["1.4.1"],
|
|
"name": "색에 무관한 콘텐츠 인식",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.4.1": {
|
|
"wcag_criteria": ["1.3.3"],
|
|
"name": "명확한 지시 사항",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.5.1": {
|
|
"wcag_criteria": ["1.4.3"],
|
|
"name": "텍스트 콘텐츠의 명도 대비",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.6.1": {
|
|
"wcag_criteria": ["1.4.2"],
|
|
"name": "자동 재생 금지",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"1.7.1": {
|
|
"wcag_criteria": ["1.3.1"],
|
|
"name": "콘텐츠 간의 구분",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
|
|
# ========================================
|
|
# 원칙 2. 운용의 용이성
|
|
# ========================================
|
|
"2.1.1": {
|
|
"wcag_criteria": ["2.1.1", "2.1.2"],
|
|
"name": "키보드 사용 보장",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.2.1": {
|
|
"wcag_criteria": ["2.4.3", "2.4.7"],
|
|
"name": "초점 이동과 표시",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.3.1": {
|
|
"wcag_criteria": ["2.5.1"],
|
|
"name": "조작 가능",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.4.1": {
|
|
"wcag_criteria": ["2.2.1"],
|
|
"name": "응답시간 조절",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.5.1": {
|
|
"wcag_criteria": ["2.2.2"],
|
|
"name": "정지 기능 제공",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.6.1": {
|
|
"wcag_criteria": ["2.3.1"],
|
|
"name": "깜빡임과 번쩍임 사용 제한",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.7.1": {
|
|
"wcag_criteria": ["2.4.1"],
|
|
"name": "반복 영역 건너뛰기",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"2.8.1": {
|
|
"wcag_criteria": ["2.4.2", "2.4.5", "2.4.6", "2.4.8"],
|
|
"name": "쉬운 내비게이션",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
|
|
# ========================================
|
|
# 원칙 3. 이해의 용이성
|
|
# ========================================
|
|
"3.1.1": {
|
|
"wcag_criteria": ["3.1.1"],
|
|
"name": "기본 언어 표시",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"3.2.1": {
|
|
"wcag_criteria": ["3.2.1", "3.2.2"],
|
|
"name": "사용자 요구에 따른 실행",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"3.3.1": {
|
|
"wcag_criteria": ["1.3.2"],
|
|
"name": "콘텐츠의 선형구조",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"3.4.1": {
|
|
"wcag_criteria": ["1.3.1"],
|
|
"name": "표의 구성",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"3.5.1": {
|
|
"wcag_criteria": ["1.3.1", "3.3.2"],
|
|
"name": "레이블 제공",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
"3.6.1": {
|
|
"wcag_criteria": ["3.3.1", "3.3.3"],
|
|
"name": "오류 정정",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": True,
|
|
},
|
|
|
|
# ========================================
|
|
# 원칙 4. 견고성
|
|
# ========================================
|
|
"4.1.1": {
|
|
"wcag_criteria": ["4.1.1"],
|
|
"name": "마크업 오류 방지",
|
|
"principle": "견고성",
|
|
"kwcag_21": True,
|
|
},
|
|
"4.2.1": {
|
|
"wcag_criteria": ["4.1.2"],
|
|
"name": "웹 애플리케이션 접근성 준수",
|
|
"principle": "견고성",
|
|
"kwcag_21": True,
|
|
},
|
|
|
|
# ========================================
|
|
# KWCAG 2.2 추가 항목 (KWCAG 2.1에 없는 항목)
|
|
# ========================================
|
|
"1.1.2": {
|
|
"wcag_criteria": ["1.1.1"],
|
|
"name": "대체 콘텐츠의 동등성",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"1.2.2": {
|
|
"wcag_criteria": ["1.2.1", "1.2.3"],
|
|
"name": "음성 및 영상 대체 수단",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"1.5.2": {
|
|
"wcag_criteria": ["1.4.11"],
|
|
"name": "비텍스트 콘텐츠의 명도 대비",
|
|
"principle": "인식의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.1.2": {
|
|
"wcag_criteria": ["2.1.4"],
|
|
"name": "키보드 단축키",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.2.2": {
|
|
"wcag_criteria": ["2.4.11"],
|
|
"name": "초점 감춤 방지",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.3.2": {
|
|
"wcag_criteria": ["2.5.1", "2.5.2"],
|
|
"name": "포인터 입력의 취소",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.3.3": {
|
|
"wcag_criteria": ["2.5.8"],
|
|
"name": "포인터 대상 크기",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"2.8.2": {
|
|
"wcag_criteria": ["2.4.4"],
|
|
"name": "링크 텍스트",
|
|
"principle": "운용의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"3.2.2": {
|
|
"wcag_criteria": ["3.2.6"],
|
|
"name": "일관된 도움 제공",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"3.6.2": {
|
|
"wcag_criteria": ["3.3.8"],
|
|
"name": "접근 가능한 인증",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
"3.6.3": {
|
|
"wcag_criteria": ["3.3.7"],
|
|
"name": "중복 입력 방지",
|
|
"principle": "이해의 용이성",
|
|
"kwcag_21": False,
|
|
},
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Reverse mapping: WCAG criterion -> KWCAG criterion(s)
|
|
# Built at module load time for O(1) lookup during result conversion.
|
|
# =============================================================================
|
|
|
|
def _build_wcag_to_kwcag_map() -> dict[str, list[str]]:
|
|
"""Build reverse mapping from WCAG criterion to KWCAG criterion(s)."""
|
|
reverse_map: dict[str, list[str]] = {}
|
|
for kwcag_id, info in KWCAG_TO_WCAG.items():
|
|
for wcag_id in info["wcag_criteria"]:
|
|
if wcag_id not in reverse_map:
|
|
reverse_map[wcag_id] = []
|
|
reverse_map[wcag_id].append(kwcag_id)
|
|
return reverse_map
|
|
|
|
|
|
WCAG_TO_KWCAG: dict[str, list[str]] = _build_wcag_to_kwcag_map()
|
|
|
|
|
|
# =============================================================================
|
|
# axe-core tag sets for KWCAG versions
|
|
# =============================================================================
|
|
# KWCAG 2.2 -> WCAG 2.1 AA level + selected 2.2 AA rules
|
|
# KWCAG 2.1 -> WCAG 2.0 AA level + selected 2.1 AA rules
|
|
|
|
_KWCAG_AXE_TAGS: dict[str, list[str]] = {
|
|
"kwcag_2.1": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
|
|
"kwcag_2.2": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"],
|
|
}
|
|
|
|
|
|
def get_kwcag_axe_tags(version: str) -> list[str]:
|
|
"""
|
|
Get axe-core tag list for a KWCAG version.
|
|
|
|
Args:
|
|
version: KWCAG version string, e.g. "kwcag_2.1" or "kwcag_2.2"
|
|
|
|
Returns:
|
|
List of axe-core tags to run.
|
|
"""
|
|
return _KWCAG_AXE_TAGS.get(version, _KWCAG_AXE_TAGS["kwcag_2.2"])
|
|
|
|
|
|
# =============================================================================
|
|
# Utility functions for result conversion
|
|
# =============================================================================
|
|
|
|
def convert_wcag_issue_to_kwcag(
|
|
issue_dict: dict,
|
|
kwcag_version: str = "kwcag_2.2",
|
|
) -> dict:
|
|
"""
|
|
Convert a single issue's wcag_criterion field from WCAG to KWCAG numbering.
|
|
|
|
If the WCAG criterion maps to a KWCAG item, the issue is updated with:
|
|
- kwcag_criterion: The KWCAG criterion number (e.g. "1.1.1")
|
|
- kwcag_name: The Korean name of the KWCAG item
|
|
- kwcag_principle: The KWCAG principle name
|
|
The original wcag_criterion field is preserved.
|
|
|
|
If no mapping exists, the issue is returned unchanged.
|
|
|
|
Args:
|
|
issue_dict: Dictionary representing a single inspection issue.
|
|
kwcag_version: "kwcag_2.1" or "kwcag_2.2"
|
|
|
|
Returns:
|
|
The issue_dict with KWCAG fields added (mutated in place and returned).
|
|
"""
|
|
wcag_criterion = issue_dict.get("wcag_criterion")
|
|
if not wcag_criterion:
|
|
return issue_dict
|
|
|
|
kwcag_ids = WCAG_TO_KWCAG.get(wcag_criterion, [])
|
|
if not kwcag_ids:
|
|
return issue_dict
|
|
|
|
# Filter to items that exist in the requested KWCAG version
|
|
is_21 = kwcag_version == "kwcag_2.1"
|
|
valid_ids = []
|
|
for kid in kwcag_ids:
|
|
info = KWCAG_TO_WCAG.get(kid)
|
|
if info is None:
|
|
continue
|
|
if is_21 and not info.get("kwcag_21", False):
|
|
continue
|
|
valid_ids.append(kid)
|
|
|
|
if not valid_ids:
|
|
return issue_dict
|
|
|
|
# Use the first matching KWCAG criterion
|
|
primary_kwcag_id = valid_ids[0]
|
|
info = KWCAG_TO_WCAG[primary_kwcag_id]
|
|
|
|
issue_dict["kwcag_criterion"] = primary_kwcag_id
|
|
issue_dict["kwcag_name"] = info["name"]
|
|
issue_dict["kwcag_principle"] = info["principle"]
|
|
|
|
# If multiple KWCAG items match, include all
|
|
if len(valid_ids) > 1:
|
|
issue_dict["kwcag_criteria_all"] = valid_ids
|
|
|
|
return issue_dict
|
|
|
|
|
|
def get_kwcag_label(version: str) -> str:
|
|
"""
|
|
Get human-readable label for a KWCAG version.
|
|
|
|
Args:
|
|
version: "kwcag_2.1" or "kwcag_2.2"
|
|
|
|
Returns:
|
|
Korean label string, e.g. "KWCAG 2.2 (한국형 웹 콘텐츠 접근성 지침 2.2)"
|
|
"""
|
|
labels = {
|
|
"kwcag_2.1": "KWCAG 2.1 (한국형 웹 콘텐츠 접근성 지침 2.1)",
|
|
"kwcag_2.2": "KWCAG 2.2 (한국형 웹 콘텐츠 접근성 지침 2.2)",
|
|
}
|
|
return labels.get(version, labels["kwcag_2.2"])
|
|
|
|
|
|
def get_kwcag_item_count(version: str) -> int:
|
|
"""
|
|
Get the total number of inspection items for a KWCAG version.
|
|
|
|
Args:
|
|
version: "kwcag_2.1" or "kwcag_2.2"
|
|
|
|
Returns:
|
|
Number of items: 24 for KWCAG 2.1, 33 for KWCAG 2.2
|
|
"""
|
|
if version == "kwcag_2.1":
|
|
return sum(1 for info in KWCAG_TO_WCAG.values() if info.get("kwcag_21", False))
|
|
return len(KWCAG_TO_WCAG)
|