[251101] fix: CheckOutPanel Mock-2
🕐 커밋 시간: 2025. 11. 01. 21:01:06 📊 변경 통계: • 총 파일: 10개 • 추가: +617줄 • 삭제: -142줄 📁 추가된 파일: + com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js ~ com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx ~ com.twin.app.shoptime/src/views/CheckOutPanel/container/OrderItemsSideBar.jsx ~ com.twin.app.shoptime/src/views/CheckOutPanel/container/SummaryContainerMock.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.module.less ~ com.twin.app.shoptime/src/views/ErrorBoundary.js 🔧 주요 변경 내용: • 공통 유틸리티 함수 최적화 • UI 컴포넌트 아키텍처 개선 • 오류 처리 로직 개선 • 대규모 기능 개발 • 모듈 구조 개선
This commit is contained in:
@@ -19,34 +19,35 @@ export const createMockProductOptionData = (originalProductData) => {
|
||||
}
|
||||
|
||||
// 원본 데이터에서 필요한 정보 추출
|
||||
const basePrice = originalProductData.prdtPrice || 0;
|
||||
const prdtId = originalProductData.prdtId;
|
||||
const prdtNm = originalProductData.prdtNm;
|
||||
const prdtId = originalProductData.prdtId || 'MOCK_PRODUCT';
|
||||
const prdtNm = originalProductData.prdtNm || 'Mock Product';
|
||||
|
||||
// Mock 옵션 데이터 생성
|
||||
return {
|
||||
// 옵션 ID (기본 값)
|
||||
prodOptSno: `MOCK_OPT_${prdtId}_1`,
|
||||
// 실제 상품 가격 정보 사용 (originalProductData의 priceInfo 또는 기본값)
|
||||
// priceInfo 포맷: "원가|할인가|할인액|할인율|할인율숫자"
|
||||
const priceInfo = originalProductData.priceInfo || '99999|99999|0|0%|0';
|
||||
|
||||
// 옵션 목록 (기본값 1개)
|
||||
optionList: [
|
||||
{
|
||||
optId: `MOCK_OPT_${prdtId}_1`,
|
||||
optNm: `${prdtNm} - 기본 옵션`,
|
||||
optPrice: basePrice,
|
||||
optDscntPrice: basePrice,
|
||||
stockCnt: 999,
|
||||
soldOutYn: 'N',
|
||||
optImgUrl: originalProductData.thumbnailUrl || null
|
||||
}
|
||||
],
|
||||
console.log('[BuyNowDataManipulator] createMockProductOptionData - priceInfo:', priceInfo);
|
||||
|
||||
// 옵션 타입 정보
|
||||
optionTypeInfo: {
|
||||
optTypeCd: 'BASIC',
|
||||
optTypeNm: '기본'
|
||||
}
|
||||
// 옵션 상세 객체
|
||||
const optionDetail = {
|
||||
prodOptCval: `${prdtNm}`,
|
||||
priceInfo: priceInfo,
|
||||
stockCnt: 999,
|
||||
soldOutYn: 'N',
|
||||
};
|
||||
|
||||
// Mock 옵션 데이터 생성 (BuyOption에서 기대하는 구조)
|
||||
// 배열 구조로 반환 - BuyOption에서 productOptionInfos[0]에 접근
|
||||
return [
|
||||
{
|
||||
// 옵션 ID (기본 값)
|
||||
prodOptSno: `MOCK_OPT_${prdtId}_1`,
|
||||
prodOptTpCdCval: 'BASIC',
|
||||
|
||||
// 옵션 상세 목록 (기본값 1개) - prdtOptDtl 배열 구조
|
||||
prdtOptDtl: [optionDetail],
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
326
com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js
Normal file
326
com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js
Normal file
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* Mock 모드에서 데이터가 없거나 불완전할 때 안전하게 처리하는 유틸리티
|
||||
* 목적: 표시만 하는 것이므로 데이터가 없으면 기본값으로 우회
|
||||
*/
|
||||
|
||||
/**
|
||||
* 상품 정보의 유효성 체크 - 표시할 데이터가 있는지 확인
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {boolean} 표시 가능 여부
|
||||
*/
|
||||
export const isProductDisplayable = (product) => {
|
||||
return product && (product.prdtNm || product.prdtId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 가격 정보를 안전하게 추출 (없으면 기본값 반환)
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {Object} { price, originalPrice, discount, currSign, currSignLoc }
|
||||
*/
|
||||
export const getSafeProductPrice = (product) => {
|
||||
if (!product) {
|
||||
return {
|
||||
price: 0,
|
||||
originalPrice: 0,
|
||||
discount: 0,
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
};
|
||||
}
|
||||
|
||||
// priceInfo가 있으면 파싱하여 사용 (BuyOption에서 전달된 원본 상품 데이터)
|
||||
if (product.priceInfo) {
|
||||
const priceParts = product.priceInfo.split('|');
|
||||
if (priceParts.length >= 5) {
|
||||
const originalPrice = parseFloat(priceParts[0]) || 0;
|
||||
const price = parseFloat(priceParts[1]) || originalPrice; // 할인가격
|
||||
const discountRate = parseFloat(priceParts[2]) || 0;
|
||||
const discountAmount = parseFloat(priceParts[3]) || 0;
|
||||
const currSign = priceParts[4] || '$';
|
||||
|
||||
return {
|
||||
price,
|
||||
originalPrice,
|
||||
discount: discountAmount > 0 ? discountAmount : Math.max(0, originalPrice - price),
|
||||
currSign,
|
||||
currSignLoc: 'left', // 기본값
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: finalPrice, discountPrice 등 기존 방식
|
||||
const price = parseFloat(product.finalPrice || product.discountPrice || 0);
|
||||
const originalPrice = parseFloat(product.origPrice || product.originalPrice || price);
|
||||
const discount = Math.max(0, originalPrice - price);
|
||||
|
||||
return {
|
||||
price,
|
||||
originalPrice,
|
||||
discount,
|
||||
currSign: product.currSign || '$',
|
||||
currSignLoc: product.currSignLoc || 'left',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 상품명을 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {string} 상품명 또는 기본값
|
||||
*/
|
||||
export const getSafeProductName = (product) => {
|
||||
return product?.prdtNm || product?.prdtId || 'Product';
|
||||
};
|
||||
|
||||
/**
|
||||
* 파트너명을 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {string} 파트너명 또는 기본값
|
||||
*/
|
||||
export const getSafePartnerName = (product) => {
|
||||
return product?.patncNm || product?.patnrId || 'Partner';
|
||||
};
|
||||
|
||||
/**
|
||||
* 수량을 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {number} 수량
|
||||
*/
|
||||
export const getSafeQuantity = (product) => {
|
||||
const qty = product?.prodQty;
|
||||
return typeof qty === 'number' && qty > 0 ? qty : 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* 배송료를 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {number} 배송료
|
||||
*/
|
||||
export const getSafeShippingCharge = (product) => {
|
||||
const shipping = product?.shippingCharge;
|
||||
return typeof shipping === 'number' ? shipping : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 필드가 'N'으로 설정되어 있는지 체크 (비활성화 항목)
|
||||
* @param {string} value - 값
|
||||
* @returns {boolean} 'N'인 경우 true
|
||||
*/
|
||||
export const isDisabled = (value) => {
|
||||
return value === 'N' || value === false || !value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 옵션 정보를 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {Array} 옵션 배열
|
||||
*/
|
||||
export const getSafeProductOptions = (product) => {
|
||||
const options = product?.prdtOpt;
|
||||
return Array.isArray(options) && options.length > 0
|
||||
? options
|
||||
: [{ prodOptCdCval: 'N/A', optNm: 'No Options' }];
|
||||
};
|
||||
|
||||
/**
|
||||
* 이미지 URL을 안전하게 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {string} 이미지 URL 또는 기본값
|
||||
*/
|
||||
export const getSafeImageUrl = (product) => {
|
||||
// 1순위: imgUrls 배열 (productInfo의 imgUrls)
|
||||
const imgUrls = product?.imgUrls;
|
||||
if (Array.isArray(imgUrls) && imgUrls.length > 0) {
|
||||
return imgUrls[0];
|
||||
}
|
||||
// 2순위: imgUrl 직접 필드
|
||||
if (product?.imgUrl) {
|
||||
return product.imgUrl;
|
||||
}
|
||||
// 3순위: thumbnailUrl (productInfo의 썸네일)
|
||||
if (product?.thumbnailUrl) {
|
||||
return product.thumbnailUrl;
|
||||
}
|
||||
// 4순위: thumbnailUrl960 (productInfo의 960px 썸네일)
|
||||
if (product?.thumbnailUrl960) {
|
||||
return product.thumbnailUrl960;
|
||||
}
|
||||
// 5순위: imgUrls 배열의 imgUrl 필드 (기존 방식)
|
||||
const imgUrlsWithImgUrl = product?.imgUrls;
|
||||
if (Array.isArray(imgUrlsWithImgUrl) && imgUrlsWithImgUrl.length > 0) {
|
||||
return imgUrlsWithImgUrl[0]?.imgUrl || '/mock/image.jpg';
|
||||
}
|
||||
// 6순위: patncLogPath (파트너 로고)
|
||||
if (product?.patncLogPath) {
|
||||
return product.patncLogPath;
|
||||
}
|
||||
// 7순위: 기본 이미지
|
||||
return '/mock/image.jpg';
|
||||
};
|
||||
|
||||
/**
|
||||
* 경매 상품 여부 안전 확인
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {boolean} 경매 상품 여부
|
||||
*/
|
||||
export const isAuctionProduct = (product) => {
|
||||
return product?.auctProdYn === 'Y';
|
||||
};
|
||||
|
||||
/**
|
||||
* 상품 데이터를 표시용으로 정규화 (모든 필드를 안전한 값으로 변환)
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {Object} 정규화된 상품 객체
|
||||
*/
|
||||
export const normalizeProductDataForDisplay = (product) => {
|
||||
if (!isProductDisplayable(product)) {
|
||||
// 표시 불가능하면 빈 제품으로 반환
|
||||
return {
|
||||
prdtId: 'N/A',
|
||||
prdtNm: 'Product',
|
||||
patnrId: 'N/A',
|
||||
patncNm: 'Partner',
|
||||
prodQty: 1,
|
||||
price: 0,
|
||||
originalPrice: 0,
|
||||
discount: 0,
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
shippingCharge: 0,
|
||||
imgUrl: '/mock/image.jpg',
|
||||
options: [],
|
||||
auctProdYn: 'N',
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const priceInfo = getSafeProductPrice(product);
|
||||
|
||||
return {
|
||||
prdtId: product.prdtId || 'N/A',
|
||||
prdtNm: getSafeProductName(product),
|
||||
patnrId: product.patnrId || 'N/A',
|
||||
patncNm: getSafePartnerName(product),
|
||||
prodQty: getSafeQuantity(product),
|
||||
price: priceInfo.price,
|
||||
originalPrice: priceInfo.originalPrice,
|
||||
discount: priceInfo.discount,
|
||||
currSign: priceInfo.currSign,
|
||||
currSignLoc: priceInfo.currSignLoc,
|
||||
shippingCharge: getSafeShippingCharge(product),
|
||||
imgUrl: getSafeImageUrl(product),
|
||||
options: getSafeProductOptions(product),
|
||||
auctProdYn: product.auctProdYn || 'N',
|
||||
isValid: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 배열의 첫 번째 상품을 안전하게 추출
|
||||
* @param {Array} productArray - 상품 배열
|
||||
* @returns {Object} 첫 번째 상품 또는 빈 객체
|
||||
*/
|
||||
export const getSafeFirstProduct = (productArray) => {
|
||||
if (Array.isArray(productArray) && productArray.length > 0) {
|
||||
return productArray[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 통화 기호 정보 추출
|
||||
* @param {Object} product - 상품 객체
|
||||
* @returns {Object} { currSign, currSignLoc }
|
||||
*/
|
||||
export const getSafeCurrencyInfo = (product) => {
|
||||
return {
|
||||
currSign: product?.currSign || '$',
|
||||
currSignLoc: product?.currSignLoc || 'left',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock Mode: productInfo에서 가격 정보를 추출하여 ORDER SUMMARY용 데이터 생성
|
||||
* @param {Object} productInfo - 원본 productInfo 객체
|
||||
* @returns {Object} ORDER SUMMARY용 가격 데이터
|
||||
*/
|
||||
export const calculateOrderSummaryFromProductInfo = (productInfo) => {
|
||||
console.log('[calculateOrderSummaryFromProductInfo] Input productInfo:', productInfo);
|
||||
|
||||
if (!productInfo) {
|
||||
console.log('[calculateOrderSummaryFromProductInfo] No productInfo, using defaults');
|
||||
return {
|
||||
items: 0,
|
||||
couponSavings: 0,
|
||||
shipping: 0,
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0,
|
||||
currency: { currSign: '$', currSignLoc: 'left' }
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Items (상품 가격) - price2 사용
|
||||
const itemsPrice = parseFloat(
|
||||
productInfo.price2?.replace(/[^\d.]/g, '') ||
|
||||
productInfo.finalPrice ||
|
||||
0
|
||||
);
|
||||
|
||||
// 2. Your Coupon Savings (쿠폰 할인) - price5를 할인액으로 사용
|
||||
const couponSavings = parseFloat(
|
||||
productInfo.price5?.replace(/[^\d.]/g, '') ||
|
||||
productInfo.discount ||
|
||||
0
|
||||
);
|
||||
|
||||
// 3. Shipping & Handling (배송비) - shippingCharge 사용
|
||||
const shippingCharge = parseFloat(
|
||||
productInfo.shippingCharge?.replace(/[^\d.]/g, '') ||
|
||||
0
|
||||
);
|
||||
|
||||
// 4. Subtotal (세전 총계) = itemsPrice - couponSavings + shippingCharge
|
||||
const subtotal = Math.max(0, itemsPrice - couponSavings + shippingCharge);
|
||||
|
||||
// 5. Tax (세금) = subtotal의 10%
|
||||
const tax = Math.round((subtotal * 0.1) * 100) / 100;
|
||||
|
||||
// 6. Total (총계) = subtotal + tax
|
||||
const total = Math.round((subtotal + tax) * 100) / 100;
|
||||
|
||||
// 통화 정보
|
||||
const currency = {
|
||||
currSign: productInfo.currSign || '$',
|
||||
currSignLoc: productInfo.currSignLoc || 'left'
|
||||
};
|
||||
|
||||
const result = {
|
||||
items: itemsPrice,
|
||||
couponSavings: couponSavings,
|
||||
shipping: shippingCharge,
|
||||
subtotal: subtotal,
|
||||
tax: tax,
|
||||
total: total,
|
||||
currency: currency
|
||||
};
|
||||
|
||||
console.log('[calculateOrderSummaryFromProductInfo] Calculated result:', result);
|
||||
return result;
|
||||
};
|
||||
|
||||
export default {
|
||||
isProductDisplayable,
|
||||
getSafeProductPrice,
|
||||
getSafeProductName,
|
||||
getSafePartnerName,
|
||||
getSafeQuantity,
|
||||
getSafeShippingCharge,
|
||||
isDisabled,
|
||||
getSafeProductOptions,
|
||||
getSafeImageUrl,
|
||||
isAuctionProduct,
|
||||
normalizeProductDataForDisplay,
|
||||
getSafeFirstProduct,
|
||||
getSafeCurrencyInfo,
|
||||
calculateOrderSummaryFromProductInfo,
|
||||
};
|
||||
@@ -30,6 +30,11 @@ import * as Config from '../../utils/Config';
|
||||
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
|
||||
import {
|
||||
normalizeProductDataForDisplay,
|
||||
getSafeFirstProduct,
|
||||
getSafeCurrencyInfo,
|
||||
} from '../../utils/mockDataSafetyUtils';
|
||||
import css from './CheckOutPanel.module.less';
|
||||
import PinCodeInput from './components/PinCodeInput';
|
||||
import FixedSideBar from './container/FixedSideBar';
|
||||
@@ -57,30 +62,58 @@ export default function CheckOutPanel({ panelInfo }) {
|
||||
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
|
||||
const popup = useSelector((state) => state.common.popup);
|
||||
|
||||
// Mock Mode: 가짜 상품 데이터 생성
|
||||
// Mock Mode: panelInfo.mockProductInfo 또는 Redux에서 상품 데이터 사용
|
||||
const productData = BUYNOW_CONFIG.isMockMode()
|
||||
? reduxProductData || [
|
||||
{
|
||||
prdtId: 'MOCK_PRODUCT_1',
|
||||
prdtNm: 'Mock Product for Demonstration',
|
||||
patnrId: '1',
|
||||
patncNm: 'Mock Partner',
|
||||
prodSno: 'MOCK_123',
|
||||
prodQty: 1,
|
||||
finalPrice: 99999,
|
||||
origPrice: 99999,
|
||||
discountPrice: 99999,
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
},
|
||||
]
|
||||
? (() => {
|
||||
// 1순위: BuyOption에서 전달된 productInfo
|
||||
if (panelInfo?.productInfo) {
|
||||
console.log('[CheckOutPanel] Using panelInfo.productInfo:', panelInfo.productInfo);
|
||||
return [panelInfo.productInfo];
|
||||
}
|
||||
// 2순위: Redux에서 가져온 상품 데이터
|
||||
if (reduxProductData && reduxProductData.length > 0) {
|
||||
console.log('[CheckOutPanel] Using reduxProductData:', reduxProductData);
|
||||
return reduxProductData;
|
||||
}
|
||||
// 3순위: 기본 Hardcoded Mock 데이터
|
||||
console.log('[CheckOutPanel] Using default hardcoded mock data');
|
||||
return [
|
||||
{
|
||||
prdtId: 'MOCK_PRODUCT_1',
|
||||
prdtNm: 'Mock Product',
|
||||
patnrId: '1',
|
||||
patncNm: 'Partner',
|
||||
prodQty: 1,
|
||||
finalPrice: 99999,
|
||||
origPrice: 99999,
|
||||
discountPrice: 99999,
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
},
|
||||
];
|
||||
})()
|
||||
: reduxProductData;
|
||||
|
||||
console.log('[CheckOutPanel] isMockMode:', BUYNOW_CONFIG.isMockMode());
|
||||
console.log('[CheckOutPanel] panelInfo:', panelInfo);
|
||||
console.log('[CheckOutPanel] reduxProductData:', reduxProductData);
|
||||
console.log('[CheckOutPanel] productData:', productData);
|
||||
console.log('[CheckOutPanel] productData (raw):', productData);
|
||||
console.log('[BuyOption][CheckOutPanel] 상품 정보:', productData);
|
||||
|
||||
const { currSign, currSignLoc } = productData?.[0] || {};
|
||||
// 표시용으로 모든 상품 데이터 정규화 (없는 필드는 안전한 기본값으로)
|
||||
// Mock 모드에서는 항상 정규화, API 모드에서는 그대로 사용
|
||||
const normalizedProductData = productData?.map((prod) => normalizeProductDataForDisplay(prod)) || [];
|
||||
const safeProductData = BUYNOW_CONFIG.isMockMode() ? normalizedProductData : productData;
|
||||
|
||||
console.log('[CheckOutPanel] productData (normalized):', normalizedProductData);
|
||||
console.log('[CheckOutPanel] safeProductData (final):', safeProductData);
|
||||
|
||||
// 첫 번째 상품 정보로 통화 정보 추출
|
||||
const firstProduct = getSafeFirstProduct(safeProductData);
|
||||
const { currSign, currSignLoc } = firstProduct
|
||||
? getSafeCurrencyInfo(firstProduct)
|
||||
: { currSign: '$', currSignLoc: 'left' };
|
||||
console.log('[CheckOutPanel] firstProduct:', firstProduct);
|
||||
console.log('[CheckOutPanel] currSign:', currSign, 'currSignLoc:', currSignLoc);
|
||||
|
||||
const [orderSideBarOpen, setOrderSideBarOpen] = useState(false);
|
||||
@@ -218,7 +251,12 @@ export default function CheckOutPanel({ panelInfo }) {
|
||||
console.log('[CheckOutPanel] cleanup useEffect - setting up cleanup');
|
||||
return () => {
|
||||
console.log('[CheckOutPanel] cleanup useEffect - calling resetCheckoutData');
|
||||
dispatch(resetCheckoutData());
|
||||
// Mock 모드일 때는 데이터를 유지해야 다시 진입했을 때 올바른 상품 정보 로드 가능
|
||||
if (!BUYNOW_CONFIG.isMockMode()) {
|
||||
dispatch(resetCheckoutData());
|
||||
} else {
|
||||
console.log('[CheckOutPanel] Mock Mode - Skipping resetCheckoutData to preserve product data');
|
||||
}
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -414,6 +452,9 @@ export default function CheckOutPanel({ panelInfo }) {
|
||||
currSign={currSign}
|
||||
currSignLoc={currSignLoc}
|
||||
doSendLogPaymentEntry={doSendLogPaymentEntry}
|
||||
productData={safeProductData}
|
||||
productInfo={panelInfo?.productInfo}
|
||||
defaultPrice={panelInfo?.defaultPrice}
|
||||
/>
|
||||
) : (
|
||||
<SummaryContainer
|
||||
@@ -456,7 +497,7 @@ export default function CheckOutPanel({ panelInfo }) {
|
||||
</TBody>
|
||||
</TPanel>
|
||||
|
||||
{orderSideBarOpen && <OrderItemsSideBar closeSideBar={toggleOrderSideBar} />}
|
||||
{orderSideBarOpen && <OrderItemsSideBar closeSideBar={toggleOrderSideBar} productData={safeProductData} productInfo={panelInfo?.productInfo} />}
|
||||
{offerSideBarOpen && (
|
||||
<FixedSideBar closeSideBar={toggleOfferSideBar} offerSideBarOpen={offerSideBarOpen} />
|
||||
)}
|
||||
|
||||
@@ -9,17 +9,23 @@ import TIConButton from '../../../components/TIconButton/TIconButton';
|
||||
import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import usePriceInfo from '../../../hooks/usePriceInfo';
|
||||
import { $L } from '../../../utils/helperMethods';
|
||||
import {
|
||||
normalizeProductDataForDisplay,
|
||||
getSafeFirstProduct,
|
||||
} from '../../../utils/mockDataSafetyUtils';
|
||||
import OrderItemCard, { SIZES } from '../components/OrderItemCard';
|
||||
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
|
||||
import css from './OrderItemsSideBar.module.less';
|
||||
|
||||
const SideBarContainer = SpotlightContainerDecorator('div');
|
||||
|
||||
export default function OrderItemsSideBar({ closeSideBar }) {
|
||||
export default function OrderItemsSideBar({ closeSideBar, productData, productInfo }) {
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar mounted');
|
||||
|
||||
const reduxOrderItemList = useSelector((state) => state.checkout?.checkoutData?.productList);
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar reduxOrderItemList:', reduxOrderItemList);
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar productData:', productData);
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar productInfo:', productInfo);
|
||||
|
||||
// Check if reduxOrderItemList has actual data
|
||||
const hasValidOrderItemList = Array.isArray(reduxOrderItemList) && reduxOrderItemList.length > 0;
|
||||
@@ -28,22 +34,75 @@ export default function OrderItemsSideBar({ closeSideBar }) {
|
||||
const orderItemList = hasValidOrderItemList
|
||||
? reduxOrderItemList
|
||||
: BUYNOW_CONFIG.isMockMode()
|
||||
? [
|
||||
{
|
||||
prdtId: 'MOCK_PRODUCT_1',
|
||||
prdtNm: 'Mock Product',
|
||||
prodQty: 1,
|
||||
prdtOpt: [{ prodOptCdCval: 'MOCK_OPT_1', optNm: 'Mock Option' }],
|
||||
patncLogPath: '/mock/image.jpg',
|
||||
expsPrdtNo: 'MOCK_EXP_1',
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
shippingCharge: 0,
|
||||
auctProdYn: 'N',
|
||||
auctFinalPriceChgDt: null,
|
||||
imgUrls: [{ imgUrl: '/mock/image.jpg' }],
|
||||
},
|
||||
]
|
||||
? (() => {
|
||||
// Mock Mode: productInfo가 있으면 우선 사용
|
||||
if (productInfo) {
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar Using productInfo for image');
|
||||
const normalized = normalizeProductDataForDisplay(productInfo);
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar productInfo normalized imgUrl:', normalized.imgUrl);
|
||||
|
||||
return [{
|
||||
prdtId: normalized.prdtId,
|
||||
prdtNm: normalized.prdtNm,
|
||||
prodQty: normalized.prodQty,
|
||||
prdtOpt: normalized.options || [{ prodOptCdCval: 'DEFAULT_OPT', optNm: 'Default Option' }],
|
||||
patncLogPath: normalized.imgUrl,
|
||||
expsPrdtNo: normalized.prdtId,
|
||||
currSign: normalized.currSign,
|
||||
currSignLoc: normalized.currSignLoc,
|
||||
shippingCharge: normalized.shippingCharge || 0,
|
||||
auctProdYn: normalized.auctProdYn || 'N',
|
||||
auctFinalPriceChgDt: null,
|
||||
imgUrls: [{ imgUrl: normalized.imgUrl }], // productInfo에서 추출한 실제 이미지
|
||||
// 표시용 추가 필드
|
||||
price: normalized.price,
|
||||
discount: normalized.discount,
|
||||
}];
|
||||
}
|
||||
// productInfo가 없으면 productData 사용
|
||||
else if (productData && productData.length > 0) {
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar Using productData (fallback)');
|
||||
return productData.map((prod) => {
|
||||
const normalized = normalizeProductDataForDisplay(prod);
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar productData normalized imgUrl:', normalized.imgUrl);
|
||||
return {
|
||||
prdtId: normalized.prdtId,
|
||||
prdtNm: normalized.prdtNm,
|
||||
prodQty: normalized.prodQty,
|
||||
prdtOpt: normalized.options || [{ prodOptCdCval: 'MOCK_OPT_1', optNm: 'Selected Option' }],
|
||||
patncLogPath: normalized.imgUrl,
|
||||
expsPrdtNo: normalized.prdtId,
|
||||
currSign: normalized.currSign,
|
||||
currSignLoc: normalized.currSignLoc,
|
||||
shippingCharge: normalized.shippingCharge || 0,
|
||||
auctProdYn: normalized.auctProdYn || 'N',
|
||||
auctFinalPriceChgDt: null,
|
||||
imgUrls: [{ imgUrl: normalized.imgUrl }], // 이미지 URL 추가
|
||||
// 표시용 추가 필드
|
||||
price: normalized.price,
|
||||
discount: normalized.discount,
|
||||
};
|
||||
});
|
||||
}
|
||||
// 둘 다 없으면 기본 Mock 데이터
|
||||
else {
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar Using default mock data');
|
||||
return [{
|
||||
prdtId: 'MOCK_PRODUCT_1',
|
||||
prdtNm: 'Mock Product',
|
||||
prodQty: 1,
|
||||
prdtOpt: [{ prodOptCdCval: 'MOCK_OPT_1', optNm: 'Mock Option' }],
|
||||
patncLogPath: '/mock/image.jpg',
|
||||
expsPrdtNo: 'MOCK_EXP_1',
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
shippingCharge: 0,
|
||||
auctProdYn: 'N',
|
||||
auctFinalPriceChgDt: null,
|
||||
imgUrls: [{ imgUrl: '/mock/image.jpg' }],
|
||||
}];
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
|
||||
console.log('[CheckOutPanel] OrderItemsSideBar effectiveOrderItemList:', orderItemList);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import TButton from '../../../components/TButton/TButton';
|
||||
import * as Config from '../../../utils/Config';
|
||||
import { $L, formatCurrencyValue } from '../../../utils/helperMethods';
|
||||
import { getSafeFirstProduct, calculateOrderSummaryFromProductInfo } from '../../../utils/mockDataSafetyUtils';
|
||||
import css from './SummaryContainer.module.less';
|
||||
|
||||
export default function SummaryContainerMock({
|
||||
@@ -12,21 +13,54 @@ export default function SummaryContainerMock({
|
||||
currSign,
|
||||
currSignLoc,
|
||||
doSendLogPaymentEntry,
|
||||
productData,
|
||||
productInfo,
|
||||
defaultPrice,
|
||||
}) {
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - START render');
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - empTermsData:', empTermsData);
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - currSign:', currSign);
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - productData:', productData);
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - productInfo:', productInfo);
|
||||
|
||||
// Mock Mode: 하드코딩된 가격 데이터
|
||||
// Mock Mode: productInfo로부터 ORDER SUMMARY용 가격 데이터 계산
|
||||
const orderSummaryData = useMemo(() => {
|
||||
if (productInfo) {
|
||||
// productInfo가 있으면 직접 계산
|
||||
return calculateOrderSummaryFromProductInfo(productInfo);
|
||||
} else {
|
||||
// productInfo가 없으면 기존 방식으로 fallback
|
||||
const selectedProduct = getSafeFirstProduct(productData);
|
||||
const productPrice = parseFloat(defaultPrice || selectedProduct?.price || 0);
|
||||
const productDiscount = parseFloat(selectedProduct?.discount || 0);
|
||||
const tax = Math.round((productPrice * 0.1) * 100) / 100;
|
||||
|
||||
return {
|
||||
items: productPrice,
|
||||
couponSavings: productDiscount,
|
||||
shipping: 0,
|
||||
subtotal: Math.max(0, productPrice - productDiscount),
|
||||
tax: tax,
|
||||
total: Math.max(0, productPrice - productDiscount) + tax,
|
||||
currency: { currSign, currSignLoc }
|
||||
};
|
||||
}
|
||||
}, [productInfo, productData, defaultPrice, currSign, currSignLoc]);
|
||||
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - orderSummaryData:', orderSummaryData);
|
||||
|
||||
// 기존 호환성을 위해 effectivePriceTotalData 유지
|
||||
const effectivePriceTotalData = {
|
||||
totProdPrc: 521.66,
|
||||
totDcAmt: 0,
|
||||
totDlvrAmt: 0,
|
||||
ordPmtNoTaxAmt: 521.66,
|
||||
ordTotTaxAmt: 50,
|
||||
ordPmtReqAmt: 571.66,
|
||||
totProdPrc: orderSummaryData.items,
|
||||
totDcAmt: orderSummaryData.couponSavings,
|
||||
totDlvrAmt: orderSummaryData.shipping,
|
||||
ordPmtNoTaxAmt: orderSummaryData.subtotal,
|
||||
ordTotTaxAmt: orderSummaryData.tax,
|
||||
ordPmtReqAmt: orderSummaryData.total,
|
||||
};
|
||||
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - effectivePriceTotalData:', effectivePriceTotalData);
|
||||
|
||||
// Mock Mode: 기본 상품 정보
|
||||
const productList = {
|
||||
auctProdYn: 'N',
|
||||
@@ -37,38 +71,38 @@ export default function SummaryContainerMock({
|
||||
() => [
|
||||
{
|
||||
name: 'Items',
|
||||
value: formatCurrencyValue(effectivePriceTotalData.totProdPrc, currSign, currSignLoc),
|
||||
value: formatCurrencyValue(effectivePriceTotalData.totProdPrc, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
|
||||
},
|
||||
{
|
||||
name: 'Your Coupon Savings',
|
||||
value: effectivePriceTotalData.totDcAmt
|
||||
? formatCurrencyValue(effectivePriceTotalData.totDcAmt, currSign, currSignLoc, true)
|
||||
? formatCurrencyValue(effectivePriceTotalData.totDcAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc, true)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
name: 'Shipping & Handling',
|
||||
value: formatCurrencyValue(effectivePriceTotalData.totDlvrAmt, currSign, currSignLoc),
|
||||
value: formatCurrencyValue(effectivePriceTotalData.totDlvrAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
|
||||
},
|
||||
{
|
||||
name: 'TOTAL (before Tax)',
|
||||
value: formatCurrencyValue(effectivePriceTotalData.ordPmtNoTaxAmt, currSign, currSignLoc),
|
||||
value: formatCurrencyValue(effectivePriceTotalData.ordPmtNoTaxAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
|
||||
},
|
||||
{
|
||||
name: 'Estimated Sales Tax',
|
||||
value: formatCurrencyValue(effectivePriceTotalData.ordTotTaxAmt, currSign, currSignLoc),
|
||||
value: formatCurrencyValue(effectivePriceTotalData.ordTotTaxAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
|
||||
},
|
||||
],
|
||||
[effectivePriceTotalData, currSign, currSignLoc]
|
||||
[effectivePriceTotalData, orderSummaryData.currency]
|
||||
);
|
||||
|
||||
const handleClickOrder = useCallback(() => {
|
||||
console.log('[SummaryContainerMock] Place order clicked');
|
||||
if (doSendLogPaymentEntry) {
|
||||
doSendLogPaymentEntry();
|
||||
}
|
||||
doSendLogPaymentEntry();
|
||||
setPlaceOrderPopup(true);
|
||||
}, [doSendLogPaymentEntry, setPlaceOrderPopup]);
|
||||
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - items:', items);
|
||||
|
||||
const renderItemList = useCallback(
|
||||
() =>
|
||||
items.map((item, index) => (
|
||||
@@ -100,8 +134,8 @@ export default function SummaryContainerMock({
|
||||
|
||||
const estimatedTotal = useMemo(() => {
|
||||
console.log('[CheckOutPanel] SummaryContainerMock - estimatedTotal useMemo');
|
||||
return formatCurrencyValue(effectivePriceTotalData.ordPmtReqAmt, currSign, currSignLoc);
|
||||
}, [effectivePriceTotalData, currSign, currSignLoc]);
|
||||
return formatCurrencyValue(effectivePriceTotalData.ordPmtReqAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc);
|
||||
}, [effectivePriceTotalData, orderSummaryData.currency]);
|
||||
|
||||
const showAuctionNotice = productList?.auctProdYn === 'Y' && !productList.auctFinalPriceChgDt;
|
||||
|
||||
|
||||
@@ -533,6 +533,25 @@
|
||||
// 배경색과 라운드는 FavoriteBtn 내부에서 처리하므로 제거
|
||||
}
|
||||
|
||||
// FavoriteBtn 컴포넌트에 적용할 스타일
|
||||
.favoriteBtn {
|
||||
width: 60px !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;
|
||||
// outline은 사용하지 않음
|
||||
}
|
||||
}
|
||||
|
||||
// 액션 버튼들 (actionButtons 참고)
|
||||
.actionButtonsWrapper {
|
||||
width: 100%;
|
||||
|
||||
@@ -110,23 +110,30 @@ const BuyOption = ({
|
||||
|
||||
// Mock Mode에서 옵션 데이터 처리
|
||||
const productOptionInfos = useMemo(() => {
|
||||
console.log('[BuyOption] productOptionInfos useMemo - isMockMode:', BUYNOW_CONFIG.isMockMode());
|
||||
console.log('[BuyOption] productOptionInfos useMemo - reduxProductOptionInfos:', reduxProductOptionInfos);
|
||||
|
||||
// API Mode: 기존 로직 100% 유지
|
||||
if (!BUYNOW_CONFIG.isMockMode()) {
|
||||
console.log('[BuyOption] API Mode - using reduxProductOptionInfos');
|
||||
return reduxProductOptionInfos;
|
||||
}
|
||||
|
||||
// Mock Mode: 옵션 데이터가 없으면 Mock 데이터 생성
|
||||
if (!reduxProductOptionInfos || reduxProductOptionInfos.length === 0) {
|
||||
// Mock Mode: 옵션 데이터가 없거나 배열이 아니면 Mock 데이터 생성
|
||||
const isValidReduxData = Array.isArray(reduxProductOptionInfos) && reduxProductOptionInfos.length > 0;
|
||||
|
||||
if (!isValidReduxData) {
|
||||
console.log('[BuyOption] Mock Mode - generating mock option data (reduxData invalid)');
|
||||
const mockOptionData = createMockProductOptionData(productData);
|
||||
// Mock 옵션 데이터 배열 반환 (기존 구조와 호환)
|
||||
return mockOptionData?.optionList || [];
|
||||
console.log('[BuyOption] Mock Mode - createMockProductOptionData result:', mockOptionData);
|
||||
// Mock 옵션 데이터는 이미 배열 구조로 반환됨
|
||||
return mockOptionData || [];
|
||||
}
|
||||
|
||||
// Mock Mode이고 옵션 데이터가 있으면 그대로 사용
|
||||
// Mock Mode이고 유효한 옵션 데이터가 있으면 그대로 사용
|
||||
console.log('[BuyOption] Mock Mode - using existing valid reduxProductOptionInfos');
|
||||
return reduxProductOptionInfos;
|
||||
}, [reduxProductOptionInfos, productData]);
|
||||
|
||||
// logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용)
|
||||
}, [reduxProductOptionInfos, productData]); // logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용)
|
||||
const logInfo = useMemo(() => {
|
||||
if (productData) {
|
||||
// productData가 있으면 SingleOption과 동일하게 처리
|
||||
@@ -222,17 +229,50 @@ const BuyOption = ({
|
||||
}, [selectedIndex, productOptionInfos, type]);
|
||||
|
||||
// 옵션 자동 선택 로직 (SingleOption과 동일)
|
||||
// Mock Mode: 항상 첫 번째 옵션을 자동으로 선택
|
||||
useEffect(() => {
|
||||
if (
|
||||
productOptionInfos &&
|
||||
selectedBtnOptIdx >= 0 &&
|
||||
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length === 1 &&
|
||||
!isOptionValue
|
||||
) {
|
||||
setSelectedOptions(productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[0]);
|
||||
console.log('[BuyOption] autoSelect useEffect - productOptionInfos:', productOptionInfos);
|
||||
console.log('[BuyOption] autoSelect useEffect - selectedBtnOptIdx:', selectedBtnOptIdx);
|
||||
console.log('[BuyOption] autoSelect useEffect - isOptionValue:', isOptionValue);
|
||||
|
||||
if (!productOptionInfos || productOptionInfos.length === 0) {
|
||||
console.log('[BuyOption] autoSelect - productOptionInfos is empty, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentOptionGroup = productOptionInfos[selectedBtnOptIdx];
|
||||
if (!currentOptionGroup) {
|
||||
console.log('[BuyOption] autoSelect - currentOptionGroup is not found at index:', selectedBtnOptIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
const optionDetails = currentOptionGroup.prdtOptDtl;
|
||||
console.log('[BuyOption] autoSelect - optionDetails:', optionDetails);
|
||||
|
||||
if (!optionDetails || optionDetails.length === 0) {
|
||||
console.log('[BuyOption] autoSelect - optionDetails is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미 선택되었으면 스킵
|
||||
if (isOptionValue) {
|
||||
console.log('[BuyOption] autoSelect - already selected, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock Mode: 첫 번째 옵션 자동 선택
|
||||
if (BUYNOW_CONFIG.isMockMode()) {
|
||||
console.log('[BuyOption] Mock Mode - Auto selecting first option:', optionDetails[0]);
|
||||
setSelectedOptions(optionDetails[0]);
|
||||
setIsOptionValue(true);
|
||||
}
|
||||
}, [productOptionInfos, selectedBtnOptIdx, isOptionValue]);
|
||||
// API Mode: 옵션이 1개일 때만 자동 선택
|
||||
else if (optionDetails.length === 1) {
|
||||
console.log('[BuyOption] API Mode - Auto selecting only option:', optionDetails[0]);
|
||||
setSelectedOptions(optionDetails[0]);
|
||||
setIsOptionValue(true);
|
||||
}
|
||||
}, [productOptionInfos, selectedBtnOptIdx]);
|
||||
|
||||
// 필수 데이터 로드 (SingleOption과 동일)
|
||||
// Mock Mode: API 호출 스킵 (Mock 데이터만 사용)
|
||||
@@ -408,10 +448,27 @@ 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];
|
||||
console.log('[BuyOption] handleClickOrder - selectedOptions:', selectedOptions);
|
||||
console.log('[BuyOption] handleClickOrder - priceInfo:', priceInfo);
|
||||
console.log('[BuyOption] handleClickOrder - logInfo:', logInfo);
|
||||
|
||||
// priceInfo 파싱 및 숫자 변환
|
||||
let regularPrice = parseInt(priceInfo?.split('|')[0], 10) || 0;
|
||||
let discountPrice = parseInt(priceInfo?.split('|')[1], 10) || 0;
|
||||
const discountRate = priceInfo?.split('|')[4];
|
||||
|
||||
// selectedOptions가 없으면 logInfo에서 가격 추출
|
||||
if (!selectedOptions && logInfo) {
|
||||
console.log('[BuyOption] handleClickOrder - selectedOptions is undefined, using logInfo prices');
|
||||
// logInfo의 dcBefPrc와 dcAftrPrc는 "$ 521.66" 형식이므로 숫자만 추출 (소수점 포함)
|
||||
const dcBefPrcMatch = logInfo.dcBefPrc?.match(/[\d.]+/);
|
||||
const dcAftrPrcMatch = logInfo.dcAftrPrc?.match(/[\d.]+/);
|
||||
regularPrice = dcBefPrcMatch ? parseFloat(dcBefPrcMatch[0]) : 0;
|
||||
discountPrice = dcAftrPrcMatch ? parseFloat(dcAftrPrcMatch[0]) : regularPrice;
|
||||
console.log('[BuyOption] handleClickOrder - extracted from logInfo - dcBefPrc:', logInfo.dcBefPrc, 'dcAftrPrc:', logInfo.dcAftrPrc);
|
||||
}
|
||||
console.log('[BuyOption] handleClickOrder - regularPrice:', regularPrice, 'discountPrice:', discountPrice, 'discountRate:', discountRate);
|
||||
|
||||
dispatch(
|
||||
sendLogTotalRecommend({
|
||||
nowMenu: nowMenu,
|
||||
@@ -452,13 +509,44 @@ const BuyOption = ({
|
||||
);
|
||||
} else {
|
||||
// Mock Mode: 체크아웃 페이지로 이동 (시뮬레이션)
|
||||
// panelInfo의 logInfo에 선택한 상품 정보를 포함시켜 CheckOutPanel에서 사용
|
||||
console.log('[BuyOption] Mock Mode - Simulating checkout');
|
||||
console.log('[BuyOption] logInfo:', logInfo);
|
||||
console.log('[BuyOption] Dispatching pushPanel to CHECKOUT_PANEL');
|
||||
dispatch(finishVideoPreview());
|
||||
dispatch(finishMediaPreview());
|
||||
const checkoutPanelInfo = { logInfo: { ...logInfo, cartTpSno: `MOCK_${Date.now()}` } };
|
||||
console.log('[BuyOption] checkoutPanelInfo:', checkoutPanelInfo);
|
||||
|
||||
// Mock 모드: 선택 상품의 정보를 panelInfo에 담아서 전달
|
||||
// CheckOutPanel에서 이 정보로 Mock 상품 데이터 생성
|
||||
// 이미지 URL 추출 (productInfo의 imgList 또는 thumbnailUrl 사용)
|
||||
const imgUrl = productInfo?.imgList?.[0]?.imgUrl ||
|
||||
productInfo?.thumbnailUrl ||
|
||||
productInfo?.patncLogPath ||
|
||||
'/mock/image.jpg';
|
||||
|
||||
const mockProductInfo = {
|
||||
prdtId: selectedPrdtId,
|
||||
prdtNm: productInfo?.prdtNm,
|
||||
patnrId: selectedPatnrId,
|
||||
patncNm: patncNm,
|
||||
prodQty: quantity,
|
||||
origPrice: regularPrice || 99999, // 원래 가격 (숫자)
|
||||
discountPrice: discountPrice || regularPrice || 99999, // 할인된 가격 (실제 판매 가격, 숫자)
|
||||
finalPrice: discountPrice || regularPrice || 99999, // 최종 가격 (숫자)
|
||||
currSign: '$',
|
||||
currSignLoc: 'left',
|
||||
imgUrl: imgUrl, // 상품 이미지 URL 추가
|
||||
};
|
||||
|
||||
const checkoutPanelInfo = {
|
||||
logInfo: { ...logInfo, cartTpSno: `MOCK_${Date.now()}` },
|
||||
productInfo: productInfo,
|
||||
defaultPrice: discountPrice,
|
||||
};
|
||||
|
||||
console.log('[BuyOption] Mock Mode - mockProductInfo:', mockProductInfo);
|
||||
console.log('[BuyOption] Mock Mode - regularPrice(숫자):', regularPrice, 'discountPrice(숫자):', discountPrice);
|
||||
console.log('[BuyOption] Mock Mode - checkoutPanelInfo:', checkoutPanelInfo);
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: Config.panel_names.CHECKOUT_PANEL,
|
||||
@@ -673,6 +761,11 @@ const BuyOption = ({
|
||||
setFavoriteFlag(ev);
|
||||
}, []);
|
||||
|
||||
// 구매창이 뜰 때 콘솔 로그 출력 (상품 정보 포함 태그 [BuyOption] 붙임)
|
||||
useEffect(() => {
|
||||
console.log('[BuyOption]', '상품 정보:', JSON.stringify(productInfo));
|
||||
}, []); // 컴포넌트 마운트 시 한 번만 출력
|
||||
|
||||
// hasOnClose 로직 (SingleOption과 동일)
|
||||
const hasOnClose = useMemo(() => {
|
||||
if (productOptionInfos && productOptionInfos.length > 0) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import {
|
||||
setHidePopup,
|
||||
@@ -28,6 +29,8 @@ import * as Config from '../../../utils/Config';
|
||||
import { $L } from '../../../utils/helperMethods';
|
||||
import css from './FavoriteBtn.module.less';
|
||||
|
||||
const SpottableDiv = Spottable('div');
|
||||
|
||||
export default function FavoriteBtn({
|
||||
selectedPatnrId,
|
||||
selectedPrdtId,
|
||||
@@ -80,7 +83,7 @@ export default function FavoriteBtn({
|
||||
}, [dispatch, favoriteFlag, onFavoriteFlagChanged]);
|
||||
|
||||
return (
|
||||
<div
|
||||
<SpottableDiv
|
||||
className={classNames(
|
||||
css.favorBtnContainer,
|
||||
kind === "item_detail" ? css.smallSize : ""
|
||||
@@ -110,6 +113,6 @@ export default function FavoriteBtn({
|
||||
onClick={PopUpOnClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SpottableDiv>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
.favorUnableBtn {
|
||||
min-width: 60px;
|
||||
height: 60px;
|
||||
background-image: url(../../../../assets/images/icons/ic-heart-nor@3x.png);
|
||||
background-color: #4f172c; // 색상 추가.
|
||||
background-color: rgba(68, 68, 68, 0.5); // 다른 버튼들과 동일한 배경색
|
||||
.imgElement(54px, 54px, center, center);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ const DEBUG_MODE = true;
|
||||
class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
errorTimestamp: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
@@ -18,11 +23,43 @@ class ErrorBoundary extends Component {
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error("Uncaught error:", error, errorInfo);
|
||||
const errorTimestamp = new Date().toLocaleString();
|
||||
|
||||
console.error("❌ Uncaught error:", error);
|
||||
console.error("📋 Error Info:", errorInfo);
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
// Development mode: log error details instead of reloading
|
||||
console.error("Error Stack:", error.stack);
|
||||
console.error("Component Stack:", errorInfo.componentStack);
|
||||
// Development mode: log detailed error information
|
||||
console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
console.error("🔴 ERROR BOUNDARY CAUGHT AN ERROR");
|
||||
console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
|
||||
// Error message and type
|
||||
console.error("⚠️ Error Type:", error.name);
|
||||
console.error("📝 Error Message:", error.message);
|
||||
|
||||
// Error stack trace
|
||||
console.error("\n📍 Stack Trace:");
|
||||
console.error(error.stack);
|
||||
|
||||
// Component stack
|
||||
console.error("\n🏗️ Component Stack:");
|
||||
console.error(errorInfo.componentStack);
|
||||
|
||||
// Additional error details
|
||||
console.error("\n📊 Error Details:");
|
||||
console.error("- Time:", errorTimestamp);
|
||||
console.error("- Type:", typeof error);
|
||||
console.error("- Error Object:", error);
|
||||
|
||||
console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
|
||||
// Store error info in state for UI display
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
errorTimestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -37,32 +74,221 @@ class ErrorBoundary extends Component {
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// In DEBUG_MODE, show error UI; otherwise show empty div and reload
|
||||
// In DEBUG_MODE, show detailed error UI; otherwise show empty div and reload
|
||||
if (DEBUG_MODE) {
|
||||
const { error, errorInfo, errorTimestamp } = this.state;
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
backgroundColor: '#f8d7da',
|
||||
color: '#721c24',
|
||||
borderRadius: '4px',
|
||||
margin: '20px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflow: 'auto'
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<h2>⚠️ Error Caught by ErrorBoundary (DEBUG_MODE)</h2>
|
||||
<p>Check browser console for detailed error information.</p>
|
||||
<button onClick={() => window.location.reload()} style={{
|
||||
padding: '10px 20px',
|
||||
marginTop: '10px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#721c24',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
{/* Header - Fixed */}
|
||||
<div style={{
|
||||
flexShrink: 0,
|
||||
padding: '30px',
|
||||
borderBottom: '4px solid #721c24',
|
||||
backgroundColor: '#f8d7da',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
Reload Page
|
||||
</button>
|
||||
<h1 style={{ margin: '0', fontSize: '48px', pointerEvents: 'auto' }}>
|
||||
🚨 ERROR BOUNDARY - DEBUG MODE
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Content - Scrollable */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '30px',
|
||||
fontSize: '24px',
|
||||
lineHeight: '1.8',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
MozUserSelect: 'text',
|
||||
msUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
{errorTimestamp && (
|
||||
<div style={{
|
||||
marginBottom: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
padding: '20px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '24px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<strong>⏰ Timestamp:</strong>
|
||||
<div style={{ marginTop: '10px', fontSize: '22px', userSelect: 'text', pointerEvents: 'auto' }}>{errorTimestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<div style={{
|
||||
marginBottom: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
padding: '20px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '24px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<strong>❌ Error Type:</strong>
|
||||
<div style={{ marginTop: '10px', fontSize: '22px', userSelect: 'text', pointerEvents: 'auto' }}>{error.name}</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginBottom: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
padding: '20px',
|
||||
borderRadius: '6px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<strong style={{ fontSize: '24px', pointerEvents: 'auto' }}>📝 Error Message:</strong>
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '22px',
|
||||
backgroundColor: '#fff',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
color: '#d9534f',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
cursor: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && error.stack && (
|
||||
<div style={{
|
||||
marginBottom: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
padding: '20px',
|
||||
borderRadius: '6px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<strong style={{ fontSize: '24px', pointerEvents: 'auto' }}>📍 Stack Trace:</strong>
|
||||
<pre style={{
|
||||
margin: '15px 0 0 0',
|
||||
overflow: 'auto',
|
||||
maxHeight: '300px',
|
||||
backgroundColor: '#fff',
|
||||
padding: '20px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '1.6',
|
||||
border: '2px solid #721c24',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
cursor: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
{error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorInfo && errorInfo.componentStack && (
|
||||
<div style={{
|
||||
marginBottom: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
padding: '20px',
|
||||
borderRadius: '6px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<strong style={{ fontSize: '24px', pointerEvents: 'auto' }}>🏗️ Component Stack:</strong>
|
||||
<pre style={{
|
||||
margin: '15px 0 0 0',
|
||||
overflow: 'auto',
|
||||
maxHeight: '300px',
|
||||
backgroundColor: '#fff',
|
||||
padding: '20px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '1.6',
|
||||
border: '2px solid #721c24',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
cursor: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
{errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
paddingTop: '30px',
|
||||
borderTop: '3px solid #721c24',
|
||||
fontSize: '20px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<p style={{ marginBottom: '20px', pointerEvents: 'auto' }}>
|
||||
ℹ️ Check browser DevTools Console (F12) for additional error details and network information
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer - Fixed */}
|
||||
<div style={{
|
||||
flexShrink: 0,
|
||||
padding: '30px',
|
||||
borderTop: '4px solid #721c24',
|
||||
backgroundColor: '#f8d7da',
|
||||
textAlign: 'center',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
padding: '20px 40px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#721c24',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
transition: 'background-color 0.3s',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#5a1620'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#721c24'}
|
||||
>
|
||||
🔄 Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user