[상품상세] 스타일 수정

1. buy now, add to cart 버튼 크기문제 수정요청. (수정완료)
 2. description, 배송문구 부분 포커스 시에도 product detail로 포커스 표시 요청.(수정완료)
 3. Review Section "+ View More" 버튼 클릭이후 노출되는 이미지 팝업 리뷰 부분 이미지 수정요청.(수정완료)
 4. user review 팝업에서 노출되는 세줄 아이콘 제거 요청.(수정완료)
 5. review 팝업에서 컨텐츠 박스 이상 노출되는 텍스트 처리요청.(히든처리)
 6. review 패널 오픈시에 필터 stars에서 s 제거요청.(수정완료)
 7. review 패널 오픈시에 필터 margin값 변경요청.(변경 완료)
 8. 가격과 할인값이 둘다 1000불 이상일때 가격노출부분 텍스트 크기 수정요청.(60->52px)
 9. qr code 노출부분에서 시간이 지날시 텍스트 이미지 노출요청.(노출 처리)
 10. 상단 타이틀 marquee변경 요청(처리완료)
 11. 타이틀 상품명에 상품아이디 노출요청.(처리는 했지만 노출부분 확인필요)
 12. shop by mobile 눌렀을때 노출되는 화면에서 디자인 맞추기.(디자인 맞춰둠)
This commit is contained in:
junghoon86.park
2025-09-30 20:44:10 +09:00
parent d8479e89ec
commit de0a38e74e
14 changed files with 522 additions and 229 deletions

View File

