From 30472bfe17de08031540ed58d4cfec0c6fd1f0c4 Mon Sep 17 00:00:00 2001 From: optrader Date: Wed, 17 Dec 2025 17:24:14 +0900 Subject: [PATCH] =?UTF-8?q?[251217]=20fix:=20MediaPanelOverlayConents?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 커밋 시간: 2025. 12. 17. 17:24:13 📊 변경 통계: • 총 파일: 5개 • 추가: +528줄 • 삭제: -70줄 📁 추가된 파일: + com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.module.less 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 대규모 기능 개발 --- .../VideoPlayer/MediaPlayer.module.less | 2 + .../VideoPlayer/VideoPlayer.module.less | 1 + .../VideoPlayer/VideoPlayer.v3.module.less | 128 ++--- .../PlayerOverlay/MediaOverlayContents.jsx | 465 +++++++++++++++++- .../MediaOverlayContents.module.less | 155 ++++++ 5 files changed, 682 insertions(+), 69 deletions(-) create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.module.less diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less index 09630954..183564b6 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less @@ -779,6 +779,8 @@ align-items: center; gap: 12px; width: 100%; + margin-left: 20px; + margin-right: 20px; } .times { diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less index 798f4739..22900d9f 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less @@ -695,6 +695,7 @@ height: 70px; width:1800px; margin-left:60px; + margin-right: 59px; bottom:92px; > *:first-child { text-align: right; diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less index d3c91ed5..d674fc53 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less @@ -1,19 +1,19 @@ // VideoPlayer.module.less // -@import "~@enact/sandstone/styles/variables.less"; +@import "~@enact/sandstone/styles/variables.less"; @import "~@enact/sandstone/styles/mixins.less"; @import "~@enact/sandstone/styles/skin.less"; @import "../../style/utils.module.less"; @import "../../style/CommonStyle.module.less"; -.fullscreen .videoPlayer, -.videoPlayer { - // Set by counting the IconButtons inside the side components. - --liftDistance: 0px; - - overflow: hidden; - padding: 2px; - margin: 0; - box-sizing: border-box; +.fullscreen .videoPlayer, +.videoPlayer { + // Set by counting the IconButtons inside the side components. + --liftDistance: 0px; + + overflow: hidden; + padding: 2px; + margin: 0; + box-sizing: border-box; :focus { outline: none !important; @@ -21,51 +21,51 @@ box-shadow: none !important; } - &.fullscreen { - width: 100%; - height: 100%; - } - - .video { - height: 100%; - width: 100%; - background: #000; - max-width: none; - max-height: none; - } + &.fullscreen { + width: 100%; + height: 100%; + } - .media { - height: var(--media-height, calc(100% - 4px)); - width: var(--media-width, calc(100% - 4px)); - background: #000; - - &.mediaBackground { - &:after { - width: 560px; - height: 200px; - position: absolute; - left: 0; - bottom: 0; - content: ""; - background: linear-gradient( - to top, - rgba(255, 255, 255, 1), - transparent - ); - opacity: 0.2; - } - } - } - - &.fullscreen { - --media-width: 100vw; - --media-height: 100vh; - } - - .fullscreen .videoPlayer .media { - --media-width: 100vw; - --media-height: 100vh; - } + .video { + height: 100%; + width: 100%; + background: #000; + max-width: none; + max-height: none; + } + + .media { + height: var(--media-height, calc(100% - 4px)); + width: var(--media-width, calc(100% - 4px)); + background: #000; + + &.mediaBackground { + &:after { + width: 560px; + height: 200px; + position: absolute; + left: 0; + bottom: 0; + content: ""; + background: linear-gradient( + to top, + rgba(255, 255, 255, 1), + transparent + ); + opacity: 0.2; + } + } + } + + &.fullscreen { + --media-width: 100vw; + --media-height: 100vh; + } + + .fullscreen .videoPlayer .media { + --media-width: 100vw; + --media-height: 100vh; + } .preloadVideo { display: none; @@ -613,11 +613,11 @@ } } - .overlay { - .position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0); - pointer-events: auto; - z-index: 10; - } + .overlay { + .position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0); + pointer-events: auto; + z-index: 10; + } @keyframes spin { 0% { transform: rotate(0.25turn); @@ -741,11 +741,11 @@ } } - .controlsHandleAbove { - pointer-events: none; - z-index: -1; - .position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0); - } + .controlsHandleAbove { + pointer-events: none; + z-index: -1; + .position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0); + } // Skin colors .applySkins({ diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx index 29ef1d9c..142d078f 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx @@ -1,9 +1,464 @@ -import React from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; -import PlayerOverlayContents from './PlayerOverlayContents'; +import classNames from 'classnames'; +import { + useDispatch, + useSelector, +} from 'react-redux'; -function MediaOverlayContents(props) { - return ; +import Spotlight from '@enact/spotlight'; +import SpotlightContainerDecorator + from '@enact/spotlight/SpotlightContainerDecorator'; +import Spottable from '@enact/spotlight/Spottable'; +import Marquee from '@enact/ui/Marquee'; + +import defaultLogoImg + from '../../../../assets/images/ic-tab-partners-default@3x.png'; +import { setShowPopup } from '../../../actions/commonActions'; +import CustomImage from '../../../components/CustomImage/CustomImage'; +import { ACTIVE_POPUP } from '../../../utils/Config'; +import { SpotlightIds } from '../../../utils/SpotlightIds'; +import PlayerTabButton from '../PlayerTabContents/TabButton/PlayerTabButton'; +import css from './MediaOverlayContents.module.less'; + +const SpottableBtn = Spottable('button'); + +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); + +function MediaOverlayContents({ + type, + onClick, + panelInfo, + disclaimer, + playListInfo, + captionEnable, + selectedIndex, + setIsSubtitleActive, + videoVerticalVisible, + sideContentsVisible, + setSideContentsVisible, + belowContentsVisible, + handleIndicatorUpClick, + handleIndicatorDownClick, + tabContainerVersion, + tabIndexV2, +}) { + const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd); + const dispatch = useDispatch(); + const onClickBack = (ev) => { + // TabContainerV2가 표시된 상태에서 백버튼 클릭 시 이벤트 버블링 방지 + // (Overlay의 onClick으로 전파되어 toggleControls()가 호출되는 것을 막음) + if (tabContainerVersion === 2 && belowContentsVisible) { + ev.stopPropagation(); + } + + if (onClick) { + onClick(ev); + } + }; + const backBtnRef = useRef(null); + + useEffect(() => { + if (type === 'MEDIA' && !panelInfo.modal && backBtnRef.current) { + Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); + } + }, [type, panelInfo.modal, backBtnRef]); + + const handleSubtitleOnClick = useCallback(() => { + if (!captionEnable) { + return dispatch(setShowPopup(ACTIVE_POPUP.alertPopup)); + } + + setIsSubtitleActive((prev) => !prev); + }, [dispatch, captionEnable, setIsSubtitleActive]); + + const patncLogoPath = useMemo(() => { + let logo = playListInfo[selectedIndex]?.patncLogoPath; + if (type === 'MEDIA') { + logo = panelInfo?.patncLogoPath; + } + + return logo; + }, [playListInfo, selectedIndex, panelInfo, type]); + + const partnerName = useMemo(() => { + let name = playListInfo[selectedIndex]?.patncNm; + if (type === 'MEDIA') { + name = panelInfo?.patncNm; + } + + return name; + }, [playListInfo, selectedIndex, panelInfo, type]); + + const showName = useMemo(() => { + let name = playListInfo[selectedIndex]?.showNm; + if (type === 'MEDIA') { + name = panelInfo?.showNm; + } + + return name ? name.replace(//gi, ' ') : ''; + }, [playListInfo, selectedIndex, panelInfo, type]); + + const onSpotlightMoveTabButton = useCallback((e) => { + e.stopPropagation(); + e.preventDefault(); + Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON); + }, []); + + const onSpotlightMoveMediaButton = useCallback( + (e) => { + e.stopPropagation(); + + // TabContainerV2의 tabIndex=2일 때 하단 버튼들로 포커스 이동 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + if (Spotlight.focus('live-channel-next-button')) return; + if (Spotlight.focus('below-tab-shop-now-button')) return; + } + + if (type === 'LIVE') { + return Spotlight.focus('videoIndicator-down-button'); + } + return Spotlight.focus(SpotlightIds.PLAYER_PLAY_BUTTON); + }, + [type, tabContainerVersion, tabIndexV2] + ); + + const onSpotlightMoveSubtitleButton = useCallback( + () => { + // 1. 먼저 자막 버튼으로 포커스 시도 + if (Spotlight.focus('player-subtitlebutton')) { + return true; + } + + // TabContainerV2의 tabIndex=2일 때 TabContainerV2 버튼들로 포커스 이동 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + let focusSuccessful = false; + + // 먼저 LiveChannelNext 버튼으로 시도 + if (Spotlight.focus('live-channel-next-button')) { + focusSuccessful = true; + } + // 실패하면 ShopNowButton으로 시도 + else if (Spotlight.focus('below-tab-shop-now-button')) { + focusSuccessful = true; + } + + if (focusSuccessful) { + return; + } + } + + // 기본 동작: 자막 버튼으로 포커스 + return Spotlight.focus('player-subtitlebutton'); + }, + [tabContainerVersion, tabIndexV2] + ); + + const onSpotlightMoveSlider = useCallback( + (e) => { + if (type === 'VOD') { + e.stopPropagation(); + + Spotlight.focus(SpotlightIds.PLAYER_SLIDER); + } + }, + [type] + ); + + const onSpotlightMoveSideTab = useCallback((e) => { + e.stopPropagation(); + e.preventDefault(); + Spotlight.focus('tab-0'); + }, []); + + const onSpotlightMoveBelowTab = useCallback( + (e) => { + e.stopPropagation(); + e.preventDefault(); + + console.log(`[onSpotlightMoveBelowTab] tabIndexV2: ${tabIndexV2}`); + + // tabIndexV2에 따라 다른 버튼으로 포커스 이동 + if (tabIndexV2 === 0) { + // ShopNow 탭: Close 버튼으로 + // Spotlight.focus('below-tab-close-button'); + const result = Spotlight.focus('shownow_close_button'); + console.log(`[onSpotlightMoveBelowTab] tabIndexV2=0, focus result:`, result); + } else if (tabIndexV2 === 1) { + // LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로 + const result = Spotlight.focus('below-tab-live-channel-button'); + console.log(`[onSpotlightMoveBelowTab] tabIndexV2=1, focus result:`, result); + } else if (tabIndexV2 === 2) { + // ShopNowButton: ShopNowButton으로 + const result = Spotlight.focus('below-tab-shop-now-button'); + console.log(`[onSpotlightMoveBelowTab] tabIndexV2=2, focus result:`, result); + } + }, + [tabIndexV2] + ); + + // Back Button arrow down 전용 핸들러 - tabIndex에 따라 다른 포커스 + const handleBackButtonDown = useCallback( + (e) => { + e.stopPropagation(); + e.preventDefault(); + + if (tabContainerVersion === 2 && belowContentsVisible) { + if (tabIndexV2 === 0) { + // tabIndexV2가 0일 때 ShopNow 닫기 버튼으로 포커스 + const result = Spotlight.focus('shownow_close_button'); + } else if (tabIndexV2 === 1) { + // tabIndexV2가 1일 때 below-tab-live-channel-button으로 포커스 + Spotlight.focus('below-tab-live-channel-button'); + } else if (tabIndexV2 === 2) { + // tabIndexV2가 2일 때 LiveChannelNext로 포커스 + Spotlight.focus('live-channel-next-button'); + } else { + // 그 외에는 기존 로직 사용 + onSpotlightMoveMediaButton(e); + } + } else { + onSpotlightMoveMediaButton(e); + } + }, + [tabContainerVersion, belowContentsVisible, tabIndexV2, onSpotlightMoveMediaButton] + ); + + const onSpotlightMoveBackButton = useCallback(() => { + return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); + }, []); + + const handleOverlayKeyDownCapture = useCallback( + (ev) => { + const currentId = Spotlight.getCurrent()?.getAttribute('data-spotlight-id'); + if (ev.keyCode === 40 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return handleBackButtonDown(ev); + } + if (ev.keyCode === 39 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveSubtitleButton(ev); + } + if (ev.keyCode === 37 && currentId === SpotlightIds.PLAYER_SUBTITLE_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveBackButton(ev); + } + if (ev.keyCode === 38 && currentId === SpotlightIds.PLAYER_PLAY_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveBackButton(); + } + }, + [onSpotlightMoveBackButton, onSpotlightMoveMediaButton, onSpotlightMoveSubtitleButton, handleBackButtonDown] + ); + + const currentSideButtonStatus = useMemo(() => { + if ( + !panelInfo?.modal && + !sideContentsVisible && + tabContainerVersion === 1 + ) { + return true; + } + return false; + }, [panelInfo, sideContentsVisible, tabContainerVersion]); + + const noLiveContentsVisible = useMemo(() => { + if (!Array.isArray(playListInfo) || playListInfo.length === 0) { + return false; + } + + const noShowIdCount = playListInfo.filter((item) => !item.showId).length; + if (playListInfo.length - 1 === noShowIdCount) { + return false; + } + + return true; + }, [playListInfo]); + + return ( + <> + + {/* 251118 임시로 unvisible */} + {/* {playListInfo.length > 1 && noLiveContentsVisible && ( + <> +
+ +
+
+ +
+ + )} */} + + {currentSideButtonStatus && !videoVerticalVisible && ( + + )} + + {cntry_cd === 'US' && ( +
+ { + e.stopPropagation(); + e.preventDefault(); + // tabIndexV2가 2일 때만 ShopNowButton으로 포커스 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + Spotlight.focus('below-tab-shop-now-button'); + } else { + onSpotlightMoveBackButton(); + } + }} + onSpotlightLeft={(e) => { + e.stopPropagation(); + e.preventDefault(); + // tabIndexV2가 2일 때만 LiveChannelNext로 포커스 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + Spotlight.focus('live-channel-next-button'); + } + }} + onSpotlightRight={(e) => { + e.stopPropagation(); + e.preventDefault(); + // tabIndexV2가 2일 때만 LiveChannelNext로 포커스 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + Spotlight.focus('below-tab-shop-now-button'); + } + }} + onSpotlightDown={(e) => { + e.stopPropagation(); + e.preventDefault(); + // tabIndexV2가 2일 때만 ShopNowButton으로 포커스 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + Spotlight.focus('live-channel-next-button'); + } + }} + aria-label="Caption" + /> +
+ )} +
+ { + e.stopPropagation(); + e.preventDefault(); + // tabIndexV2가 2일 때만 ShopNowButton으로 포커스 + if (tabContainerVersion === 2 && tabIndexV2 === 2) { + Spotlight.focus('below-tab-shop-now-button'); + } + }} + onSpotlightUp={onSpotlightMoveSubtitleButton} + aria-label="Video Player Close" + ref={backBtnRef} + /> +
+ + {partnerName && ( + + )} + +

{partnerName}

+ + {!panelInfo?.modal && ( + + {showName} + + )} +
+ + {type === 'VOD' && disclaimer && ( +
+ +

{disclaimer}

+
+ )} + + ); } -export default MediaOverlayContents; +const propsAreEqual = (prev, next) => { + return ( + prev.type === next.type && + prev.panelInfo?.showId === next.panelInfo?.showId && + prev.disclaimer === next.disclaimer && + prev.playListInfo === next.playListInfo && + prev.captionEnable === next.captionEnable && + prev.selectedIndex === next.selectedIndex && + prev.videoVerticalVisible === next.videoVerticalVisible && + prev.sideContentsVisible === next.sideContentsVisible && + prev.belowContentsVisible === next.belowContentsVisible && + prev.tabContainerVersion === next.tabContainerVersion && + prev.tabIndexV2 === next.tabIndexV2 + ); +}; + +export default React.memo(MediaOverlayContents, propsAreEqual); diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.module.less new file mode 100644 index 00000000..06f22e3a --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.module.less @@ -0,0 +1,155 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +.overlayContainer { + .position(@position: absolute, @top: 77px, @right: 0, @bottom: 0, @left: 60px); + display: flex; + z-index: 5; + + .videoButtonContainer { + .subtitleButton { + width: 60px; + height: 60px; + background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png"); + background-size: cover; + position: absolute; + right: 300px; + top: 680px; + z-index: 10; + + &.videoVericalSubtitleButton { + position: absolute; + right: 680px; + top: 850px; + } + + &:focus { + background-color: rgba(199, 8, 80, 0.5); + border-radius: 50%; + } + } + } + .indicatorUpButton { + position: absolute; + top: -77px; + left: 880px; + z-index: 10; + .size(@w: 144px, @h: 48px); + > button { + .size(@w: 144px, @h: 48px); + background-position: center center; + background-repeat: no-repeat; + background-image: url("../../../../assets/images/btn/btn-wh-arrow-top-nor.svg"); + &:focus { + background-color: @PRIMARY_COLOR_RED; + border-radius: 0 0 4px 4px; + } + } + } + .indicatorDownButton { + position: absolute; + bottom: -986px; + left: 880px; + z-index: 10; + .size(@w: 144px, @h: 48px); + > button { + .size(@w: 144px, @h: 48px); + background-position: center center; + background-repeat: no-repeat; + background-image: url("../../../../assets/images/btn/btn-wh-arrow-down-nor.svg"); + &:focus { + background-color: @PRIMARY_COLOR_RED; + border-radius: 4px 4px 0 0; + } + } + } + + .overlayHeader { + display: flex; + width: 100%; + height: auto; + + .backIcon { + .size(@w: 60px, @h: 60px); + background-size: 60px 60px; + background-repeat: no-repeat; + background-image: url("../../../../assets/images/btn/btn-60-wh-back-nor@3x.png"); + + &:focus { + background-image: url("../../../../assets/images/btn/btn-60-wh-back-foc@3x.png"); + } + } + .liveIcon { + .size(@w: 108px, @h: 48px); + margin: 6px 0 6px 30px; + background-size: 108px 48px; + background-repeat: no-repeat; + background-image: url("../../../../assets/images/tag-liveshow.png"); + vertical-align: top; + } + + > img { + .size(@w: auto, @h: 60px); + margin-left: 30px; + } + + .patnerName { + font-size: 44px; + font-weight: bold; + color: #fcfcfc; + margin-left: 14px; + padding-top: 10px; + } + + .title { + width: 1200px; + align-items: center; + font-size: 44px; + color: #fcfcfc; + margin-left: 35px; + + > div { + > div { + line-height: 1.2; + } + } + + &.videoVerticalTitle { + .size(@w: 950px , @h: 60px); + > div { + > div { + padding-top: 10px; + .size(@w: 940px , @h: 60px); + } + } + } + // .marquee { + // &.videoVerticalMarquee { + // width: 950px; + // } + // } + } + } +} + +.disclaimer { + .size(@w: 1800px , @h: 54px); + display: flex; + background-color: rgba(0, 0, 0, 0.5); + position: absolute; + top: 140px; + left: 60px; + align-items: center; + + > span { + .size(@w: 18px , @h: 18px); + background-image: url("../../../../assets/images/icons/ic-alert-20@3x.png"); + background-position: center; + background-size: cover; + margin: 0 12px 0 20px; + } + > h3 { + color: #ffffff; + font-size: 20px; + } +}