[상품 상세] 리뷰 및 디테일 변경건.#3
1. 상품 상세 진입시 초기 포커스 shop by mobile 로 변경. - spotlightids추가. 2. 상품상세 scroll down추가. - 하단부 도달했을때 노출되지않도록 처리. - 클릭시 200px씩 이동. 3. 리뷰팝업 부분 스타일변경 - 호버시 이미지 확대부분 부자연스럽지 않도록 변경. 4. 상품 상세 우측 부분에서의 포커스 이동시 좌측 버튼부분의 호버처리. - 포커스 이동시에 자연스럽게 호버 이동가능하도록 변경
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 715 B |
@@ -54,4 +54,5 @@ export const SpotlightIds = {
|
||||
|
||||
// detailPanel
|
||||
DETAIL_BUYNOW: "detail_buynow",
|
||||
DETAIL_SHOPBYMOBILE: "detail_shop_by_mobile",
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,13 +5,29 @@
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-image:
|
||||
// linear-gradient(0deg, #222222, #222222),
|
||||
// linear-gradient(180deg, rgba(34, 34, 34, 0) 89.93%, #222222 103.61%),
|
||||
// linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
|
||||
// linear-gradient(
|
||||
// 270deg,
|
||||
// rgba(0, 0, 0, 0) 43.07%,
|
||||
// rgba(0, 0, 0, 0.539) 73.73%,
|
||||
// rgba(0, 0, 0, 0.7) 100%
|
||||
// ),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.4)),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.77) 70%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
270deg,
|
||||
rgba(0, 0, 0, 0) 43.07%,
|
||||
rgba(0, 0, 0, 0.539) 73.73%,
|
||||
rgba(0, 0, 0, 0.7) 100%
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.77) 70%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
|
||||
//1차 변경본 너무 어두움 확인필요
|
||||
var(--bg-url);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,72 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import React, { useCallback, useRef, useState, useMemo, useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import { PropTypes } from "prop-types";
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
//image
|
||||
import arrowDown
|
||||
from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
|
||||
import { pushPanel } from '../../../actions/panelActions';
|
||||
import { resetShowAllReviews } from '../../../actions/productActions';
|
||||
// ProductInfoSection imports
|
||||
import TButton from "../../../components/TButton/TButton";
|
||||
import { $L } from "../../../utils/helperMethods";
|
||||
import TButton from '../../../components/TButton/TButton';
|
||||
import useReviews from '../../../hooks/useReviews/useReviews';
|
||||
import useScrollTo from '../../../hooks/useScrollTo';
|
||||
import { panel_names } from '../../../utils/Config';
|
||||
import {
|
||||
curry, pipe, when, isVal, isNotNil, defaultTo, defaultWith, get, identity, isEmpty, isNil, andThen, tap
|
||||
} from "../../../utils/fp";
|
||||
import { resetShowAllReviews } from "../../../actions/productActions";
|
||||
import useReviews from "../../../hooks/useReviews/useReviews";
|
||||
import { pushPanel } from "../../../actions/panelActions";
|
||||
import { panel_names } from "../../../utils/Config";
|
||||
import ViewAllReviewsButton from "../ProductContentSection/UserReviews/ViewAllReviewsButton";
|
||||
import FavoriteBtn from "../components/FavoriteBtn";
|
||||
import StarRating from "../components/StarRating";
|
||||
import ProductTag from "../components/ProductTag";
|
||||
import DetailMobileSendPopUp from "../components/DetailMobileSendPopUp";
|
||||
import { SpotlightIds } from "../../../utils/SpotlightIds";
|
||||
import QRCode from "../ProductInfoSection/QRCode/QRCode";
|
||||
import ProductOverview from "../ProductOverview/ProductOverview";
|
||||
|
||||
andThen,
|
||||
curry,
|
||||
defaultTo,
|
||||
defaultWith,
|
||||
get,
|
||||
identity,
|
||||
isEmpty,
|
||||
isNil,
|
||||
isNotNil,
|
||||
isVal,
|
||||
pipe,
|
||||
tap,
|
||||
when,
|
||||
} from '../../../utils/fp';
|
||||
import { $L } from '../../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||
import ShowUserReviews from '../../UserReview/ShowUserReviews';
|
||||
import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar';
|
||||
import DetailMobileSendPopUp from '../components/DetailMobileSendPopUp';
|
||||
import FavoriteBtn from '../components/FavoriteBtn';
|
||||
import ProductTag from '../components/ProductTag';
|
||||
import StarRating from '../components/StarRating';
|
||||
// ProductContentSection imports
|
||||
import TScrollerDetail from "../components/TScroller/TScrollerDetail";
|
||||
import CustomScrollbar from "../components/CustomScrollbar/CustomScrollbar";
|
||||
import useScrollTo from "../../../hooks/useScrollTo";
|
||||
import ProductDetail from "../ProductContentSection/ProductDetail/ProductDetail.new";
|
||||
import UserReviews from "../ProductContentSection/UserReviews/UserReviews";
|
||||
import YouMayAlsoLike from "../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike";
|
||||
import ProductDescription from "../ProductContentSection/ProductDescription/ProductDescription";
|
||||
import ShowUserReviews from "../../UserReview/ShowUserReviews";
|
||||
|
||||
import TScrollerDetail from '../components/TScroller/TScrollerDetail';
|
||||
import ProductDescription
|
||||
from '../ProductContentSection/ProductDescription/ProductDescription';
|
||||
import ProductDetail
|
||||
from '../ProductContentSection/ProductDetail/ProductDetail.new';
|
||||
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
|
||||
import ViewAllReviewsButton
|
||||
from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
|
||||
import YouMayAlsoLike
|
||||
from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
|
||||
import QRCode from '../ProductInfoSection/QRCode/QRCode';
|
||||
import ProductOverview from '../ProductOverview/ProductOverview';
|
||||
// CSS imports
|
||||
// import infoCSS from "../ProductInfoSection/ProductInfoSection.module.less";
|
||||
// import contentCSS from "../ProductContentSection/ProductContentSection.module.less";
|
||||
@@ -46,9 +77,9 @@ const Container = SpotlightContainerDecorator(
|
||||
enterTo: "last-focused",
|
||||
preserveld: true,
|
||||
leaveFor: { right: "content-scroller-container" },
|
||||
spotlightDirection: "vertical"
|
||||
spotlightDirection: "vertical",
|
||||
},
|
||||
"div",
|
||||
"div"
|
||||
);
|
||||
|
||||
const ContentContainer = SpotlightContainerDecorator(
|
||||
@@ -56,12 +87,12 @@ const ContentContainer = SpotlightContainerDecorator(
|
||||
enterTo: "default-element",
|
||||
preserveld: true,
|
||||
leaveFor: {
|
||||
left: "spotlight-product-info-section-container"
|
||||
left: "spotlight-product-info-section-container",
|
||||
},
|
||||
restrict: "none",
|
||||
spotlightDirection: "vertical"
|
||||
spotlightDirection: "vertical",
|
||||
},
|
||||
"div",
|
||||
"div"
|
||||
);
|
||||
|
||||
const HorizontalContainer = SpotlightContainerDecorator(
|
||||
@@ -69,17 +100,19 @@ const HorizontalContainer = SpotlightContainerDecorator(
|
||||
enterTo: "last-focused",
|
||||
preserveld: true,
|
||||
defaultElement: "spotlight-product-info-section-container",
|
||||
spotlightDirection: "horizontal"
|
||||
spotlightDirection: "horizontal",
|
||||
},
|
||||
"div",
|
||||
"div"
|
||||
);
|
||||
|
||||
|
||||
// FP: Pure function to determine product data based on type
|
||||
const getProductData = curry((productType, themeProductInfo, productInfo) =>
|
||||
pipe(
|
||||
when(
|
||||
() => isVal(productType) && productType === "theme" && isVal(themeProductInfo),
|
||||
() =>
|
||||
isVal(productType) &&
|
||||
productType === "theme" &&
|
||||
isVal(themeProductInfo),
|
||||
() => themeProductInfo
|
||||
),
|
||||
defaultTo(productInfo),
|
||||
@@ -91,19 +124,14 @@ const getProductData = curry((productType, themeProductInfo, productInfo) =>
|
||||
const deriveFavoriteFlag = curry((favoriteOverride, productData) =>
|
||||
pipe(
|
||||
when(isNotNil, identity),
|
||||
defaultWith(() =>
|
||||
pipe(
|
||||
get("favorYn"),
|
||||
defaultTo("N")
|
||||
)(productData)
|
||||
)
|
||||
defaultWith(() => pipe(get("favorYn"), defaultTo("N"))(productData))
|
||||
)(favoriteOverride)
|
||||
);
|
||||
|
||||
// FP: Pure function to extract review grade and order phone
|
||||
const extractProductMeta = (productInfo) => ({
|
||||
revwGrd: get("revwGrd", productInfo),
|
||||
orderPhnNo: get("orderPhnNo", productInfo)
|
||||
orderPhnNo: get("orderPhnNo", productInfo),
|
||||
});
|
||||
|
||||
// 레이아웃 확인용 샘플 컴포넌트 - Spottable로 변경
|
||||
@@ -130,11 +158,22 @@ export default function ProductAllSection({
|
||||
openThemeItemOverlay,
|
||||
setOpenThemeItemOverlay,
|
||||
themeProductInfo,
|
||||
onReady,
|
||||
isOnRender,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const [currentHeight, setCurrentHeight] = useState(0);
|
||||
//하단부분까지 갔을때 체크용
|
||||
const [documentHeight, setDocumentHeight] = useState();
|
||||
const [isBottom, setIsBottom] = useState(false);
|
||||
//버튼 active 표시용
|
||||
const [reviewHeight, setReviewHeight] = useState(0);
|
||||
const [activeProductBtn, setActiveProductBtn] = useState(false);
|
||||
const [activeReviewBtn, setActiveReviewBtn] = useState(false);
|
||||
const [activeYouMayLikeBtn, setActiveYouMayLikeBtn] = useState(false);
|
||||
|
||||
const productData = useMemo(() =>
|
||||
getProductData(productType, themeProductInfo, productInfo),
|
||||
const productData = useMemo(
|
||||
() => getProductData(productType, themeProductInfo, productInfo),
|
||||
[productType, themeProductInfo, productInfo]
|
||||
);
|
||||
|
||||
@@ -143,16 +182,21 @@ export default function ProductAllSection({
|
||||
previewReviews,
|
||||
stats,
|
||||
isLoading: reviewsLoading,
|
||||
hasReviews // 리뷰 존재 여부 플래그 추가
|
||||
hasReviews, // 리뷰 존재 여부 플래그 추가
|
||||
} = useReviews(productData.prdtId);
|
||||
|
||||
// YouMayAlsoLike 데이터 확인
|
||||
const youmaylikeProductData = useSelector((state) => state.main.youmaylikeData);
|
||||
const hasYouMayAlsoLike = youmaylikeProductData && youmaylikeProductData.length > 0;
|
||||
const youmaylikeProductData = useSelector(
|
||||
(state) => state.main.youmaylikeData
|
||||
);
|
||||
const hasYouMayAlsoLike =
|
||||
youmaylikeProductData && youmaylikeProductData.length > 0;
|
||||
|
||||
// ProductAllSection 마운트 시 showAllReviews 초기화
|
||||
useEffect(() => {
|
||||
console.log("[ProductAllSection] Component mounted - resetting showAllReviews to false");
|
||||
console.log(
|
||||
"[ProductAllSection] Component mounted - resetting showAllReviews to false"
|
||||
);
|
||||
dispatch(resetShowAllReviews());
|
||||
}, []); // 빈 dependency array = 마운트 시에만 실행
|
||||
|
||||
@@ -163,18 +207,22 @@ export default function ProductAllSection({
|
||||
hasProductData: !!productData,
|
||||
reviewTotalCount: stats.totalReviews,
|
||||
averageRating: stats.averageRating,
|
||||
productData: productData
|
||||
productData: productData,
|
||||
});
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.USER_REVIEW_PANEL,
|
||||
panelInfo: {
|
||||
prdtId: productData.prdtId,
|
||||
productImage: (productData.imgUrls600 && productData.imgUrls600[0]) || (productData.imgUrls && productData.imgUrls[0]) || productData.thumbnailUrl || 'https://placehold.co/150x150',
|
||||
brandLogo: productData.patncLogoPath || 'https://placehold.co/50x50',
|
||||
productName: productData.prdtNm || '상품명 정보가 없습니다',
|
||||
productImage:
|
||||
(productData.imgUrls600 && productData.imgUrls600[0]) ||
|
||||
(productData.imgUrls && productData.imgUrls[0]) ||
|
||||
productData.thumbnailUrl ||
|
||||
"https://placehold.co/150x150",
|
||||
brandLogo: productData.patncLogoPath || "https://placehold.co/50x50",
|
||||
productName: productData.prdtNm || "상품명 정보가 없습니다",
|
||||
avgRating: stats.averageRating || 5,
|
||||
reviewCount: stats.totalReviews || 0
|
||||
reviewCount: stats.totalReviews || 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -186,30 +234,38 @@ export default function ProductAllSection({
|
||||
hasProductData: !!productData,
|
||||
productDataPrdtId: productData && productData.prdtId,
|
||||
imgUrls600: productData && productData.imgUrls600,
|
||||
imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
imgUrls600Type: Array.isArray(productData && productData.imgUrls600) ? 'array' : typeof (productData && productData.imgUrls600),
|
||||
productData: productData
|
||||
imgUrls600Length:
|
||||
productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
|
||||
? "array"
|
||||
: typeof (productData && productData.imgUrls600),
|
||||
productData: productData,
|
||||
});
|
||||
}, [productData]);
|
||||
|
||||
const { revwGrd, orderPhnNo } = useMemo(() =>
|
||||
extractProductMeta(productInfo),
|
||||
const { revwGrd, orderPhnNo } = useMemo(
|
||||
() => extractProductMeta(productInfo),
|
||||
[productInfo]
|
||||
);
|
||||
|
||||
// FP: derive favorite flag from props with local override, avoid non-I/O useEffect
|
||||
const [favoriteOverride, setFavoriteOverride] = useState(null);
|
||||
const favoriteFlag = useMemo(() =>
|
||||
deriveFavoriteFlag(favoriteOverride, productData),
|
||||
const favoriteFlag = useMemo(
|
||||
() => deriveFavoriteFlag(favoriteOverride, productData),
|
||||
[favoriteOverride, productData]
|
||||
);
|
||||
|
||||
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
|
||||
|
||||
|
||||
// useReviews에서 모든 리뷰 데이터 관리
|
||||
const reviewTotalCount = stats.totalReviews;
|
||||
const reviewData = { reviewList: previewReviews, reviewDetail: { totRvwCnt: stats.totalReviews, avgRvwScr: stats.averageRating } };
|
||||
const reviewData = {
|
||||
reviewList: previewReviews,
|
||||
reviewDetail: {
|
||||
totRvwCnt: stats.totalReviews,
|
||||
avgRvwScr: stats.averageRating,
|
||||
},
|
||||
};
|
||||
|
||||
// User Reviews 스크롤 핸들러 추가
|
||||
const handleUserReviewsClick = useCallback(
|
||||
@@ -218,15 +274,16 @@ export default function ProductAllSection({
|
||||
);
|
||||
|
||||
const scrollContainerRef = useRef(null);
|
||||
const { getScrollTo, scrollTop } = useScrollTo();
|
||||
const productDetailRef = useRef(null); //높이값 변경때문
|
||||
const descriptionRef = useRef(null);
|
||||
const reviewRef = useRef(null);
|
||||
const youMayAlsoLikelRef = useRef(null);
|
||||
|
||||
const { getScrollTo, scrollTop } = useScrollTo();
|
||||
|
||||
// FP: Pure function for mobile popup state change
|
||||
const handleShopByMobileOpen = useCallback(
|
||||
pipe(
|
||||
() => true,
|
||||
setMobileSendPopupOpen
|
||||
),
|
||||
pipe(() => true, setMobileSendPopupOpen),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -275,7 +332,86 @@ export default function ProductAllSection({
|
||||
() => scrollToSection("scroll-marker-you-may-also-like"),
|
||||
[scrollToSection]
|
||||
);
|
||||
const scrollPositionRef = useRef(0);
|
||||
|
||||
const handleArrowClickAlternative = useCallback(() => {
|
||||
const currentHeight = scrollPositionRef.current;
|
||||
const scrollAmount = 200;
|
||||
scrollTop({
|
||||
y: currentHeight + scrollAmount,
|
||||
animate: true,
|
||||
});
|
||||
if (documentHeight) {
|
||||
const isAtBottom = scrollPositionRef.current + 944 >= documentHeight;
|
||||
if (isAtBottom !== isBottom) {
|
||||
setIsBottom(isAtBottom);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e) => {
|
||||
scrollPositionRef.current = e.scrollTop;
|
||||
if (documentHeight) {
|
||||
const isAtBottom =
|
||||
scrollPositionRef.current + 944 >=
|
||||
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
|
||||
if (isAtBottom !== isBottom) {
|
||||
setIsBottom(isAtBottom);
|
||||
}
|
||||
}
|
||||
},
|
||||
[documentHeight, isBottom, youMayAlsoLikelRef]
|
||||
);
|
||||
|
||||
const productFocus = useCallback(() => {
|
||||
setActiveProductBtn(true);
|
||||
setActiveReviewBtn(false);
|
||||
setActiveYouMayLikeBtn(false);
|
||||
}, []);
|
||||
|
||||
const reviewFocus = useCallback(() => {
|
||||
setActiveProductBtn(false);
|
||||
setActiveReviewBtn(true);
|
||||
setActiveYouMayLikeBtn(false);
|
||||
}, []);
|
||||
|
||||
const youmaylikeFocus = useCallback(() => {
|
||||
setActiveProductBtn(false);
|
||||
setActiveReviewBtn(false);
|
||||
setActiveYouMayLikeBtn(true);
|
||||
}, []);
|
||||
|
||||
const _onBlur = useCallback(() => {
|
||||
setActiveProductBtn(false);
|
||||
setActiveReviewBtn(false);
|
||||
setActiveYouMayLikeBtn(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentHeight(
|
||||
(productDetailRef.current?.scrollHeight || 0) +
|
||||
(descriptionRef.current?.scrollHeight || 0) +
|
||||
(reviewRef.current?.scrollHeight || 0)
|
||||
);
|
||||
setReviewHeight(
|
||||
(productDetailRef.current?.scrollHeight || 0) +
|
||||
(descriptionRef.current?.scrollHeight || 0) +
|
||||
(reviewRef.current?.scrollHeight || 0)
|
||||
);
|
||||
}, [
|
||||
productDetailRef.current,
|
||||
descriptionRef.current,
|
||||
hasReviews,
|
||||
hasYouMayAlsoLike,
|
||||
]);
|
||||
|
||||
//spot관련
|
||||
useEffect(() => {
|
||||
if (onReady && isOnRender) {
|
||||
onReady();
|
||||
}
|
||||
}, [onReady, isOnRender]);
|
||||
|
||||
return (
|
||||
<HorizontalContainer className={css.detailArea}>
|
||||
@@ -342,26 +478,31 @@ export default function ProductAllSection({
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{orderPhnNo && (
|
||||
<div className={css.callToOrderSection}>
|
||||
<div className={css.callToOrderText}>
|
||||
{$L("Call to Order")}
|
||||
</div>
|
||||
<div className={css.phoneSection}>
|
||||
<div className={css.phoneIconContainer}>
|
||||
<div className={css.phoneIcon} />
|
||||
<div className={css.callToOrderSection}>
|
||||
{orderPhnNo && (
|
||||
<>
|
||||
<div className={css.callToOrderText}>
|
||||
{$L("Call to Order")}
|
||||
</div>
|
||||
<div className={css.phoneNumber}>{orderPhnNo}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={css.phoneSection}>
|
||||
<div className={css.phoneIconContainer}>
|
||||
<div className={css.phoneIcon} />
|
||||
</div>
|
||||
<div className={css.phoneNumber}>{orderPhnNo}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Container
|
||||
className={css.actionButtonsWrapper}
|
||||
spotlightId="product-info-button-container"
|
||||
>
|
||||
<TButton
|
||||
className={css.productDetailsButton}
|
||||
className={classNames(
|
||||
css.productDetailsButton,
|
||||
activeProductBtn ? css.active : ""
|
||||
)}
|
||||
onClick={handleProductDetailsClick}
|
||||
spotlightId="product-details-button"
|
||||
>
|
||||
@@ -369,7 +510,10 @@ export default function ProductAllSection({
|
||||
</TButton>
|
||||
{hasReviews && (
|
||||
<TButton
|
||||
className={css.userReviewsButton}
|
||||
className={classNames(
|
||||
css.userReviewsButton,
|
||||
activeReviewBtn ? css.active : ""
|
||||
)}
|
||||
onClick={handleUserReviewsClick}
|
||||
spotlightId="user-reviews-button"
|
||||
>
|
||||
@@ -378,7 +522,10 @@ export default function ProductAllSection({
|
||||
)}
|
||||
{hasYouMayAlsoLike && (
|
||||
<TButton
|
||||
className={css.youMayLikeButton}
|
||||
className={classNames(
|
||||
css.youMayLikeButton,
|
||||
activeYouMayLikeBtn ? css.active : ""
|
||||
)}
|
||||
onClick={handleYouMayAlsoLikeClick}
|
||||
>
|
||||
{$L("YOU MAY ALSO LIKE")}
|
||||
@@ -387,7 +534,8 @@ export default function ProductAllSection({
|
||||
</Container>
|
||||
|
||||
{panelInfo &&
|
||||
panelInfo && panelInfo.type === "theme" &&
|
||||
panelInfo &&
|
||||
panelInfo.type === "theme" &&
|
||||
!openThemeItemOverlay && (
|
||||
<TButton
|
||||
className={css.themeButton}
|
||||
@@ -425,15 +573,23 @@ export default function ProductAllSection({
|
||||
spotlightId="main-content-scroller"
|
||||
spotlightDisabled={false}
|
||||
spotlightRestrict="none"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className={css.productDetail}>
|
||||
<div
|
||||
id="scroll-marker-product-details"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<LayoutSample onClick={handleLayoutSampleClick} />
|
||||
<div id="product-details-section">
|
||||
{productData && productData.imgUrls600 && productData.imgUrls600.length > 0 ? (
|
||||
{/* <LayoutSample onClick={handleLayoutSampleClick} /> */}
|
||||
<div
|
||||
id="product-details-section"
|
||||
ref={productDetailRef}
|
||||
onFocus={productFocus}
|
||||
onBlur={_onBlur}
|
||||
>
|
||||
{productData &&
|
||||
productData.imgUrls600 &&
|
||||
productData.imgUrls600.length > 0 ? (
|
||||
productData.imgUrls600.map((image, index) => (
|
||||
<ProductDetail
|
||||
key={`product-detail-${index}`}
|
||||
@@ -441,7 +597,7 @@ export default function ProductAllSection({
|
||||
...productData,
|
||||
singleImage: image,
|
||||
imageIndex: index,
|
||||
totalImages: productData.imgUrls600.length
|
||||
totalImages: productData.imgUrls600.length,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
@@ -449,21 +605,29 @@ export default function ProductAllSection({
|
||||
<ProductDetail productInfo={productData} />
|
||||
)}
|
||||
</div>
|
||||
<div id="product-description-section">
|
||||
<div id="product-description-section" ref={descriptionRef}>
|
||||
<ProductDescription productInfo={productData} />
|
||||
</div>
|
||||
{/* 리뷰가 있을 때만 UserReviews 섹션 표시 */}
|
||||
{hasReviews && (
|
||||
<>
|
||||
<div id="scroll-marker-user-reviews" className={css.scrollMarker}></div>
|
||||
<div id="user-reviews-section">
|
||||
<div
|
||||
id="scroll-marker-user-reviews"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<div
|
||||
id="user-reviews-section"
|
||||
ref={reviewRef}
|
||||
onFocus={reviewFocus}
|
||||
onBlur={_onBlur}
|
||||
>
|
||||
<UserReviews
|
||||
productInfo={productData}
|
||||
panelInfo={panelInfo}
|
||||
reviewsData={{
|
||||
previewReviews: previewReviews.slice(0, 5), // 처음 5개만
|
||||
stats: stats,
|
||||
isLoading: reviewsLoading
|
||||
isLoading: reviewsLoading,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -473,21 +637,34 @@ export default function ProductAllSection({
|
||||
)}
|
||||
</div>
|
||||
{hasYouMayAlsoLike && (
|
||||
<>
|
||||
<div ref={youMayAlsoLikelRef}>
|
||||
<div
|
||||
id="scroll-marker-you-may-also-like"
|
||||
className={css.scrollMarker}
|
||||
></div>
|
||||
<div id="you-may-also-like-section">
|
||||
<YouMayAlsoLike productInfo={productData} panelInfo={panelInfo} />
|
||||
<YouMayAlsoLike
|
||||
productInfo={productData}
|
||||
panelInfo={panelInfo}
|
||||
onFocus={youmaylikeFocus}
|
||||
onBlur={_onBlur}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</TScrollerDetail>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
{!isBottom && (
|
||||
<p
|
||||
className={classNames(css.arrow, css.arrowBottom)}
|
||||
onClick={handleArrowClickAlternative}
|
||||
>
|
||||
<img src={arrowDown} />
|
||||
SCROLL DOWN
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</HorizontalContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
|
||||
// 1. Left Margin Section - 60px
|
||||
.leftMarginSection {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -32,9 +29,6 @@
|
||||
|
||||
// 2. Info Section - 645px
|
||||
.infoSection {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 0;
|
||||
width: 650px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -49,9 +43,6 @@
|
||||
|
||||
// 3. Content Section - 1180px (1114px 콘텐츠 + 66px 스크롤바)
|
||||
.contentSection {
|
||||
position: absolute;
|
||||
left: 705px; // 60px + 645px
|
||||
top: 0;
|
||||
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -286,7 +277,8 @@
|
||||
}
|
||||
|
||||
// 포커스 상태 추가
|
||||
&:focus {
|
||||
&:focus,
|
||||
&.active {
|
||||
background: @PRIMARY_COLOR_RED !important; // 포커스시 빨간색 배경
|
||||
outline: 2px solid @PRIMARY_COLOR_RED !important;
|
||||
|
||||
@@ -403,7 +395,8 @@
|
||||
font-weight: 400; // Bold에서 Regular로 변경
|
||||
line-height: 35px;
|
||||
|
||||
&:focus {
|
||||
&:focus,
|
||||
&.active {
|
||||
background: #c72054; // 포커스시만 빨간색
|
||||
}
|
||||
}
|
||||
@@ -495,9 +488,6 @@
|
||||
|
||||
// 1. Left Margin Section - 60px
|
||||
.leftMarginSection {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -507,9 +497,6 @@
|
||||
|
||||
// 2. Info Section - 645px
|
||||
.infoSection {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 0;
|
||||
width: 650px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -524,9 +511,6 @@
|
||||
|
||||
// 3. Content Section - 1180px (1114px 콘텐츠 + 66px 스크롤바)
|
||||
.contentSection {
|
||||
position: absolute;
|
||||
left: 705px; // 60px + 645px
|
||||
top: 0;
|
||||
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@@ -943,3 +927,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 238px;
|
||||
height: 75px;
|
||||
background-color: #656b78;
|
||||
line-height: 75px;
|
||||
position: absolute;
|
||||
left: 1315px; //710px + 1210px /2
|
||||
bottom: 2px;
|
||||
opacity: 0.7;
|
||||
transform: translateX(-119px);
|
||||
color: @COLOR_WHITE;
|
||||
vertical-align: top;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
&:hover {
|
||||
background-color: @PRIMARY_COLOR_RED;
|
||||
opacity: 1;
|
||||
}
|
||||
> img {
|
||||
margin-right: 5px;
|
||||
width: 26px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
width: 1060px; // 절대 크기 지정
|
||||
height: 100%;
|
||||
// 좌측 30px, 우측 66px(스크롤바) 패딩을 명시적으로 적용
|
||||
padding: 30px 20px 30px 20px;
|
||||
padding: 10px 20px 0px 20px;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
@@ -88,12 +88,11 @@
|
||||
// gap 대신 margin 사용 (TV 호환성) - 화면을 적절히 채우도록 조정
|
||||
.imageGrid {
|
||||
width: 100%;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 20px 0 0 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
@@ -101,44 +100,45 @@
|
||||
align-content: flex-start;
|
||||
// padding: 30px 40px; // 좌우 패딩 증가
|
||||
// padding: 30px 0px 30px 15px; // 좌우 패딩 증가
|
||||
|
||||
.imageItem {
|
||||
//스타일 변경
|
||||
// width: 210px; // 크기 약간 증가
|
||||
// height: 190px; // 비율 맞춤
|
||||
|
||||
width: 226px; // 크기 약간 증가
|
||||
height: 218px; // 비율 맞춤
|
||||
// width: 226px; // 크기 약간 증가
|
||||
// height: 218px; // 비율 맞춤
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
//스타일 변경
|
||||
// margin-right: 35px; // 마진 증가로 균등 분배
|
||||
// margin-bottom: 30px; // 세로 마진도 증가
|
||||
margin-right: 20px; // 마진 증가로 균등 분배
|
||||
margin-top: 20px; // 세로 마진도 증가
|
||||
margin-right: 6px; // 마진 증가로 균등 분배
|
||||
> div {
|
||||
> img {
|
||||
width: 226px;
|
||||
height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
// 4개씩 배치하므로 4번째마다 margin-right 제거
|
||||
&:nth-child(4n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
//확대 이미지
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
margin-right: 6px;
|
||||
margin-top: -2px;
|
||||
// margin-top: -3px;
|
||||
outline: none;
|
||||
> div {
|
||||
> img {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
.focused(@boxShadow: 0px, @borderRadius: 12px);
|
||||
// 프로젝트 표준 포커스 스타일 사용 (4px solid @PRIMARY_COLOR_RED)
|
||||
}
|
||||
&:nth-child(4n) {
|
||||
margin-right: 0;
|
||||
margin-left: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import css from "./YouMayAlsoLike.module.less";
|
||||
import { $L } from "../../../../utils/helperMethods";
|
||||
import TVerticalPagenator from "../../../../components/TVerticalPagenator/TVerticalPagenator";
|
||||
@@ -31,14 +31,26 @@ const Container = SpotlightContainerDecorator(
|
||||
"div"
|
||||
);
|
||||
|
||||
export default function YouMayAlsoLike({ productInfo, panelInfo }) {
|
||||
export default function YouMayAlsoLike({ productInfo, panelInfo, onFocus, onBlur }) {
|
||||
const { getScrollTo, scrollLeft } = useScrollTo();
|
||||
const [newYoumaylikeProductData, setNewYoumaylikeProductData] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
const focusedContainerIdRef = useRef(null);
|
||||
|
||||
const youmaylikeProductData = useSelector(
|
||||
(state) => state.main.youmaylikeData
|
||||
);
|
||||
//노출 9개로 변경위한 처리건.
|
||||
useEffect(() => {
|
||||
if (youmaylikeProductData && youmaylikeProductData.length > 0) {
|
||||
setNewYoumaylikeProductData(
|
||||
youmaylikeProductData.slice(0, youmaylikeProductData.length - 1)
|
||||
);
|
||||
} else {
|
||||
setNewYoumaylikeProductData([]);
|
||||
}
|
||||
}, [youmaylikeProductData]);
|
||||
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
const themeProductInfos = useSelector(
|
||||
(state) => state.home.themeCurationDetailInfoData
|
||||
@@ -63,9 +75,21 @@ export default function YouMayAlsoLike({ productInfo, panelInfo }) {
|
||||
focusedContainerIdRef.current = containerId;
|
||||
}, []);
|
||||
|
||||
const _onFocus = useCallback(() => {
|
||||
if (onFocus) {
|
||||
onFocus();
|
||||
}
|
||||
}, [onFocus]);
|
||||
|
||||
const _onBlur = useCallback(() => {
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
}, [onBlur]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{youmaylikeProductData && youmaylikeProductData.length > 0 && (
|
||||
{newYoumaylikeProductData && newYoumaylikeProductData.length > 0 && (
|
||||
<TVerticalPagenator
|
||||
spotlightId={"detail_youMayAlsoLike_area"}
|
||||
data-wheel-point={true}
|
||||
@@ -74,10 +98,14 @@ export default function YouMayAlsoLike({ productInfo, panelInfo }) {
|
||||
onFocusedContainerId={onFocusedContainerId}
|
||||
topMargin={36}
|
||||
>
|
||||
<Container className={css.container}>
|
||||
<Container
|
||||
className={css.container}
|
||||
onFocus={_onFocus}
|
||||
onBlur={_onBlur}
|
||||
>
|
||||
<THeader title={$L("YOU MAY ALSO LIKE")} className={css.tHeader} />
|
||||
<div className={css.renderCardContainer}>
|
||||
{youmaylikeProductData?.map((product, index) => {
|
||||
{newYoumaylikeProductData?.map((product, index) => {
|
||||
const {
|
||||
imgUrl,
|
||||
patnrId,
|
||||
@@ -109,7 +137,6 @@ export default function YouMayAlsoLike({ productInfo, panelInfo }) {
|
||||
);
|
||||
cursorOpen.current.stop();
|
||||
};
|
||||
|
||||
return (
|
||||
<TItemCard
|
||||
key={prdtId}
|
||||
@@ -130,7 +157,7 @@ export default function YouMayAlsoLike({ productInfo, panelInfo }) {
|
||||
productName={prdtNm}
|
||||
onClick={handleItemClick}
|
||||
label={
|
||||
index * 1 + 1 + " of " + youmaylikeProductData.length
|
||||
index * 1 + 1 + " of " + newYoumaylikeProductData.length
|
||||
}
|
||||
lastLabel=" go to detail, button"
|
||||
/>
|
||||
|
||||
@@ -233,7 +233,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
</div>
|
||||
<div className={css.reviewsSection__filters__group}>
|
||||
<FilterItemButton
|
||||
text={`All stars(${filterCounts?.rating?.all || reviewCount || 0})`}
|
||||
text={`All star(${filterCounts?.rating?.all || reviewCount || 0})`}
|
||||
onClick={handleAllStarsFilter}
|
||||
spotlightId="filter-all-stars"
|
||||
ariaLabel="Filter by all star ratings"
|
||||
@@ -241,7 +241,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 'all'}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`5 stars (${filterCounts?.rating?.[5] || 0})`}
|
||||
text={`5 star (${filterCounts?.rating?.[5] || 0})`}
|
||||
onClick={handle5StarsFilter}
|
||||
spotlightId="filter-5-stars"
|
||||
ariaLabel="Filter by 5 star ratings"
|
||||
@@ -250,7 +250,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 5}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`4 stars (${filterCounts?.rating?.[4] || 0})`}
|
||||
text={`4 star (${filterCounts?.rating?.[4] || 0})`}
|
||||
onClick={handle4StarsFilter}
|
||||
spotlightId="filter-4-stars"
|
||||
ariaLabel="Filter by 4 star ratings"
|
||||
@@ -259,7 +259,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 4}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`3 stars (${filterCounts?.rating?.[3] || 0})`}
|
||||
text={`3 star (${filterCounts?.rating?.[3] || 0})`}
|
||||
onClick={handle3StarsFilter}
|
||||
spotlightId="filter-3-stars"
|
||||
ariaLabel="Filter by 3 star ratings"
|
||||
@@ -268,7 +268,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 3}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`2 stars (${filterCounts?.rating?.[2] || 0})`}
|
||||
text={`2 star (${filterCounts?.rating?.[2] || 0})`}
|
||||
onClick={handle2StarsFilter}
|
||||
spotlightId="filter-2-stars"
|
||||
ariaLabel="Filter by 2 star ratings"
|
||||
@@ -277,7 +277,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 2}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`1 stars (${filterCounts?.rating?.[1] || 0})`}
|
||||
text={`1 star (${filterCounts?.rating?.[1] || 0})`}
|
||||
onClick={handle1StarsFilter}
|
||||
spotlightId="filter-1-stars"
|
||||
ariaLabel="Filter by 1 star ratings"
|
||||
@@ -374,4 +374,4 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default UserReviewPanel;
|
||||
export default UserReviewPanel;
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
height: 150px;
|
||||
border-radius: 14px;
|
||||
object-fit: cover;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-left: 15px;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
@@ -123,6 +123,9 @@
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
margin-right: 15px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #808080;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__productId {
|
||||
|
||||
Reference in New Issue
Block a user