8 Commits

Author SHA1 Message Date
5ef0d8afae [251217] merge: gitlab develop_si 변경사항 병합 2025-12-17 14:21:27 +09:00
f6073d78c1 [251217] fix: LiveChannelContents Navigation
🕐 커밋 시간: 2025. 12. 17. 14:17:24

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 14:17:24 +09:00
junghoon86.park
0223499e12 [영상]
- 라이브 채널 next부분 관련해서 버튼부분의 배경 색상 제거
2025-12-17 13:46:23 +09:00
3fd3b66cb3 [251217] fix: PlayerPanel activity check 추가
🕐 커밋 시간: 2025. 12. 17. 13:43:33

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

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

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-12-17 13:43:33 +09:00
be9b1faeec [251217] fix: 비디오배너 클릭 방어로직추가
🕐 커밋 시간: 2025. 12. 17. 12:11:04

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +10줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx
2025-12-17 12:11:04 +09:00
07a042cca6 [251217] fix: PlayerPanel 배너동영상 위치 검증추가
🕐 커밋 시간: 2025. 12. 17. 12:03:44

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +70줄

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 12:03:44 +09:00
junghoon86.park
d93960f40a [영상]
- 프로그레스바 크기변경
 - 타임 노출부분 변경
 - cc 버튼 위치변경
2025-12-17 10:38:36 +09:00
4dfa15b4c0 [251217] fix: TabContainer.v2.jsx ShopNowButton 포커스 10ms
🕐 커밋 시간: 2025. 12. 17. 09:13:23

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx
2025-12-17 09:13:23 +09:00
11 changed files with 346 additions and 29 deletions

View File

