[251006] feat: views - PlayerPanel.jsx, BelowTabContainer.jsx, BelowTa...

🕐 커밋 시간: 2025. 10. 06. 16:40:08

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ChannelCard.jsx
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveChannelContents.jsx
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveChannelContents.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/BelowTabContainer.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/BelowTabContainer.module.less

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/BelowTabContainer.jsx (javascript):
     Deleted: handleChannelClick()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveChannelContents.module.less (unknown):
     Added: child()

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-10-06 16:40:10 +09:00
parent 64e48ef171
commit 66ba686f33
6 changed files with 283 additions and 108 deletions

View File

@@ -2059,39 +2059,16 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
{showBelowContents && (
<BelowTabContainer
channelList={[
{
channelName: 'QVC',
programName: 'JAI Jewelry',
logoSrc: 'https://via.placeholder.com/40',
thumbnailSrc: 'https://via.placeholder.com/212x119',
progressPercent: 65,
backgroundColor: '#FFFFFF',
},
{
channelName: 'ShopLC',
programName: 'JAI Jewelry',
logoSrc: 'https://via.placeholder.com/40',
thumbnailSrc: 'https://via.placeholder.com/212x119',
progressPercent: 65,
backgroundColor: '#FFFFFF',
},
{
channelName: 'HSN',
programName: 'JAI Jewelry',
logoSrc: 'https://via.placeholder.com/40',
thumbnailSrc: 'https://via.placeholder.com/212x119',
progressPercent: 65,
backgroundColor: '#1E77BC',
},
{
channelName: 'QVC',
programName: 'JAI Jewelry',
logoSrc: 'https://via.placeholder.com/40',
thumbnailSrc: 'https://via.placeholder.com/212x119',
progressPercent: 65,
backgroundColor: '#FFFFFF',
},
panelInfo={panelInfo}
liveInfos={playListInfo}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={playListInfo && playListInfo[selectedIndex]?.showId}
handleItemFocus={handleItemFocus}
tabTitle={[
$L('SHOP NOW'),
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'),
]}
/>
)}

View File

@@ -4,65 +4,23 @@ import classNames from 'classnames';
import Spottable from '@enact/spotlight/Spottable';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import css from './BelowTabContainer.module.less';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import LiveChannelContents from './LiveChannelContents';
const SpottableDiv = Spottable('div');
const ChannelCard = ({
channelName,
programName,
logoSrc,
thumbnailSrc,
progressPercent = 65,
backgroundColor = '#FFFFFF',
onFocus,
onBlur,
onClick,
spotlightId,
}) => {
return (
<SpottableDiv
className={css.channelCard}
onFocus={onFocus}
onBlur={onBlur}
onClick={onClick}
spotlightId={spotlightId}
>
<div className={css.thumbnail}>
<CustomImage src={thumbnailSrc} alt={channelName} />
</div>
<div className={css.cardContent}>
<div className={css.channelHeader}>
<div className={css.logoWrapper} style={{ background: backgroundColor }}>
<CustomImage src={logoSrc} alt={`${channelName} logo`} className={css.logo} />
</div>
<h3 className={css.channelName}>{channelName}</h3>
</div>
<div className={css.programName}>{programName}</div>
<div className={css.progressBarWrapper}>
<div className={css.progressBar}>
<div className={css.progressTrack} />
<div className={css.progressFill} style={{ width: `${progressPercent}%` }} />
</div>
</div>
</div>
</SpottableDiv>
);
};
export default function BelowTabContainer({
channelList = [],
onChannelClick,
liveInfos,
currentTime,
setSelectedIndex,
videoVerticalVisible,
currentVideoShowId,
handleItemFocus,
tabTitle,
panelInfo,
onLiveChannelButtonClick,
}) {
const handleChannelClick = (channel, index) => () => {
if (onChannelClick) {
onChannelClick(channel, index);
}
};
return (
<div className={css.container}>
<SpottableDiv
@@ -76,21 +34,16 @@ export default function BelowTabContainer({
</div>
</SpottableDiv>
<div className={css.channelList}>
{channelList.map((channel, index) => (
<ChannelCard
key={`channel-${index}`}
channelName={channel.channelName}
programName={channel.programName}
logoSrc={channel.logoSrc}
thumbnailSrc={channel.thumbnailSrc}
progressPercent={channel.progressPercent}
backgroundColor={channel.backgroundColor}
onClick={handleChannelClick(channel, index)}
spotlightId={`below-tab-channel-${index}`}
/>
))}
</div>
<LiveChannelContents
liveInfos={liveInfos}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={currentVideoShowId}
handleItemFocus={handleItemFocus}
tabTitle={tabTitle}
panelInfo={panelInfo}
/>
</div>
);
}

View File

@@ -15,8 +15,11 @@
border-top: 1px solid rgba(234, 234, 234, 0.3);
overflow: hidden;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start, @direction: column);
gap: 20px;
z-index: 5;
> * + * {
margin-top: 20px;
}
&::before {
content: "";
@@ -24,11 +27,11 @@
.size(@w: 100%, @h: 100%);
background: linear-gradient(
90deg,
rgba(199, 8, 80, 0.15) 0%,
rgba(199, 8, 80, 0.05) 30%,
rgba(30, 30, 30, 0.4) 0%,
rgba(50, 50, 50, 0.25) 30%,
transparent 50%,
rgba(199, 8, 80, 0.05) 70%,
rgba(199, 8, 80, 0.15) 100%
rgba(50, 50, 50, 0.25) 70%,
rgba(30, 30, 30, 0.4) 100%
);
pointer-events: none;
z-index: -1;
@@ -82,9 +85,12 @@
.channelList {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
gap: 30px;
overflow-x: auto;
overflow-y: hidden;
> * + * {
margin-left: 30px;
}
}
.channelCard {
@@ -96,9 +102,12 @@
border: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
gap: 10px;
transition: all 0.3s ease;
> * + * {
margin-left: 10px;
}
.thumbnail {
flex: 1 1 0;
.size(@w: 212px, @h: 119px);
@@ -120,7 +129,10 @@
.channelHeader {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: center);
gap: 9px;
> * + * {
margin-left: 9px;
}
.logoWrapper {
.size(@w: 40px, @h: 40px);
@@ -159,7 +171,10 @@
.progressBarWrapper {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
gap: 10px;
> * + * {
margin-left: 10px;
}
.progressBar {
flex: 1 1 0;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import classNames from 'classnames';
import Spottable from '@enact/spotlight/Spottable';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import css from './BelowTabContainer.module.less';
const SpottableDiv = Spottable('div');
const ChannelCard = ({
channelName,
programName,
logoSrc,
thumbnailSrc,
progressPercent = 0,
backgroundColor = '#FFFFFF',
onFocus,
onBlur,
onClick,
spotlightId,
}) => {
return (
<SpottableDiv
className={css.channelCard}
onFocus={onFocus}
onBlur={onBlur}
onClick={onClick}
spotlightId={spotlightId}
>
<div className={css.thumbnail}>
<CustomImage src={thumbnailSrc} alt={channelName} />
</div>
<div className={css.cardContent}>
<div className={css.channelHeader}>
<div className={css.logoWrapper} style={{ background: backgroundColor }}>
<CustomImage src={logoSrc} alt={`${channelName} logo`} className={css.logo} />
</div>
<h3 className={css.channelName}>{channelName}</h3>
</div>
<div className={css.programName}>{programName}</div>
<div className={css.progressBarWrapper}>
<div className={css.progressBar}>
<div className={css.progressTrack} />
<div className={css.progressFill} style={{ width: `${progressPercent}%` }} />
</div>
</div>
</div>
</SpottableDiv>
);
};
export default ChannelCard;

View File

@@ -0,0 +1,144 @@
import React, { useCallback, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { updatePanel } from '../../../../actions/panelActions';
import { sendLogTotalRecommend } from '../../../../actions/logActions';
import TScroller from '../../../../components/TScroller/TScroller';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods';
import { convertUtcToLocal } from '../../../../components/MediaPlayer/util';
import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents';
import ChannelCard from './ChannelCard';
import css from './LiveChannelContents.module.less';
export default function LiveChannelContents({
liveInfos,
currentTime,
setSelectedIndex,
videoVerticalVisible,
currentVideoShowId,
handleItemFocus,
tabTitle,
panelInfo,
}) {
const dispatch = useDispatch();
const isClickBlocked = useRef(false);
const handleFocus = useCallback(
() => () => {
if (handleItemFocus) {
handleItemFocus(LOG_MENU.FULL_LIVE_CHANNELS);
}
},
[handleItemFocus]
);
const calculateProgress = useCallback((startDt, endDt) => {
if (!startDt || !endDt) return 0;
let localStartDt = convertUtcToLocal(startDt);
let localEndDt = convertUtcToLocal(endDt);
let curDt = new Date();
let localStartSec = localStartDt?.getTime() / 1000;
let localEndSec = localEndDt?.getTime() / 1000;
let curSec = curDt?.getTime() / 1000;
let fullSec = localEndSec - localStartSec;
let diff = curSec - localStartSec;
let percent = Math.floor((diff * 100) / fullSec);
if (percent > 100 || percent < 0) {
return 0;
}
return percent;
}, []);
const handleItemClick = useCallback(
(channel, index) => () => {
const params = {
tabTitle: tabTitle && tabTitle[1] ? tabTitle[1] : 'LIVE CHANNEL',
showId: channel.showId,
showTitle: channel.showNm,
showType: panelInfo?.shptmBanrTpNm,
category: channel.catNm,
partner: channel.patncNm,
contextName: LOG_CONTEXT_NAME.SHOW,
messageId: LOG_MESSAGE_ID.CONTENTCLICK,
};
dispatch(sendLogTotalRecommend(params));
// 중복클릭방지
if (isClickBlocked.current) {
return;
}
isClickBlocked.current = true;
setTimeout(() => {
isClickBlocked.current = false;
}, 600);
if (!channel.showId) return;
if (currentVideoShowId && currentVideoShowId === channel.showId) {
return;
}
setSelectedIndex(index);
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
chanId: channel.chanId,
patnrId: channel.patnrId,
showId: channel.showId,
shptmBanrTpNm: 'LIVE',
lgCatCd: channel.catCd,
isUpdatedByClick: true,
},
})
);
},
[currentVideoShowId, isClickBlocked, dispatch, tabTitle, panelInfo, setSelectedIndex]
);
// 표시 조건: panelInfo.shptmBanrTpNm === "LIVE" && liveInfos 존재
if (panelInfo?.shptmBanrTpNm !== 'LIVE' || !liveInfos) {
return null;
}
return (
<>
<div className={css.container}>
{liveInfos.length > 0 ? (
<TScroller direction="horizontal" noScrollByWheel className={css.channelList}>
<>
{liveInfos.map((channel, index) => {
const programName = !channel.showId
? $L('No Livestream scheduled yet')
: channel.showNm || channel.patncNm;
return (
<ChannelCard
key={channel.prdtId || `channel-${index}`}
channelName={channel.patncNm}
programName={programName}
logoSrc={channel.patncLogoPath}
thumbnailSrc={channel.thumbnailUrl || channel.dfltThumbnailImgPath}
progressPercent={calculateProgress(channel.strtDt, channel.endDt)}
backgroundColor="#FFFFFF"
onClick={handleItemClick(channel, index)}
onFocus={handleFocus()}
spotlightId={`tabChannel-video-${index}`}
/>
);
})}
</>
</TScroller>
) : (
<ListEmptyContents tabIndex={1} />
)}
</div>
</>
);
}

View File

@@ -0,0 +1,32 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.container {
width: 100%;
height: 155px;
> div:nth-child(1) {
.size(@w: 100%, @h: 100%);
}
}
.channelList {
width: 100%;
height: 100%;
> div {
height: 155px;
> div {
display: flex;
> div {
flex: none;
margin-right: 30px;
&:last-child {
margin-right: 0;
}
}
}
}
}