From c624c60d98838e2af930e01d5312918ef1f0f0b9 Mon Sep 17 00:00:00 2001 From: optrader Date: Sun, 26 Oct 2025 14:59:47 +0900 Subject: [PATCH] =?UTF-8?q?[251026]=20fix:=20App.js=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=20SpotlightFocus=20FOCUS=5FDEBUG=EC=97=90=20=EB=94=B0=EB=9D=BC?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 커밋 시간: 2025. 10. 26. 14:59:46 📊 변경 통계: • 총 파일: 4개 • 추가: +191줄 • 삭제: -7줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/App/App.js ~ com.twin.app.shoptime/src/utils/Config.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/App/App.js | 169 +++++++++++++++++- com.twin.app.shoptime/src/utils/Config.js | 3 + .../views/SearchPanel/SearchPanel.new.v2.jsx | 10 +- .../SearchPanel/SearchResults.new.v2.jsx | 16 +- 4 files changed, 191 insertions(+), 7 deletions(-) diff --git a/com.twin.app.shoptime/src/App/App.js b/com.twin.app.shoptime/src/App/App.js index abe73fa1..f01f1bdf 100644 --- a/com.twin.app.shoptime/src/App/App.js +++ b/com.twin.app.shoptime/src/App/App.js @@ -267,10 +267,62 @@ console.warn = function (...args) { }; const originFocus = Spotlight.focus; +const originMove = Spotlight.move; +const originSilentlyFocus = Spotlight.silentlyFocus; +let lastLoggedSpotlightId = null; +let lastLoggedBlurSpotlightId = null; +let focusLoggingSuppressed = 0; + +const resolveSpotlightIdFromNode = (node) => { + if (!node) return undefined; + + if (node.dataset && node.dataset.spotlightId) { + return node.dataset.spotlightId; + } + + if (typeof node.getAttribute === 'function') { + const idFromAttr = node.getAttribute('data-spotlight-id'); + if (idFromAttr) { + return idFromAttr; + } + } + + if (node.id) { + return node.id; + } + + return undefined; +}; + +const logFocusTransition = (previousNode, currentNode) => { + if (!Config.FOCUS_DEBUG) { + return; + } + + const previousId = resolveSpotlightIdFromNode(previousNode); + const currentId = resolveSpotlightIdFromNode(currentNode); + + if (previousId && previousId !== currentId) { + console.log(`[SpotlightFocus] blur - ${previousId}`); + lastLoggedBlurSpotlightId = previousId; + } + + if (currentId && currentId !== lastLoggedSpotlightId) { + console.log(`[SpotlightFocus] focus - ${currentId}`); + lastLoggedSpotlightId = currentId; + } +}; + Spotlight.focus = function (elem, containerOption) { + const previousNode = Spotlight.getCurrent(); + const ret = originFocus.apply(this, [elem, containerOption]); // this 바인딩을 유지하여 originFocus 호출 + const current = Spotlight.getCurrent(); + if ((ret === true || current !== previousNode) && focusLoggingSuppressed === 0) { + logFocusTransition(previousNode, current); + } + if (ret === true) { - const current = Spotlight.getCurrent(); const floatLayerNode = document.getElementById('floatLayer'); const tabLayoutNode = document.getElementById(SpotlightIds.TAB_LAYOUT); //팝업이 존재할 경우 @@ -300,6 +352,74 @@ Spotlight.focus = function (elem, containerOption) { return ret; }; +Spotlight.move = function (...args) { + if (!originMove) { + return false; + } + const previousNode = Spotlight.getCurrent(); + focusLoggingSuppressed += 1; + let ret; + try { + ret = originMove.apply(this, args); + } finally { + focusLoggingSuppressed = Math.max(0, focusLoggingSuppressed - 1); + } + + const current = Spotlight.getCurrent(); + + if (current !== previousNode) { + logFocusTransition(previousNode, current); + } + + return ret; +}; + +Spotlight.silentlyFocus = function (...args) { + if (!originSilentlyFocus) { + return false; + } + + const previousNode = Spotlight.getCurrent(); + focusLoggingSuppressed += 1; + let ret; + try { + ret = originSilentlyFocus.apply(this, args); + } finally { + focusLoggingSuppressed = Math.max(0, focusLoggingSuppressed - 1); + } + + const current = Spotlight.getCurrent(); + + if (current !== previousNode) { + logFocusTransition(previousNode, current); + } + + return ret; +}; + +const resolveSpotlightIdFromEvent = (event) => { + if (!event) return undefined; + const { detail, target } = event; + + if (detail) { + if (detail.spotlightId) { + return detail.spotlightId; + } + if (detail.id) { + return detail.id; + } + if (detail.target && detail.target.dataset && detail.target.dataset.spotlightId) { + return detail.target.dataset.spotlightId; + } + } + + if (target && target.dataset && target.dataset.spotlightId) { + return target.dataset.spotlightId; + } + + return undefined; +}; + function AppBase(props) { const dispatch = useDispatch(); const httpHeader = useSelector((state) => state.common.httpHeader); @@ -317,6 +437,53 @@ function AppBase(props) { // const termsFlag = useSelector((state) => state.common.termsFlag); const termsData = useSelector((state) => state.home.termsData); + useEffect(() => { + if (!Config.FOCUS_DEBUG) { + return undefined; + } + + const handleFocusLog = (event) => { + const spotlightId = resolveSpotlightIdFromEvent(event); + if (!spotlightId || spotlightId === lastLoggedSpotlightId) { + return; + } + console.log(`[SpotlightFocus] focus - ${spotlightId}`); + lastLoggedSpotlightId = spotlightId; + }; + + const handleBlurLog = (event) => { + const spotlightId = resolveSpotlightIdFromEvent(event); + if (!spotlightId || spotlightId === lastLoggedBlurSpotlightId) { + return; + } + console.log(`[SpotlightFocus] blur - ${spotlightId}`); + lastLoggedBlurSpotlightId = spotlightId; + }; + + const hasSpotlightListener = typeof Spotlight.addEventListener === 'function'; + if (hasSpotlightListener) { + Spotlight.addEventListener('focus', handleFocusLog); + Spotlight.addEventListener('blur', handleBlurLog); + + return () => { + Spotlight.removeEventListener('focus', handleFocusLog); + Spotlight.removeEventListener('blur', handleBlurLog); + }; + } + + if (typeof document !== 'undefined') { + document.addEventListener('spotlightfocus', handleFocusLog); + document.addEventListener('spotlightblur', handleBlurLog); + + return () => { + document.removeEventListener('spotlightfocus', handleFocusLog); + document.removeEventListener('spotlightblur', handleBlurLog); + }; + } + + return undefined; + }, [Config.FOCUS_DEBUG]); + useEffect(() => { // Chromium68 호환성을 위해 Optional Chaining 제거 if (termsData && termsData.data && termsData.data.terms) { diff --git a/com.twin.app.shoptime/src/utils/Config.js b/com.twin.app.shoptime/src/utils/Config.js index 1712c4c2..22bec291 100644 --- a/com.twin.app.shoptime/src/utils/Config.js +++ b/com.twin.app.shoptime/src/utils/Config.js @@ -616,3 +616,6 @@ export const LOG_MESSAGE_ID = { CHECKOUT_CLICK: 'AL_CHECKOUT_CLICK', BUY_NOW: 'AL_BUY_NOW', }; + +// Spotlight 포커스 디버깅 플래그 (true 시 포커스 이동 로그 활성화) +export const FOCUS_DEBUG = true; 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 cd0fa6b3..d0d4604a 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 @@ -1749,8 +1749,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) { } // ✅ INITIAL, VOICE_RESULT & SEARCH_RESULT 모드에서 TInputSimple 내부 포커스 활성화 onKeyDown={handleKeydown} spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX} - // ✅ [251026] ADD: Spotlight 포커스 네비게이션 - data-spotlight-down 속성 사용 - data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'howAboutThese-seeMore' : undefined} + // 🎯 [포커스 중첩 해결] SearchResultsContainer로 포커스 전달 + // SearchResultsContainer가 Spotlight 컨테이너이므로, 포커스가 들어오면 + // enterTo: 'last-focused' 설정에 의해 자동으로 HowAboutThese.small의 SEE MORE로 이동 + data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'search-results-container' : undefined} // 🎯 HowAboutThese 포커스 관리 - 포커스가 검색 입력 영역으로 올 때 감지 onSpotlightUp={handleSearchInputFocus} onSpotlightLeft={handleSearchInputFocus} @@ -1768,8 +1770,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) { // onFocus={onFocusMic} onKeyDown={handleMicKeyDown} spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON} - // ✅ [251026] ADD: Spotlight 포커스 네비게이션 - data-spotlight-down 속성 사용 - data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'howAboutThese-seeMore' : undefined} + // 🎯 [포커스 중첩 해결] SearchResultsContainer로 포커스 전달 + data-spotlight-down={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ? 'search-results-container' : undefined} // 🎯 HowAboutThese 포커스 관리 - 포커스가 마이크 버튼으로 올 때 감지 onSpotlightUp={handleSearchInputFocus} onSpotlightLeft={handleSearchInputFocus} diff --git a/com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx b/com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx index b603b010..47e69606 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames'; import Spotlight from '@enact/spotlight'; +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png'; @@ -215,6 +216,13 @@ const SearchResultsNew = ({ // Spottable 컴포넌트 캐싱하여 메모리 누수 방지 const SpottableDiv = useMemo(() => Spottable('div'), []); + // 🎯 [포커스 중첩 해결] SearchResultsNew를 Spotlight 컨테이너로 감싸기 + // 이렇게 하면 외부에서 들어온 포커스를 자식 요소(HowAboutThese.small)로 자동 전달 + const SearchResultsContainer = useMemo( + () => SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'), + [] + ); + // ❌ [251026] DEPRECATED: 탭 키다운 핸들러 - Spotlight 속성으로 대체됨 // Spotlight의 data-spotlight-up 속성으로 처리하도록 변경 // 기존 코드 보존 (향후 필요시 참고용) @@ -353,7 +361,11 @@ const SearchResultsNew = ({ // 이제는 SearchPanel에서 통합 포커스 로직으로 관리됨 return ( -
+ {/* HowAboutThese Small 버전 - relativeQueries가 존재할 때만 표시 */} {relativeQueries && relativeQueries.length > 0 && (
@@ -445,7 +457,7 @@ const SearchResultsNew = ({
-
+ ); };