[251025] feat: SearchPanel.new.v2 Focus-1

🕐 커밋 시간: 2025. 10. 25. 04:57:00

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx
  ~ 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/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx (javascript):
    🔄 Modified: Spottable()
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx (javascript):
    🔄 Modified: Bubble()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx (javascript):
    🔄 Modified: SafeImage()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
    🔄 Modified: clearAllTimers()
This commit is contained in:
2025-10-25 04:57:00 +00:00
parent aefcfb10ab
commit f03e78932c
5 changed files with 406 additions and 149 deletions

View File

@@ -8,6 +8,8 @@ import Spottable from '@enact/spotlight/Spottable';
import icShoptime from '../../../../assets/images/icons/ic-shoptime.png';
import { getShopperHouseSearch } from '../../../actions/searchActions';
import { updatePanel } from '../../../actions/panelActions';
import { panel_names } from '../../../utils/Config';
import css from './HowAboutThese.module.less';
const OverlayContainer = SpotlightContainerDecorator(
@@ -51,6 +53,18 @@ const HowAboutThese = ({ relativeQueries = [], searchId = null, onQueryClick, on
// ShopperHouse API를 통해 재검색 (searchId가 존재하면 포함)
dispatch(getShopperHouseSearch(query, searchId));
// 🎯 [포커스 로직 통합] 검색어만 업데이트
// 포커스는 shopperHouseData 변경에 의해 자동으로 처리됨
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
tab: 0,
},
})
);
// 팝업 닫기
if (onClose) {
onClose();

View File

@@ -8,6 +8,8 @@ import Spottable from '@enact/spotlight/Spottable';
import icShoptime from '../../../../assets/images/icons/ic-shoptime.png';
import { getShopperHouseSearch } from '../../../actions/searchActions';
import { updatePanel } from '../../../actions/panelActions';
import { panel_names } from '../../../utils/Config';
import TButton from '../../../components/TButton/TButton.jsx';
import css from './HowAboutThese.small.module.less';
@@ -48,6 +50,18 @@ const HowAboutTheseSmall = ({
// ShopperHouse API를 통해 재검색 (searchId가 존재하면 포함)
dispatch(getShopperHouseSearch(query, searchId));
// 🎯 [포커스 로직 통합] 검색어만 업데이트
// 포커스는 shopperHouseData 변경에 의해 자동으로 처리됨
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
tab: 0,
},
})
);
},
[dispatch, onQueryClick, searchId]
);

View File

