Compare commits
15 Commits
backup-202
...
fd55c04c83
| Author | SHA1 | Date | |
|---|---|---|---|
| fd55c04c83 | |||
| 30472bfe17 | |||
|
|
51d587a1a1 | ||
|
|
349688092c | ||
| d933ca6bb7 | |||
| e86b56e14e | |||
|
|
eee8e73b97 | ||
| 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.mediaSliderVisible === nextState.mediaSliderVisible &&
|
||||||
this.state.loading === nextState.loading &&
|
this.state.loading === nextState.loading &&
|
||||||
this.props.loading === nextProps.loading &&
|
this.props.loading === nextProps.loading &&
|
||||||
(this.state.currentTime !== nextState.currentTime ||
|
this.state.currentTime === nextState.currentTime &&
|
||||||
this.state.proportionPlayed !== nextState.proportionPlayed ||
|
this.state.proportionPlayed === nextState.proportionPlayed &&
|
||||||
this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
|
this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
|
||||||
|
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
|
||||||
|
this.state.bottomControlsRendered === nextState.bottomControlsRendered
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1279,14 +1281,11 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
sourceUnavailable: true,
|
sourceUnavailable: true,
|
||||||
proportionPlayed: 0,
|
proportionPlayed: 0,
|
||||||
proportionLoaded: 0,
|
proportionLoaded: 0,
|
||||||
|
bottomControlsRendered: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.props.noAutoShowMediaControls) {
|
if (!this.props.noAutoShowMediaControls) {
|
||||||
if (!this.state.bottomControlsRendered) {
|
this.showControls();
|
||||||
this.renderBottomControl.idle();
|
|
||||||
} else {
|
|
||||||
this.showControls();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -779,6 +779,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.times {
|
.times {
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
<Overlay bottomControlsVisible={controlsVisible} onClick={handleVideoClick}>
|
<Overlay bottomControlsVisible={controlsVisible} onClick={handleVideoClick}>
|
||||||
{/* Loading + Thumbnail */}
|
{/* Loading + Thumbnail */}
|
||||||
{loading && thumbnailUrl && (
|
{/* {loading && thumbnailUrl && (
|
||||||
<>
|
<>
|
||||||
<p className={classNames(css.thumbnail, isModal && css.smallThumbnail)}>
|
<p className={classNames(css.thumbnail, isModal && css.smallThumbnail)}>
|
||||||
<img src={thumbnailUrl} alt="" />
|
<img src={thumbnailUrl} alt="" />
|
||||||
@@ -648,7 +648,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* Controls with MediaSlider */}
|
{/* Controls with MediaSlider */}
|
||||||
{controlsVisible && !isModal && (
|
{controlsVisible && !isModal && (
|
||||||
|
|||||||
@@ -855,9 +855,11 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
|
this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
|
||||||
this.state.loading === nextState.loading &&
|
this.state.loading === nextState.loading &&
|
||||||
this.props.loading === nextProps.loading &&
|
this.props.loading === nextProps.loading &&
|
||||||
(this.state.currentTime !== nextState.currentTime ||
|
this.state.currentTime === nextState.currentTime &&
|
||||||
this.state.proportionPlayed !== nextState.proportionPlayed ||
|
this.state.proportionPlayed === nextState.proportionPlayed &&
|
||||||
this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
|
this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
|
||||||
|
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
|
||||||
|
this.state.bottomControlsRendered === nextState.bottomControlsRendered
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1423,14 +1425,11 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
sourceUnavailable: true,
|
sourceUnavailable: true,
|
||||||
proportionPlayed: 0,
|
proportionPlayed: 0,
|
||||||
proportionLoaded: 0,
|
proportionLoaded: 0,
|
||||||
|
bottomControlsRendered: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.props.noAutoShowMediaControls) {
|
if (!this.props.noAutoShowMediaControls) {
|
||||||
if (!this.state.bottomControlsRendered) {
|
this.showControls();
|
||||||
this.renderBottomControl.idle();
|
|
||||||
} else {
|
|
||||||
this.showControls();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1636,103 +1635,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
updatedState.thumbnailUrl = null;
|
updatedState.thumbnailUrl = null;
|
||||||
}
|
}
|
||||||
this.setState(updatedState);
|
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(() => {
|
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`,
|
* Returns an object with the current state of the media including `currentTime`, `duration`,
|
||||||
* `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
|
* `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
|
||||||
* Redux 상태와 내부 상태를 우선적으로 사용하여 일관성 보장
|
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
* @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
|
* @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
|
||||||
@@ -1752,19 +1653,13 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getMediaState = () => {
|
getMediaState = () => {
|
||||||
// Redux 상태를 우선적으로 사용하여 일관성 보장
|
|
||||||
// Redux 상태가 없으면 내부 상태 사용 (fallback)
|
|
||||||
const reduxState = this.props.videoPlayState;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTime: reduxState?.currentTime ?? this.state.currentTime,
|
currentTime: this.state.currentTime,
|
||||||
duration: reduxState?.duration ?? this.state.duration,
|
duration: this.state.duration,
|
||||||
paused: reduxState?.isPaused ?? this.state.paused,
|
paused: this.state.paused,
|
||||||
playbackRate: reduxState?.playbackRate ?? this.video?.playbackRate ?? this.state.playbackRate,
|
playbackRate: this.video?.playbackRate,
|
||||||
proportionLoaded: this.state.proportionLoaded,
|
proportionLoaded: this.state.proportionLoaded,
|
||||||
proportionPlayed: this.state.proportionPlayed,
|
proportionPlayed: this.state.proportionPlayed,
|
||||||
// Redux 상태 정보도 포함
|
|
||||||
isPlaying: reduxState?.isPlaying ?? !this.state.paused,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1793,16 +1688,7 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
play = () => {
|
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) {
|
if (this.state.sourceUnavailable) {
|
||||||
dwarn('⚠️ [PlayerPanel][VideoPlayer] play() aborted - source unavailable');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1814,19 +1700,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
this.send('play');
|
this.send('play');
|
||||||
this.announce($L('Play'));
|
this.announce($L('Play'));
|
||||||
this.startDelayedMiniFeedbackHide(5000);
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
pause = () => {
|
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) {
|
if (this.state.sourceUnavailable) {
|
||||||
dwarn('⚠️ [VideoPlayer] pause() aborted - source unavailable');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1858,22 +1722,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
this.send('pause');
|
this.send('pause');
|
||||||
this.announce($L('Pause'));
|
this.announce($L('Pause'));
|
||||||
this.stopDelayedMiniFeedbackHide();
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
seek = (timeIndex) => {
|
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.video) {
|
||||||
if (
|
if (
|
||||||
!this.props.seekDisabled &&
|
!this.props.seekDisabled &&
|
||||||
@@ -1904,34 +1743,9 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
const actualSeekTime =
|
const actualSeekTime =
|
||||||
timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex;
|
timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex;
|
||||||
this.video.currentTime = actualSeekTime;
|
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 {
|
} else {
|
||||||
derror('❌ [VideoPlayer] seek failed - disabled or source unavailable');
|
|
||||||
forward('onSeekFailed', {}, this.props);
|
forward('onSeekFailed', {}, this.props);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
derror('❌ [VideoPlayer] seek failed - no video element');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -695,6 +695,7 @@
|
|||||||
height: 70px;
|
height: 70px;
|
||||||
width:1800px;
|
width:1800px;
|
||||||
margin-left:60px;
|
margin-left:60px;
|
||||||
|
margin-right: 59px;
|
||||||
bottom:92px;
|
bottom:92px;
|
||||||
> *:first-child {
|
> *:first-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -26,17 +27,35 @@ export default function UserReviewDetail({
|
|||||||
onNext,
|
onNext,
|
||||||
className,
|
className,
|
||||||
}) {
|
}) {
|
||||||
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||||
|
|
||||||
|
// 새로운 리뷰가 로드될 때 이미지 인덱스 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentImageIndex(0);
|
||||||
|
}, [currentReview]);
|
||||||
|
|
||||||
|
const reviewImages = currentReview?.reviewImageList || [];
|
||||||
|
const hasMultipleImages = reviewImages.length > 1;
|
||||||
|
|
||||||
const handlePrevious = useCallback(() => {
|
const handlePrevious = useCallback(() => {
|
||||||
if (onPrevious && currentIndex > 0) {
|
// 이미지가 여러 개이고 현재 이미지가 첫 번째가 아니면 이미지만 변경
|
||||||
|
if (hasMultipleImages && currentImageIndex > 0) {
|
||||||
|
setCurrentImageIndex(prev => prev - 1);
|
||||||
|
} else if (onPrevious && currentIndex > 0) {
|
||||||
|
// 이미지가 첫 번째이면 이전 리뷰로 이동
|
||||||
onPrevious();
|
onPrevious();
|
||||||
}
|
}
|
||||||
}, [onPrevious, currentIndex]);
|
}, [onPrevious, currentIndex, hasMultipleImages, currentImageIndex]);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
if (onNext && currentIndex < totalReviews - 1) {
|
// 이미지가 여러 개이고 현재 이미지가 마지막이 아니면 이미지만 변경
|
||||||
|
if (hasMultipleImages && currentImageIndex < reviewImages.length - 1) {
|
||||||
|
setCurrentImageIndex(prev => prev + 1);
|
||||||
|
} else if (onNext && currentIndex < totalReviews - 1) {
|
||||||
|
// 이미지가 마지막이면 다음 리뷰로 이동
|
||||||
onNext();
|
onNext();
|
||||||
}
|
}
|
||||||
}, [onNext, currentIndex, totalReviews]);
|
}, [onNext, currentIndex, totalReviews, hasMultipleImages, currentImageIndex, reviewImages.length]);
|
||||||
|
|
||||||
// 리뷰 데이터가 없을 때 처리
|
// 리뷰 데이터가 없을 때 처리
|
||||||
if (!currentReview) {
|
if (!currentReview) {
|
||||||
@@ -47,9 +66,7 @@ export default function UserReviewDetail({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewImage =
|
const reviewImage = reviewImages[currentImageIndex];
|
||||||
currentReview.reviewImageList && currentReview.reviewImageList[0];
|
|
||||||
const hasMultipleReviews = totalReviews > 1;
|
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
const [year, month, day] = dateStr.split("-");
|
const [year, month, day] = dateStr.split("-");
|
||||||
@@ -59,7 +76,7 @@ export default function UserReviewDetail({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Left Arrow - 이전 리뷰가 있을 때만 표시 */}
|
{/* Left Arrow - 이전 리뷰가 있을 때만 표시 */}
|
||||||
{hasMultipleReviews && currentIndex > 0 && (
|
{(hasMultipleImages || currentIndex > 0) && (
|
||||||
<SpottableButton
|
<SpottableButton
|
||||||
className={css.leftArrow}
|
className={css.leftArrow}
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
@@ -128,7 +145,7 @@ export default function UserReviewDetail({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Right Arrow - 다음 리뷰가 있을 때만 표시 */}
|
{/* Right Arrow - 다음 리뷰가 있을 때만 표시 */}
|
||||||
{hasMultipleReviews && currentIndex < totalReviews - 1 && (
|
{(hasMultipleImages || currentIndex < totalReviews - 1) && (
|
||||||
<SpottableButton
|
<SpottableButton
|
||||||
className={css.rightArrow}
|
className={css.rightArrow}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.enrgLbImg {
|
.enrgLbImg {
|
||||||
width:62px;
|
width:70px;
|
||||||
border:4px solid transparent;
|
border:4px solid transparent;
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 4px solid @PRIMARY_COLOR_RED;
|
border: 4px solid @PRIMARY_COLOR_RED;
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export default function ThemeContents({
|
|||||||
handleItemFocus?.(index);
|
handleItemFocus?.(index);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => Spotlight.focus(spotlightItemId)}
|
onMouseEnter={() => Spotlight.focus(spotlightItemId)}
|
||||||
|
euEnrgLblInfos={euEnrgLblInfos}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function ThemeItemCard({
|
|||||||
spotlightId,
|
spotlightId,
|
||||||
dataSpotlightDefault,
|
dataSpotlightDefault,
|
||||||
onFocused,
|
onFocused,
|
||||||
|
euEnrgLblInfos
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
@@ -211,9 +212,9 @@ export default function ThemeItemCard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
{/* {mockEnergyLabel && mockEnergyLabel.length > 0 && (
|
{euEnrgLblInfos && euEnrgLblInfos.length > 0 && (
|
||||||
<div className={css.energyLabels}>
|
<div className={css.energyLabels}>
|
||||||
{mockEnergyLabel.map((label, labelIndex) => (
|
{euEnrgLblInfos.map((label, labelIndex) => (
|
||||||
<SpottableTemp
|
<SpottableTemp
|
||||||
key={labelIndex}
|
key={labelIndex}
|
||||||
spotlightDisabled={Boolean(!cursorVisible)}
|
spotlightDisabled={Boolean(!cursorVisible)}
|
||||||
@@ -228,7 +229,7 @@ export default function ThemeItemCard({
|
|||||||
</SpottableTemp>
|
</SpottableTemp>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SpottableDiv>
|
</SpottableDiv>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|||||||
@@ -220,3 +220,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.energyImage {
|
||||||
|
max-height:800px;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,9 +1,464 @@
|
|||||||
import React from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import PlayerOverlayContents from './PlayerOverlayContents';
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
useDispatch,
|
||||||
|
useSelector,
|
||||||
|
} from 'react-redux';
|
||||||
|
|
||||||
function MediaOverlayContents(props) {
|
import Spotlight from '@enact/spotlight';
|
||||||
return <PlayerOverlayContents {...props} forceShowMediaOverlay />;
|
import SpotlightContainerDecorator
|
||||||
|
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
import Marquee from '@enact/ui/Marquee';
|
||||||
|
|
||||||
|
import defaultLogoImg
|
||||||
|
from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||||
|
import { setShowPopup } from '../../../actions/commonActions';
|
||||||
|
import CustomImage from '../../../components/CustomImage/CustomImage';
|
||||||
|
import { ACTIVE_POPUP } from '../../../utils/Config';
|
||||||
|
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||||
|
import PlayerTabButton from '../PlayerTabContents/TabButton/PlayerTabButton';
|
||||||
|
import css from './MediaOverlayContents.module.less';
|
||||||
|
|
||||||
|
const SpottableBtn = Spottable('button');
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||||
|
|
||||||
|
function MediaOverlayContents({
|
||||||
|
type,
|
||||||
|
onClick,
|
||||||
|
panelInfo,
|
||||||
|
disclaimer,
|
||||||
|
playListInfo,
|
||||||
|
captionEnable,
|
||||||
|
selectedIndex,
|
||||||
|
setIsSubtitleActive,
|
||||||
|
videoVerticalVisible,
|
||||||
|
sideContentsVisible,
|
||||||
|
setSideContentsVisible,
|
||||||
|
belowContentsVisible,
|
||||||
|
handleIndicatorUpClick,
|
||||||
|
handleIndicatorDownClick,
|
||||||
|
tabContainerVersion,
|
||||||
|
tabIndexV2,
|
||||||
|
}) {
|
||||||
|
const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const onClickBack = (ev) => {
|
||||||
|
// TabContainerV2가 표시된 상태에서 백버튼 클릭 시 이벤트 버블링 방지
|
||||||
|
// (Overlay의 onClick으로 전파되어 toggleControls()가 호출되는 것을 막음)
|
||||||
|
if (tabContainerVersion === 2 && belowContentsVisible) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
onClick(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const backBtnRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type === 'MEDIA' && !panelInfo.modal && backBtnRef.current) {
|
||||||
|
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
|
||||||
|
}
|
||||||
|
}, [type, panelInfo.modal, backBtnRef]);
|
||||||
|
|
||||||
|
const handleSubtitleOnClick = useCallback(() => {
|
||||||
|
if (!captionEnable) {
|
||||||
|
return dispatch(setShowPopup(ACTIVE_POPUP.alertPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubtitleActive((prev) => !prev);
|
||||||
|
}, [dispatch, captionEnable, setIsSubtitleActive]);
|
||||||
|
|
||||||
|
const patncLogoPath = useMemo(() => {
|
||||||
|
let logo = playListInfo[selectedIndex]?.patncLogoPath;
|
||||||
|
if (type === 'MEDIA') {
|
||||||
|
logo = panelInfo?.patncLogoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return logo;
|
||||||
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
|
const partnerName = useMemo(() => {
|
||||||
|
let name = playListInfo[selectedIndex]?.patncNm;
|
||||||
|
if (type === 'MEDIA') {
|
||||||
|
name = panelInfo?.patncNm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
|
const showName = useMemo(() => {
|
||||||
|
let name = playListInfo[selectedIndex]?.showNm;
|
||||||
|
if (type === 'MEDIA') {
|
||||||
|
name = panelInfo?.showNm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name ? name.replace(/<br\s*\/?>/gi, ' ') : '';
|
||||||
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
|
const onSpotlightMoveTabButton = useCallback((e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSpotlightMoveMediaButton = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// TabContainerV2의 tabIndex=2일 때 하단 버튼들로 포커스 이동
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
if (Spotlight.focus('live-channel-next-button')) return;
|
||||||
|
if (Spotlight.focus('below-tab-shop-now-button')) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'LIVE') {
|
||||||
|
return Spotlight.focus('videoIndicator-down-button');
|
||||||
|
}
|
||||||
|
return Spotlight.focus(SpotlightIds.PLAYER_PLAY_BUTTON);
|
||||||
|
},
|
||||||
|
[type, tabContainerVersion, tabIndexV2]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpotlightMoveSubtitleButton = useCallback(
|
||||||
|
() => {
|
||||||
|
// 1. 먼저 자막 버튼으로 포커스 시도
|
||||||
|
if (Spotlight.focus('player-subtitlebutton')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabContainerV2의 tabIndex=2일 때 TabContainerV2 버튼들로 포커스 이동
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
let focusSuccessful = false;
|
||||||
|
|
||||||
|
// 먼저 LiveChannelNext 버튼으로 시도
|
||||||
|
if (Spotlight.focus('live-channel-next-button')) {
|
||||||
|
focusSuccessful = true;
|
||||||
|
}
|
||||||
|
// 실패하면 ShopNowButton으로 시도
|
||||||
|
else if (Spotlight.focus('below-tab-shop-now-button')) {
|
||||||
|
focusSuccessful = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusSuccessful) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 동작: 자막 버튼으로 포커스
|
||||||
|
return Spotlight.focus('player-subtitlebutton');
|
||||||
|
},
|
||||||
|
[tabContainerVersion, tabIndexV2]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpotlightMoveSlider = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (type === 'VOD') {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
Spotlight.focus(SpotlightIds.PLAYER_SLIDER);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpotlightMoveSideTab = useCallback((e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
Spotlight.focus('tab-0');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSpotlightMoveBelowTab = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log(`[onSpotlightMoveBelowTab] tabIndexV2: ${tabIndexV2}`);
|
||||||
|
|
||||||
|
// tabIndexV2에 따라 다른 버튼으로 포커스 이동
|
||||||
|
if (tabIndexV2 === 0) {
|
||||||
|
// ShopNow 탭: Close 버튼으로
|
||||||
|
// Spotlight.focus('below-tab-close-button');
|
||||||
|
const result = Spotlight.focus('shownow_close_button');
|
||||||
|
console.log(`[onSpotlightMoveBelowTab] tabIndexV2=0, focus result:`, result);
|
||||||
|
} else if (tabIndexV2 === 1) {
|
||||||
|
// LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로
|
||||||
|
const result = Spotlight.focus('below-tab-live-channel-button');
|
||||||
|
console.log(`[onSpotlightMoveBelowTab] tabIndexV2=1, focus result:`, result);
|
||||||
|
} else if (tabIndexV2 === 2) {
|
||||||
|
// ShopNowButton: ShopNowButton으로
|
||||||
|
const result = Spotlight.focus('below-tab-shop-now-button');
|
||||||
|
console.log(`[onSpotlightMoveBelowTab] tabIndexV2=2, focus result:`, result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tabIndexV2]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Back Button arrow down 전용 핸들러 - tabIndex에 따라 다른 포커스
|
||||||
|
const handleBackButtonDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (tabContainerVersion === 2 && belowContentsVisible) {
|
||||||
|
if (tabIndexV2 === 0) {
|
||||||
|
// tabIndexV2가 0일 때 ShopNow 닫기 버튼으로 포커스
|
||||||
|
const result = Spotlight.focus('shownow_close_button');
|
||||||
|
} else if (tabIndexV2 === 1) {
|
||||||
|
// tabIndexV2가 1일 때 below-tab-live-channel-button으로 포커스
|
||||||
|
Spotlight.focus('below-tab-live-channel-button');
|
||||||
|
} else if (tabIndexV2 === 2) {
|
||||||
|
// tabIndexV2가 2일 때 LiveChannelNext로 포커스
|
||||||
|
Spotlight.focus('live-channel-next-button');
|
||||||
|
} else {
|
||||||
|
// 그 외에는 기존 로직 사용
|
||||||
|
onSpotlightMoveMediaButton(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onSpotlightMoveMediaButton(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tabContainerVersion, belowContentsVisible, tabIndexV2, onSpotlightMoveMediaButton]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpotlightMoveBackButton = useCallback(() => {
|
||||||
|
return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOverlayKeyDownCapture = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
const currentId = Spotlight.getCurrent()?.getAttribute('data-spotlight-id');
|
||||||
|
if (ev.keyCode === 40 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
return handleBackButtonDown(ev);
|
||||||
|
}
|
||||||
|
if (ev.keyCode === 39 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
return onSpotlightMoveSubtitleButton(ev);
|
||||||
|
}
|
||||||
|
if (ev.keyCode === 37 && currentId === SpotlightIds.PLAYER_SUBTITLE_BUTTON) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
return onSpotlightMoveBackButton(ev);
|
||||||
|
}
|
||||||
|
if (ev.keyCode === 38 && currentId === SpotlightIds.PLAYER_PLAY_BUTTON) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
return onSpotlightMoveBackButton();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSpotlightMoveBackButton, onSpotlightMoveMediaButton, onSpotlightMoveSubtitleButton, handleBackButtonDown]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSideButtonStatus = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!panelInfo?.modal &&
|
||||||
|
!sideContentsVisible &&
|
||||||
|
tabContainerVersion === 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [panelInfo, sideContentsVisible, tabContainerVersion]);
|
||||||
|
|
||||||
|
const noLiveContentsVisible = useMemo(() => {
|
||||||
|
if (!Array.isArray(playListInfo) || playListInfo.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noShowIdCount = playListInfo.filter((item) => !item.showId).length;
|
||||||
|
if (playListInfo.length - 1 === noShowIdCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [playListInfo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container className={css.overlayContainer} onKeyDownCapture={handleOverlayKeyDownCapture}>
|
||||||
|
{/* 251118 임시로 unvisible */}
|
||||||
|
{/* {playListInfo.length > 1 && noLiveContentsVisible && (
|
||||||
|
<>
|
||||||
|
<div className={css.indicatorUpButton}>
|
||||||
|
<SpottableBtn
|
||||||
|
onClick={handleIndicatorUpClick}
|
||||||
|
spotlightId="videoIndicator-up-button"
|
||||||
|
onSpotlightRight={
|
||||||
|
videoVerticalVisible
|
||||||
|
? onSpotlightMoveSideTab
|
||||||
|
: tabContainerVersion === 1
|
||||||
|
? onSpotlightMoveTabButton
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSpotlightDown={
|
||||||
|
tabContainerVersion === 2 && belowContentsVisible
|
||||||
|
? onSpotlightMoveBelowTab
|
||||||
|
: onSpotlightMoveSlider
|
||||||
|
}
|
||||||
|
aria-label="Previous channel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={css.indicatorDownButton}>
|
||||||
|
<SpottableBtn
|
||||||
|
onClick={handleIndicatorDownClick}
|
||||||
|
spotlightId="videoIndicator-down-button"
|
||||||
|
onSpotlightLeft={onSpotlightMoveSlider}
|
||||||
|
onSpotlightUp={onSpotlightMoveSlider}
|
||||||
|
onSpotlightRight={
|
||||||
|
videoVerticalVisible
|
||||||
|
? onSpotlightMoveSideTab
|
||||||
|
: tabContainerVersion === 1
|
||||||
|
? onSpotlightMoveTabButton
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSpotlightDown={
|
||||||
|
tabContainerVersion === 2 && belowContentsVisible
|
||||||
|
? onSpotlightMoveBelowTab
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-label="Next channel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{currentSideButtonStatus && !videoVerticalVisible && (
|
||||||
|
<PlayerTabButton
|
||||||
|
setSideContentsVisible={setSideContentsVisible}
|
||||||
|
sideContentsVisible={sideContentsVisible}
|
||||||
|
onSpotlightLeft={
|
||||||
|
playListInfo.length < 2 && onSpotlightMoveBackButton
|
||||||
|
}
|
||||||
|
videoType={type}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cntry_cd === 'US' && (
|
||||||
|
<div className={css.videoButtonContainer}>
|
||||||
|
<SpottableBtn
|
||||||
|
className={classNames(
|
||||||
|
css.subtitleButton,
|
||||||
|
videoVerticalVisible && css.videoVericalSubtitleButton
|
||||||
|
)}
|
||||||
|
onClick={handleSubtitleOnClick}
|
||||||
|
spotlightId="player-subtitlebutton"
|
||||||
|
onSpotlightUp={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// tabIndexV2가 2일 때만 ShopNowButton으로 포커스
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
Spotlight.focus('below-tab-shop-now-button');
|
||||||
|
} else {
|
||||||
|
onSpotlightMoveBackButton();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSpotlightLeft={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
Spotlight.focus('live-channel-next-button');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSpotlightRight={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
Spotlight.focus('below-tab-shop-now-button');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSpotlightDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// tabIndexV2가 2일 때만 ShopNowButton으로 포커스
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
Spotlight.focus('live-channel-next-button');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Caption"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={css.overlayHeader}>
|
||||||
|
<SpottableBtn
|
||||||
|
onClick={onClickBack}
|
||||||
|
className={css.backIcon}
|
||||||
|
spotlightId="player-back-button"
|
||||||
|
onSpotlightDown={handleBackButtonDown}
|
||||||
|
onSpotlightRight={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// tabIndexV2가 2일 때만 ShopNowButton으로 포커스
|
||||||
|
if (tabContainerVersion === 2 && tabIndexV2 === 2) {
|
||||||
|
Spotlight.focus('below-tab-shop-now-button');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSpotlightUp={onSpotlightMoveSubtitleButton}
|
||||||
|
aria-label="Video Player Close"
|
||||||
|
ref={backBtnRef}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classNames(type === 'LIVE' && css.liveIcon)}
|
||||||
|
aria-label={type === 'LIVE' && 'Live Icon'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{partnerName && (
|
||||||
|
<CustomImage
|
||||||
|
src={patncLogoPath}
|
||||||
|
fallbackSrc={defaultLogoImg}
|
||||||
|
alt={partnerName}
|
||||||
|
aria-label={partnerName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className={css.patnerName}>{partnerName}</h2>
|
||||||
|
|
||||||
|
{!panelInfo?.modal && (
|
||||||
|
<Marquee
|
||||||
|
className={classNames(css.title, videoVerticalVisible && css.videoVerticalMarquee)}
|
||||||
|
marqueeOn="render"
|
||||||
|
>
|
||||||
|
{showName}
|
||||||
|
</Marquee>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
{type === 'VOD' && disclaimer && (
|
||||||
|
<div className={css.disclaimer}>
|
||||||
|
<span className={css.icon} />
|
||||||
|
<h3 aria-label={disclaimer}>{disclaimer}</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MediaOverlayContents;
|
const propsAreEqual = (prev, next) => {
|
||||||
|
return (
|
||||||
|
prev.type === next.type &&
|
||||||
|
prev.panelInfo?.showId === next.panelInfo?.showId &&
|
||||||
|
prev.disclaimer === next.disclaimer &&
|
||||||
|
prev.playListInfo === next.playListInfo &&
|
||||||
|
prev.captionEnable === next.captionEnable &&
|
||||||
|
prev.selectedIndex === next.selectedIndex &&
|
||||||
|
prev.videoVerticalVisible === next.videoVerticalVisible &&
|
||||||
|
prev.sideContentsVisible === next.sideContentsVisible &&
|
||||||
|
prev.belowContentsVisible === next.belowContentsVisible &&
|
||||||
|
prev.tabContainerVersion === next.tabContainerVersion &&
|
||||||
|
prev.tabIndexV2 === next.tabIndexV2
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(MediaOverlayContents, propsAreEqual);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1248,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(
|
||||||
@@ -1272,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');
|
||||||
@@ -1845,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);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -2345,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) {
|
||||||
@@ -2393,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) {
|
||||||
@@ -2535,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 (버튼 또는 다른 경우) -> 이전 비디오');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2553,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 &&
|
||||||
@@ -2664,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마다 체크)
|
||||||
setTabIndexV2(2);
|
if (activityCheckIntervalRef.current) {
|
||||||
}, timeout);
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 활동이 감지되면 경과 시간 리셋
|
||||||
|
dlog('[PlayerPanel] 🔄 Activity detected - resetting elapsed time', {
|
||||||
|
previousElapsed: elapsedTime,
|
||||||
|
});
|
||||||
|
elapsedTime = 0;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
},
|
},
|
||||||
[clearTimerTabAutoAdvance]
|
[clearTimerTabAutoAdvance, isInactive]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Redux로 오버레이 숨김
|
// Redux로 오버레이 숨김
|
||||||
@@ -2699,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());
|
||||||
}
|
}
|
||||||
@@ -2913,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일 때만 실행
|
||||||
@@ -2939,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}`);
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { useDispatch } from 'react-redux';
|
|||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
|
|
||||||
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
||||||
// <<<<<<< HEAD
|
|
||||||
import { updatePanel } from '../../../../actions/panelActions';
|
import { updatePanel } from '../../../../actions/panelActions';
|
||||||
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
import TScrollerLiveChannel from './TScrollerLiveChannel';
|
||||||
import {
|
import {
|
||||||
LOG_CONTEXT_NAME,
|
LOG_CONTEXT_NAME,
|
||||||
LOG_MENU,
|
LOG_MENU,
|
||||||
@@ -20,26 +19,11 @@ import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContent
|
|||||||
import css from './LiveChannelContents.module.less';
|
import css from './LiveChannelContents.module.less';
|
||||||
import cssV2 from './LiveChannelContents.v2.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({
|
export default function LiveChannelContents({
|
||||||
liveInfos,
|
liveInfos,
|
||||||
currentTime,
|
currentTime,
|
||||||
setSelectedIndex,
|
setSelectedIndex,
|
||||||
|
selectedIndex,
|
||||||
videoVerticalVisible,
|
videoVerticalVisible,
|
||||||
currentVideoShowId,
|
currentVideoShowId,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
@@ -83,6 +67,19 @@ export default function LiveChannelContents({
|
|||||||
}
|
}
|
||||||
}, [isFilteredByPatnr19]);
|
}, [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(
|
const renderItem = useCallback(
|
||||||
({ index, ...rest }) => {
|
({ index, ...rest }) => {
|
||||||
const {
|
const {
|
||||||
@@ -222,7 +219,7 @@ export default function LiveChannelContents({
|
|||||||
<>
|
<>
|
||||||
<div className={containerClass}>
|
<div className={containerClass}>
|
||||||
{liveInfos && liveInfos.length > 0 ? (
|
{liveInfos && liveInfos.length > 0 ? (
|
||||||
<TVirtualGridList
|
<TScrollerLiveChannel
|
||||||
cbScrollTo={handleScrollTo}
|
cbScrollTo={handleScrollTo}
|
||||||
dataSize={liveInfos.length}
|
dataSize={liveInfos.length}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
@@ -230,7 +227,6 @@ export default function LiveChannelContents({
|
|||||||
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
|
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
|
||||||
itemHeight={version === 2 ? 155 : 236}
|
itemHeight={version === 2 ? 155 : 236}
|
||||||
spacing={version === 2 ? 30 : 12}
|
spacing={version === 2 ? 30 : 12}
|
||||||
noScrollByWheel={false}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ListEmptyContents tabIndex={tabIndex} />
|
<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 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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user