[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(
(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"
>
<div className={css.couponText}>COUPON</div>
@@ -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"
>
<div className={css.buyNowText}>{$L('BUY NOW')}</div>
@@ -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"
>
<div className={css.addToCartText}>{$L('ADD TO CART')}</div>
@@ -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}
>
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
</TButton>
@@ -1347,6 +1417,7 @@ export default function ProductAllSection({
favoriteFlag={favoriteFlag}
onFavoriteFlagChanged={onFavoriteFlagChanged}
kind={'item_detail'}
nextDownId={focusDownMap['favoriteBtn']}
/>
</div>
)}

View File

@@ -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
}
>
<div
className={classNames(