Compare commits
6 Commits
backup-202
...
0223499e12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0223499e12 | ||
| 3fd3b66cb3 | |||
| be9b1faeec | |||
| 07a042cca6 | |||
|
|
d93960f40a | ||
| 4dfa15b4c0 |
@@ -34,7 +34,7 @@
|
||||
padding: @slider-padding-v 0;
|
||||
height: @sand-mediaplayer-slider-height;
|
||||
right: 154px;
|
||||
width: 1466px;
|
||||
width: 1558px;
|
||||
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
position: absolute;
|
||||
font-family: @baseFont;
|
||||
width: 100%;
|
||||
right: 90px;
|
||||
right: 20px;
|
||||
bottom: -5px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
@@ -16,12 +16,12 @@
|
||||
letter-spacing: -1px;
|
||||
.separator {
|
||||
position: absolute;
|
||||
right: 105px;
|
||||
right: 95px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.currentTime {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
right: 120px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.totalTime {
|
||||
|
||||
@@ -76,6 +76,10 @@ export default function ProductDescription({ productInfo }) {
|
||||
|
||||
const _onClose = useCallback(()=>{
|
||||
dispatch(setHidePopup());
|
||||
// Restore focus to the description content after popup closes
|
||||
setTimeout(() => {
|
||||
Spotlight.focus('product-description-content');
|
||||
}, 100);
|
||||
},[dispatch])
|
||||
|
||||
// ProductDescription: Container 직접 사용 패턴
|
||||
|
||||
@@ -617,6 +617,15 @@ export default function RandomUnit({
|
||||
|
||||
// 비디오 클릭
|
||||
const videoClick = useCallback(() => {
|
||||
// 🔽 비디오가 다른 배너에서 modal=true로 이미 재생 중이면 클릭 무시
|
||||
if (playerPanelInfo?.modal === true && currentVideoBannerId && currentVideoBannerId !== spotlightId) {
|
||||
console.log('[RandomUnit] videoClick 무시: 다른 배너에서 modal=true로 재생 중', {
|
||||
currentVideoBannerId,
|
||||
clickedBannerId: spotlightId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
|
||||
const currentSpot = Spotlight.getCurrent();
|
||||
if (lastFocusedTargetId) {
|
||||
@@ -674,6 +683,7 @@ export default function RandomUnit({
|
||||
sendBannerLog,
|
||||
onBlur,
|
||||
playerPanelInfo?.modal,
|
||||
currentVideoBannerId,
|
||||
dispatch,
|
||||
handleStartVideo,
|
||||
]);
|
||||
|
||||
@@ -390,7 +390,7 @@ function PlayerOverlayContents({
|
||||
e.preventDefault();
|
||||
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스
|
||||
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||
Spotlight.focus('live-channel-next-button');
|
||||
Spotlight.focus('below-tab-shop-now-button');
|
||||
}
|
||||
}}
|
||||
onSpotlightDown={(e) => {
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png");
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 800px;
|
||||
right: 300px;
|
||||
top: 680px;
|
||||
z-index: 10;
|
||||
|
||||
&.videoVericalSubtitleButton {
|
||||
|
||||
@@ -91,6 +91,51 @@ const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 배너 위치 수집 함수 (top, left만 저장)
|
||||
const collectBannerPositions = () => {
|
||||
const positions = [];
|
||||
|
||||
// banner0, banner1 등의 배너 위치 수집
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const bannerId = `banner${i}`;
|
||||
const node = document.querySelector(`[data-spotlight-id="${bannerId}"]`);
|
||||
|
||||
if (node) {
|
||||
const { top, left } = node.getBoundingClientRect();
|
||||
positions.push({
|
||||
bannerId,
|
||||
position: { top: Math.round(top), left: Math.round(left) }
|
||||
});
|
||||
dlog(`[PlayerPanel] 배너 위치 수집: ${bannerId}`, { top: Math.round(top), left: Math.round(left) });
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
};
|
||||
|
||||
// 위치 검증 함수 (오차 범위: 1px)
|
||||
const isPositionMatching = (bannerPositions, bannerId, currentPosition) => {
|
||||
const validPosition = bannerPositions.find(p => p.bannerId === bannerId);
|
||||
|
||||
if (!validPosition) {
|
||||
dlog(`[PlayerPanel] 배너 위치 검증 실패: ${bannerId} 배너를 찾을 수 없음`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const tolerance = 1; // 1px 오차 범위
|
||||
const isMatching =
|
||||
Math.abs(currentPosition.top - validPosition.position.top) <= tolerance &&
|
||||
Math.abs(currentPosition.left - validPosition.position.left) <= tolerance;
|
||||
|
||||
dlog(`[PlayerPanel] 배너 위치 검증: ${bannerId}`, {
|
||||
expected: validPosition.position,
|
||||
current: currentPosition,
|
||||
matching: isMatching
|
||||
});
|
||||
|
||||
return isMatching;
|
||||
};
|
||||
|
||||
const getLogTpNo = (type, nowMenu) => {
|
||||
if (type === 'LIVE') {
|
||||
switch (nowMenu) {
|
||||
@@ -219,6 +264,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton
|
||||
const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단)
|
||||
const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그
|
||||
const [validBannerPositions, setValidBannerPositions] = USE_STATE('validBannerPositions', []); // 유효한 배너 위치 (top, left)
|
||||
|
||||
const panels = USE_SELECTOR('panels', (state) => state.panels.panels);
|
||||
const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData);
|
||||
@@ -1799,6 +1845,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
if (watchIntervalLive.current) clearInterval(watchIntervalLive.current);
|
||||
if (watchIntervalVod.current) clearInterval(watchIntervalVod.current);
|
||||
if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current);
|
||||
if (activityCheckIntervalRef.current) clearInterval(activityCheckIntervalRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1937,6 +1984,30 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
panelInfo: { modalStyle: modalStyle, modalScale: scale },
|
||||
})
|
||||
);
|
||||
|
||||
// 🔽 배너 위치 수집 (초기 로드 시에만 실행)
|
||||
if (validBannerPositions.length === 0) {
|
||||
const positions = collectBannerPositions();
|
||||
if (positions.length > 0) {
|
||||
setValidBannerPositions(positions);
|
||||
dlog('[PlayerPanel] ✅ 배너 위치 초기 수집 완료:', positions);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 배너 위치 검증 (위치가 맞지 않으면 비디오 재생 중단)
|
||||
if (validBannerPositions.length > 0) {
|
||||
const currentPosition = { top: Math.round(top), left: Math.round(left) };
|
||||
const isValidPosition = isPositionMatching(validBannerPositions, panelInfo.modalContainerId, currentPosition);
|
||||
|
||||
if (!isValidPosition) {
|
||||
dlog('[PlayerPanel] ⚠️ 배너 위치 검증 실패 - 비디오 재생 중단', {
|
||||
bannerId: panelInfo.modalContainerId,
|
||||
currentPosition,
|
||||
validBannerPositions
|
||||
});
|
||||
return; // 비디오 재생 중단
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle');
|
||||
setModalStyle(panelInfo.modalStyle);
|
||||
@@ -2275,7 +2346,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
setPrevChannelIndex(selectedIndex);
|
||||
}
|
||||
setSideContentsVisible(true);
|
||||
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
|
||||
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
|
||||
|
||||
const handleIndicatorUpClick = useCallback(() => {
|
||||
if (!initialEnter) {
|
||||
@@ -2323,7 +2394,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
setPrevChannelIndex(selectedIndex);
|
||||
}
|
||||
setSideContentsVisible(true);
|
||||
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]);
|
||||
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) {
|
||||
@@ -2483,6 +2554,11 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
const timerIdTabAutoAdvance = useRef(null);
|
||||
const prevTabIndexV2 = useRef(null);
|
||||
|
||||
// Activity Check for tabIndex auto-advance
|
||||
const lastActivityTimeRef = useRef(Date.now());
|
||||
const activityCheckIntervalRef = useRef(null);
|
||||
const ACTIVITY_TIMEOUT = 1000; // 1초 동안 활동이 없으면 타이머 진행
|
||||
|
||||
const showSideContents = useMemo(() => {
|
||||
return (
|
||||
sideContentsVisible &&
|
||||
@@ -2594,17 +2670,62 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
timerIdTabAutoAdvance.current = null;
|
||||
}, []);
|
||||
|
||||
// Activity 감지 함수
|
||||
const onActivityDetected = useCallback(() => {
|
||||
lastActivityTimeRef.current = Date.now();
|
||||
dlog('[PlayerPanel] 🎯 Activity detected - timer will be delayed', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Activity 여부를 확인하는 함수 (1초 타임아웃 체크)
|
||||
const isInactive = useCallback(() => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastActivity = now - lastActivityTimeRef.current;
|
||||
return timeSinceLastActivity > ACTIVITY_TIMEOUT;
|
||||
}, []);
|
||||
|
||||
const resetTimerTabAutoAdvance = useCallback(
|
||||
(timeout) => {
|
||||
if (timerIdTabAutoAdvance.current) {
|
||||
clearTimerTabAutoAdvance();
|
||||
}
|
||||
|
||||
timerIdTabAutoAdvance.current = setTimeout(() => {
|
||||
// Activity check interval 설정 (매 100ms마다 체크)
|
||||
if (activityCheckIntervalRef.current) {
|
||||
clearInterval(activityCheckIntervalRef.current);
|
||||
}
|
||||
|
||||
let elapsedTime = 0;
|
||||
|
||||
activityCheckIntervalRef.current = setInterval(() => {
|
||||
// 활동이 없을 때만 경과 시간 증가
|
||||
if (isInactive()) {
|
||||
elapsedTime += 100;
|
||||
dlog('[PlayerPanel] ⏱️ TabIndex auto-advance: inactive', {
|
||||
elapsedTime,
|
||||
requiredTime: timeout,
|
||||
});
|
||||
|
||||
// 필요한 시간만큼 경과했으면 타이머 실행
|
||||
if (elapsedTime >= timeout) {
|
||||
dlog('[PlayerPanel] ✅ TabIndex auto-advance executing - setTabIndexV2(2)', {
|
||||
totalElapsed: elapsedTime,
|
||||
timeout,
|
||||
});
|
||||
clearInterval(activityCheckIntervalRef.current);
|
||||
setTabIndexV2(2);
|
||||
}, timeout);
|
||||
}
|
||||
} else {
|
||||
// 활동이 감지되면 경과 시간 리셋
|
||||
dlog('[PlayerPanel] 🔄 Activity detected - resetting elapsed time', {
|
||||
previousElapsed: elapsedTime,
|
||||
});
|
||||
elapsedTime = 0;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
[clearTimerTabAutoAdvance]
|
||||
[clearTimerTabAutoAdvance, isInactive]
|
||||
);
|
||||
|
||||
// Redux로 오버레이 숨김
|
||||
@@ -2629,6 +2750,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
if (timerIdTabAutoAdvance.current) {
|
||||
clearTimerTabAutoAdvance();
|
||||
}
|
||||
if (activityCheckIntervalRef.current) {
|
||||
clearInterval(activityCheckIntervalRef.current);
|
||||
activityCheckIntervalRef.current = null;
|
||||
}
|
||||
|
||||
dispatch(resetPlayerOverlays());
|
||||
}
|
||||
@@ -2843,6 +2968,53 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
panelInfo?.modal,
|
||||
]);
|
||||
|
||||
// PageUp/PageDown으로 비디오 변경 시 현재 재생 배너로 포커스 이동
|
||||
useEffect(() => {
|
||||
if (tabContainerVersion === 2 &&
|
||||
tabIndexV2 === 1 &&
|
||||
panelInfo?.isIndicatorByClick &&
|
||||
selectedIndex !== null &&
|
||||
selectedIndex >= 0) {
|
||||
|
||||
dlog('[PlayerPanel] 🎯 PageUp/PageDown 후 포커스 이동 준비', {
|
||||
selectedIndex,
|
||||
tabContainerVersion,
|
||||
tabIndexV2,
|
||||
isIndicatorByClick: panelInfo.isIndicatorByClick
|
||||
});
|
||||
|
||||
const bannerSpotlightId = `banner${selectedIndex}`;
|
||||
|
||||
setTimeout(() => {
|
||||
dlog('[PlayerPanel] 🔍 포커스 이동 시도:', bannerSpotlightId);
|
||||
|
||||
const bannerElement = document.querySelector(`[data-spotlight-id="${bannerSpotlightId}"]`);
|
||||
|
||||
if (bannerElement) {
|
||||
dlog('[PlayerPanel] ✅ 배너 요소 찾음, 포커스 이동 실행');
|
||||
Spotlight.focus(bannerElement);
|
||||
} else {
|
||||
dlog('[PlayerPanel] ⚠️ 배너 요소 찾지 못함:', bannerSpotlightId);
|
||||
// 모든 배너 요소 목록 출력
|
||||
const allBanners = document.querySelectorAll('[data-spotlight-id^="banner"]');
|
||||
dlog('[PlayerPanel] 🔍 사용 가능한 배너 목록:',
|
||||
Array.from(allBanners).map(el => el.getAttribute('data-spotlight-id'))
|
||||
);
|
||||
}
|
||||
|
||||
// 플래그 리셋
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
panelInfo: {
|
||||
isIndicatorByClick: false
|
||||
},
|
||||
})
|
||||
);
|
||||
}, 200); // DOM 업데이트 대기
|
||||
}
|
||||
}, [selectedIndex, tabContainerVersion, tabIndexV2, panelInfo?.isIndicatorByClick, dispatch]);
|
||||
|
||||
// TabIndex 1 자동 다음 단계로 이동
|
||||
useEffect(() => {
|
||||
// tabIndex === 1일 때만 실행
|
||||
@@ -2869,6 +3041,31 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
clearTimerTabAutoAdvance,
|
||||
]);
|
||||
|
||||
// Activity detection for tabIndex auto-advance (mousemove, keydown, click)
|
||||
useEffect(() => {
|
||||
// tabIndex === 1일 때만 Activity 감지 활성화
|
||||
if (tabIndexV2 !== 1 || !belowContentsVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlog('[PlayerPanel] 🎙️ Activity listener registered for tabIndex=1');
|
||||
|
||||
const handleMouseMove = onActivityDetected;
|
||||
const handleKeyDown = onActivityDetected;
|
||||
const handleClick = onActivityDetected;
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
dlog('[PlayerPanel] 🎙️ Activity listener unregistered');
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, [tabIndexV2, belowContentsVisible, onActivityDetected]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const videoContainer = document.querySelector(`.${css.videoContainer}`);
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ import { compose } from 'ramda/src/compose';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { Marquee, MarqueeController } from '@enact/ui/Marquee';
|
||||
import {
|
||||
Marquee,
|
||||
MarqueeController,
|
||||
} from '@enact/ui/Marquee';
|
||||
|
||||
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import icon_arrow_dwon
|
||||
from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import { SpotlightIds } from '../../../../utils/SpotlightIds';
|
||||
import css from './LiveChannelNext.module.less';
|
||||
@@ -18,7 +22,7 @@ export default function LiveChannelNext({
|
||||
channelLogo,
|
||||
channelName = 'ShopLC',
|
||||
programName = 'Sandal Black...',
|
||||
backgroundColor = 'linear-gradient(180deg, #284998 0%, #06B0EE 100%)',
|
||||
backgroundColor = 'transparent',
|
||||
onClick,
|
||||
onFocus,
|
||||
spotlightId = 'live-channel-next-button',
|
||||
@@ -54,7 +58,6 @@ export default function LiveChannelNext({
|
||||
<div className={css.logoWrapper}>
|
||||
<div
|
||||
className={css.logoBackground}
|
||||
style={{ background: backgroundColor }}
|
||||
>
|
||||
{channelLogo ? (
|
||||
<CustomImage
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
height: 72px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.logoBackground {
|
||||
@@ -51,6 +52,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.logoImage {
|
||||
@@ -60,6 +62,7 @@
|
||||
&.qvcLogoImg {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,19 @@ export default function ShopNowButton({ onClick }) {
|
||||
};
|
||||
|
||||
const handleSpotlightDown = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기
|
||||
Spotlight.focus('live-channel-next-button');
|
||||
};
|
||||
|
||||
const handleSpotlightLeft = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기
|
||||
Spotlight.focus('player-subtitlebutton');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<SpottableDiv
|
||||
@@ -31,6 +39,7 @@ export default function ShopNowButton({ onClick }) {
|
||||
spotlightId="below-tab-shop-now-button"
|
||||
onSpotlightUp={handleSpotlightUp}
|
||||
onSpotlightDown={handleSpotlightDown}
|
||||
onSpotlightLeft={handleSpotlightLeft}
|
||||
>
|
||||
<span className={css.buttonText}>SHOP NOW</span>
|
||||
</SpottableDiv>
|
||||
|
||||
@@ -196,7 +196,7 @@ export default function TabContainerV2({
|
||||
// tabIndex = 2 (ShopNowButton)
|
||||
const timeoutId = setTimeout(() => {
|
||||
Spotlight.focus('below-tab-shop-now-button');
|
||||
}, 100);
|
||||
}, 10);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user