[251113] style: views - TReactPlayer.jsx, ProductVideo.module.less, Pro...

🕐 커밋 시간: 2025. 11. 13. 10:59:09

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +173줄
  • 삭제: -76줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 중간 규모 기능 개선
This commit is contained in:
2025-11-13 10:59:11 +09:00
parent b8cf24dc0e
commit 74619cba4e
5 changed files with 173 additions and 76 deletions

View File

@@ -29,14 +29,22 @@ export default function TReactPlayer({
const handleEvent = useCallback( const handleEvent = useCallback(
(type) => (ev) => { (type) => (ev) => {
if (type === "onReady") { if (type === "onReady") {
if (videoRef) { if (videoRef) {
const videoNode = playerRef.current.getInternalPlayer(); const videoNode = playerRef.current.getInternalPlayer();
videoRef(videoNode); videoRef(videoNode);
if ( const iframeEl =
videoNode.tagName && typeof playerRef.current?.getInternalPlayer === "function"
!Object.prototype.hasOwnProperty.call(videoNode, "proportionPlayed") ? playerRef.current.getInternalPlayer("iframe")
) { : null;
if (iframeEl) {
iframeEl.setAttribute("tabIndex", "-1");
iframeEl.setAttribute("aria-hidden", "true");
}
if (
videoNode.tagName &&
!Object.prototype.hasOwnProperty.call(videoNode, "proportionPlayed")
) {
Object.defineProperties(videoNode, { Object.defineProperties(videoNode, {
error: { error: {
get: function () { get: function () {

View File

@@ -98,16 +98,21 @@
} }
} }
.youtubeSafe {
:global(.react-player),
:global(.react-player iframe) {
pointer-events: none !important;
}
}
// 전체화면 모드 (ProductVideoV2 엔터키 토글) // 전체화면 모드 (ProductVideoV2 엔터키 토글)
.fullscreen { .fullscreen {
position: fixed !important; position: fixed !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
width: 1920px !important; width: 100vw !important;
height: 1080px !important; height: 100vh !important;
max-width: 1920px !important; z-index: 99999 !important; // 전체 화면 기준
z-index: 99999 !important; // 최상위 레이어
margin: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
background-color: @COLOR_BLACK; background-color: @COLOR_BLACK;
@@ -115,12 +120,39 @@
border-radius: 0; border-radius: 0;
} }
// 전체화면 모드에서 포커스가 밖으로 나가지 않도록 // 전체화면 모드에서 포커스가 벗어나지 않도록
// Spotlight container가 이 영역만 관리 // Spotlight container는 별도 관리
} }
.fullscreenPlayer { .fullscreenPlayer {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
border-radius: 0 !important; border-radius: 0 !important;
z-index: 100000 !important;
background-color: @COLOR_BLACK;
:global(.videoPlayer) {
width: 100% !important;
height: 100% !important;
background-color: @COLOR_BLACK;
}
:global(video) {
width: 100% !important;
height: 100% !important;
object-fit: cover;
background-color: @COLOR_BLACK;
}
:global(.react-player),
:global(.react-player iframe) {
width: 100% !important;
height: 100% !important;
background-color: @COLOR_BLACK;
}
} }
.videoThumbnailContainer { .videoThumbnailContainer {
@@ -158,8 +190,8 @@
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 1920px; width: 100vw;
height: 1080px; height: 100vh;
z-index: 99999; z-index: 99999;
background-color: @COLOR_BLACK; background-color: @COLOR_BLACK;

View File

@@ -548,7 +548,7 @@ export function ProductVideoV2({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// toggleOverlayVisibility(); toggleOverlayVisibility();
}, },
[isPlaying, isFullscreen, toggleOverlayVisibility] [isPlaying, isFullscreen, toggleOverlayVisibility]
); );
@@ -558,23 +558,29 @@ export function ProductVideoV2({
(e) => { (e) => {
if (!isPlaying || !isFullscreen) return; if (!isPlaying || !isFullscreen) return;
// ESC 키만 오버레이 토글로 처리 const isOverlayVisible = mediaOverlayState.controls?.visible;
if (e.key === 'Escape' || e.keyCode === 27) { if (e.key === 'Escape' || e.keyCode === 27) {
debugLog('🖥️ [Fullscreen Container] ESC 키 - 오버레이 토글 실행'); debugLog('[Fullscreen Container] ESC key - toggling overlay');
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
toggleOverlayVisibility(); toggleOverlayVisibility();
return; return;
} }
// Enter 키는 기본 동작 허용 (포커스된 요소의 동작 수행)
if (e.key === 'Enter' || e.keyCode === 13) { if (e.key === 'Enter' || e.keyCode === 13) {
debugLog('🖥️ [Fullscreen Container] Enter 키 - 포커스된 요소 동작 허용'); if (!isOverlayVisible) {
// Enter 키는 preventDefault하지 않고 기본 동작 허용 debugLog('[Fullscreen Container] Enter key - overlay hidden, showing controls');
return; e.preventDefault();
e.stopPropagation();
toggleOverlayVisibility();
return;
}
debugLog('[Fullscreen Container] Enter key - overlay visible, allow default behavior');
} }
}, },
[isPlaying, isFullscreen, toggleOverlayVisibility] [isPlaying, isFullscreen, toggleOverlayVisibility, mediaOverlayState.controls?.visible]
); );
// 마우스 다운 (클릭) 이벤트 - capture phase에서 처리 // 마우스 다운 (클릭) 이벤트 - capture phase에서 처리
@@ -764,6 +770,17 @@ export function ProductVideoV2({
} }
}, [isFullscreen, isPlaying]); }, [isFullscreen, isPlaying]);
useEffect(() => {
if (!isPlaying) return;
if (isFullscreen && isYoutube) {
videoPlayerRef.current?.showControls?.();
dispatch(setMediaControlShow());
dispatch(resetMediaAutoClose());
dispatch(startMediaAutoClose(5000));
}
}, [dispatch, isFullscreen, isPlaying, isYoutube]);
useEffect(() => { useEffect(() => {
const wasFullscreen = prevFullscreenRef.current; const wasFullscreen = prevFullscreenRef.current;
if (wasFullscreen && !isFullscreen && isPlaying) { if (wasFullscreen && !isFullscreen && isPlaying) {
@@ -924,11 +941,19 @@ export function ProductVideoV2({
const host = ensurePortalHost(); const host = ensurePortalHost();
if (!host) return null; if (!host) return null;
const shouldDisableIframeInteraction = isYoutube && isFullscreen;
const wrapperClasses = [
css.videoPlayerWrapper,
isFullscreen ? css.fullscreenPlayer : '',
shouldDisableIframeInteraction ? css.youtubeSafe : '',
]
.filter(Boolean)
.join(' ');
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
ref={videoPlayerWrapperRef} ref={videoPlayerWrapperRef}
className={`${css.videoPlayerWrapper} ${isFullscreen ? css.fullscreenPlayer : ''}`} className={wrapperClasses}
onMouseMove={handleUserActivity} onMouseMove={handleUserActivity}
onTouchMove={handleUserActivity} onTouchMove={handleUserActivity}
onWheel={handleUserActivity} onWheel={handleUserActivity}
@@ -1113,3 +1138,7 @@ export function ProductVideoV2({
</> </>
); );
} }

View File

@@ -100,6 +100,14 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
} }
}, [panelInfo?.isPaused, panelInfo?.modal]); }, [panelInfo?.isPaused, panelInfo?.modal]);
useEffect(() => {
if (!videoPlayer.current) return;
if (!isYoutube) return;
if (panelInfo?.modal) return;
videoPlayer.current.showControls?.();
}, [isYoutube, panelInfo?.modal]);
const getPlayer = useCallback((ref) => { const getPlayer = useCallback((ref) => {
videoPlayer.current = ref; videoPlayer.current = ref;
}, []); }, []);
@@ -463,6 +471,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// minimized 상태일 때는 spotlightRestrict 해제 (포커스 이동 허용) // minimized 상태일 때는 spotlightRestrict 해제 (포커스 이동 허용)
const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only'; const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only';
const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal;
return ( return (
<TPanel <TPanel
@@ -477,57 +486,64 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
spotlightId="spotlightId-media-video-container" spotlightId="spotlightId-media-video-container"
> >
{currentPlayingUrl && ( {currentPlayingUrl && (
<VideoPlayer <div
setApiProvider={getPlayer} className={classNames(
disabled={panelInfo.modal} css.videoPlayerWrapper,
onEnded={onEnded} shouldDisableIframeInteraction && css.youtubeSafe
noAutoPlay={false}
noAutoShowMediaControls={panelInfo.modal} // modal 상태에서는 자동으로 controls를 보여주지 않음
autoCloseTimeout={3000}
onBackButton={onClickBack}
onClick={onVideoClick}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}
src={currentPlayingUrl}
loop={panelInfo.continuousPlay || false}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal && panelInfo.modalClassName}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}
onLoadedMetadata={mediainfoHandler}
onDurationChange={mediainfoHandler}
reactPlayerConfig={reactPlayerSubtitleConfig}
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
videoComponent={
(typeof window === 'object' && !window.PalmSystem) || isYoutube ? TReactPlayer : Media
}
// VideoOverlay props - 단순화
type="MEDIA"
panelInfo={panelInfo}
captionEnable={false}
setIsSubtitleActive={setIsSubtitleActive}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
// PlayerOverlayContents props ( 배열로 전달하여 null 에러 방지)
playListInfo={[]}
selectedIndex={0}
videoVerticalVisible={false}
sideContentsVisible={false}
setSideContentsVisible={setSideContentsVisible}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />
)} )}
{isSubtitleActive && >
!panelInfo.modal && <VideoPlayer
typeof window === 'object' && setApiProvider={getPlayer}
window.PalmSystem && disabled={panelInfo.modal}
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />} onEnded={onEnded}
</VideoPlayer> noAutoPlay={false}
noAutoShowMediaControls={panelInfo.modal} // modal 모드에서는 자동으로 controls가 올라오지 않도록 설정
autoCloseTimeout={3000}
onBackButton={onClickBack}
onClick={onVideoClick}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}
src={currentPlayingUrl}
loop={panelInfo.continuousPlay || false}
style={panelInfo.modal ? modalStyle : {}}
modalScale={panelInfo.modal ? modalScale : 1}
modalClassName={panelInfo.modal && panelInfo.modalClassName}
onError={mediainfoHandler}
onTimeUpdate={mediainfoHandler}
onLoadedData={mediainfoHandler}
onLoadedMetadata={mediainfoHandler}
onDurationChange={mediainfoHandler}
reactPlayerConfig={reactPlayerSubtitleConfig}
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
videoComponent={
(typeof window === 'object' && !window.PalmSystem) || isYoutube ? TReactPlayer : Media
}
// VideoOverlay props - 간소화
type="MEDIA"
panelInfo={panelInfo}
captionEnable={false}
setIsSubtitleActive={setIsSubtitleActive}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
// PlayerOverlayContents props ( 배열로 전달하여 null 처리)
playListInfo={[]}
selectedIndex={0}
videoVerticalVisible={false}
sideContentsVisible={false}
setSideContentsVisible={setSideContentsVisible}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />
)}
{isSubtitleActive &&
!panelInfo.modal &&
typeof window === 'object' &&
window.PalmSystem &&
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />}
</VideoPlayer>
</div>
)} )}
</Container> </Container>
</TPanel> </TPanel>

View File

@@ -60,3 +60,15 @@
} }
} }
} }
.videoPlayerWrapper {
width: 100%;
height: 100%;
}
.youtubeSafe {
:global(.react-player),
:global(.react-player iframe) {
pointer-events: none !important;
}
}