[251020] fix: VoiceInputOverlay Voice Enhancement-2
🕐 커밋 시간: 2025. 10. 20. 21:01:43 📊 변경 통계: • 총 파일: 1개 • 추가: +109줄 • 삭제: -61줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript): 🔄 Modified: clearAllTimers()
This commit is contained in:
@@ -138,6 +138,7 @@ const VoiceInputOverlay = ({
|
||||
const wakeWordRestartTimerRef = useRef(null);
|
||||
const countdownIntervalRef = useRef(null);
|
||||
const sttDebounceTimerRef = useRef(null); // STT 결과 debounce 타이머
|
||||
const silenceDetectionTimerRef = useRef(null); // 3초 silence detection 타이머
|
||||
|
||||
// All timer refs array for batch cleanup
|
||||
const allTimerRefs = [
|
||||
@@ -149,6 +150,7 @@ const VoiceInputOverlay = ({
|
||||
wakeWordRestartTimerRef,
|
||||
countdownIntervalRef,
|
||||
sttDebounceTimerRef,
|
||||
silenceDetectionTimerRef,
|
||||
];
|
||||
|
||||
const [micFocused, setMicFocused] = useState(false);
|
||||
@@ -320,6 +322,17 @@ const VoiceInputOverlay = ({
|
||||
const shopperHouseDataRef = useRef(null);
|
||||
const isInitializingRef = useRef(false); // overlay 초기화 중 플래그
|
||||
|
||||
// ✅ searchId ref 추가 - processFinalVoiceInput에서 최신 값을 읽기 위함
|
||||
const shopperHouseSearchIdRef = useRef(shopperHouseSearchId);
|
||||
|
||||
// ✅ searchId가 변경될 때마다 ref 업데이트
|
||||
useEffect(() => {
|
||||
shopperHouseSearchIdRef.current = shopperHouseSearchId;
|
||||
if (shopperHouseSearchId) {
|
||||
console.log('[VoiceInput] 🔄 searchId ref 업데이트:', shopperHouseSearchId);
|
||||
}
|
||||
}, [shopperHouseSearchId]);
|
||||
|
||||
// 🔍 DEBUG: shopperHouseData 변경 추적
|
||||
useEffect(() => {
|
||||
if (DEBUG_MODE) {
|
||||
@@ -484,20 +497,41 @@ const VoiceInputOverlay = ({
|
||||
|
||||
// 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요
|
||||
|
||||
// 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트
|
||||
// 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트 + 3초 silence detection
|
||||
useEffect(() => {
|
||||
if (currentMode !== VOICE_MODES.LISTENING) return;
|
||||
if (currentMode !== VOICE_MODES.LISTENING) {
|
||||
// ✅ LISTENING 모드가 아니면 타이머 정리
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
silenceDetectionTimerRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Ref 업데이트 (15초 타이머에서 최신 값을 읽을 수 있도록)
|
||||
interimTextRef.current = interimText || '';
|
||||
|
||||
if (!interimText) return;
|
||||
if (!interimText) {
|
||||
// 입력이 없으면 silence detection 시작하지 않음
|
||||
return;
|
||||
}
|
||||
|
||||
// ✨ TInput에 실시간으로 텍스트 표시
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: interimText });
|
||||
}
|
||||
}, [interimText, currentMode, onSearchChange]);
|
||||
|
||||
// ✅ 3초 silence detection: 마지막 입력 후 3초 동안 추가 입력이 없으면 자동 종료
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
silenceDetectionTimerRef.current = setTimeout(() => {
|
||||
console.log('[VoiceInput] 🔇 3초 동안 입력 없음 - 자동 종료');
|
||||
processFinalVoiceInput('3초 silence detection');
|
||||
}, 3000); // 3초
|
||||
|
||||
// Cleanup: 새로운 입력이 들어오거나 모드가 변경되면 타이머 리셋
|
||||
return () => {
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
silenceDetectionTimerRef.current = null;
|
||||
};
|
||||
}, [interimText, currentMode, onSearchChange, processFinalVoiceInput]);
|
||||
|
||||
// 🎉 Wake Word Detection: PROMPT 모드에서 백그라운드 리스닝 시작
|
||||
useEffect(() => {
|
||||
@@ -558,23 +592,17 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
}, [currentMode, voiceInputMode]);
|
||||
|
||||
// VoiceInputOverlay 마운트 시 초기화 (한 번만 실행)
|
||||
// VoiceInputOverlay 언마운트 시에만 초기화
|
||||
useEffect(() => {
|
||||
console.log('[VoiceInput] 🚀 VoiceInputOverlay 마운트 - searchId 초기화');
|
||||
console.log('[VoiceInput] 🚀 VoiceInputOverlay 마운트됨 (searchId 유지)');
|
||||
|
||||
// ✨ Redux shopperHouseData 초기화
|
||||
dispatch(clearShopperHouseData());
|
||||
|
||||
// ✨ Redux lastSTTText 초기화
|
||||
dispatch(clearSTTText());
|
||||
|
||||
// Cleanup: 언마운트 시 초기화
|
||||
// Cleanup: 언마운트 시에만 초기화
|
||||
return () => {
|
||||
console.log('[VoiceInput] 🔚 VoiceInputOverlay 언마운트 - searchId 초기화');
|
||||
dispatch(clearShopperHouseData());
|
||||
dispatch(clearSTTText());
|
||||
};
|
||||
}, []); // 빈 dependency - 마운트/언마운트 시에만 실행
|
||||
}, []); // 빈 dependency - 언마운트 시에만 실행
|
||||
|
||||
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
||||
useEffect(() => {
|
||||
@@ -841,12 +869,75 @@ const VoiceInputOverlay = ({
|
||||
setMicFocused(false);
|
||||
}, []);
|
||||
|
||||
// 🎤 음성 입력 최종 처리 함수 (15초 타이머 & 3초 silence detection 공통 사용)
|
||||
const processFinalVoiceInput = useCallback(
|
||||
(source) => {
|
||||
console.log(`[VoiceInput] 🏁 음성 입력 종료 (${source})`);
|
||||
|
||||
// 모든 타이머 정리 및 ref 초기화
|
||||
clearTimerRef(listeningTimerRef);
|
||||
clearIntervalRef(countdownIntervalRef);
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
|
||||
// ✅ Ref 명시적 초기화 (중요!)
|
||||
listeningTimerRef.current = null;
|
||||
countdownIntervalRef.current = null;
|
||||
silenceDetectionTimerRef.current = null;
|
||||
|
||||
// 음성 인식 중지
|
||||
stopListening();
|
||||
|
||||
// ✅ 누적된 interimText를 최종 결과로 사용
|
||||
const finalText = interimTextRef.current.trim();
|
||||
console.log('[VoiceInput] └─ 최종 텍스트:', 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();
|
||||
// ✅ Ref에서 최신 searchId 읽기 (useCallback closure 문제 해결)
|
||||
const currentSearchId = shopperHouseSearchIdRef.current;
|
||||
console.log('[VoiceInput] 📤 API 요청 전송');
|
||||
console.log('[VoiceInput] ├─ query:', query);
|
||||
console.log('[VoiceInput] ├─ ref 값:', shopperHouseSearchIdRef.current);
|
||||
console.log('[VoiceInput] ├─ currentSearchId:', currentSearchId);
|
||||
console.log('[VoiceInput] └─ searchId:', currentSearchId || '(없음 - 첫 번째 발화)');
|
||||
|
||||
dispatch(getShopperHouseSearch(query, currentSearchId));
|
||||
} else {
|
||||
// 입력이 없거나 너무 짧으면 PROMPT 모드로 복귀
|
||||
console.log('[VoiceInput] ⚠️ 입력 없음 또는 너무 짧음 - PROMPT 모드로 복귀');
|
||||
setCurrentMode(VOICE_MODES.PROMPT);
|
||||
setVoiceInputMode(null);
|
||||
}
|
||||
|
||||
// Ref 초기화
|
||||
interimTextRef.current = '';
|
||||
},
|
||||
[
|
||||
stopListening,
|
||||
addToSearchHistory,
|
||||
dispatch,
|
||||
// ✅ shopperHouseSearchId 제거 - ref 사용으로 closure 문제 해결
|
||||
]
|
||||
);
|
||||
|
||||
// Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
|
||||
const handleClose = useCallback(() => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🚪 [DEBUG] handleClose called - closing overlay');
|
||||
}
|
||||
clearTimerRef(listeningTimerRef);
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
setVoiceInputMode(null);
|
||||
setCurrentMode(VOICE_MODES.PROMPT);
|
||||
setSttResponseText('');
|
||||
@@ -891,6 +982,7 @@ const VoiceInputOverlay = ({
|
||||
|
||||
// 기존 타이머 정리
|
||||
clearTimerRef(listeningTimerRef);
|
||||
clearTimerRef(silenceDetectionTimerRef);
|
||||
|
||||
setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH);
|
||||
setCurrentMode(VOICE_MODES.LISTENING);
|
||||
@@ -898,53 +990,9 @@ const VoiceInputOverlay = ({
|
||||
// WebSpeech API 시작
|
||||
startListening();
|
||||
|
||||
// 15초 타이머 설정: 15초 후 누적된 interimText를 최종 결과로 처리
|
||||
// 15초 타이머 설정: 최대 입력 시간 제한
|
||||
listeningTimerRef.current = setTimeout(() => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('⏰ [VoiceInputOverlay.v2] 15초 타임아웃 - 최종 결과 처리');
|
||||
}
|
||||
|
||||
// 카운트다운 정리
|
||||
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;
|
||||
processFinalVoiceInput('15초 타임아웃');
|
||||
}, 15000); // 15초
|
||||
} else {
|
||||
// listening 모드 또는 기타 모드에서 클릭 시 -> overlay 닫기
|
||||
@@ -954,7 +1002,16 @@ const VoiceInputOverlay = ({
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[currentMode, handleClose, playBeep, startListening, stopListening, onSearchChange]
|
||||
[
|
||||
currentMode,
|
||||
handleClose,
|
||||
playBeep,
|
||||
startListening,
|
||||
stopListening,
|
||||
onSearchChange,
|
||||
dispatch,
|
||||
processFinalVoiceInput,
|
||||
]
|
||||
);
|
||||
|
||||
// 마이크 버튼 키다운 핸들러
|
||||
|
||||
Reference in New Issue
Block a user