[251103] fix: shopperHouse API오류처리 및 fallback Data표시 개선
🕐 커밋 시간: 2025. 11. 03. 12:25:18 📊 변경 통계: • 총 파일: 9개 • 추가: +315줄 • 삭제: -26줄 📁 추가된 파일: + code/ 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/actions/searchActions.js ~ com.twin.app.shoptime/src/reducers/searchReducer.js ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.module.less ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript): 🔄 Modified: clearAllTimers() 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx (javascript): ✅ Added: VoiceResponse() 🔄 Modified: SpotlightContainerDecorator() 🔧 주요 변경 내용: • 타입 시스템 안정성 강화 • 핵심 비즈니스 로직 개선
This commit is contained in:
@@ -1,37 +1,19 @@
|
||||
// src/views/SearchPanel/SearchPanel.new.jsx
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
|
||||
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
|
||||
import hotPicksBrandImage
|
||||
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
|
||||
import {
|
||||
sendLogGNB,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../actions/logActions';
|
||||
import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-hotpicks.png';
|
||||
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
|
||||
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
|
||||
import {
|
||||
popPanel,
|
||||
pushPanel,
|
||||
updatePanel,
|
||||
} from '../../actions/panelActions';
|
||||
import { popPanel, pushPanel, updatePanel } from '../../actions/panelActions';
|
||||
import {
|
||||
clearShopperHouseData,
|
||||
getSearch,
|
||||
@@ -52,32 +34,20 @@ import {
|
||||
// } from '../../actions/toastActions';
|
||||
import TBody from '../../components/TBody/TBody';
|
||||
import TPanel from '../../components/TPanel/TPanel';
|
||||
import TVerticalPagenator
|
||||
from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import TVirtualGridList
|
||||
from '../../components/TVirtualGridList/TVirtualGridList';
|
||||
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
||||
import usePanelHistory from '../../hooks/usePanelHistory/usePanelHistory';
|
||||
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useSearchHistory } from '../../hooks/useSearchHistory';
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MENU,
|
||||
LOG_MESSAGE_ID,
|
||||
panel_names,
|
||||
} from '../../utils/Config';
|
||||
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
|
||||
import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
// import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
import SearchInputOverlay from './SearchInputOverlay';
|
||||
import css from './SearchPanel.new.module.less';
|
||||
import SearchResultsNew from './SearchResults.new.v2';
|
||||
import TInputSimple, {
|
||||
ICONS,
|
||||
KINDS,
|
||||
} from './TInput/TInputSimple';
|
||||
import VoiceInputOverlay, {
|
||||
VOICE_MODES,
|
||||
} from './VoiceInputOverlay/VoiceInputOverlay';
|
||||
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
|
||||
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
|
||||
|
||||
/**
|
||||
* ✨ Mode-Based Architecture 도입
|
||||
@@ -302,6 +272,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const isSearchOverlayVisibleRef = useRef(false);
|
||||
const currentModeRef = useRef(SEARCH_PANEL_MODES.INITIAL);
|
||||
const unifiedFocusTimerRef = useRef(null);
|
||||
|
||||
// ShopperHouse 에러 팝업 상태 가져오기
|
||||
const shopperHouseErrorPopup = useSelector((state) => state.search.shopperHouseErrorPopup);
|
||||
|
||||
// API 실패 시 fallback reference 초기화
|
||||
useEffect(() => {
|
||||
if (shopperHouseErrorPopup?.visible && shopperHouseErrorPopup?.type === 'API_FAILURE') {
|
||||
console.log('[SearchPanel] 🧹 API 실패 감지 - fallbackShopperHouseData 초기화');
|
||||
shopperHouseDataRef.current = null;
|
||||
}
|
||||
}, [shopperHouseErrorPopup?.visible, shopperHouseErrorPopup?.type]);
|
||||
const detailReturnHandledRef = useRef(false);
|
||||
|
||||
// Spottable 컴포넌트 캐싱으로 메모리 누수 방지
|
||||
@@ -315,7 +296,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
/**
|
||||
* useSearchHistory Hook 적용
|
||||
*/
|
||||
const { normalSearches, addNormalSearch, refreshHistory, executeSearchFromHistory } = useSearchHistory();
|
||||
const { normalSearches, addNormalSearch, refreshHistory, executeSearchFromHistory } =
|
||||
useSearchHistory();
|
||||
|
||||
/**
|
||||
* 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용
|
||||
@@ -324,7 +306,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
currentPanel,
|
||||
previousPanel,
|
||||
currentIsOnTop, // 🎯 usePanelHistory의 isOnTop 정보 사용
|
||||
isOnTopChange
|
||||
isOnTopChange,
|
||||
} = usePanelHistory();
|
||||
|
||||
// 🎯 DetailPanel에서 SearchPanel로 돌아왔는지 감지
|
||||
@@ -862,7 +844,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
if (
|
||||
!isReturningFromDetailPanel && // 🎯 usePanelHistory로 감지 못했을 때만
|
||||
((currentIsOnTop && isOnTopChange?.becameOnTop) || // 🎯 usePanelHistory 기반 감지 (우선)
|
||||
(isOnTop && !isOnTopRef.current)) && // 🎯 기존 방식 (fallback)
|
||||
(isOnTop && !isOnTopRef.current)) && // 🎯 기존 방식 (fallback)
|
||||
panelInfo?.currentSpot &&
|
||||
(currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT ||
|
||||
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT)
|
||||
@@ -1028,10 +1010,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
return currentSpot;
|
||||
} else {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[FOCUS] ⚠️ DETAIL_PANEL_RETURN: currentSpot이 유효하지 않음, fallback으로 이동:', {
|
||||
currentSpot,
|
||||
fallback: SPOTLIGHT_IDS.SEARCH_INPUT_BOX
|
||||
});
|
||||
console.log(
|
||||
'[FOCUS] ⚠️ DETAIL_PANEL_RETURN: currentSpot이 유효하지 않음, fallback으로 이동:',
|
||||
{
|
||||
currentSpot,
|
||||
fallback: SPOTLIGHT_IDS.SEARCH_INPUT_BOX,
|
||||
}
|
||||
);
|
||||
}
|
||||
return SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
|
||||
}
|
||||
@@ -1068,7 +1053,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [analyzeCurrentScenario, panelInfo?.currentSpot, shopperHouseData, DEBUG_MODE, isReturningFromDetailPanel, currentPanel, currentIsOnTop, isOnTopChange]);
|
||||
}, [
|
||||
analyzeCurrentScenario,
|
||||
panelInfo?.currentSpot,
|
||||
shopperHouseData,
|
||||
DEBUG_MODE,
|
||||
isReturningFromDetailPanel,
|
||||
currentPanel,
|
||||
currentIsOnTop,
|
||||
isOnTopChange,
|
||||
]);
|
||||
|
||||
/**
|
||||
* 🎯 [Phase 2] VoiceInputOverlay → SearchInputOverlay 전환 콜백
|
||||
@@ -1124,7 +1118,6 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}
|
||||
}, [DEBUG_MODE, searchDatas, searchPerformed]);
|
||||
|
||||
|
||||
/**
|
||||
* Voice overlay close handler
|
||||
*/
|
||||
@@ -1511,13 +1504,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
searchQuery,
|
||||
hasSearchResults: !!(
|
||||
searchDatas?.theme?.length > 0 ||
|
||||
searchDatas?.item?.length > 0 ||
|
||||
searchDatas?.show?.length > 0
|
||||
),
|
||||
isSearchOverlayVisible,
|
||||
isShopperHousePending,
|
||||
currentMode,
|
||||
});
|
||||
searchDatas?.item?.length > 0 ||
|
||||
searchDatas?.show?.length > 0
|
||||
),
|
||||
isSearchOverlayVisible,
|
||||
isShopperHousePending,
|
||||
currentMode,
|
||||
});
|
||||
}
|
||||
|
||||
// 우선순위 1: 음성 입력 오버레이가 열려있으면 VOICE_INPUT 모드
|
||||
@@ -1734,7 +1727,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const fallbackElement = document.querySelector(`[data-spotlight-id="${fallbackTarget}"]`);
|
||||
if (fallbackElement) {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[FOCUS] 🔄 DETAIL_PANEL_RETURN fallback: 첫 번째 상품으로 포커스:', fallbackTarget);
|
||||
console.log(
|
||||
'[FOCUS] 🔄 DETAIL_PANEL_RETURN fallback: 첫 번째 상품으로 포커스:',
|
||||
fallbackTarget
|
||||
);
|
||||
}
|
||||
Spotlight.focus(fallbackTarget);
|
||||
}
|
||||
@@ -1860,9 +1856,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
});
|
||||
if (focusTimer) {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
console.log(
|
||||
'[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리',
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
clearTimeout(focusTimer);
|
||||
}
|
||||
@@ -2033,7 +2032,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// 🎯 [포커스 중첩 해결] SearchResultsContainer로 포커스 전달
|
||||
// SearchResultsContainer가 Spotlight 컨테이너이므로, 포커스가 들어오면
|
||||
// enterTo: 'last-focused' 설정에 의해 자동으로 HowAboutThese.small의 SEE MORE로 이동
|
||||
data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'search-results-container' : undefined}
|
||||
data-spotlight-down={
|
||||
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT
|
||||
? 'search-results-container'
|
||||
: undefined
|
||||
}
|
||||
// 🎯 HowAboutThese 포커스 관리 - 포커스가 검색 입력 영역으로 올 때 감지
|
||||
onSpotlightUp={handleSearchInputFocus}
|
||||
onSpotlightLeft={handleSearchInputFocus}
|
||||
@@ -2052,7 +2055,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
onKeyDown={handleMicKeyDown}
|
||||
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
||||
// 🎯 [포커스 중첩 해결] SearchResultsContainer로 포커스 전달
|
||||
data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'search-results-container' : undefined}
|
||||
data-spotlight-down={
|
||||
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT
|
||||
? 'search-results-container'
|
||||
: undefined
|
||||
}
|
||||
// 🎯 HowAboutThese 포커스 관리 - 포커스가 마이크 버튼으로 올 때 감지
|
||||
onSpotlightUp={handleSearchInputFocus}
|
||||
onSpotlightLeft={handleSearchInputFocus}
|
||||
|
||||
Reference in New Issue
Block a user