@@ -251,6 +251,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// DOM 쿼리 최적화를 위한 ref 추가
const inputElementRef = useRef(null);
// 🎯 [포커스 로직 통합] 이전 상태 추적용 ref
const shopperHouseDataRef = useRef(null);
const searchDatasRef = useRef(null); // 일반 검색 결과 추적
const isVoiceOverlayVisibleRef = useRef(false);
const isSearchOverlayVisibleRef = useRef(false);
const currentModeRef = useRef(SEARCH_PANEL_MODES.INITIAL);
const unifiedFocusTimerRef = useRef(null);
// Spottable 컴포넌트 캐싱으로 메모리 누수 방지
const SpottableMicButton = useMemo(() => Spottable('div'), []);
const SpottableKeyword = useMemo(() => Spottable('div'), []);
@@ -401,6 +409,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
})
);
// 🎯 [포커스 로직 통합] 검색어만 업데이트
// 포커스는 searchDatas 변경에 의해 자동으로 처리됨
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
tab: 0,
},
})
);
// 검색 시작 알림 (선택사항)
// dispatch(showSuccessToast(`"${query}" 검색 중...`, { duration: 2000 }));
} else {
@@ -507,8 +527,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
console.log('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
console.log('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
}
// 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId & relativeQuerys 유지
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
return;
}
@@ -526,8 +546,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
console.log('[DEBUG]-onCancel: resetting search query');
}
setSearchQuery('');
// 🎯 [포커스 로직 통합] 포커스는 상태 변경(searchQuery)에 의해 자동으로 처리됨
dispatch(resetSearch());
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}
}, [
isVoiceOverlayVisible,
@@ -624,6 +644,204 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
[handleHistoryKeywordClick]
);
/**
* 🎯 [포커스 로직 통합]
* 현재 상태와 이전 상태를 비교하여 어떤 시나리오인지 분석
* 반환값:
* - 'DETAIL_PANEL_RETURN': DetailPanel에서 복귀 (이전 상품으로 포커스)
* - 'INITIAL_OPEN': SearchPanel 처음 열림
* - 'NEW_SEARCH_LOADED': 새로운 검색 결과 로드됨
* - 'OVERLAY_CLOSED': Overlay가 닫혔음
* - 'NO_CHANGE': 변화 없음
*/
const analyzeCurrentScenario = useCallback(() => {
// DEBUG: 모든 기본 상태값 출력
if (DEBUG_MODE) {
console.log('[DEBUG] analyzeCurrentScenario 호출됨:', {
isOnTop,
isOnTopRefCurrent: isOnTopRef.current,
panelInfo: panelInfo,
currentMode,
currentModeRefCurrent: currentModeRef.current,
isVoiceOverlayVisible,
isVoiceOverlayVisibleRefCurrent: isVoiceOverlayVisibleRef.current,
isSearchOverlayVisible,
isSearchOverlayVisibleRefCurrent: isSearchOverlayVisibleRef.current,
shopperHouseData: !!shopperHouseData,
});
}
// DetailPanel에서 방금 복귀한 상황 (우선순위 최상)
// - isOnTop이 false → true로 변경되었고
// - currentSpot이 있고
// - 검색 결과 모드인 경우 (일반검색 or 음성검색 모두)
if (DEBUG_MODE) {
console.log('[DEBUG] DETAIL_PANEL_RETURN 조건 확인:', {
isOnTop,
isOnTopRefCurrent: isOnTopRef.current,
isOnTopChanged: isOnTop && !isOnTopRef.current,
currentSpot: panelInfo?.currentSpot,
hasCurrentSpot: !!panelInfo?.currentSpot,
currentMode,
isSearchResultMode: currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT,
isVoiceResultMode: currentMode === SEARCH_PANEL_MODES.VOICE_RESULT,
allConditions:
isOnTop &&
!isOnTopRef.current &&
panelInfo?.currentSpot &&
(currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT ||
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT),
});
}
if (
isOnTop &&
!isOnTopRef.current &&
panelInfo?.currentSpot &&
(currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT ||
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT)
) {
if (DEBUG_MODE) {
console.log('[Focus] DetailPanel에서 복귀 - 이전 상품으로 포커스 이동');
console.log('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN', {
currentSpot: panelInfo.currentSpot,
mode: currentMode,
fromSearchResult: currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT,
fromVoiceResult: currentMode === SEARCH_PANEL_MODES.VOICE_RESULT,
});
}
return 'DETAIL_PANEL_RETURN';
}
// SearchPanel이 처음 열린 상황
// - isOnTop이 false → true로 변경되었고
// - 위의 DETAIL_PANEL_RETURN이 아닌 경우 (= currentSpot이 없거나 모드가 검색 결과 아님)
if (isOnTop && !isOnTopRef.current) {
if (DEBUG_MODE) {
console.log('[FOCUS] 🎯 Scenario: INITIAL_OPEN', {
currentSpot: panelInfo?.currentSpot,
mode: currentMode,
});
}
return 'INITIAL_OPEN';
}
// 일반 검색 결과 모드로 진입 (모드 변경으로 감지 - 가장 안정적)
// - currentMode가 SEARCH_RESULT로 변경되고
// - 이전에는 SEARCH_RESULT가 아니었으면
// - 🎯 중요: isOnTop이 변화하지 않았을 때만 (이미 SearchPanel이 열려있고 새로 검색한 경우)
// DetailPanel 복귀(isOnTop 변화)는 위의 DETAIL_PANEL_RETURN에서 먼저 처리됨
if (
isOnTop === isOnTopRef.current &&
currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT &&
currentModeRef.current !== SEARCH_PANEL_MODES.SEARCH_RESULT
) {
if (DEBUG_MODE) {
console.log('[FOCUS] 🎯 Scenario: SEARCH_RESULT_LOADED (Mode Changed)', {
themeCount: searchDatas?.theme?.length || 0,
itemCount: searchDatas?.item?.length || 0,
showCount: searchDatas?.show?.length || 0,
prevMode: currentModeRef.current,
nextMode: currentMode,
isOnTopChanged: isOnTop !== isOnTopRef.current,
});
}
return 'SEARCH_RESULT_LOADED';
}
// 새로운 음성 검색 결과 모드로 진입 (모드 변경으로 감지 - 일관성 유지)
// - currentMode가 VOICE_RESULT로 변경되고
// - 이전에는 VOICE_RESULT가 아니었으면
// - 🎯 중요: isOnTop이 변화하지 않았을 때만 (이미 SearchPanel이 열려있고 새로 검색한 경우)
// DetailPanel 복귀(isOnTop 변화)는 위의 DETAIL_PANEL_RETURN에서 먼저 처리됨
if (
isOnTop === isOnTopRef.current &&
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT &&
currentModeRef.current !== SEARCH_PANEL_MODES.VOICE_RESULT &&
shopperHouseData
) {
if (DEBUG_MODE) {
console.log('[FOCUS] 🎯 Scenario: NEW_SEARCH_LOADED (Voice Result Mode)', {
itemCount: shopperHouseData?.results?.[0]?.docs?.length || 0,
prevMode: currentModeRef.current,
nextMode: currentMode,
isOnTopChanged: isOnTop !== isOnTopRef.current,
});
}
return 'NEW_SEARCH_LOADED';
}
// Voice Overlay가 닫힌 상황
if (!isVoiceOverlayVisible && isVoiceOverlayVisibleRef.current) {
if (DEBUG_MODE) {
console.log('[FOCUS] 🎯 Scenario: VOICE_OVERLAY_CLOSED', {
hasShopperHouseData: !!shopperHouseData,
});
}
return 'VOICE_OVERLAY_CLOSED';
}
// Search Overlay가 닫힌 상황
if (!isSearchOverlayVisible && isSearchOverlayVisibleRef.current) {
if (DEBUG_MODE) {
console.log('[FOCUS] 🎯 Scenario: SEARCH_OVERLAY_CLOSED');
}
return 'SEARCH_OVERLAY_CLOSED';
}
// 변화 없음
return 'NO_CHANGE';
}, [
isOnTop,
panelInfo?.currentSpot,
currentMode, // 모드 변경으로 검색 결과 로드 감지
shopperHouseData,
isVoiceOverlayVisible,
isSearchOverlayVisible,
DEBUG_MODE,
]);
/**
* 🎯 [포커스 로직 통합]
* 현재 시나리오에 따라 다음 포커스 타겟을 결정
* Spotlight.focus()를 직접 호출하지 않고, "어디로 포커스할 것인가"만 결정
*/
const determineFocusTarget = useCallback(() => {
const scenario = analyzeCurrentScenario();
if (scenario === 'NO_CHANGE') {
return null;
}
switch (scenario) {
case 'DETAIL_PANEL_RETURN':
// DetailPanel에서 복귀 → 이전 포커스된 상품으로 복원
return panelInfo?.currentSpot || SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
case 'INITIAL_OPEN':
// SearchPanel 처음 열림 → TInput으로 포커스
return SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
case 'NEW_SEARCH_LOADED':
// 음성 검색 결과 로드됨 → 첫 번째 상품으로 포커스
return 'searchItemContents0';
case 'SEARCH_RESULT_LOADED':
// 일반 검색 결과 로드됨 → 첫 번째 상품으로 포커스
return 'searchItemContents0';
case 'VOICE_OVERLAY_CLOSED':
case 'SEARCH_OVERLAY_CLOSED':
// Overlay 닫힘 → ShopperHouse 데이터 있으면 상품, 없으면 TInput
if (shopperHouseData) {
return 'searchItemContents0';
}
return SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
default:
return null;
}
}, [analyzeCurrentScenario, panelInfo?.currentSpot, shopperHouseData]);
/**
* Search overlay close handler
*/
@@ -631,13 +849,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
if (DEBUG_MODE) {
console.log('[DEBUG] 🚪 SearchInputOverlay closing');
}
// 🎯 [포커스 로직 통합] 포커스는 상태 변경(isSearchOverlayVisible)에 의해 자동으로 처리됨
setIsSearchOverlayVisible(false);
// ✨ Overlay 닫힐 때 TInput 입력값 초기화
setSearchQuery('');
// VoiceInputOverlay와 동일하게 닫힐 때 TInput으로 포커스 이동
setTimeout(() => {
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}, 150); // Overlay 닫히는 시간을 고려한 지연
}, []);
/**
@@ -657,16 +872,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
setVoiceOverlayMode(VOICE_MODES.PROMPT);
setVoiceOverlayResponseText('');
setIsVoiceOverlayBubbleSearch(false);
// 🎯 [포커스 로직 통합] 포커스는 상태 변경(isVoiceOverlayVisible)에 의해 자동으로 처리됨
setIsVoiceOverlayVisible(false);
setShouldFocusVoiceResult(false);
// ShopperHouse 데이터가 없을 때만 검색 인풋으로 포커스 복원
if (!shopperHouseData) {
setTimeout(() => {
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}, 150); // Overlay 닫히는 시간을 고려한 지연
}
}, [shopperHouseData]);
}, []);
const handleHowAboutTheseQueryClick = useCallback(
(rawQuery) => {
@@ -686,6 +895,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
setShouldFocusVoiceResult(false);
dispatch(getShopperHouseSearch(trimmedQuery, shopperHouseSearchId));
// 🎯 [포커스 로직 통합] 검색어만 업데이트
// 포커스는 shopperHouseData 변경에 의해 자동으로 처리됨
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: trimmedQuery,
tab: 0,
},
})
);
},
[dispatch, shopperHouseSearchId]
);
@@ -1106,110 +1327,96 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// }, [panelInfo, isOnTop, firstSpot]);
/**
* focus 용도
* SearchPanel이 처음 열릴 때 TInput으로 포커스 설정
* 🎯 [포커스 로직 통합]
* 모든 포커스 결정을 단일 useEffect에서 처리
* 상태 변경 감지 → 시나리오 분석 → 포커스 타겟 결정 → 포커스 적용
*
* 이전 Ref 값 업데이트는 useEffect 내에서 수행하여
* 다음 렌더링 사이클에 올바른 비교가 이루어지도록 함
*/
useEffect(() => {
if (isOnTop && !isOnTopRef.current) {
// SearchPanel이 방금 열렸을 때 (이전에는 열려있지 않았음)
initialFocusTimerRef.current = setTimeout(() => {
// 🎯 DetailPanel에서 돌아왔을 때 이전 상품에 포커스 복원
if (panelInfo?.currentSpot && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
if (DEBUG_MODE) {
console.log('[DEBUG] 🎯 DetailPanel에서 복귀 - 이전 상품으로 포커스 복원:', {
currentSpot: panelInfo.currentSpot,
currentMode,
timestamp: new Date().toISOString(),
});
}
Spotlight.focus(panelInfo.currentSpot);
} else {
// 일반적인 경우: TInput으로 포커스
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}
}, 100);
if (DEBUG_MODE) {
console.log('[DEBUG][Focus] Focus useEffect 호출됨 - 상태값 확인:', {
isOnTop,
panelInfo: panelInfo,
currentMode,
shopperHouseData: !!shopperHouseData,
});
}
return () => {
// Cleanup: 컴포넌트 언마운트 또는 isOnTop 변경 시 타이머 정리
if (initialFocusTimerRef.current) {
clearTimeout(initialFocusTimerRef.current);
initialFocusTimerRef.current = null;
}
};
}, [isOnTop, panelInfo?.currentSpot, currentMode]); // isOnTopRef 제거
// 포커스 타겟 결정
const targetId = determineFocusTarget();
/**
* focus 용도
* ✨ ShopperHouse 검색 결과 수신 시 TInput으로 포커스 이동
*/
useEffect(() => {
if (shopperHouseData && isOnTop) {
// 🎯 VOICE_RESULT 모드에서는 포커스 복원을 위해 TInput으로 이동하지 않음
if (currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
if (DEBUG_MODE) {
console.log(
'[DEBUG] 🎯 VOICE_RESULT 모드 - ShopperHouse 데이터 수신 시 TInput 포커스 스킵:',
{
currentMode,
hasShopperHouseData: !!shopperHouseData,
reason: '상품 포커스 복원 우선',
}
);
}
return;
}
// ShopperHouse 검색 결과가 들어왔을 때 TInput으로 포커스 이동
const focusTimer = setTimeout(() => {
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}, 300); // VoiceInputOverlay 닫히는 시간(200ms) + 여유(100ms)
return () => {
clearTimeout(focusTimer);
};
}
}, [shopperHouseData, isOnTop, currentMode, DEBUG_MODE]);
useEffect(() => {
if (
isOnTop &&
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT &&
shopperHouseData?.results?.[0]?.docs?.length > 0 &&
!isVoiceOverlayVisible
) {
if (DEBUG_MODE) {
console.log('[DEBUG] 🎯 ShopperHouse 데이터 감지 - 상품 포커스 예약');
}
setShouldFocusVoiceResult(true);
}
}, [isOnTop, currentMode, shopperHouseData, isVoiceOverlayVisible, DEBUG_MODE]);
useEffect(() => {
if (!shouldFocusVoiceResult) {
// 변화 없으면 포커스 이동하지 않음
if (!targetId) {
// 이전 상태를 현재 상태로 업데이트 (다음 비교를 위해)
shopperHouseDataRef.current = shopperHouseData;
isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible;
isSearchOverlayVisibleRef.current = isSearchOverlayVisible;
currentModeRef.current = currentMode;
return;
}
const focusTimer = setTimeout(() => {
const targetId = 'searchItemContents0';
// 타이머 정리 (이전 타이머가 있으면)
if (unifiedFocusTimerRef.current) {
clearTimeout(unifiedFocusTimerRef.current);
}
// 통일된 지연 시간(100ms)으로 포커스 적용
unifiedFocusTimerRef.current = setTimeout(() => {
const targetElement = document.querySelector(`[data-spotlight-id="${targetId}"]`);
if (targetElement) {
if (targetElement || targetId === SPOTLIGHT_IDS.SEARCH_INPUT_BOX) {
Spotlight.focus(targetId);
if (DEBUG_MODE) {
console.log('[DEBUG] 🎯 ShopperHouse 첫 상품으로 포커스 이동:', targetId);
console.log('[FOCUS] ✅ 포커스 이동 완료:', {
targetId,
scenario: analyzeCurrentScenario(),
hasElement: !!targetElement,
timestamp: new Date().toISOString(),
});
}
} else if (DEBUG_MODE) {
console.log('[DEBUG] ⚠️ ShopperHouse 첫 상품을 찾지 못했습니다');
console.log('[FOCUS] ⚠️ 포커스 대상 요소를 찾지 못했습니다:', {
targetId,
timestamp: new Date().toISOString(),
});
}
setShouldFocusVoiceResult(false);
}, 200);
unifiedFocusTimerRef.current = null;
}, 100);
// Cleanup: 컴포넌트 언마운트 또는 targetId 변경 시 타이머 정리
return () => {
clearTimeout(focusTimer);
if (unifiedFocusTimerRef.current) {
clearTimeout(unifiedFocusTimerRef.current);
unifiedFocusTimerRef.current = null;
}
};
}, [shouldFocusVoiceResult, DEBUG_MODE]);
}, [
isOnTop,
panelInfo,
currentMode,
shopperHouseData,
isVoiceOverlayVisible,
isSearchOverlayVisible,
determineFocusTarget,
analyzeCurrentScenario,
DEBUG_MODE,
]);
/**
* 🎯 [포커스 로직 통합] Ref 값 업데이트
* 매 렌더링마다 이전 상태를 현재 상태로 업데이트
* 이를 통해 다음 useEffect에서 변화를 감지할 수 있음
*/
useEffect(() => {
shopperHouseDataRef.current = shopperHouseData;
searchDatasRef.current = searchDatas;
isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible;
isSearchOverlayVisibleRef.current = isSearchOverlayVisible;
currentModeRef.current = currentMode;
}, [shopperHouseData, searchDatas, isVoiceOverlayVisible, isSearchOverlayVisible, currentMode]);
/**
* LOG 용도,

View File

@@ -212,16 +212,15 @@ const SearchResultsNew = ({
effectiveChangePageRef.current(0, true);
}
const targetId =
hasShopperHouseItems
? 'searchItemContents0'
: themeInfo?.length > 0
? 'searchProduct-0'
: itemInfo?.length > 0
? 'searchItemContents0'
: showInfo?.length > 0
? 'categoryShowContents0'
: null;
const targetId = hasShopperHouseItems
? 'searchItemContents0'
: themeInfo?.length > 0
? 'searchProduct-0'
: itemInfo?.length > 0
? 'searchItemContents0'
: showInfo?.length > 0
? 'categoryShowContents0'
: null;
if (!targetId) {
Spotlight.focus('search-input-box');
@@ -257,22 +256,20 @@ const SearchResultsNew = ({
const spotlightId = `searchItemContents${pendingFocusIndex}`;
const focusTimer = setTimeout(() => {
const targetElement = document.querySelector(
`[data-spotlight-id="${spotlightId}"]`
);
const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`);
if (effectiveChangePageRef && effectiveChangePageRef.current) {
effectiveChangePageRef.current(spotlightId, true, true);
} else if (targetElement) {
targetElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
if (effectiveChangePageRef && effectiveChangePageRef.current) {
effectiveChangePageRef.current(spotlightId, true, true);
} else if (targetElement) {
targetElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
Spotlight.focus(spotlightId);
Spotlight.focus(spotlightId);
setPendingFocusIndex(null);
}, 150);
setPendingFocusIndex(null);
}, 150);
return () => clearTimeout(focusTimer);
return () => clearTimeout(focusTimer);
}, [pendingFocusIndex, effectiveChangePageRef]);
// ProductCard 컴포넌트 - 의존성 최적화 및 안전한 이미지 사용
@@ -324,34 +321,9 @@ const SearchResultsNew = ({
: [];
}, [shopperHouseRelativeQueries]);
useEffect(() => {
const targetId = panelInfo?.currentSpot
? panelInfo?.currentSpot
: hasShopperHouseItems
? 'searchItemContents0'
: themeInfo?.length > 0
? 'searchProduct-0'
: itemInfo?.length > 0
? 'searchItemContents0'
: showInfo?.length > 0
? 'categoryShowContents0'
: null;
if (!targetId) return;
const spotTimeout = setTimeout(() => Spotlight.focus(targetId), 300);
return () => clearTimeout(spotTimeout);
}, [
panelInfo?.currentSpot,
themeInfo?.length,
itemInfo?.length,
showInfo?.length,
convertedShopperHouseItems?.length, // shopperHouseInfo 대신 구체적인 의존성 사용
hasShopperHouseItems,
themeInfo,
itemInfo,
showInfo,
]);
// 🎯 [포커스 로직 통합] SearchPanel.new.v2.jsx로 통합되었으므로 제거됨
// 이전에는 SearchResults에서 panelInfo?.currentSpot을 감시하여 포커스를 처리했으나,
// 이제는 SearchPanel에서 통합 포커스 로직으로 관리됨
return (
<div className={css.searchBox}>

View File

@@ -11,7 +11,9 @@ import Spottable from '@enact/spotlight/Spottable';
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
import { getShopperHouseSearch, clearShopperHouseData } from '../../../actions/searchActions';
import { updatePanel } from '../../../actions/panelActions';
import { clearSTTText } from '../../../actions/webSpeechActions';
import { panel_names } from '../../../utils/Config';
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
import TInput, { ICONS, KINDS } from '../TInput/TInput';
import { useWebSpeech } from '../../../hooks/useWebSpeech';
@@ -790,6 +792,18 @@ const VoiceInputOverlay = ({
addWebSpeechEventLog('API_REQUEST', `Query: "${query}" ${searchIdInfo}`);
dispatch(getShopperHouseSearch(query, currentSearchId));
// ShopperHouse API 호출 시 포커스 초기화 - 패널 정보 업데이트
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
// 🎯 [포커스 로직 통합] currentSpot 제거됨 - shopperHouseData 변경으로 자동 처리됨
tab: 0,
},
})
);
} else {
// 입력이 없거나 너무 짧으면 PROMPT 모드로 복귀
console.log('[VoiceInput] ⚠️ 입력 없음 또는 너무 짧음 - PROMPT 모드로 복귀');
@@ -1073,6 +1087,18 @@ const VoiceInputOverlay = ({
// 다시 API 호출
dispatch(getShopperHouseSearch(query, currentSearchId));
// ShopperHouse API 호출 시 포커스 초기화 - 패널 정보 업데이트
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
// 🎯 [포커스 로직 통합] currentSpot 제거됨 - shopperHouseData 변경으로 자동 처리됨
tab: 0,
},
})
);
// RESPONSE 모드로 복귀 (로딩 상태 표시)
setCurrentMode(VOICE_MODES.RESPONSE);
} else {
@@ -1170,6 +1196,18 @@ const VoiceInputOverlay = ({
try {
dispatch(getShopperHouseSearch(query, currentSearchId));
// ShopperHouse API 호출 시 포커스 초기화 - 패널 정보 업데이트
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: query,
// 🎯 [포커스 로직 통합] currentSpot 제거됨 - shopperHouseData 변경으로 자동 처리됨
tab: 0,
},
})
);
} catch (error) {
console.error('[VoiceInput] ❌ API 호출 실패:', error);
// 에러 발생 시 PROMPT 모드로 복귀
@@ -1191,6 +1229,18 @@ const VoiceInputOverlay = ({
// ShopperHouse API 호출
dispatch(getShopperHouseSearch(searchQuery.trim()));
// ShopperHouse API 호출 시 포커스 초기화 - 패널 정보 업데이트
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: searchQuery.trim(),
// 🎯 [포커스 로직 통합] currentSpot 제거됨 - shopperHouseData 변경으로 자동 처리됨
tab: 0,
},
})
);
// Input 내용 비우기
if (onSearchChange) {
onSearchChange({ value: '' });