[포커스 충돌 해결] VoiceInputOverlay와 SearchPanel 포커스 복원 로직 개선

## 문제 상황
VoiceInputOverlay에서 listening 모드로 음성 인식 후 새로운 데이터가 도착할 때,
VoiceInputOverlay의 포커스 복원 로직과 SearchPanel의 통합 포커스 로직이 충돌하여
첫 번째 아이템으로 포커스가 이동하지 않는 문제 발생

## 해결 방법

### 1. VoiceInputOverlay - 명시적 데이터 확인 추가
- shopperHouseData prop 추가
- 포커스 복원 로직에서 음성 검색 결과 데이터 유무를 명시적으로 확인
- 데이터가 있으면 포커스 복원하지 않고 SearchPanel에 위임

### 2. SearchPanel - 우선순위 시나리오 타이밍 단축
- NEW_SEARCH_LOADED 시나리오의 포커스 타이밍을 100ms → 50ms로 단축
- VoiceInputOverlay의 포커스 복원(100ms)보다 먼저 실행되어 우선권 확보
- 타이밍 이슈로 인한 경쟁 상태(Race Condition) 방지

## 변경 파일
- VoiceInputOverlay.jsx: shopperHouseData prop 추가 및 포커스 복원 조건 개선
- SearchPanel.new.v2.jsx: shopperHouseData prop 전달 및 포커스 타이밍 최적화

## 테스트 시나리오
1. 음성 검색 실행 (listening 모드)
2. 음성 인식 완료 및 API 응답 수신
3. VoiceInputOverlay 닫힘
4.  첫 번째 검색 결과 아이템으로 포커스 이동 확인
This commit is contained in:
Claude
2025-11-09 02:00:27 +00:00
parent 3c49242722
commit ce583d9706
2 changed files with 14 additions and 5 deletions

View File

@@ -1804,9 +1804,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
clearTimeout(unifiedFocusTimerRef.current); clearTimeout(unifiedFocusTimerRef.current);
} }
// 🎯 DETAIL_PANEL_RETURN 시나리오에서는 빠른 포커스 복원 (50ms) // 🎯 [포커스 충돌 해결] 우선순위가 높은 시나리오에서는 빠른 포커스 전환 (50ms)
// DETAIL_PANEL_RETURN: DetailPanel에서 복귀 시 빠른 포커스 복원
// NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지
// 다른 시나리오에서는 기존과 같은 지연 시간 (100ms) // 다른 시나리오에서는 기존과 같은 지연 시간 (100ms)
const focusDelay = scenario === 'DETAIL_PANEL_RETURN' ? 50 : 100; const focusDelay =
scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED'
? 50
: 100;
unifiedFocusTimerRef.current = setTimeout(() => { unifiedFocusTimerRef.current = setTimeout(() => {
const targetElement = document.querySelector(`[data-spotlight-id="${targetId}"]`); const targetElement = document.querySelector(`[data-spotlight-id="${targetId}"]`);
@@ -2237,6 +2242,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
isVoiceResultMode={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT} isVoiceResultMode={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT}
externalResponseText={voiceOverlayResponseText} externalResponseText={voiceOverlayResponseText}
isExternalBubbleSearch={isVoiceOverlayBubbleSearch} isExternalBubbleSearch={isVoiceOverlayBubbleSearch}
shopperHouseData={shopperHouseData} // 🎯 [포커스 충돌 해결] 음성 검색 결과 데이터 전달
/> />
{/* ✨ [Phase 2] Search Input Overlay - isVisible 감지로 전환 자동 감지 */} {/* ✨ [Phase 2] Search Input Overlay - isVisible 감지로 전환 자동 감지 */}

View File

@@ -147,6 +147,7 @@ const VoiceInputOverlay = ({
isVoiceResultMode = false, isVoiceResultMode = false,
externalResponseText = '', externalResponseText = '',
isExternalBubbleSearch = false, isExternalBubbleSearch = false,
shopperHouseData = null, // 🎯 [포커스 충돌 해결] 음성 검색 결과 데이터
}) => { }) => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('🔄 [DEBUG] VoiceInputOverlay render - isVisible:', isVisible, 'mode:', mode); console.log('🔄 [DEBUG] VoiceInputOverlay render - isVisible:', isVisible, 'mode:', mode);
@@ -1163,9 +1164,11 @@ const VoiceInputOverlay = ({
setVoiceInputMode(null); setVoiceInputMode(null);
setCurrentMode(VOICE_MODES.PROMPT); setCurrentMode(VOICE_MODES.PROMPT);
// VoiceInputOverlay를 통한 음성 검색 후에는 TInput으로 포커스 복원하지 않음 // 🎯 [포커스 충돌 해결] VoiceInputOverlay를 통한 음성 검색 후에는 TInput으로 포커스 복원하지 않음
// SearchResults의 첫 번째 상품으로 포커스가 가도록 // SearchResults의 첫 번째 상품으로 포커스가 가도록 SearchPanel에 위임
if (lastFocusedElement.current && !isVoiceResultMode) { // shopperHouseData가 있으면 (음성 검색 결과가 있으면) 포커스 복원하지 않음
const hasVoiceSearchResult = shopperHouseData && shopperHouseData.results && shopperHouseData.results.length > 0;
if (lastFocusedElement.current && !isVoiceResultMode && !hasVoiceSearchResult) {
focusRestoreTimerRef.current = setTimeout(() => { focusRestoreTimerRef.current = setTimeout(() => {
Spotlight.focus(lastFocusedElement.current); Spotlight.focus(lastFocusedElement.current);
}, 100); }, 100);