diff --git a/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js b/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js index 81522d17..5a82ebbf 100644 --- a/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js +++ b/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js @@ -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], + } + ]; }; /** diff --git a/com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js b/com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js new file mode 100644 index 00000000..4949ae67 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/mockDataSafetyUtils.js @@ -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, +}; diff --git a/com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx b/com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx index dca28175..b47d81ce 100644 --- a/com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx +++ b/com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx @@ -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} /> ) : ( - {orderSideBarOpen && } + {orderSideBarOpen && } {offerSideBarOpen && ( )} @@ -498,7 +539,7 @@ export default function CheckOutPanel({ panelInfo }) { )} - {/* + {/* {activePopup === Config.ACTIVE_POPUP.errorPopup && ( 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); diff --git a/com.twin.app.shoptime/src/views/CheckOutPanel/container/SummaryContainerMock.jsx b/com.twin.app.shoptime/src/views/CheckOutPanel/container/SummaryContainerMock.jsx index 41df0207..a5af1ff1 100644 --- a/com.twin.app.shoptime/src/views/CheckOutPanel/container/SummaryContainerMock.jsx +++ b/com.twin.app.shoptime/src/views/CheckOutPanel/container/SummaryContainerMock.jsx @@ -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; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less index 4852ae2e..8ae7be0c 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less @@ -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%; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx index 3931eb74..fcdac5ac 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx @@ -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 데이터만 사용) @@ -320,17 +360,17 @@ const BuyOption = ({ } } else if (response.retCode === 1001) { dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup)); - dispatch(changeAppStatus({ isLoading: false })); - } else { + dispatch(changeAppStatus({ isLoading: false })); + } else { dispatch( showError( response.retCode, - response.retMsg, - false, + response.retMsg, + false, response.retDetailCode, response.returnBindStrings ) - ); + ); dispatch(changeAppStatus({ isLoading: false })); return; } @@ -369,18 +409,18 @@ const BuyOption = ({ productOptionInfos.length > 1 && productInfo?.optProdYn === 'Y' ) { - + if (selectFirstOptionIndex === 0) { dispatch( - showError( + showError( null, "PLEASE SELECT OPTION", false, null, null ) - ); - + ); + return; } } @@ -389,18 +429,18 @@ const BuyOption = ({ productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 && productInfo?.optProdYn === 'Y' ) { - + if (selectSecondOptionIndex === 0) { dispatch( - showError( + showError( null, "PLEASE SELECT OPTION", false, null, null ) - ); - + ); + return; } } @@ -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) { diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx index 6575276b..15517e80 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.jsx @@ -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 ( -
)} -
+ ); } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.module.less index f77e7866..c881fb34 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/FavoriteBtn.module.less @@ -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); } } diff --git a/com.twin.app.shoptime/src/views/ErrorBoundary.js b/com.twin.app.shoptime/src/views/ErrorBoundary.js index d9e7e22e..2f71f411 100644 --- a/com.twin.app.shoptime/src/views/ErrorBoundary.js +++ b/com.twin.app.shoptime/src/views/ErrorBoundary.js @@ -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 (
-

⚠️ Error Caught by ErrorBoundary (DEBUG_MODE)

-

Check browser console for detailed error information.

- +

+ 🚨 ERROR BOUNDARY - DEBUG MODE +

+
+ + {/* Content - Scrollable */} +
+ {errorTimestamp && ( +
+ ⏰ Timestamp: +
{errorTimestamp}
+
+ )} + + {error && ( + <> +
+ ❌ Error Type: +
{error.name}
+
+ +
+ 📝 Error Message: +
+ {error.message} +
+
+ + )} + + {error && error.stack && ( +
+ 📍 Stack Trace: +
+                    {error.stack}
+                  
+
+ )} + + {errorInfo && errorInfo.componentStack && ( +
+ 🏗️ Component Stack: +
+                    {errorInfo.componentStack}
+                  
+
+ )} + +
+

+ ℹ️ Check browser DevTools Console (F12) for additional error details and network information +

+
+
+ + {/* Footer - Fixed */} +
+ +
); }