[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 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,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 마이크 버튼 키다운 핸들러
|
// 마이크 버튼 키다운 핸들러
|
||||||
|
|||||||
Reference in New Issue
Block a user