Files
Find-Word/docs/ARCHITECTURE.md
T

11 KiB
Raw Permalink Blame History

Find-Word v0.2.5 설계

데이터와 출제

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를 로드한다. integrity.ts가 JS 번들에 고정한 109개 URL·bytes·SHA-256과 응답을 디코드 전에 대조한다. 런타임 manifest fetch나 외부 요청은 없다. mono·길이·유한 sample·non-silence와 장치 rate로 리샘플한 PCM의 경로별 peak 상한도 확인한다. 변조/상한 초과는 재시도 가능한 오류이며 파일별 자동 감쇠로 덮지 않는다.

배경은 새 FFmpeg ebur128 실측에서 구한 double precision 스칼라 volume으로 −32.5 LUFS에 맞춘 저장 PCM을 그대로 사용한다. 최종 ebur128 목표 ±0.2 LU/spread ≤0.4 LU/true peak ≤−6 dBTP를 강제한다. 생성 내부 ceiling은 −6.2 dBTP다. 독립 pyloudnorm 0.2.0도 통과했다. 새 market의 VAD ASR 실패는 불변 진단으로 남기고, 릴리스 gate는 자동 empty-ASR OR exact-byte Oz 청취 승인을 검사한다. 현재는 후자만 통과한다. scripts/release_approval.py가 전달 ZIP·manifest·8 WAV·notice 바이트와 문구/시각/범위를 검증하며 legalPrivacyClearance=false를 강제한다. voice와 intro만 기존 RMS 0.03 정규화를 유지한다. g(db)=10^(db/20)은 목표 음성 상대 gain이며 배경마다 RMS가 다르므로 표시 SNR은 기준 gain의 dB 변화다. 실제 파일 RMS 비율/순간 청감/dB SPL은 보장하지 않는다.

고정 상한 B=0.55, V=0.25, I=0.20, SNR_MAX=12, 안내 시작 배율 A=1.8, 합성 ceiling C=0.8을 사용한다. 24kHz 실제 최대값은 배경 0.47195435, RMS 정규화 voice 101개 0.20529779, intro 0.14995423이다. 새 자산의 실제 Chrome 24/44.1/48/96kHz 전수 디코드 최대값은 배경 0.48797759, voice 0.20585650, intro 0.14999594다. review-fixes 브라우저 증거에 전체 측정을 보존했다. 여유를 둔 상한은 이 모든 값보다 크며 재생 때도 검사한다. 실제 GainNode의 Float32 plateau는 0.4198884665966034로 환경/SNR에 걸쳐 동일하고 JS 예약값과의 차이는 Float32 반올림이다.

Q = B + V × 10^(12/20) + I × A = 1.905267926383743
G = min(1, C/Q) = 0.4198884518664126
backgroundGain = G
voiceGain(db) = G × 10^(db/20)
introGain = G × 1.8  (기존 ramp로 1/15까지 감소)
|mix(t)| ≤ master × G × (B + V × 10^(db/20) + I × A) ≤ 0.8

삼각부등식이 모든 위상·loop offset·세 경로가 동시에 겹치는 경우까지 덮는다. master 범위는 [0,1], 배경 fade는 [0,G], intro ramp도 [0,G×1.8] 이내이므로 상한을 키우지 않는다. intro가 없어도 같은 G를 사용한다. 환경·실제 sample peak·현재 SNR을 G 계산에 넣지 않는다. SNR은 voice만 바꾸고 master는 모든 경로를 함께 바꾼다. mixing-loudness.test.ts는 101×8×25=20,200개 조합에서 유한 gain·비율·peak 상계를 검사한다. 이는 디지털 샘플 안전성이다. DAC의 재구성 필터·기기 음압·임상 청감의 보장은 아니다.

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

배경 BufferSource → 배경 Gain (고정 G, 시작/끝 fade) ─┐
목표 BufferSource → 음성 Gain (고정 G × 10^(dB/20)) ─────┼→ master Gain → destination
안내 BufferSource → 안내 Gain (고정 G × 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의 장치별 정책까지 증명하지 않으므로 인수 시 직접 청취를 추가한다.