From 61c61eae9b3c50e89ed0c453b1b0b32e6e87cd94 Mon Sep 17 00:00:00 2001 From: "junghoon86.park" Date: Tue, 18 Nov 2025 17:17:10 +0900 Subject: [PATCH] =?UTF-8?q?[=EC=83=81=ED=92=88=20=EC=83=81=EC=84=B8]=20?= =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=85=B8=EC=B6=9C=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EA=B1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠폰 노출 처리 - 다운로드 할때 중복처리부분 처리 - 기존 다운로드 로직에서 조건 좀 더 추가. - spotlight 추가 및 변경. --- .../assets/images/icons/coupon.png | Bin 0 -> 1442 bytes .../ProductAllSection/ProductAllSection.jsx | 358 +++++++++++++++++- .../ProductAllSection.module.less | 191 +++++++++- 3 files changed, 535 insertions(+), 14 deletions(-) create mode 100644 com.twin.app.shoptime/assets/images/icons/coupon.png diff --git a/com.twin.app.shoptime/assets/images/icons/coupon.png b/com.twin.app.shoptime/assets/images/icons/coupon.png new file mode 100644 index 0000000000000000000000000000000000000000..5be891bd75495aa9148eb356c091a64b6d32fb0e GIT binary patch literal 1442 zcmV;T1zq}yP)loAZpJ|nG;zI2$_P57 z(Bq%Y&CO(cdpor~ASn8S5%~yLKdZPRh7kRvO!Spv-`w0BO(v6pitG+DeXz5$bF%Vp zukX0NzCMP?15(}$1pfxXzbxH9e?x^{E5qI0-NX4iW@2AmUG;JCos_kKQv9XeZhy;f zXJX@I7(G5dp5kgm%DUM{hL8A4$iE=;F{#k+#n=2wV6yt0l=mdd?goU8CDAy;-l7Km zg`eTx-d;)`b8&GIZES4BxGJg$7<7EZ#i#)h4{)P2n1x16uhnXeNFkGKLx`S0bnCee z`u)L|AqhSi&gi_zEB|9eKM{m>`B#XH?b;%_bda(VOwoVh;?@$@!gq%6Sp zA$m(^BosS2a{3}cA9WHgiJjl9f*G|g~l z4-$Ra9xVHWJm4*kR_+5Vc#z!HdwzcI<>@X3@!q)c5Zd*3&K*# zc}vn8k&IAcsxQ_=;?)Nvz$!KlqlE0#b~ed7Eo?EEqlg9*Rr$ zJg5+>*uFNLTtFFFk9i;*`qXx58fv!ny$MGwcCqn92ZES?sivT2Ti+XozE+L0xUxSQ zlb}y_5~?qfqc)cKw54L3tshqK$tF}&%)&`4+`O}$-wMTsH!1D>rFC9TY!*%?g3!oV zhJ}JJiOr=0LURMEPckIylYeV)l9awj_Gr(FJla{;&p5_x`+O!joYfuj03;xn)Y#!6 zOWUin%r=(KnWNB5?Xx<^A-i+>uU8`ETl0R9`_{Z+-5r{FOb1ldaGGPGj)!Jvuah-T z(?+>BS)GIA;v?U6Fc3XFJY*GvGuzeHvN{rV26ecHi`3;YC_$OVEVfHldo47>LqXPn z+%i1kQo>@(76k^u&%3+3ecjA7Z9Ai+uqiIC;rI9VO=*jjt@%%#PDivW$`?!xXP;K2 z3$J2*5j<%?N+HmYS4u@?F}wmn&4UV2u`5Kyt`HTwLR9PuQL!sTB(^&2R>balY75F6 zDataYk!KZ~XPu$Msz=O`&pX75Fq%mA7R|PK{1OCb+tK(_)RHnStL&yrzYB3PnPeIV z4vW3#Ny%>_awFSYll43~u83U*Al^&XwW@8c?&{8?&u>5ZpVP z>%D=2zOeOUp>?X|WUY*h!7yWCQqyX+`sD5)E6XHswR*4bTxkj*!w1N4@Ajsz1KwZL zZnsxX0#{Dgk>LY&7F@Sidrrc8`HO^RtacZFi9n;#_z2+-@MK;6rxR1mmjLj;Px-~G wcqLz$;Os4)pPx6kwzj+$98US|Tg>nO0BXXLJE>uOVE_OC07*qoM6N<$g5ra%t^fc4 literal 0 HcmV?d00001 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 ee18f332..b0e8ae92 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -1,30 +1,65 @@ -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'; +import couponImg from '../../../../assets/images/icons/coupon.png'; // import Spottable from '@enact/spotlight/Spottable'; //image -import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; -import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.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 { + setHidePopup, + setShowPopup, +} from '../../../actions/commonActions.js'; +import { + getProductCouponDownload, + getProductCouponSearch, + getProductCouponTotDownload, +} from '../../../actions/couponActions.js'; // import { pushPanel } from '../../../actions/panelActions'; -import { minimizeModalMedia, restoreModalMedia } from '../../../actions/mediaActions'; +import { + minimizeModalMedia, + restoreModalMedia, +} from '../../../actions/mediaActions'; import { pauseFullscreenVideo } from '../../../actions/playActions'; import { resetShowAllReviews } from '../../../actions/productActions'; -import { clearAllToasts, removeToast, showToast } from '../../../actions/toastActions'; +import { + clearAllToasts, + removeToast, + showToast, +} from '../../../actions/toastActions'; +import CustomImage from '../../../components/CustomImage/CustomImage.jsx'; // ProductInfoSection imports import TButton from '../../../components/TButton/TButton'; +import TPopUp from '../../../components/TPopUp/TPopUp.jsx'; +import TVirtualGridList + from '../../../components/TVirtualGridList/TVirtualGridList.jsx'; import useReviews from '../../../hooks/useReviews/useReviews'; import useScrollTo from '../../../hooks/useScrollTo'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { panel_names } from '../../../utils/Config'; +import * as Config from '../../../utils/Config.js'; import { andThen, curry, @@ -51,13 +86,19 @@ import StarRating from '../components/StarRating'; // ProductContentSection imports import TScrollerDetail from '../components/TScroller/TScrollerDetail'; import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton'; -import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription'; -import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new'; -import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx'; -import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo.v3'; +import ProductDescription + from '../ProductContentSection/ProductDescription/ProductDescription'; +import ProductDetail + from '../ProductContentSection/ProductDetail/ProductDetail.new'; +import { + ProductVideoV2, +} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx'; +import ProductVideo + from '../ProductContentSection/ProductVideo/ProductVideo.v3'; import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; // import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; -import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike'; +import YouMayAlsoLike + from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike'; import QRCode from '../ProductInfoSection/QRCode/QRCode'; import ProductOverview from '../ProductOverview/ProductOverview'; // CSS imports @@ -98,6 +139,8 @@ const HorizontalContainer = SpotlightContainerDecorator( 'div' ); +const SpottableComponent = Spottable("div"); + const getProductData = curry((productType, themeProductInfo, productInfo) => pipe( when( @@ -148,7 +191,16 @@ export default function ProductAllSection({ // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 const youmaylikeData = useSelector((state) => state.main.youmaylikeData); - + //coupon + const { partnerCoupon } = useSelector( + (state) => state.coupon.productCouponSearchData + ); + const { userNumber } = useSelector( + (state) => state.common.appStatus.loginUserData + ); + const { popupVisible, activePopup } = useSelector( + (state) => state.common.popup + ); // ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략) const [productVideoVersion, setProductVideoVersion] = useState(1); // 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화) @@ -177,6 +229,215 @@ export default function ProductAllSection({ // 스크롤 위치에 따른 MediaPanel 제어 상태 const [shouldMinimizeMedia, setShouldMinimizeMedia] = useState(false); + //coupon + const [promotions, setPromotions] = useState([]); + const [selectedCouponIndex, setSelectedCouponIndex] = useState(0); + const [selectedCoupon, setSelectedCoupon] = useState(); + const [downloadCouponArr, setDownloadCouponArr] = useState([]); + const [couponTypes, setCouponTypes] = useState(null); + const [couponCodes, setCouponCodes] = useState(""); + const [focused, setFocused] = useState(false); + + useEffect(()=>{ + dispatch( + getProductCouponSearch({ + patnrId: selectedPatnrId, + prdtId: selectedPrdtId, + mbrNo: userNumber, + }) + ); + },[dispatch]) + + const getCouponCode = () => { + const snoArray = []; + + for (let i = 0; i < selectedCoupon.length; i++) { + if (selectedCoupon[i].downloadYn === "Y") { + snoArray.push(selectedCoupon[i].cpnSno); + } + } + + setCouponCodes(snoArray.join(", ")); + }; + + useEffect(() => { + if (selectedCoupon) { + getCouponCode(); + } + }, [selectedCoupon]); + + useEffect(() => { + const newPromotions = []; + if (partnerCoupon && partnerCoupon.length > 0) { + newPromotions.push($L("SPECIAL PROMOTION")); + } + setPromotions(newPromotions); + }, [partnerCoupon]); + + const handleCouponClick = useCallback( + (idx, promotion) => { + if (!userNumber) { + dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup)); + return; + } + if (promotion === "SPECIAL PROMOTION") { + setSelectedCoupon(partnerCoupon); + } + setCouponTypes(idx); + dispatch(setShowPopup(Config.ACTIVE_POPUP.couponPopup)); + }, + [dispatch, popupVisible, promotions, userNumber] + ); + + const handleCouponTotDownload = useCallback(() => { + if (selectedCoupon && userNumber) { + const couponCodesArray = couponCodes + .split(",") + .map((code) => parseInt(code.trim())); + setDownloadCouponArr((prevArr) => [...prevArr, ...couponCodesArray]); + + dispatch( + getProductCouponTotDownload({ + mbrNo: userNumber, + cpnSnoAll: couponCodes, + }) + ); + } + }, [dispatch, selectedCoupon, userNumber, couponCodes]); + + const onClose = useCallback( + (spotlightId) => { + dispatch(setHidePopup()); + + let currentSpot; + if (typeof spotlightId === "string") { + currentSpot = spotlightId; + } else { + currentSpot = SpotlightIds.DETAIL_BUYNOW; + } + + if (currentSpot) { + setTimeout(() => { + Spotlight.focus(currentSpot); + }); + } + }, + [dispatch] + ) + + const renderItem = useCallback( + ({ index, ...rest }) => { + const { + cpnDctrt, + cpnTtl, + cpnAplyMinPurcAmt, + cpnAplyMaxDcAmt, + cpnAplyStrtDtt, + cpnAplyEndDtt, + shptmDcTpCd, + currSign, + downloadYn, + cpnSno, + dcAmt, + duplDwldYn, + } = selectedCoupon[index]; + + const couponAplyStartDate = cpnAplyStrtDtt.split(" ")[0]; + const couponAplyEndDate = cpnAplyEndDtt.split(" ")[0]; + + const onFocus = (index) => { + setSelectedCouponIndex(index); + setFocused(true); + }; + + const onBlur = () => { + setFocused(false); + }; + + const handleDownloadClick = () => { + if ( + downloadCouponArr.length > 0 && + downloadCouponArr.includes(cpnSno) && + (downloadYn === "N" || (duplDwldYn === "Y" && downloadYn === "Y")) + ) { + return; + } + setDownloadCouponArr((prevArr) => [...prevArr, cpnSno]); + dispatch( + getProductCouponDownload({ mbrNo: userNumber, cpnSno: cpnSno }) + ); + }; + + return ( + onFocus(index)} + onBlur={onBlur} + onClick={handleDownloadClick} + > +
+
+ {shptmDcTpCd === "CPN00401" && ( + {`${currSign}${dcAmt}`} + )} + {shptmDcTpCd === "CPN00402" && ( + {`${cpnDctrt}%`} + )} + + {cpnTtl} + +
+ +
+ + {$L( + "Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)" + ) + .replace("{cpnAplyMinPurcAmt}", cpnAplyMinPurcAmt) + .replace("{cpnAplyMaxDcAmt}", cpnAplyMaxDcAmt)} + + + {couponAplyStartDate}~{couponAplyEndDate} + +
+ +
0 && + downloadCouponArr.includes(cpnSno) && + css.disable, + downloadYn === "N" && css.disable, + duplDwldYn === "N" && downloadYn === "Y" && css.disable, + !downloadCouponArr.includes(cpnSno) && + index === selectedCouponIndex && + focused && + css.focused + )} + aria-label="Download Button" + > + {downloadCouponArr.length > 0 && + downloadCouponArr.includes(cpnSno) + ? $L("DOWNLOAD COMPLETED") + : $L("DOWNLOAD")} +
+
+
+ ); + }, + [selectedCoupon, downloadCouponArr, focused, dispatch] + ); useEffect(() => { const toggleQRCode = () => { @@ -631,6 +892,16 @@ export default function ProductAllSection({ // BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로 const handleSpotlightUpFromBuyButtons = useCallback((e) => { + e.stopPropagation(); + if(promotions){ + Spotlight.focus('detail-coupon-button'); + } else { + Spotlight.focus('spotlightId_backBtn'); + } + }, [promotions]); + + + const handleSpotlightUpFromCouponButtons = useCallback((e) => { e.stopPropagation(); Spotlight.focus('spotlightId_backBtn'); }, []); @@ -928,6 +1199,8 @@ export default function ProductAllSection({ }; }, []); + + // 초기 로딩 중에는 Skeleton 표시 if (isInitialLoading) { return ( @@ -936,6 +1209,9 @@ export default function ProductAllSection({ ); } + + + return ( @@ -983,6 +1259,30 @@ export default function ProductAllSection({ + {promotions.map((promotion, idx) => { + return( + +
+
SPECIAL PROMOTION
+
Coupon only applicable to this product!
+
+ { + handleCouponClick(idx, promotion); + }} + onSpotlightUp={handleSpotlightUpFromCouponButtons} + size="detail_very_small" + > +
+ COUPON +
+ +
+
+ ) + })} {isBillingProductVisible && ( )} + {/* COUPON POPUP */} + {activePopup === Config.ACTIVE_POPUP.couponPopup && ( + + + {selectedCoupon && selectedCoupon.length > 0 && ( + + )} + +
{`1/${selectedCoupon?.length}`}
+
+ )}
); } 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 e92555e2..36ea3189 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 @@ -407,10 +407,74 @@ } } +/* COUPON 버튼 */ +.couponContainer { + width:100%; + display:flex; + justify-content: space-between; + align-items: center; + .couponTitleText { + width:350px; + height:60px; + .firstTitle { + font-size:25px; + font-weight:600; + color:@COLOR_WHITE; + letter-spacing: -1px; + } + .secondTitle { + font-size:22px; + font-weight:400; + color:@COLOR_WHITE; + letter-spacing: -1px; + } + } + + .couponButton { + flex: 1 1 0% !important; + width: auto !important; + height: 60px !important; + background: rgba(68, 68, 68, 0.5) !important; + border-radius: 6px !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + &:focus { + background: @PRIMARY_COLOR_RED !important; + > div { + .couponText { + color: white !important; + } + } + } + > div { + display:flex; + align-items: center; + justify-content: center; + .couponText { + color: white !important; + font-size: 25px !important; + font-family: @baseFont !important; + font-weight: 400 !important; + text-align: center !important; + } + .buttonImg { + width:30px; + height:24px; + margin-left:5px; + } + } + } + +} + /* BUY NOW + ADD TO CART 버튼 스타일 */ .buyNowCartContainer { width: 100%; - padding-top: 19px; + padding-top: 10px; display: flex; justify-content: space-between; align-items: center; @@ -637,3 +701,128 @@ // PlayerPanel 모달이 이 영역에서만 재생되도록 설정 z-index: 1; } + +//coupon +.itemWrap { + overflow: hidden; + position: relative; + + &::after, + &::before { + position: absolute; + width: 253px; + height: 320px; + top: 0; + content: ""; + z-index: 2; + } + &::after { + left: 0; + background: linear-gradient(to right, #f8f8f8, transparent); + } + &::before { + right: 0; + background: linear-gradient(to left, #f8f8f8, transparent); + } + .itemList { + .size(@w: 440px , @h: 320px); + height: 320px; + } + > div > div { + margin-left: 253px; + } + > div { + > div:nth-child(2) { + left: -253px; + } + > div:nth-child(3) { + right: -520px; + } + } +} + +.couponRemain { + font-size: 24px; + font-weight: bold; + color: #808080; + text-align: center; + + margin-top: 20px; +} + +.couponContainer { + position: relative; + + &:focus { + &::after { + .focused(@boxShadow: 22px, @borderRadius: 12px); + } + } + + .couponItem { + .flex(@direction: column, @justifyCenter: space-between); + .size(@w: 440px , @h: 320px); + text-align: center; + background: @COLOR_WHITE; + + padding: 30px; + border-radius: 10px; + .border-solid(@size:1px,@color:@COLOR_GRAY02); + + .couponTopContents { + .couponLate { + font-weight: bold; + font-size: 32px; + color: @PRIMARY_COLOR_RED; + line-height: 1.1; + height: 40px; + } + + .title { + font-weight: bold; + font-size: 32px; + line-height: 1.1; + color: @COLOR_GRAY07; + width: 400px; + height: 40px; + .elip(1); + } + } + + .couponMiddleContents { + .flex(@direction: column, @justifyCenter: space-between); + font-weight: normal; + font-size: 22px; + color: @COLOR_GRAY03; + + > span { + min-height: 30px; + + &:nth-child(1) { + margin-bottom: 10px; + } + } + } + + .couponBottomButton { + .flex(); + .size(@w: 380px , @h: 60px); + border-radius: 6px; + background-color: #808080; + color: #fff; + z-index: 1; + + font-weight: bold; + font-size: 24px; + + &.disable { + background-color: #808080; + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.5); + } + + &.focused { + background-color: @PRIMARY_COLOR_RED; + } + } + } +} \ No newline at end of file