[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:
@@ -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 }) => ({
|
||||
|
||||
@@ -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} /> */}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user