Files
shoptime/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx

831 lines
25 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 Spinner from '@enact/sandstone/Spinner';
import DetailPanelSkeleton from './DetailPanelSkeleton/DetailPanelSkeleton';
import Spotlight from '@enact/spotlight';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import detailPanelBg
from '../../../assets/images/detailpanel/detailpanel-bg-1.png';
import indicatorDefaultImage
from '../../../assets/images/img-thumb-empty-144@3x.png';
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
import { getThemeCurationDetailInfo } from '../../actions/homeActions';
import {
getMainCategoryDetail,
getMainYouMayLike,
} from '../../actions/mainActions';
import {
popPanel,
updatePanel,
} from '../../actions/panelActions';
import { finishVideoPreview } from '../../actions/playActions';
import {
clearProductDetail,
getProductOptionId,
} from '../../actions/productActions';
import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel';
import { panel_names } from '../../utils/Config';
import fp from '../../utils/fp';
import {
$L,
getQRCodeUrl,
} from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
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 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);
// FP 방식으로 상태 관리
const [selectedIndex, setSelectedIndex] = useState(0);
const localRecentItems = useSelector((state) =>
fp.pipe(() => state, fp.get("localSettings.recentItems"))()
);
const { httpHeader } = useSelector((state) => state.common);
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
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 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);
// FP 방식으로 스크롤 상태 관리
const [scrollToSection, setScrollToSection] = useState(null);
const [pendingScrollSection, setPendingScrollSection] = useState(null);
// FP 방식으로 상태 업데이트 함수들
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)());
}, []);
// FP 방식으로 이벤트 핸들러 정의
const onSpotlightUpTButton = useCallback((e) => {
e.stopPropagation();
Spotlight.focus("spotlightId_backBtn");
}, []);
const onClick = useCallback(
(isCancelClick) => (ev) => {
// FP 방식으로 액션 디스패치 체이닝
fp.pipe(
() => {
dispatch(finishVideoPreview());
dispatch(popPanel(panel_names.DETAIL_PANEL));
},
() => {
// 패널 업데이트 조건 체크
const shouldUpdatePanel =
fp.pipe(
() => panels,
fp.get("length"),
(length) => length === 4
)() &&
fp.pipe(
() => panels,
fp.get("1.name"),
(name) => name === panel_names.PLAYER_PANEL
)();
if (shouldUpdatePanel) {
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
thumbnail: fp.pipe(() => panelInfo, fp.get("thumbnailUrl"))(),
},
})
);
}
}
)();
if (isCancelClick) {
ev.stopPropagation();
}
},
[dispatch, panelInfo, panels]
);
// FP 방식으로 스크롤 함수 핸들러
const handleScrollToSection = useCallback(
(sectionId) => {
console.log("DetailPanel: handleScrollToSection called with:", sectionId);
console.log("DetailPanel: scrollToSection function:", scrollToSection);
// FP 방식으로 스크롤 처리 로직
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]
);
// ===== 고정 배경 이미지 설정 (detailPanelBg만 사용) =====
// 모든 DetailPanel에서 동일한 배경 이미지(detailpanel-bg-1.png) 사용
useEffect(() => {
console.log("[PartnerId] Partner background info:", {
panelPatnrId: panelPatnrId,
productDataPatnrId: productData?.patnrId,
productDataPatncNm: productData?.patncNm,
productDataThumbnailUrl960: productData?.thumbnailUrl960,
imageUrl: imageUrl,
detailPanelBg: detailPanelBg,
});
// 고정 배경 이미지만 설정 (파트너사별 변경 없이)
document.documentElement.style.setProperty(
"--bg-url",
`url(${detailPanelBg})`
);
}, [panelPatnrId, productData, imageUrl]);
// FP 방식으로 pending scroll 처리 (메모리 누수 방지)
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]);
// FP 방식으로 초기 데이터 로딩 처리 (메모리 누수 방지)
useEffect(() => {
// FP 방식으로 액션 디스패치 체이닝
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,
]);
// FP 방식으로 추천 상품 데이터 로딩 (메모리 누수 방지)
useEffect(() => {
const shouldLoadRecommendations = fp.pipe(() => lgCatCd, fp.isNotEmpty)();
if (shouldLoadRecommendations) {
dispatch(
getMainYouMayLike({
lgCatCd: lgCatCd,
exclCurationId: panelCurationId,
exclPatnrId: panelPatnrId,
exclPrdtId: panelPrdtId,
})
);
}
}, [panelCurationId, panelPatnrId, panelPrdtId, lgCatCd]);
// FP 방식으로 카테고리 규칙 헬퍼 함수들 (curry 적용)
const categoryHelpers = useMemo(
() => ({
createCategoryRule: fp.curry((conditionFn, extractFn, data) =>
conditionFn(data) ? extractFn(data) : null
),
hasProductWithoutCuration: fp.curry(
(panelCurationId, productData) =>
fp.isNotNil(productData) && fp.isNil(panelCurationId)
),
hasThemeWithPaymentCondition: fp.curry(
(panelCurationId, themeProductInfo) => {
const hasThemeProduct = fp.isNotNil(themeProductInfo);
const equalToN = fp.curry((expected, actual) => actual === expected)(
"N"
);
const isNoPayment = equalToN(
fp.pipe(() => themeProductInfo, fp.get("pmtSuptYn"))()
);
const hasCuration = fp.isNotNil(panelCurationId);
return hasThemeProduct && isNoPayment && hasCuration;
}
),
}),
[]
);
const getlgCatCd = useCallback(() => {
// FP 방식으로 카테고리 코드 결정 - curry 적용으로 더 함수형 개선
const categoryRules = [
// 일반 상품 규칙 (curry 활용)
() =>
categoryHelpers.createCategoryRule(
categoryHelpers.hasProductWithoutCuration(panelCurationId),
(data) => fp.pipe(() => data, fp.get("catCd"))(),
productData
),
// 테마 상품 규칙 (curry 활용)
() =>
categoryHelpers.createCategoryRule(
categoryHelpers.hasThemeWithPaymentCondition(panelCurationId),
(data) => fp.pipe(() => data, fp.get("catCd"))(),
themeProductInfo
),
];
// 첫 번째로 매칭되는 규칙의 결과 사용 (curry의 reduce 활용)
const categoryCode = fp.reduce(
(result, value) => result || value || "",
"",
categoryRules.map((rule) => rule())
);
setLgCatCd(categoryCode);
}, [
productData,
selectedIndex,
panelCurationId,
themeProductInfo,
categoryHelpers,
]);
// FP 방식으로 카테고리 코드 업데이트 (메모리 누수 방지)
useEffect(() => {
const shouldUpdateCategory = fp.pipe(
() => ({ themeProductInfo, productData, panelInfo, selectedIndex }),
({ themeProductInfo, productData, panelInfo, selectedIndex }) =>
fp.isNotNil(themeProductInfo) ||
fp.isNotNil(productData) ||
fp.isNotNil(panelInfo)
)();
if (shouldUpdateCategory) {
getlgCatCd();
}
}, [themeProductInfo, productData, panelInfo, selectedIndex]);
// 최근 본 상품 저장이 필요하면:
// - 순수 유틸로 빌드/업서트 함수 작성 후, 적절한 useEffect에서 호출하세요.
// 예) saveRecentItem(panelInfo, selectedIndex)
// FP 방식으로 cleanup 처리 (메모리 누수 방지)
useEffect(() => {
return () => {
// FP 방식으로 cleanup 액션 실행
fp.pipe(
() => {
dispatch(clearProductDetail());
},
() => {
setContainerLastFocusedElement(null, ["indicator-GridListContainer"]);
}
)();
};
}, [dispatch]);
// 최근 본 상품 트리거 예시:
// useEffect(() => {
// if (panelInfo && panelInfo.patnrId && panelInfo.prdtId) {
// // saveRecentItem(panelInfo, selectedIndex)
// }
// }, [panelInfo, selectedIndex])
// 테마/호텔 기반 인덱스 초기화가 필요하면:
// - findIndex 유틸을 만들어 매칭 인덱스를 계산 후 setSelectedIndex에 반영하세요.
// FP 방식으로 버전 비교 헬퍼 함수 (curry 적용)
const versionComparators = useMemo(
() => ({
isVersionGTE: fp.curry((target, version) => version >= target),
isVersionLT: fp.curry((target, version) => version < target),
}),
[]
);
// FP 방식으로 조건 체크 헬퍼 함수들 (curry 적용)
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(() => {
// FP 방식으로 데이터 검증 및 타입 결정 - curry 적용으로 더 함수형 개선
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")
),
];
// FP 방식으로 순차적 타입 체크
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]);
const imageUrl = useMemo(
() => fp.pipe(() => productData, fp.get("thumbnailUrl960"))(),
[productData]
);
// FP 방식으로 타이틀과 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]
);
// ===== 파트너사별 배경 이미지 설정 로직 (현재 비활성화) =====
// thumbnailUrl960을 사용하여 파트너사별로 다른 배경 이미지를 설정하는 기능
// Pink Pong 등 특정 파트너사에서만 thumbnailUrl960 데이터가 있어서 배경이 변경됨
// 현재는 고정 배경(detailPanelBg)만 사용하기 위해 주석 처리
// FP 방식으로 배경 이미지 설정 (메모리 누수 방지)
/*
useLayoutEffect(() => {
const shouldSetBackground = fp.pipe(
() => ({ imageUrl, containerRef }),
({ imageUrl, containerRef }) =>
fp.isNotNil(imageUrl) && fp.isNotNil(containerRef.current)
)();
if (shouldSetBackground) {
containerRef.current.style.setProperty("--bg-url", `url('${imageUrl}')`);
}
}, [imageUrl]);
*/
console.log("productDataSource :", productDataSource);
// 언마운트 시 인덱스 초기화가 필요하면:
// useEffect(() => () => setSelectedIndex(0), [])
const handleProductAllSectionReady = useCallback(() => {
const spotTime = setTimeout(() => {
Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE);
}, 100);
return () => {
clearTimeout(spotTime);
};
}, []);
return (
<div ref={containerRef}>
<TPanel
isTabActivated={false}
className={css.detailPanelWrap}
handleCancel={onClick(true)}
spotlightId={spotlightId}
>
<THeaderCustom
className={css.header}
title={headerTitle}
onBackButton
onClick={onClick(false)}
spotlightDisabled={isLoading}
onSpotlightUp={onSpotlightUpTButton}
onSpotlightLeft={onSpotlightUpTButton}
marqueeDisabled={false}
ariaLabel={ariaLabel}
/>
<TBody
className={css.tbody}
scrollable={false}
spotlightDisabled={isLoading}
isDefaultContainer
>
{useMemo(() => {
// FP 방식으로 렌더링 조건 결정 (메모이제이션으로 최적화)
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}
selectedIndex={selectedIndex}
selectedPatnrId={panelPatnrId}
selectedPrdtId={panelPrdtId}
setSelectedIndex={updateSelectedIndex}
openThemeItemOverlay={openThemeItemOverlay}
setOpenThemeItemOverlay={updateThemeItemOverlay}
themeProductInfo={themeProductInfo}
onReady={handleProductAllSectionReady}
isOnRender={renderStates.canRender}
/>
);
}
if (renderStates.showLoading) {
return <DetailPanelSkeleton />;
}
return null;
}, [
isLoading,
panelInfo,
productDataSource,
productType,
selectedIndex,
panelPatnrId,
panelPrdtId,
updateSelectedIndex,
openThemeItemOverlay,
updateThemeItemOverlay,
themeProductInfo,
])}
</TBody>
<ThemeItemListOverlay
productInfo={productDataSource}
isOpen={openThemeItemOverlay}
panelInfo={panelInfo}
productType={productType}
setSelectedIndex={updateSelectedIndex}
openThemeItemOverlay={openThemeItemOverlay}
setOpenThemeItemOverlay={updateThemeItemOverlay}
/>
</TPanel>
</div>
);
}