이클립스 방어 횟수 즉시 표시·진동 · Music Shake 영상 재생 중 흔들기 판정 · 결과 공유

This commit is contained in:
Sn-Kinos
2026-10-05 21:39:22 +09:00
parent 65669b969e
commit 17c658b272
30 changed files with 1337 additions and 144 deletions
+18 -1
View File
@@ -2,7 +2,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { getFirestore, FieldValue, Timestamp } from 'firebase-admin/firestore';
import { bootAdmin, shutdownAdmin, wipeUser } from './support.js';
import { distanceMeters, gradeShake } from '../src/quest/verify.js';
import { distanceMeters, gradeShake, videoIdOf } from '../src/quest/verify.js';
import { DEFAULT_POLICY } from '../src/core/config.js';
import { grantPop, refundPop, spendPop } from '../src/ledger/ledger.js';
/**
@@ -43,6 +44,22 @@ describe('Music Shake 판정 (E-02)', () => {
it('구간이 0이면 실격이다', () => {
expect(gradeShake({ perfect: 0, good: 0, miss: 0, maxCombo: 0 }, 0)).toBe('F');
});
it('성공 문턱 · 등급 문턱은 운영값이다 — 🟡 임시값 회신 대기 표시가 붙어 있다', () => {
expect(DEFAULT_POLICY.musicShake.provisional).toBe(true);
const strict = { ...DEFAULT_POLICY.musicShake, passRate: 0.9 };
// 기본 문턱(0.5)이면 C, 운영값을 0.9 로 올리면 실패다.
expect(gradeShake({ perfect: 6, good: 6, miss: 8, maxCombo: 6 }, 20)).toBe('C');
expect(gradeShake({ perfect: 6, good: 6, miss: 8, maxCombo: 6 }, 20, strict)).toBe('F');
});
it('지정 공식 영상 링크에서 영상 ID 를 꺼낸다', () => {
expect(videoIdOf('https://www.youtube.com/watch?v=WMweEpGlu_U')).toBe('WMweEpGlu_U');
expect(videoIdOf('https://youtu.be/WMweEpGlu_U?si=x')).toBe('WMweEpGlu_U');
expect(videoIdOf('https://music.youtube.com/watch?v=WMweEpGlu_U')).toBe('WMweEpGlu_U');
expect(videoIdOf('https://vimeo.com/1')).toBeNull();
expect(videoIdOf(undefined)).toBeNull();
});
});
describe('방문 거리 (E-06)', () => {
@@ -519,6 +519,24 @@ describe('검증 활동 집계 규칙 — Quest 완료 1건 · 하루 1회 (Camp
expect(reply.rewarded).toBe(60);
});
it('Music Shake — 곡은 지정 영상 링크로 대조하고 · rewardPop 이 없으면 운영값 musicShake.pop · 판정 미달은 보상 0', async () => {
await db().doc('quests/music-link').set({
type: 'musicShake', totalSegments: 10, dailyLimit: 2,
link: { url: 'https://youtu.be/WMweEpGlu_U', title: 'BTS - Butter' },
});
await db().doc('config/policy').set({ musicShake: { pop: 4 } }, { merge: true });
const shake = (perfect: number, miss: number) => call(fnQuestVerifyMusicShake, {
questId: 'music-link', trackId: 'WMweEpGlu_U', totalSegments: 10,
judgement: { perfect, good: 0, miss, maxCombo: perfect },
});
// 3/10 — 임시 성공 문턱 0.5 미달 → 보상 0, 원장 없음.
expect(await codeOf(() => shake(3, 7))).toBe('E-QST-01');
expect((await db().collection(`users/${UID}/popTransactions`).get()).size).toBe(0);
// 5/10 — 문턱 0.5 충족 · perfect 0.5 < B 문턱 0.6 → C(배수 1) → 4.
const reply = await shake(5, 5);
expect(reply.rewarded).toBe(4);
});
it('Music Shake 는 완주마다 1회 · 방문은 1회', async () => {
await db().doc('quests/music-count').set({ type: 'musicShake', trackId: 't1', totalSegments: 10, dailyLimit: 2 });
const shake = () => call(fnQuestVerifyMusicShake, {