[251019] fix: PlayerPanel Optimization-2

🕐 커밋 시간: 2025. 10. 19. 22:13:44

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/convertActions.js
  ~ com.twin.app.shoptime/src/components/TItemCard/TItemCard.new.jsx
  ~ com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayChat.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayQRCode.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/convertActions.js (javascript):
     Added: attemptConversion(), onSuccess(), onFail()
     Deleted: onSuccess(), onFail()
  📄 com.twin.app.shoptime/src/components/TItemCard/TItemCard.new.jsx (javascript):
    🔄 Modified: generateMockEnergyLabels()
  📄 com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less (unknown):
     Added: style()
     Deleted: style()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayChat.jsx (javascript):
     Added: PlayerOverlayChat(), propsAreEqual()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx (javascript):
     Added: PlayerOverlayContents(), propsAreEqual()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayQRCode.jsx (javascript):
     Added: PlayerOverlayQRCode(), propsAreEqual()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx (javascript):
     Added: PlayerPanelNew()
    🔄 Modified: getLogTpNo()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-10-19 22:13:48 +09:00
parent 95bb25a135
commit 5c70f1fa78
8 changed files with 769 additions and 800 deletions

View File

@@ -3,85 +3,178 @@ import { TAxios } from '../api/TAxios';
import { types } from './actionTypes';
/**
* PDF를 이미지로 변환
* PDF를 이미지로 변환 (재시도 로직 포함)
* @param {string} pdfUrl - 변환할 PDF URL
* @param {function} callback - 성공/실패 후 실행할 콜백 (error, imageUrl)
* @param {number} maxRetries - 최대 재시도 횟수 (기본값: 5)
* @param {number} timeout - 타임아웃 (기본값: 60000ms = 60초)
*/
export const convertPdfToImage = (pdfUrl, callback) => (dispatch, getState) => {
dispatch({
type: types.CONVERT_PDF_TO_IMAGE,
payload: pdfUrl,
});
export const convertPdfToImage =
(pdfUrl, callback, maxRetries = 5, timeout = 60000) =>
(dispatch, getState) => {
let attempts = 0;
let timeoutId = null;
const onSuccess = (response) => {
// retCode 체크 (프로젝트 API 규약: 200이어도 retCode로 성공/실패 구분)
const retCode = response.headers?.retcode || response.headers?.retCode;
const attemptConversion = () => {
attempts++;
// console.log(`🔄 [EnergyLabel] Converting PDF attempt ${attempts}/${maxRetries + 1}:`, pdfUrl);
if (retCode !== undefined && retCode !== 0 && retCode !== '0') {
const error = new Error(`API Error: retCode=${retCode}`);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
return;
}
// 타임아웃 설정
timeoutId = setTimeout(() => {
clearTimeout(timeoutId);
const timeoutError = new Error(
`Conversion timeout after ${timeout}ms (attempt ${attempts})`
);
console.warn(`⏱️ [EnergyLabel] Timeout on attempt ${attempts}:`, timeoutError.message);
let imageUrl;
try {
if (response.data instanceof Blob) {
if (response.data.size < 100) {
throw new Error('Invalid image data (size too small)');
// 재시도 가능한 경우
if (attempts < maxRetries + 1) {
// console.log(`🔄 [EnergyLabel] Retrying... (${attempts}/${maxRetries + 1})`);
attemptConversion();
} else {
// 최종 실패
console.error(`❌ [EnergyLabel] Final failure after ${attempts} attempts:`, pdfUrl);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error: timeoutError },
});
callback && callback(timeoutError, null);
}
imageUrl = URL.createObjectURL(response.data);
} else if (response.data instanceof ArrayBuffer) {
if (response.data.byteLength < 100) {
throw new Error('Invalid image data (size too small)');
}, timeout);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE,
payload: pdfUrl,
});
const onSuccess = (response) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
const blob = new Blob([response.data], { type: 'image/png' });
imageUrl = URL.createObjectURL(blob);
} else {
const blob = new Blob([response.data], { type: 'image/png' });
imageUrl = URL.createObjectURL(blob);
}
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_SUCCESS,
payload: { pdfUrl, imageUrl },
});
// retCode 체크 (프로젝트 API 규약: 200이어도 retCode로 성공/실패 구분)
const retCode = response.headers?.retcode || response.headers?.retCode;
callback && callback(null, imageUrl);
} catch (error) {
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
}
if (retCode !== undefined && retCode !== 0 && retCode !== '0') {
const error = new Error(`API Error: retCode=${retCode}`);
console.warn(`⚠️ [EnergyLabel] API returned error on attempt ${attempts}:`, retCode);
// retCode 에러도 재시도
if (attempts < maxRetries + 1) {
console.log(
`🔄 [EnergyLabel] Retrying due to API error... (${attempts}/${maxRetries + 1})`
);
attemptConversion();
} else {
console.error(
`❌ [EnergyLabel] Final failure after ${attempts} attempts (API error):`,
pdfUrl
);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
}
return;
}
let imageUrl;
try {
if (response.data instanceof Blob) {
if (response.data.size === 0) {
throw new Error('Invalid image data (empty blob)');
}
imageUrl = URL.createObjectURL(response.data);
} else if (response.data instanceof ArrayBuffer) {
if (response.data.byteLength === 0) {
throw new Error('Invalid image data (empty buffer)');
}
const blob = new Blob([response.data], { type: 'image/png' });
imageUrl = URL.createObjectURL(blob);
} else {
const blob = new Blob([response.data], { type: 'image/png' });
if (blob.size === 0) {
throw new Error('Invalid image data (empty blob)');
}
imageUrl = URL.createObjectURL(blob);
}
console.log(`✅ [EnergyLabel] Conversion successful on attempt ${attempts}:`, pdfUrl);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_SUCCESS,
payload: { pdfUrl, imageUrl },
});
callback && callback(null, imageUrl);
} catch (error) {
console.error(`❌ [EnergyLabel] Image creation failed on attempt ${attempts}:`, error);
// 이미지 생성 실패도 재시도
if (attempts < maxRetries + 1) {
console.log(
`🔄 [EnergyLabel] Retrying due to image creation error... (${attempts}/${maxRetries + 1})`
);
attemptConversion();
} else {
console.error(
`❌ [EnergyLabel] Final failure after ${attempts} attempts (image error):`,
pdfUrl
);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
}
}
};
const onFail = (error) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
console.warn(`⚠️ [EnergyLabel] Network error on attempt ${attempts}:`, error.message);
// 네트워크 에러도 재시도
if (attempts < maxRetries + 1) {
console.log(
`🔄 [EnergyLabel] Retrying due to network error... (${attempts}/${maxRetries + 1})`
);
attemptConversion();
} else {
console.error(
`❌ [EnergyLabel] Final failure after ${attempts} attempts (network error):`,
pdfUrl
);
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
}
};
TAxios(
dispatch,
getState,
'post',
URLS.CONVERT_IMG,
{},
{ pdfUrl },
onSuccess,
onFail,
false,
'blob'
);
};
attemptConversion();
};
const onFail = (error) => {
dispatch({
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
payload: { pdfUrl, error },
});
callback && callback(error, null);
};
TAxios(
dispatch,
getState,
'post',
URLS.CONVERT_IMG,
{},
{ pdfUrl },
onSuccess,
onFail,
false,
'blob'
);
};
/**
* 여러 PDF를 순차적으로 변환 (백그라운드)
* @param {Array<string>} pdfUrls - 변환할 PDF URL 배열

View File

@@ -339,26 +339,42 @@ export default memo(function TItemCardNew({
// PNG 이미지는 직접 표시
if (pdfUrl.endsWith('.png')) {
// console.log(`📸 [EnergyLabel] Displaying PNG directly:`, pdfUrl);
dispatch({
type: 'CONVERT_PDF_TO_IMAGE_SUCCESS',
payload: { pdfUrl, imageUrl: pdfUrl },
});
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
return;
}
// PDF 변환 시작 (성공 시에만 팝업)
// PDF 변환 시작 (최대 5회 재시도, 60초 타임아웃)
// console.log(`📄 [EnergyLabel] Starting PDF conversion:`, pdfUrl);
dispatch(
convertPdfToImage(pdfUrl, (error, imageUrl) => {
if (error) {
console.error('[EnergyLabel] 변환 실패:', error.message || error);
} else {
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
}
})
convertPdfToImage(
pdfUrl,
(error, imageUrl) => {
if (error) {
console.error('[EnergyLabel] 최종 변환 실패:', error.message || error);
// 실패해도 팝업은 열어서 에러 메시지 표시
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
} else {
console.log(`[EnergyLabel] PDF 변환 완료, 팝업 표시`);
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
}
},
5, // 최대 5회 재시도
60000 // 60초 타임아웃
)
);
},
[dispatch]
@@ -504,8 +520,28 @@ export default memo(function TItemCardNew({
</SpottableComponent>
{(() => {
const showPopup = activePopup === Config.ACTIVE_POPUP.energyPopup && currentPdfUrl;
if (!showPopup) return null;
// 팝업이 표시되어야 하는 조건 검증
const isEnergyPopup = activePopup === Config.ACTIVE_POPUP.energyPopup;
const hasPdfUrl = !!currentPdfUrl;
const shouldShowPopup = isEnergyPopup && hasPdfUrl;
if (!shouldShowPopup) {
// console.log('[EnergyLabel] Popup not showing:', {
// isEnergyPopup,
// hasPdfUrl,
// popupVisible,
// });
return null;
}
// console.log('[EnergyLabel] Rendering popup:', {
// popupVisible,
// activePopup,
// currentPdfUrl,
// isConverting: convert?.isConverting,
// hasImage: !!convert?.convertedImage,
// hasError: !!convert?.error,
// });
return (
<TPopUp
@@ -534,9 +570,19 @@ export default memo(function TItemCardNew({
{convert.error?.message || String(convert.error)}
</p>
</div>
) : (
) : convert.isConverting ? (
<div>
<p>{$L(STRING_CONF.ENERGY_LOADING)}</p>
<p style={{ fontSize: '0.8em', marginTop: '10px', color: '#999' }}>
Converting PDF to image... (attempt in progress)
</p>
</div>
) : (
<div>
<p>{$L(STRING_CONF.ENERGY_ERROR)}</p>
<p style={{ fontSize: '0.8em', marginTop: '10px', color: '#999' }}>
Unknown state - no image or error
</p>
</div>
)}
</div>
@@ -544,6 +590,9 @@ export default memo(function TItemCardNew({
) : (
<div>
<p>{$L(STRING_CONF.ENERGY_ERROR)}</p>
<p style={{ fontSize: '0.8em', marginTop: '10px', color: '#999' }}>
Convert reducer state not found
</p>
</div>
)}
</div>

View File

@@ -814,7 +814,7 @@
}
.energyPopup {
.default-style(@width: 960px);
.default-style(@width: 700px);
.info {
background-color: #fff;
.textLayer {

View File

@@ -1,29 +1,22 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from 'react';
import classNames from "classnames";
import { useDispatch, useSelector } from "react-redux";
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { setHidePopup, setShowPopup } from "../../../actions/commonActions";
import CustomImage from "../../../components/CustomImage/CustomImage";
import TButton from "../../../components/TButton/TButton";
import TPopUp from "../../../components/TPopUp/TPopUp";
import TQRCode from "../../../components/TQRCode/TQRCode";
import { ACTIVE_POPUP } from "../../../utils/Config";
import { $L } from "../../../utils/helperMethods";
import css from "./PlayerOverlayChat.module.less";
import { setHidePopup, setShowPopup } from '../../../actions/commonActions';
import CustomImage from '../../../components/CustomImage/CustomImage';
import TButton from '../../../components/TButton/TButton';
import TPopUp from '../../../components/TPopUp/TPopUp';
import TQRCode from '../../../components/TQRCode/TQRCode';
import { ACTIVE_POPUP } from '../../../utils/Config';
import { $L } from '../../../utils/helperMethods';
import css from './PlayerOverlayChat.module.less';
export default function PlayerOverlayChat({
currentTime,
videoVerticalVisible,
imageQRCodeUrl,
QRCodeUrl,
}) {
function PlayerOverlayChat({ currentTime, videoVerticalVisible, imageQRCodeUrl, QRCodeUrl }) {
const dispatch = useDispatch();
const chatData = useSelector((state) => state.play.chatData);
const [recentChats, setRecentChats] = useState([]);
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const handleQRCodeClick = useCallback(() => {
dispatch(setShowPopup(ACTIVE_POPUP.qrPopup));
@@ -47,34 +40,20 @@ export default function PlayerOverlayChat({
useEffect(() => {
const filteredChats = filterAndSelectRecentChat();
setRecentChats(filteredChats);
}, [currentTime, chatData]);
}, [currentTime, chatData, videoVerticalVisible, filterAndSelectRecentChat]);
return (
<>
{chatData && Object.keys(chatData).length > 0 && (
<div
className={classNames(
css.chatContainer,
videoVerticalVisible && css.verticalChat
)}
>
<div className={classNames(css.chatContainer, videoVerticalVisible && css.verticalChat)}>
<div className={css.chatHeader} aria-label="LIVE CHAT">
{$L("LIVE CHAT")}
{$L('LIVE CHAT')}
</div>
<div
className={classNames(
css.contents,
videoVerticalVisible && css.verticalContent
)}
>
<div className={classNames(css.contents, videoVerticalVisible && css.verticalContent)}>
{recentChats &&
recentChats.length > 0 &&
recentChats.map((item, index) => (
<div
key={index}
className={css.chatItem}
style={{ order: -index }}
>
<div key={index} className={css.chatItem} style={{ order: -index }}>
<div className={css.username} aria-label={item?.username}>
{item?.username}
</div>
@@ -90,7 +69,7 @@ export default function PlayerOverlayChat({
onClick={handleQRCodeClick}
ariaLabel="TYPE A MESSAGE"
>
{"TYPE A MESSAGE"}
{'TYPE A MESSAGE'}
</TButton>
)}
{!videoVerticalVisible && (
@@ -104,7 +83,7 @@ export default function PlayerOverlayChat({
<TPopUp kind="qrPopup" open={popupVisible} onClose={onClose}>
<div className={css.popupContainer}>
<div className={css.header}>
<h3 aria-label="QR CODE Heading 1">{$L("QR CODE")}</h3>
<h3 aria-label="QR CODE Heading 1">{$L('QR CODE')}</h3>
</div>
<div className={css.qrcodeContainer}>
@@ -116,14 +95,10 @@ export default function PlayerOverlayChat({
)}
</div>
<h3 aria-label="To send a message, please scan the QR code.">
{$L("To send a message, please scan the QR code.")}
{$L('To send a message, please scan the QR code.')}
</h3>
<TButton
className={css.popupBtn}
onClick={onClose}
ariaLabel="Close"
>
{$L("CLOSE")}
<TButton className={css.popupBtn} onClick={onClose} ariaLabel="Close">
{$L('CLOSE')}
</TButton>
</div>
</div>
@@ -132,3 +107,14 @@ export default function PlayerOverlayChat({
</>
);
}
const propsAreEqual = (prev, next) => {
return (
prev.currentTime === next.currentTime &&
prev.videoVerticalVisible === next.videoVerticalVisible &&
prev.imageQRCodeUrl === next.imageQRCodeUrl &&
prev.QRCodeUrl === next.QRCodeUrl
);
};
export default React.memo(PlayerOverlayChat, propsAreEqual);

View File

@@ -1,24 +1,14 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import classNames from 'classnames';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
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 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';
@@ -26,14 +16,11 @@ import { SpotlightIds } from '../../../utils/SpotlightIds';
import PlayerTabButton from '../PlayerTabContents/TabButton/PlayerTabButton';
import css from './PlayerOverlayContents.module.less';
const SpottableBtn = Spottable("button");
const SpottableBtn = Spottable('button');
const Container = SpotlightContainerDecorator(
{ enterTo: "default-element" },
"div"
);
const Container = SpotlightContainerDecorator({ enterTo: 'default-element' }, 'div');
export default function PlayerOverlayContents({
function PlayerOverlayContents({
type,
onClick,
panelInfo,
@@ -68,10 +55,10 @@ export default function PlayerOverlayContents({
const backBtnRef = useRef(null);
useEffect(() => {
if (type === "MEDIA" && !panelInfo.modal && backBtnRef.current) {
if (type === 'MEDIA' && !panelInfo.modal && backBtnRef.current) {
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
}
}, [panelInfo?.shptmBanrTpNm, panelInfo.modal, backBtnRef.current]);
}, [type, panelInfo.modal, backBtnRef]);
const handleSubtitleOnClick = useCallback(() => {
if (!captionEnable) {
@@ -79,11 +66,11 @@ export default function PlayerOverlayContents({
}
setIsSubtitleActive((prev) => !prev);
}, [captionEnable]);
}, [dispatch, captionEnable]);
const patncLogoPath = useMemo(() => {
let logo = playListInfo[selectedIndex]?.patncLogoPath;
if (type === "MEDIA") {
if (type === 'MEDIA') {
logo = panelInfo?.patncLogoPath;
}
@@ -92,7 +79,7 @@ export default function PlayerOverlayContents({
const partnerName = useMemo(() => {
let name = playListInfo[selectedIndex]?.patncNm;
if (type === "MEDIA") {
if (type === 'MEDIA') {
name = panelInfo?.patncNm;
}
@@ -101,11 +88,11 @@ export default function PlayerOverlayContents({
const showName = useMemo(() => {
let name = playListInfo[selectedIndex]?.showNm;
if (type === "MEDIA") {
if (type === 'MEDIA') {
name = panelInfo?.showNm;
}
return name ? name.replace(/<br\s*\/?>/gi, " ") : "";
return name ? name.replace(/<br\s*\/?>/gi, ' ') : '';
}, [playListInfo, selectedIndex, panelInfo]);
const onSpotlightMoveTabButton = (e) => {
@@ -116,27 +103,27 @@ export default function PlayerOverlayContents({
const onSpotlightMoveMediaButton = (e) => {
e.stopPropagation();
if (type === "LIVE") {
return Spotlight.focus("videoIndicator-down-button");
if (type === 'LIVE') {
return Spotlight.focus('videoIndicator-down-button');
}
Spotlight.focus("videoPlayer_mediaControls");
Spotlight.focus('videoPlayer_mediaControls');
};
const onSpotlightMoveSlider = useCallback(
(e) => {
if (type === "VOD") {
if (type === 'VOD') {
e.stopPropagation();
Spotlight.focus(SpotlightIds.PLAYER_SLIDER);
}
},
[panelInfo]
[type]
);
const onSpotlightMoveSideTab = (e) => {
e.stopPropagation();
e.preventDefault();
Spotlight.focus("tab-0");
Spotlight.focus('tab-0');
};
const onSpotlightMoveBelowTab = (e) => {
@@ -147,13 +134,13 @@ export default function PlayerOverlayContents({
if (tabIndexV2 === 0) {
// ShopNow 탭: Close 버튼으로
// Spotlight.focus('below-tab-close-button');
Spotlight.focus("shownow_close_button");
Spotlight.focus('shownow_close_button');
} else if (tabIndexV2 === 1) {
// LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로
Spotlight.focus("below-tab-live-channel-button");
Spotlight.focus('below-tab-live-channel-button');
} else if (tabIndexV2 === 2) {
// ShopNowButton: ShopNowButton으로
Spotlight.focus("below-tab-shop-now-button");
Spotlight.focus('below-tab-shop-now-button');
}
};
@@ -163,7 +150,7 @@ export default function PlayerOverlayContents({
const currentSideButtonStatus = useMemo(() => {
if (
type !== "MEDIA" &&
type !== 'MEDIA' &&
!panelInfo?.modal &&
!sideContentsVisible &&
tabContainerVersion === 1
@@ -189,67 +176,63 @@ export default function PlayerOverlayContents({
return (
<>
<Container className={css.overlayContainer}>
{type !== "MEDIA" &&
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
{type !== 'MEDIA' && playListInfo.length > 1 && noLiveContentsVisible && (
<>
<div className={css.indicatorUpButton}>
<SpottableBtn
onClick={handleIndicatorUpClick}
spotlightId="videoIndicator-up-button"
onSpotlightRight={
videoVerticalVisible
? onSpotlightMoveSideTab
: tabContainerVersion === 1
? onSpotlightMoveTabButton
: undefined
}
aria-label="Next channel"
/>
</div>
</>
)}
}
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={
type !== "MEDIA" &&
playListInfo.length < 2 &&
onSpotlightMoveBackButton
type !== 'MEDIA' && playListInfo.length < 2 && onSpotlightMoveBackButton
}
videoType={type}
/>
)}
{cntry_cd === "US" && (
{cntry_cd === 'US' && (
<div className={css.videoButtonContainer}>
<SpottableBtn
className={classNames(
@@ -278,8 +261,8 @@ export default function PlayerOverlayContents({
ref={backBtnRef}
/>
<div
className={classNames(type === "LIVE" && css.liveIcon)}
aria-label={type === "LIVE" && "Live Icon"}
className={classNames(type === 'LIVE' && css.liveIcon)}
aria-label={type === 'LIVE' && 'Live Icon'}
/>
{partnerName && (
@@ -294,17 +277,14 @@ export default function PlayerOverlayContents({
<h2 className={css.patnerName}>{partnerName}</h2>
<Marquee
className={classNames(
css.title,
videoVerticalVisible && css.videoVerticalMarquee
)}
className={classNames(css.title, videoVerticalVisible && css.videoVerticalMarquee)}
marqueeOn="render"
>
{showName}
</Marquee>
</div>
</Container>
{type === "VOD" && disclaimer && (
{type === 'VOD' && disclaimer && (
<div className={css.disclaimer}>
<span className={css.icon} />
<h3 aria-label={disclaimer}>{disclaimer}</h3>
@@ -313,3 +293,21 @@ export default function PlayerOverlayContents({
</>
);
}
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(PlayerOverlayContents, propsAreEqual);

View File

@@ -1,17 +1,13 @@
import React, { useMemo } from "react";
import React, { useMemo } from 'react';
import classNames from "classnames";
import { useSelector } from "react-redux";
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import TQRCode from "../../../components/TQRCode/TQRCode";
import css from "./PlayerOverlayQRCode.module.less";
import { getQRCodeUrl, scaleH, scaleW } from "../../../utils/helperMethods";
import TQRCode from '../../../components/TQRCode/TQRCode';
import css from './PlayerOverlayQRCode.module.less';
import { getQRCodeUrl, scaleH, scaleW } from '../../../utils/helperMethods';
export default function PlayerOverlayQRCode({
qrCurrentItem,
type,
modalScale,
}) {
function PlayerOverlayQRCode({ qrCurrentItem, type, modalScale }) {
const { cntry_cd } = useSelector((state) => state.common.httpHeader);
const deviceInfo = useSelector((state) => state.device.deviceInfo);
@@ -20,19 +16,19 @@ export default function PlayerOverlayQRCode({
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const label = useMemo(() => {
let text = "";
let text = '';
switch (cntry_cd) {
case "US":
text = "SCAN TO SHOP";
case 'US':
text = 'SCAN TO SHOP';
break;
case "GB":
text = "SCAN TO SHOP";
case 'GB':
text = 'SCAN TO SHOP';
break;
case "DE":
text = "ZUM EINKAUFEN SCANNEN";
case 'DE':
text = 'ZUM EINKAUFEN SCANNEN';
break;
case "RU":
text = "ОТСКАНИРОВАТЬ кОД ДЛЯ ПОКУПКИ";
case 'RU':
text = 'ОТСКАНИРОВАТЬ кОД ДЛЯ ПОКУПКИ';
break;
}
return text;
@@ -41,48 +37,48 @@ export default function PlayerOverlayQRCode({
const innerStylePosition = useMemo(() => {
const { patnrId } = qrCurrentItem || {};
let top, bottom, right, left;
let transformOrigin = "";
if (cntry_cd === "US" && (patnrId === "4" || patnrId === "11")) {
let transformOrigin = '';
if (cntry_cd === 'US' && (patnrId === '4' || patnrId === '11')) {
top = 64;
right = 80;
transformOrigin = "top right";
} else if (cntry_cd === "GB" && patnrId === "1") {
transformOrigin = 'top right';
} else if (cntry_cd === 'GB' && patnrId === '1') {
bottom = 64;
right = 120;
transformOrigin = "bottom right";
} else if (cntry_cd === "DE" && patnrId === "1") {
transformOrigin = 'bottom right';
} else if (cntry_cd === 'DE' && patnrId === '1') {
bottom = 25;
right = 41;
transformOrigin = "bottom right";
} else if (cntry_cd === "RU") {
if (patnrId === "12") {
transformOrigin = 'bottom right';
} else if (cntry_cd === 'RU') {
if (patnrId === '12') {
top = 280;
right = 84;
transformOrigin = "top right";
transformOrigin = 'top right';
}
if (type === "LIVE") {
if (type === 'LIVE') {
top = 90;
left = 74;
transformOrigin = "top left";
transformOrigin = 'top left';
}
}
if (!transformOrigin) {
return null;
}
return {
position: "absolute",
top: top ? scaleH(top * modalScale) + "px" : undefined,
bottom: bottom ? scaleH(bottom * modalScale) + "px" : undefined,
right: right ? scaleW(right * modalScale) + "px" : undefined,
left: left ? scaleW(left * modalScale) + "px" : undefined,
position: 'absolute',
top: top ? scaleH(top * modalScale) + 'px' : undefined,
bottom: bottom ? scaleH(bottom * modalScale) + 'px' : undefined,
right: right ? scaleW(right * modalScale) + 'px' : undefined,
left: left ? scaleW(left * modalScale) + 'px' : undefined,
transform: `scale(${modalScale})`,
transformOrigin: transformOrigin,
};
}, [qrCurrentItem, cntry_cd, modalScale, type]);
const { width, height } = useMemo(() => {
if (cntry_cd === "GB") return { width: "132", height: "131" };
return { width: "156", height: "156" };
if (cntry_cd === 'GB') return { width: '132', height: '131' };
return { width: '156', height: '156' };
}, [cntry_cd]);
const { detailUrl } = useMemo(() => {
@@ -94,13 +90,13 @@ export default function PlayerOverlayQRCode({
prdtId: qrCurrentItem?.prdtId,
entryMenu: entryMenu,
nowMenu: nowMenu,
liveFlag: "Y",
qrType: "billingDetail",
liveFlag: 'Y',
qrType: 'billingDetail',
});
}, [serverHOST, serverType, deviceInfo, entryMenu, nowMenu, qrCurrentItem]);
const QRCodeUrl = useMemo(() => {
if (type === "LIVE" && qrCurrentItem?.patncNm === "ShopLC") {
if (type === 'LIVE' && qrCurrentItem?.patncNm === 'ShopLC') {
return detailUrl;
}
return qrCurrentItem?.qrcodeUrl;
@@ -112,10 +108,10 @@ export default function PlayerOverlayQRCode({
<div
className={classNames(
css.container,
cntry_cd === "US" && css.us,
cntry_cd === "GB" && css.gb,
cntry_cd === "RU" && css.ru,
cntry_cd === "DE" && css.de
cntry_cd === 'US' && css.us,
cntry_cd === 'GB' && css.gb,
cntry_cd === 'RU' && css.ru,
cntry_cd === 'DE' && css.de
)}
style={innerStylePosition}
>
@@ -124,7 +120,7 @@ export default function PlayerOverlayQRCode({
text={QRCodeUrl}
width={width}
height={height}
ariaLabel="QR CODE, SCAN TO SHOP go to Shoptime App"
ariaLabel='QR CODE, "SCAN TO SHOP" go to Shoptime App'
/>
<div className={css.text}>{label}</div>
</div>
@@ -133,3 +129,13 @@ export default function PlayerOverlayQRCode({
</>
);
}
const propsAreEqual = (prev, next) => {
return (
prev.qrCurrentItem?.showId === next.qrCurrentItem?.showId &&
prev.type === next.type &&
prev.modalScale === next.modalScale
);
};
export default React.memo(PlayerOverlayQRCode, propsAreEqual);

View File

@@ -830,7 +830,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
resetTimer(REGULAR_TIMEOUT);
}
},
[resetTimer, videoVerticalVisible]
[dispatch, resetTimer, videoVerticalVisible]
);
const onClickBack = useCallback(
@@ -892,7 +892,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
[
dispatch,
panelInfo,
nowMenu,
videoPlayer,
sideContentsVisible,
videoVerticalVisible,
@@ -1400,7 +1399,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
}
}
},
[currentLiveTimeSeconds, liveTotalTime]
[dispatch]
);
useEffect(() => {
@@ -1881,43 +1880,49 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
timerId.current = null;
}, []);
const resetTimer = useCallback((timeout) => {
if (timerId.current) {
clearTimer();
}
const resetTimer = useCallback(
(timeout) => {
if (timerId.current) {
clearTimer();
}
if (initialEnter) {
setInitialEnter(false);
}
if (initialEnter) {
setInitialEnter(false);
}
timerId.current = setTimeout(() => {
setSideContentsVisible(false);
// setBelowContentsVisible(false);
}, timeout);
}, []);
timerId.current = setTimeout(() => {
setSideContentsVisible(false);
// setBelowContentsVisible(false);
}, timeout);
},
[clearTimer, initialEnter, setInitialEnter, setSideContentsVisible]
);
const clearTimerV2 = useCallback(() => {
clearTimeout(timerIdV2.current);
timerIdV2.current = null;
}, []);
const resetTimerV2 = useCallback((timeout) => {
// console.log('[TabContainerV2] resetTimerV2 호출', timeout);
if (timerIdV2.current) {
// console.log('[TabContainerV2] 기존 타이머 클리어');
clearTimerV2();
}
const resetTimerV2 = useCallback(
(timeout) => {
// console.log('[TabContainerV2] resetTimerV2 호출', timeout);
if (timerIdV2.current) {
// console.log('[TabContainerV2] 기존 타이머 클리어');
clearTimerV2();
}
if (initialEnterV2) {
// console.log('[TabContainerV2] initialEnterV2 false로 변경');
setInitialEnterV2(false);
}
if (initialEnterV2) {
// console.log('[TabContainerV2] initialEnterV2 false로 변경');
setInitialEnterV2(false);
}
timerIdV2.current = setTimeout(() => {
// console.log('[TabContainerV2] 타이머 실행 - belowContentsVisible false로 변경');
setBelowContentsVisible(false);
}, timeout);
}, []);
timerIdV2.current = setTimeout(() => {
// console.log('[TabContainerV2] 타이머 실행 - belowContentsVisible false로 변경');
setBelowContentsVisible(false);
}, timeout);
},
[clearTimerV2, initialEnterV2, setInitialEnterV2, setBelowContentsVisible]
);
// Redux로 오버레이 숨김
useEffect(() => {
@@ -1982,7 +1987,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
clearTimer();
}
};
}, [showSideContents, videoVerticalVisible, tabContainerVersion]);
}, [
showSideContents,
videoVerticalVisible,
tabContainerVersion,
resetTimer,
initialEnter,
clearTimer,
]);
useEffect(() => {
if (initialEnter || !sideContentsVisible || videoVerticalVisible) return;
@@ -2041,7 +2053,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
clearTimerV2();
}
};
}, [showBelowContents, videoVerticalVisible, tabContainerVersion]);
}, [
showBelowContents,
videoVerticalVisible,
tabContainerVersion,
resetTimerV2,
initialEnterV2,
clearTimerV2,
]);
useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`);