[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:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 입력창 포커스 핸들러
|
// 입력창 포커스 핸들러
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user