웹사이트 히어로 캐러셀: 사진이 교대로 전환되며, 그 정직성을 지키는 테스트들
페이지 상단의 사진 공간으로, 두 장 이상의 사진이 몇 초마다 교대로 전환되며 각 사진마다 도트가 표시됩니다. 쉽게 보이지만 실제로는 까다롭습니다. 한 사진의 캡션이 변경될 때마다 페이지가 점프하고, 흰색 도트가 옅은 배경의 사진에서 사라지며, 휴대폰에서는 마우스보다 더 큰 터치 대상이 필요하고, 움직이는 콘텐츠는 자체적인 접근성 규칙을 따릅니다. 이 스킬은 컴포넌트와, 더 중요한 것은 이러한 모든 문제를 잡아내는 테스트를 함께 제공합니다.
이렇게 만든 이유 (소유자에게 자신의 말로 설명하세요)
- 페이지가 절대 점프하지 않습니다. 모든 사진이 하나의 공간을 공유하며, 하나의 형태로 잘리므로 첫 번째 사진과 마지막 사진의 페이지 높이가 동일합니다. 사진에 캡션이나 크레딧이 없습니다. 이러한 요소는 이 캐러셀이 가져온 원본 사이트에서 페이지를 움직이게 했던 원인이었습니다. 크레딧은 사이트의 이미지 크레딧 페이지에 속합니다.
- 도트는 모든 사진에서 읽을 수 있습니다. 작고 어두운 알약 모양의 배경 위에 있어 옅은 하늘에서도, 어두운 벽에서도 선명하게 보입니다. 테스트는 옅은 사진과 어두운 사진의 픽셀을 측정하여 이를 검증합니다.
- JavaScript 없이도 작동합니다. 사진이 세로로 나열되며 버튼은 표시되지 않습니다.
- 정중하게 동작합니다. 마우스가 위에 있을 때, 탭이 숨겨졌을 때, 키보드 포커스가 진입했을 때, 그리고 처음부터 움직임을 줄이도록 설정한 방문자에게는 작동을 멈춥니다.
- 저장하거나 전송하는 것이 없습니다. 쿠키, 스토리지, 다른 사이트로의 요청이 없습니다.
그리고 솔직한 약점도 있으니, 소유자가 다른 사람이 아닌 당신에게서 먼저 듣게 하세요:
- 회전하는 사진이 항상 더 나은 디자인은 아닙니다. 단일 강력한 사진이 더 차분하고, 로딩이 빠르며 누구도 짜증나게 하지 않습니다. 그렇게 말하세요. 소유자가 결정합니다.
- 사진 한 장 추가는 방문자의 휴대폰 다운로드 시간을 증가시킵니다. 각각 네 가지 크기로 제공되는 세 장의 사진은 괜찮지만, 열 장은 그렇지 않습니다. 몇 장으로 유지하세요.
- "도트" 일시 정지 모드는 보이는 컨트롤을 깨끗한 사진과 맞바꿉니다 (아래 "일시 정지" 섹션 참조).
0. 이것이 올바른 도구인가요? (설치 전에 물어보세요)
- 소유자가 회전을 확실히 원하는가요? 사진 한 장이라면 이 모든 것이 필요 없습니다. 한 번, 명확하게 물어보세요.
- 사진은 몇 장이며, 모두 같은 모양인가요? 두 장에서 다섯 장까지. 어떤 모양이든 작동합니다 (각각 캐러셀의 모양으로 잘립니다). 하지만 피사체가 가장자리에 있는 사진은 자르기에서 손실됩니다: 각 사진을 캐러셀 모양(기본 16/9)과 휴대폰 모양으로 확인하세요.
- 사진이 어디로 연결되나요? 모든 슬라이드가 페이지(예: 프로젝트)로 연결될 수 있습니다. 연결된 슬라이드와 연결되지 않은 슬라이드를 혼합해도 괜찮습니다 (CI 페이지에서 그렇게 합니다).
- 어떤 일시 정지 모드를 사용하나요? 아래 "일시 정지"를 소유자에게 읽어주고 선택하게 하세요. 기본값: 버튼.
1. 사이트에 추가되는 것
| 파일 | 이 스킬의 templates/ 출처 | 설명 |
|---|---|---|
src/components/HeroCarousel.astro | HeroCarousel.astro | 마크업, 도트, 스타일 |
src/components/hero-carousel-text.ts | hero-carousel-text.ts | 영어와 독일어로 표시되는 텍스트 |
public/js/hero-carousel.js | hero-carousel.js | 스크립트: 슬라이드를 한 곳에 배치, 회전, 컨트롤 처리 |
tests/hero-carousel.spec.ts | hero-carousel.spec.ts | 방문자가 만나는 캐러셀 테스트 (아래 목록 참조) |
네 파일을 모두 설치하세요. 템플릿에는 이미지 파일이 없습니다: 사진은 사이트 자체의 것으로, props로 전달됩니다.
한 문단으로 설명하는 작동 방식. 서버는 모든 슬라이드를 세로로 나열하고 컨트롤은 숨긴 상태로 보냅니다. 스크립트(인라인 스크립트가 아닌 public/js/ 아래의 사이트 자체 파일로, 엄격한 script-src 'self' 정책에서 실행 가능)는 슬라이드를 하나의 그리드 셀에 배치하고, 컨트롤을 표시하며 현재 표시 중인 슬라이드를 표시합니다. 현재 슬라이드는 데이터 속성으로 표시되며, CSS가 나머지를 처리합니다 (1초 페이드 포함). 슬라이드는 이미지가 완전히 로드된 경우에만 표시되므로, 늦게 로드되거나 실패한 사진이 빈 공간을 남기지 않습니다.
스펙이 확인하는 사항 (방문자 관점, 실제 페이지에서): 마크업과 각 도트의 이름; 고정된 시계에서 밀리초 단위의 간격; 일시 정지 버튼 또는 도트가 중단시키는 동작, 레이블 및 라이브 리전; 키보드 포커스 (중단, 중단 유지, 재개); 포인터가 잡고 있을 때와 떠난 후 새 간격; 축소된 움직임; 숨겨진 탭; 늦게 로드되거나 실패한 두 번째 이미지; JavaScript 없이 페이지; 1280, 768 및 390px에서 높이 점프 없음; 표시되지 않는 슬라이드가 키보드나 접근성 트리에서 도달 불가능함; 사진 링크가 작동하고 도트가 링크를 열지 않음; 32px 타겟 (터치 스크린에서는 44px); 모든 사진 위의 알약 배경에 대한 도트를 렌더링된 픽셀로 읽음; 강제 색상; 두 테마에서 axe; 저장되거나 전송되는 것 없음; 스크립트는 파일이지 인라인이 아님; 페이드는 중간 프레임에서 실행되고 안정되며, 축소된 움직임에서는 실행되지 않음.
2. 설치 (브랜치에서 풀 리퀘스트로: AGENTS.md §2)
- 네 파일을 §1에 명시된 위치로 복사하세요 (
mkdir -p public/js먼저 실행). - 캐러셀을 페이지 상단 근처, 해당 페이지의
<Base>내부에 배치하세요:--- import HeroCarousel from '../components/HeroCarousel.astro'; import first from '../assets/hero-1.jpg'; import second from '../assets/hero-2.jpg'; --- <HeroCarousel label="Photos of our work" slides={[ { image: first, alt: 'What the first photo shows', label: 'Short name', href: '/projects/one' }, { image: second, alt: 'What the second shows', label: 'Another name' }, ]} />label: 스크린 리더를 위한 캐러셀 설명. 필수.- 각 슬라이드:
image(가져온 사진),alt(내용을 설명하는 문장, 최소 10자: 미만이면 빌드 중단),label(도트의 짧은 이름),href(선택 사항). pause:"button"(기본값) 또는"dots".swap:"instant"(기본값) 또는"fade".intervalMs: 기본 6000, 최소 2000.ratio: 기본"16 / 9".lang: 페이지의 언어. 사진: JPEG 또는 PNG, 약 2,400px 너비, 페이지에 필요한 만큼만 유지 (빌드에서 각각 네 가지 크기로 AVIF 및 WebP 폴백 생성). 첫 번째 사진이 먼저 로드됨 (높은 우선순위); 나머지는 낮은 우선순위로 로드되며 위치를 유지합니다.
- 언어. 영어와 독일어가 내장되어 있으며, 컴포넌트는 페이지의 언어를 받습니다 (
Base.astro가Astro.locals.lang으로 전달). 다른 언어: 먼저 아래 "언어 추가"를 참조하세요. tests/hero-carousel.spec.ts에서PAGE(캐러셀이 있는 페이지),PAUSE및SWAP를 페이지가 전달하는 값으로 설정하세요 (컴포넌트 기본값은button과instant입니다. 스펙은 명시적으로 작성되어야 합니다). 하나라도 비어 있으면 스펙이 실패합니다.LANG은 페이지가lang을 전달하는 경우에만 설정하세요. 대비 테스트용 사진: 스펙은 캐러셀의 실제 사진을 읽으므로 소유자의 사진을 사용합니다. 모든 사진이 어두운 경우 "알약 위 흰색 도트" 테스트는 실패할 수 없습니다. 실패하는 모습을 보려면 한 번 밝은 사진을 수동으로 확인하세요 (§3의 변경 확인에서 설명).npm run check && npm test실행.
일시 정지 (소유자가 선택하기 전에 읽어주세요)
자체적으로 시작되고 5초 이상 지속되며 다른 콘텐츠 옆에 있는 움직이는 콘텐츠는 WCAG 2.2.2, 레벨 A에 해당하며, "사용자가 일시 중지, 중지 또는 숨길 수 있는 메커니즘"을 요구합니다 (https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html 참조). 동일한 페이지에서 일시 중지는 "사용자 요청에 의해 중단되고 사용자가 요청할 때까지 재개되지 않음"으로 정의하며, 메커니즘이 가시적이어야 한다고 명시하지 않습니다. 슬라이드 표시기에 대해서는 논의하지 않습니다.
pause="button"(기본값). 도트 옆에 있는 일시 정지 버튼 (중단 시 재개). 가시적이고, 명명되었으며, 논란의 여지가 없는 방식이며 방문자가 찾는 방식입니다. 대가: 사진 위에 작은 단어가 표시됩니다.pause="dots". 버튼 없음. 도트를 선택하면 해당 사진이 표시되고 방문자가 새로고침할 때까지 회전이 중단됩니다. 키보드 포커스가 진입하면 중단되며, 축소된 움직임 선호 시 처음부터 중단된 상태로 시작됩니다. 이는 2.2.2의 의미에서 메커니즘일 가능성이 있지만, 방문자에게 도트가 중단시킨다는 것을 알리는 것이 없으며, 재개 방법도 없습니다. 이 모드를 처음 사용한 사이트는 더 깨끗한 사진을 위해 이 방식을 선택했으며, 그 점을 인지하고 있었습니다.
이 스킬은 적합성 결론을 내리지 않습니다. "dots" 모드가 허용되는지 여부는 사이트 자체 감사자의 판단에 따릅니다. 소유자가 이 모드를 선택하면 사이트 문서에 한 줄과 날짜를 기록하세요.
언어 추가
해당 언어의 새 사이트에서, 또는 나중에 사이트가 해당 언어를 추가할 때.
- 먼저 사이트의 어조를 읽으세요 (
CONTENT_GUIDE.md; 독일어에서는 존칭 "du" 또는 "Sie"). 텍스트는 몇 단어에 불과합니다: 언어가 허용하는 경우, 아무도 지칭하지 않도록 작성하세요. src/components/hero-carousel-text.ts에서TEXT의en항목을 언어의 두 글자 코드(fr-CA가 아닌fr)로 복사하고 모든 값을 번역하세요. 하나라도 누락되면npm run check가 실패합니다.- 페이지와 테스트를 §2의 3단계와 4단계처럼 설정하세요.
3. 한 번 시도해보세요 (소유자), 그리고 이 스킬 뒤의 검증
npm run build && npm run preview 후, 또는 풀 리퀘스트의 미리보기 주소에서 소유자는:
- 기다립니다: 사진이 자동으로 변경되고, 표시 중인 도트가 따라갑니다.
- 마우스를 위로 가져갑니다: 중단됩니다. 마우스를 치웁니다: 전체 간격 후에 다시 시작됩니다.
- 휴대폰에서: 도트를 탭합니다 (해당 사진 표시); 가로 스크롤이 없습니다; 도트를 쉽게 누를 수 있습니다.
- 버튼 모드에서: 일시 정지를 누르고, 재개로 변경되는 것을 확인한 후 다시 누릅니다.
이 스킬이 구축된 검증 사항으로, 템플릿 변경을 평가할 수 있습니다: 스펙은 각각에 대해 하나의 테스트를 포함하며, 보호하는 항목이 제거되면 실패합니다. 템플릿을 변경할 때 한 번 수동으로 실행하세요 (CI 작업은 새 사이트에 설치하고 사용 중인 설정의 세 가지 조합 모두에서 스펙을 실행합니다):
HeroCarousel.astro에서 제거 | 실패하는 테스트 |
|---|---|
그리드 규칙 (display: grid, grid-area) | "슬라이드가 한 공간을 공유합니다…" |
사진의 aspect-ratio | 동일한 테스트 (4:3 사진이 더 큼) |
알약의 background | "모든 사진 위에 렌더링됩니다…" (옅은 사진에서 흰색은 약 1:1) |
표시 중인 도트의 border-width: 5px | 강제 색상 테스트 |
transition 규칙 | 페이드 테스트 ("중간 프레임") |
표시되지 않는 슬라이드의 pointer-events: none | "사진 링크가 페이지로 연결됩니다…" (페이드 아웃 중인 슬라이드가 클릭을 가로챔) |
표시되지 않는 슬라이드에 스크립트가 설정하는 inert 및 aria-hidden (hero-carousel.js) | Tab 키 및 접근성 트리 테스트 (사라지는 슬라이드의 링크가 페이드 중에 접근 가능하고 읽을 수 있음) |
스크립트의 recover() (표시 중인 실패한 사진을 현재 또는 나중에 도착하는 다른 첫 번째 사진으로 교체) | "첫 번째 이미지가 실패합니다…" |
4. 개인정보 페이지와 제거 방법
캐러셀은 아무것도 저장하거나 전송하지 않습니다: 개인정보 페이지에 추가할 내용 없음.
제거하려면: 네 파일을 삭제하고, 페이지에서 <HeroCarousel> 블록 및 import HeroCarousel 라인 (해당 캐러셀에서만 사용된 사진 import 포함) 을 삭제한 후 사진 하나를 다시 배치하세요. 다른 내용이 이를 참조하지 않습니다. PAGES 기반 스위트만 해당 페이지를 확인합니다.
5. 이 스킬에 포함되지 않은 것 (그러므로 아무도 가정하지 않음)
이전/다음 화살표, 스와이프 제스처, 사진 캡션, 사진 크레딧, 비디오 슬라이드, 페이지 내 여러 캐러셀 (ID가 고정됨), 이후 사진의 지연 로딩. 각각 나중에 추가할 수 있습니다. 첫 번째 버전에는 필요하지 않습니다.