// Main JavaScript utilities // Format time duration function formatDuration(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } // Show toast message function showToast(message, duration = 3000) { const toast = document.createElement('div'); toast.className = 'ai-notice show'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } // API request helper async function apiRequest(endpoint, method = 'GET', data = null) { const options = { method, headers: { 'Content-Type': 'application/json', } }; if (data && method !== 'GET') { options.body = JSON.stringify(data); } try { const response = await fetch(endpoint, options); const result = await response.json(); return result; } catch (error) { console.error('API Error:', error); showToast('오류가 발생했습니다'); return { success: false, error: error.message }; } } // Play audio from base64 function playAudioBase64(base64Data, text = '') { return new Promise((resolve, reject) => { const audio = new Audio(`data:audio/mp3;base64,${base64Data}`); // 사용자 설정에서 재생 속도 가져오기 if (typeof userProfile !== 'undefined' && userProfile.speechRate) { audio.playbackRate = userProfile.speechRate; console.log(`🔊 Playing audio at ${userProfile.speechRate}x speed`); } // 👩 캐릭터 표시 (텍스트 전달로 감정 분석) if (typeof showCharacter === 'function') { showCharacter(text); } // 음성 재생 시작 시 립싱크 시작 audio.addEventListener('play', () => { if (typeof startLipSync === 'function') { startLipSync(); } }); // 재생 완료 시 resolve audio.addEventListener('ended', () => { console.log('✅ Audio playback completed'); // 👩 캐릭터 숨김 (음성 재생 완료) if (typeof hideCharacter === 'function') { hideCharacter(); } resolve(); }); // 재생 오류 시 reject (하지만 계속 진행) audio.addEventListener('error', (error) => { console.error('Audio playback error:', error); // 👩 오류 시에도 캐릭터 숨김 if (typeof hideCharacter === 'function') { hideCharacter(); } reject(error); }); audio.play().catch(error => { console.error('Audio play() error:', error); // 👩 재생 실패 시에도 캐릭터 숨김 if (typeof hideCharacter === 'function') { hideCharacter(); } reject(error); }); }); } // Check microphone permission async function checkMicrophonePermission() { try { // 브라우저가 권한 API를 지원하는지 확인 if (navigator.permissions && navigator.permissions.query) { const permissionStatus = await navigator.permissions.query({ name: 'microphone' }); if (permissionStatus.state === 'denied') { showToast('마이크 권한이 거부되었습니다. 브라우저 설정에서 권한을 허용해주세요.', 5000); return false; } } // 마이크 권한 요청 (브라우저 기본 다이얼로그가 표시됨) const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); // 권한 확인용으로만 사용했으므로 스트림 정리 stream.getTracks().forEach(track => track.stop()); return true; } catch (error) { console.error('Microphone permission error:', error); if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') { showToast('마이크 사용 권한을 허용해주세요. 설정에서 권한을 변경할 수 있습니다.', 5000); } else if (error.name === 'NotFoundError') { showToast('마이크를 찾을 수 없습니다. 마이크가 연결되어 있는지 확인해주세요.', 5000); } else { showToast('마이크 접근 중 오류가 발생했습니다.', 5000); } return false; } }