[상품 상세] 리뷰 및 디테일 변경건.#3

1. 상품 상세 진입시 초기 포커스 shop by mobile 로 변경.
		- spotlightids추가.
	2. 상품상세 scroll down추가.
		- 하단부 도달했을때 노출되지않도록 처리.
		- 클릭시 200px씩 이동.
	3. 리뷰팝업 부분 스타일변경
		- 호버시 이미지 확대부분 부자연스럽지 않도록 변경.
	4. 상품 상세 우측 부분에서의 포커스 이동시 좌측 버튼부분의 호버처리.
		- 포커스 이동시에 자연스럽게 호버 이동가능하도록 변경
This commit is contained in:
junghoon86.park
2025-09-12 10:39:41 +09:00
parent d2a9388bd5
commit ddf7c352eb
10 changed files with 796 additions and 401 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

View File

@@ -54,4 +54,5 @@ export const SpotlightIds = {
// detailPanel
DETAIL_BUYNOW: "detail_buynow",
DETAIL_SHOPBYMOBILE: "detail_shop_by_mobile",
};

View File

@@ -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}

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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"
/>

View File

@@ -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;

View File

@@ -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 {