diff --git a/com.twin.app.shoptime/assets/images/icons/ic_arrow_down_3x_new.png b/com.twin.app.shoptime/assets/images/icons/ic_arrow_down_3x_new.png new file mode 100644 index 00000000..a67e947b Binary files /dev/null and b/com.twin.app.shoptime/assets/images/icons/ic_arrow_down_3x_new.png differ diff --git a/com.twin.app.shoptime/src/utils/SpotlightIds.js b/com.twin.app.shoptime/src/utils/SpotlightIds.js index 06b9ea44..2624bcc0 100644 --- a/com.twin.app.shoptime/src/utils/SpotlightIds.js +++ b/com.twin.app.shoptime/src/utils/SpotlightIds.js @@ -54,4 +54,5 @@ export const SpotlightIds = { // detailPanel DETAIL_BUYNOW: "detail_buynow", + DETAIL_SHOPBYMOBILE: "detail_shop_by_mobile", }; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index 72e0a529..bb890e19 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -6,90 +6,122 @@ import React, { useMemo, useRef, useState, -} from "react"; +} from 'react'; -import { useDispatch, useSelector } from "react-redux"; +import { + useDispatch, + useSelector, +} from 'react-redux'; -import Spotlight from "@enact/spotlight"; -import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; - -import { getDeviceAdditionInfo } from "../../actions/deviceActions"; +import Spinner from '@enact/sandstone/Spinner'; +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"; +} 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 THeaderCustom from "./components/THeaderCustom"; -import TPanel from "../../components/TPanel/TPanel"; - -import { panel_names } from "../../utils/Config"; -import { $L, getQRCodeUrl } from "../../utils/helperMethods"; -import fp from "../../utils/fp"; -import css from "./DetailPanel.module.less"; -import ProductAllSection from "./ProductAllSection/ProductAllSection"; -import { getThemeCurationDetailInfo } from "../../actions/homeActions"; -import indicatorDefaultImage from "../../../assets/images/img-thumb-empty-144@3x.png"; -import detailPanelBg from "../../../assets/images/detailpanel/detailpanel-bg-1.png"; -import ThemeItemListOverlay from "./ThemeItemListOverlay/ThemeItemListOverlay"; -import Spinner from "@enact/sandstone/Spinner"; +} 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'))() + 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]) + fp.get("home.productData.themeInfo"), + (list) => list && list[0] )() ); const webOSVersion = useSelector( - (state) => state.common.appStatus.webOSVersion, + (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'))() + fp.pipe(() => state, fp.get("localSettings.recentItems"))() ); const { httpHeader } = useSelector((state) => state.common); const { popupVisible, activePopup } = useSelector( - (state) => state.common.popup, + (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 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 - )(), + const productDataSource = useMemo( + () => + fp.pipe( + () => panelType, + (type) => (type === "theme" ? themeData : productData) + )(), [panelType, themeData, productData] ); @@ -102,17 +134,16 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // FP 방식으로 상태 업데이트 함수들 const updateSelectedIndex = useCallback((newIndex) => { - setSelectedIndex(fp.pipe( - () => newIndex, - index => Math.max(0, Math.min(index, 999)) // 범위 제한 - )()); + setSelectedIndex( + fp.pipe( + () => newIndex, + (index) => Math.max(0, Math.min(index, 999)) // 범위 제한 + )() + ); }, []); const updateThemeItemOverlay = useCallback((isOpen) => { - setOpenThemeItemOverlay(fp.pipe( - () => isOpen, - Boolean - )()); + setOpenThemeItemOverlay(fp.pipe(() => isOpen, Boolean)()); }, []); // FP 방식으로 이벤트 핸들러 정의 @@ -131,27 +162,26 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { }, () => { // 패널 업데이트 조건 체크 - const shouldUpdatePanel = fp.pipe( - () => panels, - fp.get('length'), - length => length === 4 - )() && fp.pipe( - () => panels, - fp.get('1.name'), - name => name === panel_names.PLAYER_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') - )(), + thumbnail: fp.pipe(() => panelInfo, fp.get("thumbnailUrl"))(), }, - }), + }) ); } } @@ -161,7 +191,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { ev.stopPropagation(); } }, - [dispatch, panelInfo, panels], + [dispatch, panelInfo, panels] ); // FP 방식으로 스크롤 함수 핸들러 @@ -175,56 +205,63 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { () => ({ scrollToSection, sectionId }), ({ scrollToSection, sectionId }) => { if (fp.isNotNil(scrollToSection)) { - return { action: 'execute', scrollFunction: scrollToSection, sectionId }; + return { + action: "execute", + scrollFunction: scrollToSection, + sectionId, + }; } else { - return { action: 'store', sectionId }; + return { action: "store", sectionId }; } } )(); // 액션에 따른 처리 - if (scrollAction.action === 'execute') { + if (scrollAction.action === "execute") { scrollAction.scrollFunction(scrollAction.sectionId); } else { console.log( - "DetailPanel: scrollToSection function is null, storing pending scroll", + "DetailPanel: scrollToSection function is null, storing pending scroll" ); setPendingScrollSection(scrollAction.sectionId); } }, - [scrollToSection], + [scrollToSection] ); // ===== 고정 배경 이미지 설정 (detailPanelBg만 사용) ===== // 모든 DetailPanel에서 동일한 배경 이미지(detailpanel-bg-1.png) 사용 useEffect(() => { - console.log('[PartnerId] Partner background info:', { + console.log("[PartnerId] Partner background info:", { panelPatnrId: panelPatnrId, productDataPatnrId: productData?.patnrId, productDataPatncNm: productData?.patncNm, productDataThumbnailUrl960: productData?.thumbnailUrl960, imageUrl: imageUrl, - detailPanelBg: detailPanelBg + detailPanelBg: detailPanelBg, }); - + // 고정 배경 이미지만 설정 (파트너사별 변경 없이) - document.documentElement.style.setProperty('--bg-url', `url(${detailPanelBg})`); + document.documentElement.style.setProperty( + "--bg-url", + `url(${detailPanelBg})` + ); }, [panelPatnrId, productData, imageUrl]); // FP 방식으로 pending scroll 처리 (메모리 누수 방지) useEffect(() => { const shouldExecutePendingScroll = fp.pipe( () => ({ scrollToSection, pendingScrollSection }), - ({ scrollToSection, pendingScrollSection }) => + ({ scrollToSection, pendingScrollSection }) => fp.isNotNil(scrollToSection) && fp.isNotNil(pendingScrollSection) )(); if (shouldExecutePendingScroll) { console.log( "DetailPanel: executing pending scroll to:", - pendingScrollSection, + pendingScrollSection ); - + // 메모리 누수 방지를 위한 cleanup 함수 const timeoutId = setTimeout(() => { if (scrollToSection) { @@ -252,14 +289,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { () => { // 테마 데이터 로딩 const isThemeType = panelType === "theme"; - + if (isThemeType) { dispatch( getThemeCurationDetailInfo({ patnrId: panelPatnrId, curationId: panelCurationId, bgImgNo: panelBgImgNo, - }), + }) ); } }, @@ -267,14 +304,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // 일반 상품 데이터 로딩 const hasProductId = fp.isNotNil(panelPrdtId); const hasNoCuration = fp.isNil(panelCurationId); - + if (hasProductId && hasNoCuration) { dispatch( getMainCategoryDetail({ patnrId: panelPatnrId, prdtId: panelPrdtId, liveReqFlag: panelLiveReqFlag || "N", - }), + }) ); } } @@ -296,10 +333,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // FP 방식으로 추천 상품 데이터 로딩 (메모리 누수 방지) useEffect(() => { - const shouldLoadRecommendations = fp.pipe( - () => lgCatCd, - fp.isNotEmpty - )(); + const shouldLoadRecommendations = fp.pipe(() => lgCatCd, fp.isNotEmpty)(); if (shouldLoadRecommendations) { dispatch( @@ -308,62 +342,82 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { 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 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 - ), - + () => + 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 - ) + () => + 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()) + categoryRules.map((rule) => rule()) ); - + setLgCatCd(categoryCode); - }, [productData, selectedIndex, panelCurationId, themeProductInfo, categoryHelpers]); + }, [ + 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) + ({ themeProductInfo, productData, panelInfo, selectedIndex }) => + fp.isNotNil(themeProductInfo) || + fp.isNotNil(productData) || + fp.isNotNil(panelInfo) )(); if (shouldUpdateCategory) { @@ -400,98 +454,147 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // 테마/호텔 기반 인덱스 초기화가 필요하면: // - findIndex 유틸을 만들어 매칭 인덱스를 계산 후 setSelectedIndex에 반영하세요. // FP 방식으로 버전 비교 헬퍼 함수 (curry 적용) - const versionComparators = useMemo(() => ({ - isVersionGTE: fp.curry((target, version) => version >= target), - isVersionLT: fp.curry((target, version) => version < target) - }), []); + 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 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) => + const createTypeChecker = fp.curry((type, conditions, sideEffect) => fp.pipe( () => conditions(), - isValid => isValid ? (() => { - sideEffect && sideEffect(); - return { matched: true, type }; - })() : { matched: false } + (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); - } - ), - + () => + 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") - ), - + () => + 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") - ) + () => + 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(), + (result, rule) => (result.matched ? result : rule()), { matched: false }, productTypeRules ); @@ -499,15 +602,27 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // 매칭되지 않은 경우 디버깅 정보 출력 if (!matchedRule.matched) { const debugInfo = fp.pipe( - () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }), - ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => ({ + () => ({ + 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); } @@ -530,45 +645,59 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { } }, [getProductType, productData, themeData, panelType]); - const imageUrl = useMemo(() => fp.pipe(() => productData, fp.get('thumbnailUrl960'))(), [productData]); + 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 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 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 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(() => { @@ -589,6 +718,15 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { // 언마운트 시 인덱스 초기화가 필요하면: // useEffect(() => () => setSelectedIndex(0), []) + const handleProductAllSectionReady = useCallback(() => { + const spotTime = setTimeout(() => { + Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE); + }, 100); + return () => { + clearTimeout(spotTime); + }; + }, []); + return (
+
+ SCROLL DOWN
+