[251020] fix: VoiceInputOverlay Voice Enhancement
🕐 커밋 시간: 2025. 10. 20. 20:44:44 📊 변경 통계: • 총 파일: 5개 • 추가: +140줄 • 삭제: -87줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/searchActions.js ~ com.twin.app.shoptime/src/actions/webSpeechActions.js ~ com.twin.app.shoptime/src/reducers/searchReducer.js ~ com.twin.app.shoptime/src/services/webSpeech/WebSpeechService.js ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript): 🔄 Modified: clearAllTimers() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • API 서비스 레이어 개선
This commit is contained in:
@@ -96,7 +96,12 @@ export const getShopperHouseSearch =
|
||||
const currentSearchKey = new Date().getTime();
|
||||
getShopperHouseSearchKey = currentSearchKey;
|
||||
|
||||
console.log('[ShopperHouse] 🔍 [DEBUG] API 호출 시작 - key:', currentSearchKey, 'query:', query);
|
||||
console.log(
|
||||
'[ShopperHouse] 🔍 [DEBUG] API 호출 시작 - key:',
|
||||
currentSearchKey,
|
||||
'query:',
|
||||
query
|
||||
);
|
||||
|
||||
const onSuccess = (response) => {
|
||||
console.log('[ShopperHouse] 📥 [DEBUG] API 응답 도착 - key:', currentSearchKey);
|
||||
@@ -105,21 +110,42 @@ export const getShopperHouseSearch =
|
||||
// ✨ 현재 요청이 최신 요청인지 확인
|
||||
if (currentSearchKey === getShopperHouseSearchKey) {
|
||||
console.log('[ShopperHouse] ✅ [DEBUG] 유효한 응답 - Redux 업데이트');
|
||||
console.log('[ShopperHouse] getShopperHouseSearch onSuccess: ', JSON.stringify(response.data));
|
||||
console.log(
|
||||
'[ShopperHouse] getShopperHouseSearch onSuccess: ',
|
||||
JSON.stringify(response.data)
|
||||
);
|
||||
|
||||
// ✅ API 성공 여부 확인
|
||||
const retCode = response.data?.retCode;
|
||||
if (retCode !== 0) {
|
||||
console.error('[ShopperHouse] ❌ API 실패 - retCode:', retCode, 'retMsg:', response.data?.retMsg);
|
||||
console.error(
|
||||
'[ShopperHouse] ❌ API 실패 - retCode:',
|
||||
retCode,
|
||||
'retMsg:',
|
||||
response.data?.retMsg
|
||||
);
|
||||
console.log('[VoiceInput] 📥 API 응답 실패');
|
||||
console.log('[VoiceInput] ├─ retCode:', retCode);
|
||||
console.log('[VoiceInput] └─ retMsg:', response.data?.retMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ result 데이터 존재 확인
|
||||
if (!response.data?.data?.result) {
|
||||
console.error('[ShopperHouse] ❌ API 응답에 result 데이터 없음');
|
||||
console.log('[VoiceInput] 📥 API 응답 실패 (result 데이터 없음)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 📥 [VoiceInput] API 응답 성공 로그
|
||||
const resultData = response.data.data.result;
|
||||
const results = resultData.results || [];
|
||||
const receivedSearchId = results.length > 0 ? results[0].searchId : null;
|
||||
|
||||
console.log('[VoiceInput] 📥 API 응답 성공');
|
||||
console.log('[VoiceInput] ├─ searchId:', receivedSearchId || '(없음)');
|
||||
console.log('[VoiceInput] └─ 결과 수:', results.length);
|
||||
|
||||
dispatch({
|
||||
type: types.GET_SHOPPERHOUSE_SEARCH,
|
||||
payload: response.data,
|
||||
@@ -133,6 +159,15 @@ export const getShopperHouseSearch =
|
||||
|
||||
const onFail = (error) => {
|
||||
console.error('[ShopperHouse] getShopperHouseSearch onFail: ', JSON.stringify(error));
|
||||
|
||||
const retCode = error?.data?.retCode;
|
||||
if (retCode === 401) {
|
||||
console.log('[VoiceInput] ⚠️ Access Token 만료 - 자동 갱신 중...');
|
||||
console.log('[VoiceInput] └─ TAxios가 자동으로 재요청합니다');
|
||||
} else {
|
||||
console.log('[VoiceInput] 📥 API 요청 실패');
|
||||
console.log('[VoiceInput] └─ error:', error?.message || JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
|
||||
const params = { query };
|
||||
|
||||
@@ -49,20 +49,16 @@ export const initializeWebSpeech =
|
||||
webSpeechService.on('result', (result) => {
|
||||
console.log('[WebSpeechActions] Result:', result);
|
||||
|
||||
// ✅ continuous: true 모드에서는 중간 final result를 무시하고
|
||||
// 항상 interim result만 사용 (15초 타이머로 제어)
|
||||
// Interim 결과 (중간 결과) - 전체 연결된 텍스트 사용
|
||||
if (!result.isFinal) {
|
||||
dispatch({
|
||||
type: types.WEB_SPEECH_INTERIM_RESULT,
|
||||
payload: result.transcript, // 이미 전체 연결된 텍스트 (final + interim)
|
||||
});
|
||||
}
|
||||
// Final 결과 (최종 결과)
|
||||
else {
|
||||
dispatch({
|
||||
type: types.VOICE_STT_TEXT_RECEIVED, // 기존 VUI와 동일한 액션 사용
|
||||
payload: result.transcript, // 전체 최종 텍스트
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: types.WEB_SPEECH_INTERIM_RESULT,
|
||||
payload: result.transcript, // 이미 전체 연결된 텍스트 (final + interim)
|
||||
});
|
||||
|
||||
// ✅ Final 결과는 무시 (15초 타이머가 끝날 때 VoiceInputOverlay에서 처리)
|
||||
// continuous: true일 때 중간에 final이 와도 계속 듣기
|
||||
});
|
||||
|
||||
webSpeechService.on('error', (errorInfo) => {
|
||||
|
||||
@@ -73,6 +73,10 @@ export const searchReducer = (state = initialState, action) => {
|
||||
// searchId 추출 (첫 번째 result에서)
|
||||
const searchId = results.length > 0 ? results[0].searchId : null;
|
||||
|
||||
// [VoiceInput] Redux에 searchId 저장 로그
|
||||
console.log('[VoiceInput] 💾 Redux에 searchId 저장');
|
||||
console.log('[VoiceInput] └─ searchId:', searchId || '(없음)');
|
||||
|
||||
return {
|
||||
...state,
|
||||
shopperHouseData: resultData,
|
||||
@@ -86,6 +90,7 @@ export const searchReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
case types.CLEAR_SHOPPERHOUSE_DATA:
|
||||
console.log('[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId 리셋)');
|
||||
return {
|
||||
...state,
|
||||
shopperHouseData: null,
|
||||
|
||||
@@ -83,9 +83,14 @@ class WebSpeechService {
|
||||
let finalTranscript = '';
|
||||
let interimTranscript = '';
|
||||
|
||||
console.log('[WebSpeech] 🎤 onresult 이벤트 - results.length:', results.length);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const transcript = results[i][0].transcript;
|
||||
if (results[i].isFinal) {
|
||||
const isFinal = results[i].isFinal;
|
||||
console.log(`[WebSpeech] [${i}] isFinal: ${isFinal}, transcript: "${transcript}"`);
|
||||
|
||||
if (isFinal) {
|
||||
finalTranscript += transcript + ' ';
|
||||
} else {
|
||||
interimTranscript += transcript;
|
||||
@@ -97,10 +102,10 @@ class WebSpeechService {
|
||||
const isFinal = lastResult.isFinal;
|
||||
const confidence = lastResult[0].confidence;
|
||||
|
||||
console.log('[WebSpeech] Result:', {
|
||||
console.log('[WebSpeech] 📝 Result Summary:', {
|
||||
fullTranscript,
|
||||
finalTranscript,
|
||||
interimTranscript,
|
||||
finalTranscript: finalTranscript.trim(),
|
||||
interimTranscript: interimTranscript.trim(),
|
||||
isFinal,
|
||||
confidence,
|
||||
});
|
||||
|
||||
@@ -128,6 +128,7 @@ const VoiceInputOverlay = ({
|
||||
const lastFocusedElement = useRef(null);
|
||||
const listeningTimerRef = useRef(null);
|
||||
const audioContextRef = useRef(null);
|
||||
const interimTextRef = useRef(''); // Interim text 추적용 ref
|
||||
|
||||
// Timer refs for cleanup
|
||||
const closeTimerRef = useRef(null);
|
||||
@@ -136,6 +137,7 @@ const VoiceInputOverlay = ({
|
||||
const searchSubmitFocusTimerRef = useRef(null);
|
||||
const wakeWordRestartTimerRef = useRef(null);
|
||||
const countdownIntervalRef = useRef(null);
|
||||
const sttDebounceTimerRef = useRef(null); // STT 결과 debounce 타이머
|
||||
|
||||
// All timer refs array for batch cleanup
|
||||
const allTimerRefs = [
|
||||
@@ -146,6 +148,7 @@ const VoiceInputOverlay = ({
|
||||
searchSubmitFocusTimerRef,
|
||||
wakeWordRestartTimerRef,
|
||||
countdownIntervalRef,
|
||||
sttDebounceTimerRef,
|
||||
];
|
||||
|
||||
const [micFocused, setMicFocused] = useState(false);
|
||||
@@ -182,7 +185,9 @@ const VoiceInputOverlay = ({
|
||||
useEffect(() => {
|
||||
console.log('🔀 [DEBUG][VoiceInputOverlay] currentMode changed to:', currentMode);
|
||||
if (isVisible) {
|
||||
console.log(`📍 [DEBUG][VoiceInputOverlay] Current state: isVisible=true, mode=${currentMode}`);
|
||||
console.log(
|
||||
`📍 [DEBUG][VoiceInputOverlay] Current state: isVisible=true, mode=${currentMode}`
|
||||
);
|
||||
}
|
||||
}, [currentMode, isVisible]);
|
||||
|
||||
@@ -287,68 +292,31 @@ const VoiceInputOverlay = ({
|
||||
});
|
||||
}, []); // dependency 없음 (setSearchHistory는 stable)
|
||||
|
||||
// Web Speech API Hook (WebSpeech 모드일 때만 활성화)
|
||||
const handleWebSpeechSTT = useCallback(
|
||||
(sttText) => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🎤 [DEBUG] handleWebSpeechSTT called with:', sttText);
|
||||
}
|
||||
|
||||
// 타이머 중지
|
||||
clearTimerRef(listeningTimerRef);
|
||||
|
||||
// STT 텍스트 저장
|
||||
setSttResponseText(sttText);
|
||||
|
||||
// RESPONSE 모드로 전환
|
||||
setCurrentMode(VOICE_MODES.RESPONSE);
|
||||
if (DEBUG_MODE) {
|
||||
console.log('📺 [DEBUG] Switching to RESPONSE mode with text:', sttText);
|
||||
}
|
||||
|
||||
// ✅ TInput에 텍스트 표시
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: sttText });
|
||||
}
|
||||
|
||||
// ✨ 검색 기록에 추가
|
||||
addToSearchHistory(sttText);
|
||||
|
||||
// ✨ ShopperHouse API 자동 호출
|
||||
if (sttText && sttText.trim().length >= 3) {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🔍 [DEBUG] Calling ShopperHouse API from STT with query:', sttText.trim());
|
||||
console.log('🔍 [DEBUG] Query length:', sttText.trim().length);
|
||||
console.log('🔍 [DEBUG] Query contains apostrophe:', sttText.includes("'"));
|
||||
}
|
||||
dispatch(getShopperHouseSearch(sttText.trim()));
|
||||
}
|
||||
},
|
||||
[dispatch, addToSearchHistory, onSearchChange]
|
||||
);
|
||||
|
||||
// WebSpeech config 메모이제이션 (불필요한 재초기화 방지)
|
||||
const webSpeechConfig = useMemo(
|
||||
() => ({
|
||||
lang: 'en-US',
|
||||
continuous: false, // 침묵 감지 후 자동 종료
|
||||
continuous: true, // 15초 동안 계속 듣기 (침묵 감지로 중단되지 않음)
|
||||
interimResults: true,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// useWebSpeech를 항상 활성화 (SearchPanel이 마운트되어 있는 동안 계속 유지)
|
||||
// isVisible 변경으로 인한 재초기화 방지
|
||||
// STT 결과는 Redux lastSTTText로 직접 처리 (stopListening dependency 문제 해결)
|
||||
const { isListening, interimText, startListening, stopListening, isSupported } = useWebSpeech(
|
||||
isVisible, // Overlay가 열려있을 때만 활성화 (voiceInputMode와 무관하게 초기화)
|
||||
handleWebSpeechSTT,
|
||||
true, // 항상 활성화 - VoiceInputOverlay가 마운트된 동안 유지
|
||||
null, // Redux lastSTTText useEffect로 직접 처리
|
||||
webSpeechConfig
|
||||
);
|
||||
|
||||
// ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지
|
||||
// Redux에서 voice 상태 가져오기
|
||||
// const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
||||
// Redux에서 STT 결과 가져오기
|
||||
const { lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
||||
|
||||
// Redux에서 shopperHouse 검색 결과 가져오기 (simplified ref usage)
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId); // 2차 발화용 searchId
|
||||
const shopperHouseDataRef = useRef(null);
|
||||
const isInitializingRef = useRef(false); // overlay 초기화 중 플래그
|
||||
|
||||
@@ -516,6 +484,21 @@ const VoiceInputOverlay = ({
|
||||
|
||||
// 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요
|
||||
|
||||
// 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트
|
||||
useEffect(() => {
|
||||
if (currentMode !== VOICE_MODES.LISTENING) return;
|
||||
|
||||
// ✅ Ref 업데이트 (15초 타이머에서 최신 값을 읽을 수 있도록)
|
||||
interimTextRef.current = interimText || '';
|
||||
|
||||
if (!interimText) return;
|
||||
|
||||
// ✨ TInput에 실시간으로 텍스트 표시
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: interimText });
|
||||
}
|
||||
}, [interimText, currentMode, onSearchChange]);
|
||||
|
||||
// 🎉 Wake Word Detection: PROMPT 모드에서 백그라운드 리스닝 시작
|
||||
useEffect(() => {
|
||||
if (!ENABLE_WAKE_WORD) return;
|
||||
@@ -575,6 +558,24 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
}, [currentMode, voiceInputMode]);
|
||||
|
||||
// VoiceInputOverlay 마운트 시 초기화 (한 번만 실행)
|
||||
useEffect(() => {
|
||||
console.log('[VoiceInput] 🚀 VoiceInputOverlay 마운트 - searchId 초기화');
|
||||
|
||||
// ✨ Redux shopperHouseData 초기화
|
||||
dispatch(clearShopperHouseData());
|
||||
|
||||
// ✨ Redux lastSTTText 초기화
|
||||
dispatch(clearSTTText());
|
||||
|
||||
// Cleanup: 언마운트 시 초기화
|
||||
return () => {
|
||||
console.log('[VoiceInput] 🔚 VoiceInputOverlay 언마운트 - searchId 초기화');
|
||||
dispatch(clearShopperHouseData());
|
||||
dispatch(clearSTTText());
|
||||
};
|
||||
}, []); // 빈 dependency - 마운트/언마운트 시에만 실행
|
||||
|
||||
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
||||
useEffect(() => {
|
||||
if (DEBUG_MODE) {
|
||||
@@ -584,15 +585,8 @@ const VoiceInputOverlay = ({
|
||||
if (isVisible) {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('✅ [DEBUG] ===== OVERLAY OPENING =====');
|
||||
console.log('🔍 [DEBUG] Clearing Redux shopperHouseData');
|
||||
}
|
||||
|
||||
// ✨ Redux shopperHouseData 초기화 (이전 검색 결과 제거)
|
||||
dispatch(clearShopperHouseData());
|
||||
|
||||
// ✨ Redux lastSTTText 초기화 (이전 음성 인식 텍스트 제거)
|
||||
dispatch(clearSTTText());
|
||||
|
||||
// ✨ 초기화 시작 플래그 설정 (close 로직 일시 차단)
|
||||
isInitializingRef.current = true;
|
||||
if (DEBUG_MODE) {
|
||||
@@ -634,12 +628,6 @@ const VoiceInputOverlay = ({
|
||||
// 타이머 정리
|
||||
clearTimerRef(listeningTimerRef);
|
||||
|
||||
// ⛔ 독립 테스트: WebSpeech API 호출 비활성화
|
||||
// WebSpeech 중지 (비동기로 처리)
|
||||
// if (isListening) {
|
||||
// stopListening();
|
||||
// }
|
||||
|
||||
// 상태 초기화
|
||||
setVoiceInputMode(null);
|
||||
setCurrentMode(VOICE_MODES.PROMPT);
|
||||
@@ -652,7 +640,7 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup: 컴포넌트 언마운트 또는 isVisible 변경 시 타이머 정리
|
||||
// Cleanup: isVisible 변경 시 타이머 정리
|
||||
clearTimerRef(focusTimerRef);
|
||||
clearTimerRef(focusRestoreTimerRef);
|
||||
};
|
||||
@@ -765,16 +753,23 @@ const VoiceInputOverlay = ({
|
||||
if (DEBUG_MODE) {
|
||||
console.log(
|
||||
'🎬 [DEBUG][VoiceInputOverlay] renderModeContent called',
|
||||
'| currentMode:', currentMode,
|
||||
'| voiceInputMode:', voiceInputMode,
|
||||
'| isListening:', isListening
|
||||
'| currentMode:',
|
||||
currentMode,
|
||||
'| voiceInputMode:',
|
||||
voiceInputMode,
|
||||
'| isListening:',
|
||||
isListening
|
||||
);
|
||||
}
|
||||
|
||||
switch (currentMode) {
|
||||
case VOICE_MODES.PROMPT:
|
||||
if (DEBUG_MODE) {
|
||||
console.log('✅ [DEBUG][VoiceInputOverlay] MODE = PROMPT | Rendering VoicePromptScreen with', searchHistory.length, 'suggestions');
|
||||
console.log(
|
||||
'✅ [DEBUG][VoiceInputOverlay] MODE = PROMPT | Rendering VoicePromptScreen with',
|
||||
searchHistory.length,
|
||||
'suggestions'
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VoicePromptScreen
|
||||
@@ -784,12 +779,17 @@ const VoiceInputOverlay = ({
|
||||
);
|
||||
case VOICE_MODES.LISTENING:
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🎤 [DEBUG][VoiceInputOverlay] MODE = LISTENING | Rendering VoiceListening (15초 타이머)');
|
||||
console.log(
|
||||
'🎤 [DEBUG][VoiceInputOverlay] MODE = LISTENING | Rendering VoiceListening (15초 타이머)'
|
||||
);
|
||||
}
|
||||
return <VoiceListening interimText={interimText} countdown={countdown} />;
|
||||
case VOICE_MODES.RESPONSE:
|
||||
if (DEBUG_MODE) {
|
||||
console.log('💬 [DEBUG][VoiceInputOverlay] MODE = RESPONSE | Rendering VoiceResponse with text:', sttResponseText);
|
||||
console.log(
|
||||
'💬 [DEBUG][VoiceInputOverlay] MODE = RESPONSE | Rendering VoiceResponse with text:',
|
||||
sttResponseText
|
||||
);
|
||||
}
|
||||
return <VoiceResponse responseText={sttResponseText} />;
|
||||
case VOICE_MODES.NOINIT:
|
||||
@@ -799,7 +799,9 @@ const VoiceInputOverlay = ({
|
||||
return <VoiceNotRecognized prompt={NOINIT_ERROR_MESSAGE} />;
|
||||
case VOICE_MODES.NOTRECOGNIZED:
|
||||
if (DEBUG_MODE) {
|
||||
console.log('❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized');
|
||||
console.log(
|
||||
'❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized'
|
||||
);
|
||||
}
|
||||
return <VoiceNotRecognized />;
|
||||
case VOICE_MODES.MODE_3:
|
||||
@@ -855,10 +857,7 @@ const VoiceInputOverlay = ({
|
||||
const handleWebSpeechMicClick = useCallback(
|
||||
(e) => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log(
|
||||
'🎤 [DEBUG] handleWebSpeechMicClick called, currentMode:',
|
||||
currentMode
|
||||
);
|
||||
console.log('🎤 [DEBUG] handleWebSpeechMicClick called, currentMode:', currentMode);
|
||||
}
|
||||
|
||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||
@@ -873,15 +872,20 @@ const VoiceInputOverlay = ({
|
||||
// ✨ PROMPT 모드에서만 LISTENING으로 전환 가능
|
||||
// 1. listening 모드로 전환 (15초 타이머)
|
||||
// 2. WebSpeech API 시작 (독립 동작)
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🎤 [DEBUG] Starting LISTENING mode (15s) + WebSpeech API');
|
||||
}
|
||||
console.log('[VoiceInput] 🎙️ 마이크 버튼 클릭 - 음성 입력 시작');
|
||||
console.log('[VoiceInput] └─ 이전 STT 데이터 초기화');
|
||||
|
||||
// ✅ TInput 초기화 (새로운 음성 입력 시작)
|
||||
// ✅ 이전 STT 데이터 초기화 (새로운 음성 입력 시작)
|
||||
dispatch(clearSTTText());
|
||||
|
||||
// ✅ TInput 초기화
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: '' });
|
||||
}
|
||||
|
||||
// ✅ Interim text ref 초기화
|
||||
interimTextRef.current = '';
|
||||
|
||||
// 🔊 Beep 소리 재생
|
||||
playBeep();
|
||||
|
||||
@@ -894,16 +898,53 @@ const VoiceInputOverlay = ({
|
||||
// WebSpeech API 시작
|
||||
startListening();
|
||||
|
||||
// 15초 타이머 설정 (WebSpeech 종료와 무관하게 15초 후 PROMPT 복귀)
|
||||
// 15초 타이머 설정: 15초 후 누적된 interimText를 최종 결과로 처리
|
||||
listeningTimerRef.current = setTimeout(() => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('⏰ [VoiceInputOverlay.v2] 15초 타임아웃 - PROMPT 모드로 복귀');
|
||||
console.log('⏰ [VoiceInputOverlay.v2] 15초 타임아웃 - 최종 결과 처리');
|
||||
}
|
||||
setCurrentMode(VOICE_MODES.PROMPT);
|
||||
setVoiceInputMode(null);
|
||||
listeningTimerRef.current = null;
|
||||
// WebSpeech가 아직 동작 중이면 중지
|
||||
|
||||
// 카운트다운 정리
|
||||
clearIntervalRef(countdownIntervalRef);
|
||||
|
||||
// 음성 인식 중지
|
||||
stopListening();
|
||||
|
||||
// ✅ 15초 동안 누적된 interimText를 최종 결과로 사용 (ref로부터 읽기)
|
||||
const finalText = interimTextRef.current.trim();
|
||||
console.log('[VoiceInput] 🏁 15초 타이머 종료 - 최종 텍스트:', finalText);
|
||||
|
||||
if (finalText && finalText.length >= 3) {
|
||||
// STT 텍스트 저장
|
||||
setSttResponseText(finalText);
|
||||
|
||||
// RESPONSE 모드로 전환
|
||||
setCurrentMode(VOICE_MODES.RESPONSE);
|
||||
setVoiceInputMode(null);
|
||||
|
||||
// ✨ 검색 기록에 추가
|
||||
addToSearchHistory(finalText);
|
||||
|
||||
// ✨ ShopperHouse API 자동 호출 (2차 발화 시 searchId 포함)
|
||||
const query = finalText.trim();
|
||||
console.log('[VoiceInput] 📤 API 요청 전송');
|
||||
console.log('[VoiceInput] ├─ query:', query);
|
||||
console.log(
|
||||
'[VoiceInput] └─ searchId:',
|
||||
shopperHouseSearchId || '(없음 - 첫 번째 발화)'
|
||||
);
|
||||
|
||||
dispatch(getShopperHouseSearch(query, shopperHouseSearchId));
|
||||
} else {
|
||||
// 입력이 없거나 너무 짧으면 PROMPT 모드로 복귀
|
||||
console.log('[VoiceInput] ⚠️ 입력 없음 또는 너무 짧음 - PROMPT 모드로 복귀');
|
||||
setCurrentMode(VOICE_MODES.PROMPT);
|
||||
setVoiceInputMode(null);
|
||||
}
|
||||
|
||||
// Ref 초기화
|
||||
interimTextRef.current = '';
|
||||
listeningTimerRef.current = null;
|
||||
}, 15000); // 15초
|
||||
} else {
|
||||
// listening 모드 또는 기타 모드에서 클릭 시 -> overlay 닫기
|
||||
|
||||
Reference in New Issue
Block a user