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

- 디테일부분에 대한 figma 부분과 동일하게 스타일 처리.
 - 리뷰부분 및 리뷰 팝업 부분 처리중
This commit is contained in:
junghoon86.park
2025-09-09 14:22:10 +09:00
parent 76ea1c439c
commit 8fea52fb32
16 changed files with 1326 additions and 528 deletions

View File

@@ -39,12 +39,12 @@ import ShowUserReviews from "../../UserReview/ShowUserReviews";
// CSS imports // CSS imports
// import infoCSS from "../ProductInfoSection/ProductInfoSection.module.less"; // import infoCSS from "../ProductInfoSection/ProductInfoSection.module.less";
// import contentCSS from "../ProductContentSection/ProductContentSection.module.less"; // import contentCSS from "../ProductContentSection/ProductContentSection.module.less";
import css from "./ProductAllSection.module.less"; 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"
}, },
@@ -52,10 +52,10 @@ const Container = SpotlightContainerDecorator(
); );
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",
@@ -65,9 +65,9 @@ const ContentContainer = SpotlightContainerDecorator(
); );
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"
}, },
@@ -110,7 +110,7 @@ const extractProductMeta = (productInfo) => ({
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
const LayoutSample = ({ onClick }) => ( const LayoutSample = ({ onClick }) => (
<SpottableComponent <SpottableComponent
className={css.layoutSample} className={css.layoutSample}
onClick={onClick} onClick={onClick}
spotlightId="layout-sample-button" spotlightId="layout-sample-button"
@@ -132,16 +132,16 @@ export default function ProductAllSection({
themeProductInfo, themeProductInfo,
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const productData = useMemo(() => const productData = useMemo(() =>
getProductData(productType, themeProductInfo, productInfo), getProductData(productType, themeProductInfo, productInfo),
[productType, themeProductInfo, productInfo] [productType, themeProductInfo, productInfo]
); );
// useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당 // useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당
const { const {
previewReviews, previewReviews,
stats, stats,
isLoading: reviewsLoading, isLoading: reviewsLoading,
hasReviews // 리뷰 존재 여부 플래그 추가 hasReviews // 리뷰 존재 여부 플래그 추가
} = useReviews(productData.prdtId); } = useReviews(productData.prdtId);
@@ -179,7 +179,7 @@ export default function ProductAllSection({
}) })
); );
}, [dispatch, productData, stats]); }, [dispatch, productData, stats]);
// 디버깅: 실제 이미지 데이터 확인 // 디버깅: 실제 이미지 데이터 확인
useEffect(() => { useEffect(() => {
console.log("[ProductId] ProductAllSection productData check:", { console.log("[ProductId] ProductAllSection productData check:", {
@@ -206,7 +206,7 @@ export default function ProductAllSection({
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false); const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
// useReviews에서 모든 리뷰 데이터 관리 // useReviews에서 모든 리뷰 데이터 관리
const reviewTotalCount = stats.totalReviews; 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 } };
@@ -281,7 +281,7 @@ export default function ProductAllSection({
<HorizontalContainer className={css.detailArea}> <HorizontalContainer className={css.detailArea}>
{/* Left Margin Section - 60px */} {/* Left Margin Section - 60px */}
<div className={css.leftMarginSection}></div> <div className={css.leftMarginSection}></div>
{/* Info Section - 645px */} {/* Info Section - 645px */}
<div className={css.infoSection}> <div className={css.infoSection}>
<Container <Container
@@ -292,7 +292,7 @@ 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 && 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"}
@@ -309,7 +309,11 @@ export default function ProductAllSection({
productType={productType} productType={productType}
> >
<div className={css.qrWrapper}> <div className={css.qrWrapper}>
<QRCode productInfo={productData} productType={productType} /> <QRCode
productInfo={productData}
productType={productType}
kind={"detail"}
/>
</div> </div>
</ProductOverview> </ProductOverview>
@@ -332,6 +336,7 @@ export default function ProductAllSection({
selectedPrdtId={panelInfo && panelInfo.prdtId} selectedPrdtId={panelInfo && panelInfo.prdtId}
favoriteFlag={favoriteFlag} favoriteFlag={favoriteFlag}
onFavoriteFlagChanged={onFavoriteFlagChanged} onFavoriteFlagChanged={onFavoriteFlagChanged}
kind={"item_detail"}
/> />
</div> </div>
)} )}
@@ -363,7 +368,7 @@ export default function ProductAllSection({
{$L("PRODUCT DETAILS")} {$L("PRODUCT DETAILS")}
</TButton> </TButton>
{hasReviews && ( {hasReviews && (
<TButton <TButton
className={css.userReviewsButton} className={css.userReviewsButton}
onClick={handleUserReviewsClick} onClick={handleUserReviewsClick}
spotlightId="user-reviews-button" spotlightId="user-reviews-button"
@@ -430,14 +435,14 @@ export default function ProductAllSection({
<div id="product-details-section"> <div id="product-details-section">
{productData && productData.imgUrls600 && productData.imgUrls600.length > 0 ? ( {productData && productData.imgUrls600 && productData.imgUrls600.length > 0 ? (
productData.imgUrls600.map((image, index) => ( productData.imgUrls600.map((image, index) => (
<ProductDetail <ProductDetail
key={`product-detail-${index}`} key={`product-detail-${index}`}
productInfo={{ productInfo={{
...productData, ...productData,
singleImage: image, singleImage: image,
imageIndex: index, imageIndex: index,
totalImages: productData.imgUrls600.length totalImages: productData.imgUrls600.length
}} }}
/> />
)) ))
) : ( ) : (
@@ -452,8 +457,8 @@ export default function ProductAllSection({
<> <>
<div id="scroll-marker-user-reviews" className={css.scrollMarker}></div> <div id="scroll-marker-user-reviews" className={css.scrollMarker}></div>
<div id="user-reviews-section"> <div id="user-reviews-section">
<UserReviews <UserReviews
productInfo={productData} productInfo={productData}
panelInfo={panelInfo} panelInfo={panelInfo}
reviewsData={{ reviewsData={{
previewReviews: previewReviews.slice(0, 5), // 처음 5개만 previewReviews: previewReviews.slice(0, 5), // 처음 5개만
@@ -489,4 +494,4 @@ export default function ProductAllSection({
ProductAllSection.propTypes = { ProductAllSection.propTypes = {
productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired, productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired,
}; };

View File

@@ -11,7 +11,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
position: relative; // 절대 위치 기준점 position: relative; // 절대 위치 기준점
// Spotlight 좌우 이동을 위한 설정 // Spotlight 좌우 이동을 위한 설정
&:focus-within { &:focus-within {
outline: none; outline: none;
@@ -35,21 +35,24 @@
position: absolute; position: absolute;
left: 60px; left: 60px;
top: 0; top: 0;
width: 645px; width: 650px;
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
> div {
height: 339px;
}
} }
// 3. Content Section - 1180px (1114px 콘텐츠 + 66px 스크롤바) // 3. Content Section - 1180px (1114px 콘텐츠 + 66px 스크롤바)
.contentSection { .contentSection {
position: absolute; position: absolute;
left: 705px; // 60px + 645px left: 705px; // 60px + 645px
top: 0; top: 0;
width: 1200px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바 width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -84,11 +87,13 @@
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
// gap 대신 margin 사용 (Chromium 68 호환성) // gap 대신 margin 사용 (Chromium 68 호환성)
> * { > * {
margin-bottom: 5px; margin-bottom: 5px;
&:last-child { margin-bottom: 0; } &:last-child {
margin-bottom: 0;
}
} }
} }
@@ -114,7 +119,6 @@
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
position: relative; // 자식 absolute 요소의 기준점 position: relative; // 자식 absolute 요소의 기준점
// 스크롤러 오버라이드 (1210px = 30px + content + 스크롤바) // 스크롤러 오버라이드 (1210px = 30px + content + 스크롤바)
.scrollerOverride { .scrollerOverride {
@@ -139,17 +143,16 @@
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #9C9C9C; // 스크롤바 색상 background: #9c9c9c; // 스크롤바 색상
border-radius: 3px; // 스크롤바 둥근 모서리 border-radius: 3px; // 스크롤바 둥근 모서리
} }
// 스크롤바 thumb에 hover 효과 적용 // 스크롤바 thumb에 hover 효과 적용
&:hover::-webkit-scrollbar-thumb { &:hover::-webkit-scrollbar-thumb {
background: #C72054; background: #c72054;
} }
} }
// 내부 콘텐츠는 별도 너비 계산 없이 100%를 사용 // 내부 콘텐츠는 별도 너비 계산 없이 100%를 사용
> div { > div {
width: 100%; // 부모의 패딩을 제외한 나머지 공간(1114px)을 모두 사용 width: 100%; // 부모의 패딩을 제외한 나머지 공간(1114px)을 모두 사용
@@ -174,7 +177,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
// ProductDetail.new 컴포넌트들 // ProductDetail.new 컴포넌트들
> div[class*="rollingWrap"] { > div[class*="rollingWrap"] {
width: 100% !important; // 부모 영역 전체 사용 width: 100% !important; // 부모 영역 전체 사용
@@ -182,9 +185,9 @@
box-sizing: border-box; box-sizing: border-box;
} }
} }
#product-description-section, #product-description-section,
#user-reviews-section, #user-reviews-section,
#you-may-also-like-section { #you-may-also-like-section {
width: 100%; // 부모 콘텐츠 영역 전체 사용 width: 100%; // 부모 콘텐츠 영역 전체 사용
max-width: none; max-width: none;
@@ -192,13 +195,13 @@
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
overflow: visible; overflow: visible;
> * { > * {
max-width: 100% !important; // 부모 영역 전체 사용 max-width: 100% !important; // 부모 영역 전체 사용
width: 100% !important; width: 100% !important;
box-sizing: border-box; box-sizing: border-box;
} }
// 이미지들이 컨테이너를 넘지 않도록 // 이미지들이 컨테이너를 넘지 않도록
img { img {
max-width: 100% !important; max-width: 100% !important;
@@ -229,7 +232,6 @@
// (중복 제거됨) 최상위 스크롤러/섹션 정의는 .scrollerWrapper 중첩 내부로 이동 // (중복 제거됨) 최상위 스크롤러/섹션 정의는 .scrollerWrapper 중첩 내부로 이동
// ProductDetailCard 스타일 참고 - 크기/간격만 적용 // ProductDetailCard 스타일 참고 - 크기/간격만 적용
// 헤더 컨텐츠 영역 (태그, 별점) // 헤더 컨텐츠 영역 (태그, 별점)
@@ -237,7 +239,6 @@
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
} }
// 모바일 쇼핑 섹션 (mobileSection 참고) // 모바일 쇼핑 섹션 (mobileSection 참고)
@@ -247,10 +248,12 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
> * { > * {
margin-right: 6px; margin-right: 6px;
&:last-child { margin-right: 0; } &:last-child {
margin-right: 0;
}
} }
} }
@@ -258,15 +261,18 @@
flex: 1 1 0 !important; flex: 1 1 0 !important;
width: auto !important; // flex로 크기 조정 width: auto !important; // flex로 크기 조정
height: 60px !important; height: 60px !important;
background: rgba(68, 68, 68, 0.50) !important; background: rgba(68, 68, 68, 0.5) !important;
border-radius: 6px !important; border-radius: 6px !important;
border: none !important; border: none !important;
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
justify-content: center !important; justify-content: center !important;
&.shopByMobileOne {
margin: 0 6px 0 0;
}
.shopByMobileText { .shopByMobileText {
color: white !important; color: white !important;
font-size: 25px !important; font-size: 25px !important;
@@ -275,12 +281,484 @@
line-height: 35px !important; line-height: 35px !important;
text-align: center !important; text-align: center !important;
} }
// 포커스 상태 추가 // 포커스 상태 추가
&:focus { &:focus {
background: @PRIMARY_COLOR_RED !important; // 포커스시 빨간색 배경 background: @PRIMARY_COLOR_RED !important; // 포커스시 빨간색 배경
outline: 2px solid @PRIMARY_COLOR_RED !important; outline: 2px solid @PRIMARY_COLOR_RED !important;
.shopByMobileText {
color: white !important; // 포커스시에도 텍스트는 흰색 유지
}
}
}
.favoriteBtnWrapper {
width: 60px;
height: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-left: 6px;
// 배경색과 라운드는 FavoriteBtn 내부에서 처리하므로 제거
}
// 주문 전화 섹션 (callToOrderSection 참고)
.callToOrderSection {
align-self: stretch;
height: 40px;
padding: 17px 30px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px !important;
.callToOrderText {
color: #eaeaea;
font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경
line-height: 35px;
}
.phoneSection {
padding: 0 1px;
display: flex;
align-items: center;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.phoneIconContainer {
width: 25px;
height: 25px;
position: relative;
overflow: hidden;
.phoneIcon {
width: 24.94px;
height: 24.97px;
position: absolute;
left: 0;
top: 0;
// 전화 아이콘 이미지 또는 CSS로 구현
background-image: url("../../../../assets/images/icons/ic-gr-call-1.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
}
.phoneNumber {
color: #eaeaea;
font-size: 25px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 35px;
}
}
}
// 액션 버튼들 (actionButtons 참고)
.actionButtonsWrapper {
align-self: stretch;
padding-top: 20px;
display: flex;
flex-direction: column;
> * {
margin-bottom: 5px;
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
padding: 0;
}
}
// 모든 버튼 기본 스타일 (PRODUCT DETAILS는 빨간색 아님!)
.productDetailsButton,
.userReviewsButton,
.youMayLikeButton {
align-self: stretch;
height: 60px;
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #eaeaea;
font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경
line-height: 35px;
&:focus {
background: #c72054; // 포커스시만 빨간색
}
}
.themeButton {
align-self: stretch;
height: 60px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
margin-top: 10px;
&:focus {
background: #c72054;
}
}
// QR 래퍼 (imageSection 참고 - 240px 고정)
.qrWrapper {
width: 240px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
// ProductOverview 컨테이너 스타일 수정 (자식 요소에 맞게 크기 조정)
[class*="ProductOverview"] {
padding: 0 0 5px;
// 내부 div (productInfoWrapper)
> div {
align-self: stretch;
justify-content: flex-start;
align-items: flex-start;
}
> div:first-child {
text-align: left;
}
}
// LayoutSample 포커스 테스트용 스타일
.layoutSample {
width: 1124px;
height: 300px;
background-color: yellow;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
color: black;
font-size: 24px;
font-weight: bold;
cursor: pointer;
position: relative;
border-radius: 8px;
&:focus {
&::after {
.focused(@boxShadow:22px, @borderRadius:8px);
}
}
}
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
// 1920px 화면 기준 전체 구조 (절대 위치로 정확히 배치)
.detailArea {
width: 1920px; // 명시적으로 화면 크기 설정
height: 100%;
padding: 0; // 모든 패딩 제거
margin: 0; // 모든 마진 제거
display: flex;
justify-content: flex-start;
align-items: flex-start;
position: relative; // 절대 위치 기준점
// Spotlight 좌우 이동을 위한 설정
&:focus-within {
outline: none;
}
}
// 1. Left Margin Section - 60px
.leftMarginSection {
position: absolute;
left: 0;
top: 0;
width: 60px;
height: 100%;
padding: 0;
margin: 0;
background: transparent;
}
// 2. Info Section - 645px
.infoSection {
position: absolute;
left: 60px;
top: 0;
width: 650px;
height: 100%;
padding: 0;
margin: 0;
display: flex;
justify-content: flex-start;
align-items: flex-start;
> div {
height: 339px;
}
}
// 3. Content Section - 1180px (1114px 콘텐츠 + 66px 스크롤바)
.contentSection {
position: absolute;
left: 705px; // 60px + 645px
top: 0;
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
height: 100%;
padding: 0;
margin: 0;
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
// 4. Scroll Section - 66px (삭제 - contentSection에 포함)
.scrollSection {
display: none; // 사용하지 않음
}
// 왼쪽 영역 컨테이너 (infoSection 내부)
.leftInfoContainer {
width: 635px; // 실제 콘텐츠 영역
margin-right: 10px; // 우측 10px 간격
padding: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
height: 100%;
}
// 왼쪽 영역 내부 래퍼
.leftInfoWrapper {
width: 100%;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
// gap 대신 margin 사용 (Chromium 68 호환성)
> * {
margin-bottom: 5px;
&:last-child {
margin-bottom: 0;
}
}
}
// 오른쪽 영역 컨테이너 (contentSection 내부)
.rightContentContainer {
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
height: @globalHeight - 136px;
padding: 0;
margin: 0;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: flex-start;
// 스크롤러 래퍼 (contentSection 내부)
.scrollerWrapper {
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바 (절대값)
height: 100%;
padding: 0;
margin: 0;
overflow: visible; // hidden에서 visible로 변경
display: flex;
justify-content: flex-start;
align-items: flex-start;
position: relative; // 자식 absolute 요소의 기준점
// 스크롤러 오버라이드 (1210px = 30px + content + 스크롤바)
.scrollerOverride {
width: 1210px; // 절대 크기 지정
height: 100%;
// 좌측 30px, 우측 66px(스크롤바) 패딩을 명시적으로 적용
padding: 0 10px 0 30px;
box-sizing: border-box;
margin: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
// 스크롤바 너비를 6px로 명확하게 설정
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent; // 트랙 배경은 투명하게
}
&::-webkit-scrollbar-thumb {
background: #9c9c9c; // 스크롤바 색상
border-radius: 3px; // 스크롤바 둥근 모서리
}
// 스크롤바 thumb에 hover 효과 적용
&:hover::-webkit-scrollbar-thumb {
background: #c72054;
}
}
// 내부 콘텐츠는 별도 너비 계산 없이 100%를 사용
> div {
width: 100%; // 부모의 패딩을 제외한 나머지 공간(1114px)을 모두 사용
padding: 0;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
overflow: visible;
}
// 스크롤 콘텐츠 내부의 모든 섹션들 (패딩 제거 - scrollerOverride에서 처리함)
#product-details-section {
width: 1124px; // 부모 콘텐츠 영역 전체 사용
max-width: none;
padding: 0;
margin: 0;
box-sizing: border-box;
overflow: visible;
display: flex;
flex-direction: column;
align-items: flex-start;
// ProductDetail.new 컴포넌트들
> div[class*="rollingWrap"] {
width: 100% !important; // 부모 영역 전체 사용
max-width: none !important;
box-sizing: border-box;
}
}
#product-description-section,
#user-reviews-section,
#you-may-also-like-section {
width: 100%; // 부모 콘텐츠 영역 전체 사용
max-width: none;
padding: 0;
margin: 0;
box-sizing: border-box;
overflow: visible;
> * {
max-width: 100% !important; // 부모 영역 전체 사용
width: 100% !important;
box-sizing: border-box;
}
// 이미지들이 컨테이너를 넘지 않도록
img {
max-width: 100% !important;
width: auto !important;
height: auto;
}
}
// 상품 상세 영역 (마진 포함 크기)
.productDetail {
width: 1124px; // 1114px + 10px
max-width: 1124px;
height: auto;
display: flex;
flex-direction: column;
align-items: stretch; // 자식 요소들이 전체 너비 사용하도록
}
// 스크롤 마커
.scrollMarker {
height: 1px;
visibility: hidden;
}
} // .scrollerWrapper
} // .rightContentContainer
// (중복 제거됨) .scrollerWrapper는 .rightContentContainer 하위로 중첩 이동
// (중복 제거됨) 최상위 스크롤러/섹션 정의는 .scrollerWrapper 중첩 내부로 이동
// ProductDetailCard 스타일 참고 - 크기/간격만 적용
// 헤더 컨텐츠 영역 (태그, 별점)
.headerContent {
width: 100%;
display: flex;
align-items: center;
}
// 모바일 쇼핑 섹션 (mobileSection 참고)
.buttonContainer {
align-self: stretch;
padding-top: 19px;
display: flex;
justify-content: flex-start;
align-items: center;
> * {
margin-right: 6px;
&:last-child {
margin-right: 0;
}
}
}
.shopByMobileButton {
flex: 1 1 0 !important;
width: auto !important; // flex로 크기 조정
height: 60px !important;
background: rgba(68, 68, 68, 0.5) !important;
border-radius: 6px !important;
border: none !important;
padding: 0 !important;
margin: 0;
display: flex !important;
align-items: center !important;
justify-content: center !important;
&.shopByMobileOne {
margin: 0 6px 0 0;
}
.shopByMobileText {
color: white !important;
font-size: 25px !important;
font-family: @baseFont !important; // LG Smart 폰트 사용
font-weight: 400 !important; // Bold에서 Regular로 변경
line-height: 35px !important;
text-align: center !important;
}
// 포커스 상태 추가
&:focus {
background: @PRIMARY_COLOR_RED !important; // 포커스시 빨간색 배경
outline: 2px solid @PRIMARY_COLOR_RED !important;
.shopByMobileText { .shopByMobileText {
color: white !important; // 포커스시에도 텍스트는 흰색 유지 color: white !important; // 포커스시에도 텍스트는 흰색 유지
} }
@@ -306,38 +784,39 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.callToOrderText { .callToOrderText {
color: #EAEAEA; color: #eaeaea;
font-size: 25px; font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용 font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경 font-weight: 400; // Bold에서 Regular로 변경
line-height: 35px; line-height: 35px;
} }
.phoneSection { .phoneSection {
padding: 0 1px; padding: 0 1px;
display: flex; display: flex;
align-items: center; align-items: center;
> * { > * {
margin-right: 10px; margin-right: 10px;
&:last-child { margin-right: 0; } &:last-child {
margin-right: 0;
}
} }
.phoneIconContainer { .phoneIconContainer {
width: 25px; width: 25px;
height: 25px; height: 25px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
.phoneIcon { .phoneIcon {
width: 24.94px; width: 24.94px;
height: 24.97px; height: 24.97px;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
background: #EAEAEA;
// 전화 아이콘 이미지 또는 CSS로 구현 // 전화 아이콘 이미지 또는 CSS로 구현
background-image: url("../../../../assets/images/icons/ic-gr-call-1.png"); background-image: url("../../../../assets/images/icons/ic-gr-call-1.png");
background-size: contain; background-size: contain;
@@ -345,11 +824,11 @@
background-position: center; background-position: center;
} }
} }
.phoneNumber { .phoneNumber {
color: #EAEAEA; color: #eaeaea;
font-size: 25px; font-size: 25px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 35px; line-height: 35px;
} }
@@ -362,10 +841,15 @@
padding-top: 20px; padding-top: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> * { > * {
margin-bottom: 5px; margin-bottom: 5px;
&:last-child { margin-bottom: 0; } &:last-child {
margin-bottom: 0;
}
}
&:last-child {
padding: 0;
} }
} }
@@ -380,15 +864,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #EAEAEA; color: #eaeaea;
font-size: 25px; font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용 font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경 font-weight: 400; // Bold에서 Regular로 변경
line-height: 35px; line-height: 35px;
&:focus { &:focus {
background: #C72054; // 포커스시만 빨간색 background: #c72054; // 포커스시만 빨간색
} }
} }
@@ -398,9 +882,9 @@
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 6px; border-radius: 6px;
margin-top: 10px; margin-top: 10px;
&:focus { &:focus {
background: #C72054; background: #c72054;
} }
} }
@@ -411,65 +895,34 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
> * { > * {
margin-bottom: 10px; margin-bottom: 10px;
&:last-child { margin-bottom: 0; } &:last-child {
margin-bottom: 0;
}
} }
} }
// ProductOverview 컨테이너 스타일 수정 (자식 요소에 맞게 크기 조정) // ProductOverview 컨테이너 스타일 수정 (자식 요소에 맞게 크기 조정)
[class*="ProductOverview"] { [class*="ProductOverview"] {
align-self: stretch; padding: 0 0 5px;
padding: 0 0 5px; // ProductDetailCard mainContent와 동일한 패딩
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
// 내부 div (productInfoWrapper) // 내부 div (productInfoWrapper)
> div { > div {
align-self: stretch; align-self: stretch;
display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
}
> * { > div:first-child {
margin-right: 15px; // ProductDetailCard와 동일한 간격 text-align: left;
&:last-child { margin-right: 0; }
}
// 가격 섹션 (flex로 남은 공간 차지)
> div:first-child {
flex: 1 1 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
// 이미지 섹션 (QR 포함, 고정 크기)
> div:last-child {
width: 240px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
> * {
margin-bottom: 10px;
&:last-child { margin-bottom: 0; }
}
}
} }
} }
// LayoutSample 포커스 테스트용 스타일 // LayoutSample 포커스 테스트용 스타일
.layoutSample { .layoutSample {
width: 1124px; width: 1124px;
height: 35px; height: 300px;
background-color: yellow; background-color: yellow;
// border: 2px solid white;
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -480,7 +933,7 @@
cursor: pointer; cursor: pointer;
position: relative; position: relative;
border-radius: 8px; border-radius: 8px;
&:focus { &:focus {
&::after { &::after {
.focused(@boxShadow:22px, @borderRadius:8px); .focused(@boxShadow:22px, @borderRadius:8px);

View File

@@ -17,7 +17,7 @@
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
&:focus { &:focus {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
outline: 2px solid @PRIMARY_COLOR_RED; outline: 2px solid @PRIMARY_COLOR_RED;
@@ -26,7 +26,7 @@
.title { .title {
.font(@fontFamily: @baseFont, @fontSize: 30px); .font(@fontFamily: @baseFont, @fontSize: 30px);
font-weight: 700; font-weight: 700;
margin: 90px 0 20px 0; margin: 30px 0 20px 0;
} }
} }
@@ -34,7 +34,7 @@
width: 100%; width: 100%;
border-radius: 12px; border-radius: 12px;
cursor: pointer; cursor: pointer;
&:focus { &:focus {
outline: 6px solid @PRIMARY_COLOR_RED; outline: 6px solid @PRIMARY_COLOR_RED;
outline-offset: 2px; outline-offset: 2px;

View File

@@ -1,30 +1,43 @@
import React, { useCallback, useEffect, useState } from "react"; import React, {
import css from "./CustomerImages.module.less"; useCallback,
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; useEffect,
import Spottable from "@enact/spotlight/Spottable"; useState,
import THeader from "../../../../../components/THeader/THeader"; } from 'react';
import { $L } from "../../../../../utils/helperMethods";
import classNames from "classnames"; import classNames from 'classnames';
import Spotlight from "@enact/spotlight";
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import THeader from '../../../../../components/THeader/THeader';
import { $L } from '../../../../../utils/helperMethods';
import css from './CustomerImages.module.less';
const Container = SpotlightContainerDecorator( const Container = 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",
} },
}, },
"div" "div"
); );
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
export default function CustomerImages({ onImageClick, onViewMoreClick, imageData }) { export default function CustomerImages({
onImageClick,
onViewMoreClick,
imageData,
}) {
// Props로 전달받은 imageData 사용 (useReviews Hook에서 추출된 이미지 데이터) // Props로 전달받은 imageData 사용 (useReviews Hook에서 추출된 이미지 데이터)
const [selectedIndex, setSelectedIndex] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const IMAGES_PER_PAGE = 5; const IMAGES_PER_PAGE = 5;
const [focusIdx, setFocusIdx] = useState(0);
// [CustomerImages] useReviews 이미지 데이터 수신 확인 // [CustomerImages] useReviews 이미지 데이터 수신 확인
// useEffect(() => { // useEffect(() => {
@@ -43,7 +56,7 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
const handleReviewImageClick = (index) => { const handleReviewImageClick = (index) => {
setSelectedIndex(index); setSelectedIndex(index);
// 이미지 클릭 시 All-Images 모드로 팝업 열기 // 이미지 클릭 시 All-Images 모드로 팝업 열기
if (onImageClick) { if (onImageClick) {
onImageClick(index); onImageClick(index);
@@ -58,25 +71,35 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
}; };
// 키 이벤트 처리 (왼쪽 화살표, Enter 키) // 키 이벤트 처리 (왼쪽 화살표, Enter 키)
const handleKeyDown = useCallback((ev, index) => { const handleKeyDown = useCallback(
if (ev.keyCode === 37) { // 왼쪽 화살표 키 (ev, index) => {
ev.preventDefault(); if (ev.keyCode === 37) {
ev.stopPropagation(); // 왼쪽 화살표 키
console.log("[CustomerImages] Left arrow pressed, focusing product-details-button"); ev.preventDefault();
Spotlight.focus("product-details-button"); ev.stopPropagation();
} else if (ev.keyCode === 13) { // Enter 키 console.log(
ev.preventDefault(); "[CustomerImages] Left arrow pressed, focusing product-details-button"
ev.stopPropagation(); );
console.log("[CustomerImages] Enter pressed on image:", index); Spotlight.focus("product-details-button");
handleReviewImageClick(index); } else if (ev.keyCode === 13) {
} // Enter 키
}, [handleReviewImageClick]); ev.preventDefault();
ev.stopPropagation();
console.log("[CustomerImages] Enter pressed on image:", index);
handleReviewImageClick(index);
}
},
[handleReviewImageClick]
);
// 이미지가 없을 때도 컴포넌트를 렌더링하되 내용은 표시하지 않음 // 이미지가 없을 때도 컴포넌트를 렌더링하되 내용은 표시하지 않음
return ( return (
<> <>
<Container className={css.container}> <Container className={css.container}>
<THeader className={css.tHeader} title={$L("Customer Images")} /> <THeader
className={classNames(css.tHeader, css.customTHeader)}
title={$L("Customer Images")}
/>
{imageData && imageData.length > 0 ? ( {imageData && imageData.length > 0 ? (
<div className={css.wrapper}> <div className={css.wrapper}>
{(() => { {(() => {
@@ -84,7 +107,7 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
const endIndex = startIndex + IMAGES_PER_PAGE; const endIndex = startIndex + IMAGES_PER_PAGE;
const displayImages = imageData.slice(startIndex, endIndex); const displayImages = imageData.slice(startIndex, endIndex);
const hasMoreImages = imageData.length > endIndex; const hasMoreImages = imageData.length > endIndex;
console.log("[CustomerImages] Pagination debug:", { console.log("[CustomerImages] Pagination debug:", {
currentPage, currentPage,
IMAGES_PER_PAGE, IMAGES_PER_PAGE,
@@ -92,9 +115,9 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
startIndex, startIndex,
endIndex, endIndex,
displayImagesCount: displayImages.length, displayImagesCount: displayImages.length,
hasMoreImages hasMoreImages,
}); });
return ( return (
<> <>
{displayImages.map((reviewImage, displayIndex) => { {displayImages.map((reviewImage, displayIndex) => {
@@ -105,16 +128,22 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
<SpottableComponent <SpottableComponent
className={classNames( className={classNames(
css.reviewCard, css.reviewCard,
selectedIndex === actualIndex ? css.selectedReviewImage : null selectedIndex === actualIndex
? css.selectedReviewImage
: null,
focusIdx === displayIndex ? css.focused : ""
)} )}
key={`review-image-${imgId}-${reviewId}`} key={`review-image-${imgId}-${reviewId}`}
onClick={() => handleReviewImageClick(actualIndex)} onClick={() => handleReviewImageClick(actualIndex)}
onKeyDown={(ev) => handleKeyDown(ev, actualIndex)} onKeyDown={(ev) => handleKeyDown(ev, actualIndex)}
spotlightId={`customer-image-${actualIndex}`} spotlightId={`customer-image-${actualIndex}`}
onFocus={() =>
displayIndex < 5 ? setFocusIdx(displayIndex) : ""
}
> >
<img <img
className={css.reviewImg} className={css.reviewImg}
src={imgUrl} src={imgUrl}
alt={`Review image ${actualIndex + 1}`} alt={`Review image ${actualIndex + 1}`}
onLoad={() => { onLoad={() => {
/* console.log(`[CustomerImages] Image loaded successfully:`, { /* console.log(`[CustomerImages] Image loaded successfully:`, {
@@ -124,19 +153,22 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
}); */ }); */
}} }}
onError={(e) => { onError={(e) => {
console.error(`[CustomerImages] Image load failed:`, { console.error(
index: actualIndex, `[CustomerImages] Image load failed:`,
imgUrl, {
imgId, index: actualIndex,
error: e.target.error imgUrl,
}); imgId,
e.target.style.display = 'none'; error: e.target.error,
}
);
e.target.style.display = "none";
}} }}
/> />
</SpottableComponent> </SpottableComponent>
); );
})} })}
{hasMoreImages && ( {hasMoreImages && (
<SpottableComponent <SpottableComponent
className={css.viewMoreButton} className={css.viewMoreButton}
@@ -161,13 +193,17 @@ export default function CustomerImages({ onImageClick, onViewMoreClick, imageDat
</div> </div>
) : ( ) : (
<div className={css.wrapper}> <div className={css.wrapper}>
<div style={{ <div
color: 'rgba(255, 255, 255, 0.7)', style={{
textAlign: 'center', color: "rgba(255, 255, 255, 0.7)",
padding: '20px', textAlign: "center",
fontSize: '16px' padding: "20px",
}}> fontSize: "16px",
{imageData ? 'No customer images available' : 'Loading customer images...'} }}
>
{imageData
? "No customer images available"
: "Loading customer images..."}
</div> </div>
</div> </div>
)} )}

View File

@@ -3,7 +3,7 @@
.container { .container {
width: 1124px; width: 1124px;
height: 236px; height: 299px;
max-width: 1124px; max-width: 1124px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -14,7 +14,7 @@
margin: 0 0 10px 0; margin: 0 0 10px 0;
> div { > div {
padding: 0; padding: 0 0 0 20px;
> span { > span {
.font(@fontFamily: @baseFont, @fontSize: 24px); .font(@fontFamily: @baseFont, @fontSize: 24px);
font-weight: 400; font-weight: 400;
@@ -26,7 +26,7 @@
.wrapper { .wrapper {
width: 100%; width: 100%;
height: 190px; height: 252px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
@@ -36,13 +36,25 @@
overflow: visible; overflow: visible;
.reviewCard { .reviewCard {
width: calc((100% - 75px) / 6); // 6개 슬롯(5이미지+1버튼), margin-right 15px * 5 = 75px // width: calc(
height: 150px; // (100% - 75px) / 6
// ); // 6개 슬롯(5이미지+1버튼), margin-right 15px * 5 = 75px
// height: 150px;
width: 157.3px;
height: 192.5px;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
margin-right: 15px; margin-right: 15px;
&.focused {
width: 211.75px;
height: 211.75px;
}
&:focus { &:focus {
//포커스시 확대
// width: 211.75px;
// height: 211.75px;
&::after { &::after {
.focused(@boxShadow:22px, @borderRadius:12px); .focused(@boxShadow:22px, @borderRadius:12px);
} }
@@ -61,13 +73,18 @@
} }
.viewMoreButton { .viewMoreButton {
width: calc((100% - 75px) / 6); // reviewCard와 동일한 크기 // width: calc((100% - 75px) / 6); // reviewCard와 동일한 크기
height: 150px; // height: 150px;
width: 157.3px;
height: 192.5px;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
cursor: pointer; cursor: pointer;
&:focus { &:focus {
// //포커스시 확대
// width: 211.75px;
// height: 211.75px;
&::after { &::after {
.focused(@boxShadow:22px, @borderRadius:12px); .focused(@boxShadow:22px, @borderRadius:12px);
} }
@@ -77,7 +94,11 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 4px; padding: 4px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.80) 100%); background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.8) 0%,
rgba(0, 0, 0, 0.8) 100%
);
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -1,9 +1,12 @@
import React, { useCallback } from "react"; import React, { useCallback } from 'react';
import classNames from "classnames";
import Spottable from "@enact/spotlight/Spottable"; import classNames from 'classnames';
import StarRating from "../../../components/StarRating";
import { $L } from "../../../../../utils/helperMethods"; import Spottable from '@enact/spotlight/Spottable';
import css from "./UserReviewDetail.module.less";
import { $L } from '../../../../../utils/helperMethods';
import StarRating from '../../../components/StarRating';
import css from './UserReviewDetail.module.less';
const SpottableButton = Spottable("div"); const SpottableButton = Spottable("div");
@@ -36,9 +39,15 @@ export default function UserReviewDetail({
); );
} }
const reviewImage = currentReview.reviewImageList && currentReview.reviewImageList[0]; const reviewImage =
currentReview.reviewImageList && currentReview.reviewImageList[0];
const hasMultipleReviews = totalReviews > 1; const hasMultipleReviews = totalReviews > 1;
const formatDate = (dateStr) => {
const [year, month, day] = dateStr.split("-");
return `${year.slice(2)}.${month}.${day}`;
};
return ( return (
<div className={classNames(css.container, className)}> <div className={classNames(css.container, className)}>
{/* Left Arrow - 이전 리뷰가 있을 때만 표시 */} {/* Left Arrow - 이전 리뷰가 있을 때만 표시 */}
@@ -57,14 +66,14 @@ export default function UserReviewDetail({
{/* Review Image */} {/* Review Image */}
{reviewImage && ( {reviewImage && (
<div className={css.imageSection}> <div className={css.imageSection}>
<img <img
src={reviewImage.imgUrl} src={reviewImage.imgUrl}
alt="Review image" alt="Review image"
className={css.reviewImage} className={css.reviewImage}
/> />
</div> </div>
)} )}
{/* Review Info */} {/* Review Info */}
<div className={css.infoSection}> <div className={css.infoSection}>
{/* Rating and Meta */} {/* Rating and Meta */}
@@ -78,31 +87,29 @@ export default function UserReviewDetail({
/> />
</div> </div>
)} )}
{/* Email */} {/* Email */}
{(currentReview.wrtrNknm || currentReview.rvwWrtrId) && ( {(currentReview.wrtrNknm || currentReview.rvwWrtrId) && (
<div className={css.email}> <div className={css.email}>
{currentReview.wrtrNknm || currentReview.rvwWrtrId} {currentReview.wrtrNknm || currentReview.rvwWrtrId}
</div> </div>
)} )}
{/* Date */} {/* Date */}
{currentReview.rvwRgstDtt && ( {currentReview.rvwRgstDtt && (
<div className={css.date}> <div className={css.date}>
{currentReview.rvwRgstDtt} {formatDate(currentReview.rvwRgstDtt)}
</div> </div>
)} )}
</div> </div>
{/* Review Text */} {/* Review Text */}
{currentReview.rvwCtnt && ( {currentReview.rvwCtnt && (
<div className={css.reviewText}> <div className={css.reviewText}>"{currentReview.rvwCtnt}"</div>
"{currentReview.rvwCtnt}"
</div>
)} )}
</div> </div>
</div> </div>
{/* Custom Scrollbar (피그마 스타일) */} {/* Custom Scrollbar (피그마 스타일) */}
<div className={css.customScrollbar}> <div className={css.customScrollbar}>
<div className={css.scrollTrack} /> <div className={css.scrollTrack} />
@@ -120,4 +127,4 @@ export default function UserReviewDetail({
)} )}
</div> </div>
); );
} }

View File

@@ -1,18 +1,32 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, {
import css from "./UserReviews.module.less"; useCallback,
import UserReviewsScroller from "../../components/UserReviewsScroller/UserReviewsScroller"; useEffect,
import useScrollTo from "../../../../hooks/useScrollTo"; useMemo,
import THeaderDetail from "../../components/THeaderDetail"; useRef,
import { $L } from "../../../../utils/helperMethods"; useState,
import { useMemo } from "react"; } from 'react';
import Spottable from "@enact/spotlight/Spottable";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; import classNames from 'classnames';
import { useDispatch, useSelector } from "react-redux"; import {
import { toggleShowAllReviews } from "../../../../actions/productActions"; useDispatch,
import useReviews from "../../../../hooks/useReviews/useReviews"; useSelector,
import StarRating from "../../components/StarRating"; } from 'react-redux';
import CustomerImages from "./CustomerImages/CustomerImages";
import UserReviewsPopup from "./UserReviewsPopup/UserReviewsPopup"; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { toggleShowAllReviews } from '../../../../actions/productActions';
import useReviews from '../../../../hooks/useReviews/useReviews';
import useScrollTo from '../../../../hooks/useScrollTo';
import { $L } from '../../../../utils/helperMethods';
import StarRating from '../../components/StarRating';
import THeaderDetail from '../../components/THeaderDetail';
import UserReviewsScroller
from '../../components/UserReviewsScroller/UserReviewsScroller';
import CustomerImages from './CustomerImages/CustomerImages';
import css from './UserReviews.module.less';
import UserReviewsPopup from './UserReviewsPopup/UserReviewsPopup';
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
@@ -22,10 +36,10 @@ const Container = SpotlightContainerDecorator(
preserveld: true, preserveld: true,
leaveFor: { leaveFor: {
left: "spotlight-product-info-section-container", left: "spotlight-product-info-section-container",
up: "view-all-reviews-button" up: "view-all-reviews-button",
}, },
restrict: "none", restrict: "none",
spotlightDirection: "vertical" spotlightDirection: "vertical",
}, },
"div" "div"
); );
@@ -35,32 +49,39 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerRef = useRef(null); const containerRef = useRef(null);
const tScrollerRef = useRef(null); const tScrollerRef = useRef(null);
// ProductAllSection에서 전달받은 데이터 사용 (우선순위) // ProductAllSection에서 전달받은 데이터 사용 (우선순위)
// 없으면 자체 useReviews Hook 사용 (UserReviewPanel에서 직접 접근할 때) // 없으면 자체 useReviews Hook 사용 (UserReviewPanel에서 직접 접근할 때)
const fallbackReviews = useReviews(productInfo && productInfo.prdtId); const fallbackReviews = useReviews(productInfo && productInfo.prdtId);
const actualReviewsData = reviewsData || { const actualReviewsData = reviewsData || {
previewReviews: fallbackReviews.previewReviews, previewReviews: fallbackReviews.previewReviews,
stats: fallbackReviews.stats, stats: fallbackReviews.stats,
isLoading: fallbackReviews.isLoading isLoading: fallbackReviews.isLoading,
}; };
// 팝업 상태 관리 - 모드와 선택된 이미지 인덱스 추가 // 팝업 상태 관리 - 모드와 선택된 이미지 인덱스 추가
const [isPopupOpen, setIsPopupOpen] = useState(false); const [isPopupOpen, setIsPopupOpen] = useState(false);
const [popupMode, setPopupMode] = useState("customer-images"); // "customer-images", "all-images" const [popupMode, setPopupMode] = useState("customer-images"); // "customer-images", "all-images"
const [selectedImageIndex, setSelectedImageIndex] = useState(0); const [selectedImageIndex, setSelectedImageIndex] = useState(0);
// Redux에서 showAllReviews 상태 가져오기 // Redux에서 showAllReviews 상태 가져오기
const showAllReviews = useSelector((state) => state.product.showAllReviews); const showAllReviews = useSelector((state) => state.product.showAllReviews);
// 디버깅: showAllReviews 상태 변경 확인 // 디버깅: showAllReviews 상태 변경 확인
useEffect(() => { useEffect(() => {
console.log("[UserReviews] showAllReviews state changed:", { console.log("[UserReviews] showAllReviews state changed:", {
showAllReviews, showAllReviews,
reviewListLength: (actualReviewsData.previewReviews && actualReviewsData.previewReviews.length) || 0, reviewListLength:
willShowCount: showAllReviews ? ((actualReviewsData.previewReviews && actualReviewsData.previewReviews.length) || 0) : 5, (actualReviewsData.previewReviews &&
actualReviewsData.previewReviews.length) ||
0,
willShowCount: showAllReviews
? (actualReviewsData.previewReviews &&
actualReviewsData.previewReviews.length) ||
0
: 5,
hasReviewsData: !!reviewsData, hasReviewsData: !!reviewsData,
isFromProductAllSection: !!reviewsData isFromProductAllSection: !!reviewsData,
}); });
}, [showAllReviews, actualReviewsData.previewReviews, reviewsData]); }, [showAllReviews, actualReviewsData.previewReviews, reviewsData]);
@@ -68,33 +89,36 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
useEffect(() => { useEffect(() => {
if (showAllReviews && tScrollerRef.current) { if (showAllReviews && tScrollerRef.current) {
// console.log("[UserReviews] Forcing TScroller to update scroll area for all reviews"); // console.log("[UserReviews] Forcing TScroller to update scroll area for all reviews");
// 다음 렌더링 사이클 후 스크롤 영역 재계산 // 다음 렌더링 사이클 후 스크롤 영역 재계산
setTimeout(() => { setTimeout(() => {
if (tScrollerRef.current) { if (tScrollerRef.current) {
// TScroller의 스크롤 영역을 강제로 업데이트 // TScroller의 스크롤 영역을 강제로 업데이트
if (typeof tScrollerRef.current.calculateMetrics === 'function') { if (typeof tScrollerRef.current.calculateMetrics === "function") {
tScrollerRef.current.calculateMetrics(); tScrollerRef.current.calculateMetrics();
} }
// 또는 scrollTo를 호출해서 스크롤 영역 업데이트 // 또는 scrollTo를 호출해서 스크롤 영역 업데이트
if (typeof tScrollerRef.current.scrollTo === 'function') { if (typeof tScrollerRef.current.scrollTo === "function") {
tScrollerRef.current.scrollTo({ position: { y: 0 }, animate: false }); tScrollerRef.current.scrollTo({
position: { y: 0 },
animate: false,
});
} }
// console.log("[UserReviews] TScroller scroll area updated"); // console.log("[UserReviews] TScroller scroll area updated");
} }
}, 100); }, 100);
} }
}, [showAllReviews]); }, [showAllReviews]);
// actualReviewsData에서 데이터 가져오기 (ProductAllSection에서 전달받거나 자체 useReviews) // actualReviewsData에서 데이터 가져오기 (ProductAllSection에서 전달받거나 자체 useReviews)
const reviewListData = actualReviewsData.previewReviews; // 리뷰 리스트 (5개) const reviewListData = actualReviewsData.previewReviews; // 리뷰 리스트 (5개)
const reviewTotalCount = actualReviewsData.stats.totalReviews; const reviewTotalCount = actualReviewsData.stats.totalReviews;
const reviewDetailData = { const reviewDetailData = {
totRvwCnt: actualReviewsData.stats.totalReviews, totRvwCnt: actualReviewsData.stats.totalReviews,
avgRvwScr: actualReviewsData.stats.averageRating, avgRvwScr: actualReviewsData.stats.averageRating,
totRvwAvg: actualReviewsData.stats.averageRating totRvwAvg: actualReviewsData.stats.averageRating,
}; };
// [UserReviews] 데이터 수신 확인 로그 // [UserReviews] 데이터 수신 확인 로그
@@ -105,7 +129,9 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
averageRating: actualReviewsData.stats.averageRating, averageRating: actualReviewsData.stats.averageRating,
isLoading: actualReviewsData.isLoading, isLoading: actualReviewsData.isLoading,
hasData: reviewListData && reviewListData.length > 0, hasData: reviewListData && reviewListData.length > 0,
dataSource: reviewsData ? 'ProductAllSection props' : 'useReviews fallback' dataSource: reviewsData
? "ProductAllSection props"
: "useReviews fallback",
}); });
}, [reviewListData, actualReviewsData, reviewsData]); }, [reviewListData, actualReviewsData, reviewsData]);
@@ -118,26 +144,32 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
}; };
// 리뷰 클릭으로 User Reviews 모드 팝업 열기 // 리뷰 클릭으로 User Reviews 모드 팝업 열기
const handleReviewClick = useCallback((reviewIndex) => { const handleReviewClick = useCallback(
// 클릭한 리뷰 정보 (previewReviews에서) (reviewIndex) => {
const clickedReview = reviewListData[reviewIndex]; // 클릭한 리뷰 정보 (previewReviews에서)
const clickedReview = reviewListData[reviewIndex];
// 전체 리뷰에서 클릭한 리뷰의 실제 인덱스 찾기
const realIndex = fallbackReviews.allReviews.findIndex( // 전체 리뷰에서 클릭한 리뷰의 실제 인덱스 찾기
review => review.rvwId === clickedReview.rvwId const realIndex = fallbackReviews.allReviews.findIndex(
); (review) => review.rvwId === clickedReview.rvwId
);
console.log("[UserReviews] Review clicked, opening popup in User Reviews mode:", {
previewIndex: reviewIndex, console.log(
realIndex, "[UserReviews] Review clicked, opening popup in User Reviews mode:",
clickedReviewId: clickedReview.rvwId, {
totalReviews: fallbackReviews.allReviews.length previewIndex: reviewIndex,
}); realIndex,
clickedReviewId: clickedReview.rvwId,
setSelectedImageIndex(realIndex >= 0 ? realIndex : reviewIndex); totalReviews: fallbackReviews.allReviews.length,
setPopupMode("user-reviews"); }
setIsPopupOpen(true); );
}, [reviewListData, fallbackReviews.allReviews]);
setSelectedImageIndex(realIndex >= 0 ? realIndex : reviewIndex);
setPopupMode("user-reviews");
setIsPopupOpen(true);
},
[reviewListData, fallbackReviews.allReviews]
);
// 팝업 관련 핸들러들 // 팝업 관련 핸들러들
// +View More 버튼으로 Customer Images 모드 팝업 열기 // +View More 버튼으로 Customer Images 모드 팝업 열기
@@ -149,7 +181,10 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
// 이미지 클릭으로 All-Images 모드 팝업 열기 // 이미지 클릭으로 All-Images 모드 팝업 열기
const handleOpenAllImagesPopup = useCallback((imageIndex = 0) => { const handleOpenAllImagesPopup = useCallback((imageIndex = 0) => {
console.log("[UserReviews] Opening popup in All-Images mode, image index:", imageIndex); console.log(
"[UserReviews] Opening popup in All-Images mode, image index:",
imageIndex
);
setSelectedImageIndex(imageIndex); setSelectedImageIndex(imageIndex);
setPopupMode("all-images"); setPopupMode("all-images");
setIsPopupOpen(true); setIsPopupOpen(true);
@@ -164,7 +199,10 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
// 팝업 모드 변경 핸들러 // 팝업 모드 변경 핸들러
const handleModeChange = useCallback((newMode, imageIndex = 0) => { const handleModeChange = useCallback((newMode, imageIndex = 0) => {
console.log("[UserReviews] Mode change requested:", { newMode, imageIndex }); console.log("[UserReviews] Mode change requested:", {
newMode,
imageIndex,
});
setPopupMode(newMode); setPopupMode(newMode);
if (newMode === "all-images" || newMode === "user-reviews") { if (newMode === "all-images" || newMode === "user-reviews") {
setSelectedImageIndex(imageIndex); setSelectedImageIndex(imageIndex);
@@ -173,9 +211,6 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
// handleImageClick 제거 - 더 이상 필요없음 // handleImageClick 제거 - 더 이상 필요없음
// customerImages 로직 제거 - useReviews의 extractImagesFromReviews 사용 // customerImages 로직 제거 - useReviews의 extractImagesFromReviews 사용
return ( return (
@@ -190,13 +225,15 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
verticalScrollbar="auto" verticalScrollbar="auto"
cbScrollTo={getScrollTo} cbScrollTo={getScrollTo}
forceUpdate={showAllReviews} forceUpdate={showAllReviews}
key={showAllReviews ? `all-${(reviewListData && reviewListData.length) || 0}` : 'limited-5'} key={
showAllReviews
? `all-${(reviewListData && reviewListData.length) || 0}`
: "limited-5"
}
> >
<THeaderDetail <THeaderDetail
title={$L( title={$L(`USER REVIEWS (${reviewTotalCount})`)}
`USER REVIEWS (${reviewTotalCount})` className={classNames(css.tHeader, css.tHeaderDetail)}
)}
className={css.tHeader}
> >
{reviewDetailData && reviewDetailData.totRvwAvg && ( {reviewDetailData && reviewDetailData.totRvwAvg && (
<StarRating <StarRating
@@ -205,8 +242,8 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
/> />
)} )}
</THeaderDetail> </THeaderDetail>
<CustomerImages <CustomerImages
panelInfo={panelInfo} panelInfo={panelInfo}
onImageClick={handleOpenPopup} onImageClick={handleOpenPopup}
onViewMoreClick={handleOpenPopup} onViewMoreClick={handleOpenPopup}
imageData={fallbackReviews.extractImagesFromReviews} imageData={fallbackReviews.extractImagesFromReviews}
@@ -219,31 +256,48 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
</div> </div>
{reviewListData && {reviewListData &&
(() => { (() => {
const reviewsToShow = showAllReviews ? reviewListData : reviewListData.slice(0, 5); const reviewsToShow = showAllReviews
? reviewListData
: reviewListData.slice(0, 5);
console.log("[UserReviews] Reviews to render:", { console.log("[UserReviews] Reviews to render:", {
showAllReviews, showAllReviews,
totalReviews: reviewListData.length, totalReviews: reviewListData.length,
reviewsToShowCount: reviewsToShow.length, reviewsToShowCount: reviewsToShow.length,
isShowingAll: showAllReviews, isShowingAll: showAllReviews,
reviewListData: reviewListData reviewListData: reviewListData,
}); });
// 실제 렌더링될 각 리뷰 로그 // 실제 렌더링될 각 리뷰 로그
reviewsToShow.forEach((review, index) => { reviewsToShow.forEach((review, index) => {
console.log(`[UserReviews] Review ${index + 1}/${reviewsToShow.length}:`, { console.log(
rvwId: review.rvwId, `[UserReviews] Review ${index + 1}/${reviewsToShow.length}:`,
rvwCtnt: (review.rvwCtnt && review.rvwCtnt.substring(0, 50)) + "...", {
rvwRtng: review.rvwRtng, rvwId: review.rvwId,
hasImages: (review.reviewImageList && review.reviewImageList.length) || 0, rvwCtnt:
rvwRgstDtt: review.rvwRgstDtt, (review.rvwCtnt && review.rvwCtnt.substring(0, 50)) +
fullReview: review "...",
}); rvwRtng: review.rvwRtng,
hasImages:
(review.reviewImageList &&
review.reviewImageList.length) ||
0,
rvwRgstDtt: review.rvwRgstDtt,
fullReview: review,
}
);
}); });
return reviewsToShow; return reviewsToShow;
})().map((review, index, array) => { })().map((review, index, array) => {
const { reviewImageList, rvwRtng, rvwRgstDtt, rvwCtnt, rvwId, wrtrNknm, rvwWrtrId } = const {
review; reviewImageList,
rvwRtng,
rvwRgstDtt,
rvwCtnt,
rvwId,
wrtrNknm,
rvwWrtrId,
} = review;
const isLastReview = index === array.length - 1; const isLastReview = index === array.length - 1;
/* console.log(`[UserReviews] Rendering review ${index}:`, { /* console.log(`[UserReviews] Rendering review ${index}:`, {
rvwId, rvwId,
@@ -258,7 +312,11 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
aria-label={`user-reviews-:${rvwId}`} aria-label={`user-reviews-:${rvwId}`}
className={css.reviewContentContainer} className={css.reviewContentContainer}
onClick={() => handleReviewClick(index)} onClick={() => handleReviewClick(index)}
spotlightId={isLastReview ? 'user-review-at-last' : `user-review-${index}`} spotlightId={
isLastReview
? "user-review-at-last"
: `user-review-${index}`
}
> >
{reviewImageList && reviewImageList.length > 0 && ( {reviewImageList && reviewImageList.length > 0 && (
<img <img
@@ -285,17 +343,14 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
</span> </span>
)} )}
</div> </div>
{rvwCtnt && ( {rvwCtnt && <div className={css.reviewText}>{rvwCtnt}</div>}
<div className={css.reviewText}>{rvwCtnt}</div>
)}
</div> </div>
</SpottableComponent> </SpottableComponent>
); );
})} })}
</div> </div>
</UserReviewsScroller> </UserReviewsScroller>
{/* UserReviewsPopup 추가 - 모드별 데이터 전달 */} {/* UserReviewsPopup 추가 - 모드별 데이터 전달 */}
<UserReviewsPopup <UserReviewsPopup
open={isPopupOpen} open={isPopupOpen}

View File

@@ -7,9 +7,9 @@
height: auto !important; // 동적 높이 강제 적용 height: auto !important; // 동적 높이 강제 적용
min-height: 500px; // 최소 높이 보장 min-height: 500px; // 최소 높이 보장
max-height: none !important; // 최대 높이 제한 완전 제거 max-height: none !important; // 최대 높이 제한 완전 제거
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
// 스크롤 컨테이너 내부도 동적 높이 허용 // 스크롤 컨테이너 내부도 동적 높이 허용
> * { > * {
height: auto !important; height: auto !important;
@@ -28,14 +28,26 @@
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
&.tHeaderDetail {
height: 56px;
max-width: none;
width: 100%;
justify-content: space-between;
}
> div { > div {
.size(@w:100%,@h:100%); .size(@w:100%,@h:100%);
padding: 0; padding: 0;
&:first-child {
width: 100%;
}
}
> &.title {
width: 100%;
} }
.averageOverallRating { .averageOverallRating {
.size(@w: 176px,@h:30px); width: 176px;
height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: auto; margin-left: auto;
@@ -54,7 +66,7 @@
.reviewItem { .reviewItem {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin-top: 10px;
.showReviewsText { .showReviewsText {
.size(@w:100%, @h:36px); .size(@w:100%, @h:36px);
.font(@fontFamily: @baseFont, @fontSize: 24px); .font(@fontFamily: @baseFont, @fontSize: 24px);
@@ -71,7 +83,7 @@
.flex(@justifyCenter:flex-start); .flex(@justifyCenter:flex-start);
padding: 30px; padding: 30px;
position: relative; position: relative;
&:focus { &:focus {
&::after { &::after {
.focused(@boxShadow:22px, @borderRadius:12px); .focused(@boxShadow:22px, @borderRadius:12px);
@@ -92,10 +104,10 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
> * { > * {
margin-right: 20px; margin-right: 20px;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
@@ -146,7 +158,7 @@
width: auto; // "View All Reviews +" 한 줄 표시용으로 확장 width: auto; // "View All Reviews +" 한 줄 표시용으로 확장
height: 75px; // 20 + 35 + 20 height: 75px; // 20 + 35 + 20
cursor: pointer; cursor: pointer;
&:focus { &:focus {
&::after { &::after {
.focused(@boxShadow:22px, @borderRadius:6px); .focused(@boxShadow:22px, @borderRadius:6px);
@@ -159,13 +171,13 @@
padding: 20px 30px; padding: 20px 30px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 6px; border-radius: 6px;
border: 1px solid #EEEEEE; border: 1px solid #eeeeee;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
.viewAllReviewsText { .viewAllReviewsText {
color: #EAEAEA; color: #eaeaea;
font-size: 24px; font-size: 24px;
font-family: @baseFont; font-family: @baseFont;
font-weight: 600; font-weight: 600;
@@ -181,4 +193,4 @@
} }
} }
} }
} }

View File

@@ -1,13 +1,24 @@
import React, { useCallback, useState, useEffect } from "react"; import React, {
import classNames from "classnames"; useCallback,
import Spottable from "@enact/spotlight/Spottable"; useEffect,
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; useRef,
import TNewPopUp from "../../../../../components/TPopUp/TNewPopUp"; useState,
import TButton from "../../../../../components/TButton/TButton"; } from 'react';
import { $L } from "../../../../../utils/helperMethods";
import UserReviewDetail from "../UserReviewDetail/UserReviewDetail"; import classNames from 'classnames';
import ImageSkeleton from "./ImageSkeleton/ImageSkeleton";
import css from "./UserReviewsPopup.module.less"; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import TButton from '../../../../../components/TButton/TButton';
import TNewPopUp from '../../../../../components/TPopUp/TNewPopUp';
import useScrollTo from '../../../../../hooks/useScrollTo';
import { $L } from '../../../../../utils/helperMethods';
import TScrollerDetail from '../../../components/TScroller/TScrollerDetail';
import UserReviewDetail from '../UserReviewDetail/UserReviewDetail';
import ImageSkeleton from './ImageSkeleton/ImageSkeleton';
import css from './UserReviewsPopup.module.less';
const SpottableImage = Spottable("div"); const SpottableImage = Spottable("div");
@@ -24,7 +35,7 @@ const FooterContainer = SpotlightContainerDecorator(
{ {
enterTo: "default-element", enterTo: "default-element",
preserveId: true, preserveId: true,
defaultElement: "close-button" defaultElement: "close-button",
}, },
"div" "div"
); );
@@ -40,6 +51,9 @@ export default function UserReviewsPopup({
onModeChange, // 모드 변경 콜백 함수 onModeChange, // 모드 변경 콜백 함수
className, className,
}) { }) {
const scrollContainerRef = useRef(null);
const { getScrollTo } = useScrollTo();
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (onClose) { if (onClose) {
onClose(); onClose();
@@ -47,12 +61,18 @@ export default function UserReviewsPopup({
}, [onClose]); }, [onClose]);
// Customer Images 모드에서 이미지 클릭 시 All Images 모드로 변경 // Customer Images 모드에서 이미지 클릭 시 All Images 모드로 변경
const handleCustomerImageClick = useCallback((index) => { const handleCustomerImageClick = useCallback(
console.log("[UserReviewsPopup] Customer image clicked, switching to All Images mode, index:", index); (index) => {
if (onModeChange) { console.log(
onModeChange("all-images", index); "[UserReviewsPopup] Customer image clicked, switching to All Images mode, index:",
} index
}, [onModeChange]); );
if (onModeChange) {
onModeChange("all-images", index);
}
},
[onModeChange]
);
// 모드별 헤더 정보 // 모드별 헤더 정보
const getHeaderInfo = useCallback((mode) => { const getHeaderInfo = useCallback((mode) => {
@@ -61,20 +81,20 @@ export default function UserReviewsPopup({
return { return {
title: $L("All images"), title: $L("All images"),
hasIcon: true, hasIcon: true,
iconType: "all-images" iconType: "all-images",
}; };
case "user-reviews": case "user-reviews":
return { return {
title: $L("User Reviews"), title: $L("User Reviews"),
hasIcon: true, hasIcon: true,
iconType: "user-reviews" iconType: "user-reviews",
}; };
case "customer-images": case "customer-images":
default: default:
return { return {
title: $L("Customer Images"), title: $L("Customer Images"),
hasIcon: false, hasIcon: false,
iconType: null iconType: null,
}; };
} }
}, []); }, []);
@@ -88,8 +108,8 @@ export default function UserReviewsPopup({
useEffect(() => { useEffect(() => {
if (mode === "all-images" && images && images[selectedImageIndex]) { if (mode === "all-images" && images && images[selectedImageIndex]) {
const selectedImage = images[selectedImageIndex]; const selectedImage = images[selectedImageIndex];
const reviewIndex = reviewsWithImages.findIndex(review => const reviewIndex = reviewsWithImages.findIndex(
review.rvwId === selectedImage.reviewId (review) => review.rvwId === selectedImage.reviewId
); );
if (reviewIndex !== -1) { if (reviewIndex !== -1) {
setCurrentReviewIndex(reviewIndex); setCurrentReviewIndex(reviewIndex);
@@ -108,42 +128,39 @@ export default function UserReviewsPopup({
}, [currentReviewIndex]); }, [currentReviewIndex]);
const handleNextReview = useCallback(() => { const handleNextReview = useCallback(() => {
const maxIndex = mode === "user-reviews" const maxIndex =
? allReviews.length - 1 mode === "user-reviews"
: reviewsWithImages.length - 1; ? allReviews.length - 1
: reviewsWithImages.length - 1;
if (currentReviewIndex < maxIndex) { if (currentReviewIndex < maxIndex) {
setCurrentReviewIndex(currentReviewIndex + 1); setCurrentReviewIndex(currentReviewIndex + 1);
} }
}, [currentReviewIndex, mode, allReviews.length, reviewsWithImages.length]); }, [currentReviewIndex, mode, allReviews.length, reviewsWithImages.length]);
// All Images 아이콘 컴포넌트 (PNG 파일 사용) // All Images 아이콘 컴포넌트 (PNG 파일 사용)
const AllImagesIcon = () => ( const AllImagesIcon = () => <div className={css.headerIcon} />;
<div className={css.headerIcon} />
);
// User Reviews 아이콘 컴포넌트 (추후 구현) // User Reviews 아이콘 컴포넌트 (추후 구현)
const UserReviewsIcon = () => ( const UserReviewsIcon = () => (
<div className={css.headerIcon}> <div className={css.headerIcon}>{/* User Reviews 아이콘 내용 */}</div>
{/* User Reviews 아이콘 내용 */}
</div>
); );
// 이미지 로딩 상태 관리 // 이미지 로딩 상태 관리
const [imageLoadStates, setImageLoadStates] = useState({}); const [imageLoadStates, setImageLoadStates] = useState({});
const handleImageLoad = useCallback((index) => { const handleImageLoad = useCallback((index) => {
setImageLoadStates(prev => ({ setImageLoadStates((prev) => ({
...prev, ...prev,
[index]: true [index]: true,
})); }));
}, []); }, []);
const handleImageError = useCallback((index) => { const handleImageError = useCallback((index) => {
console.error(`[UserReviewsPopup] Image load failed for index: ${index}`); console.error(`[UserReviewsPopup] Image load failed for index: ${index}`);
setImageLoadStates(prev => ({ setImageLoadStates((prev) => ({
...prev, ...prev,
[index]: false [index]: false,
})); }));
}, []); }, []);
@@ -171,63 +188,76 @@ export default function UserReviewsPopup({
{headerInfo.iconType === "user-reviews" && <UserReviewsIcon />} {headerInfo.iconType === "user-reviews" && <UserReviewsIcon />}
</> </>
)} )}
<div className={css.headerTitle}> <div className={css.headerTitle}>{headerInfo.title}</div>
{headerInfo.title}
</div>
</div> </div>
{/* Content - 모드별 내용 */} {/* Content - 모드별 내용 */}
<ContentContainer <ContentContainer
className={css.content} className={css.content}
defaultElement={ defaultElement={
mode === "all-images" ? "review-detail-prev" : mode === "all-images"
mode === "user-reviews" ? "user-review-detail-prev" : ? "review-detail-prev"
"user-review-image-0" : mode === "user-reviews"
? "user-review-detail-prev"
: "user-review-image-0"
} }
> >
{mode === "customer-images" && ( {mode === "customer-images" && (
<div className={css.imageGrid}> <TScrollerDetail
{displayImages.map((image, index) => { ref={scrollContainerRef}
const isLoaded = imageLoadStates[index] === true; verticalScrollbar="visible"
const isFailed = imageLoadStates[index] === false; className={css.scrollerOverride}
cbScrollTo={getScrollTo}
return ( spotlightDisabled={false}
<SpottableImage spotlightRestrict="none"
key={`user-review-image-${index}`} >
spotlightId={`user-review-image-${index}`} <div className={css.imageGrid}>
className={css.imageItem} {displayImages.map((image, index) => {
onClick={() => handleCustomerImageClick(index)} const isLoaded = imageLoadStates[index] === true;
> const isFailed = imageLoadStates[index] === false;
{/* Skeleton UI - 이미지 로딩 중일 때 표시 */}
{!isLoaded && !isFailed && ( return (
<ImageSkeleton className={css.image} /> <SpottableImage
)} key={`user-review-image-${index}`}
spotlightId={`user-review-image-${index}`}
{/* 실제 이미지 */} className={css.imageItem}
<img onClick={() => handleCustomerImageClick(index)}
src={image.imgUrl || image} >
alt={`Customer review ${index + 1}`} <div>
className={css.image} {/* Skeleton UI - 이미지 로딩 중일 때 표시 */}
style={{ {!isLoaded && !isFailed && (
opacity: isLoaded ? 1 : 0, <ImageSkeleton className={css.image} />
transition: 'opacity 0.3s ease-in-out' )}
}}
onLoad={() => handleImageLoad(index)} {/* 실제 이미지 */}
onError={() => handleImageError(index)} <img
/> src={image.imgUrl || image}
alt={`Customer review ${index + 1}`}
{/* 이미지 로드 실패 시 표시할 플레이스홀더 */} className={css.image}
{isFailed && ( style={{
<div className={css.imagePlaceholder}> opacity: isLoaded ? 1 : 0,
<div className={css.placeholderText}>이미지 없음</div> transition: "opacity 0.3s ease-in-out",
}}
onLoad={() => handleImageLoad(index)}
onError={() => handleImageError(index)}
/>
{/* 이미지 로드 실패 시 표시할 플레이스홀더 */}
{isFailed && (
<div className={css.imagePlaceholder}>
<div className={css.placeholderText}>
이미지 없음
</div>
</div>
)}
</div> </div>
)} </SpottableImage>
</SpottableImage> );
); })}
})} </div>
</div> </TScrollerDetail>
)} )}
{mode === "all-images" && ( {mode === "all-images" && (
<UserReviewDetail <UserReviewDetail
currentReview={reviewsWithImages[currentReviewIndex]} currentReview={reviewsWithImages[currentReviewIndex]}
@@ -238,7 +268,7 @@ export default function UserReviewsPopup({
className={css.reviewDetailContainer} className={css.reviewDetailContainer}
/> />
)} )}
{mode === "user-reviews" && ( {mode === "user-reviews" && (
<UserReviewDetail <UserReviewDetail
currentReview={allReviews[currentReviewIndex]} currentReview={allReviews[currentReviewIndex]}
@@ -265,4 +295,4 @@ export default function UserReviewsPopup({
</div> </div>
</TNewPopUp> </TNewPopUp>
); );
} }

View File

@@ -12,16 +12,16 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
// Header 영역 - 모드별 아이콘과 제목 지원 (기존 레이아웃 유지) // Header 영역 - 모드별 아이콘과 제목 지원 (기존 레이아웃 유지)
.header { .header {
align-self: stretch; align-self: stretch;
padding: 30px; padding: 30px;
background: #E7EBEF; background: #e7ebef;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
// All Images 아이콘 (PNG 파일 사용) // All Images 아이콘 (PNG 파일 사용)
.headerIcon { .headerIcon {
width: 52px; width: 52px;
@@ -32,7 +32,7 @@
background-repeat: no-repeat; background-repeat: no-repeat;
margin-right: 15px; // gap: 15 → margin-right: 15px margin-right: 15px; // gap: 15 → margin-right: 15px
} }
.headerTitle { .headerTitle {
text-align: center; text-align: center;
color: black; color: black;
@@ -43,7 +43,7 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
// Content 영역 // Content 영역
.content { .content {
align-self: stretch; align-self: stretch;
@@ -54,123 +54,177 @@
justify-content: center; // 중앙 정렬 justify-content: center; // 중앙 정렬
align-items: center; // 중앙 정렬 align-items: center; // 중앙 정렬
position: relative; position: relative;
.imageGrid {
width: 100%;
height: 100%;
display: flex;
justify-content: center; // 중앙 정렬로 균등 배치
align-items: flex-start;
flex-wrap: wrap;
align-content: flex-start;
overflow-y: scroll; // 스크롤바 항상 표시
overflow-x: hidden;
padding: 30px 40px; // 좌우 패딩 증가
box-sizing: border-box;
// gap 대신 margin 사용 (TV 호환성) - 화면을 적절히 채우도록 조정
.imageItem {
width: 210px; // 크기 약간 증가
height: 190px; // 비율 맞춤
border-radius: 12px;
position: relative;
cursor: pointer;
margin-right: 35px; // 마진 증가로 균등 분배
margin-bottom: 30px; // 세로 마진도 증가
// 4개씩 배치하므로 4번째마다 margin-right 제거
&:nth-child(4n) {
margin-right: 0;
}
&:focus {
outline: none;
&::after { .scrollerOverride {
.focused(@boxShadow: 0px, @borderRadius: 12px); width: 1060px; // 절대 크기 지정
// 프로젝트 표준 포커스 스타일 사용 (4px solid @PRIMARY_COLOR_RED) height: 100%;
} // 좌측 30px, 우측 66px(스크롤바) 패딩을 명시적으로 적용
} padding: 30px 20px 30px 20px;
box-sizing: border-box;
.image { margin: 0;
width: 100%; overflow-y: auto;
height: 100%; overflow-x: hidden;
border-radius: 12px; display: flex;
object-fit: cover; flex-direction: column;
padding: 0;
box-sizing: border-box; // 스크롤바 너비를 6px로 명확하게 설정
} &::-webkit-scrollbar {
width: 6px;
// 이미지 로드 실패 시 플레이스홀더
.imagePlaceholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f5f5f5;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
.placeholderText {
color: #999;
font-size: 14px;
font-family: @baseFont;
text-align: center;
}
}
// selectedImage 스타일 제거 - 오직 포커스만 사용
} }
// View More 아이템 &::-webkit-scrollbar-track {
.viewMoreItem { background: transparent; // 트랙 배경은 투명하게
width: 210px; // 크기 증가 }
height: 190px; // 비율 맞춤
border-radius: 12px; &::-webkit-scrollbar-thumb {
position: relative; background: #9c9c9c; // 스크롤바 색상
margin-right: 35px; // 마진 증가 border-radius: 3px; // 스크롤바 둥근 모서리
margin-bottom: 30px; // 세로 마진 증가 }
&:nth-child(4n) { // 스크롤바 thumb에 hover 효과 적용
margin-right: 0; &:hover::-webkit-scrollbar-thumb {
} background: #c72054;
}
.image { // gap 대신 margin 사용 (TV 호환성) - 화면을 적절히 채우도록 조정
width: 100%; .imageGrid {
height: 100%; width: 100%;
padding: 0;
margin: 0;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: flex-start;
overflow: visible;
height: 100%;
flex-wrap: wrap;
align-content: flex-start;
// padding: 30px 40px; // 좌우 패딩 증가
// padding: 30px 0px 30px 15px; // 좌우 패딩 증가
.imageItem {
//스타일 변경
// width: 210px; // 크기 약간 증가
// height: 190px; // 비율 맞춤
width: 226px; // 크기 약간 증가
height: 218px; // 비율 맞춤
border-radius: 12px; border-radius: 12px;
object-fit: cover; position: relative;
padding: 4px; cursor: pointer;
box-sizing: border-box;
//스타일 변경
// margin-right: 35px; // 마진 증가로 균등 분배
// margin-bottom: 30px; // 세로 마진도 증가
margin-right: 20px; // 마진 증가로 균등 분배
margin-top: 20px; // 세로 마진도 증가
// 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;
&::after {
.focused(@boxShadow: 0px, @borderRadius: 12px);
// 프로젝트 표준 포커스 스타일 사용 (4px solid @PRIMARY_COLOR_RED)
}
&:nth-child(4n) {
margin-right: 0;
margin-left: -14px;
}
}
.image {
width: 100%;
height: 100%;
border-radius: 12px;
object-fit: cover;
padding: 0;
box-sizing: border-box;
}
// 이미지 로드 실패 시 플레이스홀더
.imagePlaceholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f5f5f5;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
.placeholderText {
color: #999;
font-size: 14px;
font-family: @baseFont;
text-align: center;
}
}
// selectedImage 스타일 제거 - 오직 포커스만 사용
} }
.viewMoreOverlay { // View More 아이템
position: absolute; .viewMoreItem {
top: 0; width: 210px; // 크기 증가
left: 0; height: 190px; // 비율 맞춤
right: 0;
bottom: 0;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.80) 100%);
border-radius: 12px; border-radius: 12px;
display: flex; position: relative;
justify-content: center; margin-right: 35px; // 마진 증가
align-items: center; margin-bottom: 30px; // 세로 마진 증가
.viewMoreText { &:nth-child(4n) {
color: white; margin-right: 0;
font-size: 16px; }
font-family: @baseFont;
font-weight: 700; .image {
line-height: 31px; width: 100%;
word-wrap: break-word; height: 100%;
border-radius: 12px;
object-fit: cover;
padding: 4px;
box-sizing: border-box;
}
.viewMoreOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.8) 0%,
rgba(0, 0, 0, 0.8) 100%
);
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
.viewMoreText {
color: white;
font-size: 16px;
font-family: @baseFont;
font-weight: 700;
line-height: 31px;
word-wrap: break-word;
}
} }
} }
} }
} }
// User Reviews 모드용 reviewGrid 스타일 // User Reviews 모드용 reviewGrid 스타일
.reviewGrid { .reviewGrid {
width: 100%; width: 100%;
@@ -181,20 +235,20 @@
box-sizing: border-box; box-sizing: border-box;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
// User Reviews 콘텐츠 스타일 - 추후 구현 // User Reviews 콘텐츠 스타일 - 추후 구현
} }
// UserReviewDetail 컴포넌트를 위한 컨테이너 // UserReviewDetail 컴포넌트를 위한 컨테이너
.reviewDetailContainer { .reviewDetailContainer {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
// 스크롤바 표시 (기본 브라우저 스타일 사용) // 스크롤바 표시 (기본 브라우저 스타일 사용)
// 별도 커스텀 스크롤바 스타일 없음 // 별도 커스텀 스크롤바 스타일 없음
} }
// Footer 영역 // Footer 영역
.footer { .footer {
align-self: stretch; align-self: stretch;
@@ -202,31 +256,31 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.closeButton { .closeButton {
width: 300px !important; width: 300px !important;
height: 78px !important; height: 78px !important;
background: #7A808D !important; background: #7a808d !important;
border-radius: 12px !important; border-radius: 12px !important;
border: none !important; border: none !important;
display: flex !important; display: flex !important;
justify-content: center !important; justify-content: center !important;
align-items: center !important; align-items: center !important;
color: white !important; color: white !important;
font-size: 30px !important; font-size: 30px !important;
font-family: @baseFont !important; font-family: @baseFont !important;
font-weight: 700 !important; font-weight: 700 !important;
line-height: 30px !important; line-height: 30px !important;
text-align: center !important; text-align: center !important;
&:focus { &:focus {
background: @PRIMARY_COLOR_RED !important; background: @PRIMARY_COLOR_RED !important;
color: white !important; color: white !important;
outline: none !important; outline: none !important;
border: none !important; border: none !important;
} }
&:hover { &:hover {
background: @PRIMARY_COLOR_RED !important; background: @PRIMARY_COLOR_RED !important;
color: white !important; color: white !important;
@@ -234,4 +288,4 @@
} }
} }
} }
} }

View File

@@ -1,8 +1,10 @@
import React, { useMemo } from "react"; import React, { useMemo } from 'react';
import css from "./QRCode.module.less";
import { getQRCodeUrl } from "../../../../utils/helperMethods"; import { useSelector } from 'react-redux';
import TQRCode from "../../../../components/TQRCode/TQRCode";
import { useSelector } from "react-redux"; import TQRCode from '../../../../components/TQRCode/TQRCode';
import { getQRCodeUrl } from '../../../../utils/helperMethods';
import css from './QRCode.module.less';
export default function QRCode({ export default function QRCode({
productType, productType,
@@ -59,7 +61,7 @@ export default function QRCode({
{kind === "detail" ? ( {kind === "detail" ? (
<TQRCode text={qrCodeUrl} width="240" height="240" /> <TQRCode text={qrCodeUrl} width="240" height="240" />
) : ( ) : (
"" qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />
)} )}
{/* todo : 시나리오,UI 릴리즈 후 */} {/* todo : 시나리오,UI 릴리즈 후 */}
{/* <div className={css.tooltip}> {/* <div className={css.tooltip}>

View File

@@ -2,11 +2,12 @@
@import "../../../style/utils.module.less"; @import "../../../style/utils.module.less";
.container { .container {
.size(@w:100%,@h:100%); // .size(@w:100%,@h:100%);
.size(@w:100%,@h:334px);
.productInfoWrapper { .productInfoWrapper {
.flex(@justifyCenter:flex-start,@alignCenter:flex-start); .flex(@justifyCenter:flex-start,@alignCenter:flex-start);
margin: 54px 0 10px 0; // margin: 54px 0 10px 0;
margin: 20px 0 10px 0;
// 고정 높이로 인해 QR 영역과 하단 버튼 영역 사이에 과도한 여백이 생김 // 고정 높이로 인해 QR 영역과 하단 버튼 영역 사이에 과도한 여백이 생김
// 콘텐츠 높이에 맞춰 자동으로 계산되도록 변경하여 불필요한 간격 제거 // 콘텐츠 높이에 맞춰 자동으로 계산되도록 변경하여 불필요한 간격 제거
.size(@w:100%,@h:auto); .size(@w:100%,@h:auto);

View File

@@ -1,7 +1,11 @@
import React, { useCallback, useMemo } from "react"; import React, {
import usePriceInfo from "../../../../../hooks/usePriceInfo"; useCallback,
import { $L } from "../../../../../utils/helperMethods"; useMemo,
import css from "./BuyNowPriceDisplay.module.less"; } from 'react';
import usePriceInfo from '../../../../../hooks/usePriceInfo';
import { $L } from '../../../../../utils/helperMethods';
import css from './BuyNowPriceDisplay.module.less';
export default function BuyNowPriceDisplay({ export default function BuyNowPriceDisplay({
priceData, priceData,
@@ -41,16 +45,26 @@ export default function BuyNowPriceDisplay({
{discountRate && Number(discountRate.replace("%", "")) > 4 && ( {discountRate && Number(discountRate.replace("%", "")) > 4 && (
<div className={css.rateTag}>{discountRate}</div> <div className={css.rateTag}>{discountRate}</div>
)} )}
<span className={css.price}> <div
{isDiscountedPriceEmpty ? offerInfo : discountedPrice} style={{
</span> width: "100%",
{isDiscounted && ( marginTop: "10px",
<span className={css.discountedPrc}> display: "inline-flex",
{originalPrice && isOriginalPriceEmpty height: "60px",
? offerInfo lineHeight: "60px",
: originalPrice} }}
>
<span className={css.price}>
{isDiscountedPriceEmpty ? offerInfo : discountedPrice}
</span> </span>
)} {isDiscounted && (
<span className={css.discountedPrc}>
{originalPrice && isOriginalPriceEmpty
? offerInfo
: originalPrice}
</span>
)}
</div>
</div> </div>
{/* 할부 */} {/* 할부 */}
</div> </div>

View File

@@ -0,0 +1,105 @@
@import "../../../../../style/CommonStyle.module.less";
@import "../../../../../style/utils.module.less";
.wrapper {
height: 100%;
.partnerName {
font-weight: bold;
font-size: 36px;
color: @COLOR_WHITE;
margin-bottom: 24px;
}
.flex(@alignCenter:flex-start,@direction:column);
.topLayer {
height: 42px;
flex-wrap: wrap;
// margin-bottom: 20px;
margin-bottom: 10px;
}
.rateTag {
background: linear-gradient(309.43deg, #ef775b 23.84%, #c70850 100%);
width: 70px;
height: 42px;
padding: 6px 12px;
border-radius: 6px;
color: @COLOR_WHITE;
.font(@fontFamily: @baseFont, @fontSize: 24px);
font-weight: 700;
.flex();
}
.name {
font-weight: bold;
font-size: 36px;
color: @COLOR_WHITE;
width: 100%;
}
.btmLayer {
//margin-top:14px;
margin: 10px 0;
// display: flex;rateTag
align-items: center;
}
.price {
font-weight: bold;
font-size: 60px;
color: @COLOR_WHITE;
margin-right: 9px;
line-height: 1;
}
.offerInfo {
.elip(4);
font-size: 40px;
font-weight: bold;
line-height: 1;
color: #808080;
padding-bottom: 5px;
}
.discountedPrc {
font-size: 24px;
color: @COLOR_GRAY03;
text-decoration: line-through;
}
//rewd Layer
.rewdTopLayer {
width: 500px;
padding-bottom: 24px;
border-bottom: 1px solid @COLOR_GRAY02;
> span {
font-weight: bold;
font-size: 36px;
color: @COLOR_GRAY03;
}
.partnerPrc {
text-decoration: line-through;
align-items: center;
}
}
.rewdBtmLayer {
padding-top: 24px;
.flex(@direction:column,@alignCenter:flex-start);
.rewdNm {
font-weight: bold;
font-size: 36px;
color: @COLOR_BLACK;
}
.btmPrc {
margin-top: 17px;
.flex();
.rewdPrc {
font-size: 24px;
color: #808080;
margin-right: 10px;
padding-top: 14px;
}
}
.rewdRate {
font-size: 60px;
font-weight: bold;
color: @PRIMARY_COLOR_RED;
margin-top: 18px;
}
}
}

View File

@@ -68,7 +68,8 @@ export default function ProductPriceDisplay({ productType, productInfo }) {
return ( return (
<> <>
{productType && productInfo && ( {productType && productInfo && (
<div> /* <div> */
<div style={{ margin: "0 10px 0 0", width: "380px" }}>
{/* shop by mobile (구매불가) 상품 price render */} {/* shop by mobile (구매불가) 상품 price render */}
{(productType === "shopByMobile" || isThemeShopByMobile) && ( {(productType === "shopByMobile" || isThemeShopByMobile) && (
<ShopByMobilePriceDisplay <ShopByMobilePriceDisplay

View File

@@ -12,9 +12,10 @@
} }
.flex(@alignCenter:flex-start,@direction:column); .flex(@alignCenter:flex-start,@direction:column);
.topLayer { .topLayer {
margin-bottom: 20px;
height: 42px; height: 42px;
flex-wrap: wrap; flex-wrap: wrap;
// margin-bottom: 20px;
margin-bottom: 10px;
} }
.rateTag { .rateTag {
background: linear-gradient(309.43deg, #ef775b 23.84%, #c70850 100%); background: linear-gradient(309.43deg, #ef775b 23.84%, #c70850 100%);
@@ -34,14 +35,15 @@
width: 100%; width: 100%;
} }
.btmLayer { .btmLayer {
margin-top: 14px; //margin-top:14px;
margin: 10px 0;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.price { .price {
font-weight: bold; font-weight: bold;
font-size: 60px; font-size: 60px;
color: @PRIMARY_COLOR_RED; color: @COLOR_WHITE;
margin-right: 9px; margin-right: 9px;
line-height: 1; line-height: 1;
} }