[251018] feat: WebSpeech

🕐 커밋 시간: 2025. 10. 18. 20:56:03

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +43줄
  • 삭제: -20줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/hooks/useWebSpeech.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.v2.jsx

🗑️ 삭제된 파일:
  - com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.figma.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
This commit is contained in:
2025-10-18 20:56:05 +09:00
parent 31f051b061
commit 5bddacb3af
3 changed files with 43 additions and 20 deletions

View File

@@ -63,10 +63,12 @@ export const useWebSpeech = (isActive, onSTTText, config = {}) => {
dispatch(stopWebSpeech()); dispatch(stopWebSpeech());
}, [dispatch]); }, [dispatch]);
// WebSpeech API 지원 여부 체크 // WebSpeech API 브라우저 지원 여부 체크
// - 브라우저가 SpeechRecognition API를 지원하는지만 확인
// - 마이크 접근 불가 등의 에러는 error 필드로 별도 처리
const isSupported = const isSupported =
!webSpeech.error || typeof window !== 'undefined' &&
(typeof webSpeech.error === 'string' && !webSpeech.error.includes('not supported')); !!(window.SpeechRecognition || window.webkitSpeechRecognition);
return { return {
isInitialized: webSpeech.isInitialized, isInitialized: webSpeech.isInitialized,

View File

@@ -45,8 +45,14 @@ export const VOICE_MODES = {
MODE_4: 'mode4', // 추후 추가 MODE_4: 'mode4', // 추후 추가
}; };
// NOINIT 모드 에러 메시지 // NOINIT 모드 에러 메시지 - 에러 타입별로 구분
const NOINIT_ERROR_MESSAGE = 'Voice recognition is not supported on this device.'; const NOINIT_ERROR_MESSAGES = {
'not-supported': 'Voice recognition is not supported on this browser.',
'audio-capture': 'No microphone detected. Please connect a microphone.',
'not-allowed': 'Microphone access denied. Please allow microphone permissions.',
'service-not-allowed': 'Voice recognition service is not available.',
default: 'Voice recognition is not available.',
};
// 음성인식 입력 모드 (VUI vs WebSpeech) // 음성인식 입력 모드 (VUI vs WebSpeech)
export const VOICE_INPUT_MODE = { export const VOICE_INPUT_MODE = {
@@ -127,6 +133,8 @@ const VoiceInputOverlay = ({
const [sttResponseText, setSttResponseText] = useState(''); const [sttResponseText, setSttResponseText] = useState('');
// Voice Version (어떤 음성 시스템을 사용할지 결정) // Voice Version (어떤 음성 시스템을 사용할지 결정)
const [voiceVersion, setVoiceVersion] = useState(VOICE_VERSION.WEB_SPEECH); const [voiceVersion, setVoiceVersion] = useState(VOICE_VERSION.WEB_SPEECH);
// NOINIT 모드 에러 메시지
const [noInitErrorMessage, setNoInitErrorMessage] = useState(NOINIT_ERROR_MESSAGES.default);
// 🔊 Beep 소리 재생 함수 - zero dependencies // 🔊 Beep 소리 재생 함수 - zero dependencies
const playBeep = useCallback(() => { const playBeep = useCallback(() => {
@@ -338,15 +346,37 @@ const VoiceInputOverlay = ({
} }
}, [interimText, currentMode, handleWakeWordDetected]); }, [interimText, currentMode, handleWakeWordDetected]);
// WebSpeech가 지원되지 않을 때 NOINIT 모드로 전환 // WebSpeech가 지원되지 않거나 마이크 에러 발생 시 NOINIT 모드로 전환
useEffect(() => { useEffect(() => {
if (isVisible && voiceVersion === VOICE_VERSION.WEB_SPEECH && isSupported === false) { if (!isVisible || voiceVersion !== VOICE_VERSION.WEB_SPEECH) return;
// 브라우저가 WebSpeech를 지원하지 않는 경우
if (isSupported === false) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('⚠️ [VoiceInputOverlay.v2] WebSpeech not supported, switching to NOINIT mode'); console.log('⚠️ [VoiceInputOverlay.v2] WebSpeech not supported, switching to NOINIT mode');
} }
setNoInitErrorMessage(NOINIT_ERROR_MESSAGES['not-supported']);
setCurrentMode(VOICE_MODES.NOINIT); setCurrentMode(VOICE_MODES.NOINIT);
return;
} }
}, [isVisible, voiceVersion, isSupported]);
// 마이크 관련 에러 발생 시
if (error && typeof error === 'string') {
if (DEBUG_MODE) {
console.log('⚠️ [VoiceInputOverlay.v2] WebSpeech error detected:', error);
}
// 에러 타입 파싱 (error는 'audio-capture' 같은 문자열 또는 객체일 수 있음)
const errorType = typeof error === 'string' ? error : error.error || error.message || '';
// 마이크 접근 불가 에러들
if (['audio-capture', 'not-allowed', 'service-not-allowed'].some(e => errorType.includes(e))) {
const matchedError = ['audio-capture', 'not-allowed', 'service-not-allowed'].find(e => errorType.includes(e));
setNoInitErrorMessage(NOINIT_ERROR_MESSAGES[matchedError] || NOINIT_ERROR_MESSAGES.default);
setCurrentMode(VOICE_MODES.NOINIT);
}
}
}, [isVisible, voiceVersion, isSupported, error]);
// ⛔ 독립 테스트: WebSpeech API 호출 비활성화 // ⛔ 독립 테스트: WebSpeech API 호출 비활성화
// WebSpeech 모드로 전환되면 자동으로 음성 인식 시작 // WebSpeech 모드로 전환되면 자동으로 음성 인식 시작
@@ -582,9 +612,9 @@ const VoiceInputOverlay = ({
return <VoiceResponse responseText={sttResponseText} onTalkAgain={handleTalkAgain} />; return <VoiceResponse responseText={sttResponseText} onTalkAgain={handleTalkAgain} />;
case VOICE_MODES.NOINIT: case VOICE_MODES.NOINIT:
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('📺 Rendering: VoiceNotRecognized (NOINIT mode)'); console.log('📺 Rendering: VoiceNotRecognized (NOINIT mode) with message:', noInitErrorMessage);
} }
return <VoiceNotRecognized prompt={NOINIT_ERROR_MESSAGE} />; return <VoiceNotRecognized prompt={noInitErrorMessage} />;
case VOICE_MODES.NOTRECOGNIZED: case VOICE_MODES.NOTRECOGNIZED:
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('📺 Rendering: VoiceNotRecognized (NOTRECOGNIZED mode)'); console.log('📺 Rendering: VoiceNotRecognized (NOTRECOGNIZED mode)');
@@ -613,6 +643,7 @@ const VoiceInputOverlay = ({
interimText, interimText,
sttResponseText, sttResponseText,
handleTalkAgain, handleTalkAgain,
noInitErrorMessage,
]); ]);
// 입력창 포커스 핸들러 // 입력창 포커스 핸들러

View File

@@ -1,10 +0,0 @@
<div style={{width: '100%', height: '100%', position: 'relative'}}>
<div style={{width: 510, height: 25, left: 0, top: 0, position: 'absolute', background: '#424242', borderRadius: 100}} />
<div style={{width: 510, height: 25, left: 0, top: 0, position: 'absolute', opacity: 0.10, background: '#C70850', borderRadius: 100}} />
<div style={{width: 480, height: 25, left: 15, top: 0, position: 'absolute', opacity: 0.20, background: '#C70850', borderRadius: 100}} />
<div style={{width: 390, height: 25, left: 60, top: 0, position: 'absolute', opacity: 0.30, background: '#C70850', borderRadius: 100}} />
<div style={{width: 350, height: 25, left: 80, top: 0, position: 'absolute', opacity: 0.40, background: '#C70850', borderRadius: 100}} />
<div style={{width: 320, height: 25, left: 95, top: 0, position: 'absolute', opacity: 0.50, background: '#C70850', borderRadius: 100}} />
<div style={{width: 260, height: 25, left: 125, top: 0, position: 'absolute', background: '#C70850', borderRadius: 100}} />
</div>