Skip to content

[FEAT] 메트로놈 · 카운트다운 공통 로직 UI 및 기능 구현#28

Merged
kxxnayun merged 10 commits into
developfrom
feat/#22-metronome-common
Jul 15, 2026
Merged

[FEAT] 메트로놈 · 카운트다운 공통 로직 UI 및 기능 구현#28
kxxnayun merged 10 commits into
developfrom
feat/#22-metronome-common

Conversation

@leeminsel

@leeminsel leeminsel commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

#️⃣ 관련 이슈


📝 작업 요약

메트로놈 공통 재생 로직을 구현하고, 이를 활용한 UI(박자 점 표시), 카운트다운, 레이턴시 체크 페이지 컴포넌트 배치까지 마무리

🔧 변경 사항

메트로놈 (공통 로직)

  • utils/metronome.ts — Tone.getTransport 기반, BPM·박자표(beatsPerBar)별 강박 구분, 클릭음 재생
  • hooks/useMetronome.ts — React 훅 래핑, start/stop을 useCallback으로 고정 (재렌더 시 재시작 방지)

진행 점

  • components/metronome/MetronomeDots.tsx — 박자 진행을 점으로 표시 (현재 박 강조)

박자표 연동

  • settingsStore에 beatsPerBar 추가, 설정 페이지에서 곡 timeSignature 파싱해 주입 (3/4·4/4 대응)

레이턴시 체크 화면

  • intro 안내(블러 오버레이) → 카운트다운(4→1, 메트로놈 박자 동기) → 측정 단계 전환
  • 헤더(제목·재시작), 메트로놈 UI, 건반 배치
  • 레이턴시 체크는 4/4 고정, 측정값 임시 저장 후 설정 페이지 복귀

💬 리뷰어 참고 사항

  • 오디오 잠금 해제(Tone.start)는 설정 모달 "체크하기" 클릭 시 처리되어서, 설정 모달 거쳐서 레이턴시 체크 화면 들어가야 합니다!
  • 레이턴시 측정 로직과 노트바는 별도로 이슈 생성 후 구현 예정입니다!

✅ 체크리스트

  • 커밋 메시지 컨벤션을 준수했습니다.
  • 로컬에서 정상 동작을 확인했습니다.
  • UI 변경 사항을 직접 확인했습니다.
  • 불필요한 console.log를 제거했습니다.
  • 관련 이슈를 연결했습니다.

Summary by CodeRabbit

  • 새로운 기능
    • 레이턴시 체크 화면이 인트로/카운트다운/측정 단계로 분리되어 진행됩니다.
    • 메트로놈 진행 점(현재 박) 표시가 추가되었습니다.
  • 개선 사항
    • 레이턴시 체크 시작 및 재설정 시 오디오가 먼저 활성화된 뒤 측정이 진행됩니다.
    • 연습 설정에서 선택한 곡의 BPM과 박자(beat per bar)가 자동 반영됩니다.
  • 기타
    • 설정에 beat per bar 값(기본값 포함)과 관련 제어가 추가/갱신되었습니다.

@leeminsel leeminsel self-assigned this Jul 14, 2026
@leeminsel leeminsel added ✨ Feat New feature or request 🎨 Style UI/UX and visual design tasks labels Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 18e918af-2d85-42f8-98c7-bbc2cd314c45

📥 Commits

Reviewing files that changed from the base of the PR and between 3b9ff41 and f446c22.

📒 Files selected for processing (2)
  • src/components/metronome/MetronomeDots.tsx
  • src/pages/latency/LatencyCheckPage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/metronome/MetronomeDots.tsx
  • src/pages/latency/LatencyCheckPage.tsx

📝 Walkthrough

Walkthrough

Tone.js 기반 메트로놈과 useMetronome 훅을 추가하고, 레이턴시 체크 화면을 intro·countdown·measuring 단계로 재구성했습니다. 설정 스토어에 박자 수를 추가하고, 곡별 BPM과 박자를 설정 화면에 동기화합니다.

