[251122] fix: DetailPaneel->ProductAllSection Focus-3

🕐 커밋 시간: 2025. 11. 22. 11:01:03

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +156줄
  • 삭제: -84줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript):
     Added: tryFocusUp()
    🔄 Modified: extractProductMeta()
  📄 com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx (javascript):
     Added: Spottable()

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-11-22 11:01:03 +09:00
parent bd2a90b6f5
commit ef7615a538
2 changed files with 140 additions and 68 deletions

View File

@@ -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( const handleSpotlightUpToBackButton = useCallback(
(e) => { (e) => {
e.stopPropagation(); e.stopPropagation();
// BUY NOW 버튼이 보이면 그쪽으로 포커스 이동, 아니면 헤더의 뒤로가기 버튼으로
if (isBillingProductVisible) { const tryFocusUp = () => {
Spotlight.focus('detail-buy-now-button'); if (shopByMobileUpTarget && Spotlight.focus(shopByMobileUpTarget)) {
} else { return shopByMobileUpTarget;
Spotlight.focus('spotlightId_backBtn'); }
} 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 시: 항상 헤더 뒤로가기 버튼으로 // BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로
@@ -868,55 +880,100 @@ export default function ProductAllSection({
Spotlight.focus('spotlightId_backBtn'); Spotlight.focus('spotlightId_backBtn');
}, []); }, []);
// 아래 방향 이동 시 버튼 영역으로 포커스 이동 // SHOP BY MOBILE ↑ 타깃 계산
// BUY NOW / ADD TO CART / SHOP BY MOBILE / FavoriteBtn 등에서 공통 사용 const shopByMobileUpTarget = useMemo(() => {
const focusNextButtons = useCallback(() => { if (isBillingProductVisible) return 'detail-buy-now-button';
// 1) SHOP BY MOBILE이 있으면 우선 이동 if (userNumber && promotions && promotions.length > 0) return 'detail-coupon-button';
if (SpotlightIds?.DETAIL_SHOPBYMOBILE) { return 'spotlightId_backBtn';
if (Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE)) return; }, [isBillingProductVisible, promotions, userNumber]);
}
if (Spotlight.focus('detail_shop_by_mobile')) return;
// 2) 상품 정보 버튼들로 이동 const shopByMobileId = useMemo(
if (!Spotlight.focus('product-details-button')) { () => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
Spotlight.focus('user-reviews-button'); []
);
// 버튼 스택(위→아래) 구성: 실제 렌더링 순서에 맞춰 행(row) 단위로 설정
const { focusDownMap, focusOrder, focusRows } = useMemo(() => {
const rows = [];
if (promotions && promotions.length > 0) {
rows.push(['detail-coupon-button']);
} }
// 포커스가 비거나 다른 곳으로 튈 때 다시 보정
setTimeout(() => { if (isBillingProductVisible) {
const current = Spotlight.getCurrent(); rows.push(['detail-buy-now-button', 'detail-add-to-cart-button']);
const currentId = current?.dataset?.spotlightId; }
if (
!current || const shopRow = [shopByMobileId];
(currentId !== 'product-details-button' && currentId !== 'user-reviews-button') if (panelInfo) {
) { shopRow.push('favoriteBtn');
Spotlight.focus('product-details-button') || Spotlight.focus('user-reviews-button'); }
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) => { const handleSpotlightDown = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
}, []); }, []);
// SHOP BY MOBILE에서 아래로 이동 시 제품 정보 영역으로 포커스 이동 // COUPON에서 아래로 이동 시
const handleSpotlightDownFromShopByMobile = useCallback( const handleSpotlightDownFromCoupon = useMemo(
(e) => { () => buildSpotlightDownHandler('detail-coupon-button'),
e.stopPropagation(); [buildSpotlightDownHandler]
e.preventDefault(); );
// SHOP BY MOBILE에서 아래로 이동 시
focusNextButtons(); const handleSpotlightDownFromShopByMobile = useMemo(
}, () => buildSpotlightDownHandler(shopByMobileId),
[focusNextButtons] [buildSpotlightDownHandler, shopByMobileId]
); );
// BUY NOW / ADD TO CART에서 아래로 이동 시 동일하게 전달 // BUY NOW / ADD TO CART에서 아래로 이동 시
const handleSpotlightDownFromBuyButtons = useCallback( const handleSpotlightDownFromBuyNow = useMemo(
(e) => { () => buildSpotlightDownHandler('detail-buy-now-button'),
e.stopPropagation(); [buildSpotlightDownHandler]
e.preventDefault(); );
focusNextButtons(); const handleSpotlightDownFromAddToCart = useMemo(
}, () => buildSpotlightDownHandler('detail-add-to-cart-button'),
[focusNextButtons] [buildSpotlightDownHandler]
); );
const onFavoriteFlagChanged = useCallback( const onFavoriteFlagChanged = useCallback(
@@ -944,6 +1001,17 @@ export default function ProductAllSection({
dispatch(minimizeModalMedia()); dispatch(minimizeModalMedia());
scrollToSection('scroll-marker-you-may-also-like'); scrollToSection('scroll-marker-you-may-also-like');
}, [scrollToSection, dispatch]); }, [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 scrollPositionRef = useRef(0);
const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적 const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적 const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
@@ -1284,7 +1352,8 @@ export default function ProductAllSection({
handleCouponClick(idx, promotion); handleCouponClick(idx, promotion);
}} }}
onSpotlightUp={handleSpotlightUpFromCouponButtons} onSpotlightUp={handleSpotlightUpFromCouponButtons}
onSpotlightDown={handleSpotlightDown} onSpotlightDown={handleSpotlightDownFromCoupon}
data-spotlight-next-down={focusDownMap['detail-coupon-button']}
size="detail_very_small" size="detail_very_small"
> >
<div className={css.couponText}>COUPON</div> <div className={css.couponText}>COUPON</div>
@@ -1300,8 +1369,8 @@ export default function ProductAllSection({
className={css.buyNowButton} className={css.buyNowButton}
onClick={handleBuyNowClick} onClick={handleBuyNowClick}
onSpotlightUp={handleSpotlightUpFromBuyButtons} onSpotlightUp={handleSpotlightUpFromBuyButtons}
onSpotlightDown={handleSpotlightDownFromBuyButtons} onSpotlightDown={handleSpotlightDownFromBuyNow}
data-spotlight-next-down={SpotlightIds.DETAIL_SHOPBYMOBILE} data-spotlight-next-down={focusDownMap['detail-buy-now-button']}
type="detail_small" type="detail_small"
> >
<div className={css.buyNowText}>{$L('BUY NOW')}</div> <div className={css.buyNowText}>{$L('BUY NOW')}</div>
@@ -1312,8 +1381,8 @@ export default function ProductAllSection({
// onClick={handleAddToCartClick} // onClick={handleAddToCartClick}
onClick={handleBuyNowClick} onClick={handleBuyNowClick}
onSpotlightUp={handleSpotlightUpFromBuyButtons} onSpotlightUp={handleSpotlightUpFromBuyButtons}
onSpotlightDown={handleSpotlightDownFromBuyButtons} onSpotlightDown={handleSpotlightDownFromAddToCart}
data-spotlight-next-down={SpotlightIds.DETAIL_SHOPBYMOBILE} data-spotlight-next-down={focusDownMap['detail-add-to-cart-button']}
type="detail_small" type="detail_small"
> >
<div className={css.addToCartText}>{$L('ADD TO CART')}</div> <div className={css.addToCartText}>{$L('ADD TO CART')}</div>
@@ -1334,7 +1403,8 @@ export default function ProductAllSection({
onClick={handleShopByMobileOpen} onClick={handleShopByMobileOpen}
onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightUp={handleSpotlightUpToBackButton}
onSpotlightDown={handleSpotlightDownFromShopByMobile} onSpotlightDown={handleSpotlightDownFromShopByMobile}
data-spotlight-next-down="product-details-button" data-spotlight-next-down={focusDownMap[shopByMobileId]}
data-spotlight-next-up={shopByMobileUpTarget}
> >
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div> <div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
</TButton> </TButton>
@@ -1347,6 +1417,7 @@ export default function ProductAllSection({
favoriteFlag={favoriteFlag} favoriteFlag={favoriteFlag}
onFavoriteFlagChanged={onFavoriteFlagChanged} onFavoriteFlagChanged={onFavoriteFlagChanged}
kind={'item_detail'} kind={'item_detail'}
nextDownId={focusDownMap['favoriteBtn']}
/> />
</div> </div>
)} )}

View File

@@ -26,6 +26,7 @@ export default function FavoriteBtn({
onFavoriteFlagChanged, onFavoriteFlagChanged,
logMenu, logMenu,
kind, kind,
nextDownId,
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { popupVisible, activePopup } = useSelector((state) => state.common.popup); const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
@@ -81,23 +82,21 @@ export default function FavoriteBtn({
e.stopPropagation(); e.stopPropagation();
if (kind === 'item_detail') { if (kind === 'item_detail') {
e.preventDefault(); e.preventDefault();
if (!Spotlight.focus('product-details-button')) { const targetId = nextDownId || 'product-details-button';
Spotlight.focus('user-reviews-button'); const moved = Spotlight.focus(targetId);
}
// 포커스가 비거나 다른 곳으로 튀는 경우를 방어 // 포커스가 비거나 다른 곳으로 튀는 경우를 방어
setTimeout(() => { if (!moved) {
const current = Spotlight.getCurrent(); setTimeout(() => {
const currentId = current?.dataset?.spotlightId; const current = Spotlight.getCurrent();
if ( const currentId = current?.dataset?.spotlightId;
!current || if (!current || currentId !== targetId) {
(currentId !== 'product-details-button' && currentId !== 'user-reviews-button') Spotlight.focus(targetId);
) { }
Spotlight.focus('product-details-button') || Spotlight.focus('user-reviews-button'); }, 0);
} }
}, 0);
} }
}, },
[kind] [kind, nextDownId]
); );
return ( return (
@@ -110,7 +109,9 @@ export default function FavoriteBtn({
onClick={handleFavoriteClick} onClick={handleFavoriteClick}
onKeyDown={handleFavoriteKeyDown} onKeyDown={handleFavoriteKeyDown}
onSpotlightDown={handleSpotlightDown} 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
}
> >
<div <div
className={classNames( className={classNames(