Compare commits
8 Commits
backup-202
...
ec76d2cfc9
| Author | SHA1 | Date | |
|---|---|---|---|
| ec76d2cfc9 | |||
| a7161b8a80 | |||
| 13e32298a7 | |||
| 5ef0d8afae | |||
| f6073d78c1 | |||
|
|
0223499e12 | ||
| 3fd3b66cb3 | |||
| be9b1faeec |
@@ -840,9 +840,11 @@ const VideoPlayerBase = class extends React.Component {
|
||||
this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
|
||||
this.state.loading === nextState.loading &&
|
||||
this.props.loading === nextProps.loading &&
|
||||
(this.state.currentTime !== nextState.currentTime ||
|
||||
this.state.proportionPlayed !== nextState.proportionPlayed ||
|
||||
this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
|
||||
this.state.currentTime === nextState.currentTime &&
|
||||
this.state.proportionPlayed === nextState.proportionPlayed &&
|
||||
this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
|
||||
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
|
||||
this.state.bottomControlsRendered === nextState.bottomControlsRendered
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -1279,15 +1281,12 @@ const VideoPlayerBase = class extends React.Component {
|
||||
sourceUnavailable: true,
|
||||
proportionPlayed: 0,
|
||||
proportionLoaded: 0,
|
||||
bottomControlsRendered: true,
|
||||
});
|
||||
|
||||
if (!this.props.noAutoShowMediaControls) {
|
||||
if (!this.state.bottomControlsRendered) {
|
||||
this.renderBottomControl.idle();
|
||||
} else {
|
||||
this.showControls();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play());
|
||||
|
||||
@@ -855,9 +855,11 @@ const VideoPlayerBase = class extends React.Component {
|
||||
this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
|
||||
this.state.loading === nextState.loading &&
|
||||
this.props.loading === nextProps.loading &&
|
||||
(this.state.currentTime !== nextState.currentTime ||
|
||||
this.state.proportionPlayed !== nextState.proportionPlayed ||
|
||||
this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
|
||||
this.state.currentTime === nextState.currentTime &&
|
||||
this.state.proportionPlayed === nextState.proportionPlayed &&
|
||||
this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
|
||||
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
|
||||
this.state.bottomControlsRendered === nextState.bottomControlsRendered
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -1423,15 +1425,12 @@ const VideoPlayerBase = class extends React.Component {
|
||||
sourceUnavailable: true,
|
||||
proportionPlayed: 0,
|
||||
proportionLoaded: 0,
|
||||
bottomControlsRendered: true,
|
||||
});
|
||||
|
||||
if (!this.props.noAutoShowMediaControls) {
|
||||
if (!this.state.bottomControlsRendered) {
|
||||
this.renderBottomControl.idle();
|
||||
} else {
|
||||
this.showControls();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () => this.play());
|
||||
@@ -1636,103 +1635,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
updatedState.thumbnailUrl = null;
|
||||
}
|
||||
this.setState(updatedState);
|
||||
|
||||
// Redux에 비디오 재생 상태 업데이트 (기존 로직 유지)
|
||||
if (this.props.dispatch) {
|
||||
// 🔥 onProgress 이벤트는 Redux 업데이트하지 않음 (빈번한 이벤트)
|
||||
const shouldUpdateRedux = !['onProgress'].includes(ev.type);
|
||||
|
||||
if (shouldUpdateRedux) {
|
||||
const updateState = {
|
||||
isPlaying: !updatedState.paused,
|
||||
isPaused: updatedState.paused,
|
||||
currentTime: updatedState.currentTime,
|
||||
duration: updatedState.duration,
|
||||
playbackRate: updatedState.playbackRate,
|
||||
};
|
||||
|
||||
// 가장 중요한 이벤트만 로그
|
||||
const shouldLogEvent = ['play', 'pause', 'ended'].includes(ev.type);
|
||||
if (shouldLogEvent) {
|
||||
dlog('🔄 [PlayerPanel][VideoPlayer] Event-driven Redux update', {
|
||||
eventType: ev.type,
|
||||
videoState: updatedState,
|
||||
updateState,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// 🔍 Redux dispatch 확인
|
||||
dlog('📤 [PlayerPanel][VideoPlayer] Dispatching Redux update', {
|
||||
eventType: ev.type,
|
||||
updateState,
|
||||
hasDispatch: !!this.props.dispatch,
|
||||
propsVideoPlayState: this.props.videoPlayState,
|
||||
});
|
||||
|
||||
this.props.dispatch(updateVideoPlayState(updateState));
|
||||
}
|
||||
} else {
|
||||
derror('❌ [PlayerPanel][VideoPlayer] No dispatch prop available', {
|
||||
props: Object.keys(this.props),
|
||||
hasDispatch: !!this.props.dispatch,
|
||||
hasVideoPlayState: !!this.props.videoPlayState,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔹 [강화] 내부 상태와 Redux 상태 동기화
|
||||
// Redux 상태를 우선적으로 사용하여 내부 상태 일관성 확보
|
||||
if (this.props.videoPlayState && typeof this.props.videoPlayState === 'object') {
|
||||
// Redux 상태 디버깅 (최소한의 중요 이벤트만)
|
||||
if (ev.type === 'play' || ev.type === 'pause') {
|
||||
dlog('🔍 [PlayerPanel][VideoPlayer] Redux state debug', {
|
||||
videoPlayState: this.props.videoPlayState,
|
||||
isPaused: this.props.videoPlayState?.isPaused,
|
||||
isPlaying: this.props.videoPlayState?.isPlaying,
|
||||
currentTime: this.props.videoPlayState?.currentTime,
|
||||
eventType: ev.type,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
const { currentTime, paused, playbackRate } = this.props.videoPlayState;
|
||||
|
||||
// Redux 상태와 현재 내부 상태가 크게 다를 경우 내부 상태 업데이트
|
||||
const timeDiff = Math.abs(currentTime - this.state.currentTime);
|
||||
const shouldUpdateTime = timeDiff > 0.5; // 0.5초 이상 차이 시 업데이트
|
||||
|
||||
// 빈번한 이벤트는 로그에서 제외
|
||||
const isFrequentEvent = [
|
||||
'onProgress',
|
||||
'onBuffer',
|
||||
'onBufferEnd',
|
||||
'onReady',
|
||||
'onDuration',
|
||||
'onStart',
|
||||
].includes(ev.type);
|
||||
const hasSignificantChange =
|
||||
shouldUpdateTime || (paused !== this.state.paused && !isFrequentEvent);
|
||||
|
||||
// 중요한 상태 변화가 있고 빈번한 이벤트가 아닐 때만 로그
|
||||
if (hasSignificantChange && !isFrequentEvent) {
|
||||
dlog('🔄 [PlayerPanel][VideoPlayer] Syncing internal state with Redux', {
|
||||
timeDiff,
|
||||
shouldUpdateTime,
|
||||
pausedDiff: paused !== this.state.paused,
|
||||
reduxPaused: paused,
|
||||
internalPaused: this.state.paused,
|
||||
eventType: ev.type,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasSignificantChange) {
|
||||
this.setState({
|
||||
currentTime: shouldUpdateTime ? currentTime : this.state.currentTime,
|
||||
paused: paused !== undefined ? paused : this.state.paused,
|
||||
playbackRate: playbackRate !== undefined ? playbackRate : this.state.playbackRate,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderBottomControl = new Job(() => {
|
||||
@@ -1744,7 +1646,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
/**
|
||||
* Returns an object with the current state of the media including `currentTime`, `duration`,
|
||||
* `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
|
||||
* Redux 상태와 내부 상태를 우선적으로 사용하여 일관성 보장
|
||||
*
|
||||
* @function
|
||||
* @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
|
||||
@@ -1752,19 +1653,13 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
getMediaState = () => {
|
||||
// Redux 상태를 우선적으로 사용하여 일관성 보장
|
||||
// Redux 상태가 없으면 내부 상태 사용 (fallback)
|
||||
const reduxState = this.props.videoPlayState;
|
||||
|
||||
return {
|
||||
currentTime: reduxState?.currentTime ?? this.state.currentTime,
|
||||
duration: reduxState?.duration ?? this.state.duration,
|
||||
paused: reduxState?.isPaused ?? this.state.paused,
|
||||
playbackRate: reduxState?.playbackRate ?? this.video?.playbackRate ?? this.state.playbackRate,
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
paused: this.state.paused,
|
||||
playbackRate: this.video?.playbackRate,
|
||||
proportionLoaded: this.state.proportionLoaded,
|
||||
proportionPlayed: this.state.proportionPlayed,
|
||||
// Redux 상태 정보도 포함
|
||||
isPlaying: reduxState?.isPlaying ?? !this.state.paused,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1793,16 +1688,7 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
play = () => {
|
||||
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
paused: this.state.paused,
|
||||
sourceUnavailable: this.state.sourceUnavailable,
|
||||
prevCommand: this.prevCommand,
|
||||
});
|
||||
|
||||
if (this.state.sourceUnavailable) {
|
||||
dwarn('⚠️ [PlayerPanel][VideoPlayer] play() aborted - source unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1814,19 +1700,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
this.send('play');
|
||||
this.announce($L('Play'));
|
||||
this.startDelayedMiniFeedbackHide(5000);
|
||||
|
||||
// Redux 상태 업데이트 - 재생 상태로 변경
|
||||
if (this.props.dispatch) {
|
||||
this.props.dispatch(
|
||||
updateVideoPlayState({
|
||||
isPlaying: true,
|
||||
isPaused: false,
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
playbackRate: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1837,16 +1710,7 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
pause = () => {
|
||||
dlog('🔴 [VideoPlayer] pause() called', {
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
paused: this.state.paused,
|
||||
sourceUnavailable: this.state.sourceUnavailable,
|
||||
prevCommand: this.prevCommand,
|
||||
});
|
||||
|
||||
if (this.state.sourceUnavailable) {
|
||||
dwarn('⚠️ [VideoPlayer] pause() aborted - source unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1858,22 +1722,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
this.send('pause');
|
||||
this.announce($L('Pause'));
|
||||
this.stopDelayedMiniFeedbackHide();
|
||||
|
||||
// Redux 상태 업데이트 - 일시정지 상태로 변경
|
||||
if (this.props.dispatch) {
|
||||
const pauseState = {
|
||||
isPlaying: false,
|
||||
isPaused: true,
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
playbackRate: 1,
|
||||
};
|
||||
|
||||
dlog('📤 [VideoPlayer] Dispatching pause state', pauseState);
|
||||
this.props.dispatch(updateVideoPlayState(pauseState));
|
||||
} else {
|
||||
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1885,15 +1733,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
seek = (timeIndex) => {
|
||||
dlog('⏩ [VideoPlayer] seek() called', {
|
||||
timeIndex,
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
videoDuration: this.video?.duration,
|
||||
seekDisabled: this.props.seekDisabled,
|
||||
sourceUnavailable: this.state.sourceUnavailable,
|
||||
});
|
||||
|
||||
if (this.video) {
|
||||
if (
|
||||
!this.props.seekDisabled &&
|
||||
@@ -1904,34 +1743,9 @@ const VideoPlayerBase = class extends React.Component {
|
||||
const actualSeekTime =
|
||||
timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex;
|
||||
this.video.currentTime = actualSeekTime;
|
||||
|
||||
dlog('⏩ [VideoPlayer] Video seek completed', {
|
||||
requestedTime: timeIndex,
|
||||
actualTime: actualSeekTime,
|
||||
videoDuration: this.video.duration,
|
||||
});
|
||||
|
||||
// Redux 상태 업데이트 - 시간 이동 상태 반영
|
||||
if (this.props.dispatch) {
|
||||
const seekState = {
|
||||
isPlaying: !this.state.paused,
|
||||
isPaused: this.state.paused,
|
||||
currentTime: actualSeekTime,
|
||||
duration: this.state.duration,
|
||||
playbackRate: this.state.playbackRate,
|
||||
};
|
||||
|
||||
dlog('📤 [VideoPlayer] Dispatching seek state', seekState);
|
||||
this.props.dispatch(updateVideoPlayState(seekState));
|
||||
} else {
|
||||
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
|
||||
}
|
||||
} else {
|
||||
derror('❌ [VideoPlayer] seek failed - disabled or source unavailable');
|
||||
forward('onSeekFailed', {}, this.props);
|
||||
}
|
||||
} else {
|
||||
derror('❌ [VideoPlayer] seek failed - no video element');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -1248,12 +1248,12 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
return () => {
|
||||
// 패널이 2개 존재할때만 popPanel 진행
|
||||
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
|
||||
console.log('[PP-TRACE] cleanup start', {
|
||||
modal: panelInfo.modal,
|
||||
isOnTop,
|
||||
topPanel: panels[panels.length - 1]?.name,
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
// console.log('[PP-TRACE] cleanup start', {
|
||||
// modal: panelInfo.modal,
|
||||
// isOnTop,
|
||||
// topPanel: panels[panels.length - 1]?.name,
|
||||
// stack: panels.map((p) => p.name),
|
||||
// });
|
||||
|
||||
// 🔽 [251221] PlayerPanel unmount 시 DeepLink 플래그 리셋
|
||||
dispatch(
|
||||
@@ -1272,7 +1272,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
topPanelName === panel_names.PLAYER_PANEL &&
|
||||
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());
|
||||
} else {
|
||||
Spotlight.focus('tbody');
|
||||
@@ -1845,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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -2345,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) {
|
||||
@@ -2393,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) {
|
||||
@@ -2535,14 +2536,105 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
);
|
||||
|
||||
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();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (ev.keyCode === 33) {
|
||||
dlog('[PlayerPanel] 📺 PageDown (버튼 또는 다른 경우) -> 다음 비디오');
|
||||
} else if (ev.keyCode === 33) { // PageUp
|
||||
handleIndicatorUpClick();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
dlog('[PlayerPanel] 📺 PageUp (버튼 또는 다른 경우) -> 이전 비디오');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2553,6 +2645,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 &&
|
||||
@@ -2664,17 +2761,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로 오버레이 숨김
|
||||
@@ -2699,6 +2841,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
if (timerIdTabAutoAdvance.current) {
|
||||
clearTimerTabAutoAdvance();
|
||||
}
|
||||
if (activityCheckIntervalRef.current) {
|
||||
clearInterval(activityCheckIntervalRef.current);
|
||||
activityCheckIntervalRef.current = null;
|
||||
}
|
||||
|
||||
dispatch(resetPlayerOverlays());
|
||||
}
|
||||
@@ -2913,6 +3059,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일 때만 실행
|
||||
@@ -2939,6 +3132,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,8 @@ import { useDispatch } from 'react-redux';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
||||
// <<<<<<< HEAD
|
||||
import { updatePanel } from '../../../../actions/panelActions';
|
||||
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import TScrollerLiveChannel from './TScrollerLiveChannel';
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MENU,
|
||||
@@ -20,26 +19,11 @@ import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContent
|
||||
import css from './LiveChannelContents.module.less';
|
||||
import cssV2 from './LiveChannelContents.v2.module.less';
|
||||
|
||||
// =======
|
||||
// import { updatePanel } from "../../../../actions/panelActions";
|
||||
// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
|
||||
// import {
|
||||
// LOG_CONTEXT_NAME,
|
||||
// LOG_MENU,
|
||||
// LOG_MESSAGE_ID,
|
||||
// panel_names,
|
||||
// } from "../../../../utils/Config";
|
||||
// import { $L } from "../../../../utils/helperMethods";
|
||||
// import PlayerItemCard, { TYPES } from "../../PlayerItemCard/PlayerItemCard";
|
||||
// import ListEmptyContents from "../TabContents/ListEmptyContents/ListEmptyContents";
|
||||
// import css from "./LiveChannelContents.module.less";
|
||||
// import { sendLogTotalRecommend } from "../../../../actions/logActions";
|
||||
// >>>>>>> gitlab/develop
|
||||
|
||||
export default function LiveChannelContents({
|
||||
liveInfos,
|
||||
currentTime,
|
||||
setSelectedIndex,
|
||||
selectedIndex,
|
||||
videoVerticalVisible,
|
||||
currentVideoShowId,
|
||||
tabIndex,
|
||||
@@ -83,6 +67,19 @@ export default function LiveChannelContents({
|
||||
}
|
||||
}, [isFilteredByPatnr19]);
|
||||
|
||||
// currentVideoShowId 변경 시 해당 배너가 보이도록 스크롤
|
||||
// (LiveChannelButton에서 PageUp/PageDown으로 동영상 변경 시)
|
||||
// currentVideoShowId 기반으로 스크롤하면 포커스 이동 없이 배너만 화면에 보임
|
||||
useEffect(() => {
|
||||
if (currentVideoShowId && liveInfos && liveInfos.length > 0 && scrollToRef.current) {
|
||||
// currentVideoShowId와 일치하는 배너의 인덱스 찾기
|
||||
const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId);
|
||||
if (index !== -1) {
|
||||
scrollToRef.current({ index, animate: true, focus: false });
|
||||
}
|
||||
}
|
||||
}, [currentVideoShowId, liveInfos]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index, ...rest }) => {
|
||||
const {
|
||||
@@ -222,7 +219,7 @@ export default function LiveChannelContents({
|
||||
<>
|
||||
<div className={containerClass}>
|
||||
{liveInfos && liveInfos.length > 0 ? (
|
||||
<TVirtualGridList
|
||||
<TScrollerLiveChannel
|
||||
cbScrollTo={handleScrollTo}
|
||||
dataSize={liveInfos.length}
|
||||
direction={direction}
|
||||
@@ -230,7 +227,6 @@ export default function LiveChannelContents({
|
||||
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
|
||||
itemHeight={version === 2 ? 155 : 236}
|
||||
spacing={version === 2 ? 30 : 12}
|
||||
noScrollByWheel={false}
|
||||
/>
|
||||
) : (
|
||||
<ListEmptyContents tabIndex={tabIndex} />
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { scaleH, scaleW } from '../../../../utils/helperMethods';
|
||||
import css from './TScrollerLiveChannel.module.less';
|
||||
|
||||
/**
|
||||
* TScrollerLiveChannel - Live Channel용 간단한 스크롤 컴포넌트
|
||||
*
|
||||
* TVirtualGridList의 가상화 대신 모든 아이템을 DOM에 렌더링
|
||||
* 20개 미만의 아이템에 최적화되어 있음
|
||||
*
|
||||
* @param {number} dataSize - 아이템 개수
|
||||
* @param {string} direction - 'horizontal' 또는 'vertical'
|
||||
* @param {function} renderItem - 아이템 렌더링 함수 ({ index })
|
||||
* @param {number} itemWidth - 아이템 너비
|
||||
* @param {number} itemHeight - 아이템 높이
|
||||
* @param {number} spacing - 아이템 간 간격
|
||||
* @param {function} cbScrollTo - 스크롤 함수를 받을 콜백
|
||||
* @param {string} className - 추가 CSS 클래스
|
||||
* @param {string} spotlightId - Spotlight 포커스 ID prefix
|
||||
*/
|
||||
export default function TScrollerLiveChannel({
|
||||
dataSize,
|
||||
direction = 'horizontal',
|
||||
renderItem,
|
||||
itemWidth,
|
||||
itemHeight,
|
||||
spacing,
|
||||
cbScrollTo,
|
||||
className,
|
||||
spotlightId,
|
||||
}) {
|
||||
const scrollContainerRef = useRef(null);
|
||||
const itemsRef = useRef([]);
|
||||
|
||||
// 스크롤 컨테이너 크기 계산
|
||||
const containerStyle = useMemo(() => {
|
||||
if (direction === 'horizontal') {
|
||||
return {
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
width: '100%',
|
||||
height: scaleH(itemHeight),
|
||||
alignItems: 'center',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
width: '100%',
|
||||
};
|
||||
}
|
||||
}, [direction, itemHeight]);
|
||||
|
||||
// 아이템 래퍼 스타일 계산
|
||||
const itemsWrapperStyle = useMemo(() => {
|
||||
if (direction === 'horizontal') {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: scaleW(spacing),
|
||||
padding: `0 ${scaleW(spacing)}px`,
|
||||
alignItems: 'center',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: scaleH(spacing),
|
||||
padding: `${scaleH(spacing)}px 0`,
|
||||
};
|
||||
}
|
||||
}, [direction, spacing]);
|
||||
|
||||
// 스크롤 함수 생성
|
||||
const scrollToIndex = useCallback(
|
||||
(index, options = {}) => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container || !itemsRef.current[index]) return;
|
||||
|
||||
const item = itemsRef.current[index];
|
||||
const { animate = true } = options;
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
// 수평 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
|
||||
const itemLeft = item.offsetLeft;
|
||||
const itemWidth = item.offsetWidth;
|
||||
const containerWidth = container.clientWidth;
|
||||
|
||||
// 다음 아이템도 일부 보일 수 있도록 스크롤
|
||||
// 현재 아이템 + 다음 아이템의 일부가 보이는 위치로 스크롤
|
||||
const nextItem = itemsRef.current[index + 1];
|
||||
let 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);
|
||||
|
||||
if (animate) {
|
||||
container.scrollTo({
|
||||
left: scrollLeft,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
} else {
|
||||
container.scrollLeft = scrollLeft;
|
||||
}
|
||||
} else {
|
||||
// 수직 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
|
||||
const itemTop = item.offsetTop;
|
||||
const itemHeight = item.offsetHeight;
|
||||
const containerHeight = container.clientHeight;
|
||||
|
||||
// 다음 아이템도 일부 보일 수 있도록 스크롤
|
||||
const nextItem = itemsRef.current[index + 1];
|
||||
let 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);
|
||||
|
||||
if (animate) {
|
||||
container.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
} else {
|
||||
container.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
},
|
||||
[direction, spacing]
|
||||
);
|
||||
|
||||
// TVirtualGridList와 호환되는 콜백 인터페이스 제공
|
||||
useEffect(() => {
|
||||
if (cbScrollTo) {
|
||||
cbScrollTo((options) => {
|
||||
const { index, animate = true, focus = true } = options;
|
||||
if (typeof index === 'number' && index >= 0 && index < dataSize) {
|
||||
scrollToIndex(index, { animate });
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [cbScrollTo, scrollToIndex, dataSize]);
|
||||
|
||||
// 아이템 ref 할당 함수
|
||||
const setItemRef = useCallback((el, index) => {
|
||||
if (el) {
|
||||
itemsRef.current[index] = el;
|
||||
} else {
|
||||
delete itemsRef.current[index];
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 포커스된 아이템을 화면에 완전히 보이도록 스크롤
|
||||
const handleItemFocus = useCallback(
|
||||
(index) => {
|
||||
const container = scrollContainerRef.current;
|
||||
const item = itemsRef.current[index];
|
||||
|
||||
if (!container || !item) return;
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
const itemLeft = item.offsetLeft;
|
||||
const itemWidth = item.offsetWidth;
|
||||
const containerWidth = container.clientWidth;
|
||||
const containerScrollLeft = container.scrollLeft;
|
||||
|
||||
// 아이템이 완전히 보이는지 확인
|
||||
const itemRight = itemLeft + itemWidth;
|
||||
const containerRight = containerScrollLeft + containerWidth;
|
||||
|
||||
// 아이템이 왼쪽으로 밖에 나가 있으면 왼쪽 끝에 맞춤
|
||||
if (itemLeft < containerScrollLeft) {
|
||||
container.scrollLeft = itemLeft - scaleW(spacing);
|
||||
}
|
||||
// 아이템이 오른쪽으로 밖에 나가 있으면 오른쪽 끝에 맞춤
|
||||
else if (itemRight > containerRight) {
|
||||
container.scrollLeft = itemRight - containerWidth + scaleW(spacing);
|
||||
}
|
||||
} else {
|
||||
const itemTop = item.offsetTop;
|
||||
const itemHeight = item.offsetHeight;
|
||||
const containerHeight = container.clientHeight;
|
||||
const containerScrollTop = container.scrollTop;
|
||||
|
||||
// 아이템이 완전히 보이는지 확인
|
||||
const itemBottom = itemTop + itemHeight;
|
||||
const containerBottom = containerScrollTop + containerHeight;
|
||||
|
||||
// 아이템이 위로 밖에 나가 있으면 위쪽 끝에 맞춤
|
||||
if (itemTop < containerScrollTop) {
|
||||
container.scrollTop = itemTop - scaleH(spacing);
|
||||
}
|
||||
// 아이템이 아래로 밖에 나가 있으면 아래쪽 끝에 맞춤
|
||||
else if (itemBottom > containerBottom) {
|
||||
container.scrollTop = itemBottom - containerHeight + scaleH(spacing);
|
||||
}
|
||||
}
|
||||
},
|
||||
[direction, spacing]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(css.tScrollerLiveChannelContainer, className)}
|
||||
style={containerStyle}
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
<div className={css.itemsWrapper} style={itemsWrapperStyle}>
|
||||
{Array.from({ length: dataSize }).map((_, index) => (
|
||||
<div
|
||||
key={`item-${index}`}
|
||||
ref={(el) => setItemRef(el, index)}
|
||||
className={css.item}
|
||||
style={{
|
||||
width: direction === 'horizontal' ? scaleW(itemWidth) : 'auto',
|
||||
height: direction === 'horizontal' ? 'auto' : scaleH(itemHeight),
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onFocus={() => handleItemFocus(index)}
|
||||
>
|
||||
{renderItem({ index })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
.tScrollerLiveChannelContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 스크롤바 스타일
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.itemsWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// 포커스 상태 처리
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user