diff --git a/com.twin.app.shoptime/src/components/MobileSend/MobileSendPopUp.module.less b/com.twin.app.shoptime/src/components/MobileSend/MobileSendPopUp.module.less index 17eb39de..0a62f8b2 100644 --- a/com.twin.app.shoptime/src/components/MobileSend/MobileSendPopUp.module.less +++ b/com.twin.app.shoptime/src/components/MobileSend/MobileSendPopUp.module.less @@ -5,16 +5,16 @@ .titleHead { align-self: stretch; padding: 30px; - background: #E7EBEF; + background: #e7ebef; display: flex; - justify-content: flex-start; // center → flex-start + justify-content: flex-start; // center → flex-start align-items: center; .titleHead__text { - text-align: left; // center → left + text-align: left; // center → left color: black; font-size: 32px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 700; line-height: 42px; word-wrap: break-word; @@ -25,18 +25,19 @@ .headerTopRow { display: flex; align-items: center; - margin-bottom: 5px; // TV 호환: gap 대신 margin 사용 + margin-bottom: 5px; // TV 호환: gap 대신 margin 사용 .headerTopRow__brandLogo { width: 50px; height: 50px; - margin-right: 15px; // TV 호환: gap 대신 margin 사용 + margin-right: 15px; // TV 호환: gap 대신 margin 사용 + border-radius: 100%; } .headerTopRow__productId { color: #808080; font-size: 24px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 600; line-height: 18px; word-wrap: break-word; @@ -53,7 +54,7 @@ left: 0; top: 0; z-index: 0; - background-color: @COLOR_SKYBLUE; + content: ""; } display: flex; @@ -62,18 +63,24 @@ position: relative; z-index: 1; } + > .container__header__textBox { + margin-top: 30px; + margin-left: 15px; + width: 885px; + height: 100px; + } > .container__header__textBoxOnly { width: 100%; margin-left: 30px; } > .container__header__productImg { - margin: 30px 0px 0 30px; // 60px → 30px로 변경 + margin: 30px 0px 0 30px; // 60px → 30px로 변경 > .container__header__productImg__img { .border-solid(@size:1px,@color:#cccccc); - width: 100px; // 132px → 100px (Figma 크기) - height: 100px; // 132px → 100px (Figma 크기) + width: 100px; // 132px → 100px (Figma 크기) + height: 100px; // 132px → 100px (Figma 크기) margin-bottom: 20px; } } @@ -93,7 +100,7 @@ font-weight: normal; font-size: 28px; color: @COLOR_GRAY06; - padding: 10px 30px 0 30px; // 60px → 30px로 변경 + padding: 10px 30px 0 0px; // 60px → 30px로 변경 text-align: left; .elip(@clamp:1); } @@ -101,10 +108,10 @@ width: 100%; line-height: 60px; text-align: center; - padding: 10px 30px 10px 30px; // 60px → 30px로 변경 + padding: 10px 30px 10px 30px; // 60px → 30px로 변경 } .container__header__price { - padding: 0 30px 20px 30px; // 60px → 30px로 변경 + padding: 0 30px 20px 30px; // 60px → 30px로 변경 font-size: 28px; font-weight: bold; color: @PRIMARY_COLOR_RED; @@ -152,7 +159,7 @@ width: 345.5px; // 고정 너비 height: 412px; // PhoneInputSection과 동일한 높이 padding: 10px 30px 0 30px; // 위 10px, 좌우 30px, 아래 0 - background: #F8F8F8; + background: #f8f8f8; border-radius: 12px; display: flex; justify-content: flex-start; @@ -164,7 +171,7 @@ align-self: stretch; color: #808080; font-size: 20px; // 피그마 스펙대로 복원 - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 24px; // 피그마 스펙대로 복원 word-wrap: break-word; diff --git a/com.twin.app.shoptime/src/components/TButton/TButton.module.less b/com.twin.app.shoptime/src/components/TButton/TButton.module.less index 61fc7db3..62beb2c9 100644 --- a/com.twin.app.shoptime/src/components/TButton/TButton.module.less +++ b/com.twin.app.shoptime/src/components/TButton/TButton.module.less @@ -66,6 +66,10 @@ &.full { width: 100%; } + &.detail_small { + min-width: 312px; + max-width: 312px; + } &.basic { &.disabled { background-color: #7a808d; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index 5b3539a1..3543bb4c 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -629,7 +629,13 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { ({ panelPrdtId, productData }) => fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get("prdtNm"), fp.isNotNil)() - ? fp.pipe(() => productData, fp.get("prdtNm"))() + ? // ? fp.pipe(() => productData, fp.get("prdtNm"))() + // `${fp.get("prdtId")(productData)} ${fp.get("prdtNm")(productData)}` + fp.pipe( + () => productData, + (data) => + `${fp.get("prdtId")(data)} ${fp.get("prdtNm")(data)}` + )() : null )(); diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less index cbfaf4f2..49c46f4d 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less @@ -8,6 +8,7 @@ z-index: 1; // 배경 컴포넌트(z-index: 0) 위에 표시 background: transparent !important; // 투명 배경으로 설정하여 뒤의 배경 컴포넌트가 보이도록 height: 100%; + overflow: hidden; // 하위 요소들도 투명 배경 (detailPanelWrap 스코프 내에서만 적용) > * { background: transparent !important; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx index b9ed5954..a1a063a9 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -1,18 +1,31 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import classNames from 'classnames'; import { throttle } from 'lodash'; import { PropTypes } from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { + useDispatch, + useSelector, +} from 'react-redux'; import Spotlight from '@enact/spotlight'; /* eslint-disable react/jsx-no-bind */ // src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx -import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; +import SpotlightContainerDecorator + from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; //image -import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; +import arrowDown + from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; +import indicatorDefaultImage + from '../../../../assets/images/img-thumb-empty-144@3x.png'; import { pushPanel } from '../../../actions/panelActions'; import { resetShowAllReviews } from '../../../actions/productActions'; import { showToast } from '../../../actions/toastActions'; @@ -46,13 +59,16 @@ import ProductTag from '../components/ProductTag'; import StarRating from '../components/StarRating'; // ProductContentSection imports import TScrollerDetail from '../components/TScroller/TScrollerDetail'; -import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription'; -import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new'; -import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; -import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; -import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike'; +import ProductDescription + from '../ProductContentSection/ProductDescription/ProductDescription'; +import ProductDetail + from '../ProductContentSection/ProductDetail/ProductDetail.new'; import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo'; -import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png'; +import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; +import ViewAllReviewsButton + from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; +import YouMayAlsoLike + from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike'; import QRCode from '../ProductInfoSection/QRCode/QRCode'; import ProductOverview from '../ProductOverview/ProductOverview'; // CSS imports @@ -62,42 +78,45 @@ import css from './ProductAllSection.module.less'; const Container = SpotlightContainerDecorator( { - enterTo: 'last-focused', + enterTo: "last-focused", preserveld: true, - leaveFor: { right: 'content-scroller-container' }, - spotlightDirection: 'vertical', + leaveFor: { right: "content-scroller-container" }, + spotlightDirection: "vertical", }, - 'div' + "div" ); const ContentContainer = SpotlightContainerDecorator( { - enterTo: 'default-element', + enterTo: "default-element", preserveld: true, leaveFor: { - left: 'spotlight-product-info-section-container', + left: "spotlight-product-info-section-container", }, - restrict: 'none', - spotlightDirection: 'vertical', + restrict: "none", + spotlightDirection: "vertical", }, - 'div' + "div" ); const HorizontalContainer = SpotlightContainerDecorator( { - enterTo: 'last-focused', + enterTo: "last-focused", preserveld: true, - defaultElement: 'spotlight-product-info-section-container', - spotlightDirection: 'horizontal', + defaultElement: "spotlight-product-info-section-container", + spotlightDirection: "horizontal", }, - 'div' + "div" ); // FP: Pure function to determine product data based on typeP const getProductData = curry((productType, themeProductInfo, productInfo) => pipe( when( - () => isVal(productType) && productType === 'theme' && isVal(themeProductInfo), + () => + isVal(productType) && + productType === "theme" && + isVal(themeProductInfo), () => themeProductInfo ), defaultTo(productInfo), @@ -113,17 +132,17 @@ const deriveFavoriteFlag = curry((favoriteOverride, productData) => { return favoriteOverride; } // 그렇지 않으면 productData의 favorYn 값을 사용 (기본값 'N') - return pipe(get('favorYn'), defaultTo('N'))(productData); + return pipe(get("favorYn"), defaultTo("N"))(productData); }); // FP: Pure function to extract review grade and order phone const extractProductMeta = (productInfo) => ({ - revwGrd: get('revwGrd', productInfo), - orderPhnNo: get('orderPhnNo', productInfo), + revwGrd: get("revwGrd", productInfo), + orderPhnNo: get("orderPhnNo", productInfo), }); // 레이아웃 확인용 샘플 컴포넌트 - Spottable로 변경 -const SpottableComponent = Spottable('div'); +const SpottableComponent = Spottable("div"); const LayoutSample = ({ onClick }) => ( state.common.appStatus.webOSVersion); + const webOSVersion = useSelector( + (state) => state.common.appStatus.webOSVersion + ); const groupInfos = useSelector((state) => state.product.groupInfo); // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 @@ -162,6 +183,33 @@ export default function ProductAllSection({ //하단부분까지 갔을때 체크용 const [documentHeight, setDocumentHeight] = useState(0); const [isBottom, setIsBottom] = useState(false); + + //qr코드 노출용 + const [isShowQRCode, setIsShowQRCode] = useState(true); + const timerRef = useRef(null); + + useEffect(() => { + const toggleQRCode = () => { + if (isShowQRCode) { + timerRef.current = setTimeout(() => { + setIsShowQRCode(false); + }, 10000); + } else { + timerRef.current = setTimeout(() => { + setIsShowQRCode(true); + }, 5000); + } + }; + + toggleQRCode(); + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [isShowQRCode]); + //버튼 active 표시용 const [activeProductBtn, setActiveProductBtn] = useState(false); @@ -176,20 +224,20 @@ export default function ProductAllSection({ // 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직 const isBillingProductVisible = useMemo(() => { return ( - productData?.pmtSuptYn === 'Y' && - productData?.grPrdtProcYn === 'N' && + productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "N" && panelInfo?.prdtId && - webOSVersion >= '6.0' + webOSVersion >= "6.0" ); }, [productData, webOSVersion, panelInfo?.prdtId]); // 구매 불가 상품 - DetailPanel.backup.jsx와 동일한 로직 const isUnavailableProductVisible = useMemo(() => { return ( - productData?.pmtSuptYn === 'N' || - (productData?.pmtSuptYn === 'Y' && - productData?.grPrdtProcYn === 'N' && - webOSVersion < '6.0' && + productData?.pmtSuptYn === "N" || + (productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "N" && + webOSVersion < "6.0" && panelInfo?.prdtId) ); }, [productData, webOSVersion, panelInfo?.prdtId]); @@ -197,8 +245,8 @@ export default function ProductAllSection({ // 그룹 상품 - DetailPanel.backup.jsx와 동일한 로직 const isGroupProductVisible = useMemo(() => { return ( - productData?.pmtSuptYn === 'Y' && - productData?.grPrdtProcYn === 'Y' && + productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "Y" && groupInfos && groupInfos.length > 0 ); @@ -206,7 +254,10 @@ export default function ProductAllSection({ // 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직 const isTravelProductVisible = useMemo(() => { - return panelInfo?.curationId && (panelInfo?.type === 'theme' || panelInfo?.type === 'hotel'); + return ( + panelInfo?.curationId && + (panelInfo?.type === "theme" || panelInfo?.type === "hotel") + ); }, [panelInfo]); // useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당 @@ -232,7 +283,9 @@ export default function ProductAllSection({ // ProductAllSection 마운트 시 showAllReviews 초기화 useEffect(() => { - console.log('[ProductAllSection] Component mounted - resetting showAllReviews to false'); + console.log( + "[ProductAllSection] Component mounted - resetting showAllReviews to false" + ); dispatch(resetShowAllReviews()); }, []); // 빈 dependency array = 마운트 시에만 실행 @@ -254,9 +307,9 @@ export default function ProductAllSection({ (productData.imgUrls600 && productData.imgUrls600[0]) || (productData.imgUrls && productData.imgUrls[0]) || productData.thumbnailUrl || - 'https://placehold.co/150x150', - brandLogo: productData.patncLogoPath || 'https://placehold.co/50x50', - productName: productData.prdtNm || '상품명 정보가 없습니다', + "https://placehold.co/150x150", + brandLogo: productData.patncLogoPath || "https://placehold.co/50x50", + productName: productData.prdtNm || "상품명 정보가 없습니다", avgRating: stats.averageRating || 5, reviewCount: stats.totalReviews || 0, }, @@ -266,32 +319,33 @@ export default function ProductAllSection({ // BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시 const handleBuyNowClick = useCallback(() => { - console.log('[BuyNow] Buy Now button clicked'); + console.log("[BuyNow] Buy Now button clicked"); dispatch( showToast({ - message: '', - type: 'buyOption', + message: "", + type: "buyOption", duration: 0, - position: 'bottom-center', + position: "bottom-center", }) ); }, [dispatch]); // ADD TO CART 버튼 클릭 핸들러 const handleAddToCartClick = useCallback(() => { - console.log('[AddToCart] Add To Cart button clicked'); + console.log("[AddToCart] Add To Cart button clicked"); // TODO: 장바구니 추가 로직 구현 }, []); // 디버깅: 실제 이미지 및 동영상 데이터 확인 useEffect(() => { - console.log('[ProductId] ProductAllSection productData check:', { + console.log("[ProductId] ProductAllSection productData check:", { hasProductData: !!productData, productDataPrdtId: productData && productData.prdtId, imgUrls600: productData && productData.imgUrls600, - imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length, + imgUrls600Length: + productData && productData.imgUrls600 && productData.imgUrls600.length, imgUrls600Type: Array.isArray(productData && productData.imgUrls600) - ? 'array' + ? "array" : typeof (productData && productData.imgUrls600), // 동영상 관련 정보 추가 prdtMediaUrl: productData && productData.prdtMediaUrl, @@ -303,7 +357,10 @@ export default function ProductAllSection({ }); }, [productData, renderItems]); - const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]); + const { revwGrd, orderPhnNo } = useMemo( + () => extractProductMeta(productInfo), + [productInfo] + ); // FP: derive favorite flag from props with local override, avoid non-I/O useEffect const [favoriteOverride, setFavoriteOverride] = useState(null); @@ -326,7 +383,7 @@ export default function ProductAllSection({ // User Reviews 스크롤 핸들러 추가 const handleUserReviewsClick = useCallback( - () => scrollToSection('scroll-marker-user-reviews'), + () => scrollToSection("scroll-marker-user-reviews"), [] ); @@ -343,7 +400,7 @@ export default function ProductAllSection({ // 동영상이 있으면 첫 번째에 추가 (Indicator.jsx와 동일한 로직) if (productData && productData.prdtMediaUrl) { items.push({ - type: 'video', + type: "video", url: productData.prdtMediaUrl, thumbnail: productData.thumbnailUrl960 || indicatorDefaultImage, index: 0, @@ -351,12 +408,17 @@ export default function ProductAllSection({ } // 이미지들 추가 - if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) { + if ( + productData && + productData.imgUrls600 && + productData.imgUrls600.length > 0 + ) { productData.imgUrls600.forEach((image, imgIndex) => { items.push({ - type: 'image', + type: "image", url: image, - index: productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex, + index: + productData && productData.prdtMediaUrl ? imgIndex + 1 : imgIndex, }); }); } @@ -375,7 +437,7 @@ export default function ProductAllSection({ // FP: Pure function for focus navigation to back button const handleSpotlightUpToBackButton = useCallback((e) => { e.stopPropagation(); - Spotlight.focus('spotlightId_backBtn'); + Spotlight.focus("spotlightId_backBtn"); }, []); // FP: Pure function for favorite flag change @@ -388,7 +450,7 @@ export default function ProductAllSection({ const handleThemeItemButtonClick = useCallback( pipe( () => setOpenThemeItemOverlay(true), - tap(() => setTimeout(() => Spotlight.focus('theme-close-button'), 0)) + tap(() => setTimeout(() => Spotlight.focus("theme-close-button"), 0)) ), [setOpenThemeItemOverlay] ); @@ -409,12 +471,12 @@ export default function ProductAllSection({ // FP: Curried scroll handlers const handleProductDetailsClick = useCallback( - () => scrollToSection('scroll-marker-product-details'), + () => scrollToSection("scroll-marker-product-details"), [scrollToSection] ); const handleYouMayAlsoLikeClick = useCallback( - () => scrollToSection('scroll-marker-you-may-also-like'), + () => scrollToSection("scroll-marker-you-may-also-like"), [scrollToSection] ); const scrollPositionRef = useRef(0); @@ -484,7 +546,12 @@ export default function ProductAllSection({ (descriptionRef.current?.scrollHeight || 0) + (reviewRef.current?.scrollHeight || 0) ); - }, [productDetailRef.current, descriptionRef.current, hasReviews, hasYouMayAlsoLike]); + }, [ + productDetailRef.current, + descriptionRef.current, + hasReviews, + hasYouMayAlsoLike, + ]); //spot관련 useEffect(() => { @@ -508,10 +575,10 @@ export default function ProductAllSection({
- {revwGrd && revwGrd !== '0.0' && ( + {revwGrd && revwGrd !== "0.0" && ( )}
@@ -525,11 +592,24 @@ export default function ProductAllSection({ productType={productType} >
- + {isShowQRCode ? ( + + ) : ( +
+
+

{$L("Scan QR")}

+

{$L("with your phone, Check Product")}

+

{$L("info & Purchase easily")}

+
+
+ )}
- {/* BUY NOW + ADD TO CART 버튼들 (결제 가능 상품일 때만 렌더링) */} {isBillingProductVisible && ( -
{$L('BUY NOW')}
+
{$L("BUY NOW")}
-
{$L('ADD TO CART')}
+
{$L("ADD TO CART")}
)} - - + + {/* BUY NOW + ADD TO CART 버튼들 (결제 가능 상품일 때만 렌더링) */} -
{$L('SHOP BY MOBILE')}
+
+ {$L("SHOP BY MOBILE")} +
{panelInfo && (
@@ -568,7 +657,7 @@ export default function ProductAllSection({ selectedPrdtId={panelInfo && panelInfo.prdtId} favoriteFlag={favoriteFlag} onFavoriteFlagChanged={onFavoriteFlagChanged} - kind={'item_detail'} + kind={"item_detail"} />
)} @@ -577,7 +666,9 @@ export default function ProductAllSection({
{orderPhnNo && ( <> -
{$L('Call to Order')}
+
+ {$L("Call to Order")} +
@@ -595,31 +686,34 @@ export default function ProductAllSection({ - {$L('PRODUCT DETAILS')} + {$L("PRODUCT DETAILS")} {hasReviews && ( - {$L('USER REVIEWS')} ({reviewTotalCount}) + {$L("USER REVIEWS")} ({reviewTotalCount}) )} {hasYouMayAlsoLike && ( - {$L('YOU MAY ALSO LIKE')} + {$L("YOU MAY ALSO LIKE")} )} {/* YouMayLike 버튼 렌더링 상태 로그 */} @@ -633,15 +727,18 @@ export default function ProductAllSection({ })()} */} - {panelInfo && panelInfo && panelInfo.type === 'theme' && !openThemeItemOverlay && ( - - {$L('THEME ITEM')} - - )} + {panelInfo && + panelInfo && + panelInfo.type === "theme" && + !openThemeItemOverlay && ( + + {$L("THEME ITEM")} + + )}
-
+
{/* */}
{renderItems.length > 0 ? ( renderItems.map((item, index) => - item.type === 'video' ? ( + item.type === "video" ? ( )}
-
+
{/* 리뷰가 있을 때만 UserReviews 섹션 표시 */} {hasReviews && ( <> -
+
{hasYouMayAlsoLike && (
-
+
{/* {(() => { console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', { @@ -774,5 +885,5 @@ export default function ProductAllSection({ } ProductAllSection.propTypes = { - productType: PropTypes.oneOf(['buyNow', 'shopByMobile', 'theme']).isRequired, + productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired, }; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less index 2edb3558..84f9e601 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less @@ -710,7 +710,9 @@ display: flex; justify-content: flex-start; align-items: center; - + &.buttonHasNoCart { + padding-top: 0; + } > * { margin-right: 6px; &:last-child { @@ -885,7 +887,37 @@ display: flex; flex-direction: column; align-items: flex-end; - + .qrRollingWrap { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + text-align: center; + width: 240px; + height: 240px; + background: #fff; + border: 1px solid #fff; + .innerText { + width: 100%; + padding: 0 20px; + h3 { + word-break: break-word; + font-size: 36px; + font-weight: bold; + color: @PRIMARY_COLOR_RED; + & + p { + margin-top: 18px; + } + } + p { + font-size: 24px; + font-weight: bold; + line-height: 1.17; + color: @COLOR_GRAY05; + word-break: keep-all; + } + } + } > * { margin-bottom: 10px; &:last-child { diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewDetail/UserReviewDetail.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewDetail/UserReviewDetail.module.less index e44a6a76..902c4a98 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewDetail/UserReviewDetail.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewDetail/UserReviewDetail.module.less @@ -152,6 +152,7 @@ font-weight: 400; line-height: 31px; word-wrap: break-word; + overflow: hidden; } } } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx index 9d258938..ed2ce04e 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx @@ -86,8 +86,8 @@ export default function UserReviewsPopup({ case "user-reviews": return { title: $L("User Reviews"), - hasIcon: true, - iconType: "user-reviews", + hasIcon: false, + iconType: null, }; case "customer-images": default: diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less index 0c364b79..c19a9dd6 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less @@ -118,9 +118,13 @@ // margin-bottom: 30px; // 세로 마진도 증가 margin-right: 6px; // 마진 증가로 균등 분배 > div { + width: 226px; + height: 220px; + overflow: hidden; > img { width: 226px; height: 220px; + object-fit: contain; } } @@ -145,7 +149,7 @@ width: 100%; height: 100%; border-radius: 12px; - object-fit: cover; + object-fit: contain; padding: 0; box-sizing: border-box; } @@ -191,7 +195,7 @@ width: 100%; height: 100%; border-radius: 12px; - object-fit: cover; + object-fit: contain; padding: 4px; box-sizing: border-box; } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less index 31bf09e4..cafba670 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less @@ -42,7 +42,7 @@ } .price { font-weight: bold; - font-size: 60px; + font-size: 52px; color: @COLOR_WHITE; margin-right: 9px; line-height: 1; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx index da6fc63c..2d7778ef 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx @@ -1,14 +1,31 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import MobileSendPopUp from "../../../components/MobileSend/MobileSendPopUp"; -import * as Config from "../../../utils/Config"; -import { setHidePopup, setShowPopup } from "../../../actions/commonActions"; -import Spotlight from "@enact/spotlight"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; + +import { + useDispatch, + useSelector, +} from 'react-redux'; + +import Spotlight from '@enact/spotlight'; + +import { + setHidePopup, + setShowPopup, +} from '../../../actions/commonActions'; import { sendLogShopByMobile, sendLogTotalRecommend, -} from "../../../actions/logActions"; -import { $L, formatLocalDateTime } from "../../../utils/helperMethods"; +} from '../../../actions/logActions'; +import MobileSendPopUp from '../../../components/MobileSend/MobileSendPopUp'; +import * as Config from '../../../utils/Config'; +import { + $L, + formatLocalDateTime, +} from '../../../utils/helperMethods'; export default function DetailMobileSendPopUp({ panelInfo, @@ -167,11 +184,13 @@ export default function DetailMobileSendPopUp({ subTitle={mobileSendPopUpSubtitle} patncNm={productData?.patncNm} productImg={mobileSendPopUpProductImg} + productId={productData?.prdtId} patnrId={panelInfo?.patnrId} prdtId={panelInfo?.prdtId} smsTpCd={panelInfo?.type === "hotel" ? "APP00205" : "APP00201"} curationId={panelInfo?.curationId} curationNm={panelInfo?.curationNm} + brandLogo={productData?.patncLogoPath} // hotelId={ // panelInfo?.type === "hotel" && hotelInfos[selectedIndex]?.hotelId // } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less index dcb534a4..67e1dcd9 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less @@ -11,9 +11,10 @@ background-color: transparent; // DetailPanel에서는 배경 투명 .title { + width: 1710px; font-size: 25px; font-weight: 600; - color: #EAEAEA; + color: #eaeaea; padding-left: 0; letter-spacing: 1px; text-transform: uppercase; @@ -46,4 +47,4 @@ background-repeat: no-repeat; flex-shrink: 0; margin-right: 10px; // 파트너사 로고 후 10px gap -} \ No newline at end of file +} diff --git a/com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx b/com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx index 679fe1bb..7d55edaa 100644 --- a/com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx +++ b/com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx @@ -1,30 +1,38 @@ -import React, { useCallback, useEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import classNames from "classnames"; +import React, { + useCallback, + useEffect, + useRef, +} from 'react'; -import { popPanel } from "../../actions/panelActions"; -import useReviews from "../../hooks/useReviews/useReviews"; -import UserReviewHeader from "./UserReviewHeader"; -import TPanel from "../../components/TPanel/TPanel"; -import TBody from "../../components/TBody/TBody"; -import StarRating from "../DetailPanel/components/StarRating"; -import FilterItemButton from "./components/FilterItemButton"; -import UserReviewsList from "./components/UserReviewsList"; -import fp from "../../utils/fp"; -import css from "./UserReviewPanel.module.less"; +import classNames from 'classnames'; +import { + useDispatch, + useSelector, +} from 'react-redux'; + +import { popPanel } from '../../actions/panelActions'; +import TBody from '../../components/TBody/TBody'; +import TPanel from '../../components/TPanel/TPanel'; +import useReviews from '../../hooks/useReviews/useReviews'; +import fp from '../../utils/fp'; +import StarRating from '../DetailPanel/components/StarRating'; +import FilterItemButton from './components/FilterItemButton'; +import UserReviewsList from './components/UserReviewsList'; +import UserReviewHeader from './UserReviewHeader'; +import css from './UserReviewPanel.module.less'; const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { const dispatch = useDispatch(); // panelInfo에서 prdtId와 patnrId 추출 const prdtId = fp.pipe( () => panelInfo, - fp.get('prdtId'), + fp.get("prdtId"), fp.defaultTo(null) )(); const patnrId = fp.pipe( () => panelInfo, - fp.get('patnrId'), + fp.get("patnrId"), fp.defaultTo(null) )(); @@ -33,19 +41,19 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { previewReviews, displayReviews, filteredReviews, - userReviewPanelReviews, // 페이징된 리뷰들 (3개씩) - userReviewPanelPage, // 현재 페이지 - userReviewPanelHasNext, // 다음 페이지 존재 여부 - userReviewPanelHasPrev, // 이전 페이지 존재 여부 + userReviewPanelReviews, // 페이징된 리뷰들 (3개씩) + userReviewPanelPage, // 현재 페이지 + userReviewPanelHasNext, // 다음 페이지 존재 여부 + userReviewPanelHasPrev, // 이전 페이지 존재 여부 userReviewPanelTotalPages, // 전체 페이지 수 - goToNextUserReviewPage, // 다음 페이지로 이동 - goToPrevUserReviewPage, // 이전 페이지로 이동 + goToNextUserReviewPage, // 다음 페이지로 이동 + goToPrevUserReviewPage, // 이전 페이지로 이동 applyRatingFilter, applySentimentFilter, clearAllFilters, currentFilter, filterCounts, - stats + stats, } = useReviews(prdtId, patnrId); // 포커스 복원을 위한 ref @@ -61,7 +69,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { productDataPrdtId: productData.prdtId, hasProductData: !!productData, previewReviews: previewReviews ? previewReviews.length : 0, - displayReviews: displayReviews ? displayReviews.length : 0 + displayReviews: displayReviews ? displayReviews.length : 0, }); // UserReviewPanel은 새로운 API 호출 없이 기존 데이터만 사용 @@ -70,41 +78,43 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { hasPreviewReviews: !!previewReviews, hasDisplayReviews: !!displayReviews, previewCount: previewReviews ? previewReviews.length : 0, - displayCount: displayReviews ? displayReviews.length : 0 + displayCount: displayReviews ? displayReviews.length : 0, }); // 데이터 파싱 - panelInfo에서 직접 가져오기 (Redux productData가 빈 객체가 되는 문제 해결) const productImage = fp.pipe( () => panelInfo, - fp.get('productImage'), - fp.defaultTo('https://placehold.co/150x150') + fp.get("productImage"), + fp.defaultTo("https://placehold.co/150x150") )(); const brandLogo = fp.pipe( () => panelInfo, - fp.get('brandLogo'), - fp.defaultTo('https://placehold.co/50x50') + fp.get("brandLogo"), + fp.defaultTo("https://placehold.co/50x50") )(); const productId = fp.pipe( () => panelInfo, - fp.get('prdtId'), + fp.get("prdtId"), fp.defaultTo(null) )(); const productName = fp.pipe( () => panelInfo, - fp.get('productName'), - fp.defaultTo('상품명 정보가 없습니다') + fp.get("productName"), + fp.defaultTo("상품명 정보가 없습니다") )(); // 페이징 후 포커스 복원 함수 - 중간 리뷰(index 1)로 포커스 const restoreFocusAfterPaging = useCallback(() => { setTimeout(() => { - const targetElement = document.querySelector(`[data-spotlight-id="user-review-1"]`); + const targetElement = document.querySelector( + `[data-spotlight-id="user-review-1"]` + ); if (targetElement && targetElement.focus) { targetElement.focus(); - console.log('[UserReviewPanel] 중간 리뷰로 포커스 복원 완료'); + console.log("[UserReviewPanel] 중간 리뷰로 포커스 복원 완료"); } }, 100); }, []); @@ -112,7 +122,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { // 개선된 페이징 함수들 const handleNextPage = useCallback(() => { if (userReviewPanelHasNext) { - console.log('[UserReviewPanel] 다음 페이지로 이동'); + console.log("[UserReviewPanel] 다음 페이지로 이동"); goToNextUserReviewPage(); restoreFocusAfterPaging(); } @@ -120,7 +130,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { const handlePrevPage = useCallback(() => { if (userReviewPanelHasPrev) { - console.log('[UserReviewPanel] 이전 페이지로 이동'); + console.log("[UserReviewPanel] 이전 페이지로 이동"); goToPrevUserReviewPage(); restoreFocusAfterPaging(); } @@ -132,44 +142,92 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { const avgRating = stats.averageRating || 5; // 별점 필터링 핸들러들 - const handleRatingFilter = useCallback((rating) => { - console.log('[ProductId] Rating filter applied:', rating); - console.log('[ProductId] applyRatingFilter function:', !!applyRatingFilter); - applyRatingFilter(rating); // 'all' 값을 그대로 전달 - }, [applyRatingFilter]); + const handleRatingFilter = useCallback( + (rating) => { + console.log("[ProductId] Rating filter applied:", rating); + console.log( + "[ProductId] applyRatingFilter function:", + !!applyRatingFilter + ); + applyRatingFilter(rating); // 'all' 값을 그대로 전달 + }, + [applyRatingFilter] + ); - const handleAllStarsFilter = useCallback(() => handleRatingFilter('all'), [handleRatingFilter]); - const handle5StarsFilter = useCallback(() => handleRatingFilter(5), [handleRatingFilter]); - const handle4StarsFilter = useCallback(() => handleRatingFilter(4), [handleRatingFilter]); - const handle3StarsFilter = useCallback(() => handleRatingFilter(3), [handleRatingFilter]); - const handle2StarsFilter = useCallback(() => handleRatingFilter(2), [handleRatingFilter]); - const handle1StarsFilter = useCallback(() => handleRatingFilter(1), [handleRatingFilter]); + const handleAllStarsFilter = useCallback( + () => handleRatingFilter("all"), + [handleRatingFilter] + ); + const handle5StarsFilter = useCallback( + () => handleRatingFilter(5), + [handleRatingFilter] + ); + const handle4StarsFilter = useCallback( + () => handleRatingFilter(4), + [handleRatingFilter] + ); + const handle3StarsFilter = useCallback( + () => handleRatingFilter(3), + [handleRatingFilter] + ); + const handle2StarsFilter = useCallback( + () => handleRatingFilter(2), + [handleRatingFilter] + ); + const handle1StarsFilter = useCallback( + () => handleRatingFilter(1), + [handleRatingFilter] + ); + + const handleAromaClick = useCallback(() => console.log("Aroma clicked"), []); + const handleVanillaClick = useCallback( + () => console.log("Vanilla clicked"), + [] + ); + const handleCinnamonClick = useCallback( + () => console.log("Cinnamon clicked"), + [] + ); + const handleQualityClick = useCallback( + () => console.log("Quality clicked"), + [] + ); - const handleAromaClick = useCallback(() => console.log('Aroma clicked'), []); - const handleVanillaClick = useCallback(() => console.log('Vanilla clicked'), []); - const handleCinnamonClick = useCallback(() => console.log('Cinnamon clicked'), []); - const handleQualityClick = useCallback(() => console.log('Quality clicked'), []); - // 감정 필터링 핸들러들 - 별점 필터와 동일한 방식 - const handleSentimentFilter = useCallback((sentiment) => { - console.log('[ProductId] Sentiment filter applied:', sentiment); - applySentimentFilter(sentiment === 'all' ? null : sentiment); - }, [applySentimentFilter]); - - const handlePositiveClick = useCallback(() => handleSentimentFilter('positive'), [handleSentimentFilter]); - const handleNegativeClick = useCallback(() => handleSentimentFilter('negative'), [handleSentimentFilter]); + const handleSentimentFilter = useCallback( + (sentiment) => { + console.log("[ProductId] Sentiment filter applied:", sentiment); + applySentimentFilter(sentiment === "all" ? null : sentiment); + }, + [applySentimentFilter] + ); + + const handlePositiveClick = useCallback( + () => handleSentimentFilter("positive"), + [handleSentimentFilter] + ); + const handleNegativeClick = useCallback( + () => handleSentimentFilter("negative"), + [handleSentimentFilter] + ); // UserReviewPanel 마운트 시 기본 All stars 필터 적용 useEffect(() => { - if (prdtId && currentFilter.type === 'rating' && currentFilter.value === 'all') { - console.log('[ProductId] UserReviewPanel 기본 All stars 필터 이미 적용됨'); + if ( + prdtId && + currentFilter.type === "rating" && + currentFilter.value === "all" + ) { + console.log( + "[ProductId] UserReviewPanel 기본 All stars 필터 이미 적용됨" + ); } }, [prdtId, currentFilter]); - + // 메모리 해제를 위한 cleanup 함수 useEffect(() => { return () => { - console.log('[ProductId] UserReviewPanel unmounting - clearing filters'); + console.log("[ProductId] UserReviewPanel unmounting - clearing filters"); clearAllFilters(); // 필터 상태 초기화로 메모리 해제 }; }, [clearAllFilters]); @@ -186,22 +244,33 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { userReviewPanelReviewsLength: userReviewPanelReviews.length, // UserReviewPanel에서 표시할 4개 currentFilter: currentFilter, filterCounts: filterCounts, - isDataFromCache: true // API 호출 없이 캐시된 데이터 사용 + isDataFromCache: true, // API 호출 없이 캐시된 데이터 사용 }); } catch (error) { console.error("[ProductId] UserReviewPanel 로그 오류:", error); } - }, [reviewCount, filteredCount, avgRating, displayReviews, userReviewPanelReviews, currentFilter, filterCounts]); + }, [ + reviewCount, + filteredCount, + avgRating, + displayReviews, + userReviewPanelReviews, + currentFilter, + filterCounts, + ]); const handleBackButton = useCallback(() => { console.log(`[ProductId] Back button clicked - returning to DetailPanel`); dispatch(popPanel()); }, [dispatch]); - const handleCancel = useCallback((e) => { - dispatch(popPanel()); - e.stopPropagation(); - }, [dispatch]); + const handleCancel = useCallback( + (e) => { + dispatch(popPanel()); + e.stopPropagation(); + }, + [dispatch] + ); return ( { {/* 왼쪽 필터 영역 */}
-
Filter Reviews
+
+ Filter Reviews +
{/* 모든 필터들을 묶는 컨테이너 */} @@ -267,60 +338,82 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { {/* Rating 필터 섹션 */}
-
Rating
+
+ Rating +
@@ -328,7 +421,11 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => { {/* Keywords 필터 섹션 */}
-
Keywords
+
+ Keywords +
{ {/* Sentiment 필터 섹션 */}
-
Sentiment
+
+ Sentiment +
{ ariaLabel="Filter by positive sentiment" dataSpotlightUp="filter-quality" dataSpotlightDown="filter-negative" - isActive={currentFilter.type === 'sentiment' && currentFilter.value === 'positive'} + isActive={ + currentFilter.type === "sentiment" && + currentFilter.value === "positive" + } /> { spotlightId="filter-negative" ariaLabel="Filter by negative sentiment" dataSpotlightUp="filter-positive" - isActive={currentFilter.type === 'sentiment' && currentFilter.value === 'negative'} + isActive={ + currentFilter.type === "sentiment" && + currentFilter.value === "negative" + } />
diff --git a/com.twin.app.shoptime/src/views/UserReview/components/FilterItemButton.module.less b/com.twin.app.shoptime/src/views/UserReview/components/FilterItemButton.module.less index b0793931..a7670241 100644 --- a/com.twin.app.shoptime/src/views/UserReview/components/FilterItemButton.module.less +++ b/com.twin.app.shoptime/src/views/UserReview/components/FilterItemButton.module.less @@ -5,35 +5,35 @@ // TButton 기본 스타일 무력화 all: unset; box-sizing: border-box; - + // FilterItem 레이아웃 스타일 (요구사항에 맞는 스타일 적용) display: flex; padding: 20px; flex-direction: column; align-items: flex-start; border-radius: 100px; - border: 1px solid #DADADA; - background: #FFF; + border: 1px solid #dadada; + background: #fff; cursor: pointer; white-space: nowrap; - + // 고정 넓이와 마진 설정으로 일정한 간격 유지 (한 라인에 4개 표시) width: 90px; - margin-right: 40px; + margin-right: 15px; margin-bottom: 10px; - + // 각 라인의 마지막 아이템은 오른쪽 마진 제거 &:last-child { margin-right: 0; } - + // 포커스 상태: 빨간색 배경 &:focus { outline: none; box-shadow: none; - background: #D32F2F !important; // 빨간색 - border: 1px solid #D32F2F !important; - + background: #d32f2f !important; // 빨간색 + border: 1px solid #d32f2f !important; + .filterItemButton__text { color: white !important; } @@ -45,9 +45,9 @@ padding: 20px; flex-direction: column; align-items: flex-start; - background: #7A808D !important; // 회색 (선택됨) + background: #7a808d !important; // 회색 (선택됨) border-radius: 100px; - border: 1px solid #7A808D !important; + border: 1px solid #7a808d !important; white-space: nowrap; } @@ -55,7 +55,7 @@ text-align: center; color: black; font-size: 24px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 24px; @@ -63,7 +63,7 @@ text-align: center; color: white; font-size: 24px; - font-family: 'LG Smart UI'; + font-family: "LG Smart UI"; font-weight: 400; line-height: 24px; } @@ -85,4 +85,4 @@ padding-right: 0; } } -} \ No newline at end of file +}