[251007] feat: views - PlayerItemCard.jsx, LiveChannelContents.jsx, Li...

🕐 커밋 시간: 2025. 10. 07. 16:19:51

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

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerItemCard/PlayerItemCard.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveShowContainer.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/PlayerItemCard/PlayerItemCard.v2.module.less (unknown):
     Added: position()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.v2.module.less (unknown):
     Added: child()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveChannelContents.v2.module.less (unknown):
     Added: child()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/LiveChannelContents.module.less (unknown):
     Deleted: child()

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-10-07 16:19:54 +09:00
parent a483264c39
commit f4aac00c88
7 changed files with 399 additions and 75 deletions

View File

@@ -1,34 +1,35 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import classNames from "classnames";
import classNames from 'classnames';
import Spottable from "@enact/spotlight/Spottable";
import Spottable from '@enact/spotlight/Spottable';
import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png";
import CustomImage from "../../../components/CustomImage/CustomImage";
import { convertUtcToLocal } from "../../../components/MediaPlayer/util";
import { $L, removeSpecificTags } from "../../../utils/helperMethods";
import css from "./PlayerItemCard.module.less";
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
import CustomImage from '../../../components/CustomImage/CustomImage';
import { convertUtcToLocal } from '../../../components/MediaPlayer/util';
import { $L, removeSpecificTags } from '../../../utils/helperMethods';
import css1 from './PlayerItemCard.module.less';
import css2 from './PlayerItemCard.v2.module.less';
const SpottableComponent = Spottable("div");
const SpottableComponent = Spottable('div');
const TYPES = {
liveHorizontal: "liveHorizontal",
featuredHorizontal: "featuredHorizontal",
liveHorizontal: 'liveHorizontal',
featuredHorizontal: 'featuredHorizontal',
};
const IMAGETYPES = {
imgHorizontal: "imgHorizontal",
imgVertical: "imgVertical",
imgHorizontal: 'imgHorizontal',
imgVertical: 'imgVertical',
};
const STRING_CONF = {
SOLD_OUT: "SOLD OUT",
TOP: "TOP",
SOLD_OUT: 'SOLD OUT',
TOP: 'TOP',
};
export const removeDotAndColon = (string) => {
return /[.:]/.test(string) ? string.replace(/[.:]/g, "") : string;
return /[.:]/.test(string) ? string.replace(/[.:]/g, '') : string;
};
export default memo(function PlayerItemCard({
@@ -55,6 +56,7 @@ export default memo(function PlayerItemCard({
startDt,
endDt,
type = TYPES.liveHorizontal,
version = 1, // default = 1 , v2 = 2
...rest
}) {
@@ -108,11 +110,10 @@ export default memo(function PlayerItemCard({
}
}, [onFocus]);
const progressStyle = useMemo(
() => ({ width: `${percentGauge}%` }),
[percentGauge]
);
const ariaLabel = "Selected, " + patnerName + " " + productName();
const progressStyle = useMemo(() => ({ width: `${percentGauge}%` }), [percentGauge]);
const ariaLabel = 'Selected, ' + patnerName + ' ' + productName();
const css = version === 2 ? css2 : css1;
return (
<SpottableComponent
@@ -124,35 +125,27 @@ export default memo(function PlayerItemCard({
onBlur={_onBlur}
onClick={_onClick}
onFocus={_onFocus}
spotlightId={
productId ? "spotlightId-" + removeDotAndColon(productId) : spotlightId
}
spotlightId={productId ? 'spotlightId-' + removeDotAndColon(productId) : spotlightId}
aria-label={ariaLabel}
{...rest}
>
<div className={css.imageWrap}>
<CustomImage alt={imageAlt} delay={0} src={imageSource} />
{soldoutFlag && soldoutFlag === "Y" && (
<div>{$L(STRING_CONF.SOLD_OUT)}</div>
)}
{soldoutFlag && soldoutFlag === 'Y' && <div>{$L(STRING_CONF.SOLD_OUT)}</div>}
{currentVideoVisible && (
<div className={css.nowPlay}>
<h3>{$L("Now Playing")}</h3>
<h3>{$L('Now Playing')}</h3>
</div>
)}
</div>
<div className={css.descWrap}>
<div className={css.patnerWrap}>
<CustomImage
className={css.logo}
src={logo}
fallbackSrc={defaultLogoImg}
/>
<CustomImage className={css.logo} src={logo} fallbackSrc={defaultLogoImg} />
<h3 className={css.brandName}>{patnerName}</h3>
</div>
<h3 dangerouslySetInnerHTML={productName()} className={css.title} />
{liveInfo && liveInfo.showType === "live" && (
{liveInfo && liveInfo.showType === 'live' && (
<div
className={classNames(
css.progressBarWrap,

View File

@@ -0,0 +1,303 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
/* liveHorizontal */
.liveHorizontal {
position: relative;
.size(@w: 470px, @h: 155px);
padding: 18px;
background: rgba(44, 44, 44, 0.85);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
transition: all 0.3s ease;
> * + * {
margin-left: 10px;
}
&.vodVideoVertical {
.size(@w: 470px, @h: 155px);
}
&.liveVideoVertical {
.size(@w: 470px, @h: 155px);
}
.imageWrap {
flex: 1 1 0;
.size(@w: 212px, @h: 119px);
overflow: hidden;
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
position: relative;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
// sold out
> div {
.position(@position: absolute, @top: 0, @right: 0);
.flex();
.size(@w: 100%, @h: 100%);
background-color: rgba(26, 26, 26, 0.6);
font-weight: bold;
font-size: 24px;
}
.nowPlay {
.size(@w: 125px, @h:24px);
background-color: rgba(0, 0, 0, 0.6);
position: absolute;
bottom: 1px;
right: 1px;
top: auto;
> h3 {
font-size: 18px;
font-weight: 600;
}
}
}
.descWrap {
flex: 1 1 0;
height: 110px;
.flex(@display: flex, @justifyCenter: space-between, @alignCenter: flex-start, @direction: column);
.patnerWrap {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: center);
> * + * {
margin-left: 9px;
}
.logo {
.size(@w: 40px, @h: 40px);
border-radius: 100px;
.position(@position: relative);
overflow: hidden;
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
.brandName {
color: #EAEAEA;
font-size: 24px;
font-family: @baseFont;
font-weight: 700;
line-height: 31px;
margin: 0;
}
}
.title {
width: 100%;
color: #EAEAEA;
font-size: 24px;
font-family: @baseFont;
font-weight: 400;
line-height: 31px;
.elip(@clamp: 1);
}
.progressBarWrap {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
> * + * {
margin-left: 10px;
}
.progressBar {
flex: 1 1 0;
height: 5px;
.position(@position: relative);
overflow: hidden;
// track 역할
&::before {
content: "";
.size(@w: 100%, @h: 5px);
.position(@position: absolute, @top: 0, @left: 0);
background: #D4D4D4;
}
.gauge {
height: 5px;
.position(@position: absolute, @top: 0, @left: 0);
background: #7D848C;
transition: width 0.3s ease;
&.focused {
background-color: @PRIMARY_COLOR_RED;
}
}
}
&.videoVerticalProgress {
width: 100%;
}
}
}
&:hover {
background: rgba(44, 44, 44, 0.95);
border-color: rgba(255, 255, 255, 0.2);
}
&:focus {
background: rgba(44, 44, 44, 1);
border-color: @PRIMARY_COLOR_RED;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none;
}
}
}
// featuredHorizontal
.featuredHorizontal {
position: relative;
.size(@w: 470px, @h: 155px);
padding: 18px;
background: rgba(44, 44, 44, 0.85);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
transition: all 0.3s ease;
> * + * {
margin-left: 10px;
}
&.vodVideoVertical {
.size(@w: 470px, @h: 155px);
}
&.liveVideoVertical {
.size(@w: 470px, @h: 155px);
}
.imageWrap {
flex: 1 1 0;
.size(@w: 212px, @h: 119px);
overflow: hidden;
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
position: relative;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
.nowPlay {
.size(@w: 125px, @h:24px);
background-color: rgba(0, 0, 0, 0.6);
position: absolute;
bottom: 1px;
right: 1px;
top: auto;
> h3 {
font-size: 18px;
font-weight: 600;
}
}
// sold out
> div {
.position(@position: absolute, @top: 0, @right: 0);
.flex();
.size(@w: 100%, @h: 100%);
background-color: rgba(26, 26, 26, 0.6);
font-weight: bold;
font-size: 24px;
}
}
.descWrap {
flex: 1 1 0;
height: 110px;
.flex(@display: flex, @justifyCenter: space-between, @alignCenter: flex-start, @direction: column);
.patnerWrap {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: center);
> * + * {
margin-left: 9px;
}
.logo {
.size(@w: 40px, @h: 40px);
border-radius: 100px;
.position(@position: relative);
overflow: hidden;
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
.brandName {
color: #EAEAEA;
font-size: 24px;
font-family: @baseFont;
font-weight: 700;
line-height: 31px;
margin: 0;
}
}
.title {
width: 100%;
color: #EAEAEA;
font-size: 24px;
font-family: @baseFont;
font-weight: 400;
line-height: 31px;
.elip(@clamp: 1);
}
}
&:hover {
background: rgba(44, 44, 44, 0.95);
border-color: rgba(255, 255, 255, 0.2);
}
&:focus {
background: rgba(44, 44, 44, 1);
border-color: @PRIMARY_COLOR_RED;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none;
}
}
}

View File

@@ -1,17 +1,18 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from "react-redux";
import { useDispatch } from 'react-redux';
import Spotlight from "@enact/spotlight";
import Spotlight from '@enact/spotlight';
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";
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 cssV2 from './LiveChannelContents.v2.module.less';
import { sendLogTotalRecommend } from '../../../../actions/logActions';
export default function LiveChannelContents({
liveInfos,
@@ -22,7 +23,9 @@ export default function LiveChannelContents({
tabIndex,
handleItemFocus,
tabTitle,
panelInfo
panelInfo,
direction = 'vertical',
version = 1,
}) {
const dispatch = useDispatch();
const isClickBlocked = useRef(false);
@@ -63,8 +66,8 @@ export default function LiveChannelContents({
category: catNm,
partner: patncNm,
contextName: LOG_CONTEXT_NAME.SHOW,
messageId: LOG_MESSAGE_ID.CONTENTCLICK
}
messageId: LOG_MESSAGE_ID.CONTENTCLICK,
};
dispatch(sendLogTotalRecommend(params));
//중복클릭방지
if (isClickBlocked.current) {
@@ -89,7 +92,7 @@ export default function LiveChannelContents({
chanId,
patnrId,
showId,
shptmBanrTpNm: "LIVE",
shptmBanrTpNm: 'LIVE',
lgCatCd: catCd,
isUpdatedByClick: true,
},
@@ -99,7 +102,7 @@ export default function LiveChannelContents({
const showNameDangerouslySetInnerHTML = () => {
if (!showId) {
return { __html: $L("No Livestream scheduled yet") };
return { __html: $L('No Livestream scheduled yet') };
}
return showNm ? { __html: showNm } : { __html: patncNm };
};
@@ -122,29 +125,25 @@ export default function LiveChannelContents({
startDt={strtDt}
endDt={endDt}
currentTime={currentTime}
version={version}
/>
);
},
[
liveInfos,
currentTime,
currentVideoShowId,
isClickBlocked,
dispatch,
handleFocus,
]
[liveInfos, currentTime, currentVideoShowId, isClickBlocked, dispatch, handleFocus, version]
);
const containerClass = version === 2 ? cssV2.container : css.container;
return (
<>
<div className={css.container}>
<div className={containerClass}>
{liveInfos && liveInfos.length > 0 ? (
<TVirtualGridList
dataSize={liveInfos.length}
direction="vertical"
direction={direction}
renderItem={renderItem}
itemWidth={videoVerticalVisible ? 540 : 600}
itemHeight={236}
itemWidth={version === 2 ? 280 : videoVerticalVisible ? 540 : 600}
itemHeight={version === 2 ? 78 : 236}
spacing={12}
noScrollByWheel={false}
/>

View File

@@ -0,0 +1,22 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.container {
width: 100%;
height: 175px;
> div:nth-child(1) {
.size(@w: 100%, @h: 100%);
}
}
.channelList {
width: 100%;
.flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start);
overflow-x: auto;
overflow-y: hidden;
> * + * {
margin-left: 30px;
}
}

View File

@@ -1,12 +1,11 @@
import React from 'react';
import classNames from 'classnames';
// import classNames from 'classnames';
import Spottable from '@enact/spotlight/Spottable';
import LiveChannelContents from '../TabContents/LiveChannelContents';
import css from './LiveShowContainer.module.less';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import LiveChannelContents from './LiveChannelContents';
const SpottableDiv = Spottable('div');
@@ -20,6 +19,8 @@ export default function LiveShowContainer({
tabTitle,
panelInfo,
onLiveChannelButtonClick,
selectedIndex,
tabIndex,
}) {
return (
<div className={css.container}>
@@ -34,16 +35,22 @@ export default function LiveShowContainer({
</div>
</SpottableDiv>
<LiveChannelContents
liveInfos={liveInfos}
currentTime={currentTime}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={currentVideoShowId}
handleItemFocus={handleItemFocus}
tabTitle={tabTitle}
panelInfo={panelInfo}
/>
{panelInfo?.shptmBanrTpNm === 'LIVE' && liveInfos && (
<LiveChannelContents
tabTitle={tabTitle}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
videoVerticalVisible={videoVerticalVisible}
currentVideoShowId={currentVideoShowId}
liveInfos={liveInfos}
tabIndex={tabIndex}
handleItemFocus={handleItemFocus}
panelInfo={panelInfo}
currentTime={currentTime}
direction="horizontal"
version={2}
/>
)}
</div>
);
}