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