diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx index 20947fe1..602594bd 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -833,18 +833,30 @@ export default function ProductAllSection({ [] ); - // SHOP BY MOBILE 버튼에서 arrow up 시: BUY NOW로 가거나 헤더로 간다 + // SHOP BY MOBILE 버튼에서 arrow up 시: BUY NOW > COUPON > BACK 순으로 이동, 포커스 이탈 보정 const handleSpotlightUpToBackButton = useCallback( (e) => { e.stopPropagation(); - // BUY NOW 버튼이 보이면 그쪽으로 포커스 이동, 아니면 헤더의 뒤로가기 버튼으로 - if (isBillingProductVisible) { - Spotlight.focus('detail-buy-now-button'); - } else { - Spotlight.focus('spotlightId_backBtn'); - } + + const tryFocusUp = () => { + if (shopByMobileUpTarget && Spotlight.focus(shopByMobileUpTarget)) { + return shopByMobileUpTarget; + } + return 'spotlightId_backBtn'; + }; + + const targetId = tryFocusUp(); + + // 포커스가 바로 빠지는 케이스 보정 + setTimeout(() => { + const current = Spotlight.getCurrent(); + const currentId = current?.dataset?.spotlightId; + if (!current || currentId !== targetId) { + tryFocusUp(); + } + }, 0); }, - [isBillingProductVisible] + [shopByMobileUpTarget] ); // BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로 @@ -868,55 +880,100 @@ export default function ProductAllSection({ Spotlight.focus('spotlightId_backBtn'); }, []); - // 아래 방향 이동 시 버튼 영역으로 포커스 이동 - // BUY NOW / ADD TO CART / SHOP BY MOBILE / FavoriteBtn 등에서 공통 사용 - const focusNextButtons = useCallback(() => { - // 1) SHOP BY MOBILE이 있으면 우선 이동 - if (SpotlightIds?.DETAIL_SHOPBYMOBILE) { - if (Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE)) return; - } - if (Spotlight.focus('detail_shop_by_mobile')) return; + // SHOP BY MOBILE ↑ 타깃 계산 + const shopByMobileUpTarget = useMemo(() => { + if (isBillingProductVisible) return 'detail-buy-now-button'; + if (userNumber && promotions && promotions.length > 0) return 'detail-coupon-button'; + return 'spotlightId_backBtn'; + }, [isBillingProductVisible, promotions, userNumber]); - // 2) 상품 정보 버튼들로 이동 - if (!Spotlight.focus('product-details-button')) { - Spotlight.focus('user-reviews-button'); + const shopByMobileId = useMemo( + () => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile', + [] + ); + + // 버튼 스택(위→아래) 구성: 실제 렌더링 순서에 맞춰 행(row) 단위로 설정 + const { focusDownMap, focusOrder, focusRows } = useMemo(() => { + const rows = []; + + if (promotions && promotions.length > 0) { + rows.push(['detail-coupon-button']); } - // 포커스가 비거나 다른 곳으로 튈 때 다시 보정 - setTimeout(() => { - const current = Spotlight.getCurrent(); - const currentId = current?.dataset?.spotlightId; - if ( - !current || - (currentId !== 'product-details-button' && currentId !== 'user-reviews-button') - ) { - Spotlight.focus('product-details-button') || Spotlight.focus('user-reviews-button'); + + if (isBillingProductVisible) { + rows.push(['detail-buy-now-button', 'detail-add-to-cart-button']); + } + + const shopRow = [shopByMobileId]; + if (panelInfo) { + shopRow.push('favoriteBtn'); + } + rows.push(shopRow); + + rows.push(['product-details-button']); + + if (isReviewDataComplete) { + rows.push(['user-reviews-button']); + } + + // 아래 방향: 각 행의 모든 요소가 다음 행의 첫 번째 요소를 바라보도록 매핑 + const downMap = rows.reduce((acc, row, idx) => { + const nextRowFirst = rows[idx + 1]?.[0]; + row.forEach((id) => { + acc[id] = nextRowFirst; + }); + return acc; + }, {}); + + // order는 행을 평탄화한 순서 + const order = rows.flat(); + + return { focusDownMap: downMap, focusOrder: order, focusRows: rows }; + }, [isBillingProductVisible, panelInfo, isReviewDataComplete, promotions, shopByMobileId]); + + // 공통 ↓ 이동 핸들러 + const buildSpotlightDownHandler = useCallback( + (currentId) => (e) => { + e.stopPropagation(); + e.preventDefault(); + + const nextId = focusDownMap[currentId]; + const moved = nextId ? Spotlight.focus(nextId) : false; + + // 이동 실패 시, 스택 내 다른 항목으로라도 보정 + if (!moved) { + const fallback = focusOrder.find((id) => id !== currentId); + if (fallback) { + Spotlight.focus(fallback); + } } - }, 0); - }, []); + }, + [focusDownMap, focusOrder] + ); const handleSpotlightDown = useCallback((e) => { e.stopPropagation(); }, []); - // SHOP BY MOBILE에서 아래로 이동 시 제품 정보 영역으로 포커스 이동 - const handleSpotlightDownFromShopByMobile = useCallback( - (e) => { - e.stopPropagation(); - e.preventDefault(); - - focusNextButtons(); - }, - [focusNextButtons] + // COUPON에서 아래로 이동 시 + const handleSpotlightDownFromCoupon = useMemo( + () => buildSpotlightDownHandler('detail-coupon-button'), + [buildSpotlightDownHandler] + ); + // SHOP BY MOBILE에서 아래로 이동 시 + const handleSpotlightDownFromShopByMobile = useMemo( + () => buildSpotlightDownHandler(shopByMobileId), + [buildSpotlightDownHandler, shopByMobileId] ); - // BUY NOW / ADD TO CART에서 아래로 이동 시 동일하게 전달 - const handleSpotlightDownFromBuyButtons = useCallback( - (e) => { - e.stopPropagation(); - e.preventDefault(); - focusNextButtons(); - }, - [focusNextButtons] + // BUY NOW / ADD TO CART에서 아래로 이동 시 + const handleSpotlightDownFromBuyNow = useMemo( + () => buildSpotlightDownHandler('detail-buy-now-button'), + [buildSpotlightDownHandler] + ); + const handleSpotlightDownFromAddToCart = useMemo( + () => buildSpotlightDownHandler('detail-add-to-cart-button'), + [buildSpotlightDownHandler] ); const onFavoriteFlagChanged = useCallback( @@ -944,6 +1001,17 @@ export default function ProductAllSection({ dispatch(minimizeModalMedia()); scrollToSection('scroll-marker-you-may-also-like'); }, [scrollToSection, dispatch]); + // 헤더 Back 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정 + useEffect(() => { + const firstId = focusRows[0]?.[0]; + if (firstId && Spotlight && Spotlight.set) { + Spotlight.set('spotlightId_backBtn', { + next: { + down: firstId, + }, + }); + } + }, [focusRows]); const scrollPositionRef = useRef(0); const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적 const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적 @@ -1284,7 +1352,8 @@ export default function ProductAllSection({ handleCouponClick(idx, promotion); }} onSpotlightUp={handleSpotlightUpFromCouponButtons} - onSpotlightDown={handleSpotlightDown} + onSpotlightDown={handleSpotlightDownFromCoupon} + data-spotlight-next-down={focusDownMap['detail-coupon-button']} size="detail_very_small" >
COUPON
@@ -1300,8 +1369,8 @@ export default function ProductAllSection({ className={css.buyNowButton} onClick={handleBuyNowClick} onSpotlightUp={handleSpotlightUpFromBuyButtons} - onSpotlightDown={handleSpotlightDownFromBuyButtons} - data-spotlight-next-down={SpotlightIds.DETAIL_SHOPBYMOBILE} + onSpotlightDown={handleSpotlightDownFromBuyNow} + data-spotlight-next-down={focusDownMap['detail-buy-now-button']} type="detail_small" >
{$L('BUY NOW')}
@@ -1312,8 +1381,8 @@ export default function ProductAllSection({ // onClick={handleAddToCartClick} onClick={handleBuyNowClick} onSpotlightUp={handleSpotlightUpFromBuyButtons} - onSpotlightDown={handleSpotlightDownFromBuyButtons} - data-spotlight-next-down={SpotlightIds.DETAIL_SHOPBYMOBILE} + onSpotlightDown={handleSpotlightDownFromAddToCart} + data-spotlight-next-down={focusDownMap['detail-add-to-cart-button']} type="detail_small" >
{$L('ADD TO CART')}
@@ -1334,7 +1403,8 @@ export default function ProductAllSection({ onClick={handleShopByMobileOpen} onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightDown={handleSpotlightDownFromShopByMobile} - data-spotlight-next-down="product-details-button" + data-spotlight-next-down={focusDownMap[shopByMobileId]} + data-spotlight-next-up={shopByMobileUpTarget} >
{$L('SHOP BY MOBILE')}
@@ -1347,6 +1417,7 @@ export default function ProductAllSection({ favoriteFlag={favoriteFlag} onFavoriteFlagChanged={onFavoriteFlagChanged} kind={'item_detail'} + nextDownId={focusDownMap['favoriteBtn']} /> )} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx index 94e0620c..f2056bbd 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx @@ -26,6 +26,7 @@ export default function FavoriteBtn({ onFavoriteFlagChanged, logMenu, kind, + nextDownId, }) { const dispatch = useDispatch(); const { popupVisible, activePopup } = useSelector((state) => state.common.popup); @@ -81,23 +82,21 @@ export default function FavoriteBtn({ e.stopPropagation(); if (kind === 'item_detail') { e.preventDefault(); - if (!Spotlight.focus('product-details-button')) { - Spotlight.focus('user-reviews-button'); - } + const targetId = nextDownId || 'product-details-button'; + const moved = Spotlight.focus(targetId); // 포커스가 비거나 다른 곳으로 튀는 경우를 방어 - setTimeout(() => { - const current = Spotlight.getCurrent(); - const currentId = current?.dataset?.spotlightId; - if ( - !current || - (currentId !== 'product-details-button' && currentId !== 'user-reviews-button') - ) { - Spotlight.focus('product-details-button') || Spotlight.focus('user-reviews-button'); - } - }, 0); + if (!moved) { + setTimeout(() => { + const current = Spotlight.getCurrent(); + const currentId = current?.dataset?.spotlightId; + if (!current || currentId !== targetId) { + Spotlight.focus(targetId); + } + }, 0); + } } }, - [kind] + [kind, nextDownId] ); return ( @@ -110,7 +109,9 @@ export default function FavoriteBtn({ onClick={handleFavoriteClick} onKeyDown={handleFavoriteKeyDown} onSpotlightDown={handleSpotlightDown} - data-spotlight-next-down={kind === 'item_detail' ? 'product-details-button' : undefined} + data-spotlight-next-down={ + kind === 'item_detail' ? nextDownId || 'product-details-button' : undefined + } >