[상품 상세] 리뷰 및 디테일 변경건.#3
1. 상품 상세 진입시 초기 포커스 shop by mobile 로 변경. - spotlightids추가. 2. 상품상세 scroll down추가. - 하단부 도달했을때 노출되지않도록 처리. - 클릭시 200px씩 이동. 3. 리뷰팝업 부분 스타일변경 - 호버시 이미지 확대부분 부자연스럽지 않도록 변경. 4. 상품 상세 우측 부분에서의 포커스 이동시 좌측 버튼부분의 호버처리. - 포커스 이동시에 자연스럽게 호버 이동가능하도록 변경
This commit is contained in:
@@ -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 (
|
||||
<div ref={containerRef}>
|
||||
<TPanel
|
||||
@@ -621,13 +759,18 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
({ isLoading, panelInfo, productDataSource, productType }) => {
|
||||
const hasRequiredData = fp.pipe(
|
||||
() => [panelInfo, productDataSource, productType],
|
||||
data => fp.reduce((acc, item) => acc && fp.isNotNil(item), true, data)
|
||||
(data) =>
|
||||
fp.reduce(
|
||||
(acc, item) => acc && fp.isNotNil(item),
|
||||
true,
|
||||
data
|
||||
)
|
||||
)();
|
||||
|
||||
|
||||
return {
|
||||
canRender: !isLoading && hasRequiredData,
|
||||
showLoading: !isLoading && !hasRequiredData,
|
||||
showNothing: isLoading
|
||||
showNothing: isLoading,
|
||||
};
|
||||
}
|
||||
)();
|
||||
@@ -645,10 +788,12 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
openThemeItemOverlay={openThemeItemOverlay}
|
||||
setOpenThemeItemOverlay={updateThemeItemOverlay}
|
||||
themeProductInfo={themeProductInfo}
|
||||
onReady={handleProductAllSectionReady}
|
||||
isOnRender={renderStates.canRender}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (renderStates.showLoading) {
|
||||
return (
|
||||
<div className={css.loadingContainer}>
|
||||
@@ -656,10 +801,23 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}, [isLoading, panelInfo, productDataSource, productType, selectedIndex, panelPatnrId, panelPrdtId, updateSelectedIndex, openThemeItemOverlay, updateThemeItemOverlay, themeProductInfo])}
|
||||
}, [
|
||||
isLoading,
|
||||
panelInfo,
|
||||
productDataSource,
|
||||
productType,
|
||||
selectedIndex,
|
||||
panelPatnrId,
|
||||
panelPrdtId,
|
||||
updateSelectedIndex,
|
||||
openThemeItemOverlay,
|
||||
updateThemeItemOverlay,
|
||||
themeProductInfo,
|
||||
])}
|
||||
</TBody>
|
||||
|
||||
<ThemeItemListOverlay
|
||||
productInfo={productDataSource}
|
||||
isOpen={openThemeItemOverlay}
|
||||
|
||||
Reference in New Issue
Block a user