From 960c829744409c8b52507f02b0f128882c354a19 Mon Sep 17 00:00:00 2001 From: gm Date: Fri, 26 Jun 2026 13:38:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=91=9C=EC=8B=9C=ED=8C=90=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=9C=20=EB=9D=B5=EB=8F=99=20=ED=9A=A8=EA=B3=BC?= =?UTF-8?q?=EC=9D=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web Audio API로 E5→C5 두음 벨소리 합성 (외부 파일 불필요). 표시판 화면에 소리 켜짐/꺼짐 토글 버튼 추가. AudioContext는 역할 선택 클릭 시 초기화 (브라우저 autoplay 정책 대응). Co-Authored-By: Claude Sonnet 4.6 --- public/index.html | 54 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 266e94c..2af028d 100644 --- a/public/index.html +++ b/public/index.html @@ -320,6 +320,7 @@
현재 호출 번호
-
대기 중 0명
+ @@ -329,6 +330,49 @@ const socket = io(); let currentPin = ''; let currentRole = ''; 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) { @@ -376,7 +420,9 @@ document.getElementById('role-issue').addEventListener('click', () => { showScreen('screen-issue'); }); 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'); void displayEl.offsetWidth; // reflow 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}명`; }