10 Commits

Author SHA1 Message Date
daac18afa8 [251217] fix: LiveChannelContents PageUp/Down시 첫번째로 배너위치
🕐 커밋 시간: 2025. 12. 17. 20:11:18

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +46줄
  • 삭제: -33줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-17 20:11:19 +09:00
adfede9b44 [251217] fix: LiveChannelContents.jsx 아이템포커스 테두리 수정
🕐 커밋 시간: 2025. 12. 17. 20:02:01

📊 변경 통계:
  • 총 파일: 2개

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/style/utils.module.less
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerItemCard/PlayerItemCard.v2.module.less

🔧 주요 변경 내용:
  • 공통 유틸리티 함수 최적화
2025-12-17 20:02:02 +09:00
1fae88878f [251217] fix: MediaPanel.v3.jsx 비디오복원 방어로직 추가
🕐 커밋 시간: 2025. 12. 17. 19:51:27

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +117줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-12-17 19:51:27 +09:00
4ecb03002f [251217] fix: VideoPlayer.v3.jsx times 전체화면에만 표시
🕐 커밋 시간: 2025. 12. 17. 18:19:37

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화
2025-12-17 18:19:37 +09:00
27e1e3bb6a [251217] fix: VideoPlayer.v3.jsx slider , times layout
🕐 커밋 시간: 2025. 12. 17. 18:16:49

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +3줄
  • 삭제: -86줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-17 18:16:49 +09:00
b9d52d452c [251217] fix: VideoPlayer.v3.jsx Spinner제거
🕐 커밋 시간: 2025. 12. 17. 18:04:43

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +18줄
  • 삭제: -6줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
2025-12-17 18:04:44 +09:00
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
16 changed files with 866 additions and 189 deletions

View File

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

View File

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

View File

