{ "$schema": "E2E Accessibility Testing Configuration", "version": "1.0.0", "description": "WCAG 기반 접근성 테스트 설정", "lastUpdated": "2026-01-31", "accessibility": { "enabled": true, "standard": "WCAG 2.1", "level": "AA", "runOnEveryPage": true, "failOnViolation": false }, "wcagLevels": { "A": { "description": "최소 접근성 수준", "required": true, "criteria": 30 }, "AA": { "description": "권장 접근성 수준 (법적 기준)", "required": true, "criteria": 20 }, "AAA": { "description": "최고 접근성 수준", "required": false, "criteria": 28 } }, "categories": { "perceivable": { "description": "인식의 용이성", "checks": [ "imageAltText", "colorContrast", "textResize", "audioVideoAlternatives", "contentStructure" ] }, "operable": { "description": "운용의 용이성", "checks": [ "keyboardAccessible", "focusVisible", "skipLinks", "pageTitle", "linkPurpose", "noKeyboardTrap" ] }, "understandable": { "description": "이해의 용이성", "checks": [ "languageOfPage", "consistentNavigation", "errorIdentification", "labels", "errorSuggestion" ] }, "robust": { "description": "견고성", "checks": [ "validHTML", "ariaUsage", "nameRoleValue", "statusMessages" ] } }, "rules": { "imageAltText": { "id": "image-alt", "description": "이미지에 대체 텍스트 필수", "wcag": "1.1.1", "level": "A", "impact": "critical", "selector": "img", "check": "hasAltAttribute" }, "colorContrast": { "id": "color-contrast", "description": "텍스트와 배경의 명도 대비", "wcag": "1.4.3", "level": "AA", "impact": "serious", "ratios": { "normalText": 4.5, "largeText": 3, "uiComponents": 3 } }, "keyboardAccessible": { "id": "keyboard", "description": "키보드로 모든 기능 사용 가능", "wcag": "2.1.1", "level": "A", "impact": "critical", "check": "focusableElements" }, "focusVisible": { "id": "focus-visible", "description": "포커스 표시 명확", "wcag": "2.4.7", "level": "AA", "impact": "serious", "check": "hasFocusIndicator" }, "formLabels": { "id": "label", "description": "폼 요소에 레이블 연결", "wcag": "1.3.1", "level": "A", "impact": "critical", "selector": "input, select, textarea", "check": "hasAssociatedLabel" }, "linkPurpose": { "id": "link-name", "description": "링크 목적 명확", "wcag": "2.4.4", "level": "A", "impact": "serious", "selector": "a[href]", "check": "hasAccessibleName" }, "headingOrder": { "id": "heading-order", "description": "제목 순서 논리적", "wcag": "1.3.1", "level": "A", "impact": "moderate", "check": "sequentialHeadings" }, "ariaValid": { "id": "aria-valid", "description": "ARIA 속성 올바른 사용", "wcag": "4.1.2", "level": "A", "impact": "critical", "check": "validAriaAttributes" }, "buttonName": { "id": "button-name", "description": "버튼에 접근 가능한 이름", "wcag": "4.1.2", "level": "A", "impact": "critical", "selector": "button, [role='button']", "check": "hasAccessibleName" }, "tabIndex": { "id": "tabindex", "description": "tabindex 올바른 사용", "wcag": "2.4.3", "level": "A", "impact": "serious", "check": "validTabIndex" } }, "scripts": { "runAccessibilityAudit": "(async function() { const issues = []; const checks = { images: [], forms: [], buttons: [], links: [], headings: [], aria: [], focus: [], contrast: [] }; document.querySelectorAll('img').forEach(img => { if (!img.alt && !img.getAttribute('aria-label') && !img.getAttribute('aria-labelledby')) { checks.images.push({ element: 'img', src: img.src?.substring(0, 50), issue: 'Missing alt text', wcag: '1.1.1', impact: 'critical' }); } }); document.querySelectorAll('input, select, textarea').forEach(input => { const id = input.id; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby'); if (!hasLabel && !hasAriaLabel && input.type !== 'hidden' && input.type !== 'submit' && input.type !== 'button') { checks.forms.push({ element: input.tagName.toLowerCase(), type: input.type, issue: 'Missing label', wcag: '1.3.1', impact: 'critical' }); } }); document.querySelectorAll('button, [role=\"button\"]').forEach(btn => { const hasName = btn.innerText?.trim() || btn.getAttribute('aria-label') || btn.getAttribute('title'); if (!hasName) { checks.buttons.push({ element: 'button', issue: 'Missing accessible name', wcag: '4.1.2', impact: 'critical' }); } }); document.querySelectorAll('a[href]').forEach(link => { const hasName = link.innerText?.trim() || link.getAttribute('aria-label'); if (!hasName) { checks.links.push({ element: 'a', href: link.href?.substring(0, 30), issue: 'Missing link text', wcag: '2.4.4', impact: 'serious' }); } }); const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; headings.forEach(h => { const level = parseInt(h.tagName[1]); if (level > lastLevel + 1 && lastLevel !== 0) { checks.headings.push({ element: h.tagName, issue: `Skipped heading level (h${lastLevel} to h${level})`, wcag: '1.3.1', impact: 'moderate' }); } lastLevel = level; }); const summary = { total: 0, critical: 0, serious: 0, moderate: 0, minor: 0 }; Object.values(checks).forEach(arr => { arr.forEach(issue => { summary.total++; summary[issue.impact] = (summary[issue.impact] || 0) + 1; }); }); return { url: window.location.href, timestamp: new Date().toISOString(), summary, checks, passed: summary.critical === 0 }; })()", "checkColorContrast": "(function() { const results = []; const textElements = document.querySelectorAll('p, span, a, button, label, h1, h2, h3, h4, h5, h6, li, td, th'); const sampleSize = Math.min(textElements.length, 50); for (let i = 0; i < sampleSize; i++) { const el = textElements[i]; const style = window.getComputedStyle(el); const color = style.color; const bgColor = style.backgroundColor; if (color && bgColor && bgColor !== 'rgba(0, 0, 0, 0)') { results.push({ element: el.tagName, text: el.innerText?.substring(0, 20), color, bgColor }); } } return { checked: sampleSize, results: results.slice(0, 10) }; })()", "checkKeyboardNavigation": "(async function() { const focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'); const results = { total: focusable.length, withFocusStyle: 0, tabbable: 0, issues: [] }; focusable.forEach(el => { const tabIndex = el.getAttribute('tabindex'); if (tabIndex === null || parseInt(tabIndex) >= 0) results.tabbable++; el.focus(); const style = window.getComputedStyle(el); if (style.outlineWidth !== '0px' || style.boxShadow !== 'none') { results.withFocusStyle++; } else { results.issues.push({ element: el.tagName, text: el.innerText?.substring(0, 20) || el.placeholder, issue: 'No visible focus indicator' }); } }); return results; })()", "checkAriaUsage": "(function() { const ariaElements = document.querySelectorAll('[aria-label], [aria-labelledby], [aria-describedby], [role]'); const results = { total: ariaElements.length, valid: 0, issues: [] }; ariaElements.forEach(el => { const role = el.getAttribute('role'); const ariaLabelledby = el.getAttribute('aria-labelledby'); const ariaDescribedby = el.getAttribute('aria-describedby'); if (ariaLabelledby) { const labelEl = document.getElementById(ariaLabelledby); if (!labelEl) { results.issues.push({ element: el.tagName, issue: `aria-labelledby references non-existent id: ${ariaLabelledby}` }); } } if (ariaDescribedby) { const descEl = document.getElementById(ariaDescribedby); if (!descEl) { results.issues.push({ element: el.tagName, issue: `aria-describedby references non-existent id: ${ariaDescribedby}` }); } } if (!results.issues.find(i => i.element === el.tagName)) results.valid++; }); return results; })()", "checkFormAccessibility": "(function() { const forms = document.querySelectorAll('form'); const results = { forms: forms.length, issues: [] }; forms.forEach((form, formIndex) => { const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { if (input.type === 'hidden' || input.type === 'submit' || input.type === 'button') return; const id = input.id; const name = input.name; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label'); const hasPlaceholder = input.placeholder; if (!hasLabel && !hasAriaLabel) { results.issues.push({ form: formIndex + 1, element: input.tagName, type: input.type, name: name || id, issue: 'No associated label', suggestion: hasPlaceholder ? 'Has placeholder but no label' : 'Add label element or aria-label' }); } }); const submitBtn = form.querySelector('button[type=\"submit\"], input[type=\"submit\"]'); if (!submitBtn) { results.issues.push({ form: formIndex + 1, issue: 'No submit button found' }); } }); return results; })()", "generateAccessibilityReport": "(function() { const audit = { url: window.location.href, timestamp: new Date().toISOString(), images: { total: 0, withAlt: 0, issues: [] }, forms: { total: 0, labeled: 0, issues: [] }, buttons: { total: 0, named: 0, issues: [] }, links: { total: 0, named: 0, issues: [] }, headings: { structure: [], issues: [] }, landmarks: [], focusable: 0 }; document.querySelectorAll('img').forEach(img => { audit.images.total++; if (img.alt || img.getAttribute('aria-label')) audit.images.withAlt++; else audit.images.issues.push({ src: img.src?.split('/').pop() }); }); document.querySelectorAll('input:not([type=hidden]), select, textarea').forEach(el => { audit.forms.total++; const hasLabel = (el.id && document.querySelector(`label[for='${el.id}']`)) || el.getAttribute('aria-label'); if (hasLabel) audit.forms.labeled++; else audit.forms.issues.push({ type: el.type, name: el.name }); }); document.querySelectorAll('button, [role=button]').forEach(btn => { audit.buttons.total++; if (btn.innerText?.trim() || btn.getAttribute('aria-label')) audit.buttons.named++; else audit.buttons.issues.push({ class: btn.className?.substring(0, 30) }); }); document.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(h => { audit.headings.structure.push({ level: h.tagName, text: h.innerText?.substring(0, 30) }); }); audit.landmarks = Array.from(document.querySelectorAll('header, nav, main, aside, footer, [role=banner], [role=navigation], [role=main], [role=complementary], [role=contentinfo]')).map(el => el.tagName.toLowerCase() + (el.getAttribute('role') ? `[role=${el.getAttribute('role')}]` : '')); audit.focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])').length; const score = Math.round(((audit.images.withAlt / Math.max(audit.images.total, 1)) + (audit.forms.labeled / Math.max(audit.forms.total, 1)) + (audit.buttons.named / Math.max(audit.buttons.total, 1))) / 3 * 100); return { ...audit, score, grade: score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 50 ? 'C' : 'F' }; })()" }, "impactLevels": { "critical": { "description": "스크린 리더 사용자가 콘텐츠 접근 불가", "action": "즉시 수정", "examples": ["이미지 alt 누락", "폼 레이블 누락", "키보드 접근 불가"] }, "serious": { "description": "접근성 심각한 장애", "action": "우선 수정", "examples": ["색상 대비 부족", "포커스 표시 없음", "링크 텍스트 없음"] }, "moderate": { "description": "사용성 저하", "action": "개선 권장", "examples": ["제목 순서 불규칙", "랜드마크 누락"] }, "minor": { "description": "모범 사례 미준수", "action": "선택적 개선", "examples": ["중복 ID", "빈 링크"] } }, "reporting": { "includeInTestReport": true, "separateReport": true, "format": { "summary": { "score": "접근성 점수 (0-100)", "grade": "등급 (A/B/C/F)", "violations": "위반 사항 수", "passed": "통과 항목 수" }, "detail": { "rule": "위반 규칙", "impact": "영향도", "wcag": "WCAG 기준", "element": "해당 요소", "suggestion": "수정 제안" } }, "template": { "header": "## ♿ 접근성 테스트 결과", "sections": { "summary": "### 요약", "violations": "### 위반 사항", "passed": "### 통과 항목", "recommendations": "### 개선 권장" } } }, "thresholds": { "pass": { "score": 70, "critical": 0, "serious": 5 }, "warn": { "score": 50, "critical": 3, "serious": 10 } }, "pageSpecific": { "login": { "critical": ["formLabels", "buttonName", "colorContrast"], "focus": ["keyboardAccessible", "focusVisible"] }, "dashboard": { "critical": ["imageAltText", "linkPurpose", "headingOrder"], "focus": ["ariaValid", "landmarks"] }, "form": { "critical": ["formLabels", "errorIdentification", "inputInstructions"], "focus": ["focusOrder", "errorSuggestion"] }, "table": { "critical": ["tableHeaders", "tableCaption"], "focus": ["tableSummary"] } }, "automation": { "runOn": { "pageLoad": true, "formSubmit": false, "modalOpen": true }, "skipElements": [ "[aria-hidden='true']", "[hidden]", ".sr-only", ".visually-hidden" ] }, "recommendations": { "imageAltText": "모든 이미지에 alt 속성 추가. 장식용 이미지는 alt='' 사용", "formLabels": "