@@ -5,16 +5,16 @@
.titleHead { .titleHead {
align-self: stretch; align-self: stretch;
padding: 30px; padding: 30px;
background: #E7EBEF; background: #e7ebef;
display: flex; display: flex;
justify-content: flex-start; // center → flex-start justify-content: flex-start; // center → flex-start
align-items: center; align-items: center;
.titleHead__text { .titleHead__text {
text-align: left; // center → left text-align: left; // center → left
color: black; color: black;
font-size: 32px; font-size: 32px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 42px; line-height: 42px;
word-wrap: break-word; word-wrap: break-word;
@@ -25,18 +25,19 @@
.headerTopRow { .headerTopRow {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 5px; // TV 호환: gap 대신 margin 사용 margin-bottom: 5px; // TV 호환: gap 대신 margin 사용
.headerTopRow__brandLogo { .headerTopRow__brandLogo {
width: 50px; width: 50px;
height: 50px; height: 50px;
margin-right: 15px; // TV 호환: gap 대신 margin 사용 margin-right: 15px; // TV 호환: gap 대신 margin 사용
border-radius: 100%;
} }
.headerTopRow__productId { .headerTopRow__productId {
color: #808080; color: #808080;
font-size: 24px; font-size: 24px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 600; font-weight: 600;
line-height: 18px; line-height: 18px;
word-wrap: break-word; word-wrap: break-word;
@@ -53,7 +54,7 @@
left: 0; left: 0;
top: 0; top: 0;
z-index: 0; z-index: 0;
background-color: @COLOR_SKYBLUE;
content: ""; content: "";
} }
display: flex; display: flex;
@@ -62,18 +63,24 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
> .container__header__textBox {
margin-top: 30px;
margin-left: 15px;
width: 885px;
height: 100px;
}
> .container__header__textBoxOnly { > .container__header__textBoxOnly {
width: 100%; width: 100%;
margin-left: 30px; margin-left: 30px;
} }
> .container__header__productImg { > .container__header__productImg {
margin: 30px 0px 0 30px; // 60px → 30px로 변경 margin: 30px 0px 0 30px; // 60px → 30px로 변경
> .container__header__productImg__img { > .container__header__productImg__img {
.border-solid(@size:1px,@color:#cccccc); .border-solid(@size:1px,@color:#cccccc);
width: 100px; // 132px → 100px (Figma 크기) width: 100px; // 132px → 100px (Figma 크기)
height: 100px; // 132px → 100px (Figma 크기) height: 100px; // 132px → 100px (Figma 크기)
margin-bottom: 20px; margin-bottom: 20px;
} }
} }
@@ -93,7 +100,7 @@
font-weight: normal; font-weight: normal;
font-size: 28px; font-size: 28px;
color: @COLOR_GRAY06; color: @COLOR_GRAY06;
padding: 10px 30px 0 30px; // 60px → 30px로 변경 padding: 10px 30px 0 0px; // 60px → 30px로 변경
text-align: left; text-align: left;
.elip(@clamp:1); .elip(@clamp:1);
} }
@@ -101,10 +108,10 @@
width: 100%; width: 100%;
line-height: 60px; line-height: 60px;
text-align: center; text-align: center;
padding: 10px 30px 10px 30px; // 60px → 30px로 변경 padding: 10px 30px 10px 30px; // 60px → 30px로 변경
} }
.container__header__price { .container__header__price {
padding: 0 30px 20px 30px; // 60px → 30px로 변경 padding: 0 30px 20px 30px; // 60px → 30px로 변경
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: @PRIMARY_COLOR_RED; color: @PRIMARY_COLOR_RED;
@@ -152,7 +159,7 @@
width: 345.5px; // 고정 너비 width: 345.5px; // 고정 너비
height: 412px; // PhoneInputSection과 동일한 높이 height: 412px; // PhoneInputSection과 동일한 높이
padding: 10px 30px 0 30px; // 위 10px, 좌우 30px, 아래 0 padding: 10px 30px 0 30px; // 위 10px, 좌우 30px, 아래 0
background: #F8F8F8; background: #f8f8f8;
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@@ -164,7 +171,7 @@
align-self: stretch; align-self: stretch;
color: #808080; color: #808080;
font-size: 20px; // 피그마 스펙대로 복원 font-size: 20px; // 피그마 스펙대로 복원
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 400; font-weight: 400;
line-height: 24px; // 피그마 스펙대로 복원 line-height: 24px; // 피그마 스펙대로 복원
word-wrap: break-word; word-wrap: break-word;

View File

@@ -66,6 +66,10 @@
&.full { &.full {
width: 100%; width: 100%;
} }
&.detail_small {
min-width: 312px;
max-width: 312px;
}
&.basic { &.basic {
&.disabled { &.disabled {
background-color: #7a808d; background-color: #7a808d;

View File

@@ -629,7 +629,13 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
({ panelPrdtId, productData }) => ({ panelPrdtId, productData }) =>
fp.isNotNil(panelPrdtId) && fp.isNotNil(panelPrdtId) &&
fp.pipe(() => productData, fp.get("prdtNm"), fp.isNotNil)() fp.pipe(() => productData, fp.get("prdtNm"), fp.isNotNil)()
? fp.pipe(() => productData, fp.get("prdtNm"))() ? // ? fp.pipe(() => productData, fp.get("prdtNm"))()
// `${fp.get("prdtId")(productData)} ${fp.get("prdtNm")(productData)}`
fp.pipe(
() => productData,
(data) =>
`${fp.get("prdtId")(data)} ${fp.get("prdtNm")(data)}`
)()
: null : null
)(); )();

View File

@@ -8,6 +8,7 @@
z-index: 1; // 배경 컴포넌트(z-index: 0) 위에 표시 z-index: 1; // 배경 컴포넌트(z-index: 0) 위에 표시
background: transparent !important; // 투명 배경으로 설정하여 뒤의 배경 컴포넌트가 보이도록 background: transparent !important; // 투명 배경으로 설정하여 뒤의 배경 컴포넌트가 보이도록
height: 100%; height: 100%;
overflow: hidden;
// 하위 요소들도 투명 배경 (detailPanelWrap 스코프 내에서만 적용) // 하위 요소들도 투명 배경 (detailPanelWrap 스코프 내에서만 적용)
> * { > * {
background: transparent !important; background: transparent !important;

View File

@@ -1,18 +1,31 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { PropTypes } from 'prop-types'; import { PropTypes } from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx // src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
//image //image
import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; import arrowDown
from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
import indicatorDefaultImage
from '../../../../assets/images/img-thumb-empty-144@3x.png';
import { pushPanel } from '../../../actions/panelActions'; import { pushPanel } from '../../../actions/panelActions';
import { resetShowAllReviews } from '../../../actions/productActions'; import { resetShowAllReviews } from '../../../actions/productActions';
import { showToast } from '../../../actions/toastActions'; import { showToast } from '../../../actions/toastActions';
@@ -46,13 +59,16 @@ import ProductTag from '../components/ProductTag';
import StarRating from '../components/StarRating'; import StarRating from '../components/StarRating';
// ProductContentSection imports // ProductContentSection imports
import TScrollerDetail from '../components/TScroller/TScrollerDetail'; import TScrollerDetail from '../components/TScroller/TScrollerDetail';
import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription'; import ProductDescription
import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new'; from '../ProductContentSection/ProductDescription/ProductDescription';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; import ProductDetail
import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; from '../ProductContentSection/ProductDetail/ProductDetail.new';
import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo'; import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo';
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png'; 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 QRCode from '../ProductInfoSection/QRCode/QRCode';
import ProductOverview from '../ProductOverview/ProductOverview'; import ProductOverview from '../ProductOverview/ProductOverview';
// CSS imports // CSS imports
@@ -62,42 +78,45 @@ import css from './ProductAllSection.module.less';
const Container = SpotlightContainerDecorator( const Container = SpotlightContainerDecorator(
{ {
enterTo: 'last-focused', enterTo: "last-focused",
preserveld: true, preserveld: true,
leaveFor: { right: 'content-scroller-container' }, leaveFor: { right: "content-scroller-container" },
spotlightDirection: 'vertical', spotlightDirection: "vertical",
}, },
'div' "div"
); );
const ContentContainer = SpotlightContainerDecorator( const ContentContainer = SpotlightContainerDecorator(
{ {
enterTo: 'default-element', enterTo: "default-element",
preserveld: true, preserveld: true,
leaveFor: { leaveFor: {
left: 'spotlight-product-info-section-container', left: "spotlight-product-info-section-container",
}, },
restrict: 'none', restrict: "none",
spotlightDirection: 'vertical', spotlightDirection: "vertical",
}, },
'div' "div"
); );
const HorizontalContainer = SpotlightContainerDecorator( const HorizontalContainer = SpotlightContainerDecorator(
{ {
enterTo: 'last-focused', enterTo: "last-focused",
preserveld: true, preserveld: true,
defaultElement: 'spotlight-product-info-section-container', defaultElement: "spotlight-product-info-section-container",
spotlightDirection: 'horizontal', spotlightDirection: "horizontal",
}, },
'div' "div"
); );
// FP: Pure function to determine product data based on typeP // FP: Pure function to determine product data based on typeP
const getProductData = curry((productType, themeProductInfo, productInfo) => const getProductData = curry((productType, themeProductInfo, productInfo) =>
pipe( pipe(
when( when(
() => isVal(productType) && productType === 'theme' && isVal(themeProductInfo), () =>
isVal(productType) &&
productType === "theme" &&
isVal(themeProductInfo),
() => themeProductInfo () => themeProductInfo
), ),
defaultTo(productInfo), defaultTo(productInfo),
@@ -113,17 +132,17 @@ const deriveFavoriteFlag = curry((favoriteOverride, productData) => {
return favoriteOverride; return favoriteOverride;
} }
// 그렇지 않으면 productData의 favorYn 값을 사용 (기본값 'N') // 그렇지 않으면 productData의 favorYn 값을 사용 (기본값 'N')
return pipe(get('favorYn'), defaultTo('N'))(productData); return pipe(get("favorYn"), defaultTo("N"))(productData);
}); });
// FP: Pure function to extract review grade and order phone // FP: Pure function to extract review grade and order phone
const extractProductMeta = (productInfo) => ({ const extractProductMeta = (productInfo) => ({
revwGrd: get('revwGrd', productInfo), revwGrd: get("revwGrd", productInfo),
orderPhnNo: get('orderPhnNo', productInfo), orderPhnNo: get("orderPhnNo", productInfo),
}); });
// 레이아웃 확인용 샘플 컴포넌트 - Spottable로 변경 // 레이아웃 확인용 샘플 컴포넌트 - Spottable로 변경
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable("div");
const LayoutSample = ({ onClick }) => ( const LayoutSample = ({ onClick }) => (
<SpottableComponent <SpottableComponent
@@ -152,7 +171,9 @@ export default function ProductAllSection({
const dispatch = useDispatch(); const dispatch = useDispatch();
// Redux 상태 // Redux 상태
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion); const webOSVersion = useSelector(
(state) => state.common.appStatus.webOSVersion
);
const groupInfos = useSelector((state) => state.product.groupInfo); const groupInfos = useSelector((state) => state.product.groupInfo);
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
@@ -162,6 +183,33 @@ export default function ProductAllSection({
//하단부분까지 갔을때 체크용 //하단부분까지 갔을때 체크용
const [documentHeight, setDocumentHeight] = useState(0); const [documentHeight, setDocumentHeight] = useState(0);
const [isBottom, setIsBottom] = useState(false); const [isBottom, setIsBottom] = useState(false);
//qr코드 노출용
const [isShowQRCode, setIsShowQRCode] = useState(true);
const timerRef = useRef(null);
useEffect(() => {
const toggleQRCode = () => {
if (isShowQRCode) {
timerRef.current = setTimeout(() => {
setIsShowQRCode(false);
}, 10000);
} else {
timerRef.current = setTimeout(() => {
setIsShowQRCode(true);
}, 5000);
}
};
toggleQRCode();
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [isShowQRCode]);
//버튼 active 표시용 //버튼 active 표시용
const [activeProductBtn, setActiveProductBtn] = useState(false); const [activeProductBtn, setActiveProductBtn] = useState(false);
@@ -176,20 +224,20 @@ export default function ProductAllSection({
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직 // 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
const isBillingProductVisible = useMemo(() => { const isBillingProductVisible = useMemo(() => {
return ( return (
productData?.pmtSuptYn === 'Y' && productData?.pmtSuptYn === "Y" &&
productData?.grPrdtProcYn === 'N' && productData?.grPrdtProcYn === "N" &&
panelInfo?.prdtId && panelInfo?.prdtId &&
webOSVersion >= '6.0' webOSVersion >= "6.0"
); );
}, [productData, webOSVersion, panelInfo?.prdtId]); }, [productData, webOSVersion, panelInfo?.prdtId]);
// 구매 불가 상품 - DetailPanel.backup.jsx와 동일한 로직 // 구매 불가 상품 - DetailPanel.backup.jsx와 동일한 로직
const isUnavailableProductVisible = useMemo(() => { const isUnavailableProductVisible = useMemo(() => {
return ( return (
productData?.pmtSuptYn === 'N' || productData?.pmtSuptYn === "N" ||
(productData?.pmtSuptYn === 'Y' && (productData?.pmtSuptYn === "Y" &&
productData?.grPrdtProcYn === 'N' && productData?.grPrdtProcYn === "N" &&
webOSVersion < '6.0' && webOSVersion < "6.0" &&
panelInfo?.prdtId) panelInfo?.prdtId)
); );
}, [productData, webOSVersion, panelInfo?.prdtId]); }, [productData, webOSVersion, panelInfo?.prdtId]);
@@ -197,8 +245,8 @@ export default function ProductAllSection({
// 그룹 상품 - DetailPanel.backup.jsx와 동일한 로직 // 그룹 상품 - DetailPanel.backup.jsx와 동일한 로직
const isGroupProductVisible = useMemo(() => { const isGroupProductVisible = useMemo(() => {
return ( return (
productData?.pmtSuptYn === 'Y' && productData?.pmtSuptYn === "Y" &&
productData?.grPrdtProcYn === 'Y' && productData?.grPrdtProcYn === "Y" &&
groupInfos && groupInfos &&
groupInfos.length > 0 groupInfos.length > 0
); );
@@ -206,7 +254,10 @@ export default function ProductAllSection({
// 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직 // 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직
const isTravelProductVisible = useMemo(() => { const isTravelProductVisible = useMemo(() => {
return panelInfo?.curationId && (panelInfo?.type === 'theme' || panelInfo?.type === 'hotel'); return (
panelInfo?.curationId &&
(panelInfo?.type === "theme" || panelInfo?.type === "hotel")
);
}, [panelInfo]); }, [panelInfo]);
// useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당 // useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당
@@ -232,7 +283,9 @@ export default function ProductAllSection({
// ProductAllSection 마운트 시 showAllReviews 초기화 // ProductAllSection 마운트 시 showAllReviews 초기화
useEffect(() => { useEffect(() => {
console.log('[ProductAllSection] Component mounted - resetting showAllReviews to false'); console.log(
"[ProductAllSection] Component mounted - resetting showAllReviews to false"
);
dispatch(resetShowAllReviews()); dispatch(resetShowAllReviews());
}, []); // 빈 dependency array = 마운트 시에만 실행 }, []); // 빈 dependency array = 마운트 시에만 실행
@@ -254,9 +307,9 @@ export default function ProductAllSection({
(productData.imgUrls600 && productData.imgUrls600[0]) || (productData.imgUrls600 && productData.imgUrls600[0]) ||
(productData.imgUrls && productData.imgUrls[0]) || (productData.imgUrls && productData.imgUrls[0]) ||
productData.thumbnailUrl || productData.thumbnailUrl ||
'https://placehold.co/150x150', "https://placehold.co/150x150",
brandLogo: productData.patncLogoPath || 'https://placehold.co/50x50', brandLogo: productData.patncLogoPath || "https://placehold.co/50x50",
productName: productData.prdtNm || '상품명 정보가 없습니다', productName: productData.prdtNm || "상품명 정보가 없습니다",
avgRating: stats.averageRating || 5, avgRating: stats.averageRating || 5,
reviewCount: stats.totalReviews || 0, reviewCount: stats.totalReviews || 0,
}, },
@@ -266,32 +319,33 @@ export default function ProductAllSection({
// BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시 // BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시
const handleBuyNowClick = useCallback(() => { const handleBuyNowClick = useCallback(() => {
console.log('[BuyNow] Buy Now button clicked'); console.log("[BuyNow] Buy Now button clicked");
dispatch( dispatch(
showToast({ showToast({
message: '', message: "",
type: 'buyOption', type: "buyOption",
duration: 0, duration: 0,
position: 'bottom-center', position: "bottom-center",
}) })
); );
}, [dispatch]); }, [dispatch]);
// ADD TO CART 버튼 클릭 핸들러 // ADD TO CART 버튼 클릭 핸들러
const handleAddToCartClick = useCallback(() => { const handleAddToCartClick = useCallback(() => {
console.log('[AddToCart] Add To Cart button clicked'); console.log("[AddToCart] Add To Cart button clicked");
// TODO: 장바구니 추가 로직 구현 // TODO: 장바구니 추가 로직 구현
}, []); }, []);
// 디버깅: 실제 이미지 및 동영상 데이터 확인 // 디버깅: 실제 이미지 및 동영상 데이터 확인
useEffect(() => { useEffect(() => {
console.log('[ProductId] ProductAllSection productData check:', { console.log("[ProductId] ProductAllSection productData check:", {
hasProductData: !!productData, hasProductData: !!productData,
productDataPrdtId: productData && productData.prdtId, productDataPrdtId: productData && productData.prdtId,
imgUrls600: productData && productData.imgUrls600, imgUrls600: productData && productData.imgUrls600,
imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length, imgUrls600Length:
productData && productData.imgUrls600 && productData.imgUrls600.length,
imgUrls600Type: Array.isArray(productData && productData.imgUrls600) imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
? 'array' ? "array"
: typeof (productData && productData.imgUrls600), : typeof (productData && productData.imgUrls600),
// 동영상 관련 정보 추가 // 동영상 관련 정보 추가
prdtMediaUrl: productData && productData.prdtMediaUrl, prdtMediaUrl: productData && productData.prdtMediaUrl,
@@ -303,7 +357,10 @@ export default function ProductAllSection({
}); });
}, [productData, renderItems]); }, [productData, renderItems]);
const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]); const { revwGrd, orderPhnNo } = useMemo(
() => extractProductMeta(productInfo),
[productInfo]
);
// FP: derive favorite flag from props with local override, avoid non-I/O useEffect // FP: derive favorite flag from props with local override, avoid non-I/O useEffect
const [favoriteOverride, setFavoriteOverride] = useState(null); const [favoriteOverride, setFavoriteOverride] = useState(null);
@@ -326,7 +383,7 @@ export default function ProductAllSection({
// User Reviews 스크롤 핸들러 추가 // User Reviews 스크롤 핸들러 추가
const handleUserReviewsClick = useCallback( const handleUserReviewsClick = useCallback(
() => scrollToSection('scroll-marker-user-reviews'), () => scrollToSection("scroll-marker-user-reviews"),
[] []
); );
@@ -343,7 +400,7 @@ export default function ProductAllSection({
// 동영상이 있으면 첫 번째에 추가 (Indicator.jsx와 동일한 로직) // 동영상이 있으면 첫 번째에 추가 (Indicator.jsx와 동일한 로직)
if (productData && productData.prdtMediaUrl) { if (productData && productData.prdtMediaUrl) {
items.push({ items.push({
type: 'video', type: "video",
url: productData.prdtMediaUrl, url: productData.prdtMediaUrl,
thumbnail: productData.thumbnailUrl960 || indicatorDefaultImage, thumbnail: productData.thumbnailUrl960 || indicatorDefaultImage,
index: 0, index: 0,
@@ -351,12 +408,17 @@ export default function ProductAllSection({
} }
// 이미지들 추가 // 이미지들 추가
if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) { if (
productData &&
productData.imgUrls600 &&
productData.imgUrls600.length > 0
) {
productData.imgUrls600.forEach((image, imgIndex) => { productData.imgUrls600.forEach((image, imgIndex) => {
items.push({ items.push({
type: 'image', type: "image",
url: image, url: image,
index: productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex, index:
productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex,
}); });
}); });
} }
@@ -375,7 +437,7 @@ export default function ProductAllSection({
// FP: Pure function for focus navigation to back button // FP: Pure function for focus navigation to back button
const handleSpotlightUpToBackButton = useCallback((e) => { const handleSpotlightUpToBackButton = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
Spotlight.focus('spotlightId_backBtn'); Spotlight.focus("spotlightId_backBtn");
}, []); }, []);
// FP: Pure function for favorite flag change // FP: Pure function for favorite flag change
@@ -388,7 +450,7 @@ export default function ProductAllSection({
const handleThemeItemButtonClick = useCallback( const handleThemeItemButtonClick = useCallback(
pipe( pipe(
() => setOpenThemeItemOverlay(true), () => setOpenThemeItemOverlay(true),
tap(() => setTimeout(() => Spotlight.focus('theme-close-button'), 0)) tap(() => setTimeout(() => Spotlight.focus("theme-close-button"), 0))
), ),
[setOpenThemeItemOverlay] [setOpenThemeItemOverlay]
); );
@@ -409,12 +471,12 @@ export default function ProductAllSection({
// FP: Curried scroll handlers // FP: Curried scroll handlers
const handleProductDetailsClick = useCallback( const handleProductDetailsClick = useCallback(
() => scrollToSection('scroll-marker-product-details'), () => scrollToSection("scroll-marker-product-details"),
[scrollToSection] [scrollToSection]
); );
const handleYouMayAlsoLikeClick = useCallback( const handleYouMayAlsoLikeClick = useCallback(
() => scrollToSection('scroll-marker-you-may-also-like'), () => scrollToSection("scroll-marker-you-may-also-like"),
[scrollToSection] [scrollToSection]
); );
const scrollPositionRef = useRef(0); const scrollPositionRef = useRef(0);
@@ -484,7 +546,12 @@ export default function ProductAllSection({
(descriptionRef.current?.scrollHeight || 0) + (descriptionRef.current?.scrollHeight || 0) +
(reviewRef.current?.scrollHeight || 0) (reviewRef.current?.scrollHeight || 0)
); );
}, [productDetailRef.current, descriptionRef.current, hasReviews, hasYouMayAlsoLike]); }, [
productDetailRef.current,
descriptionRef.current,
hasReviews,
hasYouMayAlsoLike,
]);
//spot관련 //spot관련
useEffect(() => { useEffect(() => {
@@ -508,10 +575,10 @@ export default function ProductAllSection({
<div className={css.leftInfoWrapper}> <div className={css.leftInfoWrapper}>
<div className={css.headerContent}> <div className={css.headerContent}>
<ProductTag productInfo={productData} /> <ProductTag productInfo={productData} />
{revwGrd && revwGrd !== '0.0' && ( {revwGrd && revwGrd !== "0.0" && (
<StarRating <StarRating
rating={revwGrd} rating={revwGrd}
aria-label={'star rating ' + revwGrd + ' out of 5'} aria-label={"star rating " + revwGrd + " out of 5"}
/> />
)} )}
</div> </div>
@@ -525,11 +592,24 @@ export default function ProductAllSection({
productType={productType} productType={productType}
> >
<div className={css.qrWrapper}> <div className={css.qrWrapper}>
<QRCode productInfo={productData} productType={productType} kind={'detail'} /> {isShowQRCode ? (
<QRCode
productInfo={productData}
productType={productType}
kind={"detail"}
/>
) : (
<div className={css.qrRollingWrap}>
<div className={css.innerText}>
<h3>{$L("Scan QR")}</h3>
<p>{$L("with your phone, Check Product")}</p>
<p>{$L("info & Purchase easily")}</p>
</div>
</div>
)}
</div> </div>
</ProductOverview> </ProductOverview>
{/* BUY NOW + ADD TO CART 버튼들 (결제 가능 상품일 때만 렌더링) */}
{isBillingProductVisible && ( {isBillingProductVisible && (
<HorizontalContainer className={css.buyNowCartContainer}> <HorizontalContainer className={css.buyNowCartContainer}>
<TButton <TButton
@@ -537,28 +617,37 @@ export default function ProductAllSection({
className={css.buyNowButton} className={css.buyNowButton}
onClick={handleBuyNowClick} onClick={handleBuyNowClick}
onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightUp={handleSpotlightUpToBackButton}
type="detail_small"
> >
<div className={css.buyNowText}>{$L('BUY NOW')}</div> <div className={css.buyNowText}>{$L("BUY NOW")}</div>
</TButton> </TButton>
<TButton <TButton
spotlightId="detail-add-to-cart-button" spotlightId="detail-add-to-cart-button"
className={css.addToCartButton} className={css.addToCartButton}
onClick={handleAddToCartClick} onClick={handleAddToCartClick}
onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightUp={handleSpotlightUpToBackButton}
type="detail_small"
> >
<div className={css.addToCartText}>{$L('ADD TO CART')}</div> <div className={css.addToCartText}>{$L("ADD TO CART")}</div>
</TButton> </TButton>
</HorizontalContainer> </HorizontalContainer>
)} )}
<Container
<Container className={css.buttonContainer}> className={classNames(
css.buttonContainer,
isBillingProductVisible && css.buttonHasNoCart
)}
>
{/* BUY NOW + ADD TO CART 버튼들 (결제 가능 상품일 때만 렌더링) */}
<TButton <TButton
spotlightId={SpotlightIds.DETAIL_SHOPBYMOBILE} spotlightId={SpotlightIds.DETAIL_SHOPBYMOBILE}
className={css.shopByMobileButton} className={css.shopByMobileButton}
onClick={handleShopByMobileOpen} onClick={handleShopByMobileOpen}
onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightUp={handleSpotlightUpToBackButton}
> >
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div> <div className={css.shopByMobileText}>
{$L("SHOP BY MOBILE")}
</div>
</TButton> </TButton>
{panelInfo && ( {panelInfo && (
<div className={css.favoriteBtnWrapper}> <div className={css.favoriteBtnWrapper}>
@@ -568,7 +657,7 @@ export default function ProductAllSection({
selectedPrdtId={panelInfo && panelInfo.prdtId} selectedPrdtId={panelInfo && panelInfo.prdtId}
favoriteFlag={favoriteFlag} favoriteFlag={favoriteFlag}
onFavoriteFlagChanged={onFavoriteFlagChanged} onFavoriteFlagChanged={onFavoriteFlagChanged}
kind={'item_detail'} kind={"item_detail"}
/> />
</div> </div>
)} )}
@@ -577,7 +666,9 @@ export default function ProductAllSection({
<div className={css.callToOrderSection}> <div className={css.callToOrderSection}>
{orderPhnNo && ( {orderPhnNo && (
<> <>
<div className={css.callToOrderText}>{$L('Call to Order')}</div> <div className={css.callToOrderText}>
{$L("Call to Order")}
</div>
<div className={css.phoneSection}> <div className={css.phoneSection}>
<div className={css.phoneIconContainer}> <div className={css.phoneIconContainer}>
<div className={css.phoneIcon} /> <div className={css.phoneIcon} />
@@ -595,31 +686,34 @@ export default function ProductAllSection({
<TButton <TButton
className={classNames( className={classNames(
css.productDetailsButton, css.productDetailsButton,
activeProductBtn ? css.active : '' activeProductBtn ? css.active : ""
)} )}
onClick={handleProductDetailsClick} onClick={handleProductDetailsClick}
spotlightId="product-details-button" spotlightId="product-details-button"
> >
{$L('PRODUCT DETAILS')} {$L("PRODUCT DETAILS")}
</TButton> </TButton>
{hasReviews && ( {hasReviews && (
<TButton <TButton
className={classNames(css.userReviewsButton, activeReviewBtn ? css.active : '')} className={classNames(
css.userReviewsButton,
activeReviewBtn ? css.active : ""
)}
onClick={handleUserReviewsClick} onClick={handleUserReviewsClick}
spotlightId="user-reviews-button" spotlightId="user-reviews-button"
> >
{$L('USER REVIEWS')} ({reviewTotalCount}) {$L("USER REVIEWS")} ({reviewTotalCount})
</TButton> </TButton>
)} )}
{hasYouMayAlsoLike && ( {hasYouMayAlsoLike && (
<TButton <TButton
className={classNames( className={classNames(
css.youMayLikeButton, css.youMayLikeButton,
activeYouMayLikeBtn ? css.active : '' activeYouMayLikeBtn ? css.active : ""
)} )}
onClick={handleYouMayAlsoLikeClick} onClick={handleYouMayAlsoLikeClick}
> >
{$L('YOU MAY ALSO LIKE')} {$L("YOU MAY ALSO LIKE")}
</TButton> </TButton>
)} )}
{/* YouMayLike 버튼 렌더링 상태 로그 */} {/* YouMayLike 버튼 렌더링 상태 로그 */}
@@ -633,15 +727,18 @@ export default function ProductAllSection({
})()} */} })()} */}
</Container> </Container>
{panelInfo && panelInfo && panelInfo.type === 'theme' && !openThemeItemOverlay && ( {panelInfo &&
<TButton panelInfo &&
className={css.themeButton} panelInfo.type === "theme" &&
onClick={handleThemeItemButtonClick} !openThemeItemOverlay && (
spotlightId="theme-open-button" <TButton
> className={css.themeButton}
{$L('THEME ITEM')} onClick={handleThemeItemButtonClick}
</TButton> spotlightId="theme-open-button"
)} >
{$L("THEME ITEM")}
</TButton>
)}
<DetailMobileSendPopUp <DetailMobileSendPopUp
ismobileSendPopupOpen={mobileSendPopupOpen} ismobileSendPopupOpen={mobileSendPopupOpen}
@@ -673,7 +770,10 @@ export default function ProductAllSection({
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className={css.productDetail}> <div className={css.productDetail}>
<div id="scroll-marker-product-details" className={css.scrollMarker}></div> <div
id="scroll-marker-product-details"
className={css.scrollMarker}
></div>
{/* <LayoutSample onClick={handleLayoutSampleClick} /> */} {/* <LayoutSample onClick={handleLayoutSampleClick} /> */}
<div <div
id="product-details-section" id="product-details-section"
@@ -683,7 +783,7 @@ export default function ProductAllSection({
> >
{renderItems.length > 0 ? ( {renderItems.length > 0 ? (
renderItems.map((item, index) => renderItems.map((item, index) =>
item.type === 'video' ? ( item.type === "video" ? (
<ProductVideo <ProductVideo
key="product-video-0" key="product-video-0"
productInfo={productData} productInfo={productData}
@@ -706,13 +806,21 @@ export default function ProductAllSection({
<ProductDetail productInfo={productData} /> <ProductDetail productInfo={productData} />
)} )}
</div> </div>
<div id="product-description-section" ref={descriptionRef}> <div
id="product-description-section"
ref={descriptionRef}
onFocus={productFocus}
onBlur={_onBlur}
>
<ProductDescription productInfo={productData} /> <ProductDescription productInfo={productData} />
</div> </div>
{/* 리뷰가 있을 때만 UserReviews 섹션 표시 */} {/* 리뷰가 있을 때만 UserReviews 섹션 표시 */}
{hasReviews && ( {hasReviews && (
<> <>
<div id="scroll-marker-user-reviews" className={css.scrollMarker}></div> <div
id="scroll-marker-user-reviews"
className={css.scrollMarker}
></div>
<div <div
id="user-reviews-section" id="user-reviews-section"
ref={reviewRef} ref={reviewRef}
@@ -736,7 +844,10 @@ export default function ProductAllSection({
</div> </div>
{hasYouMayAlsoLike && ( {hasYouMayAlsoLike && (
<div ref={youMayAlsoLikelRef}> <div ref={youMayAlsoLikelRef}>
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div> <div
id="scroll-marker-you-may-also-like"
className={css.scrollMarker}
></div>
<div id="you-may-also-like-section"> <div id="you-may-also-like-section">
{/* {(() => { {/* {(() => {
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', { console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
@@ -774,5 +885,5 @@ export default function ProductAllSection({
} }
ProductAllSection.propTypes = { ProductAllSection.propTypes = {
productType: PropTypes.oneOf(['buyNow', 'shopByMobile', 'theme']).isRequired, productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired,
}; };

View File

@@ -710,7 +710,9 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
&.buttonHasNoCart {
padding-top: 0;
}
> * { > * {
margin-right: 6px; margin-right: 6px;
&:last-child { &:last-child {
@@ -885,7 +887,37 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
.qrRollingWrap {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
text-align: center;
width: 240px;
height: 240px;
background: #fff;
border: 1px solid #fff;
.innerText {
width: 100%;
padding: 0 20px;
h3 {
word-break: break-word;
font-size: 36px;
font-weight: bold;
color: @PRIMARY_COLOR_RED;
& + p {
margin-top: 18px;
}
}
p {
font-size: 24px;
font-weight: bold;
line-height: 1.17;
color: @COLOR_GRAY05;
word-break: keep-all;
}
}
}
> * { > * {
margin-bottom: 10px; margin-bottom: 10px;
&:last-child { &:last-child {

View File

@@ -152,6 +152,7 @@
font-weight: 400; font-weight: 400;
line-height: 31px; line-height: 31px;
word-wrap: break-word; word-wrap: break-word;
overflow: hidden;
} }
} }
} }

View File

@@ -86,8 +86,8 @@ export default function UserReviewsPopup({
case "user-reviews": case "user-reviews":
return { return {
title: $L("User Reviews"), title: $L("User Reviews"),
hasIcon: true, hasIcon: false,
iconType: "user-reviews", iconType: null,
}; };
case "customer-images": case "customer-images":
default: default:

View File

@@ -118,9 +118,13 @@
// margin-bottom: 30px; // 세로 마진도 증가 // margin-bottom: 30px; // 세로 마진도 증가
margin-right: 6px; // 마진 증가로 균등 분배 margin-right: 6px; // 마진 증가로 균등 분배
> div { > div {
width: 226px;
height: 220px;
overflow: hidden;
> img { > img {
width: 226px; width: 226px;
height: 220px; height: 220px;
object-fit: contain;
} }
} }
@@ -145,7 +149,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 12px; border-radius: 12px;
object-fit: cover; object-fit: contain;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -191,7 +195,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 12px; border-radius: 12px;
object-fit: cover; object-fit: contain;
padding: 4px; padding: 4px;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -42,7 +42,7 @@
} }
.price { .price {
font-weight: bold; font-weight: bold;
font-size: 60px; font-size: 52px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
margin-right: 9px; margin-right: 9px;
line-height: 1; line-height: 1;

View File

@@ -1,14 +1,31 @@
import React, { useCallback, useEffect, useMemo, useRef } from "react"; import React, {
import { useDispatch, useSelector } from "react-redux"; useCallback,
import MobileSendPopUp from "../../../components/MobileSend/MobileSendPopUp"; useEffect,
import * as Config from "../../../utils/Config"; useMemo,
import { setHidePopup, setShowPopup } from "../../../actions/commonActions"; useRef,
import Spotlight from "@enact/spotlight"; } from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import {
setHidePopup,
setShowPopup,
} from '../../../actions/commonActions';
import { import {
sendLogShopByMobile, sendLogShopByMobile,
sendLogTotalRecommend, sendLogTotalRecommend,
} from "../../../actions/logActions"; } from '../../../actions/logActions';
import { $L, formatLocalDateTime } from "../../../utils/helperMethods"; import MobileSendPopUp from '../../../components/MobileSend/MobileSendPopUp';
import * as Config from '../../../utils/Config';
import {
$L,
formatLocalDateTime,
} from '../../../utils/helperMethods';
export default function DetailMobileSendPopUp({ export default function DetailMobileSendPopUp({
panelInfo, panelInfo,
@@ -167,11 +184,13 @@ export default function DetailMobileSendPopUp({
subTitle={mobileSendPopUpSubtitle} subTitle={mobileSendPopUpSubtitle}
patncNm={productData?.patncNm} patncNm={productData?.patncNm}
productImg={mobileSendPopUpProductImg} productImg={mobileSendPopUpProductImg}
productId={productData?.prdtId}
patnrId={panelInfo?.patnrId} patnrId={panelInfo?.patnrId}
prdtId={panelInfo?.prdtId} prdtId={panelInfo?.prdtId}
smsTpCd={panelInfo?.type === "hotel" ? "APP00205" : "APP00201"} smsTpCd={panelInfo?.type === "hotel" ? "APP00205" : "APP00201"}
curationId={panelInfo?.curationId} curationId={panelInfo?.curationId}
curationNm={panelInfo?.curationNm} curationNm={panelInfo?.curationNm}
brandLogo={productData?.patncLogoPath}
// hotelId={ // hotelId={
// panelInfo?.type === "hotel" && hotelInfos[selectedIndex]?.hotelId // panelInfo?.type === "hotel" && hotelInfos[selectedIndex]?.hotelId
// } // }

View File

@@ -11,9 +11,10 @@
background-color: transparent; // DetailPanel에서는 배경 투명 background-color: transparent; // DetailPanel에서는 배경 투명
.title { .title {
width: 1710px;
font-size: 25px; font-size: 25px;
font-weight: 600; font-weight: 600;
color: #EAEAEA; color: #eaeaea;
padding-left: 0; padding-left: 0;
letter-spacing: 1px; letter-spacing: 1px;
text-transform: uppercase; text-transform: uppercase;
@@ -46,4 +47,4 @@
background-repeat: no-repeat; background-repeat: no-repeat;
flex-shrink: 0; flex-shrink: 0;
margin-right: 10px; // 파트너사 로고 후 10px gap margin-right: 10px; // 파트너사 로고 후 10px gap
} }

View File

@@ -1,30 +1,38 @@
import React, { useCallback, useEffect, useRef } from "react"; import React, {
import { useDispatch, useSelector } from "react-redux"; useCallback,
import classNames from "classnames"; useEffect,
useRef,
} from 'react';
import { popPanel } from "../../actions/panelActions"; import classNames from 'classnames';
import useReviews from "../../hooks/useReviews/useReviews"; import {
import UserReviewHeader from "./UserReviewHeader"; useDispatch,
import TPanel from "../../components/TPanel/TPanel"; useSelector,
import TBody from "../../components/TBody/TBody"; } from 'react-redux';
import StarRating from "../DetailPanel/components/StarRating";
import FilterItemButton from "./components/FilterItemButton"; import { popPanel } from '../../actions/panelActions';
import UserReviewsList from "./components/UserReviewsList"; import TBody from '../../components/TBody/TBody';
import fp from "../../utils/fp"; import TPanel from '../../components/TPanel/TPanel';
import css from "./UserReviewPanel.module.less"; import useReviews from '../../hooks/useReviews/useReviews';
import fp from '../../utils/fp';
import StarRating from '../DetailPanel/components/StarRating';
import FilterItemButton from './components/FilterItemButton';
import UserReviewsList from './components/UserReviewsList';
import UserReviewHeader from './UserReviewHeader';
import css from './UserReviewPanel.module.less';
const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
// panelInfo에서 prdtId와 patnrId 추출 // panelInfo에서 prdtId와 patnrId 추출
const prdtId = fp.pipe( const prdtId = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('prdtId'), fp.get("prdtId"),
fp.defaultTo(null) fp.defaultTo(null)
)(); )();
const patnrId = fp.pipe( const patnrId = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('patnrId'), fp.get("patnrId"),
fp.defaultTo(null) fp.defaultTo(null)
)(); )();
@@ -33,19 +41,19 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
previewReviews, previewReviews,
displayReviews, displayReviews,
filteredReviews, filteredReviews,
userReviewPanelReviews, // 페이징된 리뷰들 (3개씩) userReviewPanelReviews, // 페이징된 리뷰들 (3개씩)
userReviewPanelPage, // 현재 페이지 userReviewPanelPage, // 현재 페이지
userReviewPanelHasNext, // 다음 페이지 존재 여부 userReviewPanelHasNext, // 다음 페이지 존재 여부
userReviewPanelHasPrev, // 이전 페이지 존재 여부 userReviewPanelHasPrev, // 이전 페이지 존재 여부
userReviewPanelTotalPages, // 전체 페이지 수 userReviewPanelTotalPages, // 전체 페이지 수
goToNextUserReviewPage, // 다음 페이지로 이동 goToNextUserReviewPage, // 다음 페이지로 이동
goToPrevUserReviewPage, // 이전 페이지로 이동 goToPrevUserReviewPage, // 이전 페이지로 이동
applyRatingFilter, applyRatingFilter,
applySentimentFilter, applySentimentFilter,
clearAllFilters, clearAllFilters,
currentFilter, currentFilter,
filterCounts, filterCounts,
stats stats,
} = useReviews(prdtId, patnrId); } = useReviews(prdtId, patnrId);
// 포커스 복원을 위한 ref // 포커스 복원을 위한 ref
@@ -61,7 +69,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
productDataPrdtId: productData.prdtId, productDataPrdtId: productData.prdtId,
hasProductData: !!productData, hasProductData: !!productData,
previewReviews: previewReviews ? previewReviews.length : 0, previewReviews: previewReviews ? previewReviews.length : 0,
displayReviews: displayReviews ? displayReviews.length : 0 displayReviews: displayReviews ? displayReviews.length : 0,
}); });
// UserReviewPanel은 새로운 API 호출 없이 기존 데이터만 사용 // UserReviewPanel은 새로운 API 호출 없이 기존 데이터만 사용
@@ -70,41 +78,43 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
hasPreviewReviews: !!previewReviews, hasPreviewReviews: !!previewReviews,
hasDisplayReviews: !!displayReviews, hasDisplayReviews: !!displayReviews,
previewCount: previewReviews ? previewReviews.length : 0, previewCount: previewReviews ? previewReviews.length : 0,
displayCount: displayReviews ? displayReviews.length : 0 displayCount: displayReviews ? displayReviews.length : 0,
}); });
// 데이터 파싱 - panelInfo에서 직접 가져오기 (Redux productData가 빈 객체가 되는 문제 해결) // 데이터 파싱 - panelInfo에서 직접 가져오기 (Redux productData가 빈 객체가 되는 문제 해결)
const productImage = fp.pipe( const productImage = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('productImage'), fp.get("productImage"),
fp.defaultTo('https://placehold.co/150x150') fp.defaultTo("https://placehold.co/150x150")
)(); )();
const brandLogo = fp.pipe( const brandLogo = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('brandLogo'), fp.get("brandLogo"),
fp.defaultTo('https://placehold.co/50x50') fp.defaultTo("https://placehold.co/50x50")
)(); )();
const productId = fp.pipe( const productId = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('prdtId'), fp.get("prdtId"),
fp.defaultTo(null) fp.defaultTo(null)
)(); )();
const productName = fp.pipe( const productName = fp.pipe(
() => panelInfo, () => panelInfo,
fp.get('productName'), fp.get("productName"),
fp.defaultTo('상품명 정보가 없습니다') fp.defaultTo("상품명 정보가 없습니다")
)(); )();
// 페이징 후 포커스 복원 함수 - 중간 리뷰(index 1)로 포커스 // 페이징 후 포커스 복원 함수 - 중간 리뷰(index 1)로 포커스
const restoreFocusAfterPaging = useCallback(() => { const restoreFocusAfterPaging = useCallback(() => {
setTimeout(() => { setTimeout(() => {
const targetElement = document.querySelector(`[data-spotlight-id="user-review-1"]`); const targetElement = document.querySelector(
`[data-spotlight-id="user-review-1"]`
);
if (targetElement && targetElement.focus) { if (targetElement && targetElement.focus) {
targetElement.focus(); targetElement.focus();
console.log('[UserReviewPanel] 중간 리뷰로 포커스 복원 완료'); console.log("[UserReviewPanel] 중간 리뷰로 포커스 복원 완료");
} }
}, 100); }, 100);
}, []); }, []);
@@ -112,7 +122,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
// 개선된 페이징 함수들 // 개선된 페이징 함수들
const handleNextPage = useCallback(() => { const handleNextPage = useCallback(() => {
if (userReviewPanelHasNext) { if (userReviewPanelHasNext) {
console.log('[UserReviewPanel] 다음 페이지로 이동'); console.log("[UserReviewPanel] 다음 페이지로 이동");
goToNextUserReviewPage(); goToNextUserReviewPage();
restoreFocusAfterPaging(); restoreFocusAfterPaging();
} }
@@ -120,7 +130,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
const handlePrevPage = useCallback(() => { const handlePrevPage = useCallback(() => {
if (userReviewPanelHasPrev) { if (userReviewPanelHasPrev) {
console.log('[UserReviewPanel] 이전 페이지로 이동'); console.log("[UserReviewPanel] 이전 페이지로 이동");
goToPrevUserReviewPage(); goToPrevUserReviewPage();
restoreFocusAfterPaging(); restoreFocusAfterPaging();
} }
@@ -132,44 +142,92 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
const avgRating = stats.averageRating || 5; const avgRating = stats.averageRating || 5;
// 별점 필터링 핸들러들 // 별점 필터링 핸들러들
const handleRatingFilter = useCallback((rating) => { const handleRatingFilter = useCallback(
console.log('[ProductId] Rating filter applied:', rating); (rating) => {
console.log('[ProductId] applyRatingFilter function:', !!applyRatingFilter); console.log("[ProductId] Rating filter applied:", rating);
applyRatingFilter(rating); // 'all' 값을 그대로 전달 console.log(
}, [applyRatingFilter]); "[ProductId] applyRatingFilter function:",
!!applyRatingFilter
);
applyRatingFilter(rating); // 'all' 값을 그대로 전달
},
[applyRatingFilter]
);
const handleAllStarsFilter = useCallback(() => handleRatingFilter('all'), [handleRatingFilter]); const handleAllStarsFilter = useCallback(
const handle5StarsFilter = useCallback(() => handleRatingFilter(5), [handleRatingFilter]); () => handleRatingFilter("all"),
const handle4StarsFilter = useCallback(() => handleRatingFilter(4), [handleRatingFilter]); [handleRatingFilter]
const handle3StarsFilter = useCallback(() => handleRatingFilter(3), [handleRatingFilter]); );
const handle2StarsFilter = useCallback(() => handleRatingFilter(2), [handleRatingFilter]); const handle5StarsFilter = useCallback(
const handle1StarsFilter = useCallback(() => handleRatingFilter(1), [handleRatingFilter]); () => handleRatingFilter(5),
[handleRatingFilter]
);
const handle4StarsFilter = useCallback(
() => handleRatingFilter(4),
[handleRatingFilter]
);
const handle3StarsFilter = useCallback(
() => handleRatingFilter(3),
[handleRatingFilter]
);
const handle2StarsFilter = useCallback(
() => handleRatingFilter(2),
[handleRatingFilter]
);
const handle1StarsFilter = useCallback(
() => handleRatingFilter(1),
[handleRatingFilter]
);
const handleAromaClick = useCallback(() => console.log("Aroma clicked"), []);
const handleVanillaClick = useCallback(
() => console.log("Vanilla clicked"),
[]
);
const handleCinnamonClick = useCallback(
() => console.log("Cinnamon clicked"),
[]
);
const handleQualityClick = useCallback(
() => console.log("Quality clicked"),
[]
);
const handleAromaClick = useCallback(() => console.log('Aroma clicked'), []);
const handleVanillaClick = useCallback(() => console.log('Vanilla clicked'), []);
const handleCinnamonClick = useCallback(() => console.log('Cinnamon clicked'), []);
const handleQualityClick = useCallback(() => console.log('Quality clicked'), []);
// 감정 필터링 핸들러들 - 별점 필터와 동일한 방식 // 감정 필터링 핸들러들 - 별점 필터와 동일한 방식
const handleSentimentFilter = useCallback((sentiment) => { const handleSentimentFilter = useCallback(
console.log('[ProductId] Sentiment filter applied:', sentiment); (sentiment) => {
applySentimentFilter(sentiment === 'all' ? null : sentiment); console.log("[ProductId] Sentiment filter applied:", sentiment);
}, [applySentimentFilter]); applySentimentFilter(sentiment === "all" ? null : sentiment);
},
const handlePositiveClick = useCallback(() => handleSentimentFilter('positive'), [handleSentimentFilter]); [applySentimentFilter]
const handleNegativeClick = useCallback(() => handleSentimentFilter('negative'), [handleSentimentFilter]); );
const handlePositiveClick = useCallback(
() => handleSentimentFilter("positive"),
[handleSentimentFilter]
);
const handleNegativeClick = useCallback(
() => handleSentimentFilter("negative"),
[handleSentimentFilter]
);
// UserReviewPanel 마운트 시 기본 All stars 필터 적용 // UserReviewPanel 마운트 시 기본 All stars 필터 적용
useEffect(() => { useEffect(() => {
if (prdtId && currentFilter.type === 'rating' && currentFilter.value === 'all') { if (
console.log('[ProductId] UserReviewPanel 기본 All stars 필터 이미 적용됨'); prdtId &&
currentFilter.type === "rating" &&
currentFilter.value === "all"
) {
console.log(
"[ProductId] UserReviewPanel 기본 All stars 필터 이미 적용됨"
);
} }
}, [prdtId, currentFilter]); }, [prdtId, currentFilter]);
// 메모리 해제를 위한 cleanup 함수 // 메모리 해제를 위한 cleanup 함수
useEffect(() => { useEffect(() => {
return () => { return () => {
console.log('[ProductId] UserReviewPanel unmounting - clearing filters'); console.log("[ProductId] UserReviewPanel unmounting - clearing filters");
clearAllFilters(); // 필터 상태 초기화로 메모리 해제 clearAllFilters(); // 필터 상태 초기화로 메모리 해제
}; };
}, [clearAllFilters]); }, [clearAllFilters]);
@@ -186,22 +244,33 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
userReviewPanelReviewsLength: userReviewPanelReviews.length, // UserReviewPanel에서 표시할 4개 userReviewPanelReviewsLength: userReviewPanelReviews.length, // UserReviewPanel에서 표시할 4개
currentFilter: currentFilter, currentFilter: currentFilter,
filterCounts: filterCounts, filterCounts: filterCounts,
isDataFromCache: true // API 호출 없이 캐시된 데이터 사용 isDataFromCache: true, // API 호출 없이 캐시된 데이터 사용
}); });
} catch (error) { } catch (error) {
console.error("[ProductId] UserReviewPanel 로그 오류:", error); console.error("[ProductId] UserReviewPanel 로그 오류:", error);
} }
}, [reviewCount, filteredCount, avgRating, displayReviews, userReviewPanelReviews, currentFilter, filterCounts]); }, [
reviewCount,
filteredCount,
avgRating,
displayReviews,
userReviewPanelReviews,
currentFilter,
filterCounts,
]);
const handleBackButton = useCallback(() => { const handleBackButton = useCallback(() => {
console.log(`[ProductId] Back button clicked - returning to DetailPanel`); console.log(`[ProductId] Back button clicked - returning to DetailPanel`);
dispatch(popPanel()); dispatch(popPanel());
}, [dispatch]); }, [dispatch]);
const handleCancel = useCallback((e) => { const handleCancel = useCallback(
dispatch(popPanel()); (e) => {
e.stopPropagation(); dispatch(popPanel());
}, [dispatch]); e.stopPropagation();
},
[dispatch]
);
return ( return (
<TPanel <TPanel
@@ -259,7 +328,9 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
{/* 왼쪽 필터 영역 */} {/* 왼쪽 필터 영역 */}
<div className={css.reviewsSection__filters}> <div className={css.reviewsSection__filters}>
<div className={css.reviewsSection__filters__title}> <div className={css.reviewsSection__filters__title}>
<div className={css.reviewsSection__filters__title__text}>Filter Reviews</div> <div className={css.reviewsSection__filters__title__text}>
Filter Reviews
</div>
</div> </div>
{/* 모든 필터들을 묶는 컨테이너 */} {/* 모든 필터들을 묶는 컨테이너 */}
@@ -267,60 +338,82 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
{/* Rating 필터 섹션 */} {/* Rating 필터 섹션 */}
<div className={css.reviewsSection__filters__section}> <div className={css.reviewsSection__filters__section}>
<div className={css.reviewsSection__filters__sectionTitle}> <div className={css.reviewsSection__filters__sectionTitle}>
<div className={css.reviewsSection__filters__sectionTitle__text}>Rating</div> <div
className={css.reviewsSection__filters__sectionTitle__text}
>
Rating
</div>
</div> </div>
<div className={css.reviewsSection__filters__group}> <div className={css.reviewsSection__filters__group}>
<FilterItemButton <FilterItemButton
text={`All stars(${filterCounts?.rating?.all || reviewCount || 0})`} text={`All star(${filterCounts?.rating?.all || reviewCount || 0})`}
onClick={handleAllStarsFilter} onClick={handleAllStarsFilter}
spotlightId="filter-all-stars" spotlightId="filter-all-stars"
ariaLabel="Filter by all star ratings" ariaLabel="Filter by all star ratings"
dataSpotlightDown="filter-5-stars" dataSpotlightDown="filter-5-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 'all'} isActive={
currentFilter.type === "rating" &&
currentFilter.value === "all"
}
/> />
<FilterItemButton <FilterItemButton
text={`5 stars (${filterCounts?.rating?.[5] || 0})`} text={`5 star (${filterCounts?.rating?.[5] || 0})`}
onClick={handle5StarsFilter} onClick={handle5StarsFilter}
spotlightId="filter-5-stars" spotlightId="filter-5-stars"
ariaLabel="Filter by 5 star ratings" ariaLabel="Filter by 5 star ratings"
dataSpotlightUp="filter-all-stars" dataSpotlightUp="filter-all-stars"
dataSpotlightDown="filter-4-stars" dataSpotlightDown="filter-4-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 5} isActive={
currentFilter.type === "rating" &&
currentFilter.value === 5
}
/> />
<FilterItemButton <FilterItemButton
text={`4 stars (${filterCounts?.rating?.[4] || 0})`} text={`4 star (${filterCounts?.rating?.[4] || 0})`}
onClick={handle4StarsFilter} onClick={handle4StarsFilter}
spotlightId="filter-4-stars" spotlightId="filter-4-stars"
ariaLabel="Filter by 4 star ratings" ariaLabel="Filter by 4 star ratings"
dataSpotlightUp="filter-5-stars" dataSpotlightUp="filter-5-stars"
dataSpotlightDown="filter-3-stars" dataSpotlightDown="filter-3-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 4} isActive={
currentFilter.type === "rating" &&
currentFilter.value === 4
}
/> />
<FilterItemButton <FilterItemButton
text={`3 stars (${filterCounts?.rating?.[3] || 0})`} text={`3 star (${filterCounts?.rating?.[3] || 0})`}
onClick={handle3StarsFilter} onClick={handle3StarsFilter}
spotlightId="filter-3-stars" spotlightId="filter-3-stars"
ariaLabel="Filter by 3 star ratings" ariaLabel="Filter by 3 star ratings"
dataSpotlightUp="filter-4-stars" dataSpotlightUp="filter-4-stars"
dataSpotlightDown="filter-2-stars" dataSpotlightDown="filter-2-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 3} isActive={
currentFilter.type === "rating" &&
currentFilter.value === 3
}
/> />
<FilterItemButton <FilterItemButton
text={`2 stars (${filterCounts?.rating?.[2] || 0})`} text={`2 star (${filterCounts?.rating?.[2] || 0})`}
onClick={handle2StarsFilter} onClick={handle2StarsFilter}
spotlightId="filter-2-stars" spotlightId="filter-2-stars"
ariaLabel="Filter by 2 star ratings" ariaLabel="Filter by 2 star ratings"
dataSpotlightUp="filter-3-stars" dataSpotlightUp="filter-3-stars"
dataSpotlightDown="filter-1-stars" dataSpotlightDown="filter-1-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 2} isActive={
currentFilter.type === "rating" &&
currentFilter.value === 2
}
/> />
<FilterItemButton <FilterItemButton
text={`1 stars (${filterCounts?.rating?.[1] || 0})`} text={`1 star (${filterCounts?.rating?.[1] || 0})`}
onClick={handle1StarsFilter} onClick={handle1StarsFilter}
spotlightId="filter-1-stars" spotlightId="filter-1-stars"
ariaLabel="Filter by 1 star ratings" ariaLabel="Filter by 1 star ratings"
dataSpotlightUp="filter-2-stars" dataSpotlightUp="filter-2-stars"
isActive={currentFilter.type === 'rating' && currentFilter.value === 1} isActive={
currentFilter.type === "rating" &&
currentFilter.value === 1
}
/> />
</div> </div>
</div> </div>
@@ -328,7 +421,11 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
{/* Keywords 필터 섹션 */} {/* Keywords 필터 섹션 */}
<div className={css.reviewsSection__filters__section}> <div className={css.reviewsSection__filters__section}>
<div className={css.reviewsSection__filters__sectionTitle}> <div className={css.reviewsSection__filters__sectionTitle}>
<div className={css.reviewsSection__filters__sectionTitle__text}>Keywords</div> <div
className={css.reviewsSection__filters__sectionTitle__text}
>
Keywords
</div>
</div> </div>
<div className={css.reviewsSection__filters__group}> <div className={css.reviewsSection__filters__group}>
<FilterItemButton <FilterItemButton
@@ -369,7 +466,11 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
{/* Sentiment 필터 섹션 */} {/* Sentiment 필터 섹션 */}
<div className={css.reviewsSection__filters__section}> <div className={css.reviewsSection__filters__section}>
<div className={css.reviewsSection__filters__sectionTitle}> <div className={css.reviewsSection__filters__sectionTitle}>
<div className={css.reviewsSection__filters__sectionTitle__text}>Sentiment</div> <div
className={css.reviewsSection__filters__sectionTitle__text}
>
Sentiment
</div>
</div> </div>
<div className={css.reviewsSection__filters__group}> <div className={css.reviewsSection__filters__group}>
<FilterItemButton <FilterItemButton
@@ -379,7 +480,10 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
ariaLabel="Filter by positive sentiment" ariaLabel="Filter by positive sentiment"
dataSpotlightUp="filter-quality" dataSpotlightUp="filter-quality"
dataSpotlightDown="filter-negative" dataSpotlightDown="filter-negative"
isActive={currentFilter.type === 'sentiment' && currentFilter.value === 'positive'} isActive={
currentFilter.type === "sentiment" &&
currentFilter.value === "positive"
}
/> />
<FilterItemButton <FilterItemButton
text={`Negative (${filterCounts?.sentiment?.negative || 0})`} text={`Negative (${filterCounts?.sentiment?.negative || 0})`}
@@ -387,7 +491,10 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
spotlightId="filter-negative" spotlightId="filter-negative"
ariaLabel="Filter by negative sentiment" ariaLabel="Filter by negative sentiment"
dataSpotlightUp="filter-positive" dataSpotlightUp="filter-positive"
isActive={currentFilter.type === 'sentiment' && currentFilter.value === 'negative'} isActive={
currentFilter.type === "sentiment" &&
currentFilter.value === "negative"
}
/> />
</div> </div>
</div> </div>

View File

@@ -5,35 +5,35 @@
// TButton 기본 스타일 무력화 // TButton 기본 스타일 무력화
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
// FilterItem 레이아웃 스타일 (요구사항에 맞는 스타일 적용) // FilterItem 레이아웃 스타일 (요구사항에 맞는 스타일 적용)
display: flex; display: flex;
padding: 20px; padding: 20px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
border-radius: 100px; border-radius: 100px;
border: 1px solid #DADADA; border: 1px solid #dadada;
background: #FFF; background: #fff;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
// 고정 넓이와 마진 설정으로 일정한 간격 유지 (한 라인에 4개 표시) // 고정 넓이와 마진 설정으로 일정한 간격 유지 (한 라인에 4개 표시)
width: 90px; width: 90px;
margin-right: 40px; margin-right: 15px;
margin-bottom: 10px; margin-bottom: 10px;
// 각 라인의 마지막 아이템은 오른쪽 마진 제거 // 각 라인의 마지막 아이템은 오른쪽 마진 제거
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
// 포커스 상태: 빨간색 배경 // 포커스 상태: 빨간색 배경
&:focus { &:focus {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
background: #D32F2F !important; // 빨간색 background: #d32f2f !important; // 빨간색
border: 1px solid #D32F2F !important; border: 1px solid #d32f2f !important;
.filterItemButton__text { .filterItemButton__text {
color: white !important; color: white !important;
} }
@@ -45,9 +45,9 @@
padding: 20px; padding: 20px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
background: #7A808D !important; // 회색 (선택됨) background: #7a808d !important; // 회색 (선택됨)
border-radius: 100px; border-radius: 100px;
border: 1px solid #7A808D !important; border: 1px solid #7a808d !important;
white-space: nowrap; white-space: nowrap;
} }
@@ -55,7 +55,7 @@
text-align: center; text-align: center;
color: black; color: black;
font-size: 24px; font-size: 24px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
@@ -63,7 +63,7 @@
text-align: center; text-align: center;
color: white; color: white;
font-size: 24px; font-size: 24px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
} }
@@ -85,4 +85,4 @@
padding-right: 0; padding-right: 0;
} }
} }
} }