@@ -2467,9 +2467,9 @@ const VideoPlayerBase = class extends React.Component {
</p>
}
<div className={css.loaderWrap}>
{/* <div className={css.loaderWrap}>
<Loader />
</div>
</div> */}
</>
) : null}
{this.state.mediaControlsVisible && (
@@ -2534,7 +2534,7 @@ const VideoPlayerBase = class extends React.Component {
videoVerticalVisible && css.videoVertical
)}
>
{this.state.mediaSliderVisible && type ? (
{this.state.mediaSliderVisible && type && !panelInfo.modal ? (
<Times
noCurrentTime
total={type === 'LIVE' ? liveTotalTime : this.state.duration}
@@ -2542,7 +2542,7 @@ const VideoPlayerBase = class extends React.Component {
type={type}
/>
) : null}
{this.state.mediaSliderVisible && type ? (
{this.state.mediaSliderVisible && type && !panelInfo.modal ? (
<Times
noTotalTime
current={type === 'LIVE' ? currentLiveTimeSeconds : this.state.currentTime}

View File

@@ -718,7 +718,7 @@
}
.sliderContainer {
// display: flex;
display: flex;
position: relative;
align-items: center;
margin-left: 60px;
@@ -727,6 +727,10 @@
bottom: -20px;
> *:first-child {
text-align: right;
margin-right: 12px;
}
> *:nth-child(2) {
margin-right: 12px;
}
.enact-locale-rtl({
@@ -787,79 +791,4 @@
});
}
// ========== MediaPlayer.v2 Controls ==========
.controlsContainer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px 40px 30px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, transparent 100%);
z-index: 10;
display: flex;
flex-direction: column;
gap: 16px;
}
.sliderContainer {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.times {
min-width: 80px;
text-align: center;
}
.controlsButtons {
display: flex;
gap: 20px;
justify-content: center;
align-items: center;
}
.playPauseBtn {
width: 60px;
height: 60px;
font-size: 24px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.95);
}
}
.backBtn {
padding: 12px 24px;
font-size: 18px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.98);
}
}

View File

@@ -10,7 +10,6 @@
.position(@position: absolute, @top: 0, @left: 0, @right: 0, @bottom: 0);
z-index: 19;
border: 4px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 @boxShadow 0 rgba(0, 0, 0, 0.5);
border-radius: @borderRadius;
content: "";
}

View File

@@ -183,6 +183,57 @@ const ButtonStackContainer = SpotlightContainerDecorator(
const SpottableComponent = Spottable('div');
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 저장/검증 함수
// PlayerPanel의 배너 위치 검증 패턴을 ProductAllSection에 적용
/**
* ProductAllSection의 스크롤 컨테이너 위치 수집 함수
* @returns {Object|null} 스크롤 컨테이너의 위치 정보 (top, left) 또는 null
*/
const collectProductScrollPosition = () => {
const scrollContainer = document.querySelector('[data-spotlight-id="main-content-scroller"]');
if (scrollContainer) {
const { top, left } = scrollContainer.getBoundingClientRect();
const position = {
top: Math.round(top),
left: Math.round(left)
};
console.log('[ProductAllSection] 스크롤 컨테이너 위치 수집:', position);
return position;
}
console.warn('[ProductAllSection] 스크롤 컨테이너를 찾을 수 없음');
return null;
};
/**
* ProductAllSection 스크롤 컨테이너 위치 검증 함수
* @param {Object} savedPosition - 저장된 초기 위치
* @param {Object} currentPosition - 현재 위치
* @returns {boolean} 위치가 일치하는지 여부
*/
const isProductScrollPositionValid = (savedPosition, currentPosition) => {
if (!savedPosition || !currentPosition) {
console.warn('[ProductAllSection] 저장된 위치 또는 현재 위치가 없음');
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - savedPosition.top) <= tolerance &&
Math.abs(currentPosition.left - savedPosition.left) <= tolerance;
console.log('[ProductAllSection] 스크롤 위치 검증:', {
expected: savedPosition,
current: currentPosition,
matching: isMatching,
tolerance
});
return isMatching;
};
const getProductData = curry(
(productType, themeProductInfo, themeProducts, selectedIndex, productInfo) =>
pipe(
@@ -304,6 +355,9 @@ export default function ProductAllSection({
// handleScrollToImages의 timeout을 추적하기 위한 ref
const scrollToImagesTimeoutRef = useRef(null);
// 🔽 [251217] 스크롤 컨테이너 초기 위치 저장 ref (VideoPlayer modal 전환 시 위치 검증용)
const scrollPositionOnMountRef = useRef(null);
// ProductAllSection 초기 로딩 시 Skeleton 표시를 위한 상태
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -344,6 +398,19 @@ export default function ProductAllSection({
);
}, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]);
// 🔽 [251217] ProductAllSection 마운트 시 스크롤 컨테이너 위치 저장
useEffect(() => {
// 초기 렌더링 후 스크롤 컨테이너의 위치를 저장 (1회만 실행)
// 이 위치는 나중에 VideoPlayer의 modal 전환 시 유효성 검증에 사용됨
const savedPosition = collectProductScrollPosition();
if (savedPosition) {
scrollPositionOnMountRef.current = savedPosition;
// 🔽 window 객체에도 저장하여 MediaPanel.v3.jsx에서 접근 가능하게 함
window.productScrollPositionOnMount = savedPosition;
console.log('[ProductAllSection] ✅ 초기 스크롤 위치 저장 완료:', savedPosition);
}
}, []); // 마운트 시 1회만 실행
useEffect(() => {
// 필수 값이 모두 있을 때만 호출
if (selectedPatnrId && selectedPrdtId) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,6 +93,36 @@ const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => {
}
};
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 수집 함수
const collectProductScrollPosition = () => {
const scrollContainer = document.querySelector('[data-spotlight-id="main-content-scroller"]');
if (scrollContainer) {
const { top, left } = scrollContainer.getBoundingClientRect();
const position = {
top: Math.round(top),
left: Math.round(left)
};
return position;
}
return null;
};
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 검증 함수
const isProductScrollPositionValid = (savedPosition, currentPosition) => {
if (!savedPosition || !currentPosition) {
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - savedPosition.top) <= tolerance &&
Math.abs(currentPosition.left - savedPosition.left) <= tolerance;
return isMatching;
};
const getLogTpNo = (type, nowMenu) => {
if (type === 'LIVE') {
switch (nowMenu) {
@@ -1107,6 +1137,26 @@ const MediaPanel = React.forwardRef(
}, [dispatch]);
const enterFullscreen = useCallback(() => {
// 🔽 [251217] ProductAllSection 스크롤 위치 검증
// DetailPanel에서 ProductAllSection의 초기 위치와 현재 위치를 비교하여 검증
const savedPosition = window.productScrollPositionOnMount;
const currentPosition = collectProductScrollPosition();
if (savedPosition && currentPosition) {
const isValid = isProductScrollPositionValid(savedPosition, currentPosition);
if (!isValid) {
console.warn('[MediaPanel] ⚠️ ProductAllSection 스크롤 위치 검증 실패 - 전체화면 재생 가능하지만 경고 표시', {
savedPosition,
currentPosition,
});
// 사용자 피드백: 위치가 일치하지 않음을 콘솔에 기록 (비디오는 계속 재생)
} else {
console.log('[MediaPanel] ✅ ProductAllSection 스크롤 위치 검증 성공');
}
} else {
console.warn('[MediaPanel] ⚠️ ProductAllSection 위치 정보 없음 - 검증 스킵');
}
isTransitioningToFullscreen.current = true;
dispatch(switchMediaToFullscreen());
}, [dispatch]);

View File

@@ -167,8 +167,8 @@
left: 0;
width: 100%;
height: 100%;
border: 4px solid @PRIMARY_COLOR_RED;
border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none;
}
}
@@ -298,8 +298,8 @@
left: 0;
width: 100%;
height: 100%;
border: 4px solid @PRIMARY_COLOR_RED;
border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none;
}
}

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) {
return <PlayerOverlayContents {...props} forceShowMediaOverlay />;
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();
}
export default MediaOverlayContents;
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>
)}
</>
);
}
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;
}
}

