대비비 저장 게이트 + 브랜드 컬러 이미지 추출 + 네트워크 단절 대응
WCAG 대비비를 저장 게이트로 (FE-7)
lib/theme.ts에 있던 대비비 근사식(lum(L) = L³, 채도·색상은 무시)을 culori의 wcagContrast로 교체했다 — 실제 oklch 색을 sRGB로 변환해서 정확한 WCAG 대비비를 계산한다. contrastAllPass(preset, mode)로 게이트 판정을 모아서, "공개하기" 버튼과 뷰어 아이콘 둘 다 이 함수로 가드하고 미달이면 버튼을 비활성화 + 사유를 노출한다.
기존 내장 프리셋 5개는 실측으로도 전부 여유 있게 통과해서 회귀는 없었다. 이 저장소 첫 자동 테스트로 Vitest를 도입해서, 5프리셋×2모드 통과 회귀와 인위적 고채도 조합의 실패 케이스(4.48:1, 기준 4.5:1 근소 미달)를 검증했다.
브랜드 컬러를 이미지에서 추출 (FE-8)
브랜드 프리셋이 하드코딩된 5개뿐이라 그 브랜드가 아니면 고를 수 없었다. lib/colorExtract.ts로 이미지를 양자화 히스토그램(무채색 버킷 제외)으로 샘플링해서 주조색을 뽑고 culori로 OKLCH {h,c}로 변환한다.
Preset 타입을 theme.ts에서 types.ts로 옮겨 4개 파일에 흩어져 있던 PRESETS.find(...) 반복을 StudioProvider의 단일 소스로 정리했다. ThemeSection에 "+ 이미지에서 추출" 타일을 추가해서, 추출 성공 시 스와치·라벨·채도 슬라이더·실시간 대비비 배지가 뜨는 미리보기 패널을 보여주고, FE-7의 contrastAllPass가 참일 때만 저장할 수 있게 했다.
실측해보니 실제 sRGB에서 나올 수 있는 최고 채도급 색도 derive() 파이프라인의 넉넉한 명도 계층 덕에 항상 대비비를 통과했다 — 미달 UI는 실제 사진으로는 거의 트리거되지 않지만, FE-7에서 검증된 동일 함수를 재사용하는 구조라 기능 자체는 정상 동작한다.
네트워크 단절 대응, 범위를 줄이다 (FE-9)
착수 전에 확인해보니 이 저장소엔 fetch나 폴링이 아예 없었다 — Q&A/설문 연결(FE-3)이 아직 없어서 "폴링이 끊기는 상황" 자체가 존재하지 않았다. 팀원과 상의해서 온라인/오프라인 감지 인프라만 먼저 만들고, 실제 폴링 실패 처리는 나중에 FE-3에서 이 훅을 재사용하는 것으로 범위를 좁혔다.
useOnlineStatus.ts는 ThemeToggle이 이미 쓰던 useSyncExternalStore 패턴으로 navigator.onLine과 online/offline 이벤트를 구독한다. Microsite의 Q&A/설문 버튼에 연결해서 오프라인이면 비활성 스타일 + 사유 배너를 보여주고, 복귀 시 이벤트 기반으로 자동 재개하도록 했다.
하루 정리
FE-1·FE-2·FE-7·FE-8·FE-9까지, 리팩터부터 접근성·견고성까지 하루에 5개 작업을 마쳤다. 착수 전 저장소 상태를 먼저 확인하고 범위를 줄인 FE-9가 특히 기억에 남는다 — 없는 문제를 풀려고 하지 않는 것도 판단이다.