feat: 표시판 호출 시 띵동 효과음 추가

Web Audio API로 E5→C5 두음 벨소리 합성 (외부 파일 불필요).
표시판 화면에 소리 켜짐/꺼짐 토글 버튼 추가.
AudioContext는 역할 선택 클릭 시 초기화 (브라우저 autoplay 정책 대응).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gm
2026-06-26 13:38:10 +09:00
parent e45383f19f
commit 960c829744

View File

@@ -320,6 +320,7 @@
<div class="display-label">현재 호출 번호</div> <div class="display-label">현재 호출 번호</div>
<div class="display-num" id="display-num">-</div> <div class="display-num" id="display-num">-</div>
<div class="display-sub" id="display-sub">대기 중 0명</div> <div class="display-sub" id="display-sub">대기 중 0명</div>
<button class="btn" id="display-mute-btn" style="bottom:60px">🔔 소리 켜짐</button>
<button class="btn" id="display-back-btn">역할 변경</button> <button class="btn" id="display-back-btn">역할 변경</button>
</div> </div>
@@ -329,6 +330,49 @@ const socket = io();
let currentPin = ''; let currentPin = '';
let currentRole = ''; let currentRole = '';
let myTicketNumber = null; let myTicketNumber = null;
let lastCalledCount = 0;
// ── 소리 ──────────────────────────────────────
let audioCtx = null;
let soundMuted = false;
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
function playDingDong() {
if (soundMuted || !audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const t = audioCtx.currentTime;
function bell(freq, start, dur, vol) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, start);
gain.gain.setValueAtTime(0, start);
gain.gain.linearRampToValueAtTime(vol, start + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
osc.start(start);
osc.stop(start + dur + 0.05);
}
// 띵 (E5 + 옥타브 배음)
bell(659, t, 1.1, 0.40);
bell(1318, t, 1.1, 0.12);
// 동 (C5 + 옥타브 배음) — 0.45초 후
bell(523, t + 0.45, 1.4, 0.40);
bell(1046, t + 0.45, 1.4, 0.12);
}
document.getElementById('display-mute-btn').addEventListener('click', () => {
soundMuted = !soundMuted;
document.getElementById('display-mute-btn').textContent =
soundMuted ? '🔇 소리 꺼짐' : '🔔 소리 켜짐';
});
// ── 유틸 ────────────────────────────────────── // ── 유틸 ──────────────────────────────────────
function showScreen(id) { function showScreen(id) {
@@ -376,7 +420,9 @@ document.getElementById('role-issue').addEventListener('click', () => {
showScreen('screen-issue'); showScreen('screen-issue');
}); });
document.getElementById('role-display').addEventListener('click', () => { document.getElementById('role-display').addEventListener('click', () => {
currentRole = 'display'; showScreen('screen-display'); currentRole = 'display';
initAudio(); // 사용자 인터랙션 직후 AudioContext 생성 (autoplay 정책)
showScreen('screen-display');
}); });
// 역할 변경 // 역할 변경
@@ -424,8 +470,12 @@ function applyState(s) {
displayEl.classList.remove('flash'); displayEl.classList.remove('flash');
void displayEl.offsetWidth; // reflow void displayEl.offsetWidth; // reflow
displayEl.textContent = newVal; displayEl.textContent = newVal;
if (s.calledCount > 0) displayEl.classList.add('flash'); if (s.calledCount > 0) {
displayEl.classList.add('flash');
if (currentRole === 'display') playDingDong();
} }
}
lastCalledCount = s.calledCount;
document.getElementById('display-sub').textContent = `대기 중 ${s.waitingCount}`; document.getElementById('display-sub').textContent = `대기 중 ${s.waitingCount}`;
} }