[251011] fix: ProudctAllSection scroller 리렌더링 최적화
🕐 커밋 시간: 2025. 10. 11. 19:42:08 📊 변경 통계: • 총 파일: 4개 • 추가: +48줄 • 삭제: -32줄 📁 추가된 파일: + com.twin.app.shoptime/src/views/DetailPanel/components/AutoScrollAreaDetail/AutoScrollAreaDetail.jsx + com.twin.app.shoptime/src/views/DetailPanel/components/AutoScrollAreaDetail/AutoScrollAreaDetail.module.less 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: LayoutSample() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx (javascript): ✅ Added: scrollToElement() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/AutoScrollAreaDetail/AutoScrollAreaDetail.jsx (javascript): ✅ Added: animationScroll() 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선
This commit is contained in:
@@ -1,31 +1,19 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
//image
|
||||
import arrowDown
|
||||
from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
|
||||
import indicatorDefaultImage
|
||||
from '../../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
|
||||
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import { pushPanel } from '../../../actions/panelActions';
|
||||
import { resetShowAllReviews } from '../../../actions/productActions';
|
||||
import { showToast } from '../../../actions/toastActions';
|
||||
@@ -59,16 +47,12 @@ import ProductTag from '../components/ProductTag';
|
||||
import StarRating from '../components/StarRating';
|
||||
// ProductContentSection imports
|
||||
import TScrollerDetail from '../components/TScroller/TScrollerDetail';
|
||||
import ProductDescription
|
||||
from '../ProductContentSection/ProductDescription/ProductDescription';
|
||||
import ProductDetail
|
||||
from '../ProductContentSection/ProductDetail/ProductDetail.new';
|
||||
import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription';
|
||||
import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new';
|
||||
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo';
|
||||
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
|
||||
import ViewAllReviewsButton
|
||||
from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
|
||||
import YouMayAlsoLike
|
||||
from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
|
||||
import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
|
||||
import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
|
||||
import QRCode from '../ProductInfoSection/QRCode/QRCode';
|
||||
import ProductOverview from '../ProductOverview/ProductOverview';
|
||||
// CSS imports
|
||||
@@ -78,45 +62,42 @@ import css from './ProductAllSection.module.less';
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "last-focused",
|
||||
enterTo: 'last-focused',
|
||||
preserveld: true,
|
||||
leaveFor: { right: "content-scroller-container" },
|
||||
spotlightDirection: "vertical",
|
||||
leaveFor: { right: 'content-scroller-container' },
|
||||
spotlightDirection: 'vertical',
|
||||
},
|
||||
"div"
|
||||
'div'
|
||||
);
|
||||
|
||||
const ContentContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "default-element",
|
||||
enterTo: 'default-element',
|
||||
preserveld: true,
|
||||
leaveFor: {
|
||||
left: "spotlight-product-info-section-container",
|
||||
left: 'spotlight-product-info-section-container',
|
||||
},
|
||||
restrict: "none",
|
||||
spotlightDirection: "vertical",
|
||||
restrict: 'none',
|
||||
spotlightDirection: 'vertical',
|
||||
},
|
||||
"div"
|
||||
'div'
|
||||
);
|
||||
|
||||
const HorizontalContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "last-focused",
|
||||
enterTo: 'last-focused',
|
||||
preserveld: true,
|
||||
defaultElement: "spotlight-product-info-section-container",
|
||||
spotlightDirection: "horizontal",
|
||||
defaultElement: 'spotlight-product-info-section-container',
|
||||
spotlightDirection: 'horizontal',
|
||||
},
|
||||
"div"
|
||||
'div'
|
||||
);
|
||||
|
||||
// FP: Pure function to determine product data based on typeP
|
||||
const getProductData = curry((productType, themeProductInfo, productInfo) =>
|
||||
pipe(
|
||||
when(
|
||||
() =>
|
||||
isVal(productType) &&
|
||||
productType === "theme" &&
|
||||
isVal(themeProductInfo),
|
||||
() => isVal(productType) && productType === 'theme' && isVal(themeProductInfo),
|
||||
() => themeProductInfo
|
||||
),
|
||||
defaultTo(productInfo),
|
||||
@@ -132,17 +113,17 @@ const deriveFavoriteFlag = curry((favoriteOverride, productData) => {
|
||||
return favoriteOverride;
|
||||
}
|
||||
// 그렇지 않으면 productData의 favorYn 값을 사용 (기본값 'N')
|
||||
return pipe(get("favorYn"), defaultTo("N"))(productData);
|
||||
return pipe(get('favorYn'), defaultTo('N'))(productData);
|
||||
});
|
||||
|
||||
// FP: Pure function to extract review grade and order phone
|
||||
const extractProductMeta = (productInfo) => ({
|
||||
revwGrd: get("revwGrd", productInfo),
|
||||
orderPhnNo: get("orderPhnNo", productInfo),
|
||||
revwGrd: get('revwGrd', productInfo),
|
||||
orderPhnNo: get('orderPhnNo', productInfo),
|
||||
});
|
||||
|
||||
// 레이아웃 확인용 샘플 컴포넌트 - Spottable로 변경
|
||||
const SpottableComponent = Spottable("div");
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
const LayoutSample = ({ onClick }) => (
|
||||
<SpottableComponent
|
||||
@@ -171,9 +152,7 @@ export default function ProductAllSection({
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Redux 상태
|
||||
const webOSVersion = useSelector(
|
||||
(state) => state.common.appStatus.webOSVersion
|
||||
);
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
const groupInfos = useSelector((state) => state.product.groupInfo);
|
||||
|
||||
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
|
||||
@@ -224,20 +203,20 @@ export default function ProductAllSection({
|
||||
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
|
||||
const isBillingProductVisible = useMemo(() => {
|
||||
return (
|
||||
productData?.pmtSuptYn === "Y" &&
|
||||
productData?.grPrdtProcYn === "N" &&
|
||||
productData?.pmtSuptYn === 'Y' &&
|
||||
productData?.grPrdtProcYn === 'N' &&
|
||||
panelInfo?.prdtId &&
|
||||
webOSVersion >= "6.0"
|
||||
webOSVersion >= '6.0'
|
||||
);
|
||||
}, [productData, webOSVersion, panelInfo?.prdtId]);
|
||||
|
||||
// 구매 불가 상품 - DetailPanel.backup.jsx와 동일한 로직
|
||||
const isUnavailableProductVisible = useMemo(() => {
|
||||
return (
|
||||
productData?.pmtSuptYn === "N" ||
|
||||
(productData?.pmtSuptYn === "Y" &&
|
||||
productData?.grPrdtProcYn === "N" &&
|
||||
webOSVersion < "6.0" &&
|
||||
productData?.pmtSuptYn === 'N' ||
|
||||
(productData?.pmtSuptYn === 'Y' &&
|
||||
productData?.grPrdtProcYn === 'N' &&
|
||||
webOSVersion < '6.0' &&
|
||||
panelInfo?.prdtId)
|
||||
);
|
||||
}, [productData, webOSVersion, panelInfo?.prdtId]);
|
||||
@@ -245,8 +224,8 @@ export default function ProductAllSection({
|
||||
// 그룹 상품 - DetailPanel.backup.jsx와 동일한 로직
|
||||
const isGroupProductVisible = useMemo(() => {
|
||||
return (
|
||||
productData?.pmtSuptYn === "Y" &&
|
||||
productData?.grPrdtProcYn === "Y" &&
|
||||
productData?.pmtSuptYn === 'Y' &&
|
||||
productData?.grPrdtProcYn === 'Y' &&
|
||||
groupInfos &&
|
||||
groupInfos.length > 0
|
||||
);
|
||||
@@ -254,10 +233,7 @@ export default function ProductAllSection({
|
||||
|
||||
// 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직
|
||||
const isTravelProductVisible = useMemo(() => {
|
||||
return (
|
||||
panelInfo?.curationId &&
|
||||
(panelInfo?.type === "theme" || panelInfo?.type === "hotel")
|
||||
);
|
||||
return panelInfo?.curationId && (panelInfo?.type === 'theme' || panelInfo?.type === 'hotel');
|
||||
}, [panelInfo]);
|
||||
|
||||
// useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당
|
||||
@@ -283,69 +259,66 @@ export default function ProductAllSection({
|
||||
|
||||
// ProductAllSection 마운트 시 showAllReviews 초기화
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[ProductAllSection] Component mounted - resetting showAllReviews to false"
|
||||
);
|
||||
console.log('[ProductAllSection] Component mounted - resetting showAllReviews to false');
|
||||
dispatch(resetShowAllReviews());
|
||||
}, []); // 빈 dependency array = 마운트 시에만 실행
|
||||
|
||||
// [임시 테스트] LayoutSample 클릭 핸들러 - ShowUserReviews와 동일하게 UserReviewPanel 열기
|
||||
const handleLayoutSampleClick = useCallback(() => {
|
||||
console.log(`[ProductId] LayoutSample clicked - opening UserReviewPanel`, {
|
||||
productDataPrdtId: productData && productData.prdtId,
|
||||
hasProductData: !!productData,
|
||||
reviewTotalCount: stats.totalReviews,
|
||||
averageRating: stats.averageRating,
|
||||
productData: productData,
|
||||
});
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.USER_REVIEW_PANEL,
|
||||
panelInfo: {
|
||||
prdtId: productData.prdtId,
|
||||
productImage:
|
||||
(productData.imgUrls600 && productData.imgUrls600[0]) ||
|
||||
(productData.imgUrls && productData.imgUrls[0]) ||
|
||||
productData.thumbnailUrl ||
|
||||
"https://placehold.co/150x150",
|
||||
brandLogo: productData.patncLogoPath || "https://placehold.co/50x50",
|
||||
productName: productData.prdtNm || "상품명 정보가 없습니다",
|
||||
avgRating: stats.averageRating || 5,
|
||||
reviewCount: stats.totalReviews || 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dispatch, productData, stats]);
|
||||
// const handleLayoutSampleClick = useCallback(() => {
|
||||
// console.log(`[ProductId] LayoutSample clicked - opening UserReviewPanel`, {
|
||||
// productDataPrdtId: productData && productData.prdtId,
|
||||
// hasProductData: !!productData,
|
||||
// reviewTotalCount: stats.totalReviews,
|
||||
// averageRating: stats.averageRating,
|
||||
// productData: productData,
|
||||
// });
|
||||
// dispatch(
|
||||
// pushPanel({
|
||||
// name: panel_names.USER_REVIEW_PANEL,
|
||||
// panelInfo: {
|
||||
// prdtId: productData.prdtId,
|
||||
// productImage:
|
||||
// (productData.imgUrls600 && productData.imgUrls600[0]) ||
|
||||
// (productData.imgUrls && productData.imgUrls[0]) ||
|
||||
// productData.thumbnailUrl ||
|
||||
// "https://placehold.co/150x150",
|
||||
// brandLogo: productData.patncLogoPath || "https://placehold.co/50x50",
|
||||
// productName: productData.prdtNm || "상품명 정보가 없습니다",
|
||||
// avgRating: stats.averageRating || 5,
|
||||
// reviewCount: stats.totalReviews || 0,
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
// }, [dispatch, productData, stats]);
|
||||
|
||||
// BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시
|
||||
const handleBuyNowClick = useCallback(() => {
|
||||
console.log("[BuyNow] Buy Now button clicked");
|
||||
console.log('[BuyNow] Buy Now button clicked');
|
||||
dispatch(
|
||||
showToast({
|
||||
message: "",
|
||||
type: "buyOption",
|
||||
message: '',
|
||||
type: 'buyOption',
|
||||
duration: 0,
|
||||
position: "bottom-center",
|
||||
position: 'bottom-center',
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
// ADD TO CART 버튼 클릭 핸들러
|
||||
const handleAddToCartClick = useCallback(() => {
|
||||
console.log("[AddToCart] Add To Cart button clicked");
|
||||
console.log('[AddToCart] Add To Cart button clicked');
|
||||
// TODO: 장바구니 추가 로직 구현
|
||||
}, []);
|
||||
|
||||
// 디버깅: 실제 이미지 및 동영상 데이터 확인
|
||||
useEffect(() => {
|
||||
console.log("[ProductId] ProductAllSection productData check:", {
|
||||
console.log('[ProductId] ProductAllSection productData check:', {
|
||||
hasProductData: !!productData,
|
||||
productDataPrdtId: productData && productData.prdtId,
|
||||
imgUrls600: productData && productData.imgUrls600,
|
||||
imgUrls600Length:
|
||||
productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
|
||||
? "array"
|
||||
? 'array'
|
||||
: typeof (productData && productData.imgUrls600),
|
||||
// 동영상 관련 정보 추가
|
||||
prdtMediaUrl: productData && productData.prdtMediaUrl,
|
||||
@@ -357,10 +330,7 @@ export default function ProductAllSection({
|
||||
});
|
||||
}, [productData, renderItems]);
|
||||
|
||||
const { revwGrd, orderPhnNo } = useMemo(
|
||||
() => extractProductMeta(productInfo),
|
||||
[productInfo]
|
||||
);
|
||||
const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]);
|
||||
|
||||
// FP: derive favorite flag from props with local override, avoid non-I/O useEffect
|
||||
const [favoriteOverride, setFavoriteOverride] = useState(null);
|
||||
@@ -383,7 +353,7 @@ export default function ProductAllSection({
|
||||
|
||||
// User Reviews 스크롤 핸들러 추가
|
||||
const handleUserReviewsClick = useCallback(
|
||||
() => scrollToSection("scroll-marker-user-reviews"),
|
||||
() => scrollToSection('scroll-marker-user-reviews'),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -400,7 +370,7 @@ export default function ProductAllSection({
|
||||
// 동영상이 있으면 첫 번째에 추가 (Indicator.jsx와 동일한 로직)
|
||||
if (productData && productData.prdtMediaUrl) {
|
||||
items.push({
|
||||
type: "video",
|
||||
type: 'video',
|
||||
url: productData.prdtMediaUrl,
|
||||
thumbnail: productData.thumbnailUrl960 || indicatorDefaultImage,
|
||||
index: 0,
|
||||
@@ -408,17 +378,12 @@ export default function ProductAllSection({
|
||||
}
|
||||
|
||||
// 이미지들 추가
|
||||
if (
|
||||
productData &&
|
||||
productData.imgUrls600 &&
|
||||
productData.imgUrls600.length > 0
|
||||
) {
|
||||
if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) {
|
||||
productData.imgUrls600.forEach((image, imgIndex) => {
|
||||
items.push({
|
||||
type: "image",
|
||||
type: 'image',
|
||||
url: image,
|
||||
index:
|
||||
productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex,
|
||||
index: productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -437,7 +402,7 @@ export default function ProductAllSection({
|
||||
// FP: Pure function for focus navigation to back button
|
||||
const handleSpotlightUpToBackButton = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
Spotlight.focus("spotlightId_backBtn");
|
||||
Spotlight.focus('spotlightId_backBtn');
|
||||
}, []);
|
||||
|
||||
// FP: Pure function for favorite flag change
|
||||
@@ -450,7 +415,7 @@ export default function ProductAllSection({
|
||||
const handleThemeItemButtonClick = useCallback(
|
||||
pipe(
|
||||
() => setOpenThemeItemOverlay(true),
|
||||
tap(() => setTimeout(() => Spotlight.focus("theme-close-button"), 0))
|
||||
tap(() => setTimeout(() => Spotlight.focus('theme-close-button'), 0))
|
||||
),
|
||||
[setOpenThemeItemOverlay]
|
||||
);
|
||||
@@ -471,12 +436,12 @@ export default function ProductAllSection({
|
||||
|
||||
// FP: Curried scroll handlers
|
||||
const handleProductDetailsClick = useCallback(
|
||||
() => scrollToSection("scroll-marker-product-details"),
|
||||
() => scrollToSection('scroll-marker-product-details'),
|
||||
[scrollToSection]
|
||||
);
|
||||
|
||||
const handleYouMayAlsoLikeClick = useCallback(
|
||||
() => scrollToSection("scroll-marker-you-may-also-like"),
|
||||
() => scrollToSection('scroll-marker-you-may-also-like'),
|
||||
[scrollToSection]
|
||||
);
|
||||
const scrollPositionRef = useRef(0);
|
||||
@@ -546,12 +511,7 @@ export default function ProductAllSection({
|
||||
(descriptionRef.current?.scrollHeight || 0) +
|
||||
(reviewRef.current?.scrollHeight || 0)
|
||||
);
|
||||
}, [
|
||||
productDetailRef.current,
|
||||
descriptionRef.current,
|
||||
hasReviews,
|
||||
hasYouMayAlsoLike,
|
||||
]);
|
||||
}, [productDetailRef.current, descriptionRef.current, hasReviews, hasYouMayAlsoLike]);
|
||||
|
||||
//spot관련
|
||||
useEffect(() => {
|
||||
@@ -575,10 +535,10 @@ export default function ProductAllSection({
|
||||
<div className={css.leftInfoWrapper}>
|
||||
<div className={css.headerContent}>
|
||||
<ProductTag productInfo={productData} />
|
||||
{revwGrd && revwGrd !== "0.0" && (
|
||||
{revwGrd && revwGrd !== '0.0' && (
|
||||
<StarRating
|
||||
rating={revwGrd}
|
||||
aria-label={"star rating " + revwGrd + " out of 5"}
|
||||
aria-label={'star rating ' + revwGrd + ' out of 5'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -593,17 +553,13 @@ export default function ProductAllSection({
|
||||
>
|
||||
<div className={css.qrWrapper}>
|
||||
{isShowQRCode ? (
|
||||
<QRCode
|
||||
productInfo={productData}
|
||||
productType={productType}
|
||||
kind={"detail"}
|
||||
/>
|
||||
<QRCode productInfo={productData} productType={productType} kind={'detail'} />
|
||||
) : (
|
||||
<div className={css.qrRollingWrap}>
|
||||
<div className={css.innerText}>
|
||||
<h3>{$L("Scan QR")}</h3>
|
||||
<p>{$L("with your phone, Check Product")}</p>
|
||||
<p>{$L("info & Purchase easily")}</p>
|
||||
<h3>{$L('Scan QR')}</h3>
|
||||
<p>{$L('with your phone, Check Product')}</p>
|
||||
<p>{$L('info & Purchase easily')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -619,7 +575,7 @@ export default function ProductAllSection({
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.buyNowText}>{$L("BUY NOW")}</div>
|
||||
<div className={css.buyNowText}>{$L('BUY NOW')}</div>
|
||||
</TButton>
|
||||
<TButton
|
||||
spotlightId="detail-add-to-cart-button"
|
||||
@@ -628,7 +584,7 @@ export default function ProductAllSection({
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.addToCartText}>{$L("ADD TO CART")}</div>
|
||||
<div className={css.addToCartText}>{$L('ADD TO CART')}</div>
|
||||
</TButton>
|
||||
</HorizontalContainer>
|
||||
)}
|
||||
@@ -645,9 +601,7 @@ export default function ProductAllSection({
|
||||
onClick={handleShopByMobileOpen}
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
>
|
||||
<div className={css.shopByMobileText}>
|
||||
{$L("SHOP BY MOBILE")}
|
||||
</div>
|
||||
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
|
||||
</TButton>
|
||||
{panelInfo && (
|
||||
<div className={css.favoriteBtnWrapper}>
|
||||
@@ -657,7 +611,7 @@ export default function ProductAllSection({
|
||||
selectedPrdtId={panelInfo && panelInfo.prdtId}
|
||||
favoriteFlag={favoriteFlag}
|
||||
onFavoriteFlagChanged={onFavoriteFlagChanged}
|
||||
kind={"item_detail"}
|
||||
kind={'item_detail'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -666,9 +620,7 @@ export default function ProductAllSection({
|
||||
<div className={css.callToOrderSection}>
|
||||
{orderPhnNo && (
|
||||
<>
|
||||
<div className={css.callToOrderText}>
|
||||
{$L("Call to Order")}
|
||||
</div>
|
||||
<div className={css.callToOrderText}>{$L('Call to Order')}</div>
|
||||
<div className={css.phoneSection}>
|
||||
<div className={css.phoneIconContainer}>
|
||||
<div className={css.phoneIcon} />
|
||||
@@ -686,34 +638,31 @@ export default function ProductAllSection({
|
||||
<TButton
|
||||
className={classNames(
|
||||
css.productDetailsButton,
|
||||
activeProductBtn ? css.active : ""
|
||||
activeProductBtn ? css.active : ''
|
||||
)}
|
||||
onClick={handleProductDetailsClick}
|
||||
spotlightId="product-details-button"
|
||||
>
|
||||
{$L("PRODUCT DETAILS")}
|
||||
{$L('PRODUCT DETAILS')}
|
||||
</TButton>
|
||||
{hasReviews && (
|
||||
<TButton
|
||||
className={classNames(
|
||||
css.userReviewsButton,
|
||||
activeReviewBtn ? css.active : ""
|
||||
)}
|
||||
className={classNames(css.userReviewsButton, activeReviewBtn ? css.active : '')}
|
||||
onClick={handleUserReviewsClick}
|
||||
spotlightId="user-reviews-button"
|
||||
>
|
||||
{$L("USER REVIEWS")} ({reviewTotalCount})
|
||||
{$L('USER REVIEWS')} ({reviewTotalCount})
|
||||
</TButton>
|
||||
)}
|
||||
{hasYouMayAlsoLike && (
|
||||
<TButton
|
||||
className={classNames(
|
||||
css.youMayLikeButton,
|
||||
activeYouMayLikeBtn ? css.active : ""
|
||||
activeYouMayLikeBtn ? css.active : ''
|
||||
)}
|
||||
onClick={handleYouMayAlsoLikeClick}
|
||||
>
|
||||
{$L("YOU MAY ALSO LIKE")}
|
||||
{$L('YOU MAY ALSO LIKE')}
|
||||
</TButton>
|
||||
)}
|
||||
{/* YouMayLike 버튼 렌더링 상태 로그 */}
|
||||
@@ -727,18 +676,15 @@ export default function ProductAllSection({
|
||||
})()} */}
|
||||
</Container>
|
||||
|
||||
{panelInfo &&
|
||||
panelInfo &&
|
||||
panelInfo.type === "theme" &&
|
||||
!openThemeItemOverlay && (
|
||||
<TButton
|
||||
className={css.themeButton}
|
||||
onClick={handleThemeItemButtonClick}
|
||||
spotlightId="theme-open-button"
|
||||
>
|
||||
{$L("THEME ITEM")}
|
||||
</TButton>
|
||||
)}
|
||||
{panelInfo && panelInfo && panelInfo.type === 'theme' && !openThemeItemOverlay && (
|
||||
<TButton
|
||||
className={css.themeButton}
|
||||
onClick={handleThemeItemButtonClick}
|
||||
spotlightId="theme-open-button"
|
||||
>
|
||||
{$L('THEME ITEM')}
|
||||
</TButton>
|
||||
)}
|
||||
|
||||
<DetailMobileSendPopUp
|
||||
ismobileSendPopupOpen={mobileSendPopupOpen}
|
||||
@@ -770,10 +716,7 @@ export default function ProductAllSection({
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className={css.productDetail}>
|
||||
<div
|
||||
id="scroll-marker-product-details"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<div id="scroll-marker-product-details" className={css.scrollMarker}></div>
|
||||
{/* <LayoutSample onClick={handleLayoutSampleClick} /> */}
|
||||
<div
|
||||
id="product-details-section"
|
||||
@@ -783,7 +726,7 @@ export default function ProductAllSection({
|
||||
>
|
||||
{renderItems.length > 0 ? (
|
||||
renderItems.map((item, index) =>
|
||||
item.type === "video" ? (
|
||||
item.type === 'video' ? (
|
||||
<ProductVideo
|
||||
key="product-video-0"
|
||||
productInfo={productData}
|
||||
@@ -817,10 +760,7 @@ export default function ProductAllSection({
|
||||
{/* 리뷰가 있을 때만 UserReviews 섹션 표시 */}
|
||||
{hasReviews && (
|
||||
<>
|
||||
<div
|
||||
id="scroll-marker-user-reviews"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<div id="scroll-marker-user-reviews" className={css.scrollMarker}></div>
|
||||
<div
|
||||
id="user-reviews-section"
|
||||
ref={reviewRef}
|
||||
@@ -844,10 +784,7 @@ export default function ProductAllSection({
|
||||
</div>
|
||||
{hasYouMayAlsoLike && (
|
||||
<div ref={youMayAlsoLikelRef}>
|
||||
<div
|
||||
id="scroll-marker-you-may-also-like"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div>
|
||||
<div id="you-may-also-like-section">
|
||||
{/* {(() => {
|
||||
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
|
||||
@@ -885,5 +822,5 @@ export default function ProductAllSection({
|
||||
}
|
||||
|
||||
ProductAllSection.propTypes = {
|
||||
productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired,
|
||||
productType: PropTypes.oneOf(['buyNow', 'shopByMobile', 'theme']).isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import { AUTO_SCROLL_GAP } from '../../../../utils/Config';
|
||||
import { scaleW } from '../../../../utils/helperMethods';
|
||||
import css from './AutoScrollAreaDetail.module.less';
|
||||
|
||||
const AutoScrollComponent = Spottable('div');
|
||||
|
||||
const POSITION = {
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
top: 'top',
|
||||
bottom: 'bottom',
|
||||
};
|
||||
|
||||
/**
|
||||
* DetailPanel 전용 AutoScrollArea
|
||||
* cursorVisible Redux 구독을 제거하여 TScrollerDetail 재렌더링 방지
|
||||
*/
|
||||
export default function AutoScrollAreaDetail({
|
||||
position,
|
||||
autoScroll,
|
||||
scrollHorizontalPos,
|
||||
scrollVerticalPos,
|
||||
scrollToRef,
|
||||
scrollPosition,
|
||||
direction,
|
||||
}) {
|
||||
const requestIdRef = useRef();
|
||||
// cursorVisible Redux 구독 제거 - TScrollerDetail 재렌더링 방지
|
||||
|
||||
const handleFocusAutoScroll = useCallback(() => {
|
||||
if (!autoScroll) return;
|
||||
|
||||
const scrollStep =
|
||||
position === 'right' || position === 'bottom'
|
||||
? scaleW(AUTO_SCROLL_GAP)
|
||||
: scaleW(-AUTO_SCROLL_GAP);
|
||||
|
||||
let start = null;
|
||||
|
||||
const animationScroll = (timestamp) => {
|
||||
if (!start) start = timestamp;
|
||||
const progress = timestamp - start;
|
||||
const step = Math.min(progress / 1000, 1);
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
scrollHorizontalPos.current += scrollStep * step;
|
||||
scrollToRef.current({
|
||||
position: { x: scrollHorizontalPos.current },
|
||||
animate: false,
|
||||
});
|
||||
} else if (direction === 'vertical') {
|
||||
scrollVerticalPos.current += scrollStep * step;
|
||||
scrollToRef.current({
|
||||
position: { y: scrollVerticalPos.current },
|
||||
animate: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
direction === 'horizontal' &&
|
||||
((position === 'right' && scrollPosition.current === 'right') ||
|
||||
(position === 'left' && scrollPosition.current === 'left'))
|
||||
) {
|
||||
if (typeof window === 'object') {
|
||||
window.cancelAnimationFrame(requestIdRef.current);
|
||||
}
|
||||
} else if (
|
||||
direction === 'vertical' &&
|
||||
((position === 'bottom' && scrollPosition.current === 'bottom') ||
|
||||
(position === 'top' && scrollPosition.current === 'top'))
|
||||
) {
|
||||
if (typeof window === 'object') {
|
||||
window.cancelAnimationFrame(requestIdRef.current);
|
||||
}
|
||||
} else {
|
||||
if (typeof window === 'object') {
|
||||
requestIdRef.current = window.requestAnimationFrame(animationScroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window === 'object') {
|
||||
requestIdRef.current = window.requestAnimationFrame(animationScroll);
|
||||
}
|
||||
}, [
|
||||
autoScroll,
|
||||
position,
|
||||
scrollHorizontalPos,
|
||||
scrollVerticalPos,
|
||||
scrollToRef,
|
||||
scrollPosition,
|
||||
direction,
|
||||
]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
if (requestIdRef.current && typeof window === 'object') {
|
||||
window.cancelAnimationFrame(requestIdRef.current);
|
||||
}
|
||||
}, [autoScroll, direction]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (requestIdRef.current && typeof window === 'object') {
|
||||
window.cancelAnimationFrame(requestIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AutoScrollComponent
|
||||
className={classNames(css.autoScrollArea, position && css[position])}
|
||||
spotlightDisabled={false}
|
||||
disabled={false}
|
||||
onFocus={handleFocusAutoScroll}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { POSITION };
|
||||
@@ -0,0 +1,36 @@
|
||||
.autoScrollArea {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
background-color: transparent; /* production */
|
||||
// background-color: #ff00001f; /* develop */
|
||||
|
||||
&.left {
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.right {
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.top {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,236 +1,225 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState, useMemo, forwardRef } from 'react';
|
||||
|
||||
import classNames from "classnames";
|
||||
import { useSelector } from "react-redux";
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { off, on } from "@enact/core/dispatcher";
|
||||
import { Job } from "@enact/core/util";
|
||||
import Scroller from "@enact/sandstone/Scroller";
|
||||
import { off, on } from '@enact/core/dispatcher';
|
||||
import { Job } from '@enact/core/util';
|
||||
import Scroller from '@enact/sandstone/Scroller';
|
||||
|
||||
import AutoScrollArea, { POSITION } from "../../../../components/AutoScrollArea/AutoScrollArea";
|
||||
import css from "./TScrollerDetail.module.less";
|
||||
import AutoScrollAreaDetail, { POSITION } from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
|
||||
import css from './TScrollerDetail.module.less';
|
||||
|
||||
/**
|
||||
* DetailPanel 전용 TScroller - 커스텀 스크롤바 구현
|
||||
* onScroll* event can't use Callback dependency
|
||||
*/
|
||||
const TScrollerDetail = forwardRef(({
|
||||
className,
|
||||
children,
|
||||
verticalScrollbar = "hidden",
|
||||
focusableScrollbar = false,
|
||||
direction = "vertical",
|
||||
horizontalScrollbar = "hidden",
|
||||
scrollMode,
|
||||
onScrollStart,
|
||||
onScrollStop,
|
||||
onScroll,
|
||||
noScrollByWheel = false,
|
||||
cbScrollTo,
|
||||
autoScroll = direction === "horizontal",
|
||||
setScrollVerticalPos,
|
||||
setCheckScrollPosition,
|
||||
...rest
|
||||
}, ref) => {
|
||||
const { cursorVisible } = useSelector((state) => state.common.appStatus);
|
||||
|
||||
const isScrolling = useRef(false);
|
||||
const scrollPosition = useRef("top");
|
||||
|
||||
const scrollToRef = useRef(null);
|
||||
const scrollHorizontalPos = useRef(0);
|
||||
const scrollVerticalPos = useRef(0);
|
||||
const actualScrollerElement = useRef(null); // 실제 스크롤 DOM 요소
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// ref를 내부 Scroller 요소에 연결
|
||||
useEffect(() => {
|
||||
if (ref && isMounted) {
|
||||
// DOM에서 Scroller 요소 찾기
|
||||
let scrollerElement = document.querySelector(`.${css.tScroller}`);
|
||||
|
||||
if (!scrollerElement) {
|
||||
// 다른 방법으로 찾기
|
||||
scrollerElement = document.querySelector('[data-spotlight-container="true"]');
|
||||
}
|
||||
|
||||
if (!scrollerElement) {
|
||||
// 스크롤 가능한 요소 찾기
|
||||
scrollerElement = document.querySelector('[style*="overflow"]');
|
||||
}
|
||||
|
||||
if (scrollerElement) {
|
||||
// ref가 함수인 경우와 객체인 경우를 모두 처리
|
||||
if (typeof ref === 'function') {
|
||||
ref(scrollerElement);
|
||||
} else if (ref && ref.current !== undefined) {
|
||||
ref.current = scrollerElement;
|
||||
}
|
||||
actualScrollerElement.current = scrollerElement; // 실제 스크롤 요소 저장
|
||||
}
|
||||
}
|
||||
}, [ref, isMounted]);
|
||||
|
||||
// 스크롤 제어 메서드 추가
|
||||
const scrollToElement = useCallback((element) => {
|
||||
if (actualScrollerElement.current && element) {
|
||||
const scrollerRect = actualScrollerElement.current.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const relativeTop = elementRect.top - scrollerRect.top;
|
||||
const scrollTop = actualScrollerElement.current.scrollTop + relativeTop - 20;
|
||||
|
||||
actualScrollerElement.current.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
|
||||
return () => setIsMounted(false);
|
||||
}, []);
|
||||
|
||||
const _onScrollStart = useCallback(
|
||||
(e) => {
|
||||
if (onScrollStart) {
|
||||
onScrollStart(e);
|
||||
}
|
||||
|
||||
isScrolling.current = true;
|
||||
const TScrollerDetail = forwardRef(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
verticalScrollbar = 'hidden',
|
||||
focusableScrollbar = false,
|
||||
direction = 'vertical',
|
||||
horizontalScrollbar = 'hidden',
|
||||
scrollMode,
|
||||
onScrollStart,
|
||||
onScrollStop,
|
||||
onScroll,
|
||||
noScrollByWheel = false,
|
||||
cbScrollTo,
|
||||
autoScroll = direction === 'horizontal',
|
||||
setScrollVerticalPos,
|
||||
setCheckScrollPosition,
|
||||
...rest
|
||||
},
|
||||
[onScrollStart]
|
||||
);
|
||||
ref
|
||||
) => {
|
||||
// cursorVisible을 Redux에서 구독하지 않음 - children 변경 방지
|
||||
|
||||
const _onScrollStop = useCallback(
|
||||
(e) => {
|
||||
if (onScrollStop) {
|
||||
onScrollStop(e);
|
||||
const isScrolling = useRef(false);
|
||||
const scrollPosition = useRef('top');
|
||||
|
||||
const scrollToRef = useRef(null);
|
||||
const scrollHorizontalPos = useRef(0);
|
||||
const scrollVerticalPos = useRef(0);
|
||||
const actualScrollerElement = useRef(null); // 실제 스크롤 DOM 요소
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// ref를 내부 Scroller 요소에 연결
|
||||
useEffect(() => {
|
||||
if (ref && isMounted) {
|
||||
// DOM에서 Scroller 요소 찾기
|
||||
let scrollerElement = document.querySelector(`.${css.tScroller}`);
|
||||
|
||||
if (!scrollerElement) {
|
||||
// 다른 방법으로 찾기
|
||||
scrollerElement = document.querySelector('[data-spotlight-container="true"]');
|
||||
}
|
||||
|
||||
if (!scrollerElement) {
|
||||
// 스크롤 가능한 요소 찾기
|
||||
scrollerElement = document.querySelector('[style*="overflow"]');
|
||||
}
|
||||
|
||||
if (scrollerElement) {
|
||||
// ref가 함수인 경우와 객체인 경우를 모두 처리
|
||||
if (typeof ref === 'function') {
|
||||
ref(scrollerElement);
|
||||
} else if (ref && ref.current !== undefined) {
|
||||
ref.current = scrollerElement;
|
||||
}
|
||||
actualScrollerElement.current = scrollerElement; // 실제 스크롤 요소 저장
|
||||
}
|
||||
}
|
||||
}, [ref, isMounted]);
|
||||
|
||||
isScrolling.current = false;
|
||||
// 스크롤 제어 메서드 추가
|
||||
const scrollToElement = useCallback((element) => {
|
||||
if (actualScrollerElement.current && element) {
|
||||
const scrollerRect = actualScrollerElement.current.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const relativeTop = elementRect.top - scrollerRect.top;
|
||||
const scrollTop = actualScrollerElement.current.scrollTop + relativeTop - 20;
|
||||
|
||||
if (e.reachedEdgeInfo) {
|
||||
if (e.reachedEdgeInfo.top) {
|
||||
scrollPosition.current = "top";
|
||||
} else if (e.reachedEdgeInfo.bottom) {
|
||||
scrollPosition.current = "bottom";
|
||||
} else if (e.reachedEdgeInfo.left) {
|
||||
scrollPosition.current = "left";
|
||||
} else if (e.reachedEdgeInfo.right) {
|
||||
scrollPosition.current = "right";
|
||||
actualScrollerElement.current.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
|
||||
return () => setIsMounted(false);
|
||||
}, []);
|
||||
|
||||
const _onScrollStart = useCallback(
|
||||
(e) => {
|
||||
if (onScrollStart) {
|
||||
onScrollStart(e);
|
||||
}
|
||||
|
||||
isScrolling.current = true;
|
||||
},
|
||||
[onScrollStart]
|
||||
);
|
||||
|
||||
const _onScrollStop = useCallback(
|
||||
(e) => {
|
||||
if (onScrollStop) {
|
||||
onScrollStop(e);
|
||||
}
|
||||
|
||||
isScrolling.current = false;
|
||||
|
||||
if (e.reachedEdgeInfo) {
|
||||
if (e.reachedEdgeInfo.top) {
|
||||
scrollPosition.current = 'top';
|
||||
} else if (e.reachedEdgeInfo.bottom) {
|
||||
scrollPosition.current = 'bottom';
|
||||
} else if (e.reachedEdgeInfo.left) {
|
||||
scrollPosition.current = 'left';
|
||||
} else if (e.reachedEdgeInfo.right) {
|
||||
scrollPosition.current = 'right';
|
||||
} else {
|
||||
scrollPosition.current = 'middle';
|
||||
}
|
||||
} else {
|
||||
scrollPosition.current = "middle";
|
||||
scrollPosition.current = 'middle';
|
||||
}
|
||||
} else {
|
||||
scrollPosition.current = "middle";
|
||||
|
||||
scrollHorizontalPos.current = e.scrollLeft;
|
||||
scrollVerticalPos.current = e.scrollTop;
|
||||
|
||||
if (setScrollVerticalPos) {
|
||||
setScrollVerticalPos(scrollVerticalPos.current);
|
||||
}
|
||||
if (setCheckScrollPosition) {
|
||||
setCheckScrollPosition(scrollPosition.current);
|
||||
}
|
||||
},
|
||||
[onScrollStop]
|
||||
);
|
||||
|
||||
const _onScroll = useCallback(
|
||||
(ev) => {
|
||||
if (onScroll) {
|
||||
onScroll(ev);
|
||||
}
|
||||
},
|
||||
[onScroll]
|
||||
);
|
||||
|
||||
const _cbScrollTo = useCallback(
|
||||
(ref) => {
|
||||
if (cbScrollTo) {
|
||||
cbScrollTo(ref);
|
||||
}
|
||||
|
||||
scrollToRef.current = ref;
|
||||
},
|
||||
[cbScrollTo]
|
||||
);
|
||||
|
||||
const relevantPositions = useMemo(() => {
|
||||
switch (direction) {
|
||||
case 'horizontal':
|
||||
return ['left', 'right'];
|
||||
case 'vertical':
|
||||
return ['top', 'bottom'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [direction]);
|
||||
|
||||
scrollHorizontalPos.current = e.scrollLeft;
|
||||
scrollVerticalPos.current = e.scrollTop;
|
||||
|
||||
if (setScrollVerticalPos) {
|
||||
setScrollVerticalPos(scrollVerticalPos.current);
|
||||
}
|
||||
if (setCheckScrollPosition) {
|
||||
setCheckScrollPosition(scrollPosition.current);
|
||||
}
|
||||
},
|
||||
[onScrollStop]
|
||||
);
|
||||
|
||||
const _onScroll = useCallback(
|
||||
(ev) => {
|
||||
if (onScroll) {
|
||||
onScroll(ev);
|
||||
}
|
||||
},
|
||||
[onScroll]
|
||||
);
|
||||
|
||||
const _cbScrollTo = useCallback(
|
||||
(ref) => {
|
||||
if (cbScrollTo) {
|
||||
cbScrollTo(ref);
|
||||
}
|
||||
|
||||
scrollToRef.current = ref;
|
||||
},
|
||||
[cbScrollTo]
|
||||
);
|
||||
|
||||
const relevantPositions = useMemo(() => {
|
||||
switch (direction) {
|
||||
case "horizontal":
|
||||
return ["left", "right"];
|
||||
case "vertical":
|
||||
return ["top", "bottom"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [direction]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className ? className : null,
|
||||
css.scrollerContainer
|
||||
)}
|
||||
>
|
||||
<Scroller
|
||||
cbScrollTo={_cbScrollTo}
|
||||
onScrollStart={_onScrollStart}
|
||||
onScrollStop={_onScrollStop}
|
||||
onScroll={_onScroll}
|
||||
scrollMode={scrollMode || "translate"}
|
||||
focusableScrollbar={focusableScrollbar}
|
||||
className={classNames(
|
||||
isMounted && css.tScroller,
|
||||
noScrollByWheel && css.preventScroll
|
||||
)}
|
||||
direction={direction}
|
||||
horizontalScrollbar={horizontalScrollbar}
|
||||
verticalScrollbar={verticalScrollbar}
|
||||
overscrollEffectOn={{
|
||||
arrowKey: false,
|
||||
drag: false,
|
||||
pageKey: false,
|
||||
track: false,
|
||||
wheel: false,
|
||||
}}
|
||||
noScrollByWheel={noScrollByWheel}
|
||||
noScrollByDrag
|
||||
// rest props에서 ref만 제외하고 전달
|
||||
{...(rest.ref ? { ...rest, ref: undefined } : rest)}
|
||||
>
|
||||
{children}
|
||||
</Scroller>
|
||||
{cursorVisible &&
|
||||
autoScroll &&
|
||||
relevantPositions.map((pos) => (
|
||||
<AutoScrollArea
|
||||
key={pos}
|
||||
position={POSITION[pos]}
|
||||
autoScroll={autoScroll}
|
||||
scrollHorizontalPos={scrollHorizontalPos}
|
||||
scrollVerticalPos={scrollVerticalPos}
|
||||
scrollToRef={scrollToRef}
|
||||
scrollPosition={scrollPosition}
|
||||
direction={direction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className={classNames(className ? className : null, css.scrollerContainer)}>
|
||||
<Scroller
|
||||
cbScrollTo={_cbScrollTo}
|
||||
onScrollStart={_onScrollStart}
|
||||
onScrollStop={_onScrollStop}
|
||||
onScroll={_onScroll}
|
||||
scrollMode={scrollMode || 'translate'}
|
||||
focusableScrollbar={focusableScrollbar}
|
||||
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
|
||||
direction={direction}
|
||||
horizontalScrollbar={horizontalScrollbar}
|
||||
verticalScrollbar={verticalScrollbar}
|
||||
overscrollEffectOn={{
|
||||
arrowKey: false,
|
||||
drag: false,
|
||||
pageKey: false,
|
||||
track: false,
|
||||
wheel: false,
|
||||
}}
|
||||
noScrollByWheel={noScrollByWheel}
|
||||
noScrollByDrag
|
||||
// rest props에서 ref만 제외하고 전달
|
||||
{...(rest.ref ? { ...rest, ref: undefined } : rest)}
|
||||
>
|
||||
{children}
|
||||
</Scroller>
|
||||
{autoScroll &&
|
||||
relevantPositions.map((pos) => (
|
||||
<AutoScrollAreaDetail
|
||||
key={pos}
|
||||
position={POSITION[pos]}
|
||||
autoScroll={autoScroll}
|
||||
scrollHorizontalPos={scrollHorizontalPos}
|
||||
scrollVerticalPos={scrollVerticalPos}
|
||||
scrollToRef={scrollToRef}
|
||||
scrollPosition={scrollPosition}
|
||||
direction={direction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// TScrollerDetail에 메서드 노출
|
||||
TScrollerDetail.scrollToElement = (element) => {
|
||||
@@ -240,5 +229,22 @@ TScrollerDetail.scrollToElement = (element) => {
|
||||
// displayName을 명확하게 설정
|
||||
TScrollerDetail.displayName = 'TScrollerDetail';
|
||||
|
||||
// React.memo로 최적화 - props가 동일하면 재렌더링 방지
|
||||
const MemoizedTScrollerDetail = React.memo(TScrollerDetail, (prevProps, nextProps) => {
|
||||
// children 비교는 얕은 비교로 충분 (React.Children.count 사용)
|
||||
const childrenEqual =
|
||||
React.Children.count(prevProps.children) === React.Children.count(nextProps.children);
|
||||
|
||||
// 주요 props 비교
|
||||
return (
|
||||
childrenEqual &&
|
||||
prevProps.className === nextProps.className &&
|
||||
prevProps.verticalScrollbar === nextProps.verticalScrollbar &&
|
||||
prevProps.direction === nextProps.direction &&
|
||||
prevProps.cbScrollTo === nextProps.cbScrollTo &&
|
||||
prevProps.onScroll === nextProps.onScroll
|
||||
);
|
||||
});
|
||||
|
||||
// forwardRef를 사용하는 컴포넌트임을 명시
|
||||
export default TScrollerDetail;
|
||||
export default MemoizedTScrollerDetail;
|
||||
|
||||
Reference in New Issue
Block a user