From 556a0e8456c9a1824604db79210910d510047232 Mon Sep 17 00:00:00 2001 From: djaco Date: Sun, 22 Jun 2025 22:19:57 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=84=A0=ED=83=9D=EC=95=BD=EA=B4=80=204?= =?UTF-8?q?=EC=B0=A8=EC=88=98=EC=A0=95=20250622?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- com.twin.app.shoptime/src/App/App.js | 142 +- .../src/actions/actionTypes.js | 11 + .../src/actions/playActions.js | 58 +- .../components/TButton/TButton.module.less | 4 +- .../src/components/TItemCard/TItemCard.jsx | 1 + .../src/components/TPopUp/TNewPopUp.jsx | 23 +- .../components/TPopUp/TNewPopUp.module.less | 16 +- .../src/hooks/useRightPanelContent.js | 113 + .../src/hooks/useSafeFocusState.js | 40 + .../src/hooks/useTermsStateMachine.js | 95 + .../src/reducers/homeReducer.js | 23 + com.twin.app.shoptime/src/utils/Config.js | 1 + .../views/HomePanel/HomeBanner/HomeBanner.jsx | 233 +- .../HomeBanner/PersistentVideoUnit.jsx | 155 ++ .../HomePanel/HomeBanner/RandomUnit.new.jsx | 641 +++++ .../HomeBanner/RandomUnit.new.module.less | 281 ++ .../src/views/HomePanel/HomePanel.jsx | 64 +- .../src/views/IntroPanel/IntroPanel.new.jsx | 490 ++-- .../IntroPanel/IntroPanel.new.module.less | 86 +- .../src/views/MainView/MainView.jsx | 59 +- .../src/views/PlayerPanel/PlayerPanel.jsx | 18 +- .../src/views/PlayerPanel/PlayerPanel.new.jsx | 2281 +++++++++++++++++ .../PlayerPanel/PlayerPanel.new.module.less | 79 + 23 files changed, 4437 insertions(+), 477 deletions(-) create mode 100644 com.twin.app.shoptime/src/hooks/useRightPanelContent.js create mode 100644 com.twin.app.shoptime/src/hooks/useSafeFocusState.js create mode 100644 com.twin.app.shoptime/src/hooks/useTermsStateMachine.js create mode 100644 com.twin.app.shoptime/src/views/HomePanel/HomeBanner/PersistentVideoUnit.jsx create mode 100644 com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.jsx create mode 100644 com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.module.less create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.module.less diff --git a/com.twin.app.shoptime/src/App/App.js b/com.twin.app.shoptime/src/App/App.js index d05a3635..81e97a9f 100644 --- a/com.twin.app.shoptime/src/App/App.js +++ b/com.twin.app.shoptime/src/App/App.js @@ -1,28 +1,41 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; - -import { useDispatch, useSelector } from "react-redux"; - -import platform from "@enact/core/platform"; -import { Job } from "@enact/core/util"; -import ThemeDecorator from "@enact/sandstone/ThemeDecorator"; +import React, { + // useMemo, + useCallback, + useEffect, + useRef, + // useState, +} from "react"; +import { useSelector, useDispatch } from "react-redux"; +// import { I18nContext } from "@enact/i18n"; +// import classNames from "classnames"; +// import PropTypes from "prop-types"; import Spotlight from "@enact/spotlight"; +import { Job } from "@enact/core/util"; +import platform from "@enact/core/platform"; +import { ThemeDecorator } from "@enact/sandstone/ThemeDecorator"; + +// import "../../../assets/fontello/css/fontello.css"; -import appinfo from "../../webos-meta/appinfo.json"; import { changeAppStatus, - checkFirstLaunch, + // cancelFocusElement, + // focusElement, + // setExitApp, + // setPreventMouse, + // setShowPopup, + // setTermsAgreeYn, + getTermsAgreeYn, deleteOldDb8Datas, - getConnectionInfo, + sendBroadCast, getConnectionStatus, + getConnectionInfo, getDeviceId, getHttpHeaderForServiceRequest, getSystemSettings, - sendBroadCast, + checkFirstLaunch, setDeepLink, setGNBMenu, setSecondLayerInfo, - setShowPopup, - getTermsAgreeYn, } from "../actions/commonActions"; import { getShoptimeTerms } from "../actions/empActions"; import { getHomeMenu, getHomeTerms } from "../actions/homeActions"; @@ -36,7 +49,14 @@ import usePrevious from "../hooks/usePrevious"; import { lunaTest } from "../lunaSend/lunaTest"; import { store } from "../store/store"; import * as Config from "../utils/Config"; -import { $L, clearLaunchParams, getLaunchParams } from "../utils/helperMethods"; +import { + // $L, + clearLaunchParams, + // getCountry, + getLaunchParams, + // getUUID, + // resizeTo, +} from "../utils/helperMethods"; import { SpotlightIds } from "../utils/SpotlightIds"; import ErrorBoundary from "../views/ErrorBoundary"; import MainView from "../views/MainView/MainView"; @@ -44,7 +64,11 @@ import css from "./App.module.less"; import { handleBypassLink } from "./bypassLinkHandler"; import { handleDeepLink } from "./deepLinkHandler"; import { sendLogTotalRecommend } from "../actions/logActions"; -import { startFocusMonitoring, stopFocusMonitoring } from '../utils/focus-monitor'; +// import { +// startFocusMonitoring, +// stopFocusMonitoring, +// } from "../utils/focus-monitor"; +// import { PanelHoc } from "../components/TPanel/TPanel"; let foreGroundChangeTimer = null; @@ -80,7 +104,7 @@ Spotlight.focus = function (elem, containerOption) { if (!floatLayerNode.contains(current)) { if (floatLayerNode.lastElementChild) { const spottableNode = floatLayerNode.lastElementChild.querySelector( - '[data-spotlight-container="true"]' + '[data-spotlight-container="true"]', ); if (spottableNode) { originFocus.apply(this, [spottableNode]); // this 바인딩을 유지하여 originFocus 호출 @@ -98,7 +122,7 @@ Spotlight.focus = function (elem, containerOption) { sendBroadCast({ type: "deActivateTab", moreInfo: { reason: "focus" }, - }) + }), ); } } @@ -111,15 +135,15 @@ function AppBase(props) { const httpHeader = useSelector((state) => state.common.httpHeader); const httpHeaderRef = useRef(httpHeader); const webOSVersion = useSelector( - (state) => state.common.appStatus.webOSVersion + (state) => state.common.appStatus.webOSVersion, ); const deviceId = useSelector((state) => state.common.appStatus.deviceId); const loginUserData = useSelector( - (state) => state.common.appStatus.loginUserData + (state) => state.common.appStatus.loginUserData, ); const loginUserDataRef = useRef(loginUserData); const cursorVisible = useSelector( - (state) => state.common.appStatus.cursorVisible + (state) => state.common.appStatus.cursorVisible, ); const introTermsAgree = useSelector((state) => state.common.introTermsAgree); // const optionalTermsAgree = useSelector((state) => state.common.optionalTermsAgree); @@ -128,15 +152,6 @@ function AppBase(props) { // const termsFlag = useSelector((state) => state.common.termsFlag); const termsData = useSelector((state) => state.home.termsData); - // const shouldShowOptionalTermsPopup = useMemo(() => { - // const terms = termsData?.data?.terms; - // if (!terms) { - // return false; - // } - // const optionalTerm = terms.find(term => term.trmsTpCd === "MST00405"); - // return optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false; - // }, [termsData]); - useEffect(() => { if (termsData?.data?.terms) { dispatch(getTermsAgreeYn()); @@ -146,9 +161,9 @@ function AppBase(props) { const introTermsAgreeRef = usePrevious(introTermsAgree); const logEnable = useSelector((state) => state.localSettings.logEnable); const oldDb8Deleted = useSelector( - (state) => state.localSettings.oldDb8Deleted + (state) => state.localSettings.oldDb8Deleted, ); - const macAddress = useSelector((state) => state.common.macAddress); + // const macAddress = useSelector((state) => state.common.macAddress); const deviceCountryCode = httpHeader?.["X-Device-Country"] || ""; @@ -170,13 +185,13 @@ function AppBase(props) { if (!oldDb8Deleted) { dispatch(deleteOldDb8Datas()); } - }, [oldDb8Deleted]); + }, [oldDb8Deleted, dispatch]); const hideCursor = useRef( new Job((func) => { func(); console.log("hide cursor"); - }, 5000) + }, 5000), ); // 컴포넌트에서 모니터링 시작 - 한시적 모니터링 @@ -199,11 +214,13 @@ function AppBase(props) { // called by [receive httpHeader, launch, relaunch] const initService = useCallback( (haveyInit = true) => { + /* console.log( "<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ", haveyInit, appinfo ); + */ if (httpHeaderRef.current) { if (haveyInit) { dispatch(changeAppStatus({ connectionFailed: false })); @@ -211,7 +228,7 @@ function AppBase(props) { dispatch( changeAppStatus({ cursorVisible: window.PalmSystem?.cursor?.visibility, - }) + }), ); } dispatch(getHomeMenu()); @@ -224,7 +241,7 @@ function AppBase(props) { console.log( "initService...{haveyInit, launchParams}", haveyInit, - JSON.stringify(launchParams) + JSON.stringify(launchParams), ); // pyh TODO: edit or delete later (line 196 ~ 198) @@ -239,7 +256,7 @@ function AppBase(props) { contextName: Config.LOG_CONTEXT_NAME.ENTRY, messageId: Config.LOG_MESSAGE_ID.ENTRY_INFO, entry_menu: "App", - }) + }), ); } @@ -249,12 +266,12 @@ function AppBase(props) { contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, visible: true, - }) + }), ); clearLaunchParams(); } }, - [dispatch] + [dispatch], ); const handleRelaunchEvent = useCallback(() => { @@ -265,7 +282,7 @@ function AppBase(props) { if (typeof window === "object" && window.PalmSystem) { window.PalmSystem.activate(); } - }, [initService, dispatch]); + }, [initService, introTermsAgreeRef]); const visibilityChanged = useCallback(() => { console.log("document is hidden", document.hidden); @@ -279,7 +296,7 @@ function AppBase(props) { foreGroundChangeTimer = setTimeout(() => { console.log( "visibility changed !!! ==> set to foreground cursorVisible", - JSON.stringify(window.PalmSystem?.cursor?.visibility) + JSON.stringify(window.PalmSystem?.cursor?.visibility), ); // eslint-disable-line no-console if (platform.platformName !== "webos") { //for debug @@ -287,19 +304,19 @@ function AppBase(props) { changeAppStatus({ isAppForeground: true, cursorVisible: !platform.touchscreen, - }) + }), ); } else if (typeof window === "object") { dispatch( changeAppStatus({ isAppForeground: true, cursorVisible: window.PalmSystem?.cursor?.visibility, - }) + }), ); } }, 1000); } - }, [dispatch, initService]); + }, [dispatch]); useEffect(() => { const keyDownEvent = (event) => { @@ -342,7 +359,7 @@ function AppBase(props) { document.removeEventListener("wheel", mouseMoveEvent); } }; - }, [dispatch]); + }, [dispatch, visibilityChanged, handleRelaunchEvent]); useEffect(() => { let userDataChanged = false; @@ -358,7 +375,7 @@ function AppBase(props) { dispatch( changeAppStatus({ showLoadingPanel: { show: true, type: "launching" }, - }) + }), ); } dispatch(checkFirstLaunch()); @@ -367,13 +384,13 @@ function AppBase(props) { getHomeTerms({ mbrNo: loginUserData.userNumber, trmsTpCdList: "MST00401, MST00402, MST00405", // 선택약관 추가 25.06 - }) + }), ); httpHeaderRef.current = httpHeader; } loginUserDataRef.current = loginUserData; - }, [httpHeader, deviceId]); + }, [httpHeader, deviceId, dispatch, loginUserData]); useEffect(() => { if ( @@ -385,8 +402,7 @@ function AppBase(props) { ) { dispatch(getShoptimeTerms()); } - }, [webOSVersion, deviceId]); - + }, [webOSVersion, deviceId, dispatch, deviceCountryCode]); // 테스트용 인트로 화면 표시 // useEffect(() => { @@ -402,31 +418,17 @@ function AppBase(props) { // 약관 동의 여부 확인 전에는 아무것도 하지 않음 return; } - + if (introTermsAgree) { - // 필수 약관에 동의한 경우 - // if (shouldShowOptionalTermsPopup) { - // 선택 약관 팝업을 띄워야 하는 경우 - // 3초 후에 팝업을 띄우도록 설정 - // console.log("App.js optionalTermsTest 팝업 표시"); - // const timer = setTimeout(() => { - // dispatch(setShowPopup({ activePopup: "optionalTermsConfirm" })); - // }, 3000); // 3000 milliseconds = 3 seconds - - // 컴포넌트 언마운트 시 타이머 클리어 - // return () => clearTimeout(timer); - // } else { - // 선택 약관 팝업이 필요 없는 경우, 바로 서비스 초기화 - initService(true); - // } + initService(true); } else { // 필수 약관에 동의하지 않은 경우 dispatch( - pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} }) + pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} }), ); dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); } - }, [introTermsAgree, dispatch, initService]); + }, [introTermsAgree, dispatch, initService, termsLoading]); useEffect(() => { const launchParmas = getLaunchParams(); @@ -442,7 +444,7 @@ function AppBase(props) { setDeepLink({ contentTarget: launchParmas.contentTarget, isDeepLink: true, - }) + }), ); } @@ -452,9 +454,9 @@ function AppBase(props) { deeplinkId: launchParmas.contentTarget ?? "", linkTpCd, logTpNo: Config.LOG_TP_NO.SECOND_LAYER, - }) + }), ); - }, [dispatch]); + }, [dispatch, initService]); return ( diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 06660e0f..7290b862 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -194,6 +194,17 @@ export const types = { GET_SUBTITLE: "GET_SUBTITLE", CLEAR_PLAYER_INFO: "CLEAR_PLAYER_INFO", + /** + * 홈 화면 배너의 비디오 재생 제어를 위한 액션 타입. + * 여러 컴포넌트가 동시에 비디오를 재생하려고 할 때 충돌을 방지하고, + * 하나의 비디오만 재생되도록 보장하는 역할을 합니다. + * + * SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다. + * CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다. + */ + SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL", + CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL", + // reset action RESET_REDUX_STATE: "RESET_REDUX_STATE", diff --git a/com.twin.app.shoptime/src/actions/playActions.js b/com.twin.app.shoptime/src/actions/playActions.js index bbdb6b04..dd03f9ae 100644 --- a/com.twin.app.shoptime/src/actions/playActions.js +++ b/com.twin.app.shoptime/src/actions/playActions.js @@ -25,18 +25,21 @@ let startVideoTimer = null; //start Full -> modal mode let startVideoFocusTimer = null; export const startVideoPlayer = - ({ modal, modalContainerId, modalClassName, spotlightDisable, ...rest }) => + ({ modal, modalContainerId, modalClassName, spotlightDisable, useNewPlayer, ...rest }) => (dispatch, getState) => { const panels = getState().panels.panels; const topPanel = panels[panels.length - 1]; let panelWorkingAction = pushPanel; - if (topPanel && topPanel.name === panel_names.PLAYER_PANEL) { + + const panelName = useNewPlayer ? panel_names.PLAYER_PANEL_NEW : panel_names.PLAYER_PANEL; + + if (topPanel && topPanel.name === panelName) { panelWorkingAction = updatePanel; } dispatch( panelWorkingAction( { - name: panel_names.PLAYER_PANEL, + name: panelName, panelInfo: { modal, modalContainerId, @@ -116,3 +119,52 @@ export const getSubTitle = export const CLEAR_PLAYER_INFO = () => ({ type: types.CLEAR_PLAYER_INFO, }); + +/** + * 비디오 재생 제어권을 요청하는 액션. + * 컴포넌트가 비디오를 재생하고 싶을 때 이 액션을 호출합니다. + * 'playerControl' 상태를 확인하여 현재 다른 컴포넌트가 비디오를 제어하고 있지 않은 경우에만 + * 비디오 플레이어를 활성화(PlayerPanel을 modal로 push)합니다. + * + * @param {string} ownerId - 비디오 제어권을 요청하는 컴포넌트의 고유 ID. + * @param {object} videoInfo - 'startVideoPlayer'에 필요한 비디오 정보. + */ +export const requestPlayControl = + (ownerId, videoInfo) => (dispatch, getState) => { + const { playerControl } = getState().home; + const currentOwnerId = playerControl.ownerId; + + // 이미 다른 컴포넌트가 제어권을 가지고 있다면, 먼저 해제한다. (선점) + if (currentOwnerId && currentOwnerId !== ownerId) { + dispatch(releasePlayControl(currentOwnerId, true)); // fromPreemption = true + } + + // 새로운 제어권을 설정하고 비디오를 재생한다. + dispatch({ + type: types.SET_PLAYER_CONTROL, + payload: { ownerId }, + }); + dispatch(startVideoPlayer({ ...videoInfo, modal: true })); + }; + +/** + * 비디오 재생 제어권을 해제하는 액션. + * 컴포넌트가 비디오 재생을 중단할 때(예: 포커스 잃음, 언마운트) 호출합니다. + * 현재 제어권을 가진 컴포넌트가 자신일 경우에만 'playerControl' 상태를 초기화하고 + * 비디오 플레이어를 종료(PlayerPanel을 pop)합니다. + * + * @param {string} ownerId - 비디오 제어권을 해제하려는 컴포넌트의 고유 ID. + * @param {boolean} fromPreemption - 다른 요청에 의해 강제로 해제되었는지 여부. + */ +export const releasePlayControl = (ownerId, fromPreemption = false) => (dispatch, getState) => { + const { playerControl } = getState().home; + + // 제어권을 가진 컴포넌트가 자신일 경우에만 해제 + // 단, 선점 로직에 의해 호출된 경우는 소유권 확인 없이 즉시 실행 + if (fromPreemption || playerControl.ownerId === ownerId) { + dispatch({ + type: types.CLEAR_PLAYER_CONTROL, + }); + dispatch(finishVideoPreview()); + } +}; diff --git a/com.twin.app.shoptime/src/components/TButton/TButton.module.less b/com.twin.app.shoptime/src/components/TButton/TButton.module.less index c96d0817..a1ecffda 100644 --- a/com.twin.app.shoptime/src/components/TButton/TButton.module.less +++ b/com.twin.app.shoptime/src/components/TButton/TButton.module.less @@ -145,7 +145,7 @@ border-radius: 10px; box-sizing: border-box; .flex(); - box-shadow: 0 5px 5px #003 0 6px 7px #0000001a; + box-shadow: 0 5px 5px #003, 0 6px 7px #0000001a; line-height: normal; &:focus { @@ -165,7 +165,7 @@ border-radius: 10px; box-sizing: border-box; .flex(); - box-shadow: 0 5px 5px #003 0 6px 7px #0000001a; + box-shadow: 0 5px 5px #003, 0 6px 7px #0000001a; line-height: normal; &:focus { diff --git a/com.twin.app.shoptime/src/components/TItemCard/TItemCard.jsx b/com.twin.app.shoptime/src/components/TItemCard/TItemCard.jsx index 6a457f5d..f7d85a36 100644 --- a/com.twin.app.shoptime/src/components/TItemCard/TItemCard.jsx +++ b/com.twin.app.shoptime/src/components/TItemCard/TItemCard.jsx @@ -80,6 +80,7 @@ export default memo(function TItemCard({ nowProductId, nowCategory, nowProductTitle, + contentId, ...rest }) { const dispatch = useDispatch(); diff --git a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx index 4cf6dc7d..ecafd03e 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx +++ b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx @@ -286,6 +286,8 @@ export default function TNewPopUp({ onOptionalAgreeClick, // Agree 버튼용 onOptionalDeclineClick, // Not Now 버튼용 onIntroTermsAgreeClick, // introTerms Agree 버튼용 + showAgreeButton = false, + onAgreeClick, // onIntroTermsAgreeClick을 대체할 새로운 prop ...rest }) { const dispatch = useDispatch(); @@ -750,20 +752,21 @@ export default function TNewPopUp({ - {/* 기획 변경으로 Agree 버튼 임시 숨김 처리 - - {$L('Agree')} - - */} + {showAgreeButton && ( + + {$L('Agree')} + + )} {$L('Close')} diff --git a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less index 412283fc..d0ded1fb 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less +++ b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less @@ -1082,22 +1082,18 @@ .figmaTermsButtonContainer { display: flex; justify-content: center; - gap: 12px; + align-items: center; // 버튼 수직 정렬을 위해 추가 + gap: 15px; // 버튼 사이 간격 } .figmaTermsAgreeButton { - .size(240px, 80px); - - // type="agree"의 포커스 시 font-size 변경을 막음 - &.focused, - &:focus { // :focus도 함께 처리 - font-size: 30px !important; - } + // 이제 TButton의 type="popup" 스타일을 사용하므로, + // 여기서는 추가적인 스타일이 필요 없습니다. + // margin-right는 gap으로 대체되었습니다. } .figmaTermsCloseButton { - // TButton의 기본 스타일을 그대로 사용하도록 크기만 지정 - .size(240px, 80px); + // TButton의 type="popup" 스타일을 사용합니다. } } } diff --git a/com.twin.app.shoptime/src/hooks/useRightPanelContent.js b/com.twin.app.shoptime/src/hooks/useRightPanelContent.js new file mode 100644 index 00000000..de48583b --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/useRightPanelContent.js @@ -0,0 +1,113 @@ +import React, { useState, useEffect, useRef } from "react"; +import { $L } from "../utils/helperMethods"; +import OptionalTermsInfo from "../views/MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; +import css from "../views/IntroPanel/IntroPanel.new.module.less"; + +const getPanelContent = ( + focusedItem, + termsChecked, + privacyChecked, + shouldShowBenefitsView +) => { + const requiredItemIds = [ + "termsCheckbox", + "termsButton", + "privacyCheckbox", + "privacyButton", + ]; + const isRequiredFocused = requiredItemIds.includes(focusedItem); + + // 1순위: 포커스가 필수 약관 관련 아이템에 있는 경우 + if (isRequiredFocused) { + // 필수 약관이 동의되지 않았을 때 (경고 메시지 포함) + if (!termsChecked || !privacyChecked) { + return ( +
+

{$L("Required Consent")}

+

+ {$L("(Necessary for using the service)")} +

+
+

+ {$L("Please agree to the required Terms & Conditions and")} +

+

+ {$L("Privacy Policy to start enjoying Shop Time")} +

+
+
+ ); + } + // 필수 약관이 모두 동의되었을 때 (단순 안내 메시지) + return ( +
+