View File

@@ -75,7 +75,7 @@ export default function LiveChannelContents({
// currentVideoShowId와 일치하는 배너의 인덱스 찾기
const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId);
if (index !== -1) {
scrollToRef.current({ index, animate: true, focus: false });
scrollToRef.current({ index, animate: true, focus: false, alignToStart: true });
}
}
}, [currentVideoShowId, liveInfos]);

View File

@@ -82,28 +82,34 @@ export default function TScrollerLiveChannel({
if (!container || !itemsRef.current[index]) return;
const item = itemsRef.current[index];
const { animate = true } = options;
const { animate = true, alignToStart = false } = options;
if (direction === 'horizontal') {
// 수평 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
// 수평 스크롤: 스크롤 가능 여부 판단
const isScrollable = container.scrollWidth > container.clientWidth;
let scrollLeft = 0;
if (alignToStart && isScrollable) {
// 첫 번째 위치로 스크롤
scrollLeft = item.offsetLeft - scaleW(spacing);
} else if (!alignToStart) {
// 기존 로직: 현재 아이템 + 다음 아이템까지 보이도록
const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth;
// 다음 아이템도 일부 보일 수 있도록 스크롤
// 현재 아이템 + 다음 아이템의 일부가 보이는 위치로 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollLeft = itemLeft - scaleW(spacing);
scrollLeft = itemLeft - scaleW(spacing);
if (nextItem) {
// 다음 아이템의 왼쪽 끝이 컨테이너의 오른쪽 끝과 같은 위치가 되도록
const nextItemLeft = nextItem.offsetLeft;
const nextItemWidth = nextItem.offsetWidth;
const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollLeft = Math.min(scrollLeft, targetScrollLeft);
}
}
// 음수 스크롤 방지
scrollLeft = Math.max(0, scrollLeft);
@@ -117,24 +123,31 @@ export default function TScrollerLiveChannel({
container.scrollLeft = scrollLeft;
}
} else {
// 수직 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
// 수직 스크롤: 스크롤 가능 여부 판단
const isScrollable = container.scrollHeight > container.clientHeight;
let scrollTop = 0;
if (alignToStart && isScrollable) {
// 첫 번째 위치로 스크롤
scrollTop = item.offsetTop - scaleH(spacing);
} else if (!alignToStart) {
// 기존 로직: 현재 아이템 + 다음 아이템까지 보이도록
const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight;
// 다음 아이템도 일부 보일 수 있도록 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollTop = itemTop - scaleH(spacing);
scrollTop = itemTop - scaleH(spacing);
if (nextItem) {
// 다음 아이템의 위쪽 끝이 컨테이너의 아래쪽 끝과 같은 위치가 되도록
const nextItemTop = nextItem.offsetTop;
const nextItemHeight = nextItem.offsetHeight;
const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollTop = Math.min(scrollTop, targetScrollTop);
}
}
// 음수 스크롤 방지
scrollTop = Math.max(0, scrollTop);
@@ -156,9 +169,9 @@ export default function TScrollerLiveChannel({
useEffect(() => {
if (cbScrollTo) {
cbScrollTo((options) => {
const { index, animate = true, focus = true } = options;
const { index, animate = true, focus = true, alignToStart = false } = options;
if (typeof index === 'number' && index >= 0 && index < dataSize) {
scrollToIndex(index, { animate });
scrollToIndex(index, { animate, alignToStart });
}
});
}