[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:
@@ -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 용도,
|
||||
|
||||
Reference in New Issue
Block a user