[251011] feat: MediaPlayer 추가

🕐 커밋 시간: 2025. 10. 11. 09:07:17

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +416줄
  • 삭제: -518줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/actions/mediaActions.js
  + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less
  + com.twin.app.shoptime/src/views/MediaPanel/README.md

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/utils/Config.js
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/MainView/MainView.jsx (javascript):
     Added: resetWatchRecord()
     Deleted: resetWatchRecord()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx (javascript):
     Added: MediaPanel()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 개발 문서 및 가이드 개선

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-10-11 09:07:20 +09:00
parent 36bd48224c
commit 0db2966f86
7 changed files with 1238 additions and 518 deletions

View File

@@ -1,33 +1,19 @@
import React, {
use,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { use, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { indexOf } from 'ilib/lib/JSUtils';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import platform from '@enact/core/platform';
import Spotlight from '@enact/spotlight';
import defaultWatchItem
from '../../../assets/images/img-alert-banner-st@3x.png';
import defaultWatchItem from '../../../assets/images/img-alert-banner-st@3x.png';
// 테스트용 - TODO: 메인 홈 화면에 나와야 하는 이미지들 추가 후 preloadImages에 추가
import testImage from '../../../assets/images/img-banner-myinfo-login@3x.png';
import defaultImageItem
from '../../../assets/images/img-thumb-empty-product@3x.png';
import LoadingPreloadImage
from '../../../assets/images/intro/splash_02_stop.webp';
import defaultImageItem from '../../../assets/images/img-thumb-empty-product@3x.png';
import LoadingPreloadImage from '../../../assets/images/intro/splash_02_stop.webp';
import LoadingAnimation from '../../../assets/images/intro/splash_03_end.webp';
import LoadingCompleteImage
from '../../../assets/images/intro/splash_04_end.webp';
import LoadingCompleteImage from '../../../assets/images/intro/splash_04_end.webp';
import LoadingShopOnTvImage from '../../../assets/images/intro/splash_end.jpg';
import {
alertToast,
@@ -38,38 +24,22 @@ import {
setHidePopup,
setShowPopup,
} from '../../actions/commonActions';
import {
getHomeMenu,
getHomeTerms,
updateHomeInfo,
} from '../../actions/homeActions';
import { getHomeMenu, getHomeTerms, updateHomeInfo } from '../../actions/homeActions';
import {
sendLogAlarmClick,
sendLogAlarmPop,
sendLogLive,
sendLogVOD,
} from '../../actions/logActions';
import {
getMyUpcomingAlertShow,
setMyTermsWithdraw,
} from '../../actions/myPageActions';
import {
popPanel,
pushPanel,
resetPanels,
updatePanel,
} from '../../actions/panelActions';
import EndOfServicePopUp
from '../../components/EndOfServicePopUp/EndOfServicePopUp';
import { getMyUpcomingAlertShow, setMyTermsWithdraw } from '../../actions/myPageActions';
import { popPanel, pushPanel, resetPanels, updatePanel } from '../../actions/panelActions';
import EndOfServicePopUp from '../../components/EndOfServicePopUp/EndOfServicePopUp';
import Loader from '../../components/Loader/Loader';
import { convertUtcToLocal } from '../../components/MediaPlayer/util';
import OptionalTermsConfirm
from '../../components/Optional/OptionalTermsConfirm';
import OptionalTermsConfirmBottom
from '../../components/Optional/OptionalTermsConfirmBottom';
import OptionalTermsConfirm from '../../components/Optional/OptionalTermsConfirm';
import OptionalTermsConfirmBottom from '../../components/Optional/OptionalTermsConfirmBottom';
import PreloadImage from '../../components/PreloadImage/PreloadImage';
import SystemNotification
from '../../components/SystemNotification/SystemNotification';
import SystemNotification from '../../components/SystemNotification/SystemNotification';
import TabLayout from '../../components/TabLayout/TabLayout';
import TButton from '../../components/TButton/TButton';
import TNewPopUp from '../../components/TPopUp/TNewPopUp';
@@ -77,11 +47,7 @@ import TPopUp from '../../components/TPopUp/TPopUp';
import usePrevious from '../../hooks/usePrevious';
import * as Config from '../../utils/Config';
import { panel_names } from '../../utils/Config';
import {
$L,
getErrorMessage,
getSpottableDescendants,
} from '../../utils/helperMethods';
import { $L, getErrorMessage, getSpottableDescendants } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import CartPanel from '../CartPanel/CartPanel';
import CategoryPanel from '../CategoryPanel/CategoryPanel';
@@ -98,6 +64,7 @@ import IntroPanel from '../IntroPanel/IntroPanel.new';
import JustForYouPanel from '../JustForYouPanel/JustForYouPanel';
import JustForYouTestPanel from '../JustForYouTestPanel/JustForYouTestPanel';
import LoadingPanel from '../LoadingPanel/LoadingPanel';
import MediaPanel from '../MediaPanel/MediaPanel';
import MyPagePanel from '../MyPagePanel/MyPagePanel';
import OnSalePanel from '../OnSalePanel/OnSalePanel';
import PlayerPanel from '../PlayerPanel/PlayerPanel';
@@ -134,6 +101,7 @@ const panelMap = {
[Config.panel_names.DETAIL_PANEL]: DetailPanel,
[Config.panel_names.PLAYER_PANEL]: PlayerPanel,
[Config.panel_names.PLAYER_PANEL_NEW]: PlayerPanelNew,
[Config.panel_names.MEDIA_PANEL]: MediaPanel,
[Config.panel_names.CHECKOUT_PANEL]: CheckOutPanel,
[Config.panel_names.WELCOME_EVENT_PANEL]: WelcomeEventPanel,
[Config.panel_names.THEME_CURATION_PANEL]: ThemeCurationPanel,
@@ -153,12 +121,12 @@ const logTpNoLiveSet = new Set([
]);
const STRING_CONF = {
YES: "YES",
NO: "NO",
EXIT: "EXIT",
THIS_IS_AN_UNSUPPORTED_COUNTRY: "This is an unsupported country.",
YES: 'YES',
NO: 'NO',
EXIT: 'EXIT',
THIS_IS_AN_UNSUPPORTED_COUNTRY: 'This is an unsupported country.',
CHANGE_COUNTRY:
"If you have changed your LG service country setting, the usage information in Shop Time during your previous country setting will be deleted. Do you want to proceed?",
'If you have changed your LG service country setting, the usage information in Shop Time during your previous country setting will be deleted. Do you want to proceed?',
};
export default function MainView({ className, initService }) {
@@ -175,22 +143,13 @@ export default function MainView({ className, initService }) {
data: errorCode,
data: popupData,
} = useSelector((state) => state.common.popup);
const {
showLoadingPanel,
toast,
toastText,
isLoading,
webOSVersion,
deviceId,
} = useSelector((state) => state.common.appStatus);
const skipEndOfServicePopup = useSelector(
(state) => state.localSettings.skipEndOfServicePopup
);
const isInternetConnected = useSelector(
(state) => state.common.appStatus.isInternetConnected
const { showLoadingPanel, toast, toastText, isLoading, webOSVersion, deviceId } = useSelector(
(state) => state.common.appStatus
);
const skipEndOfServicePopup = useSelector((state) => state.localSettings.skipEndOfServicePopup);
const isInternetConnected = useSelector((state) => state.common.appStatus.isInternetConnected);
const deviceCountryCode = httpHeader?.["X-Device-Country"] || "";
const deviceCountryCode = httpHeader?.['X-Device-Country'] || '';
const isLogSentRef = useRef(false);
const watchRecord = useSelector((state) => state.localSettings?.watchRecord);
const watchRecordRef = usePrevious(watchRecord);
@@ -202,21 +161,18 @@ export default function MainView({ className, initService }) {
const topPanel = panels[panels.length - 1];
useEffect(() => {
console.log("🔍 MainView 팝업 상태 변경:", {
console.log('🔍 MainView 팝업 상태 변경:', {
popupVisible,
activePopup,
});
}, [popupVisible, activePopup]);
const isHomeOnTop = useMemo(() => {
return (
!mainIndex &&
(panels.length <= 0 || (panels.length === 1 && panels[0].panelInfo.modal))
);
return !mainIndex && (panels.length <= 0 || (panels.length === 1 && panels[0].panelInfo.modal));
}, [mainIndex, panels]);
const onPreImageLoadComplete = useCallback(() => {
console.log("MainView onPreImageLoadComplete");
console.log('MainView onPreImageLoadComplete');
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
setImagePreloaded(true);
}, [dispatch]);
@@ -230,9 +186,10 @@ export default function MainView({ className, initService }) {
let renderingPanels = [];
if (
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1]?.name ===
Config.panel_names.PLAYER_PANEL_NEW ||
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
panels[panels.length - 1]?.name === Config.panel_names.MEDIA_PANEL ||
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL
) {
renderingPanels = panels.slice(-2);
} else {
@@ -243,11 +200,9 @@ export default function MainView({ className, initService }) {
{(isHomeOnTop ||
(panels.length === 1 &&
(panels[0]?.name === Config.panel_names.PLAYER_PANEL ||
panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW))) && (
<HomePanel
key={Config.panel_names.HOME_PANEL}
isOnTop={isHomeOnTop}
/>
panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
panels[0]?.name === Config.panel_names.MEDIA_PANEL))) && (
<HomePanel key={Config.panel_names.HOME_PANEL} isOnTop={isHomeOnTop} />
)}
{renderingPanels.map((panel, index) => {
const Component = panelMap[panel.name];
@@ -266,8 +221,8 @@ export default function MainView({ className, initService }) {
index === 0 &&
renderingPanels.length === 2 &&
(renderingPanels[1].name === Config.panel_names.PLAYER_PANEL ||
renderingPanels[1].name ===
Config.panel_names.PLAYER_PANEL_NEW) &&
renderingPanels[1].name === Config.panel_names.PLAYER_PANEL_NEW ||
renderingPanels[1].name === Config.panel_names.MEDIA_PANEL) &&
renderingPanels[1].panelInfo.modal
) {
isPanelOnTop = true;
@@ -290,9 +245,7 @@ export default function MainView({ className, initService }) {
</>
);
} else if (isHomeOnTop) {
return (
<HomePanel key={Config.panel_names.HOME_PANEL} isOnTop={isHomeOnTop} />
);
return <HomePanel key={Config.panel_names.HOME_PANEL} isOnTop={isHomeOnTop} />;
}
return null;
@@ -306,7 +259,8 @@ export default function MainView({ className, initService }) {
if (panels && panels.length > 0) {
let targetName = panels[panels.length - 1].name;
if (
panels[panels.length - 1].name === Config.panel_names.PLAYER_PANEL &&
(panels[panels.length - 1].name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1].name === Config.panel_names.MEDIA_PANEL) &&
panels[panels.length - 1].panelInfo.modal
) {
targetName = panels[panels.length - 2]?.name;
@@ -319,10 +273,10 @@ export default function MainView({ className, initService }) {
function reload(response) {
if (response) {
if (response.retCode === 0 && typeof window === "object") {
if (response.retCode === 0 && typeof window === 'object') {
window.location.reload();
} else {
console.error("unknown error", response.retCode);
console.error('unknown error', response.retCode);
}
}
}
@@ -339,8 +293,8 @@ export default function MainView({ className, initService }) {
dispatch(
setMyTermsWithdraw(
{
mandatoryIncludeYn: "Y",
termsList: ["MST00401", "MST00402"],
mandatoryIncludeYn: 'Y',
termsList: ['MST00401', 'MST00402'],
},
reload
)
@@ -369,7 +323,8 @@ export default function MainView({ className, initService }) {
if (panels && panels.length > 0) {
let panel = panels[panels.length - 1];
if (
panels[panels.length - 1].name === Config.panel_names.PLAYER_PANEL &&
(panels[panels.length - 1].name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1].name === Config.panel_names.MEDIA_PANEL) &&
panels[panels.length - 1].panelInfo.modal
) {
panel = panels[panels.length - 2];
@@ -379,8 +334,7 @@ export default function MainView({ className, initService }) {
// focus to panel if it's not preview video status changing case
if (
!lastPanelAction ||
(lastPanelAction.indexOf("preview") !== 0 &&
lastPanelAction !== "update")
(lastPanelAction.indexOf('preview') !== 0 && lastPanelAction !== 'update')
) {
//do focus itself if has focusedContainerId
Spotlight.focus(panel?.name);
@@ -402,8 +356,8 @@ export default function MainView({ className, initService }) {
);
useEffect(() => {
document.addEventListener("cursorStateChange", cursorStateChange, false);
if (platform.platformName !== "webos") {
document.addEventListener('cursorStateChange', cursorStateChange, false);
if (platform.platformName !== 'webos') {
//for debug
dispatch(changeAppStatus({ cursorVisible: !platform.touchscreen }));
} else {
@@ -414,7 +368,7 @@ export default function MainView({ className, initService }) {
);
}
return () => {
document.removeEventListener("cursorStateChange", cursorStateChange);
document.removeEventListener('cursorStateChange', cursorStateChange);
};
}, []);
@@ -422,9 +376,7 @@ export default function MainView({ className, initService }) {
const [alertItems, setAlertItems] = useState([]);
const popupTimerRef = useRef(null); // 타이머 ID를 저장할 상태 변수
const { upComingAlertShow } = useSelector(
(state) => state.myPage.upComingData
);
const { upComingAlertShow } = useSelector((state) => state.myPage.upComingData);
useEffect(() => {
if (
@@ -447,8 +399,8 @@ export default function MainView({ className, initService }) {
dispatch(
sendLogAlarmPop({
alarmDt,
alarmType: "Upcoming",
cnt: "0",
alarmType: 'Upcoming',
cnt: '0',
hstNm,
lgCatCd,
lgCatNm,
@@ -493,13 +445,8 @@ export default function MainView({ className, initService }) {
// 알림 팝업
const checkForAlerts = () => {
if (
upComingAlertShow?.upcomAlamUseFlag === "Y" &&
upComingAlertShow.alertShows?.length > 0
) {
const alertList = upComingAlertShow.alertShows.filter((show) =>
isSameDateTime(show.strtDt)
);
if (upComingAlertShow?.upcomAlamUseFlag === 'Y' && upComingAlertShow.alertShows?.length > 0) {
const alertList = upComingAlertShow.alertShows.filter((show) => isSameDateTime(show.strtDt));
if (alertList.length > 0) {
setAlertItems(alertList);
@@ -539,11 +486,11 @@ export default function MainView({ className, initService }) {
dispatch(
sendLogAlarmClick({
alarmDt,
alarmType: "Upcoming",
clickFlag: "Ok",
cnt: "0",
alarmType: 'Upcoming',
clickFlag: 'Ok',
cnt: '0',
hstNm,
keywordList: "",
keywordList: '',
lgCatCd,
lgCatNm,
logTpNo: Config.LOG_TP_NO.ALARM_CLICK.BROADCAST,
@@ -558,7 +505,7 @@ export default function MainView({ className, initService }) {
dispatch(
pushPanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "upcoming", patnrId: patnrId.toString() },
panelInfo: { from: 'upcoming', patnrId: patnrId.toString() },
})
);
dispatch(setHidePopup());
@@ -596,11 +543,11 @@ export default function MainView({ className, initService }) {
dispatch(
sendLogAlarmClick({
alarmDt,
alarmType: "Upcoming",
clickFlag: "No",
cnt: "0",
alarmType: 'Upcoming',
clickFlag: 'No',
cnt: '0',
hstNm,
keywordList: "",
keywordList: '',
lgCatCd,
lgCatNm,
logTpNo: Config.LOG_TP_NO.ALARM_CLICK.BROADCAST,
@@ -616,17 +563,13 @@ export default function MainView({ className, initService }) {
}, [alertItems, dispatch]);
const sendLogIfNeeded = useCallback(() => {
if (
!watchRecordRef.current ||
Object.keys(watchRecordRef.current).length === 0
) {
if (!watchRecordRef.current || Object.keys(watchRecordRef.current).length === 0) {
return;
}
const { logTpNo } = watchRecordRef.current;
const sendLog = logTpNoLiveSet.has(logTpNo) ? sendLogLive : sendLogVOD;
const resetWatchRecord = () =>
dispatch(changeLocalSettings({ watchRecord: {} }));
const resetWatchRecord = () => dispatch(changeLocalSettings({ watchRecord: {} }));
dispatch(sendLog({ ...watchRecordRef.current }, resetWatchRecord));
}, [dispatch]);
@@ -650,7 +593,7 @@ export default function MainView({ className, initService }) {
activePopup === Config.ACTIVE_POPUP.unSupportedCountryPopup
) {
const timeoutId = setTimeout(() => {
Spotlight.focus("tPopupBtn1");
Spotlight.focus('tPopupBtn1');
}, 0);
return () => clearTimeout(timeoutId);
@@ -658,7 +601,7 @@ export default function MainView({ className, initService }) {
}, [activePopup, loadingComplete]);
useEffect(() => {
if (deviceCountryCode === "US") {
if (deviceCountryCode === 'US') {
setAriaHidden(false);
} else {
setAriaHidden(true);
@@ -672,12 +615,7 @@ export default function MainView({ className, initService }) {
}
}, [webOSVersion]);
const handleErrorPopupClose = useCallback(() => {
console.log(
"handleErrorPopupClose 호출됨! activePopup:",
activePopup,
"popupData:",
popupData
);
console.log('handleErrorPopupClose 호출됨! activePopup:', activePopup, 'popupData:', popupData);
if (popupData?.shouldPopPanel) {
dispatch(popPanel());
}
@@ -693,34 +631,23 @@ export default function MainView({ className, initService }) {
}, [dispatch, popupData, activePopup, topPanel?.name]);
// 딥링크 확인 테스트
const contentTarget = useSelector(
(state) => state.common.deepLinkInfo.contentTarget
);
const contentTarget = useSelector((state) => state.common.deepLinkInfo.contentTarget);
return (
<div
className={classNames(css.mainViewWrap, className)}
aria-hidden={ariaHidden}
>
<div className={classNames(css.mainViewWrap, className)} aria-hidden={ariaHidden}>
{!imagePreloaded && (
<PreloadImage
preloadImages={preloadImages}
onLoadComplete={onPreImageLoadComplete}
/>
<PreloadImage preloadImages={preloadImages} onLoadComplete={onPreImageLoadComplete} />
)}
<LoadingPanel showLoadingPanel={showLoadingPanel} />
{errorCode !== 502 &&
errorCode !== 602 &&
errorCode !== 603 &&
loadingComplete && (
<div className={classNames(css.container)}>
<div className={classNames(css.mainlayout)}>{renderTopPanel()}</div>
<TabLayout
topPanelName={topPanelName}
onTabActivated={onTabActivated}
panelInfo={currentTabLayoutPanelInfo}
/>
</div>
)}
{errorCode !== 502 && errorCode !== 602 && errorCode !== 603 && loadingComplete && (
<div className={classNames(css.container)}>
<div className={classNames(css.mainlayout)}>{renderTopPanel()}</div>
<TabLayout
topPanelName={topPanelName}
onTabActivated={onTabActivated}
panelInfo={currentTabLayoutPanelInfo}
/>
</div>
)}
{isLoading && <Loader />}
{/* 시청예약 알림 팝업 */}
@@ -736,14 +663,13 @@ export default function MainView({ className, initService }) {
logo={alertItems[0]?.patncLogoPath}
hasThumbnail
thumbnail={
alertItems[0]?.thumbnailUrl == null ||
alertItems[0]?.thumbnailUrl == ""
alertItems[0]?.thumbnailUrl == null || alertItems[0]?.thumbnailUrl == ''
? defaultWatchItem
: alertItems[0]?.thumbnailUrl
}
button1Text={$L("YES")}
button2Text={$L("NO")}
title={$L("Watch Now!")}
button1Text={$L('YES')}
button2Text={$L('NO')}
title={$L('Watch Now!')}
text={alertItems[0]?.showNm}
/>
)}
@@ -764,9 +690,9 @@ export default function MainView({ className, initService }) {
popupData.retDetailCode,
popupData.returnBindStrings
)}
</p>{" "}
</p>{' '}
<TButton className={css.popupBtn} onClick={handleErrorPopupClose}>
{$L("OK")}
{$L('OK')}
</TButton>
</div>
</TPopUp>
@@ -777,21 +703,17 @@ export default function MainView({ className, initService }) {
kind="textPopup"
hasButton
hasText
button1Text={isInternetConnected ? "Retry" : ""}
button2Text={"Exit"}
button1Text={isInternetConnected ? 'Retry' : ''}
button2Text={'Exit'}
onClick={onClickNetworkError}
onClose={onExitNetworkError}
open={popupVisible}
text={
"Failed to connect to SHOPTIME. Please check your device's network connection."
}
text={"Failed to connect to SHOPTIME. Please check your device's network connection."}
/>
)}
{(loadingComplete &&
activePopup === Config.ACTIVE_POPUP.unSupportedCountryPopup) ||
(loadingComplete &&
activePopup === Config.ACTIVE_POPUP.changeCountyPopup) ? (
{(loadingComplete && activePopup === Config.ACTIVE_POPUP.unSupportedCountryPopup) ||
(loadingComplete && activePopup === Config.ACTIVE_POPUP.changeCountyPopup) ? (
<TPopUp
kind="textPopup"
hasButton
@@ -801,15 +723,12 @@ export default function MainView({ className, initService }) {
? $L(STRING_CONF.YES)
: $L(STRING_CONF.EXIT)
}
button2Text={
activePopup == Config.ACTIVE_POPUP.changeCountyPopup &&
$L(STRING_CONF.NO)
}
button2Text={activePopup == Config.ACTIVE_POPUP.changeCountyPopup && $L(STRING_CONF.NO)}
onClick={handlePopupClick}
onClose={handlePopupClose}
open={popupVisible}
text={getMessageByPopupType(activePopup)}
title={$L("Exit Shop Time")}
title={$L('Exit Shop Time')}
/>
) : null}
@@ -827,7 +746,7 @@ export default function MainView({ className, initService }) {
hasText
hasButton
button1Text={toastText}
button2Text={$L("OK")}
button2Text={$L('OK')}
/>
)}
{/* OptionalTermsConfirmPopup */}
@@ -851,15 +770,15 @@ export default function MainView({ className, initService }) {
{/* /딥링크 테스트 */}
<div
style={{
position: "fixed",
left: "0",
bottom: "0",
color: "#fff",
fontSize: "13px",
opacity: ".5",
zIndex: "999",
backgroundColor: "black",
padding: "10px",
position: 'fixed',
left: '0',
bottom: '0',
color: '#fff',
fontSize: '13px',
opacity: '.5',
zIndex: '999',
backgroundColor: 'black',
padding: '10px',
}}
>
deepLinkInfo