[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:
2025-10-20 21:01:45 +09:00
parent d685c32c90
commit 9920facaf5

View File

@@ -138,6 +138,7 @@ const VoiceInputOverlay = ({
const wakeWordRestartTimerRef = useRef(null); const wakeWordRestartTimerRef = useRef(null);
const countdownIntervalRef = useRef(null); const countdownIntervalRef = useRef(null);
const sttDebounceTimerRef = useRef(null); // STT 결과 debounce 타이머 const sttDebounceTimerRef = useRef(null); // STT 결과 debounce 타이머
const silenceDetectionTimerRef = useRef(null); // 3초 silence detection 타이머
// All timer refs array for batch cleanup // All timer refs array for batch cleanup
const allTimerRefs = [ const allTimerRefs = [
@@ -149,6 +150,7 @@ const VoiceInputOverlay = ({
wakeWordRestartTimerRef, wakeWordRestartTimerRef,
countdownIntervalRef, countdownIntervalRef,
sttDebounceTimerRef, sttDebounceTimerRef,
silenceDetectionTimerRef,
]; ];
const [micFocused, setMicFocused] = useState(false); const [micFocused, setMicFocused] = useState(false);
@@ -320,6 +322,17 @@ const VoiceInputOverlay = ({
const shopperHouseDataRef = useRef(null); const shopperHouseDataRef = useRef(null);
const isInitializingRef = useRef(false); // overlay 초기화 중 플래그 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 변경 추적 // 🔍 DEBUG: shopperHouseData 변경 추적
useEffect(() => { useEffect(() => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
@@ -484,20 +497,41 @@ const VoiceInputOverlay = ({
// 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요 // 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요
// 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트 // 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트 + 3초 silence detection
useEffect(() => { useEffect(() => {
if (currentMode !== VOICE_MODES.LISTENING) return; if (currentMode !== VOICE_MODES.LISTENING) {
// ✅ LISTENING 모드가 아니면 타이머 정리
clearTimerRef(silenceDetectionTimerRef);
silenceDetectionTimerRef.current = null;
return;
}
// ✅ Ref 업데이트 (15초 타이머에서 최신 값을 읽을 수 있도록) // ✅ Ref 업데이트 (15초 타이머에서 최신 값을 읽을 수 있도록)
interimTextRef.current = interimText || ''; interimTextRef.current = interimText || '';
if (!interimText) return; if (!interimText) {
// 입력이 없으면 silence detection 시작하지 않음
return;
}
// ✨ TInput에 실시간으로 텍스트 표시 // ✨ TInput에 실시간으로 텍스트 표시
if (onSearchChange) { if (onSearchChange) {
onSearchChange({ value: interimText }); 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 모드에서 백그라운드 리스닝 시작 // 🎉 Wake Word Detection: PROMPT 모드에서 백그라운드 리스닝 시작
useEffect(() => { useEffect(() => {
@@ -558,23 +592,17 @@ const VoiceInputOverlay = ({
} }
}, [currentMode, voiceInputMode]); }, [currentMode, voiceInputMode]);
// VoiceInputOverlay 마운트 시 초기화 (한 번만 실행) // VoiceInputOverlay 마운트 시에만 초기화
useEffect(() => { useEffect(() => {
console.log('[VoiceInput] 🚀 VoiceInputOverlay 마운트 - searchId 초기화'); console.log('[VoiceInput] 🚀 VoiceInputOverlay 마운트됨 (searchId 유지)');
// ✨ Redux shopperHouseData 초기화 // Cleanup: 언마운트 시에만 초기화
dispatch(clearShopperHouseData());
// ✨ Redux lastSTTText 초기화
dispatch(clearSTTText());
// Cleanup: 언마운트 시 초기화
return () => { return () => {
console.log('[VoiceInput] 🔚 VoiceInputOverlay 언마운트 - searchId 초기화'); console.log('[VoiceInput] 🔚 VoiceInputOverlay 언마운트 - searchId 초기화');
dispatch(clearShopperHouseData()); dispatch(clearShopperHouseData());
dispatch(clearSTTText()); dispatch(clearSTTText());
}; };
}, []); // 빈 dependency - 마운트/언마운트 시에만 실행 }, []); // 빈 dependency - 언마운트 시에만 실행
// Overlay가 열릴 때 포커스를 overlay 내부로 이동 // Overlay가 열릴 때 포커스를 overlay 내부로 이동
useEffect(() => { useEffect(() => {
@@ -841,12 +869,75 @@ const VoiceInputOverlay = ({
setMicFocused(false); 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 닫기 핸들러 (모든 닫기 동작을 통합) // Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('🚪 [DEBUG] handleClose called - closing overlay'); console.log('🚪 [DEBUG] handleClose called - closing overlay');
} }
clearTimerRef(listeningTimerRef); clearTimerRef(listeningTimerRef);
clearTimerRef(silenceDetectionTimerRef);
setVoiceInputMode(null); setVoiceInputMode(null);
setCurrentMode(VOICE_MODES.PROMPT); setCurrentMode(VOICE_MODES.PROMPT);
setSttResponseText(''); setSttResponseText('');
@@ -891,6 +982,7 @@ const VoiceInputOverlay = ({
// 기존 타이머 정리 // 기존 타이머 정리
clearTimerRef(listeningTimerRef); clearTimerRef(listeningTimerRef);
clearTimerRef(silenceDetectionTimerRef);
setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH); setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH);
setCurrentMode(VOICE_MODES.LISTENING); setCurrentMode(VOICE_MODES.LISTENING);
@@ -898,53 +990,9 @@ const VoiceInputOverlay = ({
// WebSpeech API 시작 // WebSpeech API 시작
startListening(); startListening();
// 15초 타이머 설정: 15초 후 누적된 interimText를 최종 결과로 처리 // 15초 타이머 설정: 최대 입력 시간 제한
listeningTimerRef.current = setTimeout(() => { listeningTimerRef.current = setTimeout(() => {
if (DEBUG_MODE) { processFinalVoiceInput('15초 타임아웃');
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;
}, 15000); // 15초 }, 15000); // 15초
} else { } else {
// listening 모드 또는 기타 모드에서 클릭 시 -> overlay 닫기 // listening 모드 또는 기타 모드에서 클릭 시 -> overlay 닫기
@@ -954,7 +1002,16 @@ const VoiceInputOverlay = ({
handleClose(); handleClose();
} }
}, },
[currentMode, handleClose, playBeep, startListening, stopListening, onSearchChange] [
currentMode,
handleClose,
playBeep,
startListening,
stopListening,
onSearchChange,
dispatch,
processFinalVoiceInput,
]
); );
// 마이크 버튼 키다운 핸들러 // 마이크 버튼 키다운 핸들러