{$L("Required Consent")}

+

+ {$L("(Necessary for using the service)")} +

+
+ ); + } + + // 2순위: 포커스가 그 외 모든 아이템에 있는 경우 + return ( + <> + {shouldShowBenefitsView ? ( +
+ {$L( + 'By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features' + )} +
+ ) : ( + + )} + + ); +}; + +const useRightPanelContent = ( + focusedItem, + termsChecked, + privacyChecked, + shouldShowBenefitsView +) => { + const [content, setContent] = useState(() => + getPanelContent( + focusedItem, + termsChecked, + privacyChecked, + shouldShowBenefitsView + ) + ); + const updateTimeoutRef = useRef(null); + + useEffect(() => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + + updateTimeoutRef.current = setTimeout(() => { + const newContent = getPanelContent( + focusedItem, + termsChecked, + privacyChecked, + shouldShowBenefitsView + ); + setContent(newContent); + }, 50); + + return () => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + }; + }, [focusedItem, termsChecked, privacyChecked, shouldShowBenefitsView]); + + return content; +}; + +export default useRightPanelContent; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/hooks/useSafeFocusState.js b/com.twin.app.shoptime/src/hooks/useSafeFocusState.js new file mode 100644 index 00000000..595c4341 --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/useSafeFocusState.js @@ -0,0 +1,40 @@ +import { useState, useCallback, useRef } from "react"; + +const useSafeFocusState = () => { + const [focusedItem, setFocusedItem] = useState(null); + const blurTimeoutRef = useRef(null); + + const setFocusAsync = useCallback((item) => { + return new Promise((resolve) => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + blurTimeoutRef.current = null; + } + if (process.env.NODE_ENV === "development") { + console.log("[useSafeFocusState] Focus Set:", item); + } + setFocusedItem(item); + resolve(item); + }); + }, []); + + const clearFocusAsync = useCallback((delay = 0) => { + return new Promise((resolve) => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } + blurTimeoutRef.current = setTimeout(() => { + if (process.env.NODE_ENV === "development") { + console.log("[useSafeFocusState] Focus Cleared"); + } + setFocusedItem(null); + blurTimeoutRef.current = null; + resolve(); + }, delay); + }); + }, []); + + return { focusedItem, setFocusAsync, clearFocusAsync }; +}; + +export default useSafeFocusState; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/hooks/useTermsStateMachine.js b/com.twin.app.shoptime/src/hooks/useTermsStateMachine.js new file mode 100644 index 00000000..a2f95e2a --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/useTermsStateMachine.js @@ -0,0 +1,95 @@ +import { useState, useCallback } from "react"; + +const useTermsStateMachine = (initialState) => { + const [isTransitioning, setIsTransitioning] = useState(false); + const [state, setState] = useState({ + termsChecked: false, + privacyChecked: false, + optionalChecked: false, + selectAllChecked: false, + error: null, // 1. 에러 상태 추가 + ...initialState, + }); + + const updateStateAsync = useCallback( + async (updates) => { + if (isTransitioning) { + const err = new Error("State transition already in progress."); + if (process.env.NODE_ENV === "development") { + console.warn("[useTermsStateMachine] Rejected:", err.message); + } + // 2. 에러 상태를 설정하고 throw + setState((s) => ({ ...s, error: err })); + throw err; + } + + setIsTransitioning(true); + // 3. 이전 에러 상태 초기화 + setState((s) => ({ ...s, error: null })); + + try { + const newState = await new Promise((resolve) => { + setState((currentState) => { + const nextState = { ...currentState, ...updates }; + + const { + termsChecked: currentTerms, + privacyChecked: currentPrivacy, + optionalChecked: currentOptional, + } = currentState; + + const termsChecked = + "termsChecked" in updates + ? updates.termsChecked + : currentTerms; + const privacyChecked = + "privacyChecked" in updates + ? updates.privacyChecked + : currentPrivacy; + const optionalChecked = + "optionalChecked" in updates + ? updates.optionalChecked + : currentOptional; + + if ( + "termsChecked" in updates || + "privacyChecked" in updates || + "optionalChecked" in updates + ) { + nextState.selectAllChecked = + termsChecked && privacyChecked && optionalChecked; + } else if ("selectAllChecked" in updates) { + nextState.termsChecked = updates.selectAllChecked; + nextState.privacyChecked = updates.selectAllChecked; + nextState.optionalChecked = updates.selectAllChecked; + } + + if (process.env.NODE_ENV === "development") { + console.log( + "[useTermsStateMachine] State transition:", + currentState, + "->", + nextState + ); + } + resolve(nextState); + return nextState; + }); + }); + setIsTransitioning(false); + return newState; + } catch (error) { + setIsTransitioning(false); + console.error("[useTermsStateMachine] State update failed:", error); + // 4. 에러 상태를 설정하고 다시 throw + setState((s) => ({ ...s, error })); + throw error; + } + }, + [isTransitioning] + ); + + return { state, updateStateAsync }; +}; + +export default useTermsStateMachine; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/reducers/homeReducer.js b/com.twin.app.shoptime/src/reducers/homeReducer.js index 77bfa976..988084e6 100644 --- a/com.twin.app.shoptime/src/reducers/homeReducer.js +++ b/com.twin.app.shoptime/src/reducers/homeReducer.js @@ -21,6 +21,9 @@ const initialState = { homeInfo: null, curationId: "", curationTitle: "", + playerControl: { + ownerId: null, + }, }; export const homeReducer = (state = initialState, action) => { @@ -189,6 +192,26 @@ export const homeReducer = (state = initialState, action) => { } } + case types.SET_PLAYER_CONTROL: { + return { + ...state, + playerControl: { + ...state.playerControl, + ownerId: action.payload.ownerId, + }, + }; + } + + case types.CLEAR_PLAYER_CONTROL: { + return { + ...state, + playerControl: { + ...state.playerControl, + ownerId: null, + }, + }; + } + default: return state; } diff --git a/com.twin.app.shoptime/src/utils/Config.js b/com.twin.app.shoptime/src/utils/Config.js index 2857e36a..32e423af 100644 --- a/com.twin.app.shoptime/src/utils/Config.js +++ b/com.twin.app.shoptime/src/utils/Config.js @@ -25,6 +25,7 @@ export const panel_names = { CONFIRM_PANEL: "confirmpanel", DETAIL_PANEL: "detailpanel", PLAYER_PANEL: "playerpanel", + PLAYER_PANEL_NEW: "playerpanel.new", CHECKOUT_PANEL: "checkoutpanel", THEME_CURATION_PANEL: "themeCurationPanel", IMAGE_PANEL: "imagepanel", diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx index 4059f00c..b6ddfd7b 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx @@ -6,28 +6,38 @@ import { useDispatch, useSelector } from "react-redux"; import Spotlight from "@enact/spotlight"; import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator"; import Spottable from "@enact/spotlight/Spottable"; -import { $L, scaleH, scaleW } from '../../../utils/helperMethods'; -import { setDefaultFocus, setShowPopup, fetchCurrentUserHomeTerms } from "../../../actions/homeActions"; +import { $L, scaleH, scaleW } from "../../../utils/helperMethods"; +import { + setDefaultFocus, + setShowPopup, + fetchCurrentUserHomeTerms, +} from "../../../actions/homeActions"; import { changeAppStatus } from "../../../actions/commonActions"; -import { setMyPageTermsAgree } from '../../../actions/myPageActions'; +import { setMyPageTermsAgree } from "../../../actions/myPageActions"; import { pushPanel } from "../../../actions/panelActions"; +import { + requestPlayControl, + releasePlayControl, +} from "../../../actions/playActions"; import CustomImage from "../../../components/CustomImage/CustomImage"; import css from "./HomeBanner.module.less"; import Random from "./RandomUnit"; import Rolling from "./RollingUnit"; +import RandomUnitNew from "./RandomUnit.new"; import TNewPopUp from "../../../components/TPopUp/TNewPopUp"; -import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller"; +// import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller"; import OptionalConfirm from "../../../components/Optional/OptionalConfirm"; -import * as Config from "../../../utils/Config"; +// import * as Config from "../../../utils/Config"; +import PersistentVideoUnit from "./PersistentVideoUnit"; const SpottableComponent = Spottable("div"); const Container = SpotlightContainerDecorator( { enterTo: "last-focused" }, - "div" + "div", ); const ContainerBasic = SpotlightContainerDecorator( { enterTo: "last-focused" }, - "div" + "div", ); export default function HomeBanner({ @@ -38,11 +48,11 @@ export default function HomeBanner({ }) { const dispatch = useDispatch(); const homeTopDisplayInfo = useSelector( - (state) => state.home.homeTopDisplayInfo + (state) => state.home.homeTopDisplayInfo, ); const bannerDataList = useSelector( - (state) => state.home.bannerData?.bannerInfos + (state) => state.home.bannerData?.bannerInfos, ); const popupVisible = useSelector((state) => state.common.popup.popupVisible); @@ -57,9 +67,39 @@ export default function HomeBanner({ } }, [handleItemFocus]); + const handleSecondBannerFocus = useCallback(() => { + const secondBannerData = bannerDataList?.[1]; + if (secondBannerData) { + const randomData = + secondBannerData.bannerDetailInfos[secondBannerData.randomIndex]; + const videoInfo = { + showUrl: randomData.showUrl, + patnrId: randomData.patnrId, + showId: randomData.showId, + shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA", + lgCatCd: randomData.lgCatCd, + chanId: randomData.brdcChnlId, + modal: true, + modalContainerId: "banner1", + modalClassName: css.videoModal, + isVerticalModal: true, // Assuming second banner is horizontal, so modal is vertical + }; + dispatch(requestPlayControl("banner1_preview", videoInfo)); + } + if (handleItemFocus) { + handleItemFocus(); + } + }, [dispatch, bannerDataList, handleItemFocus]); + + const handleSecondBannerBlur = useCallback(() => { + dispatch(releasePlayControl("banner1_preview")); + }, [dispatch]); + const termsData = useSelector((state) => state.home.termsData); - const optionalTermsData = useSelector((state) => - state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") + const optionalTermsData = useSelector((state) => + state.home.termsData?.data?.terms.find( + (term) => term.trmsTpCd === "MST00405", + ), ); const termsLoading = useSelector((state) => state.common.termsLoading); const isGnbOpened = useSelector((state) => state.common.isGnbOpened); @@ -68,7 +108,8 @@ export default function HomeBanner({ //------------------------------------------------------------------------------ // 팝업표시 상태 - const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false); + const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = + useState(false); const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false); // 선택약관 팝업 표시 여부 @@ -80,35 +121,42 @@ export default function HomeBanner({ if (!terms) { return false; } - const optionalTerm = terms.find(term => term.trmsTpCd === "MST00405"); - return optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false; + const optionalTerm = terms.find((term) => term.trmsTpCd === "MST00405"); + return optionalTerm + ? optionalTerm.trmsPopFlag === "Y" && optionalTerm.trmsAgrFlag === "N" + : false; }, [termsData, termsLoading, isGnbOpened]); - + const handleOptionalAgree = useCallback(() => { - console.log('handleAgree Click'); + console.log("handleAgree Click"); // 약관 동의할 항목들 (string array) - const termsList = ["TID0000222", "TID0000223", "TID0000232"]; + const termsList = ["TID0000222", "TID0000223", "TID0000232"]; // 동의하지 않을 항목들 (빈 배열) const notTermsList = []; - console.log('OptionalTermsConfirm -약관 동의 API 호출 파라미터:', { termsList, notTermsList }); + console.log("OptionalTermsConfirm -약관 동의 API 호출 파라미터:", { + termsList, + notTermsList, + }); const callback = (response) => { if (response.retCode === "000" || response.retCode === 0) { - console.log('약관 동의 성공:', response); + console.log("약관 동의 성공:", response); // 약관 정보 갱신 dispatch(fetchCurrentUserHomeTerms()); } else { - console.error('약관 동의 실패:', response); + console.error("약관 동의 실패:", response); } }; - console.log('OptionalTermsConfirm - 약관 동의 API 호출 payload:', { termsList, notTermsList }); + console.log("OptionalTermsConfirm - 약관 동의 API 호출 payload:", { + termsList, + notTermsList, + }); dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback)); - }, [dispatch]); const handleOptionalTermsClick = useCallback(() => { - console.log('약관 자세히 보기 클릭'); + console.log("약관 자세히 보기 클릭"); setIsOptionalConfirmVisible(false); setIsOptionalTermsVisible(true); // 약관 상세 팝업을 띄우는 로직 추가 @@ -120,7 +168,7 @@ export default function HomeBanner({ }, []); const handleOptionalDeclineClick = useCallback(() => { - console.log('거절/다음에 하기 버튼 클릭'); + console.log("거절/다음에 하기 버튼 클릭"); setIsOptionalConfirmVisible(false); // 거절 처리 로직 추가 }, []); @@ -135,11 +183,10 @@ export default function HomeBanner({ // 선택약관 팝업 Agree const handleTermsPopupAgree = useCallback(() => { console.log("handleTermsPopupAgree"); - handleOptionalAgree(); - setIsOptionalTermsVisible(false); + handleOptionalAgree(); + setIsOptionalTermsVisible(false); }, []); - //------------------------------------------------------------------------------ const _handleShelfFocus = useCallback(() => { if (handleShelfFocus) { @@ -164,7 +211,7 @@ export default function HomeBanner({ } } else if ( bannerDetailInfos.find( - (el) => el.shptmBanrTpNm === "LIVE" || el.shptmBanrTpNm === "VOD" + (el) => el.shptmBanrTpNm === "LIVE" || el.shptmBanrTpNm === "VOD", ) ) { targetIndex = i; @@ -192,7 +239,7 @@ export default function HomeBanner({ // 테스트용 팝업 표시 // useEffect(() => { // setTimeout(() => { - // console.log("App.js optionalTermsTest 팝업 표시"); + // console.log("App.js optionalTermsTest 팝업 표시"); // setIsOptionalConfirmVisible(true); // // setIsOptionalTermsVisible(true); // }, 1000); @@ -203,7 +250,7 @@ export default function HomeBanner({ if (termsLoading) { // 약관 데이터 로딩 중에는 아무것도 하지 않음 return; - } + } // 선택 약관 팝업을 띄워야 하는 경우 if (shouldShowOptionalTermsPopup) { // 3초 후에 팝업을 띄우도록 설정 @@ -213,15 +260,33 @@ export default function HomeBanner({ setIsOptionalConfirmVisible(true); // dispatch(setShowPopup({ activePopup: "optionalTermsConfirm" })); }, 1000); // 3000 milliseconds = 3 seconds - + // 컴포넌트 언마운트 시 타이머 클리어 return () => clearTimeout(timer); } - }, [shouldShowOptionalTermsPopup, termsLoading]); + }, [shouldShowOptionalTermsPopup, termsLoading]); const renderItem = useCallback( (index, isHorizontal) => { const data = bannerDataList?.[index] ?? {}; + + if (index === 1) { + return ( +
+ +
+ ); + } + return (
{data.shptmDspyTpNm === "Rolling" ? ( @@ -263,7 +328,60 @@ export default function HomeBanner({
); }, - [_handleItemFocus, _handleShelfFocus, bannerDataList] + [ + bannerDataList, + _handleItemFocus, + _handleShelfFocus, + handleSecondBannerFocus, + handleSecondBannerBlur, + ], + ); + + const renderItemPersistentVideo = useCallback( + (index, isHorizontal) => { + const data = bannerDataList?.[index] ?? {}; + return ( +
+ {data.shptmDspyTpNm === "Rolling" ? ( + + ) : data.shptmDspyTpNm === "Random" ? ( + + ) : ( + + + + )} +
+ ); + }, + [_handleItemFocus, _handleShelfFocus, bannerDataList, homeTopDisplayInfo], ); const renderLayout = useCallback(() => { @@ -272,6 +390,7 @@ export default function HomeBanner({ return ( <> + {/* {renderItemPersistentVideo(0, true)} */} {renderItem(0, true)} {renderItem(1, true)} @@ -306,7 +425,7 @@ export default function HomeBanner({ } } return null; - }, [selectTemplate, renderItem]); + }, [selectTemplate, renderItem, renderItemPersistentVideo]); return ( <> @@ -317,7 +436,7 @@ export default function HomeBanner({ >
{renderLayout()}
- {/* 선택약관 동의 팝업 */} + {/* 선택약관 동의 팝업 */} - {/* 선택약관 자세히 보기 팝업 */} + /> + {/* 선택약관 자세히 보기 팝업 */} - {optionalTermsData && ( -
-
{$L("Optional Terms")}
- -
- -
- )} - + onAgreeClick={handleTermsPopupAgree} + showAgreeButton={true} + /> ); } diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/PersistentVideoUnit.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/PersistentVideoUnit.jsx new file mode 100644 index 00000000..dbafdd42 --- /dev/null +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/PersistentVideoUnit.jsx @@ -0,0 +1,155 @@ +import React, { useCallback, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import classNames from "classnames"; +import Spottable from "@enact/spotlight/Spottable"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; + +import { requestPlayControl, releasePlayControl, startVideoPlayer } from "../../../actions/playActions"; +import CustomImage from "../../../components/CustomImage/CustomImage"; +import liveShow from "../../../../assets/images/tag-liveshow.png"; +import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png"; +import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png"; +import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png"; +import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png"; +import css from "./RandomUnit.module.less"; + +const SpottableComponent = Spottable("div"); +const Container = SpotlightContainerDecorator( + { enterTo: "last-focused" }, + "div" +); + +const PersistentVideoUnit = (props) => { + const { bannerData, spotlightId, isHorizontal, randomNumber, handleItemFocus, handleShelfFocus } = props; + const dispatch = useDispatch(); + + const randomData = bannerData?.bannerDetailInfos?.[randomNumber]; + + const requestVideo = useCallback(() => { + if (randomData) { + const videoInfo = { + showUrl: randomData.showUrl, + patnrId: randomData.patnrId, + showId: randomData.showId, + shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA", + lgCatCd: randomData.lgCatCd, + chanId: randomData.brdcChnlId, + modal: true, + modalContainerId: spotlightId, + modalClassName: css.videoModal, + isVerticalModal: !isHorizontal, + }; + dispatch(requestPlayControl(spotlightId, videoInfo)); + } + }, [dispatch, randomData, spotlightId, isHorizontal]); + + useEffect(() => { + requestVideo(); + + return () => { + dispatch(releasePlayControl(spotlightId)); + }; + }, [dispatch, requestVideo, spotlightId]); + + const handleFocus = useCallback(() => { + requestVideo(); + if (handleItemFocus) { + handleItemFocus(); + } + }, [requestVideo, handleItemFocus]); + + const handleClick = useCallback(() => { + if (randomData) { + const videoInfo = { + showUrl: randomData.showUrl, + patnrId: randomData.patnrId, + showId: randomData.showId, + shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA", + lgCatCd: randomData.lgCatCd, + chanId: randomData.brdcChnlId, + modal: false, + }; + dispatch(startVideoPlayer(videoInfo)); + } + }, [dispatch, randomData]); + + return ( + + + {randomData?.shptmBanrTpNm === "LIVE" && ( +

+ +

+ )} + +
+ {randomData?.tmnlImgPath ? ( + + ) : ( + + )} +
+ +
+ {randomData?.tmnlImgPath == null ? "" : play} +
+ +

+ {randomData?.showId && ( + + )} +

+
+
+ ); +}; + +export default PersistentVideoUnit; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.jsx new file mode 100644 index 00000000..657791bf --- /dev/null +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.jsx @@ -0,0 +1,641 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + } from "react"; + + import classNames from "classnames"; + import { useDispatch, useSelector } from "react-redux"; + + import Spotlight from "@enact/spotlight"; + import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; + import Spottable from "@enact/spotlight/Spottable"; + import { getContainerId } from "@enact/spotlight/src/container"; + + import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png"; + import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png"; + import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png"; + import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png"; + import defaultImageItem from "../../../../assets/images/img-thumb-empty-product@3x.png"; + import liveShow from "../../../../assets/images/tag-liveshow.png"; + import { changeAppStatus } from "../../../actions/commonActions"; + import { updateHomeInfo } from "../../../actions/homeActions"; + import { + sendLogTopContents, + sendLogTotalRecommend, + } from "../../../actions/logActions"; + import { pushPanel } from "../../../actions/panelActions"; + import { + finishVideoPreview, + startVideoPlayer, + } from "../../../actions/playActions"; + import CustomImage from "../../../components/CustomImage/CustomImage"; + import usePriceInfo from "../../../hooks/usePriceInfo"; + import { + LOG_CONTEXT_NAME, + LOG_MENU, + LOG_MESSAGE_ID, + LOG_TP_NO, + panel_names, + } from "../../../utils/Config"; + import { $L, formatGMTString } from "../../../utils/helperMethods"; + import { TEMPLATE_CODE_CONF } from "../HomePanel"; + import css from "./RandomUnit.new.module.less"; + + const SpottableComponent = Spottable("div"); + + const Container = SpotlightContainerDecorator( + { enterTo: "last-focused" }, + "div" + ); + + export default function RandomUnitNew({ + bannerData, + spotlightId, + isHorizontal, + handleShelfFocus, + randomNumber, + onFocus, + onBlur, + }) { + const dispatch = useDispatch(); + + const bannerDetailInfos = bannerData.bannerDetailInfos; + + const shptmTmplCd = useSelector( + (state) => state.home?.bannerData?.shptmTmplCd + ); + const nowMenu = useSelector((state) => state.common.menu.nowMenu); + const entryMenu = useSelector((state) => state.common.menu.entryMenu); + + const homeCategory = useSelector( + (state) => state.home.menuData?.data?.homeCategory + ); + const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd); + + const broadcast = useSelector((state) => state.common.broadcast); + const { curationId, curationTitle } = useSelector((state) => state.home); + const [randomData, setRandomData] = useState(""); + const [priceInfos, setpriceInfos] = useState(""); + const [videoError, setVideoError] = useState(false); + const [liveIndicies, setLiveIndicies] = useState([]); + + const bannerDataRef = useRef(bannerData); + const randomDataRef = useRef(bannerDetailInfos[randomNumber]); + + const topContentsLogInfo = useMemo(() => { + if (randomDataRef.current) { + const currentRandomData = randomDataRef.current; + + let contId, contNm; + + switch (currentRandomData?.shptmBanrTpCd) { + // case: "LIVE" or "VOD" + case "DSP00301": + case "DSP00302": + contId = currentRandomData?.showId; + contNm = currentRandomData?.showNm; + break; + + // case: "Image Banner" + case "DSP00303": + contId = currentRandomData?.shptmLnkTpCd; + contNm = currentRandomData?.shptmLnkTpNm; + break; + + // case: "Today's Deals" + default: + contId = currentRandomData?.prdtId; + contNm = currentRandomData?.prdtNm; + break; + } + + if ( + currentRandomData?.shptmLnkTpCd === "DSP00503" || // "Hot Picks" + currentRandomData?.shptmLnkTpCd === "DSP00509" // "Theme" + ) { + contNm = contNm + " | " + currentRandomData?.lnkCurationId; + } + + return { + banrNo: `${currentRandomData?.banrDpOrd}`, + banrTpNm: currentRandomData?.vtctpYn + ? currentRandomData.vtctpYn === "Y" + ? "Vertical" + : "Horizontal" + : "", + contId, + contNm, + contTpNm: currentRandomData?.shptmBanrTpNm ?? "", + dspyTpNm: bannerDataRef.current?.shptmDspyTpNm ?? "", + expsOrd: bannerDataRef.current?.banrLctnNo ?? "", + linkTpCd: "", + patncNm: currentRandomData?.patncNm ?? "", + patnrId: currentRandomData?.patnrId ?? "", + tmplCd: shptmTmplCd, + }; + } + + return {}; + }, [shptmTmplCd]); + + const sendBannerLog = useCallback(() => { + const data = randomDataRef.current; + + if (data && nowMenu === LOG_MENU.HOME_TOP) { + dispatch( + sendLogTotalRecommend({ + contextName: LOG_CONTEXT_NAME.HOME, + messageId: LOG_MESSAGE_ID.BANNER, + curationId, + curationTitle, + contentType: data.shptmBanrTpNm, + contentId: data.showId, + contentTitle: data.showNm, + productId: data.prdtId, + productTitle: data.prdtNm, + displayType: "rolling", + partner: data.patncNm, + brand: data.brndNm, // <- 'brnad' 확인 + location: data.dspyOrdr, + bannerType: data.vtctpYn === "Y" ? "Vertical" : "Horizontal", + }) + ); + } + }, [randomDataRef, nowMenu]); + + useEffect(() => { + if (bannerDetailInfos && randomNumber) { + const indices = bannerDetailInfos + .map((info, index) => (info.shptmBanrTpNm === "LIVE" ? index : null)) + .filter((index) => index !== null && index !== randomNumber); + + setLiveIndicies(indices); + } + }, [bannerDetailInfos, randomNumber]); + + const videoErrorClick = useCallback(() => { + return dispatch( + pushPanel({ + name: panel_names.FEATURED_BRANDS_PANEL, + panelInfo: { from: "gnb", patnrId: randomData.patnrId }, + }) + ); + }, [randomData, dispatch]); + + const shelfFocus = useCallback(() => { + if (handleShelfFocus) { + handleShelfFocus(); + } + }, [handleShelfFocus]); + + const categoryData = useMemo(() => { + if (randomData && randomData.shptmLnkTpCd === "DSP00505") { + if (homeCategory && homeCategory.length > 0) { + const foundCategory = homeCategory.find( + (data) => data.lgCatCd === randomData.lgCatCd + ); + if (foundCategory) { + return { + lgCatNm: foundCategory.lgCatNm, + COUNT: foundCategory.COUNT, + }; + } + return; + } + } + }, [homeCategory, randomData.shptmLnkTpCd]); + + const imageBannerClick = useCallback(() => { + let linkInfo = {}; + const linkType = randomData.shptmLnkTpCd; + + switch (linkType) { + case "DSP00501": + linkInfo = { + name: panel_names.FEATURED_BRANDS_PANEL, + panelInfo: { from: "gnb", patnrId: randomData.patnrId }, + }; + break; + + case "DSP00502": + linkInfo = { + name: panel_names.TRENDING_NOW_PANEL, + panelInfo: {}, + }; + break; + + case "DSP00503": + linkInfo = { + name: panel_names.HOT_PICKS_PANEL, + panelInfo: { + patnrId: randomData.patnrId, + curationId: randomData.lnkCurationId, + }, + }; + break; + + case "DSP00504": + linkInfo = { + name: panel_names.ON_SALE_PANEL, + panelInfo: { + lgCatCd: randomData.lgCatCd, + }, + }; + break; + + case "DSP00505": + if (Object.keys(categoryData).length > 0) { + linkInfo = { + name: panel_names.CATEGORY_PANEL, + panelInfo: { + lgCatCd: randomData.lgCatCd, + lgCatNm: categoryData.lgCatNm, + COUNT: categoryData.COUNT, + currentSpot: null, + dropDownTab: 0, + tab: 0, + focusedContainerId: null, + }, + }; + } + break; + + case "DSP00506": + linkInfo = { + name: panel_names.DETAIL_PANEL, + panelInfo: { + patnrId: randomData.patnrId, + prdtId: randomData.prdtId, + curationId: randomData.lnkCurationId, + }, + }; + break; + + case "DSP00507": + linkInfo = { + patnrId: randomData.patnrId, + showId: randomData.showId, + shptmBanrTpNm: "VOD", + lgCatCd: randomData.lgCatCd, + modal: false, + }; + break; + + case "DSP00508": + linkInfo = { + name: panel_names.DETAIL_PANEL, + panelInfo: { + patnrId: randomData.patnrId, + curationId: randomData.lnkCurationId, + prdtId: randomData.prdtId, + type: "theme", + }, + }; + break; + + case "DSP00509": + linkInfo = { + name: panel_names.THEME_CURATION_PANEL, + panelInfo: { + curationId: randomData.lnkCurationId, + }, + }; + break; + + default: + linkInfo = { + name: panel_names.HOME_PANEL, + panelInfo: {}, + }; + break; + } + + let action = linkType === "DSP00507" ? startVideoPlayer : pushPanel; + + dispatch(action(linkInfo)); + sendBannerLog(); + dispatch( + sendLogTopContents({ + ...topContentsLogInfo, + inDt: formatGMTString(new Date()) ?? "", + logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, + }) + ); + }, [ + categoryData, + dispatch, + randomData?.lgCatCd, + randomData?.lnkCurationId, + randomData?.patnrId, + randomData?.prdtId, + randomData?.showId, + randomData?.shptmLnkTpCd, + topContentsLogInfo, + ]); + + const todayDealClick = useCallback(() => { + dispatch( + pushPanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + patnrId: randomData.patnrId, + prdtId: randomData.prdtId, + }, + }) + ); + + sendBannerLog(); + + dispatch( + sendLogTopContents({ + ...topContentsLogInfo, + inDt: formatGMTString(new Date()) ?? "", + logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, + }) + ); + }, [ + dispatch, + randomData?.patnrId, + randomData?.prdtId, + randomDataRef, + topContentsLogInfo, + ]); + + const videoClick = useCallback(() => { + const lastFocusedTargetId = getContainerId(Spotlight.getCurrent()); + const currentSpot = Spotlight.getCurrent(); + + if (lastFocusedTargetId) { + dispatch( + updateHomeInfo({ + name: panel_names.HOME_PANEL, + panelInfo: { + lastFocusedTargetId, + focusedContainerId: TEMPLATE_CODE_CONF.TOP, + currentSpot: currentSpot?.getAttribute("data-spotlight-id"), + }, + }) + ); + } + + dispatch( + startVideoPlayer({ + showUrl: randomData.showUrl, + patnrId: randomData.patnrId, + showId: randomData.showId, + shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA", + lgCatCd: randomData.lgCatCd, + chanId: randomData.brdcChnlId, + modal: false, + modalContainerId: spotlightId, + modalClassName: css.videoModal, + }) + ); + + sendBannerLog(); + + dispatch( + sendLogTopContents({ + ...topContentsLogInfo, + inDt: formatGMTString(new Date()) ?? "", + logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK, + }) + ); + + if (onBlur) { + onBlur(); + } + }, [randomData, spotlightId, topContentsLogInfo, nowMenu, randomDataRef, onBlur]); + + const { originalPrice, discountedPrice, discountRate, offerInfo } = + usePriceInfo(priceInfos) || {}; + + useEffect(() => { + let _nowMenu = nowMenu; + let _entryMenu = entryMenu; + + if (nowMenu === LOG_MENU.HOME_TOP) { + const params = { + ...topContentsLogInfo, + entryMenu: _entryMenu, + inDt: formatGMTString(new Date()) ?? "", + logTpNo: LOG_TP_NO.TOP_CONTENTS.VIEW, + nowMenu: _nowMenu, + }; + + return () => dispatch(sendLogTopContents(params)); + } + }, [dispatch, entryMenu, nowMenu, topContentsLogInfo]); + + useEffect(() => { + sendBannerLog(); + }, [randomDataRef, nowMenu]); + + useEffect(() => { + if (bannerData) { + setRandomData(bannerDetailInfos[randomNumber]); + } + }, [bannerData, dispatch, randomNumber]); + + useEffect(() => { + if (randomData && randomData.priceInfo !== null) { + return setpriceInfos(randomData.priceInfo); + } + }, [randomData]); + + useEffect(() => { + if (broadcast?.type === "videoError") { + setVideoError(true); + if (liveIndicies.length > 0) { + const nextIndex = liveIndicies[0]; + + setLiveIndicies((prev) => prev.slice(1)); + setRandomData(bannerDetailInfos[nextIndex]); + + setTimeout(() => { + setVideoError(false); + }, 0); + } + } + }, [broadcast, liveIndicies, bannerDetailInfos]); + + return ( + <> + + {randomData?.shptmBanrTpNm == "Image Banner" ? ( + +
+ +
+
+ ) : randomData?.shptmBanrTpNm == "LIVE" || + randomData?.shptmBanrTpNm == "VOD" ? ( + + {randomData.shptmBanrTpNm == "LIVE" && videoError === false && ( +

+ +

+ )} + + {videoError === true && ( +
+
+ {randomData.patncLogoPath && ( + + )} +

+ {$L("Click the screen to see more products!")} +

+
+
+ )} + + {videoError === false && ( +
+ {randomData.tmnlImgPath ? ( + + ) : ( + + )} +
+ )} + + {videoError === false && ( +
+ {randomData.tmnlImgPath == null ? "" : } +
+ )} + + {videoError === false && ( +

+ {randomData.showId && ( + + )} +

+ )} +
+ ) : randomData?.shptmBanrTpNm == "Today's Deals" ? ( + +
+
{$L("TODAY's DEALS")}
+
+
+ {parseFloat(originalPrice?.replace("$", "")) === 0 + ? randomData?.offerInfo + : discountRate + ? discountedPrice + : originalPrice} + {discountRate && !isHorizontal && ( + {originalPrice} + )} +
+ {isHorizontal && + parseFloat(originalPrice?.replace("$", "")) !== 0 && ( + {originalPrice} + )} +
+ +
+ +
+ + ) : null} + + + ); + } + \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.module.less b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.module.less new file mode 100644 index 00000000..91cd742d --- /dev/null +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.new.module.less @@ -0,0 +1,281 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +.rollingWrap { + position: relative; + .itemBox { + .size(@w: 486px, @h: 858px); + position: relative; + text-align: center; + .brandIcon { + overflow: hidden; + position: absolute; + right: 30px; + bottom: 30px; + > img { + border-radius: 0; + .size(@w: 60px, @h: 60px); + } + } + .liveIcon { + z-index: 2; + position: absolute; + left: 18px; + top: 18px; + > img { + .size(@w: 108px, @h: 48px); + } + } + .imgBanner { + > img { + border-radius: 10px; + .size(@w: 486px, @h: 858px); + } + } + .btnPlay { + .size(@w: 100%, @h: 100%); + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + top: 0; + z-index: 2; + > img { + .size(@w: 120px, @h: 120px); + } + } + &.isHorizontal { + .size(@w: 744px, @h: 420px); + .imgBanner { + > img { + border-radius: 10px; + .size(@w: 744px, @h: 420px); + } + } + } + .errorContents { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + .errorlogo { + width: 120px; + height: 120px; + object-fit: cover; + } + + .errorText { + margin-top: 30px; + color: #ff0000; + padding-top: 10px; + } + } + + &.todaysDeals { + background-image: url(../../../../assets/images/img-home-banner-td-ver@3x.png); + background-size: 486px 858px; + background-position: left top; + border-radius: 10px; + padding: 73px 36px 0; + &.ru { + .productInfo { + .todaysDealTitle { + font-size: 58px; + line-height: 60px; + font-family: @arialFontBold; + } + } + } + &.de { + .productInfo { + .todaysDealTitle { + font-size: 59px !important; + line-height: 63px !important; + letter-spacing: -1px !important; + } + } + } + .productInfo { + margin-bottom: 33px; + .todaysDealTitle { + .size(@w:100%,@h:132px); + font-size: 76px; + word-break: break-word; + font-stretch: normal; + color: #151515; + text-align: center; + line-height: 76px; + font-family: @arialFontBold; + } + .textBox { + .size(@w: 100%, @h: 80px); + margin-top: 71px; + .elip(@clamp:2); + font-weight: bold; + font-size: 30px; + color: @COLOR_GRAY06; + line-height: 1.27; + margin-bottom: 6px; + } + .accBox { + width: 100%; + text-align: center; + font-weight: bold; + font-size: 42px; + color: @PRIMARY_COLOR_RED; + line-height: 1.14; + display: inline-block; + .elip(@clamp:1); + > strong { + width: 260px; + font-size: 30px; + line-height: 1.27; + display: block; + .elip(@clamp:2); + } + .saleAccBox { + font-weight: normal; + font-size: 24px; + color: @COLOR_GRAY04; + vertical-align: middle; + text-decoration: line-through; + margin-left: 9px; + } + } + } + .itemImgBox { + > img { + .size(@w: 356px, @h: 356px); + border-radius: 0; + } + } + &.isHorizontal { + background-image: url(../../../../assets/images/img-home-banner-td-hor@3x.png); + background-size: 744px 420px; + background-position: center center; + display: flex; + padding: 0 30px 0 0; + border-radius: 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -o-border-radius: 10px; + > div { + flex: none; + } + &.ru { + .productInfo { + .todaysDealTitle { + font-size: 58px; + line-height: 60px; + font-family: @arialFontBold; + } + } + } + &.de { + .productInfo { + .todaysDealTitle { + font-size: 59px !important; + line-height: 63px !important; + letter-spacing: -2px !important; + } + } + } + .productInfo { + margin-bottom: 0; + .todaysDealTitle { + .size(@w:305px,@h:114px); + margin-top: 53px; + margin-left: 49px; + font-size: 66px; + word-break: break-word; + color: #151515; + text-align: left; + line-height: 57px; + font-family: @arialBlack; + } + .textBox { + .size(@w: 294px, @h: 80px); + margin: 67px 0 5px 50px; + text-align: left; + } + .accBox { + .size(@w: 320px, @h: 50px); + margin-left: 50px; + text-align: left; + display: block; + .elip(@clamp:1); + } + .saleAccBox { + color: #767676; + display: block; + text-align: left; + margin: 5px 0 0 55px; + text-decoration: line-through; + } + } + .itemImgBox { + .position(@position: absolute, @top: 47px, @left: 389px); + .size(@w: 326px, @h: 326px); + > img { + .size(@w: inherit, @h: inherit); + } + } + } + } + &:focus { + &::after { + .focused(@boxShadow:22px, @borderRadius: 12px); + border: 6px solid @PRIMARY_COLOR_RED; + top: -4px; + right: -4px; + bottom: -4px; + left: -4px; + } + } + } + + .arrow { + z-index: 9999; + .size(@w: 42px, @h: 42px); + background-size: 42px 42px; + background-position: center center; + &.leftBtn { + .position(@position: absolute, @top: 406px, @left: 18px); + background-image: url("../../../../assets/images/btn/btn_prev_thumb_nor.png"); + &:focus { + background-image: url("../../../../assets/images/btn/btn_prev_thumb_foc.png"); + } + } + &.rightBtn { + .position(@position: absolute, @top: 406px, @right: 18px); + background-image: url("../../../../assets/images/btn/btn_next_thumb_nor.png"); + &:focus { + background-image: url("../../../../assets/images/btn/btn_next_thumb_foc.png"); + } + } + } + + &.isHorizontalWrap { + border-radius: 10px; + .arrow { + &.leftBtn { + .position(@position: absolute, @top: 189px, @left: 18px); + } + &.rightBtn { + .position(@position: absolute, @top: 189px, @right: 18px); + } + } + } +} + +.videoModal { + &::after { + .focused(@boxShadow:0, @borderRadius: 12px); + border: 6px solid @PRIMARY_COLOR_RED; + top: -4px; + right: -4px; + bottom: -4px; + left: -4px; + } +} diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx index 220d6de7..95e7db8f 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx @@ -75,43 +75,43 @@ const HomePanel = ({ isOnTop }) => { const isGnbOpened = useSelector((state) => state.common.isGnbOpened); const homeLayoutInfo = useSelector((state) => state.home.layoutData); const panelInfo = useSelector( - (state) => state.home.homeInfo?.panelInfo ?? {} + (state) => state.home.homeInfo?.panelInfo ?? {}, ); const panels = useSelector((state) => state.panels.panels); const webOSVersion = useSelector( - (state) => state.common.appStatus?.webOSVersion + (state) => state.common.appStatus?.webOSVersion, ); const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB); const defaultFocus = useSelector((state) => state.home.defaultFocus); const categoryInfos = useSelector( - (state) => state.onSale.homeOnSaleData?.data?.categoryInfos + (state) => state.onSale.homeOnSaleData?.data?.categoryInfos, ); const categoryItemInfos = useSelector( - (state) => state.main.subCategoryData?.categoryItemInfos + (state) => state.main.subCategoryData?.categoryItemInfos, ); const { popupVisible, activePopup } = useSelector( - (state) => state.common.popup + (state) => state.common.popup, ); const eventPopInfosData = useSelector( - (state) => state.event.eventData.eventPopInfo + (state) => state.event.eventData.eventPopInfo, ); const eventData = useSelector((state) => state.event.eventData); const eventClickSuccess = useSelector( - (state) => state.event.eventClickSuccess + (state) => state.event.eventClickSuccess, ); const homeOnSaleInfos = useSelector( - (state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos + (state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos, ); const bestSellerDatas = useSelector( - (state) => state.product.bestSellerData?.bestSeller + (state) => state.product.bestSellerData?.bestSeller, ); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const isDeepLink = useSelector( - (state) => state.common.deepLinkInfo.isDeepLink + (state) => state.common.deepLinkInfo.isDeepLink, ); const [btnDisabled, setBtnDisabled] = useState(true); @@ -120,13 +120,13 @@ const HomePanel = ({ isOnTop }) => { const [eventPopOpen, setEventPopOpen] = useState(false); const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf); const [firstLgCatCd, setFirstLgCatCd] = useState( - panelInfo.currentCatCd ?? null + panelInfo.currentCatCd ?? null, ); const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null); const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null); const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); const [focusedContainerId, setFocusedContainerId] = useState( - panelInfo.focusedContainerId + panelInfo.focusedContainerId, ); const isInitialRender = useRef(true); @@ -139,7 +139,7 @@ const HomePanel = ({ isOnTop }) => { sendLogTotalRecommend({ messageId: LOG_MESSAGE_ID.HOME, contextName: LOG_CONTEXT_NAME.HOME, - }) + }), ); } }, [entryMenu, nowMenu]); @@ -147,7 +147,7 @@ const HomePanel = ({ isOnTop }) => { const sortedHomeLayoutInfo = useMemo(() => { if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) { const sorted = [...homeLayoutInfo.homeLayoutInfo].sort( - (x, y) => x.expsOrd - y.expsOrd + (x, y) => x.expsOrd - y.expsOrd, ); return sorted; } @@ -183,7 +183,7 @@ const HomePanel = ({ isOnTop }) => { panelInfo: { currentSpot: currentSpot, }, - }) + }), ); dispatch(setShowPopup(ACTIVE_POPUP.exitPopup)); @@ -197,7 +197,7 @@ const HomePanel = ({ isOnTop }) => { contextName: LOG_CONTEXT_NAME.SHOPTIME, messageId: LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, - }) + }), ); }, [dispatch]); @@ -261,7 +261,7 @@ const HomePanel = ({ isOnTop }) => { shelfLocation: location, shelfId: containerId, shelfTitle: title, - }) + }), ); setNowShelf(containerId); @@ -271,14 +271,14 @@ const HomePanel = ({ isOnTop }) => { currentSentMenuRef.current = nowMenu; } }, - [pageSpotIds, nowShelf, panelInfo.nowShelf] + [pageSpotIds, nowShelf, panelInfo.nowShelf], ); const handleItemFocus = useCallback( (containerId, location, title) => () => { doSendLogGNB(containerId, location, title); }, - [doSendLogGNB] + [doSendLogGNB], ); const renderPageItem = useCallback(() => { @@ -298,7 +298,7 @@ const HomePanel = ({ isOnTop }) => { handleShelfFocus={handleItemFocus( el.shptmApphmDspyOptCd, el.expsOrd, - el.shptmApphmDspyOptNm + el.shptmApphmDspyOptNm, )} handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)} /> @@ -320,7 +320,7 @@ const HomePanel = ({ isOnTop }) => { handleShelfFocus={handleItemFocus( el.shptmApphmDspyOptCd, el.expsOrd, - el.shptmApphmDspyOptNm + el.shptmApphmDspyOptNm, )} handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)} shelfLocation={el.expsOrd} @@ -338,7 +338,7 @@ const HomePanel = ({ isOnTop }) => { handleShelfFocus={handleItemFocus( el.shptmApphmDspyOptCd, el.expsOrd, - el.shptmApphmDspyOptNm + el.shptmApphmDspyOptNm, )} handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)} shelfLocation={el.expsOrd} @@ -356,7 +356,7 @@ const HomePanel = ({ isOnTop }) => { handleShelfFocus={handleItemFocus( el.shptmApphmDspyOptCd, el.expsOrd, - el.shptmApphmDspyOptNm + el.shptmApphmDspyOptNm, )} handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)} shelfLocation={el.expsOrd} @@ -374,7 +374,7 @@ const HomePanel = ({ isOnTop }) => { handleShelfFocus={handleItemFocus( el.shptmApphmDspyOptCd, el.expsOrd, - el.shptmApphmDspyOptNm + el.shptmApphmDspyOptNm, )} handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)} shelfLocation={el.expsOrd} @@ -473,7 +473,7 @@ const HomePanel = ({ isOnTop }) => { cbChangePageRef, dispatch, isOnTop, - ] + ], ); const bestSellerLoaded = useCallback(() => { @@ -486,7 +486,7 @@ const HomePanel = ({ isOnTop }) => { if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) { dispatch( - changeAppStatus({ showLoadingPanel: { show: true, type: "wait" } }) + changeAppStatus({ showLoadingPanel: { show: true, type: "wait" } }), ); dispatch(getHomeMainContents()); dispatch(getHomeLayout()); @@ -495,7 +495,7 @@ const HomePanel = ({ isOnTop }) => { homeSaleInfosIncFlag: "Y", categoryIncFlag: "Y", saleInfosIncFlag: "N", - }) + }), ); dispatch(getTop20Show()); dispatch(getBestSeller(bestSellerLoaded)); @@ -528,8 +528,8 @@ const HomePanel = ({ isOnTop }) => { tabType: "CAT00102", filterType: "CAT00202", }, - 1 - ) + 1, + ), ); } }, [categoryInfos, firstLgCatCd, dispatch]); @@ -544,7 +544,7 @@ const HomePanel = ({ isOnTop }) => { } else setEventPopOpen(true); } else setEventPopOpen(false); }, - [webOSVersion] + [webOSVersion], ); useEffect(() => { @@ -583,7 +583,7 @@ const HomePanel = ({ isOnTop }) => { } const tBody = document.querySelector( - `[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]` + `[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`, ); const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null; @@ -597,7 +597,7 @@ const HomePanel = ({ isOnTop }) => { currentCateName: targetSpotlightCateNm, focusedContainerId: focusedContainerIdRef.current, }, - }) + }), ); }; }, [dispatch]); diff --git a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx index d338216f..34f06d75 100644 --- a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx +++ b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx @@ -1,14 +1,16 @@ // src: views/IntroPanel/IntroPanel.new.jsx -import React, { useCallback, useEffect, useState, useMemo, useRef } from "react"; -import { flushSync } from "react-dom"; - +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useDispatch, useSelector } from "react-redux"; - import Region from "@enact/sandstone/Region"; import Spotlight from "@enact/spotlight"; import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; - import { setExitApp, setHidePopup, @@ -17,7 +19,7 @@ import { } from "../../actions/commonActions"; import { registerDevice } from "../../actions/deviceActions"; import { getWelcomeEventInfo } from "../../actions/eventActions"; -import { fetchCurrentUserHomeTerms } from "../../actions/homeActions"; +// import { fetchCurrentUserHomeTermsPromise } from "../../actions/homeActions"; import { sendLogGNB, sendLogTerms, @@ -29,27 +31,29 @@ import TButton, { TYPES } from "../../components/TButton/TButton"; // import TButtonTab from "../../components/TButtonTab/TButtonTab"; import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare"; import TPanel from "../../components/TPanel/TPanel"; -import TPopUp, { CONTENT_TYPES } from "../../components/TPopUp/TPopUp"; +import TPopUp from "../../components/TPopUp/TPopUp"; import TNewPopUp from "../../components/TPopUp/TNewPopUp"; -import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; +// import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; import useDebugKey from "../../hooks/useDebugKey"; +import useSafeFocusState from "../../hooks/useSafeFocusState"; +import useTermsStateMachine from "../../hooks/useTermsStateMachine"; +import useRightPanelContent from "../../hooks/useRightPanelContent"; import * as Config from "../../utils/Config"; import { panel_names } from "../../utils/Config"; -import { $L, scaleH, scaleW } from "../../utils/helperMethods"; +import { $L } from "../../utils/helperMethods"; import css from "./IntroPanel.new.module.less"; import { types } from "../../actions/actionTypes"; import { focusById } from "../../utils/spotlight-utils"; - const Container = SpotlightContainerDecorator( { enterTo: "last-focused" }, - "div" + "div", ); export default function IntroPanel({ - children, - isTabActivated, - handleCancel, + // children, + // isTabActivated, + // handleCancel, spotlightId, ...rest }) { @@ -58,74 +62,85 @@ export default function IntroPanel({ useDebugKey({}); const dispatch = useDispatch(); - const blurTimeout = useRef(null); + // const blurTimeout = useRef(null); const termsData = useSelector((state) => state.home.termsData); const { popupVisible, activePopup, ...popupState } = useSelector( - (state) => state.common.popup + (state) => state.common.popup, ); - const eventInfos = useSelector((state) => state.event.eventData); + // const eventInfos = useSelector((state) => state.event.eventData); const regDeviceData = useSelector((state) => state.device.regDeviceData); - const regDeviceInfoData = useSelector( - (state) => state.device.regDeviceInfoData - ); + // const regDeviceInfoData = useSelector( + // (state) => state.device.regDeviceInfoData + // ); // registerDevice API 호출 중 여부 const [isProcessing, setIsProcessing] = useState(false); const [showExitMessagePopup, setShowExitMessagePopup] = useState(false); - const [isRequiredFocused, setIsRequiredFocused] = useState(false); + // const [isRequiredFocused, setIsRequiredFocused] = useState(false); + + const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState(); + const { state: termsState, updateStateAsync } = useTermsStateMachine(); + const { + termsChecked, + privacyChecked, + optionalChecked, + selectAllChecked, + error: termsError, // 훅의 에러 상태를 가져옴 + } = termsState; const introTermsData = useMemo(() => { - return termsData?.data?.terms.filter( - (item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402" - ) || []; + return ( + termsData?.data?.terms.filter( + (item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402", + ) || [] + ); }, [termsData]); const optionalTermsData = useMemo(() => { - return termsData?.data?.terms.filter( - (item) => item.trmsTpCd === "MST00405" - ) || []; + return ( + termsData?.data?.terms.filter( + (item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406", + ) || [] + ); }, [termsData]); const webOSVersion = useSelector( - (state) => state.common.appStatus?.webOSVersion + (state) => state.common.appStatus?.webOSVersion, ); // WebOS 버전별 UI 표시 모드 결정 // 이미지 표시: 4.0, 5.0, 23, 24 // 텍스트 표시: 4.5, 6.0, 22 const shouldShowBenefitsView = useMemo(() => { if (!webOSVersion) return false; - + const version = String(webOSVersion); - + // 텍스트 표시 버전들 - const textVersions = ['4.5', '6.0', '22']; - - // 이미지 표시 버전들 - const imageVersions = ['4.0', '5.0', '23', '24']; - + const textVersions = ["4.5", "6.0", "22"]; + + // 이미지 표시 버전들 + const imageVersions = ["4.0", "5.0", "23", "24"]; + // 텍스트 버전인지 확인 const shouldShowText = textVersions.includes(version); - - console.log('🔍 WebOS 버전별 UI 모드:'); - console.log(' - webOSVersion:', version); - console.log(' - shouldShowText (텍스트 모드):', shouldShowText); - console.log(' - 텍스트 버전들:', textVersions); - console.log(' - 이미지 버전들:', imageVersions); - + + if (process.env.NODE_ENV === "development") { + console.log("🔍 WebOS 버전별 UI 모드:"); + console.log(" - webOSVersion:", version); + console.log(" - shouldShowText (텍스트 모드):", shouldShowText); + console.log(" - 텍스트 버전들:", textVersions); + console.log(" - 이미지 버전들:", imageVersions); + } + return shouldShowText; }, [webOSVersion]); // 상태 관리 const [currentTerms, setCurrentTerms] = useState(null); - const [termsChecked, setTermsChecked] = useState(false); // Terms & Conditions 기본 체크 - const [privacyChecked, setPrivacyChecked] = useState(false); // Privacy Policy 기본 체크 - const [optionalChecked, setOptionalChecked] = useState(false); // Optional Terms 기본 체크 안됨 - const [selectAllChecked, setSelectAllChecked] = useState(false); - const [focusedItem, setFocusedItem] = useState(null); // 포커스 추적 상태 추가 - + useEffect(() => { dispatch({ type: types.REGISTER_DEVICE_RESET }); - dispatch(sendLogGNB(Config.LOG_MENU.TERMS_CONDITIONS)); + dispatch(sendLogGNB(Config.LOG_MENU.TERMS_CONDITIONS)); }, [dispatch]); // 컴포넌트 마운트 시 현재 Redux 상태 로깅 @@ -139,9 +154,11 @@ export default function IntroPanel({ // 디버깅용 WebOS 버전 로그 useEffect(() => { - console.log('🔍 IntroPanel WebOS 버전 정보:'); - console.log(' - webOSVersion:', webOSVersion); - console.log(' - shouldShowBenefitsView:', shouldShowBenefitsView); + if (process.env.NODE_ENV === "development") { + console.log("🔍 IntroPanel WebOS 버전 정보:"); + console.log(" - webOSVersion:", webOSVersion); + console.log(" - shouldShowBenefitsView:", shouldShowBenefitsView); + } }, [webOSVersion, shouldShowBenefitsView]); useEffect(() => { @@ -153,22 +170,13 @@ export default function IntroPanel({ contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, - }) + }), ); }, 3000); return () => clearTimeout(timer); } }, [showExitMessagePopup, dispatch]); - // Select All 상태 업데이트 - useEffect(() => { - const allChecked = termsChecked && privacyChecked && optionalChecked; - setSelectAllChecked(allChecked); - if (allChecked) { - Spotlight.focus("agreeButton"); - } - }, [termsChecked, privacyChecked, optionalChecked]); - // 컴포넌트 마운트 후 0.5초 뒤에 selectAllCheckbox으로 강제 포커스 useEffect(() => { const focusTimer = setTimeout(() => { @@ -180,6 +188,17 @@ export default function IntroPanel({ }; }, []); + // [추가] useTermsStateMachine의 에러 상태를 감지하여 팝업으로 표시 + useEffect(() => { + if (termsError) { + dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, { + title: $L("Error"), + text: termsError.message, + button1Text: $L("OK") + })); + } + }, [termsError, dispatch]); + // 약관 팝업 동의여부에 따른 이벤트 핸들러 const handleTermsAgree = useCallback(() => { if (!currentTerms) { @@ -187,23 +206,22 @@ export default function IntroPanel({ } const termType = currentTerms.trmsTpCd; if (termType === "MST00402") { - setTermsChecked(true); + updateStateAsync({ termsChecked: true }); } else if (termType === "MST00401") { - setPrivacyChecked(true); + updateStateAsync({ privacyChecked: true }); } else if (termType === "MST00405") { // Optional Terms - setOptionalChecked(true); + updateStateAsync({ optionalChecked: true }); } // 팝업 닫기 dispatch(setHidePopup()); - }, [currentTerms, dispatch]); - + }, [currentTerms, dispatch, updateStateAsync]); const handleTermsClick = useCallback( (trmsTpCdList) => { if (introTermsData) { const selectedTerms = introTermsData.find( - (term) => term.trmsTpCd === trmsTpCdList + (term) => term.trmsTpCd === trmsTpCdList, ); setCurrentTerms(selectedTerms); @@ -216,14 +234,14 @@ export default function IntroPanel({ dispatch(sendLogTerms({ logTpNo })); } }, - [introTermsData, dispatch] + [introTermsData, dispatch], ); const handleOptionalTermsClick = useCallback( (trmsTpCdList) => { if (optionalTermsData) { const selectedTerms = optionalTermsData.find( - (term) => term.trmsTpCd === trmsTpCdList + (term) => term.trmsTpCd === trmsTpCdList, ); setCurrentTerms(selectedTerms); @@ -236,17 +254,16 @@ export default function IntroPanel({ // dispatch(sendLogTerms({ logTpNo })); } }, - [optionalTermsData, dispatch] + [optionalTermsData, dispatch], ); - const onClose = useCallback(() => { dispatch(setHidePopup()); - }, [dispatch]); - + }, [dispatch]); + const handleAgree = useCallback(() => { if (isProcessing) return; - + // 필수 약관이 체크되어 있는지 확인 // if (!termsChecked || !privacyChecked) { // // 필수 약관이 체크되지 않았을 때 알림 @@ -260,12 +277,12 @@ export default function IntroPanel({ // } setIsProcessing(true); - // 약관 동의 처리 시작 시 로딩 상태로 설정 + // 약관 동의 처리 시작 시 로딩 상태로 설정 dispatch({ type: types.GET_TERMS_AGREE_YN_START }); // 약관 ID 정확하게 매핑 const agreeTerms = []; - + if (termsChecked) { agreeTerms.push("TID0000222"); // MST00402 -> TID0000222 (이용약관) } @@ -276,13 +293,15 @@ export default function IntroPanel({ agreeTerms.push("TID0000232"); // MST00405 -> TID0000232 (선택약관) } - console.log('최종 전송될 agreeTerms:', agreeTerms); + if (process.env.NODE_ENV === "development") { + console.log("최종 전송될 agreeTerms:", agreeTerms); + } dispatch( registerDevice( { agreeTerms: agreeTerms }, - (regDeviceData) => { - if (regDeviceData && regDeviceData.retCode === 0) { + (newRegDeviceData) => { + if (newRegDeviceData && newRegDeviceData.retCode === 0) { dispatch( getWelcomeEventInfo((eventInfos) => { if ( @@ -301,7 +320,7 @@ export default function IntroPanel({ } dispatch( - sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }) + sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }), ); if (displayWelcomeEventPanel) { @@ -309,13 +328,13 @@ export default function IntroPanel({ pushPanel({ name: panel_names.WELCOME_EVENT_PANEL, panelInfo: { eventInfos: eventInfos.data }, - }) + }), ); } } dispatch(popPanel(panel_names.INTRO_PANEL)); setIsProcessing(false); - }) + }), ); } else { dispatch( @@ -323,7 +342,7 @@ export default function IntroPanel({ title: $L("Error"), text: $L("Device registration failed. Please try again."), button1Text: $L("OK"), - }) + }), ); setIsProcessing(false); } @@ -334,11 +353,11 @@ export default function IntroPanel({ title: $L("Error"), text: $L("Device registration failed. Please try again."), button1Text: $L("OK"), - }) + }), ); setIsProcessing(false); - } - ) + }, + ), ); }, [ termsChecked, @@ -353,16 +372,18 @@ export default function IntroPanel({ useEffect(() => { // isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만) if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) { - console.error( - `[IntroPanel] registerDevice 실패: isProcessing=${isProcessing}, retCode=${regDeviceData.retCode}`, - regDeviceData - ); + if (process.env.NODE_ENV === "development") { + console.error( + `[IntroPanel] registerDevice 실패: isProcessing=${isProcessing}, retCode=${regDeviceData.retCode}`, + regDeviceData, + ); + } dispatch( setShowPopup(Config.ACTIVE_POPUP.alertPopup, { title: $L("Error"), text: $L("Device registration failed. Please try again."), button1Text: $L("OK"), - }) + }), ); setIsProcessing(false); } @@ -373,7 +394,7 @@ export default function IntroPanel({ dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.DO_NOT_AGREE })); }, [dispatch]); - const onExit = useCallback(() => { + const onExit = useCallback(() => { dispatch(setHidePopup()); setShowExitMessagePopup(true); dispatch(setExitApp()); @@ -382,7 +403,7 @@ export default function IntroPanel({ contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, - }) + }), ); }, [dispatch]); @@ -392,102 +413,139 @@ export default function IntroPanel({ } }, [dispatch, activePopup]); - const handleFocus = useCallback((item) => { - if (blurTimeout.current) { - clearTimeout(blurTimeout.current); - blurTimeout.current = null; - } - setFocusedItem(item); - }, []); + const handleFocus = useCallback( + (item) => { + setFocusAsync(item); + }, + [setFocusAsync] + ); const handleBlur = useCallback(() => { - blurTimeout.current = setTimeout(() => { - setFocusedItem(null); - }, 0); - }, []); + clearFocusAsync(0); + }, [clearFocusAsync]); // 체크박스 핸들러들 - const handleTermsToggle = useCallback(({ selected }) => { - setTermsChecked(selected); - }, []); + const handleTermsToggle = useCallback( + async ({ selected }) => { + try { + const newState = await updateStateAsync({ termsChecked: selected }); + if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) { + setTimeout(() => Spotlight.focus("agreeButton"), 100); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("Toggle failed:", error); + } + } + }, + [updateStateAsync] + ); - const handlePrivacyToggle = useCallback(({ selected }) => { - setPrivacyChecked(selected); - }, []); + const handlePrivacyToggle = useCallback( + async ({ selected }) => { + try { + const newState = await updateStateAsync({ privacyChecked: selected }); + if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) { + setTimeout(() => Spotlight.focus("agreeButton"), 100); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("Toggle failed:", error); + } + } + }, + [updateStateAsync] + ); - const handleOptionalToggle = useCallback(({ selected }) => { - setOptionalChecked(selected); - }, []); + const handleOptionalToggle = useCallback( + async ({ selected }) => { + try { + const newState = await updateStateAsync({ optionalChecked: selected }); + if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) { + setTimeout(() => Spotlight.focus("agreeButton"), 100); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("Toggle failed:", error); + } + } + }, + [updateStateAsync] + ); - const handleSelectAllToggle = useCallback(({ selected }) => { - setSelectAllChecked(selected); - setTermsChecked(selected); - setPrivacyChecked(selected); - setOptionalChecked(selected); - }, []); - + const handleSelectAllToggle = useCallback( + async ({ selected }) => { + try { + const newState = await updateStateAsync({ selectAllChecked: selected }); + if (newState.selectAllChecked) { + setTimeout(() => Spotlight.focus("agreeButton"), 100); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("Toggle failed:", error); + } + } + }, + [updateStateAsync] + ); - const rightPanelContent = useMemo(() => { - const requiredItemIds = [ - "termsCheckbox", - "termsButton", - "privacyCheckbox", - "privacyButton", - ]; - if (!requiredItemIds.includes(focusedItem)) { - return ( - <> - {shouldShowBenefitsView ? ( -
- {$L( - 'By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features' - )} -
- ) : ( - - )} - - ); - } + const handleTermsClickMST00402 = useCallback( + () => handleTermsClick("MST00402"), + [handleTermsClick], + ); + const handleTermsClickMST00401 = useCallback( + () => handleTermsClick("MST00401"), + [handleTermsClick], + ); + const handleOptionalTermsClickMST00405 = useCallback( + () => handleOptionalTermsClick("MST00405"), + [handleOptionalTermsClick], + ); - const hasRequiredUnchecked = !termsChecked || !privacyChecked; + const handleFocusTermsCheckbox = useCallback( + () => handleFocus("termsCheckbox"), + [handleFocus], + ); + const handleFocusTermsButton = useCallback( + () => handleFocus("termsButton"), + [handleFocus], + ); + const handleFocusPrivacyCheckbox = useCallback( + () => handleFocus("privacyCheckbox"), + [handleFocus], + ); + const handleFocusPrivacyButton = useCallback( + () => handleFocus("privacyButton"), + [handleFocus], + ); + const handleFocusOptionalCheckbox = useCallback( + () => handleFocus("optionalCheckbox"), + [handleFocus], + ); + const handleFocusOptionalButton = useCallback( + () => handleFocus("optionalButton"), + [handleFocus], + ); + const handleFocusSelectAllCheckbox = useCallback( + () => handleFocus("selectAllCheckbox"), + [handleFocus], + ); + const handleFocusAgreeButton = useCallback( + () => handleFocus("agreeButton"), + [handleFocus], + ); + const handleFocusDisagreeButton = useCallback( + () => handleFocus("disagreeButton"), + [handleFocus], + ); - // 우선순위 1: 필수 약관이 체크 안됨 → 항상 경고 메시지 - if (hasRequiredUnchecked) { - return ( -
-

{$L("Required Consent")}

-

- {$L("(Necessary for using the service)")} -

-
-

- {$L("Please agree to the required Terms & Conditions and")} -

-

- {$L("Privacy Policy to start enjoying Shop Time")} -

-
-
- ); - } + const rightPanelContent = useRightPanelContent( + focusedItem, + termsChecked, + privacyChecked, + shouldShowBenefitsView + ); - // 우선순위 2: 필수 약관에 포커스 있음 → 필수 약관 안내 - return ( -
-

{$L("Required Consent")}

-

- {$L("(Necessary for using the service)")} -

-
- ); - }, [termsChecked, privacyChecked, focusedItem, shouldShowBenefitsView]); - - useEffect(() => { Spotlight.focus(); }, [popupVisible]); @@ -500,19 +558,23 @@ export default function IntroPanel({ // }, [dispatch, regDeviceInfoData]); const description = $L( - "Check out more LIVE SHOWS now and enjoy shopping at Shop Time's special price on your TV by agreeing to the LG TV Shopping Terms and Conditions." + "Check out more LIVE SHOWS now and enjoy shopping at Shop Time's special price on your TV by agreeing to the LG TV Shopping Terms and Conditions.", ); const title = "welcome to shoptime!"; delete rest.isOnTop; - + // [추가] 약관 종류에 따라 팝업 제목을 반환하는 헬퍼 함수 const getTermsPopupTitle = (terms) => { - if (!terms) return ''; + if (!terms) return ""; switch (terms.trmsTpCd) { - case "MST00401": return $L("Privacy Policy"); - case "MST00402": return $L("Terms & Conditions"); - case "MST00405": return $L("Optional Terms"); - default: return ''; + case "MST00401": + return $L("Privacy Policy"); + case "MST00402": + return $L("Terms & Conditions"); + case "MST00405": + return $L("Optional Terms"); + default: + return ""; } }; @@ -528,9 +590,7 @@ export default function IntroPanel({ {/* 첫 번째 영역: 헤더 섹션 */}
-
- {$L("Welcome to")} -
+
{$L("Welcome to")}
{$L("Sh")} {$L("o")} @@ -538,9 +598,7 @@ export default function IntroPanel({
-
- {description} -
+
{description}
@@ -553,15 +611,15 @@ export default function IntroPanel({ className={css.customeCheckbox} selected={termsChecked} onToggle={handleTermsToggle} - onFocus={() => handleFocus('termsCheckbox')} + onFocus={handleFocusTermsCheckbox} onBlur={handleBlur} spotlightId="termsCheckbox" ariaLabel={$L("Terms & Conditions checkbox")} /> handleTermsClick("MST00402")} - onFocus={() => handleFocus('termsButton')} + onClick={handleTermsClickMST00402} + onFocus={handleFocusTermsButton} onBlur={handleBlur} spotlightId="termsButton" type={TYPES.terms} @@ -579,15 +637,15 @@ export default function IntroPanel({ className={css.customeCheckbox} selected={privacyChecked} onToggle={handlePrivacyToggle} - onFocus={() => handleFocus('privacyCheckbox')} + onFocus={handleFocusPrivacyCheckbox} onBlur={handleBlur} spotlightId="privacyCheckbox" ariaLabel={$L("Privacy Policy checkbox")} /> handleTermsClick("MST00401")} - onFocus={() => handleFocus('privacyButton')} + onClick={handleTermsClickMST00401} + onFocus={handleFocusPrivacyButton} onBlur={handleBlur} spotlightId="privacyButton" type={TYPES.terms} @@ -605,29 +663,25 @@ export default function IntroPanel({ className={css.customeCheckbox} selected={optionalChecked} onToggle={handleOptionalToggle} - onFocus={() => handleFocus('optionalCheckbox')} + onFocus={handleFocusOptionalCheckbox} onBlur={handleBlur} spotlightId="optionalCheckbox" ariaLabel={$L("Optional Terms checkbox")} /> handleOptionalTermsClick("MST00405")} - onFocus={() => handleFocus('optionalButton')} + onClick={handleOptionalTermsClickMST00405} + onFocus={handleFocusOptionalButton} onBlur={handleBlur} spotlightId="optionalButton" type={TYPES.terms} ariaLabel={$L("View Optional Terms")} > - - {$L("Optional Terms")} - + {$L("Optional Terms")}
-
-
- {rightPanelContent} -
+ +
{rightPanelContent}
{/* 세 번째 영역: Select All */} @@ -636,14 +690,12 @@ export default function IntroPanel({ className={css.selectAllCheckbox} selected={selectAllChecked} onToggle={handleSelectAllToggle} - onFocus={() => handleFocus('selectAllCheckbox')} + onFocus={handleFocusSelectAllCheckbox} onBlur={handleBlur} spotlightId="selectAllCheckbox" ariaLabel={$L("Select All checkbox")} /> - - {$L("Select All")} - + {$L("Select All")} {/* 네 번째 영역: 버튼 섹션 */} @@ -651,7 +703,7 @@ export default function IntroPanel({ handleFocus('agreeButton')} + onFocus={handleFocusAgreeButton} onBlur={handleBlur} spotlightId="agreeButton" type={TYPES.agree} @@ -664,7 +716,7 @@ export default function IntroPanel({ handleFocus('disagreeButton')} + onFocus={handleFocusDisagreeButton} onBlur={handleBlur} spotlightId="disagreeButton" type={TYPES.agree} @@ -693,7 +745,7 @@ export default function IntroPanel({ spotlightId="tPopupBtn1" /> */} - + {/* TermsPopup 호출은 완전히 삭제 */} {/* TNewPopUp을 사용한 새로운 팝업 구현 */} @@ -701,8 +753,7 @@ export default function IntroPanel({ open={popupVisible} kind="figmaTermsPopup" title={getTermsPopupTitle(currentTerms)} - text={currentTerms?.trmsCntt || ''} - onIntroTermsAgreeClick={handleTermsAgree} // Agree 버튼 핸들러 연결 + text={currentTerms?.trmsCntt || ""} onClose={onClose} // Close 버튼 핸들러 연결 /> @@ -745,10 +796,11 @@ export default function IntroPanel({ open={showExitMessagePopup} hasText title={$L("Exit Shop Time")} - text={$L("Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds.")} + text={$L( + "Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds.", + )} /> )} ); } - diff --git a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less index ceafc45e..1cc7fe22 100644 --- a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less +++ b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less @@ -2,13 +2,13 @@ @import "../../style/utils.module.less"; @COLOR_GREEN: #207847; -@COLOR_RED: #C91D53; -@COLOR_GRAY: #807F81; -@COLOR_GRAY02: #CFCFCF; -@COLOR_GRAY03: #57585A; -@COLOR_GRAY04: #C5C6C9; -@COLOR_GRAY05: #F8F8F8; -@COLOR_GRAY06: #57585A; +@COLOR_RED: #c91d53; +@COLOR_GRAY: #807f81; +@COLOR_GRAY02: #cfcfcf; +@COLOR_GRAY03: #57585a; +@COLOR_GRAY04: #c5c6c9; +@COLOR_GRAY05: #f8f8f8; +@COLOR_GRAY06: #57585a; .panel { > section { @@ -47,9 +47,9 @@ display: inline-flex; .welcomeText { - color: #807F81; + color: #807f81; font-size: 62px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 62px; word-wrap: break-word; @@ -57,27 +57,27 @@ .brandContainer { .shopText { - color: #57585A; + color: #57585a; font-size: 70px; - font-family: 'LG Smart_Korean'; + font-family: "LG Smart_Korean"; font-weight: 700; line-height: 75px; word-wrap: break-word; } .oText { - color: #C91D53; + color: #c91d53; font-size: 70px; - font-family: 'LG Smart_Korean'; + font-family: "LG Smart_Korean"; font-weight: 700; line-height: 75px; word-wrap: break-word; } .timeText { - color: #57585A; + color: #57585a; font-size: 70px; - font-family: 'LG Smart_Korean'; + font-family: "LG Smart_Korean"; font-weight: 700; line-height: 75px; word-wrap: break-word; @@ -95,9 +95,9 @@ .descriptionText { width: 1012.49px; text-align: center; - color: #807F81; + color: #807f81; font-size: 36px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 43px; word-wrap: break-word; @@ -126,7 +126,8 @@ justify-content: flex-start; align-items: center; gap: 20px; - display: inline-flex; .checkbox { + display: inline-flex; + .checkbox { /* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */ width: 45px; height: 45px; @@ -137,9 +138,9 @@ height: 120px; padding: 0 50px; background: @COLOR_WHITE; - box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20); + box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2); border-radius: 6px; - border: 1px solid #CFCFCF; + border: 1px solid #cfcfcf; justify-content: space-between; align-items: center; display: flex; @@ -150,7 +151,7 @@ .termsText { color: black; font-size: 35px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 700; line-height: 35px; word-wrap: break-word; @@ -166,23 +167,24 @@ &:focus, &:focus-visible, &:hover { - outline: 4px #C91D53 solid !important; + outline: 4px #c91d53 solid !important; outline-offset: 2px !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(201, 29, 83, 0.3) !important; - + .termsText { font-weight: bold !important; } } } } - } .termsRightPanel { + } + .termsRightPanel { flex: 1 1 0; align-self: stretch; padding-left: 60px; padding-right: 60px; - border-left: 1px #C5C6C9 solid; + border-left: 1px #c5c6c9 solid; justify-content: center; align-items: flex-start; display: flex; @@ -195,7 +197,8 @@ justify-content: center; align-items: center; gap: 15px; - display: inline-flex; .selectAllCheckbox { + display: inline-flex; + .selectAllCheckbox { /* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */ width: 45px; height: 45px; @@ -204,7 +207,7 @@ .selectAllText { color: @COLOR_BLACK; font-size: 35px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 700; line-height: 35px; word-wrap: break-word; @@ -279,30 +282,33 @@ width: 45px; height: 45px; position: relative; - + // 기본 상자 스타일 &:before { - content: ''; + content: ""; width: 42px; height: 42px; background-color: @COLOR_WHITE; border: 2px solid @COLOR_GRAY02; border-radius: 4px; display: block; - transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + transition: + background-color 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; } // ✅ 선택된 상태: 배경색과 테두리 모두 붉은색 + 굵은 테두리 &.selected:before { - background-color: #C91D53 !important; - border: 4px solid #C91D53 !important; // 굵은 테두리로 변경 + background-color: #c91d53 !important; + border: 4px solid #c91d53 !important; // 굵은 테두리로 변경 box-shadow: 0 0 8px rgba(201, 29, 83, 0.3); // 약간의 그림자 효과 } - + // 포커스 받았지만 선택 안됨 &.focused:not(.selected):before { background-color: rgba(201, 29, 83, 0.1); - border: 4px solid #C91D53 !important; + border: 4px solid #c91d53 !important; box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important; } @@ -315,13 +321,13 @@ // 포커스 받음 (선택된 상태가 아닌 경우에만 적용) &.focused:not(.selected):before { - border: 4px solid #C91D53 !important; + border: 4px solid #c91d53 !important; box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important; } - + // 체크마크 &.selected:after { - content: '✓'; + content: "✓"; color: @COLOR_WHITE; font-size: 24px; font-weight: bold; @@ -356,10 +362,10 @@ .optionalDescription { width: 595px; - color: #807F81; + color: #807f81; font-size: 26px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 43px; word-wrap: break-word; -} \ No newline at end of file +} diff --git a/com.twin.app.shoptime/src/views/MainView/MainView.jsx b/com.twin.app.shoptime/src/views/MainView/MainView.jsx index 1254881a..451d5832 100644 --- a/com.twin.app.shoptime/src/views/MainView/MainView.jsx +++ b/com.twin.app.shoptime/src/views/MainView/MainView.jsx @@ -76,6 +76,7 @@ import LoadingPanel from "../LoadingPanel/LoadingPanel"; import MyPagePanel from "../MyPagePanel/MyPagePanel"; import OnSalePanel from "../OnSalePanel/OnSalePanel"; import PlayerPanel from "../PlayerPanel/PlayerPanel"; +import PlayerPanelNew from "../PlayerPanel/PlayerPanel.new"; import SearchPanel from "../SearchPanel/SearchPanel"; import ThemeCurationPanel from "../ThemeCurationPanel/ThemeCurationPanel"; import TrendingNowPanel from "../TrendingNowPanel/TrendingNowPanel"; @@ -108,6 +109,7 @@ const panelMap = { [Config.panel_names.VIDEO_TEST_PANEL]: VideoTestPanel, [Config.panel_names.DETAIL_PANEL]: DetailPanel, [Config.panel_names.PLAYER_PANEL]: PlayerPanel, + [Config.panel_names.PLAYER_PANEL_NEW]: PlayerPanelNew, [Config.panel_names.CHECKOUT_PANEL]: CheckOutPanel, [Config.panel_names.WELCOME_EVENT_PANEL]: WelcomeEventPanel, [Config.panel_names.THEME_CURATION_PANEL]: ThemeCurationPanel, @@ -139,7 +141,8 @@ export default function MainView({ className, initService }) { const panels = useSelector((state) => state.panels.panels); const lastPanelAction = useSelector((state) => state.panels.lastPanelAction); const loadingComplete = useSelector((state) => state.common?.loadingComplete); - const menuData = useSelector((state) => state.home.menuData?.data); const { + const menuData = useSelector((state) => state.home.menuData?.data); + const { popupVisible, activePopup, data: errorCode, @@ -154,10 +157,10 @@ export default function MainView({ className, initService }) { deviceId, } = useSelector((state) => state.common.appStatus); const skipEndOfServicePopup = useSelector( - (state) => state.localSettings.skipEndOfServicePopup + (state) => state.localSettings.skipEndOfServicePopup, ); const isInternetConnected = useSelector( - (state) => state.common.appStatus.isInternetConnected + (state) => state.common.appStatus.isInternetConnected, ); const deviceCountryCode = httpHeader?.["X-Device-Country"] || ""; @@ -172,9 +175,8 @@ export default function MainView({ className, initService }) { const [showEndOfServicePopup, setShowEndOfServicePopup] = useState(false); const topPanel = panels[panels.length - 1]; - useEffect(() => { - console.log('🔍 MainView 팝업 상태 변경:', { + console.log("🔍 MainView 팝업 상태 변경:", { popupVisible, activePopup, }); @@ -202,6 +204,8 @@ 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 ) { renderingPanels = panels.slice(-2); @@ -212,7 +216,8 @@ 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 || + panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW))) && ( { @@ -377,7 +384,7 @@ export default function MainView({ className, initService }) { dispatch( changeAppStatus({ cursorVisible: window.cursorEvent && window.cursorEvent.visibility, - }) + }), ); } return () => { @@ -390,7 +397,7 @@ export default function MainView({ className, initService }) { const popupTimerRef = useRef(null); // 타이머 ID를 저장할 상태 변수 const { upComingAlertShow } = useSelector( - (state) => state.myPage.upComingData + (state) => state.myPage.upComingData, ); useEffect(() => { @@ -423,7 +430,7 @@ export default function MainView({ className, initService }) { patnrId: patnrId.toString(), showId, showNm, - }) + }), ); } }, [activePopup, alertItems, popupVisible]); @@ -465,7 +472,7 @@ export default function MainView({ className, initService }) { upComingAlertShow.alertShows?.length > 0 ) { const alertList = upComingAlertShow.alertShows.filter((show) => - isSameDateTime(show.strtDt) + isSameDateTime(show.strtDt), ); if (alertList.length > 0) { @@ -518,7 +525,7 @@ export default function MainView({ className, initService }) { patnrId: patnrId.toString(), showId, showNm, - }) + }), ); dispatch(resetPanels()); @@ -526,7 +533,7 @@ export default function MainView({ className, initService }) { pushPanel({ name: panel_names.FEATURED_BRANDS_PANEL, panelInfo: { from: "upcoming", patnrId: patnrId.toString() }, - }) + }), ); dispatch(setHidePopup()); setIntervalActive((prev) => !prev); @@ -575,7 +582,7 @@ export default function MainView({ className, initService }) { patnrId: patnrId.toString(), showId, showNm, - }) + }), ); setIntervalActive((prev) => !prev); @@ -639,7 +646,12 @@ 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()); } @@ -678,7 +690,7 @@ export default function MainView({ className, initService }) { loadingComplete && (
@@ -731,9 +743,10 @@ export default function MainView({ className, initService }) { popupData.errorCode, popupData.errorMsg, popupData.retDetailCode, - popupData.returnBindStrings + popupData.returnBindStrings, )} -

+

{" "} + {$L("OK")}
@@ -781,8 +794,6 @@ export default function MainView({ className, initService }) { /> ) : null} - - {loadingComplete && activePopup === Config.ACTIVE_POPUP.endOfServicePopup && diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index a5a49e60..d40135de 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -1535,8 +1535,24 @@ const PlayerPanel = ({ if (broadcast.type === "videoError") { return null; } + + // For previews, always use the direct URL from panelInfo. + if (panelInfo.modal) { + return panelInfo.showUrl; + } + + // For fullscreen, the playlist is the primary source. + if (playListInfo && playListInfo.length > 0 && playListInfo[selectedIndex]?.showUrl) { + return playListInfo[selectedIndex]?.showUrl; + } + + // Fallback for fullscreen if playlist isn't ready, use panelInfo URL. + if (!panelInfo.modal && panelInfo.showUrl) { + return panelInfo.showUrl; + } + return playListInfo && playListInfo[selectedIndex]?.showUrl; - }, [playListInfo, selectedIndex, broadcast]); + }, [panelInfo, playListInfo, selectedIndex, broadcast]); const isYoutube = useMemo(() => { if (currentPlayingUrl && currentPlayingUrl.includes("youtu")) { diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx new file mode 100644 index 00000000..8f6234b7 --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.jsx @@ -0,0 +1,2281 @@ +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + } from "react"; + + import classNames from "classnames"; + import { useDispatch } from "react-redux"; + + import { Job } from "@enact/core/util"; + import Spotlight from "@enact/spotlight"; + import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; + import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; + + import dummyVtt from "../../../assets/mock/video.vtt"; + import { + changeAppStatus, + changeLocalSettings, + requestLiveSubtitle, + sendBroadCast, + setHidePopup, + } from "../../actions/commonActions"; + import { + sendLogGNB, + sendLogLive, + sendLogTotalRecommend, + sendLogVOD, + } from "../../actions/logActions"; + import { + clearShopNowInfo, + getHomeFullVideoInfo, + getMainCategoryShowDetail, + getMainLiveShow, + getMainLiveShowNowProduct, + } from "../../actions/mainActions"; + import * as PanelActions from "../../actions/panelActions"; + import { updatePanel } from "../../actions/panelActions"; + import { + CLEAR_PLAYER_INFO, + getChatLog, + getSubTitle, + startVideoPlayer, + } from "../../actions/playActions"; + import { convertUtcToLocal } from "../../components/MediaPlayer/util"; + import TPanel from "../../components/TPanel/TPanel"; + import TPopUp from "../../components/TPopUp/TPopUp"; + import Media from "../../components/VideoPlayer/Media"; + import TReactPlayer from "../../components/VideoPlayer/TReactPlayer"; + import { VideoPlayer } from "../../components/VideoPlayer/VideoPlayer"; + import usePrevious from "../../hooks/usePrevious"; + import useWhyDidYouUpdate from "../../hooks/useWhyDidYouUpdate"; + import * as Config from "../../utils/Config"; + import { ACTIVE_POPUP, panel_names } from "../../utils/Config"; + import { $L, formatGMTString } from "../../utils/helperMethods"; + import { SpotlightIds } from "../../utils/SpotlightIds"; + import { removeDotAndColon } from "./PlayerItemCard/PlayerItemCard"; + import PlayerOverlayChat from "./PlayerOverlay/PlayerOverlayChat"; + import PlayerOverlayQRCode from "./PlayerOverlay/PlayerOverlayQRCode"; + import css from "./PlayerPanel.module.less"; + import PlayerTabButton from "./PlayerTabContents/TabButton/PlayerTabButton"; + import TabContainer from "./PlayerTabContents/TabContainer"; + + const Container = SpotlightContainerDecorator( + { enterTo: "default-element", preserveld: true }, + "div" + ); + + const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => { + try { + if (currentAttempts >= maxAttempts) { + throw new Error("selector not found"); + } + + const initialSelector = document.querySelector(selector); + + if (initialSelector) { + return initialSelector; + } else { + return findSelector(selector, maxAttempts, currentAttempts + 1); + } + } catch (error) { + // console.error(error.message); + } + }; + + const getLogTpNo = (type, nowMenu) => { + if (type === "LIVE") { + switch (nowMenu) { + case Config.LOG_MENU.HOME_TOP: + return Config.LOG_TP_NO.LIVE.HOME; + // case Config.LOG_MENU.FEATURED_BRANDS_LIVE_CHANNELS: + // return Config.LOG_TP_NO.LIVE.FEATURED_BRANDS; + case Config.LOG_MENU.FULL_SHOP_NOW: + case Config.LOG_MENU.FULL_YOU_MAY_LIKE: + case Config.LOG_MENU.FULL_LIVE_CHANNELS: + return Config.LOG_TP_NO.LIVE.FULL; + default: + return Config.LOG_TP_NO.LIVE.FEATURED_BRANDS; + } + } else if (type === "VOD") { + switch (nowMenu) { + case Config.LOG_MENU.HOME_TOP: + return Config.LOG_TP_NO.VOD.HOME_VOD; // 153 + case Config.LOG_MENU.TRENDING_NOW_POPULAR_SHOWS: + return Config.LOG_TP_NO.VOD.POPULAR_SHOWS_AND_HOT_PICKS; // 151 + case Config.LOG_MENU.FULL_SHOP_NOW: + case Config.LOG_MENU.FULL_YOU_MAY_LIKE: + case Config.LOG_MENU.FULL_FEATURED_SHOWS: + return Config.LOG_TP_NO.VOD.FULL_VOD; // 150 + default: + return; + } + } else if (type === "MEDIA") { + switch (nowMenu) { + case Config.LOG_MENU.HOME_TOP: + return Config.LOG_TP_NO.VOD.HOME_VOD; // 153 + case Config.LOG_MENU.HOT_PICKS: + return Config.LOG_TP_NO.VOD.POPULAR_SHOWS_AND_HOT_PICKS; // 151 + case Config.LOG_MENU.DETAIL_PAGE_BILLING_PRODUCT_DETAIL: + case Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL: + case Config.LOG_MENU.DETAIL_PAGE_GROUP_DETAIL: + case Config.LOG_MENU.DETAIL_PAGE_THEME_DETAIL: + case Config.LOG_MENU.DETAIL_PAGE_TRAVEL_THEME_DETAIL: + return Config.LOG_TP_NO.VOD.ITEM_DETAIL_MEDIA; // 156 + case Config.LOG_MENU.FULL: + return Config.LOG_TP_NO.VOD.FULL_MEDIA; // 155 + default: + return; + } + } + }; + + const YOUTUBECONFIG = { + playerVars: { + controls: 0, // 플레이어 컨트롤 표시 + autoplay: 1, + disablekb: 1, + enablejsapi: 1, + listType: "user_uploads", + fs: 0, + rel: 0, // 관련 동영상 표시 안 함 + showinfo: 0, + loop: 0, + iv_load_policy: 3, + modestbranding: 1, + wmode: "opaque", + cc_lang_pref: "en", + cc_load_policy: 0, + playsinline: 1, + }, + }; + + const INITIAL_TIMEOUT = 30000; + const REGULAR_TIMEOUT = 30000; + const TAB_CONTAINER_SPOTLIGHT_ID = "tab-container-spotlight-id"; + const TARGET_EVENTS = ["mousemove", "keydown", "click"]; + + // last time error + const VIDEO_END_ACTION_DELAY = 1500; + + const PlayerPanelNew = ({ + isTabActivated, + panelInfo, + isOnTop, + spotlightId, + ...props + }) => { + const dispatch = useDispatch(); + const { USE_STATE, USE_SELECTOR } = useWhyDidYouUpdate(spotlightId, { + isTabActivated, + panelInfo, + isOnTop, + ...props, + }); + + const videoPlayer = useRef(null); + const [playListInfo, setPlayListInfo] = USE_STATE("playListInfo", ""); + const [shopNowInfo, setShopNowInfo] = USE_STATE("shopNowInfo"); + const [backupInitialIndex, setBackupInitialIndex] = USE_STATE( + "backupInitialIndex", + 0 + ); + const [modalStyle, setModalStyle] = USE_STATE("modalStyle", {}); + const [modalScale, setModalScale] = USE_STATE("modalScale", 1); + const [mediaId, setMediaId] = USE_STATE("mediaId", null); + const [currentLiveTimeSeconds, setCurrentLiveTimeSeconds] = USE_STATE( + "currentLiveTimeSeconds", + 1 + ); + const [prevChannelIndex, setPrevChannelIndex] = USE_STATE( + "prevChannelIndex", + 0 + ); + const [sideContentsVisible, setSideContentsVisible] = USE_STATE( + "sideContentsVisible", + true + ); + const [currentTime, setCurrentTime] = USE_STATE("currentTime", 0); + const [isInitialFocusOccurred, setIsInitialFocusOccurred] = USE_STATE( + "isInitialFocusOccurred", + false + ); + const [selectedIndex, setSelectedIndex] = USE_STATE( + "selectedIndex", + panelInfo.shptmBanrTpNm === "LIVE" ? null : 0 + ); + const [isUpdate, setIsUpdate] = USE_STATE("isUpdate", false); + const [isSubtitleActive, setIsSubtitleActive] = USE_STATE( + "isSubtitleActive", + true + ); + const [logStatus, setLogStatus] = USE_STATE("logStatus", { + isModalLiveLogReady: false, + isFullLiveLogReady: false, + isDetailLiveLogReady: false, + isModalVodLogReady: false, + isFullVodLogReady: false, + isDetailVodLogReady: false, + isModalMediaLogReady: false, + isFullMediaLogReady: false, + isDetailMediaReady: false, + }); + const [isVODPaused, setIsVODPaused] = USE_STATE("isVODPaused", false); + + const panels = USE_SELECTOR("panels", (state) => state.panels.panels); + const chatData = USE_SELECTOR("chatData", (state) => state.play.chatData); + + const popupVisible = USE_SELECTOR( + "popupVisible", + (state) => state.common.popup.popupVisible + ); + const activePopup = USE_SELECTOR( + "activePopup", + (state) => state.common.popup.activePopup + ); + const showDetailInfo = USE_SELECTOR( + "showDetailInfo", + (state) => state.main.showDetailInfo + ); + const productImageLength = USE_SELECTOR( + "productImageLength", + (state) => state.product.productImageLength + ); + const themeProductInfos = USE_SELECTOR( + "themeProductInfos", + (state) => state.home.themeCurationDetailInfoData + ); + const hotelInfos = USE_SELECTOR( + "hotelInfos", + (state) => state.home.themeCurationHotelDetailData + ); + const captionEnable = USE_SELECTOR( + "captionEnable", + (state) => state.common.appStatus.captionEnable + ); + const fullVideolgCatCd = USE_SELECTOR( + "fullVideolgCatCd", + (state) => state.main.fullVideolgCatCd + ); + const featuredShowsInfos = USE_SELECTOR( + "featuredShowsInfos", + (state) => state.main.featuredShowsInfos + ); + const localRecentItems = USE_SELECTOR( + "localRecentItems", + (state) => state.localSettings?.recentItems + ); + const httpHeader = USE_SELECTOR( + "httpHeader", + (state) => state.common?.httpHeader + ); + const countryCode = USE_SELECTOR( + "countryCode", + (state) => state.common.httpHeader?.cntry_cd + ); + const liveChannelInfos = USE_SELECTOR( + "liveChannelInfos", + (state) => state.main.liveChannelInfos + ); + const showNowInfos = USE_SELECTOR( + "showNowInfos", + (state) => state.main.showNowInfo + ); + const liveShowInfos = USE_SELECTOR( + "liveShowInfos", + (state) => state.main.liveShowInfos + ); + const vodSubtitleData = USE_SELECTOR( + "vodSubtitleData", + (state) => state.play.subTitleBlobs + ); + const broadcast = USE_SELECTOR( + "broadcast", + (state) => state.common.broadcast + ); + + const lastPanelAction = USE_SELECTOR( + "lastPanelAction", + (state) => state.panels.lastPanelAction + ); + const nowMenu = USE_SELECTOR("nowMenu", (state) => state.common.menu.nowMenu); + const nowMenuRef = usePrevious(nowMenu); + const entryMenu = USE_SELECTOR( + "entryMenu", + (state) => state.common.menu.entryMenu + ); + const [videoLoaded, setVideoLoaded] = USE_STATE("videoLoaded", false); + const entryMenuRef = usePrevious(entryMenu); + const panelInfoRef = usePrevious(panelInfo); + + const initialFocusTimeoutJob = useRef(new Job((func) => func(), 100)); + const liveLogParamsRef = useRef(null); + const vodLogParamsRef = useRef(null); + const mediaLogParamsRef = useRef(null); + const prevNowMenuRef = useRef(null); + const watchInterval = useRef(null); + // useEffect(() => { + // console.log("###videoLoaded", videoLoaded); + // if (nowMenu) { + // } + // }, [videoLoaded]); + const currentLiveShowInfo = useMemo(() => { + if (liveShowInfos && liveShowInfos.length > 0) { + const panelInfoChanId = panelInfo?.chanId; + const isLive = panelInfo?.shptmBanrTpNm === "LIVE"; + + if (isLive) { + const liveShowInfo = liveShowInfos // + .find(({ chanId }) => panelInfoChanId === chanId); + + return liveShowInfo; + } + + return {}; + } + + return {}; + }, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm]); + + const currentVODShowInfo = useMemo(() => { + if (showDetailInfo && showDetailInfo.length > 0) { + const isVOD = panelInfo?.shptmBanrTpNm === "VOD"; + + if (isVOD) { + const vodShowInfo = showDetailInfo[0]; + + return vodShowInfo; + } + + return {}; + } + + return {}; + }, [panelInfo?.shptmBanrTpNm, showDetailInfo]); + + useEffect(() => { + if (!panelInfo?.modal && panelInfo?.shptmBanrTpNm === "MEDIA") { + dispatch(sendLogGNB(Config.LOG_MENU.FULL)); + prevNowMenuRef.current = nowMenuRef.current; + + return () => dispatch(sendLogGNB(prevNowMenuRef.current)); + } + }, [panelInfo?.modal, panelInfo?.shptmBanrTpNm]); + + // creating live log params + useEffect(() => { + if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) { + if (currentLiveShowInfo.showId !== panelInfo?.showId) { + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + chanId: currentLiveShowInfo.chanId, + modalContainerId: panelInfo?.modalContainerId, + patnrId: currentLiveShowInfo.patnrId, + showId: currentLiveShowInfo.showId, + showUrl: currentLiveShowInfo.showUrl, + }, + }) + ); + + return; + } + + let logTemplateNo; + + if (isOnTop && panelInfo?.modal) { + logTemplateNo = getLogTpNo("LIVE", nowMenu); + setLogStatus((status) => ({ ...status, isModalLiveLogReady: true })); + } + // + else if (isOnTop && !panelInfo?.modal) { + logTemplateNo = Config.LOG_TP_NO.LIVE.FULL; + setLogStatus((status) => ({ ...status, isFullLiveLogReady: true })); + } + // + else if (!isOnTop && !panelInfo?.modal) { + logTemplateNo = Config.LOG_TP_NO.LIVE.ITEM_DETAIL; + setLogStatus((status) => ({ ...status, isDetailLiveLogReady: true })); + } + + liveLogParamsRef.current = { + entryMenu: entryMenuRef.current, + lgCatCd: currentLiveShowInfo.catCd ?? "", + lgCatNm: currentLiveShowInfo.catNm ?? "", + linkTpCd: panelInfo?.linkTpCd ?? "", + logTpNo: logTemplateNo, + nowMenu: nowMenuRef.current, + patncNm: currentLiveShowInfo.patncNm, + patnrId: currentLiveShowInfo.patnrId, + showId: currentLiveShowInfo.showId, + showNm: currentLiveShowInfo.showNm, + vdoTpNm: currentLiveShowInfo.vtctpYn + ? currentLiveShowInfo.vtctpYn === "Y" + ? "Vertical" + : "Horizontal" + : "", + }; + } + }, [ + currentLiveShowInfo, + isOnTop, + nowMenu, + panelInfo?.linkTpCd, + panelInfo?.modal, + panelInfo?.modalContainerId, + panelInfo?.showId, + setLogStatus, + ]); + + // send live log + useEffect(() => { + if (broadcast && Object.keys(broadcast).length === 0) { + // case: live, modal + if ( + logStatus.isModalLiveLogReady && + isOnTop && + panelInfo?.modal && + liveLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...liveLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isModalLiveLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + + // case: live, full + if ( + logStatus.isFullLiveLogReady && + isOnTop && + !panelInfo?.modal && + liveLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...liveLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isFullLiveLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + + // case: live, item detail + if ( + logStatus.isDetailLiveLogReady && + !isOnTop && + !panelInfo?.modal && + liveLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...liveLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isDetailLiveLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + } + }, [ + broadcast, + isOnTop, + logStatus.isDetailLiveLogReady, + logStatus.isFullLiveLogReady, + logStatus.isModalLiveLogReady, + panelInfo?.modal, + panelInfo?.showId, + setLogStatus, + ]); + + // creating VOD log params + useEffect(() => { + if ( + currentVODShowInfo && + Object.keys(currentVODShowInfo).length > 0 && + !isVODPaused + ) { + let logTemplateNo; + + if (isOnTop && panelInfo?.modal) { + logTemplateNo = getLogTpNo("VOD", nowMenu); + setLogStatus((status) => ({ ...status, isModalVodLogReady: true })); + } + // + else if (isOnTop && !panelInfo?.modal) { + logTemplateNo = Config.LOG_TP_NO.VOD.FULL_VOD; + setLogStatus((status) => ({ ...status, isFullVodLogReady: true })); + } + // + else if (!isOnTop && !panelInfo?.modal) { + logTemplateNo = Config.LOG_TP_NO.VOD.ITEM_DETAIL_VOD; + setLogStatus((status) => ({ ...status, isDetailVodLogReady: true })); + } + + vodLogParamsRef.current = { + entryMenu: entryMenuRef.current, + lgCatCd: currentVODShowInfo?.showCatCd ?? "", + lgCatNm: currentVODShowInfo?.showCatNm ?? "", + logTpNo: logTemplateNo, + linkTpCd: panelInfo?.linkTpCd ?? "", + nowMenu: nowMenuRef.current, + patncNm: currentVODShowInfo?.patncNm, + patnrId: currentVODShowInfo?.patnrId, + showId: currentVODShowInfo?.showId, + showNm: currentVODShowInfo?.showNm, + vdoTpNm: currentVODShowInfo?.vtctpYn + ? currentVODShowInfo.vtctpYn === "Y" + ? "Vertical" + : "Horizontal" + : "", + }; + } + }, [ + currentVODShowInfo, + isOnTop, + isVODPaused, + nowMenu, + panelInfo?.linkTpCd, + panelInfo?.modal, + ]); + + // send VOD log + useEffect(() => { + if (broadcast && Object.keys(broadcast).length === 0 && !isVODPaused) { + // case: VOD, modal + if ( + logStatus.isModalVodLogReady && + isOnTop && + panelInfo?.modal && + vodLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...vodLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isModalVodLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + + // case: VOD, full + if ( + logStatus.isFullVodLogReady && + isOnTop && + !panelInfo?.modal && + vodLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...vodLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isFullVodLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + + // case: VOD, item detail + if ( + logStatus.isDetailVodLogReady && + !isOnTop && + !panelInfo?.modal && + vodLogParamsRef.current?.showId === panelInfo?.showId + ) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...vodLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isDetailVodLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogVOD({ ...vodLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + } + }, [ + broadcast, + isOnTop, + isVODPaused, + logStatus.isDetailVodLogReady, + logStatus.isFullVodLogReady, + logStatus.isModalVodLogReady, + panelInfo?.modal, + panelInfo?.showId, + setLogStatus, + ]); + + // creating media log params + useEffect(() => { + if (panelInfo?.shptmBanrTpNm === "MEDIA" && isOnTop && !isVODPaused) { + let logTemplateNo; + + if (panelInfo?.modal) { + logTemplateNo = getLogTpNo("MEDIA", nowMenu); + setLogStatus((status) => ({ ...status, isModalMediaLogReady: true })); + } + // + else if (!panelInfo?.modal) { + logTemplateNo = Config.LOG_TP_NO.VOD.FULL_MEDIA; + setLogStatus((status) => ({ ...status, isFullMediaLogReady: true })); + } + + mediaLogParamsRef.current = { + entryMenu: entryMenuRef.current, + lgCatCd: panelInfo?.lgCatCd ?? "", + lgCatNm: panelInfo?.lgCatNm ?? "", + logTpNo: logTemplateNo, + linkTpCd: panelInfo?.linkTpCd ?? "", + nowMenu: nowMenuRef.current, + patncNm: panelInfo?.patncNm ?? "", + patnrId: panelInfo?.patnrId ?? "", + showId: panelInfo?.prdtId ?? panelInfo?.showId, + showNm: panelInfo?.prdtNm ?? panelInfo?.showNm, + vdoTpNm: "Horizontal", + }; + } + }, [ + isOnTop, + isVODPaused, + nowMenu, + panelInfo?.lgCatCd, + panelInfo?.lgCatNm, + panelInfo?.linkTpCd, + panelInfo?.modal, + panelInfo?.patncNm, + panelInfo?.patnrId, + panelInfo?.prdtId, + panelInfo?.prdtNm, + panelInfo?.showId, + panelInfo?.showNm, + panelInfo?.shptmBanrTpNm, + setLogStatus, + ]); + + // send log media + useEffect(() => { + if (broadcast && Object.keys(broadcast).length === 0 && !isVODPaused) { + // case: media, modal + if (logStatus.isModalMediaLogReady && panelInfo?.modal) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...mediaLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isModalMediaLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogVOD({ ...mediaLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + } + + // case: media, full + if (logStatus.isFullMediaLogReady && !panelInfo?.modal) { + let watchStrtDt = formatGMTString(new Date()); + + watchInterval.current = setInterval(() => { + let watchEndDt = formatGMTString(new Date()); + let watchRecord = { + ...mediaLogParamsRef.current, + watchStrtDt, + watchEndDt, + }; + dispatch(changeLocalSettings({ watchRecord })); + }, 10000); + + return () => { + setLogStatus((status) => ({ + ...status, + isFullMediaLogReady: false, + })); + clearInterval(watchInterval.current); + dispatch( + sendLogVOD({ ...mediaLogParamsRef.current, watchStrtDt }, () => + dispatch(changeLocalSettings({ watchRecord: {} })) + ) + ); + }; + } + }, [ + broadcast, + isVODPaused, + logStatus.isFullMediaLogReady, + logStatus.isModalMediaLogReady, + panelInfo?.modal, + setLogStatus, + ]); + + const videoVerticalVisible = useMemo(() => { + if (playListInfo && playListInfo[selectedIndex]?.vtctpYn === "Y") { + return true; + } + return false; + }, [playListInfo, selectedIndex]); + + const handleItemFocus = useCallback( + (menu) => { + dispatch(sendLogGNB(menu)); + + if (!videoVerticalVisible) { + resetTimer(REGULAR_TIMEOUT); + } + }, + [resetTimer, videoVerticalVisible] + ); + + const onClickBack = useCallback( + (ev, isEnd) => { + //modal로부터 Full 전환된 경우 다시 preview 모드로 돌아감. + + if ( + sideContentsVisible && + !videoVerticalVisible && + panelInfo.shptmBanrTpNm !== "MEDIA" + ) { + setSideContentsVisible(false); + ev?.stopPropagation(); + // ev?.preventDefault(); + return; + } + + if (panelInfo.modalContainerId && !panelInfo.modal) { + dispatch( + startVideoPlayer({ + ...panelInfo, + modal: true, + modalClassName: "", + }) + ); + videoPlayer.current?.hideControls(); + setSelectedIndex(backupInitialIndex); + if (panelInfo.shptmBanrTpNm === "MEDIA") { + dispatch( + updatePanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + launchedFromPlayer: false, + }, + }) + ); + } + ev?.stopPropagation(); + // ev?.preventDefault(); + return; + } + + if (!panelInfo.modal) { + dispatch(PanelActions.popPanel()); + dispatch(changeAppStatus({ cursorVisible: false })); + + //딮링크로 플레이어 진입 후 이전버튼 클릭시 + if (panels.length === 1) { + setTimeout(() => { + Spotlight.focus(SpotlightIds.HOME_TBODY); + }); + } + ev?.stopPropagation(); + // ev?.preventDefault(); + return; + } + }, + [ + dispatch, + panelInfo, + videoPlayer, + sideContentsVisible, + videoVerticalVisible, + backupInitialIndex, + ] + ); + + useEffect(() => { + //todo if(modal) + return () => { + // 패널이 2개 존재할때만 popPanel 진행 + if (panelInfo.modal && !isOnTop) { + dispatch(PanelActions.popPanel()); + } else { + Spotlight.focus("tbody"); + } + }; + }, [panelInfo?.modal, isOnTop]); + + useEffect(() => { + if (showNowInfos && panelInfo.shptmBanrTpNm === "LIVE") { + const period = + showNowInfos.period !== undefined ? showNowInfos.period : 30; + const periodInMilliseconds = period * 1000; + + const timer = setTimeout(() => { + dispatch( + getMainLiveShowNowProduct({ + patnrId: panelInfo.patnrId + ? panelInfo.patnrId + : playListInfo[selectedIndex].patnrId, + showId: panelInfo.showId + ? panelInfo.showId + : playListInfo[selectedIndex].showId, + lstChgDt: showNowInfos.lstChgDt, + }) + ); + }, periodInMilliseconds); + + return () => { + clearTimeout(timer); + }; + } + }, [showNowInfos, panelInfo]); + + const videoItemFocused = useCallback(() => { + // 아이템클릭 진입시 포커스 + let hasProperSpot = false; + let targetId; + if (!isInitialFocusOccurred) { + targetId = panelInfo.targetId; + + initialFocusTimeoutJob.current.start(() => { + const initialFocusTarget = findSelector( + `[data-spotlight-id="${targetId}"]` + ); + + if (initialFocusTarget) { + hasProperSpot = true; + Spotlight.focus(initialFocusTarget); + setIsInitialFocusOccurred(true); + + return; + } + }); + } + }, [ + isInitialFocusOccurred, + panelInfo.targetId, + panelInfo.modal, + videoVerticalVisible, + ]); + + const videoInitialFocused = useCallback(() => { + if (panelInfo.isUpdatedByClick || !isOnTop) { + return; + } + setContainerLastFocusedElement(null, ["playVideoShopNowBox"]); + + // 세로형모드 포커스 + if (videoVerticalVisible) { + Spotlight.focus("tab-0"); + return; + } + + // 화살표버튼 포커스 + const current = Spotlight.getCurrent(); + let hasProperSpot = false; + if (current) { + const spotId = current.getAttribute("data-spotlight-id"); + if (spotId && spotId.indexOf("tabChannel-video") >= 0) { + hasProperSpot = true; + } + } + + if (!panelInfo.modal && !videoVerticalVisible && !hasProperSpot) { + Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON); + return; + } + //비디오 진입시 포커스 + if (panelInfo.isIndicatorByClick && shopNowInfo?.length > 0) { + Spotlight.focus("playVideoShopNowBox"); + return; + } + }, [ + shopNowInfo, + videoVerticalVisible, + isOnTop, + panelInfo.modal, + panelInfo.isUpdatedByClick, + panelInfo.isIndicatorByClick, + + panelInfo.shptmBanrTpNm, + ]); + + const cannotPlay = useMemo(() => { + const topPanel = panels[panels.length - 1]; + return !isOnTop && topPanel?.name === panel_names.PLAYER_PANEL; + }, [panels, isOnTop]); + + const getPlayer = useCallback((ref) => { + videoPlayer.current = ref; + }, []); + + /** for VOD */ + const addPanelInfoToPlayList = useCallback( + (featuredShowsInfos) => { + if (Array.isArray(featuredShowsInfos)) { + const showId = showDetailInfo[0]?.showId; + + const index = featuredShowsInfos.findIndex( + (show) => show.showId === showId + ); + + let newArray = []; + if (index > -1) { + // 인덱스를 찾은 경우 그대로 + newArray = [...featuredShowsInfos]; + setBackupInitialIndex(index); + setSelectedIndex(index); + } else { + // 인덱스를 찾지 못한 경우 showDetailInfo 를 제일 앞에 배치 + newArray = [{ ...showDetailInfo[0] }, ...featuredShowsInfos]; + setBackupInitialIndex(0); + setSelectedIndex(0); + } + setPlayListInfo(newArray); + } + }, + + [showDetailInfo] + ); + + useEffect(() => { + if (panelInfo.shptmBanrTpNm === "LIVE") { + if (panelInfo.patnrId && panelInfo.showId) { + dispatch( + getMainCategoryShowDetail({ + patnrId: panelInfo.patnrId, + showId: panelInfo.showId, + curationId: panelInfo.curationId, + }) + ); + } + dispatch(getMainLiveShow({ vodIncFlag: "Y" })); + } + }, [ + dispatch, + panelInfo?.curationId, + panelInfo?.lgCatCd, + panelInfo?.patnrId, + panelInfo?.showId, + panelInfo?.shptmBanrTpNm, + ]); + + useEffect(() => { + if ( + panelInfo.shptmBanrTpNm === "VOD" && + showDetailInfo && + showDetailInfo.length > 0 && + showDetailInfo[0]?.showCatCd && + fullVideolgCatCd !== showDetailInfo[0]?.showCatCd //기존에 호출했으면 안한다. + ) { + dispatch( + getHomeFullVideoInfo({ + lgCatCd: showDetailInfo[0].showCatCd, + }) + ); + } + if ( + panelInfo.shptmBanrTpNm === "VOD" && + showDetailInfo && + showDetailInfo[0] && + showDetailInfo[0].showId && + showDetailInfo[0].patnrId + ) { + if (!featuredShowsInfos || Object.keys(featuredShowsInfos).length === 0) { + setPlayListInfo(showDetailInfo); + } + setShopNowInfo(showDetailInfo[0].productInfos); + saveToLocalSettings(showDetailInfo[0].showId, showDetailInfo[0].patnrId); + } + }, [showDetailInfo]); + + //LIVE + useEffect(() => { + if ( + playListInfo && + playListInfo.length > 0 && + panelInfo.shptmBanrTpNm === "LIVE" + ) { + if (playListInfo[selectedIndex]?.patnrId) { + dispatch( + getMainLiveShowNowProduct({ + patnrId: playListInfo[selectedIndex]?.patnrId, + showId: playListInfo[selectedIndex]?.showId, + }) + ); + } + + if (playListInfo[selectedIndex]?.catCd) { + dispatch( + getHomeFullVideoInfo({ + lgCatCd: playListInfo[selectedIndex]?.catCd, + }) + ); + } + } + }, [selectedIndex]); + + useEffect(() => { + if (showDetailInfo && showDetailInfo.length > 0) { + dispatch(CLEAR_PLAYER_INFO()); + + if ( + showDetailInfo[0]?.liveFlag === "N" && + showDetailInfo[0]?.chatLogFlag === "Y" && + panelInfo.shptmBanrTpNm === "VOD" + ) { + dispatch( + getChatLog({ patnrId: panelInfo.patnrId, showId: panelInfo.showId }) + ); + } + } + }, [showDetailInfo]); + + // videoClick focused + useEffect(() => { + if (playListInfo && playListInfo.length > 0) { + if (panelInfo.targetId) { + videoItemFocused(); + } else { + videoInitialFocused(); + } + } + }, [playListInfo]); + + //10초 후 닫힐때 TabButton 포커스 + useEffect(() => { + if (playListInfo && playListInfo.length > 0) { + videoInitialFocused(); + } + }, [sideContentsVisible, panelInfo.modal]); + + // liveChannel initial selectedIndex + useEffect(() => { + if (panelInfo?.shptmBanrTpNm === "LIVE" && playListInfo?.length > 0) { + const index = playListInfo.findIndex( + (item) => item.chanId === panelInfo.chanId + ); + if (index !== -1 && !isUpdate) { + setBackupInitialIndex(index); + setSelectedIndex(index); + setIsUpdate(true); + } + } + }, [panelInfo?.shptmBanrTpNm, playListInfo]); + + // live subtitle Luna API + useEffect(() => { + if (currentSubtitleBlob) { + return; + } else if (isYoutube) { + if (mediaId) { + dispatch(requestLiveSubtitle({ mediaId, enable: false })); + setMediaId(null); + } + return; + //do caption action on VideoPlayer(componentDidUpdate) + } else { + if (mediaId && captionEnable && isSubtitleActive && !panelInfo?.modal) { + dispatch(requestLiveSubtitle({ mediaId, enable: true })); + } else { + if (mediaId) { + dispatch(requestLiveSubtitle({ mediaId, enable: false })); + } + } + } + }, [ + mediaId, + isYoutube, + captionEnable, + isSubtitleActive, + currentSubtitleBlob, + panelInfo.modal, + panelInfo.shptmBanrTpNm, + ]); + + // get PlayListInfo + useEffect(() => { + if (panelInfo?.shptmBanrTpNm === "VOD") { + if (showDetailInfo && showDetailInfo.length > 0) { + if (featuredShowsInfos && featuredShowsInfos.length > 0) { + addPanelInfoToPlayList(featuredShowsInfos); + } + } + } + }, [featuredShowsInfos]); + + // get PlayListInfo + useEffect(() => { + if (!panelInfo) return; + + switch (panelInfo.shptmBanrTpNm) { + case "LIVE": { + const playlist = liveShowInfos ?? liveChannelInfos; + + if (!Array.isArray(playlist)) return; + + const modifiedList = []; + + playlist.forEach((item) => { + if (item.showType === "vod" && Array.isArray(item.vodInfos)) { + const mergedVodInfos = item.vodInfos.map((vod) => ({ + ...vod, + patnrId: item.patnrId, + patncNm: item.patncNm, + patncLogoPath: item.patncLogoPath, + showType: "vod", + })); + + modifiedList.push(...mergedVodInfos); + } else { + modifiedList.push(item); + } + }); + + setPlayListInfo(modifiedList); + + if (showNowInfos?.prdtChgYn === "N") { + return; + } + + if (showNowInfos || showDetailInfo?.length > 0) { + const productInfos = showNowInfos + ? showNowInfos.productInfos + : showDetailInfo[0]?.productInfos; + setShopNowInfo(productInfos); + } + break; + } + case "MEDIA": + setPlayListInfo([panelInfo]); + break; + default: + break; + } + }, [ + panelInfo, + showDetailInfo, + featuredShowsInfos, + liveChannelInfos, + liveShowInfos, + showNowInfos, + dispatch, + ]); + + const liveTotalTime = useMemo(() => { + let liveTotalTime; + if (liveShowInfos && panelInfo?.shptmBanrTpNm === "LIVE") { + const startDtMoment = new Date(liveShowInfos[selectedIndex]?.strtDt); + const endDtMoment = new Date(liveShowInfos[selectedIndex]?.endDt); + + liveTotalTime = Math.floor((endDtMoment - startDtMoment) / 1000); + + return liveTotalTime; + } + }, [liveShowInfos, selectedIndex, panelInfo.shptmBanrTpNm]); + + useEffect(() => { + const handleVisibilityChange = () => { + if ( + document.visibilityState === "visible" && + liveShowInfos && + panelInfo?.shptmBanrTpNm === "LIVE" + ) { + const localStartDt = convertUtcToLocal( + liveShowInfos[selectedIndex]?.strtDt + ); + + const curDt = new Date(); + const localStartSec = localStartDt?.getTime() / 1000; + const curSec = curDt?.getTime() / 1000; + + const calculatedLiveTime = curSec - localStartSec; + if (calculatedLiveTime >= liveTotalTime) { + setCurrentLiveTimeSeconds(0); + } else { + setCurrentLiveTimeSeconds(calculatedLiveTime); + } + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + if (panelInfo.offsetHour) { + setCurrentLiveTimeSeconds(parseInt(panelInfo.offsetHour)); + } else if (liveShowInfos && panelInfo?.shptmBanrTpNm === "LIVE") { + const localStartDt = convertUtcToLocal( + liveShowInfos[selectedIndex]?.strtDt + ); + + const curDt = new Date(); + const localStartSec = localStartDt?.getTime() / 1000; + const curSec = curDt?.getTime() / 1000; + + const calculatedLiveTime = curSec - localStartSec; + if (calculatedLiveTime >= liveTotalTime) { + setCurrentLiveTimeSeconds(0); + } else { + setCurrentLiveTimeSeconds(calculatedLiveTime); + } + } else { + setCurrentLiveTimeSeconds(0); + } + + return () => { + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [ + liveShowInfos, + selectedIndex, + panelInfo.offsetHour, + panelInfo.shptmBanrTpNm, + playListInfo, + liveTotalTime, + ]); + + useEffect(() => { + if (panelInfo.shptmBanrTpNm == "LIVE" && liveTotalTime > 0) { + const interval = setInterval(() => { + setCurrentLiveTimeSeconds((prev) => { + if (prev >= liveTotalTime) { + return 1; + } + return prev + 1; + }); + }, 1000); + + return () => clearInterval(interval); + } + }, [liveTotalTime]); + + useEffect(() => { + if (currentLiveTimeSeconds > liveTotalTime) { + setTimeout(() => { + dispatch(getMainLiveShow()); + setShopNowInfo(""); + dispatch( + getHomeFullVideoInfo({ + lgCatCd: playListInfo[selectedIndex].showCatCd, + }) + ); + }, 3000); + } + }, [currentLiveTimeSeconds, liveTotalTime]); + + const mediainfoHandler = useCallback( + (ev) => { + const type = ev.type; + if (type !== "timeupdate" && type !== "durationchange") { + console.log( + "mediainfoHandler....", + type, + ev, + videoPlayer.current?.getMediaState() + ); + } + if ( + ev === "hlsError" && + isNaN(Number(videoPlayer.current?.getMediaState().playbackRate)) + ) { + dispatch( + sendBroadCast({ + type: "videoError", + moreInfo: { reason: "hlsError" }, + }) + ); + + return; + } + + switch (type) { + case "timeupdate": { + setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime); + break; + } + case "error": { + dispatch( + sendBroadCast({ + type: "videoError", + moreInfo: { reason: videoPlayer.current?.getMediaState().error }, + }) + ); + break; + } + case "loadeddata": { + const mediaId = videoPlayer.current?.video?.media?.mediaId; + setMediaId(mediaId); + setVideoLoaded(true); + } + } + }, + [currentLiveTimeSeconds, liveTotalTime] + ); + + useEffect(() => { + // case: video error when the video is in fullscreen mode + if ( + broadcast?.type === "videoError" && + isOnTop && + !panelInfo?.modal && + panelInfo?.modalContainerId + ) { + // case: Featured Brands + if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) { + dispatch(PanelActions.popPanel()); + } + } + }, [ + broadcast?.type, + isOnTop, + panelInfo?.modal, + panelInfo?.modalContainerId, + panelInfo?.sourcePanel, + ]); + + useEffect(() => { + if ( + panelInfo.modal && + panelInfo.modalContainerId && + (lastPanelAction === "previewPush" || lastPanelAction === "previewUpdate") + ) { + const node = document.querySelector( + `[data-spotlight-id="${panelInfo.modalContainerId}"]` + ); + if (node) { + const { width, height, top, left } = node.getBoundingClientRect(); + const modalStyle = { + width: width + "px", + height: height + "px", + top: top + "px", + left: left + "px", + position: "fixed", + overflow: "visible", + }; + setModalStyle(modalStyle); + let scale = 1; + if (typeof window === "object") { + scale = width / window.innerWidth; + setModalScale(scale); + } + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { modalStyle: modalStyle, modalScale: scale }, + }) + ); + } else { + setModalStyle(panelInfo.modalStyle); + setModalScale(panelInfo.modalScale); + console.error( + "PlayerPanel modalContainerId node not found", + panelInfo.modalContainerId + ); + } + } else if (isOnTop && !panelInfo.modal && videoPlayer.current) { + if (videoPlayer.current?.getMediaState()?.paused) { + videoPlayer.current.play(); + } + + if ( + videoPlayer.current.areControlsVisible && + !videoPlayer.current.areControlsVisible() + ) { + videoPlayer.current.showControls(); + } + } + }, [panelInfo, isOnTop]); + + const smallestOffsetHourIndex = useMemo(() => { + if (shopNowInfo) { + const filteredVideos = shopNowInfo.filter( + (video) => video.offsetHour >= currentTime + ); + const newSmallestOffsetHour = Math.min( + ...filteredVideos.map((video) => video.offsetHour) + ); + + const newSmallestOffsetHourIndex = shopNowInfo.findIndex( + (video) => video.offsetHour === newSmallestOffsetHour.toString() + ); + + if (shopNowInfo.length === 1) { + return 0; + } + if (newSmallestOffsetHourIndex >= 1) { + return newSmallestOffsetHourIndex - 1; + } + return newSmallestOffsetHourIndex; + } + }, [shopNowInfo, currentTime]); + + const currentSubtitleUrl = useMemo(() => { + if (panelInfo?.shptmBanrTpNm === "MEDIA") { + return panelInfo.subtitle; + } + return playListInfo && playListInfo[selectedIndex]?.showSubtitlUrl; + }, [playListInfo, selectedIndex, panelInfo]); + + const currentPlayingUrl = useMemo(() => { + // return "https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4"; + + if (broadcast.type === "videoError") { + return null; + } + + // For previews, always use the direct URL from panelInfo. + if (panelInfo.modal) { + return panelInfo.showUrl; + } + + // For fullscreen, the playlist is the primary source. + if (playListInfo && playListInfo.length > 0 && playListInfo[selectedIndex]?.showUrl) { + return playListInfo[selectedIndex]?.showUrl; + } + + // Fallback for fullscreen if playlist isn't ready, use panelInfo URL. + if (!panelInfo.modal && panelInfo.showUrl) { + return panelInfo.showUrl; + } + + return playListInfo && playListInfo[selectedIndex]?.showUrl; + }, [panelInfo, playListInfo, selectedIndex, broadcast]); + + const isYoutube = useMemo(() => { + if (currentPlayingUrl && currentPlayingUrl.includes("youtu")) { + return true; + } else { + return false; + } + }, [currentPlayingUrl]); + + const currentSubtitleBlob = useMemo(() => { + if (Config.DEBUG_VIDEO_SUBTITLE_TEST) { + return dummyVtt; + } + return vodSubtitleData[currentSubtitleUrl]; + }, [vodSubtitleData, currentSubtitleUrl]); + + const isReadyToPlay = useMemo(() => { + if (!currentPlayingUrl) { + return false; + } + if ( + !Config.DEBUG_VIDEO_SUBTITLE_TEST && + currentSubtitleUrl && + !currentSubtitleBlob + ) { + return false; + } + return true; + }, [currentPlayingUrl, currentSubtitleUrl, currentSubtitleBlob, broadcast]); + + const chatVisible = useMemo(() => { + if ( + playListInfo && + chatData && + !panelInfo.modal && + isOnTop && + panelInfo?.shptmBanrTpNm !== "MEDIA" + ) { + return true; + } + return false; + }, [playListInfo, chatData, panelInfo.modal, isOnTop]); + + useEffect(() => { + if (currentSubtitleUrl) { + dispatch(getSubTitle({ showSubtitleUrl: currentSubtitleUrl })); + } + }, [currentSubtitleUrl]); + + useEffect(() => { + setVideoLoaded(false); + }, [currentPlayingUrl]); + + const handlePopupClose = useCallback(() => { + dispatch(setHidePopup()); + setTimeout(() => Spotlight.focus(SpotlightIds.PLAYER_SUBTITLE_BUTTON)); + }, [dispatch]); + const reactPlayerSubtitleConfig = useMemo(() => { + if (isSubtitleActive && currentSubtitleBlob) { + return { + file: { + attributes: { + crossOrigin: "true", + }, + tracks: [ + { kind: "subtitles", src: currentSubtitleBlob, default: true }, + ], + }, + youtube: YOUTUBECONFIG, + }; + } else { + return { + youtube: YOUTUBECONFIG, + }; + } + }, [currentSubtitleBlob, isSubtitleActive]); + + const currentSideButtonStatus = useMemo(() => { + if ( + panelInfo?.shptmBanrTpNm !== "MEDIA" && + !panelInfo?.modal && + sideContentsVisible + ) { + return true; + } + return false; + }, [panelInfo, sideContentsVisible]); + + const videoType = useMemo(() => { + if (currentPlayingUrl) { + if (currentPlayingUrl.toLowerCase().endsWith(".mp4")) { + return "video/mp4"; + } else if (currentPlayingUrl.toLowerCase().endsWith(".mpd")) { + return "application/dash+xml"; + } else if (currentPlayingUrl.toLowerCase().endsWith(".m3u8")) { + return "application/mpegurl"; + } + } + return "application/mpegurl"; + }, [currentPlayingUrl]); + + const orderPhoneNumber = useMemo(() => { + if (panelInfo?.shptmBanrTpNm !== "MEDIA" && showDetailInfo) { + return showDetailInfo[0]?.orderPhnNo; + } else { + return playListInfo[selectedIndex]?.orderPhnNo; + } + }, [panelInfo?.shptmBanrTpNm, showDetailInfo, playListInfo, selectedIndex]); + + const videoThumbnailUrl = useMemo(() => { + let res = null; + + if (panelInfo.shptmBanrTpNm === "MEDIA") { + res = panelInfo?.thumbnailUrl; + } else if (playListInfo && playListInfo.length > 0) { + res = playListInfo[selectedIndex]?.thumbnailUrl; + } else if (!res) { + res = showDetailInfo[0]?.thumbnailUrl; + } + + return res; + }, [ + showDetailInfo, + playListInfo, + selectedIndex, + panelInfo.thumbnailUrl, + panelInfo.shptmBanrTpNm, + ]); + + const saveToLocalSettings = useCallback( + (showId, patnrId) => { + let recentItems = []; + if (localRecentItems) { + recentItems = [...localRecentItems]; + } + + const currentDate = new Date(); + + const formattedDate = `${ + currentDate.getMonth() + 1 + }/${currentDate.getDate()}`; + + const existingProductIndex = recentItems.findIndex((item) => { + if (item.showId) return item.showId === showId; + }); + + if (existingProductIndex !== -1) { + recentItems.splice(existingProductIndex, 1); + } + + recentItems.push({ + patnrId: patnrId, + showId: showId, + date: formattedDate, + expireTime: currentDate.getTime() + 1000 * 60 * 60 * 24 * 14, + cntryCd: httpHeader["X-Device-Country"], + }); + + if (recentItems.length >= 51) { + const data = [...recentItems]; + dispatch(changeLocalSettings({ recentItems: data.slice(1) })); + } else { + dispatch(changeLocalSettings({ recentItems })); + } + }, + [httpHeader, localRecentItems, dispatch] + ); + + const handleIndicatorDownClick = useCallback(() => { + if (!initialEnter) { + setInitialEnter(true); + } + + let newIndex = + selectedIndex === playListInfo.length - 1 ? 0 : selectedIndex + 1; + let initialIndex = newIndex; + let attempts = 0; + + while (!playListInfo[newIndex]?.showId && attempts < playListInfo.length) { + newIndex = newIndex === playListInfo.length - 1 ? 0 : newIndex + 1; + attempts++; + if (newIndex === initialIndex) break; + } + if (playListInfo[newIndex]?.showId) { + setSelectedIndex(newIndex); + if (panelInfo.shptmBanrTpNm === "VOD") { + dispatch( + getMainCategoryShowDetail({ + patnrId: playListInfo[newIndex]?.patnrId, + showId: playListInfo[newIndex]?.showId, + curationId: playListInfo[newIndex]?.curationId, + }) + ); + Spotlight.focus("playVideoShopNowBox"); + } else { + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + chanId: playListInfo[newIndex].chanId, + patnrId: playListInfo[newIndex].patnrId, + showId: playListInfo[newIndex].showId, + shptmBanrTpNm: panelInfo?.shptmBanrTpNm, + isIndicatorByClick: true, + }, + }) + ); + } + } + if (!sideContentsVisible) { + setPrevChannelIndex(selectedIndex); + } + setSideContentsVisible(true); + }, [ + dispatch, + playListInfo, + selectedIndex, + sideContentsVisible, + initialEnter, + ]); + + const handleIndicatorUpClick = useCallback(() => { + if (!initialEnter) { + setInitialEnter(true); + } + + let newIndex = + selectedIndex === 0 ? playListInfo.length - 1 : selectedIndex - 1; + let initialIndex = newIndex; + let attempts = 0; + + while (!playListInfo[newIndex]?.showId && attempts < playListInfo.length) { + newIndex = newIndex === 0 ? playListInfo.length - 1 : newIndex - 1; + attempts++; + if (newIndex === initialIndex) break; + } + + if (playListInfo[newIndex]?.showId) { + setSelectedIndex(newIndex); + if (panelInfo.shptmBanrTpNm === "VOD") { + dispatch( + getMainCategoryShowDetail({ + patnrId: playListInfo[newIndex]?.patnrId, + showId: playListInfo[newIndex]?.showId, + curationId: playListInfo[newIndex]?.curationId, + }) + ); + Spotlight.focus("playVideoShopNowBox"); + } else { + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + chanId: playListInfo[newIndex].chanId, + patnrId: playListInfo[newIndex].patnrId, + showId: playListInfo[newIndex].showId, + shptmBanrTpNm: panelInfo?.shptmBanrTpNm, + isIndicatorByClick: true, + }, + }) + ); + } + } + if (!sideContentsVisible) { + setPrevChannelIndex(selectedIndex); + } + setSideContentsVisible(true); + }, [ + dispatch, + playListInfo, + selectedIndex, + sideContentsVisible, + initialEnter, + ]); + + useEffect(() => { + if ( + panelInfo.shptmBanrTpNm === "VOD" && + panelInfo.patnrId && + panelInfo.showId + ) { + //VOD의 panelInfo.showId 가 변경된 최초 한번만 호출하고, FearchedShow 항목에서 선택시 또는 상하 indicator 선택시 호출한다. + dispatch( + getMainCategoryShowDetail({ + patnrId: panelInfo.patnrId, + showId: panelInfo.showId, + curationId: panelInfo.curationId, + }) + ); + } + }, [panelInfo.showId]); + + useEffect(() => { + return () => { + dispatch(clearShopNowInfo()); + dispatch(CLEAR_PLAYER_INFO()); + setShopNowInfo([]); + }; + }, []); + + const onEnded = useCallback((e) => { + if (panelInfoRef.current.shptmBanrTpNm === "MEDIA") { + dispatch( + updatePanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + launchedFromPlayer: true, + isPlayerFinished: true, + }, + }) + ); + Spotlight.pause(); + setTimeout(() => { + Spotlight.resume(); + dispatch(PanelActions.popPanel()); + }, VIDEO_END_ACTION_DELAY); + return; + } + if (panelInfoRef.current.shptmBanrTpNm === "VOD") { + Spotlight.pause(); + setTimeout(() => { + Spotlight.resume(); + if (panelInfoRef.current.modal) { + videoPlayer.current.play(); + } else { + dispatch(PanelActions.popPanel(panel_names.PLAYER_PANEL)); + } + }, VIDEO_END_ACTION_DELAY); + e?.stopPropagation(); + e?.preventDefault(); + return; + } + }, []); + + const onKeyDown = (ev) => { + if (ev.keyCode === 34) { + handleIndicatorDownClick(); + ev.stopPropagation(); + ev.preventDefault(); + } else if (ev.keyCode === 33) { + handleIndicatorUpClick(); + ev.stopPropagation(); + ev.preventDefault(); + } + }; + + const [initialEnter, setInitialEnter] = USE_STATE("initialEnter", true); + const timerId = useRef(null); + const showSideContents = useMemo(() => { + return ( + sideContentsVisible && + playListInfo && + panelInfo?.shptmBanrTpNm !== "MEDIA" && + !panelInfo?.modal && + isOnTop + ); + }, [sideContentsVisible, playListInfo, panelInfo, isOnTop]); + + const qrCurrentItem = useMemo(() => { + if (shopNowInfo?.length && panelInfo?.shptmBanrTpNm === "LIVE") { + return shopNowInfo[shopNowInfo.length - 1]; + } + + if ( + shopNowInfo?.length && + smallestOffsetHourIndex >= 0 && + panelInfo?.shptmBanrTpNm !== "LIVE" + ) { + return shopNowInfo[smallestOffsetHourIndex]; + } + + if (panelInfo?.shptmBanrTpNm === "MEDIA" && panelInfo?.qrCurrentItem) { + return panelInfo.qrCurrentItem; + } + + return null; + }, [ + shopNowInfo, + smallestOffsetHourIndex, + panelInfo?.shptmBanrTpNm, + panelInfo?.qrCurrentItem, + ]); + + const isShowType = useMemo(() => { + if (["VOD", "MEDIA"].includes(panelInfo.shptmBanrTpNm)) { + return panelInfo.shptmBanrTpNm; + } + + const showType = playListInfo?.[selectedIndex]?.showType; + if (showType === "live") return panelInfo.shptmBanrTpNm; + if (showType === "vod") return "VOD"; + + return panelInfo.shptmBanrTpNm; + }, [panelInfo.shptmBanrTpNm, playListInfo, selectedIndex]); + const clearTimer = useCallback(() => { + clearTimeout(timerId.current); + timerId.current = null; + }, []); + + const resetTimer = useCallback((timeout) => { + if (timerId.current) { + clearTimer(); + } + + if (initialEnter) { + setInitialEnter(false); + } + + timerId.current = setTimeout(() => { + setSideContentsVisible(false); + }, timeout); + }, []); + + useEffect(() => { + if (isOnTop && !panelInfo.modal && !videoVerticalVisible) { + setSideContentsVisible(true); + } + }, [panelInfo.modal]); + + useEffect(() => { + const node = document.querySelector( + `[data-spotlight-id=${TAB_CONTAINER_SPOTLIGHT_ID}]` + ); + + if (!showSideContents || !node || videoVerticalVisible) return; + + // NOTE 첫 진입 시에는 10초 후 탭이 닫히도록 설정 + if (initialEnter) { + resetTimer(INITIAL_TIMEOUT); + } + + const handleEvent = () => resetTimer(REGULAR_TIMEOUT); + TARGET_EVENTS.forEach((event) => node.addEventListener(event, handleEvent)); + + return () => { + TARGET_EVENTS.forEach((event) => + node.removeEventListener(event, handleEvent) + ); + + if (timerId.current) { + clearTimer(); + } + }; + }, [showSideContents, videoVerticalVisible]); + + useEffect(() => { + if (initialEnter || !sideContentsVisible || videoVerticalVisible) return; + + // NOTE button을 통해 탭을 연 경우 5초 후 탭이 닫히도록 설정 + if (sideContentsVisible) { + resetTimer(REGULAR_TIMEOUT); + } + + return () => { + if (timerId.current) { + clearTimer(); + } + }; + }, [sideContentsVisible]); + + useLayoutEffect(() => { + const videoContainer = document.querySelector(`.${css.videoContainer}`); + + if (panelInfo.thumbnail && !videoVerticalVisible) { + videoContainer.style.background = `url(${panelInfo.thumbnail}) center center / contain no-repeat`; + videoContainer.style.backgroundColor = "black"; + } + + if (broadcast.type === "videoError" && videoThumbnailUrl) { + videoContainer.style.background = `url(${videoThumbnailUrl}) center center / contain no-repeat`; + videoContainer.style.backgroundColor = "black"; + } + }, [panelInfo.thumbnail, broadcast]); + + const isPlayer = useMemo(() => { + if (!panelInfo?.modal) { + return "full player"; + } + + switch (panels[0].name) { + case "categorypanel": + return "category"; + case "mypagepanel": + return "my page"; + case "searchpanel": + return "search"; + case "hotpickpanel": + return "hot picks"; + case "featuredbrandspanel": + return "featured brands"; + case "trendingnowpanel": + return "trending now"; + case "playerpanel": + return "home"; + } + }, [panelInfo.modal, panels]); + + const createLogParams = useCallback( + (visible) => { + if (videoLoaded && isShowType) { + if (showDetailInfo?.[0]) { + return { + visible, + showType: isShowType, + player: isPlayer, + category: showDetailInfo[0].showCatNm, + contentId: showDetailInfo[0].showId, + contentTitle: showDetailInfo[0].showNm, + partner: showDetailInfo[0].patncNm, + contextName: Config.LOG_CONTEXT_NAME.SHOW, + messageId: Config.LOG_MESSAGE_ID.SHOWVIEW, + }; + } else if (playListInfo?.[selectedIndex]) { + const currentItem = playListInfo[selectedIndex]; + return { + visible, + showType: isShowType, + player: isPlayer, + category: currentItem.catNm, + contentId: currentItem.showId, + contentTitle: currentItem.showNm, + partner: currentItem.patncNm, + contextName: Config.LOG_CONTEXT_NAME.SHOW, + messageId: Config.LOG_MESSAGE_ID.SHOWVIEW, + }; + } + } + return null; + }, + [ + isShowType, + videoLoaded, + showDetailInfo?.[0]?.showId, + playListInfo?.[selectedIndex]?.showId, + ] + ); + + // isVODPaused 상태 변경 시에만 로그를 보냄 + useEffect(() => { + if (showDetailInfo?.[0]) { + const params = createLogParams(!isVODPaused); + if (params) { + dispatch(sendLogTotalRecommend(params)); + } + } else if (playListInfo?.[selectedIndex]) { + const params = createLogParams(true); + if (params) { + dispatch(sendLogTotalRecommend(params)); + } + } + }, [isVODPaused, createLogParams]); + + // 컴포넌트 언마운트 시에만 로그를 보냄 + useEffect(() => { + return () => { + const params = createLogParams(false); + if (params) { + dispatch(sendLogTotalRecommend(params)); + } + }; + }, [createLogParams, dispatch]); + + return ( + + + {isReadyToPlay && ( + + {typeof window === "object" && window.PalmSystem && ( + + )} + {isSubtitleActive && + !panelInfo.modal && + captionEnable && + typeof window === "object" && + window.PalmSystem && + currentSubtitleBlob && ( + + )} + + )} + + {chatVisible && ( + + )} + + {currentSideButtonStatus && !videoVerticalVisible && playListInfo && ( + + )} + + {showSideContents && ( + + )} + + + {activePopup === ACTIVE_POPUP.alertPopup && ( + + )} + + ); + }; + + const propsAreEqual = (prev, next) => { + const keys = Object.keys(prev); + const nextKeys = Object.keys(next); + // if (!next.isOnTop) { + // //ignore event on background + // return true; + // } + if (keys.length !== nextKeys.length) { + return false; + } + for (let i = 0; i < keys.length; i++) { + if (prev[keys[i]] !== next[keys[i]]) { + if (JSON.stringify(prev[keys[i]]) === JSON.stringify(next[keys[i]])) { + continue; + } + return false; + } + } + return true; + }; + export default React.memo(PlayerPanelNew, propsAreEqual); + \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.module.less new file mode 100644 index 00000000..39fd00b4 --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.new.module.less @@ -0,0 +1,79 @@ +@import "../../style/CommonStyle.module.less"; +@import "../../style/utils.module.less"; + +@videoBackgroundColor: black; +.videoContainer { + position: absolute; + background-color: @videoBackgroundColor; + z-index: 21; + + .playButton { + .size(@w: 60px, @h: 60px); + background-image: url("../../../assets/images/btn/btn-video-play-nor@3x.png"); + background-size: cover; + + &:focus { + .size(@w: 60px, @h: 60px); + border-radius: 50%; + background-color: #c70850; + opacity: 0.5; + } + } + + .pauseButton { + .size(@w: 60px, @h: 60px); + background-image: url("../../../assets/images/btn/btn-voc-pause-nor@3x.png"); + background-size: cover; + + &:focus { + .size(@w: 60px, @h: 60px); + border-radius: 50%; + background-color: #c70850; + opacity: 0.5; + } + } + .toOpenBtn { + .size(@w: 147px, @h: 243px); + min-width: 60px !important; + margin: 192px 0 136px 512px; + text-align: center; + background-image: url(../../../assets/images/btn/btn-toopen-foc.svg); + } + + .videoReduce { + .size(@w:78px, @h:78px); + background: url("../../../assets/images/btn/btn-video-min-nor@3x.png") + no-repeat center center/60px 60px; + position: absolute; + right: 60px; + bottom: 150px; + z-index: 3; + + &:focus { + .size(@w:78px, @h:78px); + background-color: rgba(199, 8, 80, 0.5); + border-radius: 50%; + } + } + &.modal, + &.background { + width: 1px; + height: 1px; + left: -1px; + top: -1px; + pointer-events: none; + z-index: 1; + background-color: @videoBackgroundColor; + overflow: visible; + .tabContainer, + .arrow, + .toOpenBtn { + display: none; + } + } + &.hideSubtitle { + video::cue { + visibility: hidden; + } + } +}