Files
shoptime/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
optrader 61f67708a9 [251216] fix: 로그정리,PlayerPanel ShopNowContents
🕐 커밋 시간: 2025. 12. 16. 16:07:17

📊 변경 통계:
  • 총 파일: 11개
  • 추가: +94줄
  • 삭제: -90줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/actions/playActions.js
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/utils/lodashFpEx.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선
2025-12-16 16:07:19 +09:00

1311 lines
44 KiB
JavaScript

// 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,
pauseModalVideo,
resumeFullscreenVideo,
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';
// =======
// changeAppStatus,
// changeLocalSettings,
// setHidePopup,
// } from "../../actions/commonActions";
// import { clearCouponInfo } from "../../actions/couponActions";
// import { getDeviceAdditionInfo } from "../../actions/deviceActions";
// import {
// clearThemeDetail,
// getThemeCurationDetailInfo,
// getThemeHotelDetailInfo,
// } from "../../actions/homeActions";
// import {
// getMainCategoryDetail,
// getMainYouMayLike,
// } from "../../actions/mainActions";
// import { popPanel, updatePanel } from "../../actions/panelActions";
// import { finishVideoPreview } from "../../actions/playActions";
// import {
// clearProductDetail,
// getProductGroup,
// getProductImageLength,
// getProductOptionId,
// } from "../../actions/productActions";
// import MobileSendPopUp from "../../components/MobileSend/MobileSendPopUp";
// import TBody from "../../components/TBody/TBody";
// import THeader from "../../components/THeader/THeader";
// import TPanel from "../../components/TPanel/TPanel";
// import * as Config from "../../utils/Config";
// import { panel_names } from "../../utils/Config";
// import { $L, getQRCodeUrl } from "../../utils/helperMethods";
// import css from "./DetailPanel.module.less";
// import GroupProduct from "./GroupProduct/GroupProduct";
// import SingleProduct from "./SingleProduct/SingleProduct";
// import ThemeProduct from "./ThemeProduct/ThemeProduct";
// import UnableProduct from "./UnableProduct/UnableProduct";
// import YouMayLike from "./YouMayLike/YouMayLike";
// import { now } from "lodash";
// >>>>>>> gitlab/develop
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);
// <<<<<<< HEAD
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)) // 범위 제한
)()
);
}, []);
// =======
// useEffect(() => {
// if (lgCatCd) {
// dispatch(
// getMainYouMayLike({
// 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,
// })
// );
// }
// }, [panelInfo?.curationId, panelInfo?.patnrId, panelInfo?.prdtId, lgCatCd]);
// >>>>>>> gitlab/develop
const updateThemeItemOverlay = useCallback((isOpen) => {
setOpenThemeItemOverlay(fp.pipe(() => isOpen, Boolean)());
}, []);
// <<<<<<< HEAD
const onSpotlightUpTButton = useCallback((e) => {
// =======
// useEffect(() => {
// if (
// themeProductInfos &&
// themeProductInfos.length > 0 &&
// panelInfo?.themePrdtId
// ) {
// for (let i = 0; i < themeProductInfos.length; i++) {
// if (themeProductInfos[i].prdtId === panelInfo?.themePrdtId) {
// setSelectedIndex(i);
// }
// }
// }
// if (hotelInfos && hotelInfos.length > 0 && panelInfo?.themeHotelId) {
// for (let i = 0; i < hotelInfos.length; i++) {
// if (hotelInfos[i].hotelId === panelInfo?.themeHotelId) {
// setSelectedIndex(i);
// }
// }
// }
// }, [
// themeProductInfos,
// hotelInfos,
// panelInfo?.themePrdtId,
// panelInfo?.themeHotelId,
// ]);
// const { detailUrl } = useMemo(() => {
// return getQRCodeUrl({
// serverHOST,
// serverType,
// index: deviceInfo?.dvcIndex,
// patnrId: productInfo?.patnrId,
// prdtId: productInfo?.prdtId,
// entryMenu: entryMenu,
// nowMenu: nowMenu,
// liveFlag: "Y",
// qrType: "billingDetail",
// });
// }, [serverHOST, serverType, deviceInfo, entryMenu, productInfo]);
// const onSpotlightUpTButton = (e) => {
// >>>>>>> gitlab/develop
e.stopPropagation();
Spotlight.focus('spotlightId_backBtn');
}, []);
useEffect(() => {
return () => {
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] DetailPanel cleanup - calling finishModalMediaForce');
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;
// console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel cleanup/unmount triggered', {
// sourcePanel,
// sourceMenu,
// panelsSnapshot: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
// DetailPanel이 unmount되는 시점
// console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel unmount details:', {
// sourcePanel,
// sourceMenu,
// timestamp: Date.now(),
// });
// sourcePanel에 따른 상태 업데이트
switch (sourcePanel) {
case panel_names.PLAYER_PANEL: {
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
console.log('[PANEL][DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
detailPanelClosed: true, // ✅ flag
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달
lockModalFalse: false, // Detail 종료 시 lock 해제
},
})
);
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;
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] onBackClick triggered', {
// sourcePanel,
// sourceMenu,
// isCancelClick,
// currentPanels: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
fp.pipe(
() => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
// sourcePanel에 따른 사전 처리
switch (sourcePanel) {
case panel_names.PLAYER_PANEL:
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
console.log('[🟠UNIQUE_DETAIL_BACK🟠] PlayerPanel 출신: 모달 정리만 수행');
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview());
break;
case panel_names.HOME_PANEL:
case panel_names.SEARCH_PANEL:
default:
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
// console.log(
// '[🟠UNIQUE_DETAIL_BACK🟠] source panel:',
// sourcePanel,
// '백그라운드 비디오 일시 중지'
// );
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview());
break;
}
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] Calling popPanel(DETAIL_PANEL)');
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(
'[PANEL][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 메모이제이션 (성능 최적화 // themeTitle과 haederTitle 분리.)
const headerTitle = useMemo(
() =>
fp.pipe(
() => ({ panelPrdtId, productData }),
({ panelPrdtId, productData }) => {
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
)();
return productTitle || '';
}
)(),
[panelPrdtId, productData]
);
const themeHeaderTitle = useMemo(
() =>
fp.pipe(
() => ({ panelType, themeData }),
({ panelType, themeData }) => {
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 themeTitle || '';
}
)(),
[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('[Detail-BG] 🎬 DetailPanel - Video Control Check (mount/update):', {
hasPlayerPanel,
playerPanelModalStatus: isModal,
hasProductVideo,
sourceMenu: panelInfo?.sourceMenu,
productDataUrl: productData?.prdtMediaUrl,
timestamp: Date.now(),
});
// PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
if (hasPlayerPanel && hasProductVideo) {
console.log('[Detail-BG] ⏸️ DetailPanel - Pausing PlayerPanel video (match: playerPanel + productVideo)', {
isModalVideo: isModal,
action: isModal ? 'pauseModalVideo' : 'pauseFullscreenVideo',
timestamp: Date.now(),
});
if (isModal) {
dispatch(pauseModalVideo());
} else {
dispatch(pauseFullscreenVideo());
}
} else {
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping pause (no playerPanel or no productVideo)', {
hasPlayerPanel,
hasProductVideo,
reason: !hasPlayerPanel ? 'no playerPanel' : 'no productVideo',
timestamp: Date.now(),
});
}
return () => {
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
if (hasPlayerPanel && hasProductVideo) {
console.log('[Detail-BG] ▶️ DetailPanel - Resuming PlayerPanel video (unmount cleanup)', {
isModalVideo: isModal,
action: isModal ? 'resumeModalVideo' : 'resumeFullscreenVideo',
sourceMenu: panelInfo?.sourceMenu,
timestamp: Date.now(),
});
if (isModal) {
dispatch(resumeModalVideo());
} else {
dispatch(resumeFullscreenVideo());
}
} else {
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping resume on unmount', {
hasPlayerPanel,
hasProductVideo,
timestamp: Date.now(),
});
}
};
// 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
);
const launchedFromPlayer = panelInfo?.fromPlayer || panelInfo?.sourcePanel === panel_names.PLAYER_PANEL;
if (hasPlayerPanel && !launchedFromPlayer) {
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview (non-player source)');
dispatch(finishVideoPreview());
} else if (hasPlayerPanel && launchedFromPlayer) {
console.log('[DetailPanel] PlayerPanel modal=true detected - launched from Player, skip finishVideoPreview');
// Detail 동안 modal=true로 바뀌지 않도록 lockModalFalse 설정
const playerPanelEntry = panels.find(
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
);
if (playerPanelEntry?.panelInfo?.modal === true) {
dispatch(
updatePanel({
name: playerPanelEntry.name,
panelInfo: {
...playerPanelEntry.panelInfo,
modal: false,
modalContainerId: undefined,
lockModalFalse: true,
},
})
);
}
}
}, [panels, dispatch, panelInfo?.fromPlayer, panelInfo?.sourcePanel]);
return (
<div ref={containerRef}>
<DetailPanelBackground
launchedFromPlayer={panelLaunchedFromPlayer}
patnrId={panelPatnrId}
onImageReady={handleBackgroundImageReady}
/>
<TPanel
isTabActivated={false}
className={css.detailPanelWrap}
handleCancel={onBackClick(true)}
spotlightId={spotlightId}
>
<THeaderCustom
className={css.header}
prdtId={productData?.prdtId}
title={headerTitle}
themeTitle={themeHeaderTitle}
selectedIndex={selectedIndex}
type={panelInfo?.type === "theme" ? "theme" : null}
onBackButton
onClick={onBackClick(false)}
onBackButtonFocus={onBackButtonFocus}
spotlightDisabled={isLoading}
onSpotlightUp={onSpotlightUpTButton}
onSpotlightLeft={onSpotlightUpTButton}
marqueeDisabled={false}
ariaLabel={ariaLabel}
logoImg={productData?.patncLogoPath ? productData?.patncLogoPath : themeData?.productInfos[0]?.patncLogoPath}
patnrId={panelPatnrId}
themeData={themeData}
/>
<TBody
className={css.tbody}
scrollable={false}
spotlightDisabled={isLoading}
isDefaultContainer
>
{useMemo(() => {
const renderStates = fp.pipe(
() => ({ isLoading, panelInfo, productDataSource, productType }),
({ isLoading, panelInfo, productDataSource, productType }) => {
const hasRequiredData = fp.pipe(
() => [panelInfo, productDataSource, productType],
(data) => fp.reduce((acc, item) => acc && fp.isNotNil(item), true, data)
)();
return {
canRender: !isLoading && hasRequiredData,
showLoading: !isLoading && !hasRequiredData,
showNothing: isLoading,
};
}
)();
if (renderStates.canRender) {
return (
<ProductAllSection
productType={productType}
productInfo={productDataSource}
panelInfo={panelInfo}
hasThemeContents={hasThemeContents}
themeProducts={themeProducts}
selectedIndex={selectedIndex}
selectedPatnrId={panelPatnrId}
selectedPrdtId={panelPrdtId}
setSelectedIndex={updateSelectedIndex}
openThemeItemOverlay={openThemeItemOverlay}
setOpenThemeItemOverlay={updateThemeItemOverlay}
themeProductInfo={themeProductInfo}
onReady={handleProductAllSectionReady}
isOnRender={renderStates.canRender}
launchedFromPlayer={panelLaunchedFromPlayer}
launchedFromUserReviewPanel={panelLaunchedFromUserReviewPanel}
bgVideoInfo={panelBgVideoInfo}
/>
);
}
return null;
}, [
isLoading,
panelInfo,
productDataSource,
productType,
selectedIndex,
panelPatnrId,
panelPrdtId,
updateSelectedIndex,
openThemeItemOverlay,
updateThemeItemOverlay,
themeProductInfo,
])}
</TBody>
<ThemeItemListOverlay
productInfo={productDataSource}
isOpen={openThemeItemOverlay}
panelInfo={panelInfo}
// <<<<<<< HEAD
productType={productType}
setSelectedIndex={updateSelectedIndex}
openThemeItemOverlay={openThemeItemOverlay}
setOpenThemeItemOverlay={updateThemeItemOverlay}
/>
</TPanel>
</div>
// =======
// setSelectedIndex={setSelectedIndex}
// p
// roductInfo={productData || themeProductInfos[selectedIndex]}
// setIsYouMayLikeOpened={setIsYouMayLikeOpened}
// />
// )}
// {activePopup === Config.ACTIVE_POPUP.smsPopup && (
// <MobileSendPopUp
// open={popupVisible}
// onClose={handleSMSonClose}
// title={$L("Send a purchase link for this item via SMS")}
// subTitle={mobileSendPopUpSubtitle}
// patncNm={
// panelInfo?.type === "theme" &&
// themeProductInfos &&
// themeProductInfos[selectedIndex]
// ? themeProductInfos[selectedIndex].patncNm
// : productData?.patncNm
// }
// productImg={mobileSendPopUpProductImg}
// patnrId={panelInfo?.patnrId}
// prdtId={
// panelInfo?.type === "theme" &&
// themeProductInfos &&
// themeProductInfos[selectedIndex]
// ? themeProductInfos[selectedIndex].prdtId
// : panelInfo?.prdtId
// }
// smsTpCd={panelInfo?.type === "hotel" ? "APP00205" : "APP00201"}
// curationId={panelInfo?.curationId}
// curationNm={panelInfo?.curationNm}
// hotelId={
// panelInfo?.type === "hotel" && hotelInfos[selectedIndex]?.hotelId
// }
// hotelNm={
// panelInfo?.type === "hotel" && hotelInfos[selectedIndex]?.hotelNm
// }
// hotelDtlUrl={
// panelInfo?.type === "hotel" &&
// hotelInfos[selectedIndex]?.hotelDetailInfo?.hotelDtlUrl
// }
// productPrice={panelInfo?.type === "hotel" && Price()}
// shopByMobileLogRef={shopByMobileLogRef}
// spotlightId="shopbymobile_Btn"
// smsText={productInfo?.pmtSuptYn === "Y" ? detailUrl : undefined}
// />
// )}
// </>
// >>>>>>> gitlab/develop
);
}