diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx index ebef2735..1bd3c794 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowButton.jsx @@ -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 (
{ - e.stopPropagation(); - e.preventDefault(); - Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); - }} + onSpotlightUp={handleSpotlightUp} > SHOP NOW diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx index 34fe338c..6cdeae6d 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx @@ -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 ( - - ); - case VOICE_MODES.LISTENING: - if (DEBUG_MODE) { - console.log( - '🎤 [DEBUG][VoiceInputOverlay] MODE = LISTENING | Rendering VoiceListening (15초 타이머)' - ); - } - return ; - 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 ( - - ); - case VOICE_MODES.NOINIT: - if (DEBUG_MODE) { - console.log('⚠️ [DEBUG][VoiceInputOverlay] MODE = NOINIT | Rendering VoiceNotRecognized'); - } - return ; - case VOICE_MODES.NOTRECOGNIZED: - if (DEBUG_MODE) { - console.log( - '❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized with error message' - ); - } - return ( - - ); - case VOICE_MODES.APIERROR: - if (DEBUG_MODE) { - console.log( - '💥 [DEBUG][VoiceInputOverlay] MODE = APIERROR | Rendering VoiceApiError with error details', - shopperHouseError - ); - } - return ( - - ); - case VOICE_MODES.MODE_3: - // 추후 MODE_3 컴포넌트 추가 - return ; - case VOICE_MODES.MODE_4: - // 추후 MODE_4 컴포넌트 추가 - return ; - default: - if (DEBUG_MODE) { - console.log('🔄 [DEBUG][VoiceInputOverlay] MODE = DEFAULT | Rendering VoicePromptScreen'); - } - return ( - - ); - } - }, [ - 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 ( + + ); + case VOICE_MODES.LISTENING: + if (DEBUG_MODE) { + console.log( + '🎤 [DEBUG][VoiceInputOverlay] MODE = LISTENING | Rendering VoiceListening (15초 타이머)' + ); + } + return ; + 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 ( + + ); + case VOICE_MODES.NOINIT: + if (DEBUG_MODE) { + console.log('⚠️ [DEBUG][VoiceInputOverlay] MODE = NOINIT | Rendering VoiceNotRecognized'); + } + return ; + case VOICE_MODES.NOTRECOGNIZED: + if (DEBUG_MODE) { + console.log( + '❌ [DEBUG][VoiceInputOverlay] MODE = NOTRECOGNIZED | Rendering VoiceNotRecognized with error message' + ); + } + return ( + + ); + case VOICE_MODES.APIERROR: + if (DEBUG_MODE) { + console.log( + '💥 [DEBUG][VoiceInputOverlay] MODE = APIERROR | Rendering VoiceApiError with error details', + shopperHouseError + ); + } + return ( + + ); + case VOICE_MODES.MODE_3: + // 추후 MODE_3 컴포넌트 추가 + return ; + case VOICE_MODES.MODE_4: + // 추후 MODE_4 컴포넌트 추가 + return ; + default: + if (DEBUG_MODE) { + console.log('🔄 [DEBUG][VoiceInputOverlay] MODE = DEFAULT | Rendering VoicePromptScreen'); + } + return ( + + ); + } + }, [ + 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 = ({
Interim:{' '} - "{interimText || 'N/A'}" + "{interimText || 'N/A'}"
Ref Text:{' '} - "{interimTextRef.current || 'N/A'}" + "{interimTextRef.current || 'N/A'}"
@@ -1449,7 +1468,7 @@ const VoiceInputOverlay = ({
STT Result:{' '} - "{lastSTTText || 'None'}" + "{lastSTTText || 'None'}"
@@ -1515,7 +1534,7 @@ const VoiceInputOverlay = ({ spotlightDisabled={!isVisible} > {/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */} -
e.stopPropagation()}> +
@@ -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, diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx index d2b62705..8791810f 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceApiError.jsx @@ -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; diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx index dc76ef5b..36b0eb22 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx @@ -34,12 +34,11 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestio
{title}
{suggestions.map((suggestion, index) => { - const handleClick = () => handleBubbleClick(suggestion); return ( handleBubbleClick(suggestion)} spotlightId={`voice-bubble-${index}`} >
{suggestion}
diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx index 4cf749c9..6ba7af22 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx @@ -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(() => {