113 lines
6.4 KiB
Markdown
113 lines
6.4 KiB
Markdown
# 설계
|
||
|
||
## 데이터와 출제
|
||
|
||
`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는 편집상 예상 혼동 순서이며 검증된 청각 척도가
|
||
아니다. 모든 문항에 서로 다른 세 변형을 포함한다. 음량 단계는 이 값과 분리된다.
|
||
|
||
## 게임 상태
|
||
|
||
```mermaid
|
||
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점,
|
||
현재 연속 정답을 초기화한다. 최고 연속 정답과 난이도별 정답 수는 유지한다.
|
||
|
||
## 오디오
|
||
|
||
`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을 보장하는 수치는 아니다.
|
||
|
||
```text
|
||
배경 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의 장치별 정책까지
|
||
증명하지 않으므로 인수 시 직접 청취를 추가한다.
|