@@ -34,7 +34,7 @@
padding: @slider-padding-v 0; padding: @slider-padding-v 0;
height: @sand-mediaplayer-slider-height; height: @sand-mediaplayer-slider-height;
right: 154px; right: 154px;
width: 1466px; width: 1558px;
// Add a tap area that extends to the edges of the screen, to make the slider more accessible // Add a tap area that extends to the edges of the screen, to make the slider more accessible
&::before { &::before {
content: ""; content: "";

View File

@@ -7,7 +7,7 @@
position: absolute; position: absolute;
font-family: @baseFont; font-family: @baseFont;
width: 100%; width: 100%;
right: 90px; right: 20px;
bottom: -5px; bottom: -5px;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@@ -16,12 +16,12 @@
letter-spacing: -1px; letter-spacing: -1px;
.separator { .separator {
position: absolute; position: absolute;
right: 105px; right: 95px;
bottom: -5px; bottom: -5px;
} }
.currentTime { .currentTime {
position: absolute; position: absolute;
right: 130px; right: 120px;
bottom: -5px; bottom: -5px;
} }
.totalTime { .totalTime {

View File

@@ -76,6 +76,10 @@ export default function ProductDescription({ productInfo }) {
const _onClose = useCallback(()=>{ const _onClose = useCallback(()=>{
dispatch(setHidePopup()); dispatch(setHidePopup());
// Restore focus to the description content after popup closes
setTimeout(() => {
Spotlight.focus('product-description-content');
}, 100);
},[dispatch]) },[dispatch])
// ProductDescription: Container 직접 사용 패턴 // ProductDescription: Container 직접 사용 패턴

View File

@@ -617,6 +617,15 @@ export default function RandomUnit({
// 비디오 클릭 // 비디오 클릭
const videoClick = useCallback(() => { 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 lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
if (lastFocusedTargetId) { if (lastFocusedTargetId) {
@@ -674,6 +683,7 @@ export default function RandomUnit({
sendBannerLog, sendBannerLog,
onBlur, onBlur,
playerPanelInfo?.modal, playerPanelInfo?.modal,
currentVideoBannerId,
dispatch, dispatch,
handleStartVideo, handleStartVideo,
]); ]);

View File

@@ -390,7 +390,7 @@ function PlayerOverlayContents({
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스 // tabIndexV2가 2일 때만 LiveChannelNext로 포커스
if (tabContainerVersion === 2 && tabIndexV2 === 2) { if (tabContainerVersion === 2 && tabIndexV2 === 2) {
Spotlight.focus('live-channel-next-button'); Spotlight.focus('below-tab-shop-now-button');
} }
}} }}
onSpotlightDown={(e) => { onSpotlightDown={(e) => {

View File

@@ -13,8 +13,8 @@
background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png"); background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png");
background-size: cover; background-size: cover;
position: absolute; position: absolute;
right: 60px; right: 300px;
top: 800px; top: 680px;
z-index: 10; z-index: 10;
&.videoVericalSubtitleButton { &.videoVericalSubtitleButton {

View File

@@ -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) => { const getLogTpNo = (type, nowMenu) => {
if (type === 'LIVE') { if (type === 'LIVE') {
switch (nowMenu) { 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 [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton
const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단) const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단)
const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그 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 panels = USE_SELECTOR('panels', (state) => state.panels.panels);
const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData); const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData);
@@ -1202,12 +1248,12 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return () => { return () => {
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 2개 존재할때만 popPanel 진행
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지) // 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
console.log('[PP-TRACE] cleanup start', { // console.log('[PP-TRACE] cleanup start', {
modal: panelInfo.modal, // modal: panelInfo.modal,
isOnTop, // isOnTop,
topPanel: panels[panels.length - 1]?.name, // topPanel: panels[panels.length - 1]?.name,
stack: panels.map((p) => p.name), // stack: panels.map((p) => p.name),
}); // });
// 🔽 [251221] PlayerPanel unmount 시 DeepLink 플래그 리셋 // 🔽 [251221] PlayerPanel unmount 시 DeepLink 플래그 리셋
dispatch( dispatch(
@@ -1226,7 +1272,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
topPanelName === panel_names.PLAYER_PANEL && topPanelName === panel_names.PLAYER_PANEL &&
panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지) panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지)
) { ) {
console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)'); // console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
@@ -1799,6 +1845,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (watchIntervalLive.current) clearInterval(watchIntervalLive.current); if (watchIntervalLive.current) clearInterval(watchIntervalLive.current);
if (watchIntervalVod.current) clearInterval(watchIntervalVod.current); if (watchIntervalVod.current) clearInterval(watchIntervalVod.current);
if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.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 }, 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 { } else {
dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle'); dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle');
setModalStyle(panelInfo.modalStyle); setModalStyle(panelInfo.modalStyle);
@@ -2275,7 +2346,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
const handleIndicatorUpClick = useCallback(() => { const handleIndicatorUpClick = useCallback(() => {
if (!initialEnter) { if (!initialEnter) {
@@ -2323,7 +2394,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
useEffect(() => { useEffect(() => {
if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) { if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) {
@@ -2465,14 +2536,105 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
const onKeyDown = (ev) => { const onKeyDown = (ev) => {
if (ev.keyCode === 34) { // tabIndex === 1 (LiveChannelContents 표시)이고 비디오 배너에 포커스가 있는 경우
const currentFocused = Spotlight.getCurrent();
const spotlightId = currentFocused?.getAttribute('data-spotlight-id');
const isVideoItemFocused = spotlightId?.startsWith('tabChannel-video');
// LiveChannelContents의 비디오 배너에 포커스가 있는 경우: PageUp/PageDown을 좌우 이동으로 변환
if (tabIndexV2 === 1 && isVideoItemFocused) {
// DOM에서 실제로 렌더링된 모든 비디오 배너 찾기 (가상화 대응)
const allVideoBanners = Array.from(
document.querySelectorAll('[data-spotlight-id^="tabChannel-video-"]')
);
if (allVideoBanners.length > 0) {
// 현재 포커스된 배너의 인덱스 찾기
const currentBannerIndex = allVideoBanners.findIndex(
(el) => el === currentFocused
);
if (currentBannerIndex !== -1) {
if (ev.keyCode === 34) { // PageDown -> 오른쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 다음 배너로 이동 (마지막이면 무시 또는 첫 번째로)
if (currentBannerIndex < allVideoBanners.length - 1) {
// 다음 배너가 DOM에 있으면 이동
const nextBanner = allVideoBanners[currentBannerIndex + 1];
const nextSpotlightId = nextBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (비디오 배너) -> 오른쪽으로 이동', {
current: spotlightId,
next: nextSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(nextSpotlightId);
} else {
// 마지막 배너면 첫 번째로 이동 시도 (DOM에 있으면)
const firstBanner = allVideoBanners[0];
const firstSpotlightId = firstBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (마지막 배너) -> 첫 번째 배너로 이동 시도', {
current: spotlightId,
next: firstSpotlightId,
isWrapAround: true,
});
Spotlight.focus(firstSpotlightId);
}
return;
} else if (ev.keyCode === 33) { // PageUp -> 왼쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 이전 배너로 이동 (첫 번째면 무시 또는 마지막으로)
if (currentBannerIndex > 0) {
// 이전 배너가 DOM에 있으면 이동
const prevBanner = allVideoBanners[currentBannerIndex - 1];
const prevSpotlightId = prevBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (비디오 배너) -> 왼쪽으로 이동', {
current: spotlightId,
prev: prevSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(prevSpotlightId);
} else {
// 첫 번째 배너면 마지막으로 이동 시도 (DOM에 있으면)
const lastBanner = allVideoBanners[allVideoBanners.length - 1];
const lastSpotlightId = lastBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (첫 번째 배너) -> 마지막 배너로 이동 시도', {
current: spotlightId,
prev: lastSpotlightId,
isWrapAround: true,
});
Spotlight.focus(lastSpotlightId);
}
return;
}
}
}
}
// 기존 로직: LiveChannelButton 또는 다른 경우에는 상/하 이동
if (ev.keyCode === 34) { // PageDown
handleIndicatorDownClick(); handleIndicatorDownClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} else if (ev.keyCode === 33) { dlog('[PlayerPanel] 📺 PageDown (버튼 또는 다른 경우) -> 다음 비디오');
} else if (ev.keyCode === 33) { // PageUp
handleIndicatorUpClick(); handleIndicatorUpClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
dlog('[PlayerPanel] 📺 PageUp (버튼 또는 다른 경우) -> 이전 비디오');
} }
}; };
@@ -2483,6 +2645,11 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const timerIdTabAutoAdvance = useRef(null); const timerIdTabAutoAdvance = useRef(null);
const prevTabIndexV2 = 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(() => { const showSideContents = useMemo(() => {
return ( return (
sideContentsVisible && sideContentsVisible &&
@@ -2594,17 +2761,62 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
timerIdTabAutoAdvance.current = null; 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( const resetTimerTabAutoAdvance = useCallback(
(timeout) => { (timeout) => {
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); 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); setTabIndexV2(2);
}, timeout); }
} else {
// 활동이 감지되면 경과 시간 리셋
dlog('[PlayerPanel] 🔄 Activity detected - resetting elapsed time', {
previousElapsed: elapsedTime,
});
elapsedTime = 0;
}
}, 100);
}, },
[clearTimerTabAutoAdvance] [clearTimerTabAutoAdvance, isInactive]
); );
// Redux로 오버레이 숨김 // Redux로 오버레이 숨김
@@ -2629,6 +2841,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); clearTimerTabAutoAdvance();
} }
if (activityCheckIntervalRef.current) {
clearInterval(activityCheckIntervalRef.current);
activityCheckIntervalRef.current = null;
}
dispatch(resetPlayerOverlays()); dispatch(resetPlayerOverlays());
} }
@@ -2843,6 +3059,53 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo?.modal, 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 자동 다음 단계로 이동 // TabIndex 1 자동 다음 단계로 이동
useEffect(() => { useEffect(() => {
// tabIndex === 1일 때만 실행 // tabIndex === 1일 때만 실행
@@ -2869,6 +3132,31 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
clearTimerTabAutoAdvance, 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(() => { useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`); const videoContainer = document.querySelector(`.${css.videoContainer}`);

View File

@@ -5,9 +5,13 @@ import { compose } from 'ramda/src/compose';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable'; 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 CustomImage from '../../../../components/CustomImage/CustomImage';
import { SpotlightIds } from '../../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../../utils/SpotlightIds';
import css from './LiveChannelNext.module.less'; import css from './LiveChannelNext.module.less';
@@ -18,7 +22,7 @@ export default function LiveChannelNext({
channelLogo, channelLogo,
channelName = 'ShopLC', channelName = 'ShopLC',
programName = 'Sandal Black...', programName = 'Sandal Black...',
backgroundColor = 'linear-gradient(180deg, #284998 0%, #06B0EE 100%)', backgroundColor = 'transparent',
onClick, onClick,
onFocus, onFocus,
spotlightId = 'live-channel-next-button', spotlightId = 'live-channel-next-button',
@@ -54,7 +58,6 @@ export default function LiveChannelNext({
<div className={css.logoWrapper}> <div className={css.logoWrapper}>
<div <div
className={css.logoBackground} className={css.logoBackground}
style={{ background: backgroundColor }}
> >
{channelLogo ? ( {channelLogo ? (
<CustomImage <CustomImage

View File

@@ -41,6 +41,7 @@
height: 72px; height: 72px;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
background-color: transparent;
} }
.logoBackground { .logoBackground {
@@ -51,6 +52,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: transparent;
} }
.logoImage { .logoImage {
@@ -60,6 +62,7 @@
&.qvcLogoImg { &.qvcLogoImg {
width: 70%; width: 70%;
height: 70%; height: 70%;
background-color: transparent;
} }
} }

View File

@@ -18,11 +18,19 @@ export default function ShopNowButton({ onClick }) {
}; };
const handleSpotlightDown = (e) => { const handleSpotlightDown = (e) => {
e.stopPropagation();
e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('live-channel-next-button');
};
const handleSpotlightLeft = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기 // tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('player-subtitlebutton'); Spotlight.focus('player-subtitlebutton');
}; };
return ( return (
<div className={css.container}> <div className={css.container}>
<SpottableDiv <SpottableDiv
@@ -31,6 +39,7 @@ export default function ShopNowButton({ onClick }) {
spotlightId="below-tab-shop-now-button" spotlightId="below-tab-shop-now-button"
onSpotlightUp={handleSpotlightUp} onSpotlightUp={handleSpotlightUp}
onSpotlightDown={handleSpotlightDown} onSpotlightDown={handleSpotlightDown}
onSpotlightLeft={handleSpotlightLeft}
> >
<span className={css.buttonText}>SHOP NOW</span> <span className={css.buttonText}>SHOP NOW</span>
</SpottableDiv> </SpottableDiv>

View File

@@ -196,7 +196,7 @@ export default function TabContainerV2({
// tabIndex = 2 (ShopNowButton) // tabIndex = 2 (ShopNowButton)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
Spotlight.focus('below-tab-shop-now-button'); Spotlight.focus('below-tab-shop-now-button');
}, 100); }, 10);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
}, []); }, []);