[251111] feat: views - MediaPlayer.jsx, ProductAllSection.jsx, Product...

🕐 커밋 시간: 2025. 11. 11. 14:07:12

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +211줄
  • 삭제: -37줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 대규모 기능 개발
This commit is contained in:
2025-11-11 14:07:14 +09:00
parent c5ce58fc43
commit 22550bdb39
3 changed files with 211 additions and 37 deletions

View File

@@ -65,7 +65,7 @@ const isEnter = is('enter');
const isLeft = is('left');
const isRight = is('right');
const jumpBackKeyCode = 37;
const jumpBackKeyCode = 3
const jumpForwardKeyCode = 39;
const controlsHandleAboveSelectionKeys = [13, 16777221, jumpBackKeyCode, jumpForwardKeyCode];
const getControlsHandleAboveHoldConfig = ({ frequency, time }) => ({

View File

@@ -71,7 +71,7 @@ import ProductDescription
from '../ProductContentSection/ProductDescription/ProductDescription';
import ProductDetail
from '../ProductContentSection/ProductDetail/ProductDetail.new';
import { ProductVideoV2 } from '../ProductContentSection/ProductVideo';
import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
@@ -405,6 +405,39 @@ export default function ProductAllSection({
dispatch(clearAllToasts())
},[dispatch])
// 스크롤 컨테이너의 클릭 이벤트 추적용 로깅
const handleScrollContainerClick = useCallback((e) => {
console.log('📱 [ProductAllSection] TScrollerDetail onClick 감지됨', {
eventType: e.type,
target: e.target?.className,
currentTarget: e.currentTarget?.className,
bubbles: e.bubbles,
defaultPrevented: e.defaultPrevented,
timestamp: new Date().getTime(),
eventPath: e.composedPath?.().slice(0, 5).map(el => ({
tag: el.tagName,
className: el.className,
id: el.id
}))
});
}, []);
// ContentContainer 레벨 클릭 이벤트 추적
const handleContentContainerClick = useCallback((e) => {
console.log('🎯 [ProductAllSection] ContentContainer onClick 감지됨', {
eventType: e.type,
target: e.target?.className,
currentTarget: e.currentTarget?.className,
bubbles: e.bubbles,
defaultPrevented: e.defaultPrevented,
eventPath: e.composedPath?.().slice(0, 8).map(el => ({
tag: el.tagName,
className: el.className,
id: el.id
}))
});
}, []);
// ADD TO CART 버튼 클릭 핸들러
const handleAddToCartClick = useCallback(() => {
// console.log('[AddToCart] Add To Cart button clicked');
@@ -966,6 +999,7 @@ export default function ProductAllSection({
<ContentContainer
className={css.rightContentContainer}
spotlightId="content-scroller-container"
onClick={handleContentContainerClick}
>
<div className={css.scrollerWrapper}>
<TScrollerDetail
@@ -978,6 +1012,7 @@ export default function ProductAllSection({
spotlightDisabled={false}
spotlightRestrict="none"
onScroll={handleScroll}
onClick={handleScrollContainerClick}
>
<div className={css.productDetail}>
{/* <LayoutSample onClick={handleLayoutSampleClick} /> */}

View File

@@ -49,7 +49,7 @@ const YOUTUBECONFIG = {
},
};
export default function ProductVideoV2({
export function ProductVideoV2({
productInfo,
videoUrl,
thumbnailUrl,
@@ -141,10 +141,14 @@ export default function ProductVideoV2({
}, [canPlayVideo, isPlaying]);
// 썸네일 클릭 핸들러 - 비디오 재생 시작 + Redux dispatch + MediaPlayer 메서드 호출
const handleThumbnailClick = useCallback(() => {
const handleThumbnailClick = useCallback((e) => {
console.log('🎬 [handleThumbnailClick] 썸네일 클릭됨', {
canPlayVideo,
isPlaying,
eventType: e?.type,
target: e?.target?.className,
bubbles: e?.bubbles,
timestamp: new Date().getTime(),
});
if (canPlayVideo && !isPlaying) {
// console.log('[BgVideo] ProductVideoV2 - Starting video playback');
@@ -276,6 +280,9 @@ export default function ProductVideoV2({
eventType: e.type,
target: e.target?.className,
currentTarget: e.currentTarget?.className,
bubbles: e.bubbles,
cancelable: e.cancelable,
composed: e.composed,
});
// 비디오 재생 중이고 전체화면이 아닐 때만 작동
@@ -300,24 +307,14 @@ export default function ProductVideoV2({
// 마우스 다운 (클릭) 이벤트 - capture phase에서 처리
const handleVideoPlayerMouseDown = useCallback(
(e) => {
// ⚠️ 이 함수는 사용되지 않으므로 제거 예정
// videoPlayerWrapper의 onMouseDownCapture에서 처리됨
console.log('🎬 [ProductVideoV2] handleVideoPlayerMouseDown 실행됨', {
isPlaying,
isFullscreen,
eventType: e.type,
target: e.target?.className,
});
// 비디오 재생 중이고 전체화면이 아닐 때만 작동
if (!isPlaying || isFullscreen) {
console.log('🎬 [ProductVideoV2] MouseDown 조건 불만족');
return;
}
// capture phase에서 이벤트 처리
e.preventDefault();
e.stopPropagation();
console.log('🎬 [ProductVideoV2] ✅ MouseDown detected at capture phase');
},
[isPlaying, isFullscreen]
);
@@ -349,6 +346,34 @@ export default function ProductVideoV2({
}
}, [isPlaying, isFullscreen, handleFullscreenKeyDown]);
// 전역 클릭 이벤트 감시 (debugging용 - 모든 클릭이 document에 도달하는지 확인)
useEffect(() => {
const handleDocumentClick = (e) => {
// ProductVideoV2 관련 요소인 경우만 로깅
const isVideoElement = e.target?.closest('[class*="videoContainer"]') ||
e.target?.closest('[class*="videoPlayer"]') ||
e.target?.closest('[class*="videoThumbnail"]');
if (isVideoElement) {
console.log('📄 [Document Level] 전역 클릭 감지됨', {
eventPhase: e.eventPhase,
bubbles: e.bubbles,
target: e.target?.className,
eventPath: e.composedPath?.().slice(0, 6).map(el => ({
tag: el.tagName,
className: el.className,
id: el.id
}))
});
}
};
document.addEventListener('click', handleDocumentClick, true);
return () => {
document.removeEventListener('click', handleDocumentClick, true);
};
}, []);
// autoPlay 기능: 컴포넌트 마운트 후 500ms 후 자동 재생
useEffect(() => {
if (autoPlay && canPlayVideo && !isPlaying) {
@@ -397,6 +422,101 @@ export default function ProductVideoV2({
};
}, [dispatch, isPlaying]);
// VideoContainer의 모든 클릭 감시 (Hooks는 early return 전에 정의되어야 함)
const handleVideoContainerClick = useCallback(
(e) => {
console.log('🎥 [ProductVideoV2] videoContainer onClick 감지됨', {
isPlaying,
isFullscreen,
eventType: e.type,
target: e.target?.className,
currentTarget: e.currentTarget?.className,
bubbles: e.bubbles,
defaultPrevented: e.defaultPrevented,
timestamp: new Date().getTime(),
});
if (isPlaying && !isFullscreen) {
console.log('🎥 [ProductVideoV2] videoContainer 클릭 → 직접 전체화면 토글 실행', {
direct: true,
});
e.preventDefault?.();
e.stopPropagation?.();
toggleFullscreen();
}
},
[isPlaying, isFullscreen, toggleFullscreen]
);
const containerProps = useMemo(() => {
const baseProps = {
onClick: handleVideoContainerClick, // 모든 상태에서 동일하게 적용
};
if (isFullscreen) {
return {
...baseProps,
spotlightRestrict: 'self-only', // 포커스가 밖으로 나가지 않도록
spotlightId: 'product-video-v2-fullscreen',
onSpotlightDown: handleSpotlightDown, // 전체화면에서도 Down 키 동작
};
} else if (isPlaying) {
return {
...baseProps,
spotlightId: 'product-video-v2-playing',
onKeyDown: handleContainerKeyDown, // 일반 모드: 컨테이너에서 직접 처리
onSpotlightDown: handleSpotlightDown, // 일반 재생에서도 Down 키 동작
};
} else {
return baseProps;
}
}, [isFullscreen, isPlaying, handleVideoContainerClick, handleSpotlightDown, handleContainerKeyDown]);
// ⚠️ 간단한 해결책: 비디오 영역 클릭 시 직접 동작 실행
const handleContainerClickFallback = useCallback((e) => {
const isThumbnailArea = e.target?.closest('[class*="videoThumbnail"]') ||
e.target?.closest('[class*="playButton"]');
const isVideoPlayerArea = e.target?.closest('[class*="videoPlayer"]') ||
e.target?.closest('[class*="VideoPlayer"]');
// 썸네일 클릭: 비디오 재생 시작
if (isThumbnailArea && !isPlaying) {
console.log('🎬 [handleContainerClickFallback] 썸네일 클릭 → 비디오 재생 시작');
handleThumbnailClick(e);
}
// 비디오 재생 중 클릭: 전체화면 토글
if (isVideoPlayerArea && isPlaying && !isFullscreen) {
console.log('🎬 [handleContainerClickFallback] 비디오 클릭 → 전체화면 토글');
toggleFullscreen();
}
}, [isPlaying, isFullscreen, handleThumbnailClick, toggleFullscreen]);
// ⚠️ 핵심: TScrollerDetail이 capture phase에서 이벤트를 막으므로,
// ProductVideoV2 컨테이너 전체에서 capture phase를 다시 열어줌 (Hooks는 early return 전에)
const handleContainerMouseDownCapture = useCallback((e) => {
// 비디오 영역인지 확인
const isVideoArea = e.currentTarget === containerRef.current;
console.log('🔓 [ProductVideoV2 Container] onMouseDownCapture - TScrollerDetail 차단 우회', {
isVideoArea,
target: e.target?.className,
isPlaying,
isFullscreen,
});
toggleFullscreen();
// TScrollerDetail의 onClick 실행을 막기 위해 preventDefault 호출
if (isPlaying && !isFullscreen) {
console.log('🔓 [ProductVideoV2] TScrollerDetail onClick 실행 차단');
e.preventDefault();
e.stopPropagation();
}
}, [isPlaying, isFullscreen]);
// Early return: 비디오 재생 불가능한 경우
if (!canPlayVideo) return null;
// 컨테이너 컴포넌트 결정
@@ -409,22 +529,6 @@ export default function ProductVideoV2({
? SpottableComponent
: 'div';
const containerProps = isFullscreen
? {
spotlightRestrict: 'self-only', // 포커스가 밖으로 나가지 않도록
spotlightId: 'product-video-v2-fullscreen',
onSpotlightDown: handleSpotlightDown, // 전체화면에서도 Down 키 동작
// 전체화면 모드: window 레벨에서 이벤트 처리
}
: isPlaying
? {
spotlightId: 'product-video-v2-playing',
onKeyDown: handleContainerKeyDown, // 일반 모드: 컨테이너에서 직접 처리
onSpotlightDown: handleSpotlightDown, // 일반 재생에서도 Down 키 동작
// 일반 재생 모드: 컨테이너가 포커스 받음
}
: {};
// VideoPlayer 컴포넌트 생성 함수
const renderVideoPlayer = () => {
if (!isPlaying) return null;
@@ -447,14 +551,26 @@ export default function ProductVideoV2({
}
}}
onMouseDownCapture={(e) => {
// ⚠️ 핵심: VideoPlayer 내부(실제 비디오 영역)에서만 이벤트를 살리고,
// 그 외 wrapper 영역에서만 preventDefault
const isVideoElement = e.target?.closest('[class*="videoPlayer"]') ||
e.target?.closest('video') ||
e.target?.closest('[class*="react-player"]');
console.log('🎬 [videoPlayerWrapper] onMouseDownCapture 실행됨', {
isFullscreen,
isPlaying,
isVideoElement,
target: e.target?.className,
});
if (!isFullscreen && isPlaying) {
// VideoPlayer가 아닌 wrapper 영역에서만 preventDefault
if (!isFullscreen && isPlaying && !isVideoElement) {
e.preventDefault();
e.stopPropagation();
console.log('🎬 [videoPlayerWrapper] MouseDown at wrapper - capturing');
console.log('🛑 [videoPlayerWrapper] preventDefault - wrapper 배경 영역');
} else if (!isFullscreen && isPlaying && isVideoElement) {
// 실제 비디오 영역이면 이벤트를 전파시켜 click이 정상 발생하도록
console.log('✅ [videoPlayerWrapper] 이벤트 전파 허용 - 비디오 요소');
}
}}
>
@@ -508,12 +624,20 @@ export default function ProductVideoV2({
{...containerProps}
ref={containerRef}
className={`${css.videoContainer} ${isFullscreen ? css.fullscreen : ''}`}
onMouseDownCapture={handleContainerMouseDownCapture}
onClick={handleContainerClickFallback}
>
{!isPlaying ? (
// 썸네일 + 재생 버튼 표시
<SpottableComponent
className={css.videoThumbnailContainer}
onClick={handleThumbnailClick}
onClick={(e) => {
console.log('🎬 [SpottableComponent] onClick 실행됨', {
eventType: e.type,
target: e.target?.className,
});
handleThumbnailClick(e);
}}
onFocus={videoContainerOnFocus}
onBlur={videoContainerOnBlur}
spotlightId="product-video-v2-thumbnail"
@@ -545,11 +669,26 @@ export default function ProductVideoV2({
handleVideoPlayerClick(e);
}}
onMouseDownCapture={(e) => {
// ⚠️ 핵심: VideoPlayer 영역 내에서만 이벤트를 살리고,
// 그 외 영역(스크롤 영역)에서만 preventDefault
const isVideoPlayerArea = e.target?.closest('[class*="videoPlayer"]') ||
e.target?.closest('[class*="VideoPlayer"]');
console.log('🎬 [normalContainerRef] onMouseDownCapture 실행됨', {
isPlaying,
isFullscreen,
isVideoPlayerArea,
target: e.target?.className,
});
handleVideoPlayerMouseDown(e);
// VideoPlayer가 아닌 영역(스크롤 영역)에서만 preventDefault
if (!isVideoPlayerArea) {
e.preventDefault();
console.log('🛑 [normalContainerRef] preventDefault - 스크롤 영역에서의 클릭');
} else {
// VideoPlayer 영역이면 이벤트를 전파시켜 click이 정상 발생하도록
console.log('✅ [normalContainerRef] 이벤트 전파 허용 - VideoPlayer 영역');
}
}}
>
{renderVideoPlayer()}