[상품 상세] 쿠폰 노출관련 처리건

- 쿠폰 노출 처리
 - 다운로드 할때 중복처리부분 처리
 - 기존 다운로드 로직에서 조건 좀 더 추가.
 - spotlight 추가 및 변경.
This commit is contained in:
junghoon86.park
2025-11-18 17:17:10 +09:00
parent e95e4b828f
commit 61c61eae9b
3 changed files with 535 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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 classNames from 'classnames';
// import { throttle } from 'lodash'; // import { throttle } from 'lodash';
import { PropTypes } from 'prop-types'; import { PropTypes } from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx // src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import couponImg from '../../../../assets/images/icons/coupon.png';
// import Spottable from '@enact/spotlight/Spottable'; // import Spottable from '@enact/spotlight/Spottable';
//image //image
import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png'; import arrowDown
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png'; 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 { pushPanel } from '../../../actions/panelActions';
import { minimizeModalMedia, restoreModalMedia } from '../../../actions/mediaActions'; import {
minimizeModalMedia,
restoreModalMedia,
} from '../../../actions/mediaActions';
import { pauseFullscreenVideo } from '../../../actions/playActions'; import { pauseFullscreenVideo } from '../../../actions/playActions';
import { resetShowAllReviews } from '../../../actions/productActions'; 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 // ProductInfoSection imports
import TButton from '../../../components/TButton/TButton'; 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 useReviews from '../../../hooks/useReviews/useReviews';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
import { panel_names } from '../../../utils/Config'; import { panel_names } from '../../../utils/Config';
import * as Config from '../../../utils/Config.js';
import { import {
andThen, andThen,
curry, curry,
@@ -51,13 +86,19 @@ import StarRating from '../components/StarRating';
// ProductContentSection imports // ProductContentSection imports
import TScrollerDetail from '../components/TScroller/TScrollerDetail'; import TScrollerDetail from '../components/TScroller/TScrollerDetail';
import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton'; import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton';
import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription'; import ProductDescription
import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new'; from '../ProductContentSection/ProductDescription/ProductDescription';
import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx'; import ProductDetail
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo.v3'; 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 UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; // 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 QRCode from '../ProductInfoSection/QRCode/QRCode';
import ProductOverview from '../ProductOverview/ProductOverview'; import ProductOverview from '../ProductOverview/ProductOverview';
// CSS imports // CSS imports
@@ -98,6 +139,8 @@ const HorizontalContainer = SpotlightContainerDecorator(
'div' 'div'
); );
const SpottableComponent = Spottable("div");
const getProductData = curry((productType, themeProductInfo, productInfo) => const getProductData = curry((productType, themeProductInfo, productInfo) =>
pipe( pipe(
when( when(
@@ -148,7 +191,16 @@ export default function ProductAllSection({
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독 // YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
const youmaylikeData = useSelector((state) => state.main.youmaylikeData); 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: 비디오 생략) // ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(1); const [productVideoVersion, setProductVideoVersion] = useState(1);
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화) // 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
@@ -177,6 +229,215 @@ export default function ProductAllSection({
// 스크롤 위치에 따른 MediaPanel 제어 상태 // 스크롤 위치에 따른 MediaPanel 제어 상태
const [shouldMinimizeMedia, setShouldMinimizeMedia] = useState(false); 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 (
<SpottableComponent
className={css.couponContainer}
spotlightid={`spotlightId-coupon-${index}`}
{...rest}
onFocus={() => onFocus(index)}
onBlur={onBlur}
onClick={handleDownloadClick}
>
<div
className={css.couponItem}
aria-label={
"Purchase over " +
cpnAplyMinPurcAmt +
"up to " +
cpnAplyMaxDcAmt +
" off"
}
>
<div className={css.couponTopContents}>
{shptmDcTpCd === "CPN00401" && (
<span className={css.couponLate}>{`${currSign}${dcAmt}`}</span>
)}
{shptmDcTpCd === "CPN00402" && (
<span className={css.couponLate}>{`${cpnDctrt}%`}</span>
)}
<span className={css.title} aria-label={cpnTtl}>
{cpnTtl}
</span>
</div>
<div className={css.couponMiddleContents}>
<span>
{$L(
"Purchase over ${cpnAplyMinPurcAmt} (up to ${cpnAplyMaxDcAmt} off)"
)
.replace("{cpnAplyMinPurcAmt}", cpnAplyMinPurcAmt)
.replace("{cpnAplyMaxDcAmt}", cpnAplyMaxDcAmt)}
</span>
<span className={classNames(css.content, css.date)}>
{couponAplyStartDate}~{couponAplyEndDate}
</span>
</div>
<div
className={classNames(
css.couponBottomButton,
downloadCouponArr.length > 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")}
</div>
</div>
</SpottableComponent>
);
},
[selectedCoupon, downloadCouponArr, focused, dispatch]
);
useEffect(() => { useEffect(() => {
const toggleQRCode = () => { const toggleQRCode = () => {
@@ -631,6 +892,16 @@ export default function ProductAllSection({
// BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로 // BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로
const handleSpotlightUpFromBuyButtons = useCallback((e) => { 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(); e.stopPropagation();
Spotlight.focus('spotlightId_backBtn'); Spotlight.focus('spotlightId_backBtn');
}, []); }, []);
@@ -928,6 +1199,8 @@ export default function ProductAllSection({
}; };
}, []); }, []);
// 초기 로딩 중에는 Skeleton 표시 // 초기 로딩 중에는 Skeleton 표시
if (isInitialLoading) { if (isInitialLoading) {
return ( return (
@@ -936,6 +1209,9 @@ export default function ProductAllSection({
</div> </div>
); );
} }
return ( return (
<HorizontalContainer className={css.detailArea} onClick={handleCloseToast}> <HorizontalContainer className={css.detailArea} onClick={handleCloseToast}>
@@ -983,6 +1259,30 @@ export default function ProductAllSection({
</div> </div>
</ProductOverview> </ProductOverview>
{promotions.map((promotion, idx) => {
return(
<HorizontalContainer className={css.couponContainer} key={idx}>
<div className={css.couponTitleText}>
<div className={css.firstTitle}>SPECIAL PROMOTION</div>
<div className={css.secondTitle}>Coupon only applicable to this product!</div>
</div>
<TButton
spotlightId="detail-coupon-button"
className={css.couponButton}
onClick={() => {
handleCouponClick(idx, promotion);
}}
onSpotlightUp={handleSpotlightUpFromCouponButtons}
size="detail_very_small"
>
<div className={css.couponText}>
COUPON
</div>
<img className={css.buttonImg} src={couponImg} />
</TButton>
</HorizontalContainer>
)
})}
{isBillingProductVisible && ( {isBillingProductVisible && (
<HorizontalContainer className={css.buyNowCartContainer}> <HorizontalContainer className={css.buyNowCartContainer}>
<TButton <TButton
@@ -1277,6 +1577,38 @@ export default function ProductAllSection({
</p> </p>
)} )}
</div> </div>
{/* COUPON POPUP */}
{activePopup === Config.ACTIVE_POPUP.couponPopup && (
<TPopUp
kind="couponPopup"
hasText
title={couponTypes === 0 ? promotions[0] : promotions[1]}
open={popupVisible}
onClick={handleCouponTotDownload}
onClose={onClose}
hasButton
button1Text={$L("DOWNLOAD ALL")}
button2Text={$L("CLOSE")}
>
<Container className={css.itemWrap}>
{selectedCoupon && selectedCoupon.length > 0 && (
<TVirtualGridList
dataSize={selectedCoupon.length}
direction="horizontal"
autoScroll
renderItem={renderItem}
itemWidth={440}
itemHeight={320}
spacing={15}
className={css.itemList}
/>
)}
</Container>
<div
className={css.couponRemain}
>{`1/${selectedCoupon?.length}`}</div>
</TPopUp>
)}
</HorizontalContainer> </HorizontalContainer>
); );
} }

View File

@@ -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 버튼 스타일 */ /* BUY NOW + ADD TO CART 버튼 스타일 */
.buyNowCartContainer { .buyNowCartContainer {
width: 100%; width: 100%;
padding-top: 19px; padding-top: 10px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -637,3 +701,128 @@
// PlayerPanel 모달이 이 영역에서만 재생되도록 설정 // PlayerPanel 모달이 이 영역에서만 재생되도록 설정
z-index: 1; 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;
}
}
}
}