[상품 상세] 리뷰 및 디테일 변경건.#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,7 +39,7 @@ import ShowUserReviews from "../../UserReview/ShowUserReviews";
// CSS imports
// import infoCSS from "../ProductInfoSection/ProductInfoSection.module.less";
// import contentCSS from "../ProductContentSection/ProductContentSection.module.less";
import css from "./ProductAllSection.module.less";
import css from './ProductAllSection.module.less';
const Container = SpotlightContainerDecorator(
{
@@ -292,7 +292,7 @@ export default function ProductAllSection({
<div className={css.leftInfoWrapper}>
<div className={css.headerContent}>
<ProductTag productInfo={productData} />
{revwGrd && (
{revwGrd && revwGrd !== "0.0" && (
<StarRating
rating={revwGrd}
aria-label={"star rating " + revwGrd + " out of 5"}
@@ -309,7 +309,11 @@ export default function ProductAllSection({
productType={productType}
>
<div className={css.qrWrapper}>
<QRCode productInfo={productData} productType={productType} />
<QRCode
productInfo={productData}
productType={productType}
kind={"detail"}
/>
</div>
</ProductOverview>
@@ -332,6 +336,7 @@ export default function ProductAllSection({
selectedPrdtId={panelInfo && panelInfo.prdtId}
favoriteFlag={favoriteFlag}
onFavoriteFlagChanged={onFavoriteFlagChanged}
kind={"item_detail"}
/>
</div>
)}

View File

@@ -35,13 +35,16 @@
position: absolute;
left: 60px;
top: 0;
width: 645px;
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 스크롤바)
@@ -49,7 +52,7 @@
position: absolute;
left: 705px; // 60px + 645px
top: 0;
width: 1200px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
width: 1210px; // 30px 마진 + 1114px 콘텐츠 + 66px 스크롤바
height: 100%;
padding: 0;
margin: 0;
@@ -88,7 +91,9 @@
// gap 대신 margin 사용 (Chromium 68 호환성)
> * {
margin-bottom: 5px;
&:last-child { margin-bottom: 0; }
&:last-child {
margin-bottom: 0;
}
}
}
@@ -115,7 +120,6 @@
align-items: flex-start;
position: relative; // 자식 absolute 요소의 기준점
// 스크롤러 오버라이드 (1210px = 30px + content + 스크롤바)
.scrollerOverride {
width: 1210px; // 절대 크기 지정
@@ -139,17 +143,16 @@
}
&::-webkit-scrollbar-thumb {
background: #9C9C9C; // 스크롤바 색상
background: #9c9c9c; // 스크롤바 색상
border-radius: 3px; // 스크롤바 둥근 모서리
}
// 스크롤바 thumb에 hover 효과 적용
&:hover::-webkit-scrollbar-thumb {
background: #C72054;
background: #c72054;
}
}
// 내부 콘텐츠는 별도 너비 계산 없이 100%를 사용
> div {
width: 100%; // 부모의 패딩을 제외한 나머지 공간(1114px)을 모두 사용
@@ -229,7 +232,6 @@
// (중복 제거됨) 최상위 스크롤러/섹션 정의는 .scrollerWrapper 중첩 내부로 이동
// ProductDetailCard 스타일 참고 - 크기/간격만 적용
// 헤더 컨텐츠 영역 (태그, 별점)
@@ -237,7 +239,6 @@
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
// 모바일 쇼핑 섹션 (mobileSection 참고)
@@ -250,7 +251,9 @@
> * {
margin-right: 6px;
&:last-child { margin-right: 0; }
&:last-child {
margin-right: 0;
}
}
}
@@ -258,14 +261,489 @@
flex: 1 1 0 !important;
width: auto !important; // flex로 크기 조정
height: 60px !important;
background: rgba(68, 68, 68, 0.50) !important;
background: rgba(68, 68, 68, 0.5) !important;
border-radius: 6px !important;
border: none !important;
padding: 0 !important;
margin: 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 {
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;
@@ -308,7 +786,7 @@
align-items: center;
.callToOrderText {
color: #EAEAEA;
color: #eaeaea;
font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경
@@ -322,7 +800,9 @@
> * {
margin-right: 10px;
&:last-child { margin-right: 0; }
&:last-child {
margin-right: 0;
}
}
.phoneIconContainer {
@@ -337,7 +817,6 @@
position: absolute;
left: 0;
top: 0;
background: #EAEAEA;
// 전화 아이콘 이미지 또는 CSS로 구현
background-image: url("../../../../assets/images/icons/ic-gr-call-1.png");
background-size: contain;
@@ -347,9 +826,9 @@
}
.phoneNumber {
color: #EAEAEA;
color: #eaeaea;
font-size: 25px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 35px;
}
@@ -365,7 +844,12 @@
> * {
margin-bottom: 5px;
&:last-child { margin-bottom: 0; }
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
padding: 0;
}
}
@@ -381,14 +865,14 @@
align-items: center;
justify-content: center;
color: #EAEAEA;
color: #eaeaea;
font-size: 25px;
font-family: @baseFont; // LG Smart 폰트 사용
font-weight: 400; // Bold에서 Regular로 변경
line-height: 35px;
&:focus {
background: #C72054; // 포커스시만 빨간색
background: #c72054; // 포커스시만 빨간색
}
}
@@ -400,7 +884,7 @@
margin-top: 10px;
&:focus {
background: #C72054;
background: #c72054;
}
}
@@ -414,62 +898,31 @@
> * {
margin-bottom: 10px;
&:last-child { margin-bottom: 0; }
&:last-child {
margin-bottom: 0;
}
}
}
// ProductOverview 컨테이너 스타일 수정 (자식 요소에 맞게 크기 조정)
[class*="ProductOverview"] {
align-self: stretch;
padding: 0 0 5px; // ProductDetailCard mainContent와 동일한 패딩
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 0 0 5px;
// 내부 div (productInfoWrapper)
> div {
align-self: stretch;
display: flex;
justify-content: flex-start;
align-items: flex-start;
> * {
margin-right: 15px; // ProductDetailCard와 동일한 간격
&: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; }
}
}
text-align: left;
}
}
// LayoutSample 포커스 테스트용 스타일
.layoutSample {
width: 1124px;
height: 35px;
height: 300px;
background-color: yellow;
// border: 2px solid white;
margin-bottom: 20px;
display: flex;
justify-content: center;

View File

@@ -26,7 +26,7 @@
.title {
.font(@fontFamily: @baseFont, @fontSize: 30px);
font-weight: 700;
margin: 90px 0 20px 0;
margin: 30px 0 20px 0;
}
}

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
import React, { useCallback } from "react";
import classNames from "classnames";
import Spottable from "@enact/spotlight/Spottable";
import StarRating from "../../../components/StarRating";
import { $L } from "../../../../../utils/helperMethods";
import css from "./UserReviewDetail.module.less";
import React, { useCallback } from 'react';
import classNames from 'classnames';
import Spottable from '@enact/spotlight/Spottable';
import { $L } from '../../../../../utils/helperMethods';
import StarRating from '../../../components/StarRating';
import css from './UserReviewDetail.module.less';
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 formatDate = (dateStr) => {
const [year, month, day] = dateStr.split("-");
return `${year.slice(2)}.${month}.${day}`;
};
return (
<div className={classNames(css.container, className)}>
{/* Left Arrow - 이전 리뷰가 있을 때만 표시 */}
@@ -89,16 +98,14 @@ export default function UserReviewDetail({
{/* Date */}
{currentReview.rvwRgstDtt && (
<div className={css.date}>
{currentReview.rvwRgstDtt}
{formatDate(currentReview.rvwRgstDtt)}
</div>
)}
</div>
{/* Review Text */}
{currentReview.rvwCtnt && (
<div className={css.reviewText}>
"{currentReview.rvwCtnt}"
</div>
<div className={css.reviewText}>"{currentReview.rvwCtnt}"</div>
)}
</div>
</div>

View File

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

View File

@@ -28,14 +28,26 @@
margin-bottom: 20px;
display: flex;
align-items: center;
&.tHeaderDetail {
height: 56px;
max-width: none;
width: 100%;
justify-content: space-between;
}
> div {
.size(@w:100%,@h:100%);
padding: 0;
&:first-child {
width: 100%;
}
}
> &.title {
width: 100%;
}
.averageOverallRating {
.size(@w: 176px,@h:30px);
width: 176px;
height: 30px;
display: flex;
align-items: center;
margin-left: auto;
@@ -54,7 +66,7 @@
.reviewItem {
width: 100%;
height: 100%;
margin-top: 10px;
.showReviewsText {
.size(@w:100%, @h:36px);
.font(@fontFamily: @baseFont, @fontSize: 24px);
@@ -159,13 +171,13 @@
padding: 20px 30px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
border: 1px solid #EEEEEE;
border: 1px solid #eeeeee;
display: flex;
justify-content: flex-start;
align-items: center;
.viewAllReviewsText {
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 600;

View File

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

View File

@@ -17,7 +17,7 @@
.header {
align-self: stretch;
padding: 30px;
background: #E7EBEF;
background: #e7ebef;
display: flex;
justify-content: flex-start;
align-items: center;
@@ -55,28 +55,68 @@
align-items: center; // 중앙 정렬
position: relative;
.scrollerOverride {
width: 1060px; // 절대 크기 지정
height: 100%;
// 좌측 30px, 우측 66px(스크롤바) 패딩을 명시적으로 적용
padding: 30px 20px 30px 20px;
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;
}
// gap 대신 margin 사용 (TV 호환성) - 화면을 적절히 채우도록 조정
.imageGrid {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
box-sizing: border-box;
display: flex;
justify-content: center; // 중앙 정렬로 균등 배치
justify-content: center;
align-items: flex-start;
overflow: visible;
height: 100%;
flex-wrap: wrap;
align-content: flex-start;
overflow-y: scroll; // 스크롤바 항상 표시
overflow-x: hidden;
padding: 30px 40px; // 좌우 패딩 증가
box-sizing: border-box;
// gap 대신 margin 사용 (TV 호환성) - 화면을 적절히 채우도록 조정
// padding: 30px 40px; // 좌우 패딩 증가
// padding: 30px 0px 30px 15px; // 좌우 패딩 증가
.imageItem {
width: 210px; // 크기 약간 증가
height: 190px; // 비율 맞춤
//스타일 변경
// width: 210px; // 크기 약간 증가
// height: 190px; // 비율 맞춤
width: 226px; // 크기 약간 증가
height: 218px; // 비율 맞춤
border-radius: 12px;
position: relative;
cursor: pointer;
margin-right: 35px; // 마진 증가로 균등 분배
margin-bottom: 30px; // 세로 마진도 증가
//스타일 변경
// margin-right: 35px; // 마진 증가로 균등 분배
// margin-bottom: 30px; // 세로 마진도 증가
margin-right: 20px; // 마진 증가로 균등 분배
margin-top: 20px; // 세로 마진도 증가
// 4개씩 배치하므로 4번째마다 margin-right 제거
&:nth-child(4n) {
@@ -84,12 +124,21 @@
}
&: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 {
@@ -153,7 +202,11 @@
left: 0;
right: 0;
bottom: 0;
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;
display: flex;
justify-content: center;
@@ -170,6 +223,7 @@
}
}
}
}
// User Reviews 모드용 reviewGrid 스타일
.reviewGrid {
@@ -206,7 +260,7 @@
.closeButton {
width: 300px !important;
height: 78px !important;
background: #7A808D !important;
background: #7a808d !important;
border-radius: 12px !important;
border: none !important;
display: flex !important;

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import React, { useCallback, useMemo } from "react";
import usePriceInfo from "../../../../../hooks/usePriceInfo";
import { $L } from "../../../../../utils/helperMethods";
import css from "./BuyNowPriceDisplay.module.less";
import React, {
useCallback,
useMemo,
} from 'react';
import usePriceInfo from '../../../../../hooks/usePriceInfo';
import { $L } from '../../../../../utils/helperMethods';
import css from './BuyNowPriceDisplay.module.less';
export default function BuyNowPriceDisplay({
priceData,
@@ -41,6 +45,15 @@ export default function BuyNowPriceDisplay({
{discountRate && Number(discountRate.replace("%", "")) > 4 && (
<div className={css.rateTag}>{discountRate}</div>
)}
<div
style={{
width: "100%",
marginTop: "10px",
display: "inline-flex",
height: "60px",
lineHeight: "60px",
}}
>
<span className={css.price}>
{isDiscountedPriceEmpty ? offerInfo : discountedPrice}
</span>
@@ -52,6 +65,7 @@ export default function BuyNowPriceDisplay({
</span>
)}
</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 (
<>
{productType && productInfo && (
<div>
/* <div> */
<div style={{ margin: "0 10px 0 0", width: "380px" }}>
{/* shop by mobile (구매불가) 상품 price render */}
{(productType === "shopByMobile" || isThemeShopByMobile) && (
<ShopByMobilePriceDisplay

View File

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