[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:
@@ -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 () {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -60,3 +60,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.videoPlayerWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtubeSafe {
|
||||||
|
:global(.react-player),
|
||||||
|
:global(.react-player iframe) {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user