[251122] fix: DetailPaneel->ProductAllSection Focus\

🕐 커밋 시간: 2025. 11. 22. 06:07:46

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

📝 수정된 파일:
  ~ 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):
    🔄 Modified: SpotlightContainerDecorator(), extractProductMeta()
  📄 com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx (javascript):
    🔄 Modified: Spottable()

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-11-22 06:07:46 +09:00
parent 5278151102
commit 8b64875bfe
2 changed files with 155 additions and 165 deletions

View File

@@ -1,60 +1,37 @@
import React, { import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
// import { throttle } from 'lodash'; // import { throttle } from 'lodash';
import { PropTypes } from 'prop-types'; import { PropTypes } from 'prop-types';
import { import { useDispatch, useSelector } from 'react-redux';
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx // src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
import SpotlightContainerDecorator import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import couponImg from '../../../../assets/images/icons/coupon.png'; import couponImg from '../../../../assets/images/icons/coupon.png';
// import Spottable from '@enact/spotlight/Spottable'; // import Spottable from '@enact/spotlight/Spottable';
//image //image
import arrowDown import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png';
import indicatorDefaultImage import { setHidePopup, setShowPopup } from '../../../actions/commonActions.js';
from '../../../../assets/images/img-thumb-empty-144@3x.png';
import {
setHidePopup,
setShowPopup,
} from '../../../actions/commonActions.js';
import { import {
getProductCouponDownload, getProductCouponDownload,
getProductCouponSearch, getProductCouponSearch,
getProductCouponTotDownload, getProductCouponTotDownload,
} from '../../../actions/couponActions.js'; } from '../../../actions/couponActions.js';
// import { pushPanel } from '../../../actions/panelActions'; // import { pushPanel } from '../../../actions/panelActions';
import { import { minimizeModalMedia, restoreModalMedia } from '../../../actions/mediaActions';
minimizeModalMedia,
restoreModalMedia,
} from '../../../actions/mediaActions';
import { pauseFullscreenVideo } from '../../../actions/playActions'; import { pauseFullscreenVideo } from '../../../actions/playActions';
import { resetShowAllReviews } from '../../../actions/productActions'; import { resetShowAllReviews } from '../../../actions/productActions';
import { import { clearAllToasts, removeToast, showToast } from '../../../actions/toastActions';
clearAllToasts,
removeToast,
showToast,
} from '../../../actions/toastActions';
import CustomImage from '../../../components/CustomImage/CustomImage.jsx'; import CustomImage from '../../../components/CustomImage/CustomImage.jsx';
// ProductInfoSection imports // ProductInfoSection imports
import TButton from '../../../components/TButton/TButton'; import TButton from '../../../components/TButton/TButton';
import TPopUp from '../../../components/TPopUp/TPopUp.jsx'; import TPopUp from '../../../components/TPopUp/TPopUp.jsx';
import TVirtualGridList import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList.jsx';
from '../../../components/TVirtualGridList/TVirtualGridList.jsx';
import useReviews from '../../../hooks/useReviews/useReviews'; import useReviews from '../../../hooks/useReviews/useReviews';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
@@ -86,19 +63,13 @@ import StarRating from '../components/StarRating';
// ProductContentSection imports // ProductContentSection imports
import TScrollerDetail from '../components/TScroller/TScrollerDetail'; import TScrollerDetail from '../components/TScroller/TScrollerDetail';
import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton'; import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton';
import ProductDescription import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription';
from '../ProductContentSection/ProductDescription/ProductDescription'; import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new';
import ProductDetail import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
from '../ProductContentSection/ProductDetail/ProductDetail.new'; import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import {
ProductVideoV2,
} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo
from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; // import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
import YouMayAlsoLike import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
import QRCode from '../ProductInfoSection/QRCode/QRCode'; import QRCode from '../ProductInfoSection/QRCode/QRCode';
import ProductOverview from '../ProductOverview/ProductOverview'; import ProductOverview from '../ProductOverview/ProductOverview';
// CSS imports // CSS imports
@@ -139,7 +110,7 @@ const HorizontalContainer = SpotlightContainerDecorator(
'div' 'div'
); );
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable('div');
const getProductData = curry((productType, themeProductInfo, productInfo) => const getProductData = curry((productType, themeProductInfo, productInfo) =>
pipe( pipe(
@@ -192,15 +163,9 @@ export default function ProductAllSection({
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
const youmaylikeData = useSelector((state) => state.main.youmaylikeData); const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
//coupon //coupon
const { partnerCoupon } = useSelector( const { partnerCoupon } = useSelector((state) => state.coupon.productCouponSearchData);
(state) => state.coupon.productCouponSearchData const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
); const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략) // ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(1); const [productVideoVersion, setProductVideoVersion] = useState(1);
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화) // 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
@@ -235,10 +200,10 @@ export default function ProductAllSection({
const [selectedCoupon, setSelectedCoupon] = useState(); const [selectedCoupon, setSelectedCoupon] = useState();
const [downloadCouponArr, setDownloadCouponArr] = useState([]); const [downloadCouponArr, setDownloadCouponArr] = useState([]);
const [couponTypes, setCouponTypes] = useState(null); const [couponTypes, setCouponTypes] = useState(null);
const [couponCodes, setCouponCodes] = useState(""); const [couponCodes, setCouponCodes] = useState('');
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
useEffect(()=>{ useEffect(() => {
dispatch( dispatch(
getProductCouponSearch({ getProductCouponSearch({
patnrId: selectedPatnrId, patnrId: selectedPatnrId,
@@ -246,18 +211,18 @@ export default function ProductAllSection({
mbrNo: userNumber, mbrNo: userNumber,
}) })
); );
},[dispatch]) }, [dispatch]);
const getCouponCode = () => { const getCouponCode = () => {
const snoArray = []; const snoArray = [];
for (let i = 0; i < selectedCoupon.length; i++) { for (let i = 0; i < selectedCoupon.length; i++) {
if (selectedCoupon[i].downloadYn === "Y") { if (selectedCoupon[i].downloadYn === 'Y') {
snoArray.push(selectedCoupon[i].cpnSno); snoArray.push(selectedCoupon[i].cpnSno);
} }
} }
setCouponCodes(snoArray.join(", ")); setCouponCodes(snoArray.join(', '));
}; };
useEffect(() => { useEffect(() => {
@@ -269,7 +234,7 @@ export default function ProductAllSection({
useEffect(() => { useEffect(() => {
const newPromotions = []; const newPromotions = [];
if (partnerCoupon && partnerCoupon.length > 0) { if (partnerCoupon && partnerCoupon.length > 0) {
newPromotions.push($L("SPECIAL PROMOTION")); newPromotions.push($L('SPECIAL PROMOTION'));
} }
setPromotions(newPromotions); setPromotions(newPromotions);
}, [partnerCoupon]); }, [partnerCoupon]);
@@ -280,7 +245,7 @@ export default function ProductAllSection({
dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup)); dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup));
return; return;
} }
if (promotion === "SPECIAL PROMOTION") { if (promotion === 'SPECIAL PROMOTION') {
setSelectedCoupon(partnerCoupon); setSelectedCoupon(partnerCoupon);
} }
setCouponTypes(idx); setCouponTypes(idx);
@@ -291,9 +256,7 @@ export default function ProductAllSection({
const handleCouponTotDownload = useCallback(() => { const handleCouponTotDownload = useCallback(() => {
if (selectedCoupon && userNumber) { if (selectedCoupon && userNumber) {
const couponCodesArray = couponCodes const couponCodesArray = couponCodes.split(',').map((code) => parseInt(code.trim()));
.split(",")
.map((code) => parseInt(code.trim()));
setDownloadCouponArr((prevArr) => [...prevArr, ...couponCodesArray]); setDownloadCouponArr((prevArr) => [...prevArr, ...couponCodesArray]);
dispatch( dispatch(
@@ -302,7 +265,7 @@ export default function ProductAllSection({
cpnSnoAll: couponCodes, cpnSnoAll: couponCodes,
}) })
); );
dispatch(showToast({message : "Your coupon download is complete."})); dispatch(showToast({ message: 'Your coupon download is complete.' }));
} }
}, [dispatch, selectedCoupon, userNumber, couponCodes]); }, [dispatch, selectedCoupon, userNumber, couponCodes]);
@@ -311,7 +274,7 @@ export default function ProductAllSection({
dispatch(setHidePopup()); dispatch(setHidePopup());
let currentSpot; let currentSpot;
if (typeof spotlightId === "string") { if (typeof spotlightId === 'string') {
currentSpot = spotlightId; currentSpot = spotlightId;
} else { } else {
currentSpot = SpotlightIds.DETAIL_BUYNOW; currentSpot = SpotlightIds.DETAIL_BUYNOW;
@@ -324,7 +287,7 @@ export default function ProductAllSection({
} }
}, },
[dispatch] [dispatch]
) );
const renderItem = useCallback( const renderItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
@@ -343,8 +306,8 @@ export default function ProductAllSection({
duplDwldYn, duplDwldYn,
} = selectedCoupon[index]; } = selectedCoupon[index];
const couponAplyStartDate = cpnAplyStrtDtt.split(" ")[0]; const couponAplyStartDate = cpnAplyStrtDtt.split(' ')[0];
const couponAplyEndDate = cpnAplyEndDtt.split(" ")[0]; const couponAplyEndDate = cpnAplyEndDtt.split(' ')[0];
const onFocus = (index) => { const onFocus = (index) => {
setSelectedCouponIndex(index); setSelectedCouponIndex(index);
@@ -359,16 +322,14 @@ export default function ProductAllSection({
if ( if (
downloadCouponArr.length > 0 && downloadCouponArr.length > 0 &&
downloadCouponArr.includes(cpnSno) && downloadCouponArr.includes(cpnSno) &&
(downloadYn === "N" || (duplDwldYn === "Y" && downloadYn === "Y")) (downloadYn === 'N' || (duplDwldYn === 'Y' && downloadYn === 'Y'))
) { ) {
return; return;
} }
setDownloadCouponArr((prevArr) => [...prevArr, cpnSno]); setDownloadCouponArr((prevArr) => [...prevArr, cpnSno]);
dispatch( dispatch(getProductCouponDownload({ mbrNo: userNumber, cpnSno: cpnSno }));
getProductCouponDownload({ mbrNo: userNumber, cpnSno: cpnSno }) dispatch(showToast({ message: 'Your coupon download is complete.' }));
); };
dispatch(showToast({message : "Your coupon download is complete."}));
};
return ( return (
<SpottableComponent <SpottableComponent
@@ -377,23 +338,17 @@ export default function ProductAllSection({
{...rest} {...rest}
onFocus={() => onFocus(index)} onFocus={() => onFocus(index)}
onBlur={onBlur} onBlur={onBlur}
onClick={downloadYn === "N" ? null : handleDownloadClick} onClick={downloadYn === 'N' ? null : handleDownloadClick}
> >
<div <div
className={css.couponItem} className={css.couponItem}
aria-label={ aria-label={'Purchase over ' + cpnAplyMinPurcAmt + 'up to ' + cpnAplyMaxDcAmt + ' off'}
"Purchase over " +
cpnAplyMinPurcAmt +
"up to " +
cpnAplyMaxDcAmt +
" off"
}
> >
<div className={css.couponTopContents}> <div className={css.couponTopContents}>
{shptmDcTpCd === "CPN00401" && ( {shptmDcTpCd === 'CPN00401' && (
<span className={css.couponLate}>{`${currSign}${dcAmt}`}</span> <span className={css.couponLate}>{`${currSign}${dcAmt}`}</span>
)} )}
{shptmDcTpCd === "CPN00402" && ( {shptmDcTpCd === 'CPN00402' && (
<span className={css.couponLate}>{`${cpnDctrt}%`}</span> <span className={css.couponLate}>{`${cpnDctrt}%`}</span>
)} )}
<span className={css.title} aria-label={cpnTtl}> <span className={css.title} aria-label={cpnTtl}>
@@ -403,25 +358,21 @@ export default function ProductAllSection({
<div className={css.couponMiddleContents}> <div className={css.couponMiddleContents}>
<span> <span>
{$L( {$L('Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)')
"Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)" .replace('{cpnAplyMinPurcAmt}', cpnAplyMinPurcAmt)
) .replace('{cpnAplyMaxDcAmt}', cpnAplyMaxDcAmt)}
.replace("{cpnAplyMinPurcAmt}", cpnAplyMinPurcAmt)
.replace("{cpnAplyMaxDcAmt}", cpnAplyMaxDcAmt)}
</span> </span>
<span className={classNames(css.content, css.date)}> <span className={classNames(css.content, css.date)}>
{couponAplyStartDate}~{couponAplyEndDate} {couponAplyStartDate}~{couponAplyEndDate}
</span> </span>
</div> </div>
{downloadYn !== "N" && ( {downloadYn !== 'N' && (
<div <div
className={classNames( className={classNames(
css.couponBottomButton, css.couponBottomButton,
downloadCouponArr.length > 0 && downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && css.disable,
downloadCouponArr.includes(cpnSno) && duplDwldYn === 'N' && downloadYn === 'Y' && css.disable,
css.disable,
duplDwldYn === "N" && downloadYn === "Y" && css.disable,
!downloadCouponArr.includes(cpnSno) && !downloadCouponArr.includes(cpnSno) &&
index === selectedCouponIndex && index === selectedCouponIndex &&
focused && focused &&
@@ -429,13 +380,9 @@ export default function ProductAllSection({
)} )}
aria-label="Download Button" aria-label="Download Button"
> >
{downloadCouponArr.length > 0 && {downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno)
downloadCouponArr.includes(cpnSno) ? $L('DOWNLOAD COMPLETED')
? : $L('DOWNLOAD')}
$L("DOWNLOAD COMPLETED")
:
$L("DOWNLOAD")
}
</div> </div>
)} )}
</div> </div>
@@ -666,13 +613,13 @@ export default function ProductAllSection({
//닫히도록 //닫히도록
const handleCloseToast = useCallback(() => { const handleCloseToast = useCallback(() => {
// 팝업이 열려있으면 닫지 않음 // 팝업이 열려있으면 닫지 않음
if (popupVisible) { if (popupVisible) {
return; // 팝업이 활성이면 무시 return; // 팝업이 활성이면 무시
} }
dispatch(clearAllToasts()); dispatch(clearAllToasts());
setOpenToast(false); setOpenToast(false);
}, [dispatch, popupVisible]); }, [dispatch, popupVisible]);
// 스크롤 컨테이너의 클릭 이벤트 추적용 로깅 // 스크롤 컨테이너의 클릭 이벤트 추적용 로깅
const handleScrollContainerClick = useCallback((e) => { const handleScrollContainerClick = useCallback((e) => {
@@ -901,18 +848,20 @@ export default function ProductAllSection({
); );
// BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로 // BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로
const handleSpotlightUpFromBuyButtons = useCallback((e) => { const handleSpotlightUpFromBuyButtons = useCallback(
e.stopPropagation(); (e) => {
if(promotions && promotions.length > 0){ e.stopPropagation();
Spotlight.focus('detail-coupon-button'); if (promotions && promotions.length > 0) {
} else { Spotlight.focus('detail-coupon-button');
// 쿠폰 버튼이 없을 때 뒤로가기 버튼으로 포커스 이동 (특정 Chrome 버전에서의 포커스 문제 해결) } else {
setTimeout(() => { // 쿠폰 버튼이 없을 때 뒤로가기 버튼으로 포커스 이동 (특정 Chrome 버전에서의 포커스 문제 해결)
Spotlight.focus('spotlightId_backBtn'); setTimeout(() => {
}, 0); Spotlight.focus('spotlightId_backBtn');
} }, 0);
}, [promotions]); }
},
[promotions]
);
const handleSpotlightUpFromCouponButtons = useCallback((e) => { const handleSpotlightUpFromCouponButtons = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
@@ -923,6 +872,28 @@ export default function ProductAllSection({
e.stopPropagation(); e.stopPropagation();
}, []); }, []);
// SHOP BY MOBILE에서 아래로 이동 시 제품 정보 영역으로 포커스 이동
const handleSpotlightDownFromShopByMobile = useCallback((e) => {
e.stopPropagation();
e.preventDefault();
if (!Spotlight.focus('product-details-button')) {
Spotlight.focus('user-reviews-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');
}
}, 0);
}, []);
const onFavoriteFlagChanged = useCallback( const onFavoriteFlagChanged = useCallback(
(newFavoriteFlag) => setFavoriteOverride(newFavoriteFlag), (newFavoriteFlag) => setFavoriteOverride(newFavoriteFlag),
[] []
@@ -1216,8 +1187,6 @@ export default function ProductAllSection({
}; };
}, []); }, []);
// 초기 로딩 중에는 Skeleton 표시 // 초기 로딩 중에는 Skeleton 표시
if (isInitialLoading) { if (isInitialLoading) {
return ( return (
@@ -1226,9 +1195,6 @@ export default function ProductAllSection({
</div> </div>
); );
} }
return ( return (
<HorizontalContainer className={css.detailArea} onClick={handleCloseToast}> <HorizontalContainer className={css.detailArea} onClick={handleCloseToast}>
@@ -1276,31 +1242,32 @@ export default function ProductAllSection({
</div> </div>
</ProductOverview> </ProductOverview>
{userNumber && promotions.map((promotion, idx) => { {userNumber &&
return( promotions.map((promotion, idx) => {
<div className={css.couponContainer} key={idx}> return (
<div className={css.couponTitleText}> <div className={css.couponContainer} key={idx}>
<div className={css.firstTitle}>SPECIAL PROMOTION</div> <div className={css.couponTitleText}>
<div className={css.secondTitle}>Coupon only applicable to this product!</div> <div className={css.firstTitle}>SPECIAL PROMOTION</div>
</div> <div className={css.secondTitle}>
<TButton Coupon only applicable to this product!
spotlightId="detail-coupon-button" </div>
className={css.couponButton}
onClick={() => {
handleCouponClick(idx, promotion);
}}
onSpotlightUp={handleSpotlightUpFromCouponButtons}
onSpotlightDown={handleSpotlightDown}
size="detail_very_small"
>
<div className={css.couponText}>
COUPON
</div> </div>
<img className={css.buttonImg} src={couponImg} /> <TButton
</TButton> spotlightId="detail-coupon-button"
</div> className={css.couponButton}
) onClick={() => {
})} handleCouponClick(idx, promotion);
}}
onSpotlightUp={handleSpotlightUpFromCouponButtons}
onSpotlightDown={handleSpotlightDown}
size="detail_very_small"
>
<div className={css.couponText}>COUPON</div>
<img className={css.buttonImg} src={couponImg} />
</TButton>
</div>
);
})}
{isBillingProductVisible && ( {isBillingProductVisible && (
<div className={css.buyNowCartContainer}> <div className={css.buyNowCartContainer}>
<TButton <TButton
@@ -1339,7 +1306,8 @@ export default function ProductAllSection({
className={css.shopByMobileButton} className={css.shopByMobileButton}
onClick={handleShopByMobileOpen} onClick={handleShopByMobileOpen}
onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightUp={handleSpotlightUpToBackButton}
onSpotlightDown={handleSpotlightDown} onSpotlightDown={handleSpotlightDownFromShopByMobile}
data-spotlight-next-down="product-details-button"
> >
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div> <div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
</TButton> </TButton>
@@ -1608,8 +1576,8 @@ export default function ProductAllSection({
onClick={handleCouponTotDownload} onClick={handleCouponTotDownload}
onClose={onClose} onClose={onClose}
hasButton hasButton
button1Text={$L("DOWNLOAD ALL")} button1Text={$L('DOWNLOAD ALL')}
button2Text={$L("CLOSE")} button2Text={$L('CLOSE')}
> >
<Container className={css.itemWrap}> <Container className={css.itemWrap}>
{selectedCoupon && selectedCoupon.length > 0 && ( {selectedCoupon && selectedCoupon.length > 0 && (
@@ -1625,9 +1593,7 @@ export default function ProductAllSection({
/> />
)} )}
</Container> </Container>
<div <div className={css.couponRemain}>{`1/${selectedCoupon?.length}`}</div>
className={css.couponRemain}
>{`1/${selectedCoupon?.length}`}</div>
</TPopUp> </TPopUp>
)} )}
</HorizontalContainer> </HorizontalContainer>

View File

@@ -66,16 +66,39 @@ export default function FavoriteBtn({
onFavoriteFlagChanged(favoriteFlag === 'Y' ? 'N' : 'Y'); onFavoriteFlagChanged(favoriteFlag === 'Y' ? 'N' : 'Y');
}, [dispatch, favoriteFlag, onFavoriteFlagChanged]); }, [dispatch, favoriteFlag, onFavoriteFlagChanged]);
const handleFavoriteKeyDown = useCallback((e) => { const handleFavoriteKeyDown = useCallback(
if (e.key === 'Enter') { (e) => {
e.preventDefault(); if (e.key === 'Enter') {
handleFavoriteClick(); e.preventDefault();
} handleFavoriteClick();
}, [handleFavoriteClick]); }
},
[handleFavoriteClick]
);
const handleSpotlightDown = useCallback((e) => { const handleSpotlightDown = useCallback(
e.stopPropagation(); (e) => {
}, []); e.stopPropagation();
if (kind === 'item_detail') {
e.preventDefault();
if (!Spotlight.focus('product-details-button')) {
Spotlight.focus('user-reviews-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');
}
}, 0);
}
},
[kind]
);
return ( return (
<SpottableDiv <SpottableDiv
@@ -87,6 +110,7 @@ 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}
> >
<div <div
className={classNames( className={classNames(