Changes

메트로놈 및 레이턴시 체크

Layer / File(s) Summary
메트로놈 상태와 오디오 실행
src/stores/settingsStore.ts, src/utils/metronome.ts, src/hooks/useMetronome.ts
박자 수 상태와 액션을 추가하고, Tone Transport 기반 메트로놈 및 생명주기 관리 훅을 구현했습니다.
단계형 레이턴시 체크 화면
src/pages/latency/LatencyCheckPage.tsx, src/components/metronome/MetronomeDots.tsx
단계별 화면과 박자별 카운트다운 UI를 추가하고 메트로놈을 연결했습니다.
설정 및 곡 정보 연결
src/components/settings/SettingsModal.tsx, src/pages/practice/PracticeSettingsPage.tsx
레이턴시 체크 전 Tone.start()를 실행하고, 선택한 곡의 BPM과 박자를 설정 스토어에 반영합니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PracticeSettingsPage
  participant SettingsModal
  participant LatencyCheckPage
  participant useMetronome
  participant ToneTransport
  PracticeSettingsPage->>PracticeSettingsPage: 곡의 BPM과 박자 설정
  PracticeSettingsPage->>SettingsModal: 레이턴시 체크 콜백 전달
  SettingsModal->>SettingsModal: Tone.start() 실행
  SettingsModal->>LatencyCheckPage: 레이턴시 체크 화면 이동
  LatencyCheckPage->>useMetronome: start(bpm, beatsPerBar, onBeat)
  useMetronome->>ToneTransport: 반복 박자 등록
  ToneTransport-->>LatencyCheckPage: 박자 및 카운트다운 전달
  LatencyCheckPage->>useMetronome: measuring 전환 시 stop()
Loading

Possibly related PRs

  • Musereview/FE#20: SettingsModal.tsxLatencyCheckPage.tsx의 레이턴시 체크 흐름을 함께 변경합니다.

Suggested reviewers: seooyoon, andada-creator

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 메트로놈·카운트다운 공통 로직과 UI 구현이라는 핵심 변경을 잘 요약합니다.
Linked Issues check ✅ Passed 메트로놈 UI/로직, 카운트다운 UI/기능, 곡별 BPM 적용이 링크된 이슈의 요구사항과 일치합니다.
Out of Scope Changes check ✅ Passed 설정 모달·레이턴시 페이지의 UI 조정도 공통 메트로놈/카운트다운 구현 범위 안에 있어 보입니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#22-metronome-common

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/utils/metronome.ts (1)

11-40: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

전역 Transport 초기화 및 오디오 노드 생성 최적화

현재 코드에는 두 가지 잠재적인 문제가 있습니다:

  1. transport.cancel()은 Transport에 예약된 모든 이벤트를 전역적으로 삭제합니다. 향후 메트로놈과 함께 백킹 트랙 등 다른 Tone.Transport 이벤트를 함께 재생하게 되면, 메트로놈을 정지할 때 다른 트랙의 이벤트까지 모두 삭제되는 문제가 발생할 수 있습니다. scheduleRepeat이 반환하는 이벤트 ID를 저장해두고 transport.clear(eventId)를 사용하는 것이 안전합니다.
  2. 매 박자마다 new Tone.Player를 생성하고 폐기하는 방식은 오디오 스레드에 가비지 컬렉션(GC) 부하를 주어 성능 저하나 오디오 글리치(끊김 현상)를 유발할 수 있습니다. 메트로놈 클릭음은 매우 짧기 때문에 Player 인스턴스를 한 번만 생성하고 재사용하는 것이 좋습니다.
