[251022] fix: VoiceInputOverlay Bubble Search and 코드정리

🕐 커밋 시간: 2025. 10. 22. 13:07:25

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +163줄
  • 삭제: -142줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx (javascript):
     Added: handleSpotlightUp()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
     Deleted: clearAllTimers()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx (javascript):
     Added: getErrorMessage(), getErrorDetails()
    🔄 Modified: Spottable()
     Deleted: getErrorMessage(), getErrorDetails()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator()
     Deleted: handleClick()

🔧 주요 변경 내용:
  • API 서비스 레이어 개선
This commit is contained in:
2025-10-22 13:07:27 +09:00
parent 94f06f9820
commit 595f4bd473
5 changed files with 326 additions and 309 deletions

View File

@@ -9,17 +9,18 @@ import css from './ShopNowButton.module.less';
const SpottableDiv = Spottable('div');
export default function ShopNowButton({ onClick }) {
const handleSpotlightUp = (e) => {
e.stopPropagation();
e.preventDefault();
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
};
return (
<div className={css.container}>
<SpottableDiv
className={css.shopNowButton}
onClick={onClick}
spotlightId="below-tab-shop-now-button"
onSpotlightUp={(e) => {
e.stopPropagation();
e.preventDefault();
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
}}
onSpotlightUp={handleSpotlightUp}
>
<span className={css.buttonText}>SHOP NOW</span>
</SpottableDiv>

View File

@@ -115,7 +115,6 @@ const VoiceInputOverlay = ({
isVisible,
onClose,
mode = VOICE_MODES.PROMPT,
suggestions = [],
searchQuery = '',
onSearchChange,
}) => {
@@ -164,6 +163,8 @@ const VoiceInputOverlay = ({
const [errorMessage, setErrorMessage] = useState('');
// WebSpeech 이벤트 로그 저장 (디버그용)
const [webSpeechEventLogs, setWebSpeechEventLogs] = useState([]);
// Bubble 클릭으로 검색이 시작되었는지 추적하는 상태
const [isBubbleClickSearch, setIsBubbleClickSearch] = useState(false);
// 검색 기록 관리 (localStorage 기반, 최근 5개)
const [searchHistory, setSearchHistory] = useState(() => {
const history = readLocalStorage(SEARCH_HISTORY_KEY, DEFAULT_SUGGESTIONS);
@@ -241,28 +242,7 @@ const VoiceInputOverlay = ({
);
// Redux에서 STT 결과 및 에러 상태 가져오기
const { lastSTTText, sttTimestamp, webSpeech } = useSelector((state) => state.voice);
// 🎤 WebSpeech 이벤트 로그 관리 함수
const addWebSpeechEventLog = useCallback((event, details = '') => {
const timestamp = new Date().toLocaleTimeString();
const newLog = {
id: Date.now(),
timestamp,
event,
details,
type: getWebSpeechEventType(event),
};
if (DEBUG_MODE) {
console.log(`[WebSpeech Event] ${event}: ${details}`);
}
setWebSpeechEventLogs((prev) => {
const updated = [newLog, ...prev];
return updated.slice(0, 10); // 최근 10개만 유지
});
}, []);
const { lastSTTText, sttTimestamp, webSpeech = {} } = useSelector((state) => state.voice);
// 이벤트 타입별 색상 분류
const getWebSpeechEventType = useCallback((event) => {
@@ -280,6 +260,30 @@ const VoiceInputOverlay = ({
return types[event] || 'info';
}, []);
// 🎤 WebSpeech 이벤트 로그 관리 함수
const addWebSpeechEventLog = useCallback(
(event, details = '') => {
const timestamp = new Date().toLocaleTimeString();
const newLog = {
id: Date.now(),
timestamp,
event,
details,
type: getWebSpeechEventType(event),
};
if (DEBUG_MODE) {
console.log(`[WebSpeech Event] ${event}: ${details}`);
}
setWebSpeechEventLogs((prev) => {
const updated = [newLog, ...prev];
return updated.slice(0, 10); // 최근 10개만 유지
});
},
[getWebSpeechEventType]
);
// Redux에서 shopperHouse 검색 결과 및 에러 가져오기 (simplified ref usage)
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId); // 2차 발화용 searchId
@@ -312,10 +316,18 @@ const VoiceInputOverlay = ({
// 🚨 WebSpeech 에러 감시 및 처리 (Phase 1)
useEffect(() => {
if (webSpeech.error) {
// Bubble 클릭으로 검색이 시작된 경우 WebSpeech 오류를 우회
if (isBubbleClickSearch) {
if (DEBUG_MODE) {
console.log('[VoiceInput] 🔇 WebSpeech 오류 우회 (bubble click search)');
}
return;
}
if (webSpeech && webSpeech.error) {
console.error('[VoiceInput] 🔴 WebSpeech error detected:', webSpeech.error);
console.error('[VoiceInput] ├─ error type:', webSpeech.error);
console.error('[VoiceInput] ├─ message:', webSpeech.message);
console.error('[VoiceInput] ├─ message:', webSpeech.message || 'No message');
console.error('[VoiceInput] ├─ current mode:', currentMode);
console.error('[VoiceInput] └─ is listening:', isListening);
@@ -373,12 +385,14 @@ const VoiceInputOverlay = ({
}
}, [
webSpeech.error,
webSpeech.message,
currentMode,
isListening,
isVisible,
stopListening,
restartWebSpeech,
addWebSpeechEventLog,
isBubbleClickSearch,
]);
// 🎤 WebSpeech 이벤트 감지 (전용 디버그용)
@@ -739,7 +753,7 @@ const VoiceInputOverlay = ({
dispatch(clearShopperHouseData());
dispatch(clearSTTText());
};
}, []); // 빈 dependency - 언마운트 시에만 실행
}, [dispatch]); // 언마운트 시에만 실행
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
useEffect(() => {
@@ -841,6 +855,7 @@ const VoiceInputOverlay = ({
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
setSttResponseText('');
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
if (DEBUG_MODE) {
console.log('[VoiceInput] ✅ API error restart complete - ready for new input');
@@ -916,6 +931,7 @@ const VoiceInputOverlay = ({
// PROMPT 모드로 복귀
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
// 약간의 지연 후 새로 시작 (안정성을 위해)
setTimeout(() => {
@@ -925,240 +941,6 @@ const VoiceInputOverlay = ({
}, 300);
}, [dispatch, onSearchChange, addWebSpeechEventLog]);
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트 설정 + API 자동 호출 + response 모드 전환
const handleSuggestionClick = useCallback(
(suggestion) => {
if (DEBUG_MODE) {
console.log('💡 [DEBUG] handleSuggestionClick called with:', suggestion);
}
// 따옴표 제거
const query = suggestion.replace(/^["']|["']$/g, '').trim();
// Input 창에 텍스트 설정 (유지)
if (onSearchChange) {
onSearchChange({ value: query });
}
// ✨ 검색 기록에 추가
addToSearchHistory(query);
// ✨ RESPONSE 모드로 전환을 위한 텍스트 설정
setSttResponseText(query);
// ✨ RESPONSE 모드로 직접 전환
setCurrentMode(VOICE_MODES.RESPONSE);
setVoiceInputMode(null);
// ✨ ShopperHouse API 자동 호출 (searchId 포함)
if (query && query.length >= 3) {
// ✅ Ref에서 최신 searchId 읽기 (이전 검색이 있는 경우 2차 발화 처리)
const currentSearchId = shopperHouseSearchIdRef.current;
if (DEBUG_MODE) {
console.log('🔍 [DEBUG] Calling ShopperHouse API from bubble click');
console.log('[VoiceInput] ├─ query:', query);
console.log('[VoiceInput] └─ searchId:', currentSearchId || '(없음 - 첫 번째 발화)');
}
try {
dispatch(getShopperHouseSearch(query, currentSearchId));
} catch (error) {
console.error('[VoiceInput] ❌ API 호출 실패:', error);
// 에러 발생 시 PROMPT 모드로 복귀
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
}
}
},
[onSearchChange, dispatch, addToSearchHistory]
);
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
const handleSearchSubmit = useCallback(() => {
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] Search submit:', searchQuery);
}
if (searchQuery && searchQuery.trim()) {
// ShopperHouse API 호출
dispatch(getShopperHouseSearch(searchQuery.trim()));
// Input 내용 비우기
if (onSearchChange) {
onSearchChange({ value: '' });
}
// Clear existing timer before setting new one
clearTimerRef(searchSubmitFocusTimerRef);
// API 호출 후 Input 박스로 포커스 이동
searchSubmitFocusTimerRef.current = setTimeout(() => {
Spotlight.focus(INPUT_SPOTLIGHT_ID);
}, 100);
// VoiceInputOverlay는 SearchPanel과 다른 API를 사용하므로 onSearchSubmit 호출 안 함
// if (onSearchSubmit) {
// onSearchSubmit(searchQuery);
// }
}
}, [dispatch, searchQuery, onSearchChange]);
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
const handleInputKeyDown = useCallback((e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
// Enter 키로는 API 호출하지 않음
// 돋보기 아이콘 클릭/Enter로만 API 호출
}
}, []);
// Input 모드 변경 핸들러 - Input 모드로 전환되면 VoiceInputOverlay 닫기
const handleInputModeChange = useCallback(
(mode) => {
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay] TInput 모드 변경:', mode);
}
if (mode === 'input') {
// Input 모드로 전환되면 Overlay 닫기
// SearchPanel의 TInput으로 자연스럽게 전환됨
if (DEBUG_MODE) {
console.log('⌨️ [DEBUG] Input 모드로 전환됨 - VoiceInputOverlay 닫고 SearchPanel로 이동');
}
handleClose();
}
},
[handleClose]
);
// ⛔ TALK AGAIN 버튼 제거됨 - 더 이상 사용하지 않음
// const handleTalkAgain = useCallback(() => { ... }, []);
// 모드에 따른 컨텐츠 렌더링 - Memoized
const renderModeContent = useMemo(() => {
if (DEBUG_MODE) {
console.log(
'🎬 [DEBUG][VoiceInputOverlay] renderModeContent called',
'| 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'
);
}
return (
<VoicePromptScreen
suggestions={searchHistory}
onSuggestionClick={handleSuggestionClick}
/>
);
case VOICE_MODES.LISTENING:
if (DEBUG_MODE) {
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,
'isLoading: true (항상 로딩 애니메이션 표시)'
);
}
// ✅ RESPONSE 모드에서는 항상 로딩 애니메이션 표시
// shopperHouseData는 overlay 닫기에만 사용 (searchId 유지 필요)
return (
<VoiceResponse
responseText={sttResponseText}
isLoading={true}
query={sttResponseText}
searchId={shopperHouseSearchId || ''}
/>
);
case VOICE_MODES.NOINIT:
if (DEBUG_MODE) {
console.log('⚠️ [DEBUG][VoiceInputOverlay] MODE = NOINIT | Rendering VoiceNotRecognized');
}
return <VoiceNotRecognized prompt={NOINIT_ERROR_MESSAGE} />;
case VOICE_MODES.NOTRECOGNIZED:
if (DEBUG_MODE) {
console.log(
'❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized with error message'
);
}
return (
<VoiceNotRecognized
prompt={errorMessage || '음성 인식에 문제가 발생했습니다.'}
onRestart={restartWebSpeech}
/>
);
case VOICE_MODES.APIERROR:
if (DEBUG_MODE) {
console.log(
'💥 [DEBUG][VoiceInputOverlay] MODE = APIERROR | Rendering VoiceApiError with error details',
shopperHouseError
);
}
return (
<VoiceApiError
error={shopperHouseError}
onRetry={handleApiErrorRetry}
onRestart={handleApiErrorRestart}
/>
);
case VOICE_MODES.MODE_3:
// 추후 MODE_3 컴포넌트 추가
return <VoiceNotRecognized />;
case VOICE_MODES.MODE_4:
// 추후 MODE_4 컴포넌트 추가
return <VoiceNotRecognizedCircle />;
default:
if (DEBUG_MODE) {
console.log('🔄 [DEBUG][VoiceInputOverlay] MODE = DEFAULT | Rendering VoicePromptScreen');
}
return (
<VoicePromptScreen
suggestions={searchHistory}
onSuggestionClick={handleSuggestionClick}
/>
);
}
}, [
currentMode,
voiceInputMode,
isListening,
searchHistory,
handleSuggestionClick,
interimText,
sttResponseText,
countdown,
errorMessage,
restartWebSpeech,
shopperHouseError,
handleApiErrorRetry,
handleApiErrorRestart,
]);
// 마이크 버튼 포커스 핸들러 (VUI)
const handleMicFocus = useCallback(() => {
setMicFocused(true);
}, []);
const handleMicBlur = useCallback(() => {
setMicFocused(false);
}, []);
// 🎤 음성 입력 최종 처리 함수 (15초 타이머 & 3초 silence detection 공통 사용)
const processFinalVoiceInput = useCallback(
(source) => {
@@ -1232,9 +1014,254 @@ const VoiceInputOverlay = ({
setCurrentMode(VOICE_MODES.PROMPT);
setSttResponseText('');
setErrorMessage('');
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
onClose();
}, [onClose]);
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트 설정 + API 자동 호출 + response 모드 전환
const handleSuggestionClick = useCallback(
(suggestion) => {
if (DEBUG_MODE) {
console.log('💡 [DEBUG] handleSuggestionClick called with:', suggestion);
}
// Bubble 클릭으로 검색 시작 상태 설정
setIsBubbleClickSearch(true);
// 따옴표 제거
const query = suggestion.replace(/^["']|["']$/g, '').trim();
// Input 창에 텍스트 설정 (유지)
if (onSearchChange) {
onSearchChange({ value: query });
}
// ✨ 검색 기록에 추가
addToSearchHistory(query);
// ✨ RESPONSE 모드로 전환을 위한 텍스트 설정
setSttResponseText(query);
// ✨ RESPONSE 모드로 직접 전환
setCurrentMode(VOICE_MODES.RESPONSE);
setVoiceInputMode(null);
// ✨ ShopperHouse API 자동 호출 (searchId 포함)
if (query && query.length >= 3) {
// ✅ Ref에서 최신 searchId 읽기 (이전 검색이 있는 경우 2차 발화 처리)
const currentSearchId = shopperHouseSearchIdRef.current;
if (DEBUG_MODE) {
console.log('🔍 [DEBUG] Calling ShopperHouse API from bubble click');
console.log('[VoiceInput] ├─ query:', query);
console.log('[VoiceInput] └─ searchId:', currentSearchId || '(없음 - 첫 번째 발화)');
}
try {
dispatch(getShopperHouseSearch(query, currentSearchId));
} catch (error) {
console.error('[VoiceInput] ❌ API 호출 실패:', error);
// 에러 발생 시 PROMPT 모드로 복귀
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
setIsBubbleClickSearch(false); // 상태 초기화
}
}
},
[onSearchChange, dispatch, addToSearchHistory]
);
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
const handleSearchSubmit = useCallback(() => {
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] Search submit:', searchQuery);
}
if (searchQuery && searchQuery.trim()) {
// ShopperHouse API 호출
dispatch(getShopperHouseSearch(searchQuery.trim()));
// Input 내용 비우기
if (onSearchChange) {
onSearchChange({ value: '' });
}
// Clear existing timer before setting new one
clearTimerRef(searchSubmitFocusTimerRef);
// API 호출 후 Input 박스로 포커스 이동
searchSubmitFocusTimerRef.current = setTimeout(() => {
Spotlight.focus(INPUT_SPOTLIGHT_ID);
}, 100);
// VoiceInputOverlay는 SearchPanel과 다른 API를 사용하므로 onSearchSubmit 호출 안 함
// if (onSearchSubmit) {
// onSearchSubmit(searchQuery);
// }
}
}, [dispatch, searchQuery, onSearchChange]);
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
const handleInputKeyDown = useCallback((e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
// Enter 키로는 API 호출하지 않음
// 돋보기 아이콘 클릭/Enter로만 API 호출
}
}, []);
// Input 모드 변경 핸들러 - Input 모드로 전환되면 VoiceInputOverlay 닫기
const handleInputModeChange = useCallback(
(inputMode) => {
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay] TInput 모드 변경:', inputMode);
}
if (inputMode === 'input') {
// Input 모드로 전환되면 Overlay 닫기
// SearchPanel의 TInput으로 자연스럽게 전환됨
if (DEBUG_MODE) {
console.log('⌨️ [DEBUG] Input 모드로 전환됨 - VoiceInputOverlay 닫고 SearchPanel로 이동');
}
handleClose();
}
},
[handleClose]
);
// ⛔ TALK AGAIN 버튼 제거됨 - 더 이상 사용하지 않음
// const handleTalkAgain = useCallback(() => { ... }, []);
// 모드에 따른 컨텐츠 렌더링 - Memoized
const renderModeContent = useMemo(() => {
if (DEBUG_MODE) {
console.log(
'🎬 [DEBUG][VoiceInputOverlay] renderModeContent called',
'| 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'
);
}
return (
<VoicePromptScreen
suggestions={searchHistory}
onSuggestionClick={handleSuggestionClick}
/>
);
case VOICE_MODES.LISTENING:
if (DEBUG_MODE) {
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,
'isLoading: true (항상 로딩 애니메이션 표시)'
);
}
// ✅ RESPONSE 모드에서는 항상 로딩 애니메이션 표시
// shopperHouseData는 overlay 닫기에만 사용 (searchId 유지 필요)
return (
<VoiceResponse
responseText={sttResponseText}
isLoading
query={sttResponseText}
searchId={shopperHouseSearchId || ''}
/>
);
case VOICE_MODES.NOINIT:
if (DEBUG_MODE) {
console.log('⚠️ [DEBUG][VoiceInputOverlay] MODE = NOINIT | Rendering VoiceNotRecognized');
}
return <VoiceNotRecognized prompt={NOINIT_ERROR_MESSAGE} />;
case VOICE_MODES.NOTRECOGNIZED:
if (DEBUG_MODE) {
console.log(
'❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized with error message'
);
}
return (
<VoiceNotRecognized
prompt={errorMessage || '음성 인식에 문제가 발생했습니다.'}
onRestart={restartWebSpeech}
/>
);
case VOICE_MODES.APIERROR:
if (DEBUG_MODE) {
console.log(
'💥 [DEBUG][VoiceInputOverlay] MODE = APIERROR | Rendering VoiceApiError with error details',
shopperHouseError
);
}
return (
<VoiceApiError
error={shopperHouseError}
onRetry={handleApiErrorRetry}
onRestart={handleApiErrorRestart}
/>
);
case VOICE_MODES.MODE_3:
// 추후 MODE_3 컴포넌트 추가
return <VoiceNotRecognized />;
case VOICE_MODES.MODE_4:
// 추후 MODE_4 컴포넌트 추가
return <VoiceNotRecognizedCircle />;
default:
if (DEBUG_MODE) {
console.log('🔄 [DEBUG][VoiceInputOverlay] MODE = DEFAULT | Rendering VoicePromptScreen');
}
return (
<VoicePromptScreen
suggestions={searchHistory}
onSuggestionClick={handleSuggestionClick}
/>
);
}
}, [
currentMode,
voiceInputMode,
isListening,
searchHistory,
handleSuggestionClick,
interimText,
sttResponseText,
countdown,
errorMessage,
restartWebSpeech,
shopperHouseError,
shopperHouseSearchId,
handleApiErrorRetry,
handleApiErrorRestart,
]);
// 마이크 버튼 포커스 핸들러 (VUI)
const handleMicFocus = useCallback(() => {
setMicFocused(true);
}, []);
const handleMicBlur = useCallback(() => {
setMicFocused(false);
}, []);
// Input wrapper click handler to prevent event propagation
const handleInputWrapperClick = useCallback((e) => {
e.stopPropagation();
}, []);
// WebSpeech 마이크 버튼 클릭 핸들러
const handleWebSpeechMicClick = useCallback(
(e) => {
@@ -1290,15 +1317,7 @@ const VoiceInputOverlay = ({
handleClose();
}
},
[
currentMode,
handleClose,
startListening,
stopListening,
onSearchChange,
dispatch,
processFinalVoiceInput,
]
[currentMode, handleClose, startListening, onSearchChange, dispatch, processFinalVoiceInput]
);
// 마이크 버튼 키다운 핸들러
@@ -1431,13 +1450,13 @@ const VoiceInputOverlay = ({
<div>
Interim:{' '}
<span style={{ color: '#9C27B0', wordBreak: 'break-all' }}>
"{interimText || 'N/A'}"
&quot;{interimText || 'N/A'}&quot;
</span>
</div>
<div>
Ref Text:{' '}
<span style={{ color: '#9C27B0', wordBreak: 'break-all' }}>
"{interimTextRef.current || 'N/A'}"
&quot;{interimTextRef.current || 'N/A'}&quot;
</span>
</div>
<div>
@@ -1449,7 +1468,7 @@ const VoiceInputOverlay = ({
<div>
STT Result:{' '}
<span style={{ color: '#607D8B', wordBreak: 'break-all' }}>
"{lastSTTText || 'None'}"
&quot;{lastSTTText || 'None'}&quot;
</span>
</div>
<div>
@@ -1515,7 +1534,7 @@ const VoiceInputOverlay = ({
spotlightDisabled={!isVisible}
>
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
<div className={css.inputWrapper} onClick={(e) => e.stopPropagation()}>
<div className={css.inputWrapper} onClick={handleInputWrapperClick}>
<div className={css.searchInputWrapper}>
<TInput
className={css.inputBox}
@@ -1526,8 +1545,8 @@ const VoiceInputOverlay = ({
onKeyDown={handleInputKeyDown}
onIconClick={handleSearchSubmit}
spotlightId={INPUT_SPOTLIGHT_ID}
displayMode={true}
allowModeToggle={true}
displayMode
allowModeToggle
onModeChange={handleInputModeChange}
placeholder="Search"
/>
@@ -1596,7 +1615,6 @@ VoiceInputOverlay.propTypes = {
isVisible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
mode: PropTypes.oneOf(Object.values(VOICE_MODES)),
suggestions: PropTypes.arrayOf(PropTypes.string),
searchQuery: PropTypes.string,
onSearchChange: PropTypes.func,
onSearchSubmit: PropTypes.func,
@@ -1604,7 +1622,6 @@ VoiceInputOverlay.propTypes = {
VoiceInputOverlay.defaultProps = {
mode: VOICE_MODES.PROMPT,
suggestions: [],
searchQuery: '',
onSearchChange: null,
onSearchSubmit: null,

View File

@@ -9,53 +9,53 @@ const SpottableButton = Spottable('div');
const VoiceApiError = ({ error = null, onRestart = null, onRetry = null }) => {
// 에러 객체에서 메시지 추출
const getErrorMessage = (error) => {
if (!error) return 'An unknown error occurred.';
const getErrorMessage = (err) => {
if (!err) return 'An unknown error occurred.';
if (typeof error === 'string') {
return error;
if (typeof err === 'string') {
return err;
}
// API 에러 객체 구조에 따라 메시지 추출
if (error.message) {
return error.message;
if (err.message) {
return err.message;
}
if (error.data && error.data.message) {
return error.data.message;
if (err.data && err.data.message) {
return err.data.message;
}
if (error.response && error.response.data && error.response.data.message) {
return error.response.data.message;
if (err.response && err.response.data && err.response.data.message) {
return err.response.data.message;
}
if (error.status) {
return `API Error: ${error.status} - ${error.statusText || 'Unknown error'}`;
if (err.status) {
return `API Error: ${err.status} - ${err.statusText || 'Unknown error'}`;
}
return JSON.stringify(error);
return JSON.stringify(err);
};
// 에러 상세 정보 추출
const getErrorDetails = (error) => {
if (!error) return null;
const getErrorDetails = (err) => {
if (!err) return null;
const details = [];
if (error.status) {
details.push(`Status: ${error.status}`);
if (err.status) {
details.push(`Status: ${err.status}`);
}
if (error.code) {
details.push(`Code: ${error.code}`);
if (err.code) {
details.push(`Code: ${err.code}`);
}
if (error.timestamp) {
details.push(`Time: ${new Date(error.timestamp).toLocaleString()}`);
if (err.timestamp) {
details.push(`Time: ${new Date(err.timestamp).toLocaleString()}`);
}
if (error.endpoint) {
details.push(`Endpoint: ${error.endpoint}`);
if (err.endpoint) {
details.push(`Endpoint: ${err.endpoint}`);
}
return details.length > 0 ? details : null;

View File

@@ -34,12 +34,11 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestio
<div className={css.title}>{title}</div>
<div className={css.suggestionsContainer}>
{suggestions.map((suggestion, index) => {
const handleClick = () => handleBubbleClick(suggestion);
return (
<SpottableBubble
key={index}
className={css.bubbleMessage}
onClick={handleClick}
onClick={() => handleBubbleClick(suggestion)}
spotlightId={`voice-bubble-${index}`}
>
<div className={css.bubbleText}>{suggestion}</div>

View File

@@ -5,7 +5,7 @@ import Spottable from '@enact/spotlight/Spottable';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import css from './VoiceResponse.module.less';
const SpottableBubble = Spottable('div');
// const SpottableBubble = Spottable('div');
const ResponseContainer = SpotlightContainerDecorator(
{
@@ -46,7 +46,7 @@ const VoiceResponse = ({ responseText = '', isLoading = true, query = '', search
return result;
};
const displayText = capitalizeSentences(responseText);
// const displayText = capitalizeSentences(responseText);
// 타이핑 애니메이션 효과 (반복)
useEffect(() => {