// src/views/DetailPanel/DetailPanel.new.jsx import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Spotlight from '@enact/spotlight'; import { setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { getDeviceAdditionInfo } from '../../actions/deviceActions'; import { getThemeCurationDetailInfo, updateHomeInfo } from '../../actions/homeActions'; import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions'; import { finishModalMediaForce } from '../../actions/mediaActions'; import { popPanel, updatePanel } from '../../actions/panelActions'; import { finishVideoPreview, pauseFullscreenVideo, resumeFullscreenVideo, pauseModalVideo, resumeModalVideo, } from '../../actions/playActions'; import { clearProductDetail, getProductOptionId } from '../../actions/productActions'; import { clearAllToasts } from '../../actions/toastActions'; import TBody from '../../components/TBody/TBody'; import TPanel from '../../components/TPanel/TPanel'; import { panel_names } from '../../utils/Config'; import fp from '../../utils/fp'; import { SpotlightIds } from '../../utils/SpotlightIds'; import DetailPanelBackground from './components/DetailPanelBackground'; import THeaderCustom from './components/THeaderCustom'; import css from './DetailPanel.module.less'; import ProductAllSection from './ProductAllSection/ProductAllSection'; import ThemeItemListOverlay from './ThemeItemListOverlay/ThemeItemListOverlay'; export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { const dispatch = useDispatch(); const productData = useSelector((state) => state.main.productData); const youmaylikeData = useSelector((state) => state.main.youmaylikeData); const themeProductInfos = useSelector((state) => state.home.themeCurationDetailInfoData); const isLoading = useSelector((state) => fp.pipe(() => state, fp.get('common.appStatus.showLoadingPanel.show'))() ); const themeData = useSelector((state) => fp.pipe( () => state, fp.get('home.productData.themeInfo'), (list) => list && list[0] )() ); const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion); const panels = useSelector((state) => state.panels.panels); const [selectedIndex, setSelectedIndex] = useState(0); const [lgCatCd, setLgCatCd] = useState(''); const [themeProductInfo, setThemeProductInfo] = useState(null); const containerRef = useRef(null); const panelType = useMemo(() => fp.pipe(() => panelInfo, fp.get('type'))(), [panelInfo]); const panelCurationId = useMemo( () => fp.pipe(() => panelInfo, fp.get('curationId'))(), [panelInfo] ); const panelPatnrId = useMemo(() => fp.pipe(() => panelInfo, fp.get('patnrId'))(), [panelInfo]); const panelPrdtId = useMemo(() => fp.pipe(() => panelInfo, fp.get('prdtId'))(), [panelInfo]); const panelLiveReqFlag = useMemo( () => fp.pipe(() => panelInfo, fp.get('liveReqFlag'))(), [panelInfo] ); const panelBgImgNo = useMemo(() => fp.pipe(() => panelInfo, fp.get('bgImgNo'))(), [panelInfo]); const panelLaunchedFromPlayer = useMemo( () => fp.pipe(() => panelInfo, fp.get('launchedFromPlayer'))(), [panelInfo] ); const panelLaunchedFromUserReviewPanel = useMemo( () => fp.pipe(() => panelInfo, fp.get('launchedFromUserReviewPanel'), fp.defaultTo(false))(), [panelInfo] ); const panelBgVideoInfo = useMemo( () => fp.pipe(() => panelInfo, fp.get('bgVideoInfo'), fp.defaultTo(null))(), [panelInfo] ); const hasThemeContents = useMemo( () => fp.pipe( () => ({ panelType, themeData }), ({ panelType, themeData }) => panelType === 'theme' && fp.pipe( () => themeData, fp.get('productInfos'), (list) => Array.isArray(list) && list.length > 0 )() )(), [panelType, themeData] ); const themeProducts = useMemo( () => fp.pipe( () => themeData, fp.get('productInfos'), (list) => (Array.isArray(list) ? list : []) )(), [themeData] ); const panelShouldReload = useMemo( () => fp.pipe(() => panelInfo, fp.get('shouldReload'), fp.defaultTo(false))(), [panelInfo] ); const productPmtSuptYn = useMemo( () => fp.pipe(() => productData, fp.get('pmtSuptYn'))(), [productData] ); const productGrPrdtProcYn = useMemo( () => fp.pipe(() => productData, fp.get('grPrdtProcYn'))(), [productData] ); const productDataSource = useMemo( () => fp.pipe( () => panelType, (type) => (type === 'theme' ? themeData : productData) )(), [panelType, themeData, productData] ); const [productType, setProductType] = useState(null); const [openThemeItemOverlay, setOpenThemeItemOverlay] = useState(false); const [scrollToSection, setScrollToSection] = useState(null); const [pendingScrollSection, setPendingScrollSection] = useState(null); const updateSelectedIndex = useCallback((newIndex) => { setSelectedIndex( fp.pipe( () => newIndex, (index) => Math.max(0, Math.min(index, 999)) // 범위 제한 )() ); }, []); const updateThemeItemOverlay = useCallback((isOpen) => { setOpenThemeItemOverlay(fp.pipe(() => isOpen, Boolean)()); }, []); const onSpotlightUpTButton = useCallback((e) => { e.stopPropagation(); Spotlight.focus('spotlightId_backBtn'); }, []); useEffect(() => { return () => { dispatch(finishModalMediaForce()); }; }, [dispatch]); // ✅ DetailPanelBackground 이미지 완전 렌더링 후 그라데이션 배경 숨기기 const handleBackgroundImageReady = useCallback(() => { // console.log('[TRACE-GRADIENT] ✅ DetailPanel - BackgroundImage fully rendered, hiding gradient'); dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { showGradientBackground: false, }, }) ); }, [dispatch]); // ✅ [251120] DetailPanel이 사라질 때 처리 - sourcePanel에 따라 switch 문으로 처리 useEffect(() => { return () => { const sourcePanel = panelInfo?.sourcePanel; const sourceMenu = panelInfo?.sourceMenu; // DetailPanel이 unmount되는 시점 console.log('[DetailPanel] unmount:', { sourcePanel, sourceMenu, timestamp: Date.now(), }); // sourcePanel에 따른 상태 업데이트 switch (sourcePanel) { case panel_names.PLAYER_PANEL: { // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달'); dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달 }, }) ); break; } case panel_names.HOME_PANEL: { // HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달 // console.log('[DetailPanel] unmount - HomePanel에 detailPanelClosed flag 전달'); // console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - HomePanel 복귀'); dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기 }, }) ); break; } case panel_names.SEARCH_PANEL: { // SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달 console.log('[DetailPanel] unmount - SearchPanel에 detailPanelClosed flag 전달'); dispatch( updatePanel({ name: panel_names.SEARCH_PANEL, panelInfo: { detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 }, }) ); break; } default: console.warn('[DetailPanel] unmount - 처리되지 않은 sourcePanel:', sourcePanel); break; } }; }, [dispatch, panelInfo?.sourcePanel]); const onBackClick = useCallback( (isCancelClick) => (ev) => { const sourcePanel = panelInfo?.sourcePanel; const sourceMenu = panelInfo?.sourceMenu; fp.pipe( () => { dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 // sourcePanel에 따른 사전 처리 switch (sourcePanel) { case panel_names.PLAYER_PANEL: // PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리 console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행'); dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishVideoPreview()); break; case panel_names.HOME_PANEL: case panel_names.SEARCH_PANEL: default: // HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지 console.log( '[DetailPanel] onBackClick - source panel:', sourcePanel, '백그라운드 비디오 일시 중지' ); dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishVideoPreview()); break; } dispatch(popPanel(panel_names.DETAIL_PANEL)); }, () => { // sourcePanel에 따른 상태 업데이트 switch (sourcePanel) { case panel_names.PLAYER_PANEL: { // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 const shouldUpdatePanel = fp.pipe( () => panels, fp.get('length'), (length) => length === 3 // PlayerPanel이 [1]에 있고 DetailPanel이 [2]에 있는 상태 )() && fp.pipe( () => panels, fp.get('1.name'), (name) => name === panel_names.PLAYER_PANEL )(); if (shouldUpdatePanel) { console.log( '[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달' ); dispatch( updatePanel({ name: panel_names.PLAYER_PANEL, panelInfo: { thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(), detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달 }, }) ); } break; } case panel_names.HOME_PANEL: { // HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달 console.log('[DetailPanel] onBackClick - HomePanel에 detailPanelClosed flag 전달'); dispatch( updateHomeInfo({ name: panel_names.HOME_PANEL, panelInfo: { detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 showGradientBackground: false, }, }) ); break; } case panel_names.SEARCH_PANEL: { // SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달 console.log('[DetailPanel] onBackClick - SearchPanel에 detailPanelClosed flag 전달'); dispatch( updatePanel({ name: panel_names.SEARCH_PANEL, panelInfo: { detailPanelClosed: true, // ✅ flag detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedFromSource: sourceMenu, // ✅ 출처 }, }) ); break; } default: console.warn('[DetailPanel] onBackClick - 처리되지 않은 sourcePanel:', sourcePanel); break; } } )(); if (isCancelClick) { ev.stopPropagation(); } }, [dispatch, panelInfo, panels] ); const onBackButtonFocus = useCallback(() => { dispatch(clearAllToasts()); }, [dispatch]); const handleScrollToSection = useCallback( (sectionId) => { console.log('DetailPanel: handleScrollToSection called with:', sectionId); console.log('DetailPanel: scrollToSection function:', scrollToSection); const scrollAction = fp.pipe( () => ({ scrollToSection, sectionId }), ({ scrollToSection, sectionId }) => { if (fp.isNotNil(scrollToSection)) { return { action: 'execute', scrollFunction: scrollToSection, sectionId, }; } else { return { action: 'store', sectionId }; } } )(); // 액션에 따른 처리 if (scrollAction.action === 'execute') { scrollAction.scrollFunction(scrollAction.sectionId); } else { console.log('DetailPanel: scrollToSection function is null, storing pending scroll'); setPendingScrollSection(scrollAction.sectionId); } }, [scrollToSection] ); // ===== 배경 이미지 설정 (컴포넌트로 구현되어 useEffect 불필요) ===== // DetailPanelBackground 컴포넌트로 배경 렌더링 useEffect(() => { const shouldExecutePendingScroll = fp.pipe( () => ({ scrollToSection, pendingScrollSection }), ({ scrollToSection, pendingScrollSection }) => fp.isNotNil(scrollToSection) && fp.isNotNil(pendingScrollSection) )(); if (shouldExecutePendingScroll) { console.log('DetailPanel: executing pending scroll to:', pendingScrollSection); // 메모리 누수 방지를 위한 cleanup 함수 const timeoutId = setTimeout(() => { if (scrollToSection) { scrollToSection(pendingScrollSection); } setPendingScrollSection(null); }, 100); // cleanup 함수 반환으로 메모리 누수 방지 return () => { clearTimeout(timeoutId); }; } }, [scrollToSection, pendingScrollSection]); useEffect(() => { const loadInitialData = fp.pipe( () => { // 기본 액션 디스패치 dispatch(getProductOptionId(undefined)); dispatch(getDeviceAdditionInfo()); }, () => { // 테마 데이터 로딩 const isThemeType = panelType === 'theme'; if (isThemeType) { dispatch( getThemeCurationDetailInfo({ patnrId: panelPatnrId, curationId: panelCurationId, bgImgNo: panelBgImgNo, }) ); } }, () => { // 일반 상품 데이터 로딩 const hasProductId = fp.isNotNil(panelPrdtId); const hasNoCuration = fp.isNil(panelCurationId); if (hasProductId && hasNoCuration) { dispatch( getMainCategoryDetail({ patnrId: panelPatnrId, prdtId: panelPrdtId, liveReqFlag: panelLiveReqFlag || 'N', }) ); } } )(); // cleanup 함수로 메모리 누수 방지 return () => { // 필요한 경우 cleanup 로직 추가 }; }, [ dispatch, panelLiveReqFlag, panelCurationId, panelPrdtId, panelType, panelPatnrId, panelBgImgNo, ]); useEffect(() => { const shouldLoadRecommendations = fp.pipe(() => lgCatCd, fp.isNotEmpty)(); if (shouldLoadRecommendations) { const youMayLikeParams = { lgCatCd: lgCatCd, exclCurationId: panelInfo?.curationId, exclPatnrId: panelInfo?.patnrId, exclPrdtId: panelInfo?.prdtId, catDpTh3: panelInfo?.type === 'theme' ? themeProductInfos[selectedIndex]?.catDpTh3 : productData?.catDpTh3, catDpTh4: panelInfo?.type === 'theme' ? themeProductInfos[selectedIndex]?.catDpTh4 : productData?.catDpTh4, }; // console.log('[YouMayLike]-youmaylikeData 요청 파라미터:', youMayLikeParams); dispatch(getMainYouMayLike(youMayLikeParams)); } }, [panelInfo?.curationId, panelInfo?.patnrId, panelInfo?.prdtId, lgCatCd]); const getlgCatCd = useCallback(() => { // DetailPanel.backup.jsx와 완전히 동일한 로직 if (productData && !panelInfo?.curationId) { // console.log('[YouMayLike] lgCatCd 설정 (일반상품):', productData.catCd); setLgCatCd(productData.catCd); } else if ( themeProductInfos && themeProductInfos[selectedIndex]?.pmtSuptYn === 'N' && panelInfo?.curationId ) { const themeCatCd = themeProductInfos[selectedIndex]?.catCd; // console.log('[YouMayLike] lgCatCd 설정 (테마상품):', themeCatCd); setLgCatCd(themeCatCd); } else { // console.log('[YouMayLike] lgCatCd 설정 (빈값):', { // hasProductData: !!productData, // panelCurationId: panelInfo?.curationId, // hasThemeProductInfos: !!themeProductInfos, // selectedIndex, // themeProductPmtSuptYn: themeProductInfos?.[selectedIndex]?.pmtSuptYn // }); setLgCatCd(''); } }, [productData, themeProductInfos, selectedIndex, panelInfo?.curationId]); // 카테고리 코드 업데이트 - DetailPanel.backup.jsx와 동일한 의존성 useEffect(() => { getlgCatCd(); }, [themeProductInfos, productData, panelInfo, selectedIndex, getlgCatCd]); // lgCatCd 변경 추적 로그 // useEffect(() => { // console.log('[YouMayLike] lgCatCd 변경됨:', { // lgCatCd, // willTriggerYouMayLike: !!lgCatCd // }); // }, [lgCatCd]); // youmaylikeData 변경 추적 로그 // useEffect(() => { // console.log('[YouMayLike] DetailPanel - youmaylikeData 변경됨:', { // youmaylikeData, // hasData: !!(youmaylikeData && youmaylikeData.length > 0), // dataLength: youmaylikeData?.length || 0 // }); // }, [youmaylikeData]); // 최근 본 상품 저장이 필요하면: // - 순수 유틸로 빌드/업서트 함수 작성 후, 적절한 useEffect에서 호출하세요. // 예) saveRecentItem(panelInfo, selectedIndex) useEffect(() => { return () => { fp.pipe( () => { dispatch(clearProductDetail()); }, () => { setContainerLastFocusedElement(null, ['indicator-GridListContainer']); } )(); }; }, [dispatch]); // CheckOutPanel에서 돌아올 때 DetailPanel 재시작 useEffect(() => { if (panelShouldReload) { console.log('[DetailPanel] Reloading panel data...'); // 기존 데이터 초기화 dispatch(clearProductDetail()); // 데이터 다시 로딩 const isThemeType = panelType === 'theme'; if (isThemeType && panelCurationId) { dispatch( getThemeCurationDetailInfo({ patnrId: panelPatnrId, curationId: panelCurationId, bgImgNo: panelBgImgNo, }) ); } else if (panelPrdtId && !panelCurationId) { dispatch( getMainCategoryDetail({ patnrId: panelPatnrId, prdtId: panelPrdtId, liveReqFlag: panelLiveReqFlag || 'N', }) ); } // 재시작 플래그 제거 dispatch( updatePanel({ name: panel_names.DETAIL_PANEL, panelInfo: { shouldReload: false }, }) ); console.log('[DetailPanel] Reload complete'); } }, [ panelShouldReload, dispatch, panelType, panelPatnrId, panelCurationId, panelBgImgNo, panelPrdtId, panelLiveReqFlag, ]); // 최근 본 상품 트리거 예시: // useEffect(() => { // if (panelInfo && panelInfo.patnrId && panelInfo.prdtId) { // // saveRecentItem(panelInfo, selectedIndex) // } // }, [panelInfo, selectedIndex]) const versionComparators = useMemo( () => ({ isVersionGTE: fp.curry((target, version) => version >= target), isVersionLT: fp.curry((target, version) => version < target), }), [] ); const conditionCheckers = useMemo( () => ({ hasDataAndCondition: fp.curry((conditionFn, data) => fp.isNotNil(data) && conditionFn(data)), equalTo: fp.curry((expected, actual) => actual === expected), checkAllConditions: fp.curry((conditions, data) => fp.reduce( (acc, condition) => acc && condition, true, conditions.map((fn) => fn(data)) ) ), }), [] ); const getProductType = useCallback(() => { const createTypeChecker = fp.curry((type, conditions, sideEffect) => fp.pipe( () => conditions(), (isValid) => isValid ? (() => { sideEffect && sideEffect(); return { matched: true, type }; })() : { matched: false } )() ); const productTypeRules = [ // 테마 타입 체크 () => createTypeChecker( 'theme', () => fp.pipe( () => ({ panelCurationId, themeData }), ({ panelCurationId, themeData }) => fp.isNotNil(panelCurationId) && fp.isNotNil(themeData) )(), () => { const themeProduct = fp.pipe( () => themeData, fp.get('productInfos'), fp.get(selectedIndex.toString()) )(); setProductType('theme'); setThemeProductInfo(themeProduct); } ), // Buy Now 타입 체크 (curry 활용) () => createTypeChecker( 'buyNow', () => fp.pipe( () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion, }), ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion, }) => { const conditions = [ () => fp.isNotNil(productData), () => conditionCheckers.equalTo('Y')(productPmtSuptYn), () => conditionCheckers.equalTo('N')(productGrPrdtProcYn), () => fp.isNotNil(panelPrdtId), () => versionComparators.isVersionGTE('6.0')(webOSVersion), ]; return conditionCheckers.checkAllConditions(conditions)({}); } )(), () => setProductType('buyNow') ), // Shop By Mobile 타입 체크 (curry 활용) () => createTypeChecker( 'shopByMobile', () => fp.pipe( () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion, }), ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion, }) => { if (!productData) return false; const isDirectMobile = conditionCheckers.equalTo('N')(productPmtSuptYn); const conditionalMobileConditions = [ () => conditionCheckers.equalTo('Y')(productPmtSuptYn), () => conditionCheckers.equalTo('N')(productGrPrdtProcYn), () => versionComparators.isVersionLT('6.0')(webOSVersion), () => fp.isNotNil(panelPrdtId), ]; const isConditionalMobile = conditionCheckers.checkAllConditions( conditionalMobileConditions )({}); return isDirectMobile || isConditionalMobile; } )(), () => setProductType('shopByMobile') ), ]; const matchedRule = fp.reduce( (result, rule) => (result.matched ? result : rule()), { matched: false }, productTypeRules ); // 매칭되지 않은 경우 디버깅 정보 출력 if (!matchedRule.matched) { const debugInfo = fp.pipe( () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion, }), ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => ({ pmtSuptYn: productPmtSuptYn, grPrdtProcYn: productGrPrdtProcYn, prdtId: panelPrdtId, webOSVersion, }) )(); console.warn('Unknown product type:', productData); console.warn('Product data properties:', debugInfo); } }, [ panelCurationId, themeData, productPmtSuptYn, productGrPrdtProcYn, panelPrdtId, webOSVersion, selectedIndex, versionComparators, conditionCheckers, ]); useEffect(() => { // productData가 로드된 후에만 getProductType 실행 if (productData || (panelType === 'theme' && themeData)) { getProductType(); } }, [getProductType, productData, themeData, panelType]); // Theme 콘텐츠 진입 시 전달된 themePrdtId를 초기 선택으로 반영 useEffect(() => { if (panelType !== 'theme') return; if (Array.isArray(themeProducts) && panelInfo?.themePrdtId) { const matchedIndex = themeProducts.findIndex( (item) => item?.prdtId === panelInfo.themePrdtId ); if (matchedIndex >= 0) { setSelectedIndex(matchedIndex); } } }, [panelType, themeProducts, panelInfo?.themePrdtId, setSelectedIndex]); // themeProductInfo 업데이트 - selectedIndex 변경 시마다 실행 useEffect(() => { if (themeData?.productInfos && selectedIndex !== undefined) { const themeProduct = themeData.productInfos[selectedIndex]; setThemeProductInfo(themeProduct); } }, [themeData, selectedIndex]); // 타이틀과 aria-label 메모이제이션 (성능 최적화) const headerTitle = useMemo( () => fp.pipe( () => ({ panelPrdtId, productData, panelType, themeData }), ({ panelPrdtId, productData, panelType, themeData }) => { const productTitle = fp.pipe( () => ({ panelPrdtId, productData }), ({ panelPrdtId, productData }) => fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)() ? fp.pipe(() => productData, fp.get('prdtNm'))() : null )(); const themeTitle = fp.pipe( () => ({ panelType, themeData }), ({ panelType, themeData }) => panelType === 'theme' && fp.pipe(() => themeData, fp.get('curationNm'), fp.isNotNil)() ? fp.pipe(() => themeData, fp.get('curationNm'))() : null )(); return productTitle || themeTitle || ''; } )(), [panelPrdtId, productData, panelType, themeData] ); const ariaLabel = useMemo( () => fp.pipe( () => ({ panelPrdtId, productData }), ({ panelPrdtId, productData }) => fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)() ? fp.pipe(() => productData, fp.get('prdtNm'))() : '' )(), [panelPrdtId, productData] ); const handleProductAllSectionReady = useCallback(() => { console.log('############## ShopByMobile focus'); const spotTime = setTimeout(() => { // Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE); Spotlight.focus(SpotlightIds.DETAIL_PRODUCTVIDEO); }, 100); return () => { clearTimeout(spotTime); }; }, []); // 백그라운드 전체화면 비디오 제어: DetailPanel 진입/퇴장 시 useEffect(() => { // PlayerPanel이 존재하는지 확인 (Modal 또는 Fullscreen) const playerPanel = panels.find((panel) => panel.name === panel_names.PLAYER_PANEL); const hasPlayerPanel = !!playerPanel; const isModal = playerPanel?.panelInfo?.modal; // ProductAllSection에 비디오가 있는지 확인 const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)(); console.log('[BgVideo] DetailPanel - Video Control Check:', { hasPlayerPanel, isModal, hasProductVideo, sourceMenu: panelInfo?.sourceMenu, }); // PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤 if (hasPlayerPanel && hasProductVideo) { console.log('[BgVideo] DetailPanel - Pausing video'); if (isModal) { dispatch(pauseModalVideo()); } else { dispatch(pauseFullscreenVideo()); } } else { console.log('[BgVideo] DetailPanel - Skipping pause'); } return () => { // DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개 if (hasPlayerPanel && hasProductVideo) { console.log('[BgVideo] DetailPanel - Resuming video'); if (isModal) { dispatch(resumeModalVideo()); } else { dispatch(resumeFullscreenVideo()); } } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [panelInfo?.sourceMenu, productData?.prdtMediaUrl]); // MediaPanel modal 상태 변화 감지 -> ProductVideo로 포커스 이동 useEffect(() => { const topPanel = panels[panels.length - 1]; // MediaPanel이 modal=true로 복귀했을 때 포커스를 ProductVideo로 이동 // 하지만 MediaPanel에서 이미 포커스를 시도하므로 여기서는 보조 역할만 함 if ( topPanel && topPanel.name === panel_names.MEDIA_PANEL && topPanel.panelInfo.modal === true ) { console.log('[DetailPanel] MediaPanel modal=true detected - will not interfere with focus'); // MediaPanel의 포커스 이동을 방해하지 않기 위해 아무것도 하지 않음 return; } }, [panels]); // PlayerPanel이 modal=true인 경우 비디오 미리보기 중지 useEffect(() => { const hasPlayerPanel = panels.some( (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true ); if (hasPlayerPanel) { console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview'); dispatch(finishVideoPreview()); } }, [panels, dispatch]); return (