> find-word@0.1.0 test > vitest run tests/release.test.ts -t Node 최소 RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1 ❯ tests/release.test.ts (4 tests | 1 failed | 3 skipped) 27ms ❯ 릴리스 문서 계약 (4) × Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 25ms ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: expected '>=22.12.0' to be '>=22.22.2' // Object.is equality Expected: ">=22.22.2" Received: ">=22.12.0" ❯ tests/release.test.ts:11:40 9| const metadata = JSON.parse(readFileSync("package.json", "utf8")); 10| const lock = JSON.parse(readFileSync("package-lock.json", "utf8")); 11| expect.soft(metadata.engines.node).toBe(">=22.22.2"); | ^ 12| expect.soft(lock.packages[""].engines.node).toBe(">=22.22.2"); 13| expect(lock.packages["node_modules/jsdom"].engines.node).toContain… ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/6]⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: expected '>=22.12.0' to be '>=22.22.2' // Object.is equality Expected: ">=22.22.2" Received: ">=22.12.0" ❯ tests/release.test.ts:12:49 10| const lock = JSON.parse(readFileSync("package-lock.json", "utf8")); 11| expect.soft(metadata.engines.node).toBe(">=22.22.2"); 12| expect.soft(lock.packages[""].engines.node).toBe(">=22.22.2"); | ^ 13| expect(lock.packages["node_modules/jsdom"].engines.node).toContain… 14| for (const path of ["README.md", "docs/DEPLOYMENT.md"]) { ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/6]⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' to contain '22.22.2' - Expected + Received - 22.22.2 + # 단어를 맞춰라 · Find-Word v0.1 + + 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. + + **Designed by OZ at Heat-T Medical Inc.** + + Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). + 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 + `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. + 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 + [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, + `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. + 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. + + React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, + 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 + 의료적 진단·치료나 효과를 주장하지 않습니다. + + ## 실행 + + Node.js **22.12 이상**의 지원되는 LTS 버전을 권장합니다. `package-lock.json`을 + 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. + + ```bash + npm ci + npm run dev + ``` + + 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 + 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. + + ## 플레이 + + - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. + - 8개 환경을 순환하거나 하나를 지정합니다. + - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. + - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. + 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. + - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. + - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. + - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. + + 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 + 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 + [WORDS.md](docs/WORDS.md)에 설명했습니다. + + ## 검증과 정적 빌드 + + ```bash + npm run lint + npm test + npm run build + npm run preview + ``` + + `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 + 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 + 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. + + 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: + + ```bash + # Chromium이 없는 개발 환경에서 한 번만 실행합니다. + npx playwright install chromium + npm run test:browser + # 이미 설치된 Chrome을 사용할 때의 대안입니다. + CHROME_PATH=/usr/bin/google-chrome npm run test:browser + ``` + + 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. + 실제 음성 재생이 끝나기를 기다려 약 1–2분이 걸립니다. 로컬 4173 포트를 + 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. + + ## 음성과 문서 + + 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 + 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. + 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 + 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. + + - [설계와 상태 전이](docs/ARCHITECTURE.md) + - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) + - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) + + 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. + ❯ tests/release.test.ts:16:35 14| for (const path of ["README.md", "docs/DEPLOYMENT.md"]) { 15| const document = readFileSync(path, "utf8"); 16| expect.soft(document, path).toContain("22.22.2"); | ^ 17| expect.soft(document, path).not.toContain("22.12"); 18| } ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' not to contain '22.12' - Expected + Received - 22.12 + # 단어를 맞춰라 · Find-Word v0.1 + + 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. + + **Designed by OZ at Heat-T Medical Inc.** + + Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). + 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 + `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. + 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 + [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, + `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. + 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. + + React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, + 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 + 의료적 진단·치료나 효과를 주장하지 않습니다. + + ## 실행 + + Node.js **22.12 이상**의 지원되는 LTS 버전을 권장합니다. `package-lock.json`을 + 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. + + ```bash + npm ci + npm run dev + ``` + + 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 + 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. + + ## 플레이 + + - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. + - 8개 환경을 순환하거나 하나를 지정합니다. + - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. + - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. + 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. + - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. + - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. + - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. + + 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 + 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 + [WORDS.md](docs/WORDS.md)에 설명했습니다. + + ## 검증과 정적 빌드 + + ```bash + npm run lint + npm test + npm run build + npm run preview + ``` + + `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 + 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 + 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. + + 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: + + ```bash + # Chromium이 없는 개발 환경에서 한 번만 실행합니다. + npx playwright install chromium + npm run test:browser + # 이미 설치된 Chrome을 사용할 때의 대안입니다. + CHROME_PATH=/usr/bin/google-chrome npm run test:browser + ``` + + 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. + 실제 음성 재생이 끝나기를 기다려 약 1–2분이 걸립니다. 로컬 4173 포트를 + 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. + + ## 음성과 문서 + + 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 + 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. + 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 + 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. + + - [설계와 상태 전이](docs/ARCHITECTURE.md) + - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) + - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) + + 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. + ❯ tests/release.test.ts:17:39 15| const document = readFileSync(path, "utf8"); 16| expect.soft(document, path).toContain("22.22.2"); 17| expect.soft(document, path).not.toContain("22.12"); | ^ 18| } 19| }); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: docs/DEPLOYMENT.md: expected '# 정적 배포 인계\n\n이 문서는 **Neo가 검토 후 수행할 절…' to contain '22.22.2' - Expected + Received - 22.22.2 + # 정적 배포 인계 + + 이 문서는 **Neo가 검토 후 수행할 절차**다. Codex는 서버, DNS, 인증정보에 + 접근하지 않았고 배포·서비스 생성·Git commit/push도 하지 않았다. + + ## 산출물 + + Node.js 22.12 이상 지원 LTS 환경에서 `npm ci`, `npm run lint`, `npm test`, + `npm run build`를 실행한다. `dist/` 전체를 하나의 릴리스로 전달한다. + `dist/audio/`의 WAV 101개와 manifest가 반드시 포함되어야 한다. 런타임은 + Nginx 정적 파일만 필요하며 Node 서버·TTS 엔진·DB·환경변수·API 키는 필요 없다. + + Canonical repository는 [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word)다. + 프로젝트 자체는 private / all-rights-reserved이며 [LICENSE](../LICENSE)를 따른다. + React, ReactDOM, scheduler의 MIT 전문을 담은 `dist/THIRD_PARTY_NOTICES.md`도 + 반드시 배포한다. `build` 검증기는 고지 파일이 없거나 축약·변조되면 실패한다. + + 현재 앱은 도메인 루트(`/`) 배포 기준이다. 하위 경로 배포 시 Vite base뿐 아니라 + 음성 URL도 함께 변경하고 테스트해야 한다. 예정 도메인은 `findword.heart-m.com`이다. + + ## Nginx 예시 + + 아래 root는 예시다. 실제 경로·기존 서버 블록·인증서는 Neo가 확인한다. + 기존 서비스 설정을 덮어쓰지 않고 검토 후 적용한다. + + ```nginx + # 정적 릴리스 디렉터리를 가리킨다. 인증서와 HTTPS 설정은 운영자가 별도 추가한다. + server { + listen 80; + server_name findword.heart-m.com; + root /var/www/findword/current; + index index.html; + + # 기본 MIME 정의로 JS/CSS/WAV가 올바른 타입으로 제공되게 한다. + include /etc/nginx/mime.types; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy no-referrer always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always; + + # 파일명이 해시로 바뀌는 번들만 장기 캐시한다. + location /assets/ { + try_files $uri =404; + expires 1y; + } + + # 음성 파일명은 재생성해도 같으므로 재검증한다. 누락 시 index.html 대신 + # 404를 반환해야 앱이 명확한 음성 로드 오류와 재시도를 제공할 수 있다. + location /audio/ { + try_files $uri =404; + expires -1; + } + + # SPA의 직접 진입·새로고침을 index.html로 받되 HTML은 재검증한다. + location / { + try_files $uri $uri/ /index.html; + expires -1; + } + } + ``` + + ## 인수 확인 + + 1. 서버에 적용하기 전 `nginx -t`, 기존 서비스 충돌, 파일 읽기 권한을 확인한다. + 2. TLS/HTTPS와 DNS를 운영자가 확인한다. 실제 도메인에서 시작 후 음성이 들리는지 확인한다. + 3. `audio/intro.wav`와 목표 WAV가 정상 응답하고 존재하지 않는 오디오는 404인지 확인한다. + 4. 모바일 Safari/Chrome, 데스크톱에서 음량·중단·다시 듣기·10문제 완주를 직접 확인한다. + 5. 원자적으로 릴리스 디렉터리를 전환하고 이전 디렉터리를 보관해 되돌릴 수 있게 한다. + + 오프라인 캐시를 제공하는 PWA는 아니다. 정적 자산의 최초 로드에는 해당 서버 + 연결이 필요하다. 페이지·오디오 캐시가 서로 다른 버전으로 섞이지 않게 HTML과 + 음성의 재검증 정책을 유지한다. + ❯ tests/release.test.ts:16:35 14| for (const path of ["README.md", "docs/DEPLOYMENT.md"]) { 15| const document = readFileSync(path, "utf8"); 16| expect.soft(document, path).toContain("22.22.2"); | ^ 17| expect.soft(document, path).not.toContain("22.12"); 18| } ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ FAIL tests/release.test.ts > 릴리스 문서 계약 > Node 최소 버전은 잠긴 의존성이 요구하는 22.22.2와 일치한다 AssertionError: docs/DEPLOYMENT.md: expected '# 정적 배포 인계\n\n이 문서는 **Neo가 검토 후 수행할 절…' not to contain '22.12' - Expected + Received - 22.12 + # 정적 배포 인계 + + 이 문서는 **Neo가 검토 후 수행할 절차**다. Codex는 서버, DNS, 인증정보에 + 접근하지 않았고 배포·서비스 생성·Git commit/push도 하지 않았다. + + ## 산출물 + + Node.js 22.12 이상 지원 LTS 환경에서 `npm ci`, `npm run lint`, `npm test`, + `npm run build`를 실행한다. `dist/` 전체를 하나의 릴리스로 전달한다. + `dist/audio/`의 WAV 101개와 manifest가 반드시 포함되어야 한다. 런타임은 + Nginx 정적 파일만 필요하며 Node 서버·TTS 엔진·DB·환경변수·API 키는 필요 없다. + + Canonical repository는 [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word)다. + 프로젝트 자체는 private / all-rights-reserved이며 [LICENSE](../LICENSE)를 따른다. + React, ReactDOM, scheduler의 MIT 전문을 담은 `dist/THIRD_PARTY_NOTICES.md`도 + 반드시 배포한다. `build` 검증기는 고지 파일이 없거나 축약·변조되면 실패한다. + + 현재 앱은 도메인 루트(`/`) 배포 기준이다. 하위 경로 배포 시 Vite base뿐 아니라 + 음성 URL도 함께 변경하고 테스트해야 한다. 예정 도메인은 `findword.heart-m.com`이다. + + ## Nginx 예시 + + 아래 root는 예시다. 실제 경로·기존 서버 블록·인증서는 Neo가 확인한다. + 기존 서비스 설정을 덮어쓰지 않고 검토 후 적용한다. + + ```nginx + # 정적 릴리스 디렉터리를 가리킨다. 인증서와 HTTPS 설정은 운영자가 별도 추가한다. + server { + listen 80; + server_name findword.heart-m.com; + root /var/www/findword/current; + index index.html; + + # 기본 MIME 정의로 JS/CSS/WAV가 올바른 타입으로 제공되게 한다. + include /etc/nginx/mime.types; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy no-referrer always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always; + + # 파일명이 해시로 바뀌는 번들만 장기 캐시한다. + location /assets/ { + try_files $uri =404; + expires 1y; + } + + # 음성 파일명은 재생성해도 같으므로 재검증한다. 누락 시 index.html 대신 + # 404를 반환해야 앱이 명확한 음성 로드 오류와 재시도를 제공할 수 있다. + location /audio/ { + try_files $uri =404; + expires -1; + } + + # SPA의 직접 진입·새로고침을 index.html로 받되 HTML은 재검증한다. + location / { + try_files $uri $uri/ /index.html; + expires -1; + } + } + ``` + + ## 인수 확인 + + 1. 서버에 적용하기 전 `nginx -t`, 기존 서비스 충돌, 파일 읽기 권한을 확인한다. + 2. TLS/HTTPS와 DNS를 운영자가 확인한다. 실제 도메인에서 시작 후 음성이 들리는지 확인한다. + 3. `audio/intro.wav`와 목표 WAV가 정상 응답하고 존재하지 않는 오디오는 404인지 확인한다. + 4. 모바일 Safari/Chrome, 데스크톱에서 음량·중단·다시 듣기·10문제 완주를 직접 확인한다. + 5. 원자적으로 릴리스 디렉터리를 전환하고 이전 디렉터리를 보관해 되돌릴 수 있게 한다. + + 오프라인 캐시를 제공하는 PWA는 아니다. 정적 자산의 최초 로드에는 해당 서버 + 연결이 필요하다. 페이지·오디오 캐시가 서로 다른 버전으로 섞이지 않게 HTML과 + 음성의 재검증 정책을 유지한다. + ❯ tests/release.test.ts:17:39 15| const document = readFileSync(path, "utf8"); 16| expect.soft(document, path).toContain("22.22.2"); 17| expect.soft(document, path).not.toContain("22.12"); | ^ 18| } 19| }); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯ Test Files 1 failed (1) Tests 1 failed | 3 skipped (4) Start at 05:54:57 Duration 357ms (setup 64%, transform 18%, tests 14%, worker 2%, import 2%)