[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:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user