[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:
djaco
2025-09-29 10:12:11 +09:00
parent d21984af12
commit 15c1176861
3 changed files with 193 additions and 14 deletions

View File

@@ -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} />
)}

View File

@@ -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>
);
}

View File

@@ -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;
}