[251019] fix: DetailPanel,ProductAllSection 개선
🕐 커밋 시간: 2025. 10. 19. 13:04:56 📊 변경 통계: • 총 파일: 7개 • 추가: +50줄 • 삭제: -9줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/toastActions.js ~ com.twin.app.shoptime/src/components/TToast/TToastEnhanced.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.module.less ~ com.twin.app.shoptime/src/views/DetailPanel/components/CustomDropDown.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/ProductQuantity.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/actions/toastActions.js (javascript): 🔄 Modified: curry() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: extractProductMeta() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/CustomDropDown.jsx (javascript): ✅ Added: Spottable() 📄 com.twin.app.shoptime/src/views/DetailPanel/components/ProductQuantity.jsx (javascript): ✅ Added: ProductQuantity() ❌ Deleted: ProductQuantity() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선
This commit is contained in:
@@ -4,39 +4,43 @@ import { curry } from '../utils/fp';
|
||||
export const TOAST_ACTIONS = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
CLEAR_ALL_TOASTS: 'CLEAR_ALL_TOASTS'
|
||||
CLEAR_ALL_TOASTS: 'CLEAR_ALL_TOASTS',
|
||||
};
|
||||
|
||||
// 기본 Toast 추가 액션
|
||||
const createId = () => Date.now() + Math.random();
|
||||
|
||||
export const showToast = curry(({
|
||||
message,
|
||||
type = 'success',
|
||||
duration = 4000,
|
||||
position = 'bottom-center',
|
||||
id,
|
||||
}) => ({
|
||||
type: TOAST_ACTIONS.ADD_TOAST,
|
||||
payload: {
|
||||
id: id || createId(), // 고유 ID 생성 (옵션으로 주입 가능)
|
||||
text: message,
|
||||
type,
|
||||
duration,
|
||||
position,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}));
|
||||
export const showToast = curry(
|
||||
({
|
||||
message,
|
||||
type = 'success',
|
||||
duration = 4000,
|
||||
position = 'bottom-center',
|
||||
id,
|
||||
onToastClose, // Toast가 닫힐 때 호출할 콜백
|
||||
}) => ({
|
||||
type: TOAST_ACTIONS.ADD_TOAST,
|
||||
payload: {
|
||||
id: id || createId(), // 고유 ID 생성 (옵션으로 주입 가능)
|
||||
text: message,
|
||||
type,
|
||||
duration,
|
||||
position,
|
||||
onToastClose, // 콜백 전달
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Toast 제거 액션
|
||||
export const removeToast = curry((id) => ({
|
||||
type: TOAST_ACTIONS.REMOVE_TOAST,
|
||||
payload: id
|
||||
payload: id,
|
||||
}));
|
||||
|
||||
// 모든 Toast 제거 액션
|
||||
export const clearAllToasts = () => ({
|
||||
type: TOAST_ACTIONS.CLEAR_ALL_TOASTS
|
||||
type: TOAST_ACTIONS.CLEAR_ALL_TOASTS,
|
||||
});
|
||||
|
||||
// 편의 함수들 - 타입별 Toast 생성
|
||||
@@ -83,4 +87,4 @@ export const showLoadingToast = curry((message = '로딩 중...') =>
|
||||
duration: 0, // 수동으로 닫을 때까지 유지
|
||||
position: 'bottom-center',
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useDispatch } from "react-redux";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import { changeAppStatus } from "../../actions/commonActions";
|
||||
import BuyOption from "../../views/DetailPanel/components/BuyOption";
|
||||
import css from "./TToastEnhanced.module.less";
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { changeAppStatus } from '../../actions/commonActions';
|
||||
import BuyOption from '../../views/DetailPanel/components/BuyOption';
|
||||
import css from './TToastEnhanced.module.less';
|
||||
|
||||
const SpottableToast = Spottable("div");
|
||||
const SpottableToast = Spottable('div');
|
||||
|
||||
// 타입별 아이콘 정의
|
||||
const toastIcons = {
|
||||
success: "✓",
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
info: "ℹ"
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ',
|
||||
};
|
||||
|
||||
export default function TToastEnhanced({
|
||||
text,
|
||||
type = "success",
|
||||
type = 'success',
|
||||
duration = 4000,
|
||||
position = "bottom-center",
|
||||
position = 'bottom-center',
|
||||
id,
|
||||
onClose,
|
||||
onClose, // ToastContainer에서 전달하는 toast 제거 콜백
|
||||
onToastClose, // payload의 onClose - Toast 닫힐 때 호출할 추가 콜백
|
||||
...rest
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
@@ -63,7 +64,7 @@ export default function TToastEnhanced({
|
||||
if (type === 'buyOption') {
|
||||
// BuyOption이 포커스를 받았는지 추적하는 플래그
|
||||
let hasBuyOptionReceivedFocus = false;
|
||||
|
||||
|
||||
const handleFocusChange = (e) => {
|
||||
// 1. BuyOption 내부로 포커스가 들어온 경우 - 플래그를 true로 설정
|
||||
if (buyOptionRef.current && buyOptionRef.current.contains(e.target)) {
|
||||
@@ -73,19 +74,23 @@ export default function TToastEnhanced({
|
||||
}
|
||||
return; // 내부에 포커스가 있으면 아무것도 하지 않음
|
||||
}
|
||||
|
||||
|
||||
// 2. BuyOption이 포커스를 받은 적이 있고, 현재 외부로 포커스가 이동한 경우 - Toast 닫기
|
||||
if (hasBuyOptionReceivedFocus &&
|
||||
buyOptionRef.current &&
|
||||
!buyOptionRef.current.contains(e.target)) {
|
||||
console.log('[TToastEnhanced] Focus left BuyOption after receiving focus - closing toast');
|
||||
if (
|
||||
hasBuyOptionReceivedFocus &&
|
||||
buyOptionRef.current &&
|
||||
!buyOptionRef.current.contains(e.target)
|
||||
) {
|
||||
console.log(
|
||||
'[TToastEnhanced] Focus left BuyOption after receiving focus - closing toast'
|
||||
);
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
// focusin 이벤트로 포커스 변경 감지
|
||||
document.addEventListener('focusin', handleFocusChange);
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('focusin', handleFocusChange);
|
||||
};
|
||||
@@ -109,6 +114,11 @@ export default function TToastEnhanced({
|
||||
setIsExiting(true);
|
||||
clearTimer();
|
||||
|
||||
// Toast 닫기 전에 onToastClose 콜백 호출 (포커스 복구 등)
|
||||
if (onToastClose) {
|
||||
onToastClose();
|
||||
}
|
||||
|
||||
// 애니메이션 완료 후 제거
|
||||
setTimeout(() => {
|
||||
if (onClose) {
|
||||
@@ -123,23 +133,26 @@ export default function TToastEnhanced({
|
||||
const handleMouseEnter = () => {
|
||||
clearTimer();
|
||||
if (progressRef.current) {
|
||||
progressRef.current.style.animationPlayState = "paused";
|
||||
progressRef.current.style.animationPlayState = 'paused';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isExiting) {
|
||||
const remainingTime = duration > 0 ?
|
||||
(progressRef.current?.offsetWidth / progressRef.current?.parentElement?.offsetWidth) * duration :
|
||||
0;
|
||||
const remainingTime =
|
||||
duration > 0
|
||||
? (progressRef.current?.offsetWidth / progressRef.current?.parentElement?.offsetWidth) *
|
||||
duration
|
||||
: 0;
|
||||
|
||||
if (remainingTime > 100) { // 최소 100ms 남아있을 때만 재시작
|
||||
if (remainingTime > 100) {
|
||||
// 최소 100ms 남아있을 때만 재시작
|
||||
clearTimer();
|
||||
timerRef.current = setTimeout(handleClose, remainingTime);
|
||||
}
|
||||
|
||||
if (progressRef.current) {
|
||||
progressRef.current.style.animationPlayState = "running";
|
||||
progressRef.current.style.animationPlayState = 'running';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -169,4 +182,4 @@ export default function TToastEnhanced({
|
||||
)}
|
||||
</SpottableToast>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function ProductAllSection({
|
||||
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
|
||||
|
||||
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
|
||||
const [productVideoVersion, setProductVideoVersion] = useState(3);
|
||||
const [productVideoVersion, setProductVideoVersion] = useState(2);
|
||||
|
||||
// const [currentHeight, setCurrentHeight] = useState(0);
|
||||
//하단부분까지 갔을때 체크용
|
||||
@@ -264,6 +264,12 @@ export default function ProductAllSection({
|
||||
type: 'buyOption',
|
||||
duration: 0,
|
||||
position: 'bottom-center',
|
||||
// BuyOption Toast가 닫힐 때 BUY NOW 버튼으로 포커스 복구
|
||||
onToastClose: () => {
|
||||
setTimeout(() => {
|
||||
Spotlight.focus('detail-buy-now-button');
|
||||
}, 100);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
@@ -416,7 +422,22 @@ export default function ProductAllSection({
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSpotlightUpToBackButton = useCallback((e) => {
|
||||
// SHOP BY MOBILE 버튼에서 arrow up 시: BUY NOW로 가거나 헤더로 간다
|
||||
const handleSpotlightUpToBackButton = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
// BUY NOW 버튼이 보이면 그쪽으로 포커스 이동, 아니면 헤더의 뒤로가기 버튼으로
|
||||
if (isBillingProductVisible) {
|
||||
Spotlight.focus('detail-buy-now-button');
|
||||
} else {
|
||||
Spotlight.focus('spotlightId_backBtn');
|
||||
}
|
||||
},
|
||||
[isBillingProductVisible]
|
||||
);
|
||||
|
||||
// BUY NOW, ADD TO CART 버튼에서 arrow up 시: 항상 헤더 뒤로가기 버튼으로
|
||||
const handleSpotlightUpFromBuyButtons = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
Spotlight.focus('spotlightId_backBtn');
|
||||
}, []);
|
||||
@@ -582,7 +603,7 @@ export default function ProductAllSection({
|
||||
spotlightId="detail-buy-now-button"
|
||||
className={css.buyNowButton}
|
||||
onClick={handleBuyNowClick}
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
onSpotlightUp={handleSpotlightUpFromBuyButtons}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.buyNowText}>{$L('BUY NOW')}</div>
|
||||
@@ -591,7 +612,7 @@ export default function ProductAllSection({
|
||||
spotlightId="detail-add-to-cart-button"
|
||||
className={css.addToCartButton}
|
||||
onClick={handleAddToCartClick}
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
onSpotlightUp={handleSpotlightUpFromBuyButtons}
|
||||
type="detail_small"
|
||||
>
|
||||
<div className={css.addToCartText}>{$L('ADD TO CART')}</div>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
|
||||
import { addToCart } from '../../../actions/cartActions';
|
||||
import { getMyInfoCheckoutInfo } from '../../../actions/checkoutActions';
|
||||
@@ -23,15 +14,9 @@ import {
|
||||
showError,
|
||||
} from '../../../actions/commonActions';
|
||||
import { getProductCouponSearch } from '../../../actions/couponActions';
|
||||
import {
|
||||
sendLogPaymentEntry,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../../actions/logActions';
|
||||
import { sendLogPaymentEntry, sendLogTotalRecommend } from '../../../actions/logActions';
|
||||
import { pushPanel } from '../../../actions/panelActions';
|
||||
import {
|
||||
getProductOption,
|
||||
getProductOptionId,
|
||||
} from '../../../actions/productActions';
|
||||
import { getProductOption, getProductOptionId } from '../../../actions/productActions';
|
||||
import { clearAllToasts } from '../../../actions/toastActions';
|
||||
import TButton from '../../../components/TButton/TButton';
|
||||
import TPopUp from '../../../components/TPopUp/TPopUp';
|
||||
@@ -44,8 +29,11 @@ import CustomDropDown from './CustomDropDown';
|
||||
import ProductQuantity from './ProductQuantity';
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ spotlightRestrict: "self-only" },
|
||||
"div"
|
||||
{
|
||||
spotlightRestrict: 'self-only',
|
||||
leaveFor: {}, // arrow key로 빠져나가지 않도록 설정
|
||||
},
|
||||
'div'
|
||||
);
|
||||
|
||||
const BuyOption = ({
|
||||
@@ -61,23 +49,15 @@ const BuyOption = ({
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Redux 상태 (props가 있으면 props 우선, 없으면 Redux에서)
|
||||
const { userId, userNumber } = useSelector(
|
||||
(state) => state.common.appStatus.loginUserData
|
||||
);
|
||||
const { userId, userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
|
||||
const reduxProductInfo = useSelector((state) => state.main.productData);
|
||||
const productInfo = propsProductInfo || reduxProductInfo;
|
||||
const productOptionInfos = useSelector((state) => state.product.prdtOptInfo);
|
||||
const productData = useSelector((state) => state.main.productData);
|
||||
const { partnerCoupon } = useSelector(
|
||||
(state) => state.coupon.productCouponSearchData || {}
|
||||
);
|
||||
const { popupVisible, activePopup } = useSelector(
|
||||
(state) => state.common.popup
|
||||
);
|
||||
const { partnerCoupon } = useSelector((state) => state.coupon.productCouponSearchData || {});
|
||||
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
const webOSVersion = useSelector(
|
||||
(state) => state.common.appStatus.webOSVersion
|
||||
);
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
|
||||
// 옵션 선택 상태 관리 (SingleOption과 동일한 구조)
|
||||
const [selectedBtnOptIdx, setSelectedBtnOptIdx] = useState(0);
|
||||
@@ -104,27 +84,26 @@ const BuyOption = ({
|
||||
couponInfo = partnerCoupon[0];
|
||||
}
|
||||
|
||||
const { catCd, catNm, patncNm, patnrId, prdtId, prdtNm, priceInfo } =
|
||||
productData;
|
||||
const { catCd, catNm, patncNm, patnrId, prdtId, prdtNm, priceInfo } = productData;
|
||||
const { cpnSno, cpnTtl } = couponInfo || {};
|
||||
const prodOptSno =
|
||||
(productOptionInfos &&
|
||||
productOptionInfos.length > 0 &&
|
||||
productOptionInfos[0]?.prodOptSno) ||
|
||||
"";
|
||||
'';
|
||||
const prodOptTpCdCval =
|
||||
(productOptionInfos &&
|
||||
productOptionInfos.length > 0 &&
|
||||
productOptionInfos[0]?.prodOptTpCdCval) ||
|
||||
"";
|
||||
'';
|
||||
|
||||
return {
|
||||
cpnSno: String(cpnSno) || "",
|
||||
cpnTtl: cpnTtl || "",
|
||||
dcAftrPrc: priceInfo.split("|")[1],
|
||||
dcBefPrc: priceInfo.split("|")[0],
|
||||
lgCatCd: catCd || "",
|
||||
lgCatNm: catNm || "",
|
||||
cpnSno: String(cpnSno) || '',
|
||||
cpnTtl: cpnTtl || '',
|
||||
dcAftrPrc: priceInfo.split('|')[1],
|
||||
dcBefPrc: priceInfo.split('|')[0],
|
||||
lgCatCd: catCd || '',
|
||||
lgCatNm: catNm || '',
|
||||
patncNm,
|
||||
patnrId,
|
||||
prodId: prdtId,
|
||||
@@ -141,27 +120,26 @@ const BuyOption = ({
|
||||
couponInfo = partnerCoupon[0];
|
||||
}
|
||||
|
||||
const { catCd, catNm, patncNm, patnrId, prdtId, prdtNm, priceInfo } =
|
||||
productInfo;
|
||||
const { catCd, catNm, patncNm, patnrId, prdtId, prdtNm, priceInfo } = productInfo;
|
||||
const { cpnSno, cpnTtl } = couponInfo || {};
|
||||
const prodOptSno =
|
||||
(productOptionInfos &&
|
||||
productOptionInfos.length > 0 &&
|
||||
productOptionInfos[0]?.prodOptSno) ||
|
||||
"";
|
||||
'';
|
||||
const prodOptTpCdCval =
|
||||
(productOptionInfos &&
|
||||
productOptionInfos.length > 0 &&
|
||||
productOptionInfos[0]?.prodOptTpCdCval) ||
|
||||
"";
|
||||
'';
|
||||
|
||||
return {
|
||||
cpnSno: String(cpnSno) || "",
|
||||
cpnTtl: cpnTtl || "",
|
||||
dcAftrPrc: priceInfo.split("|")[1],
|
||||
dcBefPrc: priceInfo.split("|")[0],
|
||||
lgCatCd: catCd || "",
|
||||
lgCatNm: catNm || "",
|
||||
cpnSno: String(cpnSno) || '',
|
||||
cpnTtl: cpnTtl || '',
|
||||
dcAftrPrc: priceInfo.split('|')[1],
|
||||
dcBefPrc: priceInfo.split('|')[0],
|
||||
lgCatCd: catCd || '',
|
||||
lgCatNm: catNm || '',
|
||||
patncNm,
|
||||
patnrId,
|
||||
prodId: prdtId,
|
||||
@@ -177,7 +155,7 @@ const BuyOption = ({
|
||||
|
||||
// 옵션 리셋 로직 (SingleOption과 동일)
|
||||
useEffect(() => {
|
||||
if (type !== "theme") {
|
||||
if (type !== 'theme') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,20 +202,20 @@ const BuyOption = ({
|
||||
useEffect(() => {
|
||||
if (!isSpotlight) {
|
||||
// isSpotlight이 false면 일반적인 BuyOption 포커스
|
||||
console.log("[BuyOption] Component mounted - focusing BUY NOW button");
|
||||
console.log('[BuyOption] Component mounted - focusing BUY NOW button');
|
||||
setTimeout(() => {
|
||||
Spotlight.focus("buy-option-buy-now-button");
|
||||
Spotlight.focus('buy-option-buy-now-button');
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// isSpotlight이 true이고 SingleOption 동작이 필요한 경우
|
||||
if (productInfo?.optProdYn === "N") {
|
||||
Spotlight.focus("buy-option-buy-now-button");
|
||||
if (productInfo?.optProdYn === 'N') {
|
||||
Spotlight.focus('buy-option-buy-now-button');
|
||||
} else if (productOptionInfos && productOptionInfos.length > 0) {
|
||||
Spotlight.focus("buy-option-first-dropdown");
|
||||
Spotlight.focus('buy-option-first-dropdown');
|
||||
if (productOptionInfos.length === 1) {
|
||||
Spotlight.focus("buy-option-second-dropdown");
|
||||
Spotlight.focus('buy-option-second-dropdown');
|
||||
}
|
||||
}
|
||||
}, [productOptionInfos, productInfo, isSpotlight]);
|
||||
@@ -285,19 +263,19 @@ const BuyOption = ({
|
||||
}
|
||||
}
|
||||
|
||||
Spotlight.focus("buy-option-buy-now-button");
|
||||
Spotlight.focus('buy-option-buy-now-button');
|
||||
}
|
||||
|
||||
const handleBuyNowClick = useCallback(() => {
|
||||
console.log("[BuyOption] BUY NOW clicked");
|
||||
console.log('[BuyOption] BUY NOW clicked');
|
||||
|
||||
// const optionName = renderOptionName();
|
||||
// const optionValueName = renderOptionValue();
|
||||
|
||||
if (productInfo && productInfo?.soldoutFlag === "Y") {
|
||||
if (productInfo && productInfo?.soldoutFlag === 'Y') {
|
||||
return;
|
||||
}
|
||||
if (!userNumber || userNumber === "") {
|
||||
if (!userNumber || userNumber === '') {
|
||||
return dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup));
|
||||
}
|
||||
|
||||
@@ -314,9 +292,9 @@ const BuyOption = ({
|
||||
if (userNumber && selectedPatnrId && selectedPrdtId && quantity) {
|
||||
const { prodOptCval, priceInfo } = selectedOptions || {};
|
||||
const { patncNm, brndNm, catNm, prdtNm, prdtId } = productInfo;
|
||||
const regularPrice = priceInfo?.split("|")[0];
|
||||
const discountPrice = priceInfo?.split("|")[1];
|
||||
const discountRate = priceInfo?.split("|")[4];
|
||||
const regularPrice = priceInfo?.split('|')[0];
|
||||
const discountPrice = priceInfo?.split('|')[1];
|
||||
const discountRate = priceInfo?.split('|')[4];
|
||||
|
||||
dispatch(
|
||||
sendLogTotalRecommend({
|
||||
@@ -327,7 +305,7 @@ const BuyOption = ({
|
||||
price: discountRate ? discountPrice : regularPrice,
|
||||
discount: discountRate,
|
||||
brand: brndNm,
|
||||
productOption: prodOptCval || "",
|
||||
productOption: prodOptCval || '',
|
||||
category: catNm,
|
||||
contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE,
|
||||
messageId: Config.LOG_MESSAGE_ID.BUY_NOW,
|
||||
@@ -338,7 +316,7 @@ const BuyOption = ({
|
||||
getMyInfoCheckoutInfo(
|
||||
{
|
||||
mbrNo: userNumber,
|
||||
dirPurcSelYn: "Y",
|
||||
dirPurcSelYn: 'Y',
|
||||
cartList: [
|
||||
{
|
||||
patnrId: selectedPatnrId,
|
||||
@@ -368,15 +346,15 @@ const BuyOption = ({
|
||||
|
||||
// ADD TO CART 버튼 클릭 핸들러
|
||||
const handleAddToCartClick = useCallback(() => {
|
||||
console.log("[BuyOption] ADD TO CART clicked");
|
||||
console.log('[BuyOption] ADD TO CART clicked');
|
||||
|
||||
// 상품 품절 체크
|
||||
if (productInfo && productInfo?.soldoutFlag === "Y") {
|
||||
if (productInfo && productInfo?.soldoutFlag === 'Y') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 로그인 체크
|
||||
if (!userNumber || userNumber === "") {
|
||||
if (!userNumber || userNumber === '') {
|
||||
return dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup));
|
||||
}
|
||||
|
||||
@@ -384,9 +362,9 @@ const BuyOption = ({
|
||||
if (userNumber && selectedPatnrId && selectedPrdtId && quantity) {
|
||||
const { prodOptCval, priceInfo } = selectedOptions || {};
|
||||
const { patncNm, brndNm, catNm, prdtNm, prdtId } = productInfo;
|
||||
const regularPrice = priceInfo?.split("|")[0];
|
||||
const discountPrice = priceInfo?.split("|")[1];
|
||||
const discountRate = priceInfo?.split("|")[4];
|
||||
const regularPrice = priceInfo?.split('|')[0];
|
||||
const discountPrice = priceInfo?.split('|')[1];
|
||||
const discountRate = priceInfo?.split('|')[4];
|
||||
|
||||
// 로그 전송
|
||||
dispatch(
|
||||
@@ -398,7 +376,7 @@ const BuyOption = ({
|
||||
price: discountRate ? discountPrice : regularPrice,
|
||||
discount: discountRate,
|
||||
brand: brndNm,
|
||||
productOption: prodOptCval || "",
|
||||
productOption: prodOptCval || '',
|
||||
category: catNm,
|
||||
contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE,
|
||||
messageId: Config.LOG_MESSAGE_ID.ADD_TO_CART,
|
||||
@@ -441,7 +419,7 @@ const BuyOption = ({
|
||||
// 첫번째 옵션 선택 핸들러 (SingleOption과 동일)
|
||||
const handleFirstOptionSelect = (selected) => {
|
||||
const optionValIdx = selected.selected;
|
||||
console.log("[BuyOption] First option selected:", optionValIdx);
|
||||
console.log('[BuyOption] First option selected:', optionValIdx);
|
||||
|
||||
setSelectedBtnOptIdx(optionValIdx);
|
||||
setSelectedOptionItemIndex(0);
|
||||
@@ -453,15 +431,11 @@ const BuyOption = ({
|
||||
// 두번째 옵션 선택 핸들러 (SingleOption과 동일)
|
||||
const handleSecondOptionSelect = (selected) => {
|
||||
const index = selected.selected;
|
||||
console.log("[BuyOption] Second option selected:", index);
|
||||
console.log('[BuyOption] Second option selected:', index);
|
||||
setSelectedOptionItemIndex(index);
|
||||
setSelectedOptions(
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[index]
|
||||
);
|
||||
setSelectedOptions(productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[index]);
|
||||
dispatch(
|
||||
getProductOptionId(
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[index]?.prodOptCdCval
|
||||
)
|
||||
getProductOptionId(productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[index]?.prodOptCdCval)
|
||||
);
|
||||
setIsOptionValue(true);
|
||||
};
|
||||
@@ -469,7 +443,7 @@ const BuyOption = ({
|
||||
// 수량 선택 핸들러
|
||||
const handleQuantitySelect = (selected) => {
|
||||
const qty = selected.selected + 1;
|
||||
console.log("[BuyOption] Quantity selected:", qty);
|
||||
console.log('[BuyOption] Quantity selected:', qty);
|
||||
setQuantity(qty);
|
||||
};
|
||||
|
||||
@@ -506,7 +480,7 @@ const BuyOption = ({
|
||||
|
||||
// Favorite 플래그 업데이트 (SingleOption과 동일)
|
||||
useEffect(() => {
|
||||
setFavoriteFlag(productInfo?.favorYn ? productInfo?.favorYn : "N");
|
||||
setFavoriteFlag(productInfo?.favorYn ? productInfo?.favorYn : 'N');
|
||||
}, [productInfo]);
|
||||
|
||||
// Favorite 플래그 변경 콜백 (SingleOption과 동일)
|
||||
@@ -525,24 +499,30 @@ const BuyOption = ({
|
||||
// 로그인 팝업 텍스트 로직 (SingleOption과 동일)
|
||||
const loginPopupText = useMemo(() => {
|
||||
if (!userNumber) {
|
||||
return $L("Would you like to sign in?");
|
||||
return $L('Would you like to sign in?');
|
||||
}
|
||||
if (!hasOnClose) {
|
||||
return $L("Please select Option");
|
||||
return $L('Please select Option');
|
||||
}
|
||||
return $L("Would you like to sign in?");
|
||||
return $L('Would you like to sign in?');
|
||||
}, [hasOnClose, userNumber]);
|
||||
|
||||
// BuyOption 내에서 arrow up으로 빠져나가지 않도록 처리
|
||||
const handleArrowUpWithinBuyOption = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
// arrow up 이벤트를 막음 (컨테이너의 leaveFor: {}와 함께 작동)
|
||||
}, []);
|
||||
|
||||
// 팝업 닫기 핸들러 (SingleOption과 동일)
|
||||
const onClose = useCallback(
|
||||
(spotlightId) => {
|
||||
dispatch(setHidePopup());
|
||||
|
||||
let currentSpot;
|
||||
if (typeof spotlightId === "string") {
|
||||
if (typeof spotlightId === 'string') {
|
||||
currentSpot = spotlightId;
|
||||
} else {
|
||||
currentSpot = "buy-option-buy-now-button";
|
||||
currentSpot = 'buy-option-buy-now-button';
|
||||
}
|
||||
|
||||
if (currentSpot) {
|
||||
@@ -557,9 +537,9 @@ const BuyOption = ({
|
||||
// 로그인 팝업 열기 핸들러 (SingleOption과 동일)
|
||||
const handleLoginPopUpOpen = useCallback(() => {
|
||||
if (!userNumber) {
|
||||
if (webOSVersion >= "6.0") {
|
||||
if (webOSVersion >= '6.0') {
|
||||
setTimeout(() => {
|
||||
Spotlight.focus("buy-option-buy-now-button");
|
||||
Spotlight.focus('buy-option-buy-now-button');
|
||||
});
|
||||
|
||||
dispatch(setHidePopup());
|
||||
@@ -573,11 +553,11 @@ const BuyOption = ({
|
||||
if (hasOnClose) {
|
||||
dispatch(setHidePopup());
|
||||
|
||||
let spotlightId = "buy-option-first-dropdown";
|
||||
let spotlightId = 'buy-option-first-dropdown';
|
||||
|
||||
//옵션이 하나만 있는경우 isOptionValue === false
|
||||
if (!isOptionValue) {
|
||||
spotlightId = "buy-option-second-dropdown";
|
||||
spotlightId = 'buy-option-second-dropdown';
|
||||
}
|
||||
setTimeout(() => {
|
||||
Spotlight.focus(spotlightId);
|
||||
@@ -590,57 +570,52 @@ const BuyOption = ({
|
||||
<Container className={styles.buy_option}>
|
||||
<div className={styles.buy_option__left_section}>
|
||||
{/* 동적 옵션 렌더링 */}
|
||||
{productOptionInfos &&
|
||||
productOptionInfos.length > 0 &&
|
||||
productInfo?.optProdYn === "Y" && (
|
||||
<>
|
||||
{/* 첫번째 옵션 (여러 옵션이 있을 때만) */}
|
||||
{productOptionInfos.length > 1 && (
|
||||
{productOptionInfos && productOptionInfos.length > 0 && productInfo?.optProdYn === 'Y' && (
|
||||
<>
|
||||
{/* 첫번째 옵션 (여러 옵션이 있을 때만) */}
|
||||
{productOptionInfos.length > 1 && (
|
||||
<div className={styles.buy_option__option_row}>
|
||||
<div className={styles.buy_option__option_label}>
|
||||
<div className={styles.buy_option__label_text}>OPTION 1</div>
|
||||
</div>
|
||||
<div className={styles.buy_option__option_control}>
|
||||
<CustomDropDown
|
||||
options={productOptionInfos.map((option) => option.optNm)}
|
||||
selectedIndex={selectedBtnOptIdx}
|
||||
onSelect={handleFirstOptionSelect}
|
||||
spotlightId="buy-option-first-dropdown"
|
||||
onSpotlightUp={handleArrowUpWithinBuyOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 두번째 옵션 (옵션 상세값들) */}
|
||||
{productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl &&
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 && (
|
||||
<div className={styles.buy_option__option_row}>
|
||||
<div className={styles.buy_option__option_label}>
|
||||
<div className={styles.buy_option__label_text}>
|
||||
OPTION 1
|
||||
{productOptionInfos.length === 1 ? 'OPTION' : 'OPTION 2'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buy_option__option_control}>
|
||||
<CustomDropDown
|
||||
options={productOptionInfos.map((option) => option.optNm)}
|
||||
selectedIndex={selectedBtnOptIdx}
|
||||
onSelect={handleFirstOptionSelect}
|
||||
spotlightId="buy-option-first-dropdown"
|
||||
options={
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.map(
|
||||
(detail) => detail.prodOptCval
|
||||
) || []
|
||||
}
|
||||
selectedIndex={selectedOptionItemIndex}
|
||||
onSelect={handleSecondOptionSelect}
|
||||
spotlightId="buy-option-second-dropdown"
|
||||
onSpotlightUp={handleArrowUpWithinBuyOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 두번째 옵션 (옵션 상세값들) */}
|
||||
{productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl &&
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length >
|
||||
1 && (
|
||||
<div className={styles.buy_option__option_row}>
|
||||
<div className={styles.buy_option__option_label}>
|
||||
<div className={styles.buy_option__label_text}>
|
||||
{productOptionInfos.length === 1
|
||||
? "OPTION"
|
||||
: "OPTION 2"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buy_option__option_control}>
|
||||
<CustomDropDown
|
||||
options={
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.map(
|
||||
(detail) => detail.prodOptCval
|
||||
) || []
|
||||
}
|
||||
selectedIndex={selectedOptionItemIndex}
|
||||
onSelect={handleSecondOptionSelect}
|
||||
spotlightId="buy-option-second-dropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 수량 선택 */}
|
||||
<div className={styles.buy_option__option_row}>
|
||||
@@ -654,7 +629,7 @@ const BuyOption = ({
|
||||
onSelect={handleQuantitySelect}
|
||||
spotlightId="buy-option-quantity-dropdown"
|
||||
/> */}
|
||||
<ProductQuantity />
|
||||
<ProductQuantity onSpotlightUp={handleArrowUpWithinBuyOption} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -671,6 +646,7 @@ const BuyOption = ({
|
||||
spotlightId="buy-option-buy-now-button"
|
||||
size="detailButton"
|
||||
onClick={handleBuyNowClick}
|
||||
onSpotlightUp={handleArrowUpWithinBuyOption}
|
||||
>
|
||||
<span className={styles.buy_option__button_text}>BUY NOW</span>
|
||||
</TButton>
|
||||
@@ -679,6 +655,7 @@ const BuyOption = ({
|
||||
spotlightId="buy-option-add-to-cart-button"
|
||||
size="detailButton"
|
||||
onClick={handleAddToCartClick}
|
||||
onSpotlightUp={handleArrowUpWithinBuyOption}
|
||||
>
|
||||
<span className={styles.buy_option__button_text}>ADD TO CART</span>
|
||||
</TButton>
|
||||
@@ -694,8 +671,8 @@ const BuyOption = ({
|
||||
text={loginPopupText}
|
||||
hasButton
|
||||
hasOnClose={hasOnClose}
|
||||
button1Text={$L("OK")}
|
||||
button2Text={$L("CANCEL")}
|
||||
button1Text={$L('OK')}
|
||||
button2Text={$L('CANCEL')}
|
||||
onClick={handleLoginPopUpOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
// buy-option__button-text (Element)
|
||||
&__button_text {
|
||||
color: white;
|
||||
font-size: 25px;
|
||||
font-size: 22px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 600;
|
||||
line-height: 35px;
|
||||
|
||||
@@ -13,6 +13,7 @@ const CustomDropDown = ({
|
||||
onSelect,
|
||||
spotlightId,
|
||||
placeholder = 'Select option',
|
||||
onSpotlightUp,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -56,6 +57,7 @@ const CustomDropDown = ({
|
||||
}`}
|
||||
onClick={handleToggle}
|
||||
spotlightId={spotlightId}
|
||||
onSpotlightUp={onSpotlightUp}
|
||||
>
|
||||
<div className={styles.custom_dropdown__text}>{selectedOption}</div>
|
||||
<div className={styles.custom_dropdown__icon}>
|
||||
@@ -88,4 +90,4 @@ const CustomDropDown = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDropDown;
|
||||
export default CustomDropDown;
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import css from './ProductQuantity.module.less';
|
||||
|
||||
const Container = SpotlightContainerDecorator("div");
|
||||
const SpotDiv = Spottable("div");
|
||||
const Container = SpotlightContainerDecorator('div');
|
||||
const SpotDiv = Spottable('div');
|
||||
|
||||
const ProductQuantity = () => {
|
||||
const ProductQuantity = ({ onSpotlightUp }) => {
|
||||
const [ea, setEa] = useState(1);
|
||||
|
||||
const minusClick = useCallback(() => {
|
||||
@@ -25,7 +21,7 @@ const ProductQuantity = () => {
|
||||
}, [ea]);
|
||||
|
||||
return (
|
||||
<Container className={css.container}>
|
||||
<Container className={css.container} onSpotlightUp={onSpotlightUp}>
|
||||
<SpotDiv className={css.minusBtn} onClick={minusClick} />
|
||||
<div className={css.inputBox}>{ea}</div>
|
||||
<SpotDiv className={css.plusBtn} onClick={plusClick} />
|
||||
|
||||
Reference in New Issue
Block a user