[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:
2025-11-03 12:25:18 +09:00
parent 1bbe60a801
commit e661a18458
9 changed files with 546 additions and 293 deletions

View File

@@ -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}