From ce583d9706c20d6b5b2537a33584359730a3f010 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 02:00:27 +0000 Subject: [PATCH] =?UTF-8?q?[=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0]=20VoiceInputOverlay=EC=99=80=20?= =?UTF-8?q?SearchPanel=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EB=B3=B5=EC=9B=90?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 문제 상황 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. ✅ 첫 번째 검색 결과 아이템으로 포커스 이동 확인 --- .../src/views/SearchPanel/SearchPanel.new.v2.jsx | 10 ++++++++-- .../VoiceInputOverlay/VoiceInputOverlay.jsx | 9 ++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx b/com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx index 3852f1ef..505d4fcd 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx @@ -1804,9 +1804,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) { clearTimeout(unifiedFocusTimerRef.current); } - // 🎯 DETAIL_PANEL_RETURN 시나리오에서는 더 빠른 포커스 복원 (50ms) + // 🎯 [포커스 충돌 해결] 우선순위가 높은 시나리오에서는 빠른 포커스 전환 (50ms) + // DETAIL_PANEL_RETURN: DetailPanel에서 복귀 시 빠른 포커스 복원 + // NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지 // 다른 시나리오에서는 기존과 같은 지연 시간 (100ms) - const focusDelay = scenario === 'DETAIL_PANEL_RETURN' ? 50 : 100; + const focusDelay = + scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED' + ? 50 + : 100; unifiedFocusTimerRef.current = setTimeout(() => { 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} externalResponseText={voiceOverlayResponseText} isExternalBubbleSearch={isVoiceOverlayBubbleSearch} + shopperHouseData={shopperHouseData} // 🎯 [포커스 충돌 해결] 음성 검색 결과 데이터 전달 /> {/* ✨ [Phase 2] Search Input Overlay - isVisible 감지로 전환 자동 감지 */} diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx index 74f07ae5..e23ddbc5 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx @@ -147,6 +147,7 @@ const VoiceInputOverlay = ({ isVoiceResultMode = false, externalResponseText = '', isExternalBubbleSearch = false, + shopperHouseData = null, // 🎯 [포커스 충돌 해결] 음성 검색 결과 데이터 }) => { if (DEBUG_MODE) { console.log('🔄 [DEBUG] VoiceInputOverlay render - isVisible:', isVisible, 'mode:', mode); @@ -1163,9 +1164,11 @@ const VoiceInputOverlay = ({ setVoiceInputMode(null); setCurrentMode(VOICE_MODES.PROMPT); - // VoiceInputOverlay를 통한 음성 검색 후에는 TInput으로 포커스 복원하지 않음 - // SearchResults의 첫 번째 상품으로 포커스가 가도록 함 - if (lastFocusedElement.current && !isVoiceResultMode) { + // 🎯 [포커스 충돌 해결] VoiceInputOverlay를 통한 음성 검색 후에는 TInput으로 포커스 복원하지 않음 + // SearchResults의 첫 번째 상품으로 포커스가 가도록 SearchPanel에 위임 + // shopperHouseData가 있으면 (음성 검색 결과가 있으면) 포커스 복원하지 않음 + const hasVoiceSearchResult = shopperHouseData && shopperHouseData.results && shopperHouseData.results.length > 0; + if (lastFocusedElement.current && !isVoiceResultMode && !hasVoiceSearchResult) { focusRestoreTimerRef.current = setTimeout(() => { Spotlight.focus(lastFocusedElement.current); }, 100);