4 Commits

Author SHA1 Message Date
fd55c04c83 [251217] merge: gitlab develop_si 변경사항 병합
🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-17 17:34:36 +09:00
30472bfe17 [251217] fix: MediaPanelOverlayConents분리
🕐 커밋 시간: 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 컴포넌트 아키텍처 개선
  • 대규모 기능 개발
2025-12-17 17:24:14 +09:00
junghoon86.park
51d587a1a1 [상세에서의 에너지 라벨 크기 변경]
- 62px -> 70px로 변경.
2025-12-17 16:51:35 +09:00
junghoon86.park
349688092c [테마부분의 에너지 라벨 노출]
- 에너지 라벨 목업데이터 제거 및 받아와서 데이터 노출되도록 처리
 - 에너지 라벨 관련 처리. max-height: 800px로 제한.
2025-12-17 16:36:03 +09:00
9 changed files with 692 additions and 73 deletions

View File

@@ -779,6 +779,8 @@
align-items: center; align-items: center;
gap: 12px; gap: 12px;
width: 100%; width: 100%;
margin-left: 20px;
margin-right: 20px;
} }
.times { .times {

View File

@@ -695,6 +695,7 @@
height: 70px; height: 70px;
width:1800px; width:1800px;
margin-left:60px; margin-left:60px;
margin-right: 59px;
bottom:92px; bottom:92px;
> *:first-child { > *:first-child {
text-align: right; text-align: right;

View File

@@ -105,7 +105,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
.enrgLbImg { .enrgLbImg {
width:62px; width:70px;
border:4px solid transparent; border:4px solid transparent;
&:focus { &:focus {
border: 4px solid @PRIMARY_COLOR_RED; border: 4px solid @PRIMARY_COLOR_RED;

View File

@@ -248,6 +248,7 @@ export default function ThemeContents({
handleItemFocus?.(index); handleItemFocus?.(index);
}} }}
onMouseEnter={() => Spotlight.focus(spotlightItemId)} onMouseEnter={() => Spotlight.focus(spotlightItemId)}
euEnrgLblInfos={euEnrgLblInfos}
/> />
); );
}, },

View File

@@ -51,6 +51,7 @@ export default function ThemeItemCard({
spotlightId, spotlightId,
dataSpotlightDefault, dataSpotlightDefault,
onFocused, onFocused,
euEnrgLblInfos
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
@@ -211,9 +212,9 @@ export default function ThemeItemCard({
))} ))}
</div> </div>
)} */} )} */}
{/* {mockEnergyLabel && mockEnergyLabel.length > 0 && ( {euEnrgLblInfos && euEnrgLblInfos.length > 0 && (
<div className={css.energyLabels}> <div className={css.energyLabels}>
{mockEnergyLabel.map((label, labelIndex) => ( {euEnrgLblInfos.map((label, labelIndex) => (
<SpottableTemp <SpottableTemp
key={labelIndex} key={labelIndex}
spotlightDisabled={Boolean(!cursorVisible)} spotlightDisabled={Boolean(!cursorVisible)}
@@ -228,7 +229,7 @@ export default function ThemeItemCard({
</SpottableTemp> </SpottableTemp>
))} ))}
</div> </div>
)} */} )}
</div> </div>
</SpottableDiv> </SpottableDiv>
{(() => { {(() => {

View File

@@ -220,3 +220,7 @@
} }
} }
} }
.energyImage {
max-height:800px;
}

View File

@@ -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) { import Spotlight from '@enact/spotlight';
return <PlayerOverlayContents {...props} forceShowMediaOverlay />; 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(/<br\s*\/?>/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 (
<>
<Container className={css.overlayContainer} onKeyDownCapture={handleOverlayKeyDownCapture}>
{/* 251118 임시로 unvisible */}
{/* {playListInfo.length > 1 && noLiveContentsVisible && (
<>
<div className={css.indicatorUpButton}>
<SpottableBtn
onClick={handleIndicatorUpClick}
spotlightId="videoIndicator-up-button"
onSpotlightRight={
videoVerticalVisible
? onSpotlightMoveSideTab
: tabContainerVersion === 1
? onSpotlightMoveTabButton
: undefined
}
onSpotlightDown={
tabContainerVersion === 2 && belowContentsVisible
? onSpotlightMoveBelowTab
: onSpotlightMoveSlider
}
aria-label="Previous channel"
/>
</div>
<div className={css.indicatorDownButton}>
<SpottableBtn
onClick={handleIndicatorDownClick}
spotlightId="videoIndicator-down-button"
onSpotlightLeft={onSpotlightMoveSlider}
onSpotlightUp={onSpotlightMoveSlider}
onSpotlightRight={
videoVerticalVisible
? onSpotlightMoveSideTab
: tabContainerVersion === 1
? onSpotlightMoveTabButton
: undefined
}
onSpotlightDown={
tabContainerVersion === 2 && belowContentsVisible
? onSpotlightMoveBelowTab
: undefined
}
aria-label="Next channel"
/>
</div>
</>
)} */}
{currentSideButtonStatus && !videoVerticalVisible && (
<PlayerTabButton
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
onSpotlightLeft={
playListInfo.length < 2 && onSpotlightMoveBackButton
}
videoType={type}
/>
)}
{cntry_cd === 'US' && (
<div className={css.videoButtonContainer}>
<SpottableBtn
className={classNames(
css.subtitleButton,
videoVerticalVisible && css.videoVericalSubtitleButton
)}
onClick={handleSubtitleOnClick}
spotlightId="player-subtitlebutton"
onSpotlightUp={(e) => {
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"
/>
</div>
)}
<div className={css.overlayHeader}>
<SpottableBtn
onClick={onClickBack}
className={css.backIcon}
spotlightId="player-back-button"
onSpotlightDown={handleBackButtonDown}
onSpotlightRight={(e) => {
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}
/>
<div
className={classNames(type === 'LIVE' && css.liveIcon)}
aria-label={type === 'LIVE' && 'Live Icon'}
/>
{partnerName && (
<CustomImage
src={patncLogoPath}
fallbackSrc={defaultLogoImg}
alt={partnerName}
aria-label={partnerName}
/>
)}
<h2 className={css.patnerName}>{partnerName}</h2>
{!panelInfo?.modal && (
<Marquee
className={classNames(css.title, videoVerticalVisible && css.videoVerticalMarquee)}
marqueeOn="render"
>
{showName}
</Marquee>
)}
</div>
</Container>
{type === 'VOD' && disclaimer && (
<div className={css.disclaimer}>
<span className={css.icon} />
<h3 aria-label={disclaimer}>{disclaimer}</h3>
</div>
)}
</>
);
} }
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);

View File

@@ -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;
}
}