♻️ 제안하는 리팩토링
 export function createMetronome() {
   const transport = Tone.getTransport();
 
   // 음원 버퍼를 미리 로드 (파일은 한 번만 받음)
   const hiBuffer = new Tone.ToneAudioBuffer('/sounds/click1.mp3');
   const loBuffer = new Tone.ToneAudioBuffer('/sounds/click2.mp3');
   
+  // Player 인스턴스를 한 번만 생성하여 재사용
+  const hiPlayer = new Tone.Player(hiBuffer).toDestination();
+  const loPlayer = new Tone.Player(loBuffer).toDestination();
+
+  let eventId: number | null = null;
 
   return {
     start(bpm: number, beatsPerBar: number, onBeat: (time: number, beatInBar: number) => void) {
-      transport.stop();
-      transport.cancel();
+      if (eventId !== null) {
+        transport.clear(eventId);
+      }
+      // 전역 Transport를 제어해야 하는 경우에만 사용 (다른 로직과 충돌 방지를 위해 확인 필요)
+      // transport.stop(); 
 
       let beat = 0;
       transport.bpm.value = bpm;
-      transport.scheduleRepeat((time) => {
+      eventId = transport.scheduleRepeat((time) => {
         const beatInBar = beat % beatsPerBar;
-        const buffer = beatInBar === 0 ? hiBuffer : loBuffer;
-
-        if (buffer.loaded) {
-          // 매 박마다 새 Player 생성 → 겹침 없음, 재생 후 자동 정리
-          const player = new Tone.Player(buffer).toDestination();
-          player.start(time);
-          player.onstop = () => player.dispose();
-        }
+        const player = beatInBar === 0 ? hiPlayer : loPlayer;
+
+        if (player.loaded) {
+          player.start(time);
+        }
         onBeat(time, beatInBar);
         beat += 1;
       }, '4n');
       transport.start();
     },
     stop() {
-      transport.stop();
-      transport.cancel();
+      if (eventId !== null) {
+        transport.clear(eventId);
+        eventId = null;
+      }
+      transport.stop();
     },
     dispose() {
+      if (eventId !== null) {
+        transport.clear(eventId);
+      }
       hiBuffer.dispose();
       loBuffer.dispose();
+      hiPlayer.dispose();
+      loPlayer.dispose();
     },
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/metronome.ts` around lines 11 - 40, Update the metronome start/stop
lifecycle to store the event ID returned by scheduleRepeat and clear only that
event with transport.clear instead of calling transport.cancel. Create reusable
hi- and lo-click Tone.Player instances once, reuse the selected player for each
beat, and dispose both players in dispose; remove per-beat Player creation and
disposal while preserving onBeat timing and beat selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/useMetronome.ts`:
- Around line 4-26: Update useMetronome so the Metronome instance is lazily
created when start or stop is invoked, rather than only during render via ref
initialization. Keep the cleanup in the useEffect lifecycle, but ensure it
disposes the current instance and clears the ref so a subsequent Strict Mode
effect setup or callback invocation creates a fresh instance through the same
accessor.

In `@src/pages/latency/LatencyCheckPage.tsx`:
- Around line 58-61: 재시작 버튼에 클릭 핸들러가 연결되지 않았습니다. LatencyCheckPage의 해당 버튼에
handleRestart를 연결하고, 이 함수에서 카운트다운과 메트로놈 상태를 초기화한 뒤 측정 흐름을 intro 단계부터 다시 시작하도록
구현하세요.

---

Nitpick comments:
In `@src/utils/metronome.ts`:
- Around line 11-40: Update the metronome start/stop lifecycle to store the
event ID returned by scheduleRepeat and clear only that event with
transport.clear instead of calling transport.cancel. Create reusable hi- and
lo-click Tone.Player instances once, reuse the selected player for each beat,
and dispose both players in dispose; remove per-beat Player creation and
disposal while preserving onBeat timing and beat selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b9bdf97d-35e1-4d55-ad2c-d503c0aaa9b2

📥 Commits

Reviewing files that changed from the base of the PR and between 78f720a and 9045e53.

⛔ Files ignored due to path filters (5)
  • public/sounds/click1.mp3 is excluded by !**/*.mp3, !public/**
  • public/sounds/click2.mp3 is excluded by !**/*.mp3, !public/**
  • src/assets/metronome/metronome-empty.svg is excluded by !**/*.svg
  • src/assets/metronome/metronome-full.svg is excluded by !**/*.svg
  • src/assets/restart.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • src/components/metronome/MetronomeDots.tsx
  • src/components/settings/SettingsModal.tsx
  • src/hooks/useMetronome.ts
  • src/pages/latency/LatencyCheckPage.tsx
  • src/pages/practice/PracticeSettingsPage.tsx
  • src/stores/settingsStore.ts
  • src/utils/metronome.ts

Comment thread src/hooks/useMetronome.ts
Comment on lines +58 to +61
<button className="button-large2 flex h-[60px] w-[175px] items-center justify-center gap-2 rounded-[6px] bg-gray-800 px-3 py-[6px] text-gray-300">
재시작
<RefreshIcon className="h-5 w-5" />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

'재시작' 버튼의 onClick 핸들러가 누락되었습니다.

버튼 UI가 추가되었지만 클릭 시 동작하는 핸들러가 연결되어 있지 않아 아무런 동작을 하지 않습니다.
의도된 것이 아니라면 카운트다운과 메트로놈 상태를 초기화하고 처음부터 다시 시작하는 로직을 연결해야 합니다.

필요하시다면 상태를 초기화하고 측정 흐름을 처음(intro)부터 다시 실행하는 handleRestart 함수 구현을 도와드릴 수 있습니다. 이슈로 등록할까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/latency/LatencyCheckPage.tsx` around lines 58 - 61, 재시작 버튼에 클릭 핸들러가
연결되지 않았습니다. LatencyCheckPage의 해당 버튼에 handleRestart를 연결하고, 이 함수에서 카운트다운과 메트로놈 상태를
초기화한 뒤 측정 흐름을 intro 단계부터 다시 시작하도록 구현하세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재시작 버튼 동작은 지금은 작동 안하는게 맞습니다~!

current: number; // 현재 박 (0-based)
}

function Metronome({ total, current }: MetronomeProps) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 함수명 MetronomeDots로 파일명과 통일 시켜주시면 좋을 것 같아요~!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네~~ 수정했습니다!

Comment thread src/pages/latency/LatencyCheckPage.tsx Outdated
useEffect(() => {
const timer = setTimeout(() => {
setPhase('countdown');
start(bpm, 4, (_time, bib) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tone은 오디오를 끊김 없이 내보내려고 실제 소리가 날 시점보다 조금 앞서서 콜백을 돌려서 아래처럼 바꾸는 거 어떨까요?!

  import * as Tone from 'tone';

  start(bpm, 4, (time, bib) => {
    Tone.getDraw().schedule(() => {
      setBeatInBar(bib);
      setCountdown(4 - bib);
      if (bib === 0 && countdownDoneRef.current) {
        setPhase('measuring');
        stop();
        Tone.getDraw().cancel();
      }
    }, time);

    if (bib === 3) countdownDoneRef.current = true; // 상태전이 조건용 ref는 즉시 갱신 OK
  });

추가로 Tone.getDraw()는 자체 큐라 stop()으론 안 비워지기 때문에 클린업에서 stop()은 그대로 두고 Tone.getDraw().cancel()을 추가로 호출해 예약된 시각 갱신 콜백을 정리하면 좋을 것 같아요~~!!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다~! 레이턴시 측정 로직 정확도에 오류가 생길 뻔했네요. 중요한 부분 짚어주셔서 감사해요~~!

kxxnayun

This comment was marked as duplicate.

@kxxnayun kxxnayun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하신 것까지 확인했습니다! 수고하셨습니당~~!!

@kxxnayun
kxxnayun merged commit 3d53dfd into develop Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feat New feature or request 🎨 Style UI/UX and visual design tasks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 메트로놈 · 카운트다운 공통 로직 UI 및 기능 구현

2 participants