7.2 KiB
Find-Word v0.1.1 설계
데이터와 출제
src/content.ts는 패턴, 질문, 단어, 오답 규칙, 배경 프리셋, 상대 dB를 서로 다른
배열로 보관한다. renderSentence가 각 패턴의 {word}를 정답으로,
{distractor1}·{distractor2}를 해당 보기의 오답으로 채운다. 같은 함수를
음성 생성기도 사용하며 런타임 문장과 WAV의 대응은 manifest 테스트가 검증한다.
Pattern.mechanic은 다음 10개 고유 청취 연산을 나타낸다. 인용 단어를 활용형으로
바꾸지 않으므로 동사·부사 표제어가 오답으로 나와도 문장이 자연스럽다.
| mechanic | 청취 과제 |
|---|---|
direct-target |
한 개의 목표 단어를 직접 식별 |
distractor-password |
연습 암호를 제외하고 진짜 암호 선택 |
ignore-then-remember |
앞 단어를 잊고 이후에 지시된 단어 기억 |
first |
세 단어 중 처음 선택 |
last |
세 단어 중 마지막 선택 |
middle |
세 단어 중 가운데 선택 |
repetition |
오답 사이에서 두 번 등장한 단어 찾기 |
correction |
처음 답을 취소하고 정정된 답 선택 |
conditional-cue |
초록/빨강 규칙과 현재 신호를 함께 적용 |
spatial-right-cue |
좌우 표지 중 오른쪽에 해당하는 단어 선택 |
방향 단서는 문장의 의미이며 stereo 위치 재현 과제는 아니다. 모든 음성은 mono다. 직접 식별을 제외한 문장은 실제 보기의 경쟁 단어도 발화하므로, 들린 단어 하나를 무조건 고르는 것으로 해결되지 않는다. 테스트는 100개 문장 전체에서 단어 순서, 목표 반복 횟수, 취소·조건·방향 단서와 질문 의도를 검증한다.
src/game.ts의 createQuestions는 seed 기반 PRNG와 Fisher–Yates로
유형·단어·정답 위치를 섞는다. 한 세션에서 유형과 정답 단어는 각각 한 번씩
나온다. 100개 문장 조합에 응답 방식·환경·난이도를 독립적으로 결합한다.
오답의 rule은 RULE_DIFFICULTY에 연결된다. 음절 교체 1, 초성 교체 2,
모음 교체 3, 받침 교체 4는 편집상 예상 혼동 순서이며 검증된 청각 척도가
아니다. 모든 문항에 서로 다른 세 변형을 포함한다. 음량 단계는 이 값과 분리된다.
게임 상태
stateDiagram-v2
[*] --> ready
ready --> loading: PLAY
loading --> playing: STARTED
loading --> ready: ERROR
playing --> ready: ERROR / 중단
playing --> answer: ENDED
answer --> loading: 다시 듣기
answer --> feedback: ANSWER
feedback --> ready: NEXT (다음 문제)
feedback --> results: NEXT (마지막 문제)
허용되지 않은 이벤트는 동일 상태를 반환한다. 재생 전/중에는 채점할 수 없고, 피드백 이후의 중복 답변·다음 클릭도 한 번만 반영된다. UI에서도 버튼 비활성화와 동기 ref 잠금을 함께 사용한다. 세션 토큰으로 처음 화면 복귀·unmount 뒤 완료된 Promise를 무시한다. 질문과 보기는 재생 완료 후 DOM에 생성되며 정답 문장은 채점 후에만 표시된다.
주관식은 NFC 한글 정규화 후 공백과 유니코드 문장부호만 제거한다. 비어 있는 답변과 부분 일치는 정답으로 인정하지 않는다. React 텍스트 노드로만 출력하여 입력을 HTML로 해석하지 않는다.
점수는 정답 시 100 × 단계 + 10 × min(연속 정답 - 1, 4)이다. 오답은 0점,
현재 연속 정답을 초기화한다. 최고 연속 정답과 난이도별 정답 수는 유지한다.
오디오
101개 저장 WAV는 edge-tts 7.3.1의 ko-KR-HyunsuMultilingualNeural로
빌드 전에 생성한다. 24kHz mono PCM16이며 원문·도구·해시는 manifest에 남긴다.
생성기는 임시 디렉터리에서 전체 검증 후 원자 교체한다. 생성 절차는
AUDIO_PROVENANCE에 있으며 앱은 provider에 연결하지 않는다.
src/audio/dsp.ts는 순수 수치 함수이며 장치가 없어도 테스트할 수 있다.
정수 seed 잡음에 저역 통과 필터, 사인파, 진폭 변조를 조합하고 양 끝을 페이드한다.
8개 프리셋은 실제 현장 녹음이 아니라 환경을 추상적으로 표현한 합성 신호다.
src/audio/engine.ts는 클릭 이벤트에서 AudioContext 생성·resume을 호출한 뒤
같은 출처의 WAV를 로드한다. mono PCM을 decode하고 음성과 배경의 전체 버퍼
RMS를 0.03으로 정규화한다. gain = 10^(dB/20)으로 목표 gain을 지정한다.
시간별·주파수별 청감이나 물리적 dB SPL을 보장하는 수치는 아니다.
src/audio/assets.ts는 음성 자산 버전과 URL 생성을 한 곳에서 관리한다.
intro와 목표 100개 모두 ?v=0.1.1을 붙여 동일 파일명에 남은 이전 캐시를
피한다. 파일을 다시 생성하면 제품 버전 유지 여부와 관계없이 자산 버전을
갱신한다. 버전 확인을 위한 외부 요청이나 런타임 manifest 요청은 없다.
배경 BufferSource → 배경 Gain (1, 시작/끝 fade) ─┐
목표 BufferSource → 음성 Gain (10^(dB/20)) ─────┼→ master Gain → destination
안내 BufferSource → 안내 Gain (1.8 → 0.12) ─────┘
첫 문제에서는 웃음과 안내 음성을 낮추어 배경에 묻히게 한 후 목표 문장을 재생한다. 이후에는 목표만 재생한다. 안내 도중 실패했으면 재시도 시 안내도 다시 재생한다. 기본 master는 35%이며 음량 조절은 두 신호의 비율을 유지한다.
동시 play는 거부한다. 정지·실패·완료는 onended 해제, 소스 stop/disconnect, AbortController 취소, 타이머 제거를 수행한다. load/decode 뒤에도 작업 토큰을 검사하여 취소된 이전 작업이 소리를 시작하지 못하게 한다. 준비 15초, 재생 길이 + 5초의 제한으로 무한 대기를 피한다. unmount는 context까지 닫는다.
화면·접근성·보안
App.tsx는 시작·플레이·결과 화면, 음량, 모드, 배경 선택을 담당한다.
정답/피드백/결과 제목으로 초점을 이동하고 결과표에 caption에 해당하는
접근성 이름과 scope를 둔다. 네이티브 radio/select/button/form으로 방향키,
Tab/Enter와 터치 조작을 지원한다. 장식 파형·캐릭터는 aria-hidden이며
reduced motion에서는 파형이 움직이지 않는다. 오류는 role=alert로 전달한다.
외부 폰트, CDN, 이미지, 분석 도구, 로그인, 쿠키, localStorage 사용이 없다. 개인 입력과 점수는 현재 React 메모리에만 존재한다. 단어·정답은 클라이언트 번들에 있으므로 개발자 도구를 통한 정답 열람을 막는 시험 시스템은 아니다.
검증 경계
Vitest는 실제 RED/GREEN으로 데이터·채점·상태·오디오 그래프·화면을 검증한다. Playwright는 production build와 실제 Chrome AudioContext로 두 크기의 10문제 완주·재청취·오류 복구·axe 검사를 수행한다. 테스트 자동화는 한국어 발음의 자연스러움, 실제 사용자의 청각 난이도, 모바일 Safari의 장치별 정책까지 증명하지 않으므로 인수 시 직접 청취를 추가한다.