// src: views/IntroPanel/IntroPanel.new.jsx 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, setShowPopup, // setTermsAgreeYn, } from "../../actions/commonActions"; import { registerDevice } from "../../actions/deviceActions"; import { getWelcomeEventInfo } from "../../actions/eventActions"; // import { fetchCurrentUserHomeTermsPromise } from "../../actions/homeActions"; import { sendLogGNB, sendLogTerms, sendLogTotalRecommend, } from "../../actions/logActions"; import { popPanel, pushPanel } from "../../actions/panelActions"; import TButton, { TYPES } from "../../components/TButton/TButton"; // import TButtonScroller from "../../components/TButtonScroller/TButtonScroller"; // import TButtonTab from "../../components/TButtonTab/TButtonTab"; import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare"; import TPanel from "../../components/TPanel/TPanel"; import TPopUp from "../../components/TPopUp/TPopUp"; import TNewPopUp from "../../components/TPopUp/TNewPopUp"; // 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 } 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", ); export default function IntroPanel({ // children, // isTabActivated, // handleCancel, spotlightId, ...rest }) { delete rest.isTabActivated; delete rest.panelInfo; useDebugKey({}); const dispatch = useDispatch(); // const blurTimeout = useRef(null); const termsData = useSelector((state) => state.home.termsData); const termsIdMap = useSelector((state) => state.home.termsIdMap); const { popupVisible, activePopup, ...popupState } = useSelector( (state) => state.common.popup, ); // const eventInfos = useSelector((state) => state.event.eventData); const regDeviceData = useSelector((state) => state.device.regDeviceData); // const regDeviceInfoData = useSelector( // (state) => state.device.regDeviceInfoData // ); // registerDevice API 호출 중 여부 const [isProcessing, setIsProcessing] = useState(false); const [showExitMessagePopup, setShowExitMessagePopup] = useState(false); // race condition 방지를 위한 안전장치 const processingTimeoutRef = useRef(null); // 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", ) || [] ); }, [termsData]); const optionalTermsData = useMemo(() => { return ( termsData?.data?.terms.filter( (item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406", ) || [] ); }, [termsData]); const webOSVersion = useSelector( (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 shouldShowText = textVersions.includes(version); 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); useEffect(() => { dispatch({ type: types.REGISTER_DEVICE_RESET }); dispatch(sendLogGNB(Config.LOG_MENU.TERMS_CONDITIONS)); }, [dispatch]); // 컴포넌트 마운트 시 현재 Redux 상태 로깅 // useEffect(() => { // console.log('🔍 IntroPanel 마운트 시 Redux 상태:'); // console.log(' - regDeviceData:', regDeviceData); // console.log(' - regDeviceInfoData:', regDeviceInfoData); // console.log(' - eventInfos:', eventInfos); // console.log(' - termsData:', termsData); // }, []); // 디버깅용 WebOS 버전 로그 useEffect(() => { if (process.env.NODE_ENV === "development") { console.log("🔍 IntroPanel WebOS 버전 정보:"); console.log(" - webOSVersion:", webOSVersion); console.log(" - shouldShowBenefitsView:", shouldShowBenefitsView); } }, [webOSVersion, shouldShowBenefitsView]); useEffect(() => { if (showExitMessagePopup) { const timer = setTimeout(() => { dispatch(setExitApp()); dispatch( sendLogTotalRecommend({ contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, }), ); }, 3000); return () => clearTimeout(timer); } }, [showExitMessagePopup, dispatch]); // 컴포넌트 마운트 후 0.5초 뒤에 selectAllCheckbox으로 강제 포커스 useEffect(() => { const focusTimer = setTimeout(() => { focusById("selectAllCheckbox"); }, 500); return () => { clearTimeout(focusTimer); }; }, []); // [추가] 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) { return; } const termType = currentTerms.trmsTpCd; if (termType === "MST00402") { updateStateAsync({ termsChecked: true }); } else if (termType === "MST00401") { updateStateAsync({ privacyChecked: true }); } else if (termType === "MST00405") { // Optional Terms updateStateAsync({ optionalChecked: true }); } // 팝업 닫기 dispatch(setHidePopup()); }, [currentTerms, dispatch, updateStateAsync]); const handleTermsClick = useCallback( (trmsTpCdList) => { if (introTermsData) { const selectedTerms = introTermsData.find( (term) => term.trmsTpCd === trmsTpCdList, ); setCurrentTerms(selectedTerms); dispatch(setShowPopup(Config.ACTIVE_POPUP.termsPopup)); const logTpNo = trmsTpCdList === "MST00402" ? Config.LOG_TP_NO.TERMS.TEARMS_CONDITIONS : Config.LOG_TP_NO.TERMS.PRIVACY_POLICY; dispatch(sendLogTerms({ logTpNo })); } }, [introTermsData, dispatch], ); const handleOptionalTermsClick = useCallback( (trmsTpCdList) => { if (optionalTermsData) { const selectedTerms = optionalTermsData.find( (term) => term.trmsTpCd === trmsTpCdList, ); setCurrentTerms(selectedTerms); dispatch(setShowPopup(Config.ACTIVE_POPUP.termsPopup)); // const logTpNo = // trmsTpCdList === "MST00402" // ? Config.LOG_TP_NO.TERMS.TEARMS_CONDITIONS // : Config.LOG_TP_NO.TERMS.PRIVACY_POLICY; // dispatch(sendLogTerms({ logTpNo })); } }, [optionalTermsData, dispatch], ); const onClose = useCallback(() => { dispatch(setHidePopup()); }, [dispatch]); const handleAgree = useCallback(() => { console.log("[IntroPanel] handleAgree isProcessing=", isProcessing); if (isProcessing){ return; } // 필수 약관이 체크되어 있는지 확인 // if (!termsChecked || !privacyChecked) { // // 필수 약관이 체크되지 않았을 때 알림 // // window.alert($L("Please agree to Terms & Conditions and Privacy Policy.")); // dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, { // title: $L("Required Terms"), // text: $L("Please agree to Terms & Conditions and Privacy Policy."), // button1Text: $L("OK") // })); // return; // } setIsProcessing(true); // 안전장치: 30초 후 자동으로 isProcessing 해제 processingTimeoutRef.current = setTimeout(() => { console.warn("[IntroPanel] handleAgree 타임아웃 - isProcessing 강제 해제"); setIsProcessing(false); }, 30000); // 약관 동의 처리 시작 시 로딩 상태로 설정 dispatch({ type: types.GET_TERMS_AGREE_YN_START }); // Redux에서 가져온 termsIdMap을 사용하여 동적으로 약관 ID 매핑 const agreeTerms = []; if (termsChecked && termsIdMap["MST00402"]) { agreeTerms.push(termsIdMap["MST00402"]); // 이용약관 } if (privacyChecked && termsIdMap["MST00401"]) { agreeTerms.push(termsIdMap["MST00401"]); // 개인정보처리방침 } if (optionalChecked && termsIdMap["MST00405"]) { agreeTerms.push(termsIdMap["MST00405"]); // 선택약관 } if (process.env.NODE_ENV === "development") { console.log("[IntroPanel] 현재 termsIdMap:", termsIdMap); console.log("[IntroPanel] 최종 전송될 agreeTerms:", agreeTerms); } dispatch( registerDevice( { agreeTerms: agreeTerms }, (newRegDeviceData) => { if (newRegDeviceData && newRegDeviceData.retCode === 0) { dispatch( getWelcomeEventInfo((eventInfos) => { if ( eventInfos && Object.keys(eventInfos.data).length > 0 && webOSVersion ) { let displayWelcomeEventPanel = false; if ( eventInfos.data?.welcomeEventFlag === "Y" || (eventInfos.data?.welcomeBillCpnEventFlag === "Y" && Number(webOSVersion) >= 6) ) { displayWelcomeEventPanel = true; } dispatch( sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }), ); if (displayWelcomeEventPanel) { dispatch( pushPanel({ name: panel_names.WELCOME_EVENT_PANEL, panelInfo: { eventInfos: eventInfos.data }, }), ); } } dispatch(popPanel(panel_names.INTRO_PANEL)); setIsProcessing(false); clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 }), ); } else { dispatch( setShowPopup(Config.ACTIVE_POPUP.alertPopup, { title: $L("Error"), text: $L("Device registration failed. Please try again."), button1Text: $L("OK"), }), ); setIsProcessing(false); clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 } }, () => { dispatch( setShowPopup(Config.ACTIVE_POPUP.alertPopup, { title: $L("Error"), text: $L("Device registration failed. Please try again."), button1Text: $L("OK"), }), ); setIsProcessing(false); clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 }, ), ); }, [ termsChecked, privacyChecked, optionalChecked, dispatch, isProcessing, webOSVersion, termsIdMap, ]); // 실패 감지를 위한 useEffect 추가 useEffect(() => { // isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만) if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) { 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); clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 } }, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가 // 컴포넌트 언마운트 시 타임아웃 정리 useEffect(() => { return () => { if (processingTimeoutRef.current) { clearTimeout(processingTimeoutRef.current); } }; }, []); const handleDisagree = useCallback(() => { dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup)); dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.DO_NOT_AGREE })); }, [dispatch]); const onExit = useCallback(() => { dispatch(setHidePopup()); setShowExitMessagePopup(true); dispatch(setExitApp()); dispatch( sendLogTotalRecommend({ contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, }), ); }, [dispatch]); const onCancel = useCallback(() => { if (activePopup === null) { dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup)); } }, [dispatch, activePopup]); const handleFocus = useCallback( (item) => { setFocusAsync(item); }, [setFocusAsync] ); const handleBlur = useCallback(() => { clearFocusAsync(0); }, [clearFocusAsync]); // 체크박스 핸들러들 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( 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( 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( 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 handleTermsClickMST00402 = useCallback( () => handleTermsClick("MST00402"), [handleTermsClick], ); const handleTermsClickMST00401 = useCallback( () => handleTermsClick("MST00401"), [handleTermsClick], ); const handleOptionalTermsClickMST00405 = useCallback( () => handleOptionalTermsClick("MST00405"), [handleOptionalTermsClick], ); 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], ); const rightPanelContent = useRightPanelContent( focusedItem, termsChecked, privacyChecked, shouldShowBenefitsView ); useEffect(() => { Spotlight.focus(); }, [popupVisible]); // useEffect(() => { // if (regDeviceInfoData && regDeviceInfoData.retCode === 0) { // dispatch(setHidePopup()); // dispatch(popPanel(panel_names.INTRO_PANEL)); // } // }, [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.", ); const title = "welcome to shoptime!"; delete rest.isOnTop; // [추가] 약관 종류에 따라 팝업 제목을 반환하는 헬퍼 함수 const getTermsPopupTitle = (terms) => { 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 ""; } }; return ( {/* 첫 번째 영역: 헤더 섹션 */}
{$L("Welcome to")}
{$L("Sh")} {$L("o")} {$L("pTime !")}
{description}
{/* 두 번째 영역: 약관 섹션 */}
{/* Terms & Conditions */}
{$L("Terms & Conditions")}
{/* Privacy Policy */}
{$L("Privacy Policy")}
{/* Optional Terms */}
{$L("Optional Terms")}
{rightPanelContent}
{/* 세 번째 영역: Select All */}
{$L("Select All")}
{/* 네 번째 영역: 버튼 섹션 */}
{$L("Agree")} {$L("Do Not Agree")}
{/* 약관 보기 팝업 */} {activePopup === Config.ACTIVE_POPUP.termsPopup && ( <> {/* */} {/* TermsPopup 호출은 완전히 삭제 */} {/* TNewPopUp을 사용한 새로운 팝업 구현 */} )} {/* DO NOT AGREE */} {activePopup === Config.ACTIVE_POPUP.exitPopup && ( )} {/* ALERT POPUP (필수 약관 알림) */} {activePopup === Config.ACTIVE_POPUP.alertPopup && ( )} {/* Final Exit Message Popup */} {showExitMessagePopup && ( )}
); }