import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import classNames from 'classnames'; import { useDispatch, useSelector, } from 'react-redux'; import { applyMiddleware } from 'redux'; import Spotlight from '@enact/spotlight'; import { getContainerNavigableElements, setContainerLastFocusedElement, } from '@enact/spotlight/src/container'; // DetailPanelBackground 이미지 imports for preloading import hsn from '../../../assets/images/bg/hsn_new.png'; import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png'; import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png'; import ontv4u from '../../../assets/images/bg/ontv4u_new.png'; import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png'; import qvc from '../../../assets/images/bg/qvc_new.png'; import shoplc from '../../../assets/images/bg/shoplc_new.png'; import nbcu from '../../../assets/images/bg/nbcu_new.png'; import { types } from '../../actions/actionTypes'; import { changeAppStatus, setDeepLink, setExitApp, setHidePopup, setOptionalTermsPopupShown, setShowPopup, updateOptionalTermsAgreement, } from '../../actions/commonActions'; import { getWelcomeEventInfo } from '../../actions/eventActions'; import { checkEnterThroughGNB, getHomeLayout, getHomeMainContents, updateHomeInfo, } from '../../actions/homeActions'; import { sendLogGNB, sendLogTotalRecommend, } from '../../actions/logActions'; import { getSubCategory, getTop20Show, } from '../../actions/mainActions'; import { setMyPageTermsAgree } from '../../actions/myPageActions'; import { getHomeOnSaleInfo } from '../../actions/onSaleActions'; import { updatePanel } from '../../actions/panelActions'; import { finishVideoPreview, hideModalVideo, showModalVideo, startVideoPlayerNew, } from '../../actions/playActions'; import { getBestSeller } from '../../actions/productActions'; import OptionalConfirm from '../../components/Optional/OptionalConfirm'; import TBody from '../../components/TBody/TBody'; import TButton, { TYPES } from '../../components/TButton/TButton'; import TPanel from '../../components/TPanel/TPanel'; import TNewPopUp from '../../components/TPopUp/TNewPopUp'; import TPopUp from '../../components/TPopUp/TPopUp'; import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator'; import useDebugKey from '../../hooks/useDebugKey'; import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory'; import usePrevious from '../../hooks/usePrevious'; import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay'; // [COMMENTED OUT] useVideoMove 관련 코드 주석 처리 - 향후 사용 검토 필요 // import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove'; // ======= // } from "../../actions/homeActions"; // import { sendLogGNB, sendLogTotalRecommend } from "../../actions/logActions"; // import { getSubCategory, getTop20Show } from "../../actions/mainActions"; // import { getHomeOnSaleInfo } from "../../actions/onSaleActions"; // import { finishVideoPreview } from "../../actions/playActions"; // import { getBestSeller } from "../../actions/productActions"; // import TBody from "../../components/TBody/TBody"; // import TButton, { TYPES } from "../../components/TButton/TButton"; // import TPanel from "../../components/TPanel/TPanel"; // import TPopUp from "../../components/TPopUp/TPopUp"; // import TVerticalPagenator from "../../components/TVerticalPagenator/TVerticalPagenator"; // import useDebugKey from "../../hooks/useDebugKey"; // >>>>>>> gitlab/develop import { ACTIVE_POPUP, LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names, } from '../../utils/Config'; import { createDebugHelpers } from '../../utils/debug'; import { $L } from '../../utils/helperMethods'; import ImagePreloader from '../../utils/ImagePreloader'; import { SpotlightIds } from '../../utils/SpotlightIds'; import BestSeller from '../HomePanel/BestSeller/BestSeller'; import HomeBanner from '../HomePanel/HomeBanner/HomeBanner'; import HomeOnSale from '../HomePanel/HomeOnSale/HomeOnSale'; import css from '../HomePanel/HomePanel.module.less'; import PopularShow from '../HomePanel/PopularShow/PopularShow'; import SubCategory from '../HomePanel/SubCategory/SubCategory'; import EventPopUpBanner from './EventPopUpBanner/EventPopUpBanner'; import PickedForYou from './PickedForYou/PickedForYou'; // 디버그 헬퍼 설정 const DEBUG_MODE = false; const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); // 파트너사별 배경 이미지 맵 const BACKGROUND_IMAGES = { 1: qvc, // QVC 2: hsn, // HSN 4: ontv4u, // ONTV 9: lgelectronics, // LG ELECTRONICS 11: shoplc, // SHOPLC 16: koreaKiosk, // KOREA KIOSK 19: Pinkfong, // PINKFONG 21: nbcu, // NBCU }; export const TEMPLATE_CODE_CONF = { TOP: 'DSP00101', CATEGORY_ITEM: 'DSP00102', ON_SALE: 'DSP00103', POPULAR_SHOW: 'DSP00104', BEST_SELLER: 'DSP00105', PICK_FOR_YOU: 'DSP00106', }; const HomePanel = ({ isOnTop, showGradientBackground = false }) => { const dispatch = useDispatch(); // ✅ showGradientBackground prop 변경 추적 로그 // const prevShowGradientBackground = usePrevious(showGradientBackground); // useEffect(() => { // if (prevShowGradientBackground !== showGradientBackground) { // console.log('[TRACE-GRADIENT] 📊 HomePanel prop changed:', { // prev: prevShowGradientBackground, // current: showGradientBackground, // isOnTop: isOnTop // }); // } // }, [showGradientBackground, prevShowGradientBackground, isOnTop]); useDebugKey({ isLandingPage: true }); // 🔽 HomeBanner 외부 7개 아이콘들의 focusHistory 추적 const focusHistory = useFocusHistory({ enableLogging: true, useGlobalState: true, logPrefix: '[HomePanel-Focus]', }); // 🔽 useVideoPlay - 동영상 재생 제어 const videoPlay = useVideoPlay({ enableLogging: true, logPrefix: '[HomePanel-VideoPlay]', }); // 🔽 비디오 재생 의도 공유 ref const videoPlayIntentRef = useRef(null); // ✅ [251116] 비디오 중복 재생 방지 ref const lastPlayedBannerIdRef = useRef(null); // 🔽 useVideoMove - 포커스 전환 기반 동영상 제어 // [COMMENTED OUT] useVideoMove 미사용 - cleanup() 호출되지 않음 // const { playByTransition, cleanup } = useVideoMove({ // enableLogging: true, // logPrefix: '[HomePanel-VideoMove]', // }); const isGnbOpened = useSelector((state) => state.common.isGnbOpened); const homeLayoutInfo = useSelector((state) => state.home.layoutData); const panelInfo = useSelector((state) => state.home.homeInfo?.panelInfo ?? {}); const panels = useSelector((state) => state.panels.panels); const webOSVersion = useSelector((state) => state.common.appStatus?.webOSVersion); const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB); const defaultFocus = useSelector((state) => state.home.defaultFocus); const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos); // ✅ PlayerPanel의 shouldShrinkTo1px 상태 추적 const playerPanelShouldShrink = useSelector((state) => { const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL); return playerPanel?.panelInfo?.shouldShrinkTo1px ?? false; }); // ✅ PlayerPanel의 modal 상태 추적 (false → true 감지용) const playerModalState = useSelector((state) => { const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL); return playerPanel?.panelInfo?.modal ?? false; }); const prevPlayerModalStateRef = useRef(false); const categoryInfos = useSelector((state) => state.onSale.homeOnSaleData?.data?.categoryInfos); const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos); const { popupVisible, activePopup } = useSelector((state) => state.common.popup); const eventPopInfosData = useSelector((state) => state.event.eventData.eventPopInfo); const eventData = useSelector((state) => state.event.eventData); const eventClickSuccess = useSelector((state) => state.event.eventClickSuccess); const homeOnSaleInfos = useSelector((state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos); const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const isDeepLink = useSelector((state) => state.common.deepLinkInfo.isDeepLink); // 선택약관 관련 Redux 상태 const termsData = useSelector((state) => state.home.termsData); const termsIdMap = useSelector((state) => state.home.termsIdMap); const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable); const optionalTermsData = useSelector((state) => { if (state.home.termsData && state.home.termsData.data && state.home.termsData.data.terms) { return state.home.termsData.data.terms.find((term) => term.trmsTpCd === 'MST00405'); } return null; }); const termsLoading = useSelector((state) => state.common.termsLoading); const currentTermsFlag = useSelector((state) => state.common.termsFlag); const optionalTermsPopupFlow = useSelector((state) => state.common.optionalTermsPopupFlow); const [btnDisabled, setBtnDisabled] = useState(true); const [arrowBottom, setArrowBottom] = useState(true); const [firstSpot, setFirstSpot] = useState(false); const [eventPopOpen, setEventPopOpen] = useState(false); const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf); const [firstLgCatCd, setFirstLgCatCd] = useState(panelInfo.currentCatCd ?? null); const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null); const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null); // 선택약관 팝업 상태 const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false); const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false); const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false); const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); const [focusedContainerId, setFocusedContainerId] = useState(panelInfo.focusedContainerId); // DetailPanel 진입 시 포커스 대상 저장 const lastFocusedTargetRef = useRef(panelInfo.lastFocusedTargetId || null); const isInitialRender = useRef(true); const verticalPagenatorRef = useRef(null); const currentSentMenuRef = useRef(null); const lastRestoredIdRef = useRef(null); const focusedContainerIdRef = useRef(null); const prevIsOnTopRef = useRef(isOnTop); // ✅ [251124] HomePanel을 벗어날 때(isOnTop: true -> false) 현재 포커스 저장 useEffect(() => { if (prevIsOnTopRef.current && !isOnTop) { const current = Spotlight.getCurrent(); const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`); if (current && tBody && tBody.contains(current)) { const targetId = current.getAttribute('data-spotlight-id'); if (targetId) { dlog('[HomePanel] Saving focus before leaving:', targetId); lastFocusedTargetRef.current = targetId; dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { lastFocusedTargetId: targetId, }, }) ); } } } prevIsOnTopRef.current = isOnTop; }, [isOnTop, dispatch]); // ✅ [251119] DetailPanelBackground 이미지 프리로딩 // HomePanel 마운트 시 백그라운드로 모든 파트너사 배경 이미지를 미리 로드하여 // DetailPanel 진입 시 로딩 지연을 방지함 useEffect(() => { dlog('[HomePanel] Starting background image preloading...'); // HomePanel의 다른 기능들에 영향을 주지 않도록 비동기로 조용히 실행 setTimeout(() => { ImagePreloader.preloadAllImages(BACKGROUND_IMAGES) .then((results) => { const successCount = results.filter((r) => r !== null).length; dlog(`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`); // 프리로딩 통계 정보 로깅 (디버깅용) const stats = ImagePreloader.getStats(); dlog('[HomePanel] Preloader stats:', stats); }) .catch((error) => { derror('[HomePanel] Background image preloading failed:', error); // 프리로딩 실패가 HomePanel 기능에 영향을 주지 않도록 조용히 처리 }); }, 1000); // HomePanel 안정화 후 1초 뒤 시작 }, []); // 마운트 시 한 번만 실행 useEffect(() => { if (nowMenu === 'Home/Top') { dispatch( sendLogTotalRecommend({ messageId: LOG_MESSAGE_ID.HOME, contextName: LOG_CONTEXT_NAME.HOME, }) ); } }, [entryMenu, nowMenu]); const sortedHomeLayoutInfo = useMemo(() => { if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) { const sorted = [...homeLayoutInfo.homeLayoutInfo].sort((x, y) => x.expsOrd - y.expsOrd); return sorted; } return []; }, [homeLayoutInfo]); const pageSpotIds = useMemo(() => { const spots = []; for (let i = 0; i < sortedHomeLayoutInfo.length; i++) { spots.push(sortedHomeLayoutInfo[i].shptmApphmDspyOptCd); } return spots; }, [sortedHomeLayoutInfo]); const cbChangePageRef = useRef(null); const prevScrollTopRef = useRef(0); // 이전 scrollTop 추적 const isScrollingUpRef = useRef(false); // 스크롤 위로 감지 const scrollExpandTimerRef = useRef(null); // 스크롤 복구 타이머 // ✅ 최상단 비디오 복구 관련 Refs const shouldShrinkRef = useRef(false); // PlayerPanel의 shouldShrinkTo1px 추적 const expandIntervalRef = useRef(null); // 최상단에서의 interval const expandAttemptRef = useRef(0); // 복구 시도 횟수 const loadingComplete = useSelector((state) => state.common?.loadingComplete); const isVideoTransitionLocked = useSelector((state) => state.home.videoTransitionLocked); // 선택약관 동의 핸들러 const handleOptionalAgree = useCallback(() => { if (!termsIdMap || Object.keys(termsIdMap).length === 0) { if (process.env.NODE_ENV === 'development') { derror('[HomePanel] termsIdMap이 없습니다:', termsIdMap); } return; } const requiredTermTypes = ['MST00401', 'MST00402', 'MST00405']; const missingTerms = requiredTermTypes.filter((type) => !termsIdMap[type]); if (missingTerms.length > 0) { if (process.env.NODE_ENV === 'development') { derror('[HomePanel] 누락된 약관 타입:', missingTerms); } return; } const termsList = []; if (termsIdMap['MST00401']) { termsList.push(termsIdMap['MST00401']); // 개인정보처리방침 } if (termsIdMap['MST00402']) { termsList.push(termsIdMap['MST00402']); // 이용약관 } if (termsIdMap['MST00405']) { termsList.push(termsIdMap['MST00405']); // 선택약관 } const notTermsList = []; if (process.env.NODE_ENV === 'development') { dlog('[HomePanel] 현재 termsIdMap:', termsIdMap); dlog('[HomePanel] 약관 동의 API 호출 파라미터:', { termsList, notTermsList, }); } const callback = (response) => { if (response.retCode === '000' || response.retCode === 0) { if (process.env.NODE_ENV === 'development') { dlog('[HomePanel] 약관 동의 성공:', response); } dispatch(updateOptionalTermsAgreement(true)); setOptionalTermsAgreed(true); } else { if (process.env.NODE_ENV === 'development') { derror('[HomePanel] 약관 동의 실패:', response); } } }; dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback)); }, [dispatch, termsIdMap]); const handleOptionalTermsClick = useCallback(() => { if (process.env.NODE_ENV === 'development') { dlog('[HomePanel] 약관 자세히 보기 클릭'); } setIsOptionalConfirmVisible(false); setIsOptionalTermsVisible(true); }, []); const handleOptionalAgreeClick = useCallback(() => { handleOptionalAgree(); setIsOptionalConfirmVisible(false); dispatch({ type: types.GET_TERMS_AGREE_YN_SUCCESS, payload: { ...currentTermsFlag, optionalTerms: 'Y', }, }); setTimeout(()=>{ Spotlight.focus('home_tbody'); },100) }, [handleOptionalAgree, dispatch, currentTermsFlag]); const handleOptionalDeclineClick = useCallback(() => { if (process.env.NODE_ENV === 'development') { dlog('[HomePanel] 거절/다음에 하기 버튼 클릭'); } dispatch(updateOptionalTermsAgreement(false)); setIsOptionalConfirmVisible(false); setTimeout(()=>{ Spotlight.focus('home_tbody'); },100) }, [dispatch]); const handleTermsPopupClosed = useCallback(() => { setIsOptionalTermsVisible(false); setIsOptionalConfirmVisible(true); Spotlight.focus('optional-confirm-popup'); }, []); const handleTermsPopupAgree = useCallback(() => { if (process.env.NODE_ENV === 'development') { dlog('[HomePanel] handleTermsPopupAgree'); } handleOptionalAgree(); setIsOptionalTermsVisible(false); }, [handleOptionalAgree]); // shouldShowOptionalTermsPopup 계산 const shouldShowOptionalTermsPopup = useMemo(() => { // 1. 기본 조건 확인 if (termsLoading || isGnbOpened || !optionalTermsAvailable) { return false; } // 2. 새로운 Redux 상태 확인 (TV 환경 최적화) if ( optionalTermsPopupFlow.popupShown || optionalTermsPopupFlow.userDecision || optionalTermsPopupFlow.agreedInSession ) { return false; } // 3. 서버 데이터 확인 const terms = termsData && termsData.data && termsData.data.terms; if (!terms) { return false; } const optionalTerm = terms.find((term) => term.trmsTpCd === 'MST00405'); const result = optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false; return result; }, [ termsData.data?.terms, termsLoading, isGnbOpened, optionalTermsAvailable, optionalTermsPopupFlow, ]); // 선택약관 팝업 표시 처리 useEffect(() => { if (termsLoading) { return; } if (shouldShowOptionalTermsPopup && !isOptionalConfirmVisible) { dlog('shouldShowOptionalTermsPopup', shouldShowOptionalTermsPopup); dlog('HomePanel optionalTermsConfirm 팝업 표시'); const timer = setTimeout(() => { setIsOptionalConfirmVisible(true); dispatch(setOptionalTermsPopupShown(true)); }, 1000); return () => clearTimeout(timer); } }, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]); const onCancel = useCallback(() => { const currentSpot = Spotlight.getCurrent(); if (!isOnTop) { return; } if (isGnbOpened) { return; } dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { currentSpot: currentSpot, }, }) ); dispatch(setShowPopup(ACTIVE_POPUP.exitPopup)); }, [isOnTop, isGnbOpened, dispatch]); const onExit = useCallback(() => { dispatch(setExitApp()); dispatch( sendLogTotalRecommend({ contextName: LOG_CONTEXT_NAME.SHOPTIME, messageId: LOG_MESSAGE_ID.VIEW_CHANGE, visible: false, }) ); }, [dispatch, isVideoTransitionLocked]); const onClose = useCallback(() => { dispatch(setHidePopup()); setTimeout(() => { Spotlight.focus(SpotlightIds.HOME_TBODY); }, 0); }, [dispatch]); const handleTopButtonClick = useCallback(() => { if (cbChangePageRef.current) { cbChangePageRef.current(0, true); } if (sortedHomeLayoutInfo?.[0]) { const containerId = sortedHomeLayoutInfo[0].shptmApphmDspyOptCd; const navigableEls = getContainerNavigableElements(containerId); const navigableIds = navigableEls.filter((el) => typeof el === 'string'); const target = containerId === TEMPLATE_CODE_CONF.TOP ? 'banner0' : containerId; if (navigableIds.length > 0) { setContainerLastFocusedElement(null, navigableIds); } setContainerLastFocusedElement(null, [containerId]); setTimeout(() => Spotlight.focus(target)); } }, [sortedHomeLayoutInfo]); const doSendLogGNB = useCallback( (containerId, location = null, title = null) => { let nowMenu; switch (containerId) { case TEMPLATE_CODE_CONF.TOP: nowMenu = LOG_MENU.HOME_TOP; break; case TEMPLATE_CODE_CONF.CATEGORY_ITEM: nowMenu = LOG_MENU.HOME_CATEGORY; break; case TEMPLATE_CODE_CONF.ON_SALE: nowMenu = LOG_MENU.HOME_ON_SALE; break; case TEMPLATE_CODE_CONF.POPULAR_SHOW: nowMenu = LOG_MENU.HOME_POPULAR_SHOWS; break; case TEMPLATE_CODE_CONF.BEST_SELLER: nowMenu = LOG_MENU.HOME_BEST_SELLER; break; case TEMPLATE_CODE_CONF.PICK_FOR_YOU: nowMenu = LOG_MENU.HOME_PICKED_FOR_YOU; break; default: nowMenu = LOG_MENU.HOME_TOP; break; } if (containerId !== nowShelf && location && title) { dispatch( sendLogTotalRecommend({ contextName: LOG_CONTEXT_NAME.HOME, messageId: LOG_MESSAGE_ID.SHELF, shelfLocation: location, shelfId: containerId, shelfTitle: title, }) ); setNowShelf(containerId); } if (nowMenu && nowMenu !== currentSentMenuRef.current) { dispatch(sendLogGNB(nowMenu)); currentSentMenuRef.current = nowMenu; } }, [pageSpotIds, nowShelf, panelInfo.nowShelf] ); const handleItemFocus = useCallback( (containerId, location, title) => () => { doSendLogGNB(containerId, location, title); }, [doSendLogGNB] ); const renderPageItem = useCallback(() => { return ( <> {sortedHomeLayoutInfo.map((el) => { switch (el.shptmApphmDspyOptCd) { case TEMPLATE_CODE_CONF.TOP: { return ( ); } case TEMPLATE_CODE_CONF.CATEGORY_ITEM: { if ( categoryInfos && categoryInfos.length > 0 && categoryItemInfos?.subCatItemList && categoryItemInfos.subCatItemList.length > 0 ) { return ( ); } else break; } case TEMPLATE_CODE_CONF.ON_SALE: { if (homeOnSaleInfos && homeOnSaleInfos.length > 0) { return ( ); } else break; } case TEMPLATE_CODE_CONF.POPULAR_SHOW: { if (topInfos && topInfos.length > 0) { return ( ); } else break; } case TEMPLATE_CODE_CONF.BEST_SELLER: { if (bestSellerDatas && bestSellerDatas.length > 0) { return ( ); } else break; } case TEMPLATE_CODE_CONF.PICK_FOR_YOU: { if (bestSellerDatas && bestSellerDatas.length > 0) { return ( ); } else break; } } })} {loadingComplete && sortedHomeLayoutInfo && sortedHomeLayoutInfo.length > 0 && ( )} ); }, [ sortedHomeLayoutInfo, cateCd, cateNm, handleItemFocus, handleTopButtonClick, btnDisabled, panelInfo.focusedContainerId, panelInfo.currentSpot, categoryItemInfos, categoryInfos, homeOnSaleInfos, bestSellerDatas, topInfos, ]); // ✅ useEffect: Redux 상태 동기화 & Interval 정리 useEffect(() => { // PlayerPanel의 shouldShrinkTo1px를 Ref에 동기화 shouldShrinkRef.current = playerPanelShouldShrink; // shouldShrinkTo1px가 false가 되면 interval 즉시 정리 if (!playerPanelShouldShrink && expandIntervalRef.current) { // console.log('[HomePanel] shouldShrinkTo1px=false - clearing expand interval'); clearInterval(expandIntervalRef.current); expandIntervalRef.current = null; expandAttemptRef.current = 0; } }, [playerPanelShouldShrink]); const _onScrollStatusChanged = useCallback((status) => { if (status === 'end') { setArrowBottom(false); } else { setArrowBottom(true); } }, []); // ✅ useCallback: 의존성은 dispatch만 const _onScroll = useCallback( (e) => { const currentScrollTop = e.scrollTop; const prevScrollTop = prevScrollTopRef.current; // ✅ 최상단 도달: 1px 비디오 복구 시도 (shouldShrinkRef.current로 읽음) if (currentScrollTop <= 0) { if (shouldShrinkRef.current && !expandIntervalRef.current) { // console.log('[HomePanel] At top (scrollTop <= 1) - starting video expansion'); expandAttemptRef.current = 0; // Interval 시작: 200ms마다 복구 시도 expandIntervalRef.current = setInterval(() => { // 종료 조건: 최대 3회 시도 if (expandAttemptRef.current >= 3) { // console.log('[HomePanel] Max expansion attempts reached (3)'); clearInterval(expandIntervalRef.current); expandIntervalRef.current = null; return; } // console.log('[HomePanel] Expansion attempt', expandAttemptRef.current + 1); dispatch(showModalVideo()); expandAttemptRef.current++; }, 200); } } // 최상단 벗어남: interval 정리 else if (currentScrollTop > 1 && expandIntervalRef.current) { // console.log('[HomePanel] Left top - clearing expand interval'); clearInterval(expandIntervalRef.current); expandIntervalRef.current = null; } // 기존 로직: 아래로 스크롤 if (currentScrollTop > prevScrollTop) { // 아래로 스크롤: 비디오를 1px로 축소 // console.log('[HomePanel] Scrolling down - shrinking video'); if (!isVideoTransitionLocked) { dispatch(hideModalVideo()); } // 기존 타이머 취소 if (scrollExpandTimerRef.current) { clearTimeout(scrollExpandTimerRef.current); scrollExpandTimerRef.current = null; } } // 기존 로직: 위로 스크롤 (0이 아닌) else if (currentScrollTop < prevScrollTop && currentScrollTop > 1) { // 위로 스크롤 (최상단 아님): 1초 후 복구 // console.log('[HomePanel] Scrolling up - will expand after 1s'); // 기존 타이머 취소 if (scrollExpandTimerRef.current) { clearTimeout(scrollExpandTimerRef.current); } // 1초 후 자동으로 크기 조정 scrollExpandTimerRef.current = setTimeout(() => { // console.log('[HomePanel] 1s passed - auto expanding video'); dispatch(showModalVideo()); scrollExpandTimerRef.current = null; }, 1000); } // 이전 scrollTop 업데이트 prevScrollTopRef.current = currentScrollTop; }, [dispatch] ); const _onFocusedContainerId = useCallback( (containerId) => { setFocusedContainerId(containerId); dlog('[_onFocusedContainerId] 호출됨:', { containerId, enterThroughGNB, isOnTop, condition: enterThroughGNB && cbChangePageRef.current && !isOnTop, }); if (enterThroughGNB && cbChangePageRef.current && !isOnTop) { dlog('[_onFocusedContainerId] ⚠️ 맨 위로 스크롤 강제 실행!'); Spotlight.resume(); cbChangePageRef.current(0, false); Spotlight.focus(defaultFocus); setBtnDisabled(false); dispatch(checkEnterThroughGNB(false)); return; } if (!firstSpot) { // 초기 포커스 복원으로 스크롤이 튀는 현상 방지: 첫 진입 시 포커스 이동을 생략 setFirstSpot(true); setBtnDisabled(false); return; } }, [ panelInfo, firstSpot, enterThroughGNB, defaultFocus, cbChangePageRef, dispatch, isOnTop, panels, ] ); const bestSellerLoaded = useCallback(() => { dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); }, []); useEffect(() => { if (isInitialRender.current) { isInitialRender.current = false; // ✅ 마운트 시 banner0 자동 재생 설정 // console.log('[HomePanel] 마운트 - banner0 재생 시도', videoPlay); videoPlayIntentRef.current = { bannerId: 'banner0' }; // console.log('[HomePanel] videoPlayIntentRef 설정:', videoPlayIntentRef.current); // videoPlay.playVideo('banner0', { reason: 'mount-init' }); // console.log('[HomePanel] playVideo 호출 완료'); if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) { dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })); dispatch(getHomeMainContents()); dispatch(getHomeLayout()); dispatch( getHomeOnSaleInfo({ homeSaleInfosIncFlag: 'Y', categoryIncFlag: 'Y', saleInfosIncFlag: 'N', }) ); dispatch(getTop20Show()); dispatch(getBestSeller(bestSellerLoaded)); if (isDeepLink) { dispatch(setDeepLink({ contentTarget: '', isDeepLink: false })); } } } }, [dispatch, isDeepLink, panels.length, panelInfo.focusedContainerId]); useEffect(() => { doSendLogGNB(focusedContainerId); }, [focusedContainerId]); useEffect(() => { if (firstLgCatCd === null) { if (categoryInfos) { setFirstLgCatCd(categoryInfos[0]?.lgCatCd); setCateCd(categoryInfos[0]?.lgCatCd); setCateNm(categoryInfos[0]?.lgCatNm); } } if (firstLgCatCd !== null) { dispatch( getSubCategory( { lgCatCd: firstLgCatCd, //LG Electronics Base pageSize: 10, tabType: 'CAT00102', filterType: 'CAT00202', recommendIncFlag: 'Y', }, 1 ) ); } }, [categoryInfos, firstLgCatCd, dispatch]); const checkBillngEvent = useCallback( (eventTpCd) => { if (webOSVersion && Number(webOSVersion) >= 4) { if (eventTpCd === 'EVT00108' || eventTpCd === 'EVT00107') { if (webOSVersion && Number(webOSVersion) >= 6) { setEventPopOpen(true); } else setEventPopOpen(false); } else setEventPopOpen(true); } else setEventPopOpen(false); }, [webOSVersion] ); useEffect(() => { if (Object.keys(eventData).length >= 1) { if (eventPopInfosData && eventClickSuccess === null) { checkBillngEvent(eventPopInfosData.evntTpCd); if (eventPopOpen) { dispatch(setShowPopup(ACTIVE_POPUP.eventPopup)); } } } }, [eventPopOpen, eventData]); useEffect(() => { if (Object.keys(eventData).length <= 0 && eventClickSuccess === null) { dispatch(getWelcomeEventInfo()); } }, [dispatch]); // [COMMENTED OUT] 수동 포커스 복원 비활성화 - TBody의 enterTo: 'last-focused' 자동 복원 사용 // useEffect(() => { // if (isOnTop && panelInfo?.lastFocusedTargetId) { // Spotlight.focus(panelInfo.lastFocusedTargetId); // } // }, [isOnTop]); // ✅ HomePanel 활성화 조건: 최상단 패널 또는 PlayerPanel 아래의 두 번째 패널 // 비디오가 재생이 아니면 videoPlayIntentRef의 bannerId로 비디오 재생 // [251116] isHomeOnTop인 경우에는 비디오가 항상 재생되어야 함 useEffect(() => { // <<<<<<< HEAD // console.log('[HomeActive] useEffect 실행 - isOnTop:', isOnTop); // console.log('[HomeActive] videoPlayIntentRef.current:', videoPlayIntentRef.current); // console.log( // '[HomeActive] panels 상태:', // panels.map((p) => ({ name: p.name, modal: p.panelInfo?.modal })) // ); const isHomePanelActive = isOnTop; // console.log('[HomeActive] isHomePanelActive:', isHomePanelActive); if (!isHomePanelActive) { // console.log('[HomeActive] 조건 실패: isHomePanelActive가 false'); return; } if (!videoPlayIntentRef.current?.bannerId) { // console.log('[HomeActive] 조건 실패: videoPlayIntentRef.current?.bannerId가 없음', { // hasRef: !!videoPlayIntentRef.current, // hasBannerId: !!videoPlayIntentRef.current?.bannerId, // }); return; } const bannerId = videoPlayIntentRef.current.bannerId; // console.log('[HomeActive] 비디오 재생 시도 - bannerId:', bannerId); // console.log('[HomeActive] 마지막 재생한 배너:', lastPlayedBannerIdRef.current); // ✅ [251116] 중복 재생 방지: 같은 배너면 스킵 if (lastPlayedBannerIdRef.current === bannerId) { // console.log('[HomeActive] 중복 호출 감지 - 스킵 (이미 재생 중)'); return; } // const currentPlaying = videoPlay.getCurrentPlayingBanner(); // 이미 재생 중인 배너가 다르면 새로 재생 // if (currentPlaying !== bannerId) { // videoPlay.playVideo(bannerId, { // reason: 'homePanel-active', // }); // } // ✅ [251116] startVideoPlayerNew로 비디오 자동 재생 // console.log('[HomeActive] dispatch(startVideoPlayerNew) 호출 직전:', { bannerId, modal: true }); // Spotlight.focus(bannerId); // dispatch( // startVideoPlayerNew({ // bannerId: bannerId, // modal: true, // modalContainerId: 'banner-modal-' + bannerId, // }) // ); // console.log('[HomeActive] dispatch(startVideoPlayerNew) 호출 완료'); // 재생 기록 업데이트 lastPlayedBannerIdRef.current = bannerId; // console.log('[HomeActive] 재생 기록 업데이트:', bannerId); }, [isOnTop, dispatch]); // ✅ [251120] DetailPanel 닫힘 감지 useEffect - detailPanelClosed flag 사용 const detailPanelClosed = useSelector( (state) => state.home.homeInfo?.panelInfo?.detailPanelClosed ); const detailPanelClosedTime = useSelector( (state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt ); useEffect(() => { if (detailPanelClosed && isOnTop) { // console.log('[TRACE-GRADIENT] 🔄 detailPanelClosed flag triggered - HomePanel reactivated'); // console.log('[HomePanel] *** ✅ HomePanel isOnTop = true'); // console.log('[HomePanel] *** detailPanelClosed:', detailPanelClosed); // console.log('[HomePanel] *** detailPanelClosedTime:', detailPanelClosedTime); // console.log('[HomePanel] *** isOnTop:', isOnTop); // console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current); // console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current); // 🔽 videoPlayIntentRef가 null인 경우: 비디오 재생 가능한 첫 번째 배너 찾기 if (!videoPlayIntentRef.current && bannerDataList) { dlog('[HomePanel] *** videoPlayIntentRef가 null - 첫 번째 비디오 배너 검색'); // HomeBanner.jsx의 defaultFocus 계산 로직과 동일 let targetIndex = 0; let targetBannerData = null; let videoData = null; for (let i = 0; i < bannerDataList.length; i++) { const data = bannerDataList[i]; const bannerDetailInfos = data.bannerDetailInfos; if (data.shptmDspyTpNm === 'Random') { if ( bannerDetailInfos[data.randomIndex].shptmBanrTpNm === 'LIVE' || bannerDetailInfos[data.randomIndex].shptmBanrTpNm === 'VOD' ) { targetIndex = i; targetBannerData = data; videoData = bannerDetailInfos[data.randomIndex]; break; } } else if ( bannerDetailInfos.find( (el) => el.shptmBanrTpNm === 'LIVE' || el.shptmBanrTpNm === 'VOD' ) ) { targetIndex = i; targetBannerData = data; videoData = bannerDetailInfos[0]; break; } } if (targetBannerData && videoData) { const bannerId = 'banner' + targetIndex; dlog('[HomePanel] *** 찾은 비디오 배너:', bannerId, videoData); // videoPlayIntentRef에 값 할당 (HomeBanner.jsx의 초기 재생 로직과 동일) videoPlayIntentRef.current = { bannerId: bannerId, videoProps: { bannerId: bannerId, showUrl: videoData.showUrl, patnrId: videoData.patnrId, showId: videoData.showId, shptmBanrTpNm: videoData.shptmBanrTpNm, lgCatCd: videoData.lgCatCd, chanId: videoData.brdcChnlId, modal: true, modalContainerId: bannerId, }, }; lastPlayedBannerIdRef.current = bannerId; dlog('[HomePanel] *** videoPlayIntentRef 설정 완료:', videoPlayIntentRef.current); } else { dlog('[HomePanel] *** ⚠️ 비디오 재생 가능한 배너를 찾지 못함'); } } // DetailPanel 닫힘 후 비디오 자동 재생 복구 if (videoPlayIntentRef.current && lastPlayedBannerIdRef.current) { // 🔽 videoPlayIntentRef는 videoProps에 비디오 정보를 담고 있으므로 풀어서 전달 // 혹시 videoProps에 없는 필드는 상위 레벨을 fallback으로 사용 const intent = videoPlayIntentRef.current; const videoProps = intent.videoProps || {}; // 🔽 [251118] 현재 스크롤 위치 확인하여 비디오 크기 결정 const currentScrollTop = prevScrollTopRef.current; const shouldShrink = currentScrollTop > 0; dlog( '[HomePanel] *** 비디오 복구 - currentScrollTop:', currentScrollTop, ', shouldShrink:', shouldShrink ); dispatch( startVideoPlayerNew({ ...videoProps, modal: true, modalContainerId: videoProps.modalContainerId || 'home-video-modal', modalClassName: videoProps.modalClassName || 'home-video-modal', spotlightDisable: true, // 자동 복구 시 스크롤 이동 막기 위해 포커스 이동 금지 bannerId: lastPlayedBannerIdRef.current, videoId: videoProps.videoId || intent.videoId, showUrl: videoProps.showUrl || intent.showUrl, patnrId: videoProps.patnrId || intent.patnrId, showId: videoProps.showId || intent.showId, shptmBanrTpNm: videoProps.shptmBanrTpNm || intent.shptmBanrTpNm, lgCatCd: videoProps.lgCatCd || intent.lgCatCd, shouldShrinkTo1px: shouldShrink, // 스크롤 위치에 따라 설정 }) ); // 🔽 DetailPanel에서 돌아온 뒤 포커스를 마지막 포커스 대상에 복원 dlog('[HomePanel] *** 🎯 Focus 복원 준비'); const targetFocusId = panelInfo.lastFocusedTargetId || lastFocusedTargetRef.current; dlog( '[HomePanel] *** 📍 targetFocusId:', targetFocusId, '(panelInfo.lastFocusedTargetId:', panelInfo.lastFocusedTargetId, ', lastFocusedTargetRef:', lastFocusedTargetRef.current, ')' ); if (targetFocusId) { dlog('[HomePanel] *** ⏰ 300ms 후 Spotlight.focus 호출 예정'); setTimeout(() => { dlog('[HomePanel] *** 🔍 Spotlight.focus 호출:', targetFocusId); Spotlight.focus(targetFocusId); }, 300); } else { dlog('[HomePanel] *** ⚠️ targetFocusId가 없음 - Focus 복원 스킵'); } // refs 초기화 videoPlayIntentRef.current = null; lastPlayedBannerIdRef.current = null; // detailPanelClosed 플래그 초기화 (다음 사이클에서 재사용 방지) dlog('[HomePanel] *** detailPanelClosed flag 초기화'); dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { detailPanelClosed: false, detailPanelClosedAt: undefined, detailPanelClosedFromSource: undefined, }, }) ); } } }, [detailPanelClosed, isOnTop, bannerDataList, dispatch]); // ======= // const justCameBack = !prevIsOnTopRef.current && isOnTop; // if ( // justCameBack && // panelInfo?.lastFocusedTargetId && // panelInfo.lastFocusedTargetId !== lastRestoredIdRef.current // ) { // lastRestoredIdRef.current = panelInfo.lastFocusedTargetId; // setTimeout(() => { // Spotlight.focus(panelInfo.lastFocusedTargetId); // }, 150); // } // prevIsOnTopRef.current = isOnTop; // }, [isOnTop, panelInfo?.lastFocusedTargetId]); // >>>>>>> gitlab/develop useEffect(() => { return () => { const c = Spotlight.getCurrent(); let targetSpotlightId = null; let targetSpotlightCatcd = null; let targetSpotlightCateNm = null; if (c) { targetSpotlightId = c.getAttribute('data-spotlight-id'); targetSpotlightCatcd = c.getAttribute('data-catcd-num'); targetSpotlightCateNm = c.getAttribute('data-catcd-nm'); } const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`); const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null; dispatch(checkEnterThroughGNB(false)); dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { currentSpot: currentSpot, currentCatCd: targetSpotlightCatcd, currentCateName: targetSpotlightCateNm, // <<<<<<< HEAD focusedContainerId: focusedContainerIdRef.current, lastFocusedTargetId: lastFocusedTargetRef.current || panelInfo.lastFocusedTargetId, // ======= // focusedContainerId: focusedContainerId, // >>>>>>> gitlab/develop }, }) ); }; }, [dispatch, focusedContainerId]); const handleArrowClick = useCallback(() => { if (verticalPagenatorRef.current) { const nextNode = verticalPagenatorRef.current.findNextWheelPoint(); if (nextNode) { verticalPagenatorRef.current.moveToNode(nextNode, true, true); } } }, [verticalPagenatorRef]); // ✅ PlayerPanel modal 상태 변화 감지 (false → true) : Fullscreen -> Banner useEffect(() => { const prevModalState = prevPlayerModalStateRef.current; dlog('[HomePanel] 🔍 Modal 상태 체크:', { prevModalState, playerModalState, shouldExecute: prevModalState === false && playerModalState === true, }); if (prevModalState === false && playerModalState === true) { dlog('>>>>>[HomePanel] ▶️ PlayerPanel이 Fullscreen에서 Banner로 전환됨'); // 0.5초 후 비디오가 재생되는 배너에 포커스 테두리 효과 적용 // const focusTimer = setTimeout(() => { // if (videoPlayIntentRef.current?.bannerId) { // const bannerId = videoPlayIntentRef.current.bannerId; // const bannerElement = document.querySelector(`[data-spotlight-id="${bannerId}"]`); // if (bannerElement) { // // 포커스 테두리 효과 적용 (기존 .focused mixin 스타일 적용) // bannerElement.style.border = '4px solid #c70850'; // bannerElement.style.borderRadius = '12px'; // bannerElement.style.boxShadow = '0 0 22px 0 rgba(0, 0, 0, 0.5)'; // bannerElement.style.transition = 'all 0.3s ease'; // // 2초 후 효과 제거 // setTimeout(() => { // bannerElement.style.border = ''; // bannerElement.style.borderRadius = ''; // bannerElement.style.boxShadow = ''; // }, 2000); // } // } // }, 500); } prevPlayerModalStateRef.current = playerModalState; }, [playerModalState, videoPlayIntentRef]); // ✅ Cleanup: 컴포넌트 언마운트 시 모든 타이머 정리 useEffect(() => { return () => { if (scrollExpandTimerRef.current) { clearTimeout(scrollExpandTimerRef.current); scrollExpandTimerRef.current = null; } if (expandIntervalRef.current) { clearInterval(expandIntervalRef.current); expandIntervalRef.current = null; } }; }, []); return ( <> {/* HomePanel용 메모리 상주 그라데이션 배경 */}