[250929] feat: views - ProductAllSection.jsx, ProductVideo.jsx, Produc...
🕐 커밋 시간: 2025. 09. 29. 10:12:10 📊 변경 통계: • 총 파일: 3개 • 추가: +58줄 • 삭제: -13줄 📁 추가된 파일: + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: LayoutSample() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less (unknown): ✅ Added: translate(), shadow()
This commit is contained in:
@@ -51,6 +51,8 @@ import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.
|
||||
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
|
||||
import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
|
||||
import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
|
||||
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo';
|
||||
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import QRCode from '../ProductInfoSection/QRCode/QRCode';
|
||||
import ProductOverview from '../ProductOverview/ProductOverview';
|
||||
// CSS imports
|
||||
@@ -266,7 +268,7 @@ export default function ProductAllSection({
|
||||
// TODO: 장바구니 추가 로직 구현
|
||||
}, []);
|
||||
|
||||
// 디버깅: 실제 이미지 데이터 확인
|
||||
// 디버깅: 실제 이미지 및 동영상 데이터 확인
|
||||
useEffect(() => {
|
||||
console.log('[ProductId] ProductAllSection productData check:', {
|
||||
hasProductData: !!productData,
|
||||
@@ -276,9 +278,15 @@ export default function ProductAllSection({
|
||||
imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
|
||||
? 'array'
|
||||
: typeof (productData && productData.imgUrls600),
|
||||
// 동영상 관련 정보 추가
|
||||
prdtMediaUrl: productData && productData.prdtMediaUrl,
|
||||
thumbnailUrl960: productData && productData.thumbnailUrl960,
|
||||
hasVideo: !!(productData && productData.prdtMediaUrl),
|
||||
renderItemsLength: renderItems.length,
|
||||
renderItems: renderItems,
|
||||
productData: productData,
|
||||
});
|
||||
}, [productData]);
|
||||
}, [productData, renderItems]);
|
||||
|
||||
const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]);
|
||||
|
||||
@@ -313,6 +321,34 @@ export default function ProductAllSection({
|
||||
const reviewRef = useRef(null);
|
||||
const youMayAlsoLikelRef = useRef(null);
|
||||
|
||||
// 동영상과 이미지를 통합한 렌더링 아이템 리스트 생성 (Indicator.jsx 로직 기반)
|
||||
const renderItems = useMemo(() => {
|
||||
const items = [];
|
||||
|
||||
// 동영상이 있으면 첫 번째에 추가 (Indicator.jsx와 동일한 로직)
|
||||
if (productData && productData.prdtMediaUrl) {
|
||||
items.push({
|
||||
type: 'video',
|
||||
url: productData.prdtMediaUrl,
|
||||
thumbnail: productData.thumbnailUrl960 || indicatorDefaultImage,
|
||||
index: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 이미지들 추가
|
||||
if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) {
|
||||
productData.imgUrls600.forEach((image, imgIndex) => {
|
||||
items.push({
|
||||
type: 'image',
|
||||
url: image,
|
||||
index: productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [productData]);
|
||||
|
||||
const { getScrollTo, scrollTop } = useScrollTo();
|
||||
|
||||
// FP: Pure function for mobile popup state change
|
||||
@@ -621,18 +657,27 @@ export default function ProductAllSection({
|
||||
onFocus={productFocus}
|
||||
onBlur={_onBlur}
|
||||
>
|
||||
{productData && productData.imgUrls600 && productData.imgUrls600.length > 0 ? (
|
||||
productData.imgUrls600.map((image, index) => (
|
||||
<ProductDetail
|
||||
key={`product-detail-${index}`}
|
||||
productInfo={{
|
||||
...productData,
|
||||
singleImage: image,
|
||||
imageIndex: index,
|
||||
totalImages: productData.imgUrls600.length,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
{renderItems.length > 0 ? (
|
||||
renderItems.map((item, index) =>
|
||||
item.type === 'video' ? (
|
||||
<ProductVideo
|
||||
key="product-video-0"
|
||||
productInfo={productData}
|
||||
videoUrl={item.url}
|
||||
thumbnailUrl={item.thumbnail}
|
||||
/>
|
||||
) : (
|
||||
<ProductDetail
|
||||
key={`product-detail-${index}`}
|
||||
productInfo={{
|
||||
...productData,
|
||||
singleImage: item.url,
|
||||
imageIndex: item.index,
|
||||
totalImages: renderItems.length,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<ProductDetail productInfo={productData} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { startVideoPlayer } from '../../../../actions/playActions';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||
import css from './ProductVideo.module.less';
|
||||
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Indicator.jsx의 canPlayVideo 로직 이식
|
||||
const canPlayVideo = useMemo(() => {
|
||||
return Boolean(productInfo?.prdtMediaUrl && videoUrl);
|
||||
}, [productInfo, videoUrl]);
|
||||
|
||||
// Indicator.jsx의 handleVideoOnClick 로직 완전히 이식
|
||||
const handleVideoClick = useCallback(() => {
|
||||
if (canPlayVideo) {
|
||||
dispatch(
|
||||
startVideoPlayer({
|
||||
qrCurrentItem: productInfo,
|
||||
showUrl: productInfo?.prdtMediaUrl,
|
||||
showNm: productInfo?.prdtNm,
|
||||
patnrNm: productInfo?.patncNm,
|
||||
patncLogoPath: productInfo?.patncLogoPath,
|
||||
orderPhnNo: productInfo?.orderPhnNo,
|
||||
disclaimer: productInfo?.disclaimer,
|
||||
subtitle: productInfo?.prdtMediaSubtitlUrl,
|
||||
lgCatCd: productInfo?.catCd,
|
||||
patnrId: productInfo?.patnrId,
|
||||
lgCatNm: productInfo?.catNm,
|
||||
prdtId: productInfo?.prdtId,
|
||||
patncNm: productInfo?.patncNm,
|
||||
prdtNm: productInfo?.prdtNm,
|
||||
thumbnailUrl: productInfo?.thumbnailUrl960,
|
||||
shptmBanrTpNm: 'MEDIA',
|
||||
// Indicator.jsx와 동일하게: 클릭 시 전체화면 모드
|
||||
modal: false,
|
||||
modalContainerId: 'product-video-player', // 우리 컴포넌트의 spotlightId
|
||||
modalClassName: css.videoModal,
|
||||
spotlightDisable: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, productInfo, canPlayVideo]);
|
||||
|
||||
if (!canPlayVideo) return null;
|
||||
|
||||
return (
|
||||
<SpottableComponent
|
||||
className={css.videoContainer}
|
||||
onClick={handleVideoClick}
|
||||
spotlightId="product-video-player"
|
||||
aria-label={`${productInfo?.prdtNm} 동영상 재생`}
|
||||
>
|
||||
<div className={css.videoThumbnailWrapper}>
|
||||
<CustomImage
|
||||
src={thumbnailUrl}
|
||||
alt={`${productInfo?.prdtNm} 동영상 썸네일`}
|
||||
className={css.videoThumbnail}
|
||||
/>
|
||||
<div className={css.playButtonOverlay}>
|
||||
<img src={playImg} alt="재생" />
|
||||
</div>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.videoContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.videoThumbnailWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.videoThumbnail {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.playButtonOverlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.playButtonOverlay {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// Spotlight 포커스 스타일
|
||||
&:focus {
|
||||
outline: 2px solid #0078d4;
|
||||
outline-offset: 2px;
|
||||
|
||||
.playButtonOverlay {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// webOS TV 환경에서의 포커스 스타일
|
||||
&.spottable:focus {
|
||||
outline: 3px solid #ffffff;
|
||||
outline-offset: 3px;
|
||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 동영상 모달 클래스 (PlayerPanel에서 사용)
|
||||
.videoModal {
|
||||
// PlayerPanel 모달에서 사용되는 스타일
|
||||
z-index: 1000;
|
||||
}
|
||||
Reference in New Issue
Block a user