Files
Find-Word/docs/ARCHITECTURE.md
2026-10-14 10:01:46 +00:00

8.9 KiB
Raw Permalink Blame History

Find-Word v0.2.3 설계

데이터와 출제

src/content.ts는 패턴, 질문, 단어, 오답 규칙, 현장음 URL, 상대 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로 유형·단어·정답 위치를 섞는다. 한 라운드는 3문항이며 유형과 정답 단어가 중복되지 않는다. 시작 단계는 1~5로 정규화하며 세 문항에 같은 level/db를 적용한다. 혼합 모드는 선택/입력/선택이다. 환경 순환은 seed로 여덟 곳을 섞어 세 곳을 선택한다. 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 (세 번째 문제)
    ready --> results: EXIT
    loading --> results: EXIT
    playing --> results: EXIT
    answer --> results: EXIT
    feedback --> results: EXIT

정상 결과는 다음 단계(최대 5)의 새 라운드로 연결되며 중도 종료는 응답 수·정답 수를 표시한 뒤 홈으로 연결된다. 무응답 정답률은 — / 응답 없음, 응답하지 않은 단계는 미진행으로 표시한다. 중도 종료 제목과 현재 단계 완료 제목을 구분한다. 진행 막대는 답변 수가 아닌 현재 문항의 1/3, 2/3, 3/3이다. 결과 단계는 모든 지연 이벤트를 무시한다. SNR은 −12~+12 dB 정수로 제한하고 NaN/Infinity는 단계 기본값으로 복구한다. 수동 SNR은 재생 도중 현재 gain을 바꾸지 않고 현재 재청취/남은 문항/다음 라운드에 적용한다. heardDb는 실제 재생 시작값을 보존해 결과를 정확히 기록한다. 홈은 수동 SNR·답·입력·IME·토큰을 초기화하고 시작 단계·모드·환경·전체 음량은 유지한다.

허용되지 않은 이벤트는 동일 상태를 반환한다. 재생 전/중에는 채점할 수 없고, 피드백 이후의 중복 답변·다음 클릭도 한 번만 반영된다. UI에서도 버튼 비활성화와 동기 ref 잠금을 함께 사용한다. 세션 토큰으로 처음 화면 복귀·unmount 뒤 완료된 Promise를 무시한다. 질문과 보기는 재생 완료 후 DOM에 생성되며 정답 문장은 채점 후에만 표시된다.

주관식은 NFC 한글 정규화 후 공백과 유니코드 문장부호만 제거한다. 비어 있는 답변과 부분 일치는 정답으로 인정하지 않는다. React 텍스트 노드로만 출력하여 입력을 HTML로 해석하지 않는다.

점수는 정답 시 100 × 단계 + 10 × min(연속 정답 - 1, 4)이다. 오답은 0점, 현재 연속 정답을 초기화한다. 기존 공식의 cap은 40점이며 세 문항 라운드에서는 문항별 최대 20점, 라운드 합계 최대 30점(0 + 10 + 20)이다. 1단계 세 정답은 330점이다. 최고 연속 정답과 난이도별 정답 수는 유지한다.

오디오

101개 저장 WAV는 edge-tts 7.3.1의 ko-KR-HyunsuMultilingualNeural로 빌드 전에 생성한다. 24kHz mono PCM16이며 원문·도구·해시는 manifest에 남긴다. 생성기는 임시 디렉터리에서 전체 검증 후 원자 교체한다. 생성 절차는 AUDIO_PROVENANCE에 있으며 앱은 provider에 연결하지 않는다.

src/audio/dsp.ts는 순수 수치 함수이며 장치가 없어도 테스트할 수 있다. 8개 현장 녹음 WAV를 디코드해 재생한다. 각 12초, 24kHz mono PCM16이며 양끝 50ms fade로 루프 접합 클릭을 줄인다. 절차적 잡음 합성 코드는 제거했다.

src/audio/engine.ts는 클릭 이벤트에서 AudioContext 생성·resume을 호출한 뒤 같은 출처의 WAV를 로드한다. mono PCM을 decode하고 음성과 배경의 전체 버퍼 RMS를 0.03으로 정규화한다. gain = 10^(dB/20)으로 목표/배경 비율을 지정한다. 정규화한 세 신호의 최대 피크 합을 계산해 모든 경로를 공통 감쇠하므로 합계 peak ≤ 0.8이고 SNR이 유지된다. 시간별·주파수별 청감이나 물리적 dB SPL을 보장하는 수치는 아니다.

src/audio/assets.ts는 음성 자산 버전과 URL 생성을 한 곳에서 관리한다. intro·목표 100개·현장음 8개 모두 ?v=0.2.3을 붙여 동일 파일명에 남은 이전 캐시를 피한다. 파일을 다시 생성하면 제품 버전 유지 여부와 관계없이 자산 버전을 갱신한다. 버전 확인을 위한 외부 요청이나 런타임 manifest 요청은 없다.

배경 BufferSource → 배경 Gain (공통 감쇠, 시작/끝 fade) ─┐
목표 BufferSource → 음성 Gain (공통 감쇠 × 10^(dB/20)) ─────┼→ master Gain → destination
안내 BufferSource → 안내 Gain (공통 감쇠 × 1.8 → 0.12) ─────┘

배경 소스는 loop=true이며 목표 ended에서 즉시 stop/disconnect한다. 각 라운드의 첫 문제에서는 웃음과 안내 음성을 낮추어 배경에 묻히게 한 후 목표 문장을 재생한다. 이후에는 목표만 재생한다. 안내 도중 실패했으면 재시도 시 안내도 다시 재생한다. 기본 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로 두 크기의 3문항 완주·다음 단계·SNR·중도 종료·재청취·오류 복구·axe 검사를 수행한다. 테스트 자동화는 한국어 발음의 자연스러움, 실제 사용자의 청각 난이도, 모바일 Safari의 장치별 정책까지 증명하지 않으므로 인수 시 직접 청취를 추가한다.