[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:
2025-11-01 21:01:11 +09:00
parent 0e5282476d
commit 6292e84052
10 changed files with 942 additions and 141 deletions

View File

@@ -19,34 +19,35 @@ export const createMockProductOptionData = (originalProductData) => {
} }
// 원본 데이터에서 필요한 정보 추출 // 원본 데이터에서 필요한 정보 추출
const basePrice = originalProductData.prdtPrice || 0; const prdtId = originalProductData.prdtId || 'MOCK_PRODUCT';
const prdtId = originalProductData.prdtId; const prdtNm = originalProductData.prdtNm || 'Mock Product';
const prdtNm = originalProductData.prdtNm;
// Mock 옵션 데이터 생성 // 실제 상품 가격 정보 사용 (originalProductData의 priceInfo 또는 기본값)
return { // priceInfo 포맷: "원가|할인가|할인액|할인율|할인율숫자"
// 옵션 ID (기본 값) const priceInfo = originalProductData.priceInfo || '99999|99999|0|0%|0';
prodOptSno: `MOCK_OPT_${prdtId}_1`,
// 옵션 목록 (기본값 1개) console.log('[BuyNowDataManipulator] createMockProductOptionData - priceInfo:', priceInfo);
optionList: [
{
optId: `MOCK_OPT_${prdtId}_1`,
optNm: `${prdtNm} - 기본 옵션`,
optPrice: basePrice,
optDscntPrice: basePrice,
stockCnt: 999,
soldOutYn: 'N',
optImgUrl: originalProductData.thumbnailUrl || null
}
],
// 옵션 타입 정보 // 옵션 상세 객체
optionTypeInfo: { const optionDetail = {
optTypeCd: 'BASIC', prodOptCval: `${prdtNm}`,
optTypeNm: '기본' priceInfo: priceInfo,
} stockCnt: 999,
soldOutYn: 'N',
}; };
// Mock 옵션 데이터 생성 (BuyOption에서 기대하는 구조)
// 배열 구조로 반환 - BuyOption에서 productOptionInfos[0]에 접근
return [
{
// 옵션 ID (기본 값)
prodOptSno: `MOCK_OPT_${prdtId}_1`,
prodOptTpCdCval: 'BASIC',
// 옵션 상세 목록 (기본값 1개) - prdtOptDtl 배열 구조
prdtOptDtl: [optionDetail],
}
];
}; };
/** /**

View 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,
};

View File

@@ -30,6 +30,11 @@ import * as Config from '../../utils/Config';
import { $L, scaleH, scaleW } from '../../utils/helperMethods'; import { $L, scaleH, scaleW } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds'; import { SpotlightIds } from '../../utils/SpotlightIds';
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig'; import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
import {
normalizeProductDataForDisplay,
getSafeFirstProduct,
getSafeCurrencyInfo,
} from '../../utils/mockDataSafetyUtils';
import css from './CheckOutPanel.module.less'; import css from './CheckOutPanel.module.less';
import PinCodeInput from './components/PinCodeInput'; import PinCodeInput from './components/PinCodeInput';
import FixedSideBar from './container/FixedSideBar'; import FixedSideBar from './container/FixedSideBar';
@@ -57,30 +62,58 @@ export default function CheckOutPanel({ panelInfo }) {
const { popupVisible, activePopup } = useSelector((state) => state.common.popup); const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const popup = useSelector((state) => state.common.popup); const popup = useSelector((state) => state.common.popup);
// Mock Mode: 가짜 상품 데이터 생성 // Mock Mode: panelInfo.mockProductInfo 또는 Redux에서 상품 데이터 사용
const productData = BUYNOW_CONFIG.isMockMode() const productData = BUYNOW_CONFIG.isMockMode()
? reduxProductData || [ ? (() => {
{ // 1순위: BuyOption에서 전달된 productInfo
prdtId: 'MOCK_PRODUCT_1', if (panelInfo?.productInfo) {
prdtNm: 'Mock Product for Demonstration', console.log('[CheckOutPanel] Using panelInfo.productInfo:', panelInfo.productInfo);
patnrId: '1', return [panelInfo.productInfo];
patncNm: 'Mock Partner', }
prodSno: 'MOCK_123', // 2순위: Redux에서 가져온 상품 데이터
prodQty: 1, if (reduxProductData && reduxProductData.length > 0) {
finalPrice: 99999, console.log('[CheckOutPanel] Using reduxProductData:', reduxProductData);
origPrice: 99999, return reduxProductData;
discountPrice: 99999, }
currSign: '$', // 3순위: 기본 Hardcoded Mock 데이터
currSignLoc: 'left', 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; : reduxProductData;
console.log('[CheckOutPanel] isMockMode:', BUYNOW_CONFIG.isMockMode()); console.log('[CheckOutPanel] isMockMode:', BUYNOW_CONFIG.isMockMode());
console.log('[CheckOutPanel] panelInfo:', panelInfo);
console.log('[CheckOutPanel] reduxProductData:', reduxProductData); 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); console.log('[CheckOutPanel] currSign:', currSign, 'currSignLoc:', currSignLoc);
const [orderSideBarOpen, setOrderSideBarOpen] = useState(false); const [orderSideBarOpen, setOrderSideBarOpen] = useState(false);
@@ -218,7 +251,12 @@ export default function CheckOutPanel({ panelInfo }) {
console.log('[CheckOutPanel] cleanup useEffect - setting up cleanup'); console.log('[CheckOutPanel] cleanup useEffect - setting up cleanup');
return () => { return () => {
console.log('[CheckOutPanel] cleanup useEffect - calling resetCheckoutData'); 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]); }, [dispatch]);
@@ -414,6 +452,9 @@ export default function CheckOutPanel({ panelInfo }) {
currSign={currSign} currSign={currSign}
currSignLoc={currSignLoc} currSignLoc={currSignLoc}
doSendLogPaymentEntry={doSendLogPaymentEntry} doSendLogPaymentEntry={doSendLogPaymentEntry}
productData={safeProductData}
productInfo={panelInfo?.productInfo}
defaultPrice={panelInfo?.defaultPrice}
/> />
) : ( ) : (
<SummaryContainer <SummaryContainer
@@ -456,7 +497,7 @@ export default function CheckOutPanel({ panelInfo }) {
</TBody> </TBody>
</TPanel> </TPanel>
{orderSideBarOpen && <OrderItemsSideBar closeSideBar={toggleOrderSideBar} />} {orderSideBarOpen && <OrderItemsSideBar closeSideBar={toggleOrderSideBar} productData={safeProductData} productInfo={panelInfo?.productInfo} />}
{offerSideBarOpen && ( {offerSideBarOpen && (
<FixedSideBar closeSideBar={toggleOfferSideBar} offerSideBarOpen={offerSideBarOpen} /> <FixedSideBar closeSideBar={toggleOfferSideBar} offerSideBarOpen={offerSideBarOpen} />
)} )}
@@ -498,7 +539,7 @@ export default function CheckOutPanel({ panelInfo }) {
</TPopUp> </TPopUp>
)} )}
{/* {/*
{activePopup === Config.ACTIVE_POPUP.errorPopup && ( {activePopup === Config.ACTIVE_POPUP.errorPopup && (
<TPopUp <TPopUp
kind="errorPopup" kind="errorPopup"

View File

@@ -9,17 +9,23 @@ import TIConButton from '../../../components/TIconButton/TIconButton';
import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList';
import usePriceInfo from '../../../hooks/usePriceInfo'; import usePriceInfo from '../../../hooks/usePriceInfo';
import { $L } from '../../../utils/helperMethods'; import { $L } from '../../../utils/helperMethods';
import {
normalizeProductDataForDisplay,
getSafeFirstProduct,
} from '../../../utils/mockDataSafetyUtils';
import OrderItemCard, { SIZES } from '../components/OrderItemCard'; import OrderItemCard, { SIZES } from '../components/OrderItemCard';
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
import css from './OrderItemsSideBar.module.less'; import css from './OrderItemsSideBar.module.less';
const SideBarContainer = SpotlightContainerDecorator('div'); const SideBarContainer = SpotlightContainerDecorator('div');
export default function OrderItemsSideBar({ closeSideBar }) { export default function OrderItemsSideBar({ closeSideBar, productData, productInfo }) {
console.log('[CheckOutPanel] OrderItemsSideBar mounted'); console.log('[CheckOutPanel] OrderItemsSideBar mounted');
const reduxOrderItemList = useSelector((state) => state.checkout?.checkoutData?.productList); const reduxOrderItemList = useSelector((state) => state.checkout?.checkoutData?.productList);
console.log('[CheckOutPanel] OrderItemsSideBar reduxOrderItemList:', reduxOrderItemList); console.log('[CheckOutPanel] OrderItemsSideBar reduxOrderItemList:', reduxOrderItemList);
console.log('[CheckOutPanel] OrderItemsSideBar productData:', productData);
console.log('[CheckOutPanel] OrderItemsSideBar productInfo:', productInfo);
// Check if reduxOrderItemList has actual data // Check if reduxOrderItemList has actual data
const hasValidOrderItemList = Array.isArray(reduxOrderItemList) && reduxOrderItemList.length > 0; const hasValidOrderItemList = Array.isArray(reduxOrderItemList) && reduxOrderItemList.length > 0;
@@ -28,22 +34,75 @@ export default function OrderItemsSideBar({ closeSideBar }) {
const orderItemList = hasValidOrderItemList const orderItemList = hasValidOrderItemList
? reduxOrderItemList ? reduxOrderItemList
: BUYNOW_CONFIG.isMockMode() : BUYNOW_CONFIG.isMockMode()
? [ ? (() => {
{ // Mock Mode: productInfo가 있으면 우선 사용
prdtId: 'MOCK_PRODUCT_1', if (productInfo) {
prdtNm: 'Mock Product', console.log('[CheckOutPanel] OrderItemsSideBar Using productInfo for image');
prodQty: 1, const normalized = normalizeProductDataForDisplay(productInfo);
prdtOpt: [{ prodOptCdCval: 'MOCK_OPT_1', optNm: 'Mock Option' }], console.log('[CheckOutPanel] OrderItemsSideBar productInfo normalized imgUrl:', normalized.imgUrl);
patncLogPath: '/mock/image.jpg',
expsPrdtNo: 'MOCK_EXP_1', return [{
currSign: '$', prdtId: normalized.prdtId,
currSignLoc: 'left', prdtNm: normalized.prdtNm,
shippingCharge: 0, prodQty: normalized.prodQty,
auctProdYn: 'N', prdtOpt: normalized.options || [{ prodOptCdCval: 'DEFAULT_OPT', optNm: 'Default Option' }],
auctFinalPriceChgDt: null, patncLogPath: normalized.imgUrl,
imgUrls: [{ imgUrl: '/mock/image.jpg' }], 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; : null;
console.log('[CheckOutPanel] OrderItemsSideBar effectiveOrderItemList:', orderItemList); console.log('[CheckOutPanel] OrderItemsSideBar effectiveOrderItemList:', orderItemList);

View File

@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import TButton from '../../../components/TButton/TButton'; import TButton from '../../../components/TButton/TButton';
import * as Config from '../../../utils/Config'; import * as Config from '../../../utils/Config';
import { $L, formatCurrencyValue } from '../../../utils/helperMethods'; import { $L, formatCurrencyValue } from '../../../utils/helperMethods';
import { getSafeFirstProduct, calculateOrderSummaryFromProductInfo } from '../../../utils/mockDataSafetyUtils';
import css from './SummaryContainer.module.less'; import css from './SummaryContainer.module.less';
export default function SummaryContainerMock({ export default function SummaryContainerMock({
@@ -12,21 +13,54 @@ export default function SummaryContainerMock({
currSign, currSign,
currSignLoc, currSignLoc,
doSendLogPaymentEntry, doSendLogPaymentEntry,
productData,
productInfo,
defaultPrice,
}) { }) {
console.log('[CheckOutPanel] SummaryContainerMock - START render'); console.log('[CheckOutPanel] SummaryContainerMock - START render');
console.log('[CheckOutPanel] SummaryContainerMock - empTermsData:', empTermsData); console.log('[CheckOutPanel] SummaryContainerMock - empTermsData:', empTermsData);
console.log('[CheckOutPanel] SummaryContainerMock - currSign:', currSign); 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 = { const effectivePriceTotalData = {
totProdPrc: 521.66, totProdPrc: orderSummaryData.items,
totDcAmt: 0, totDcAmt: orderSummaryData.couponSavings,
totDlvrAmt: 0, totDlvrAmt: orderSummaryData.shipping,
ordPmtNoTaxAmt: 521.66, ordPmtNoTaxAmt: orderSummaryData.subtotal,
ordTotTaxAmt: 50, ordTotTaxAmt: orderSummaryData.tax,
ordPmtReqAmt: 571.66, ordPmtReqAmt: orderSummaryData.total,
}; };
console.log('[CheckOutPanel] SummaryContainerMock - effectivePriceTotalData:', effectivePriceTotalData);
// Mock Mode: 기본 상품 정보 // Mock Mode: 기본 상품 정보
const productList = { const productList = {
auctProdYn: 'N', auctProdYn: 'N',
@@ -37,38 +71,38 @@ export default function SummaryContainerMock({
() => [ () => [
{ {
name: 'Items', name: 'Items',
value: formatCurrencyValue(effectivePriceTotalData.totProdPrc, currSign, currSignLoc), value: formatCurrencyValue(effectivePriceTotalData.totProdPrc, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
}, },
{ {
name: 'Your Coupon Savings', name: 'Your Coupon Savings',
value: effectivePriceTotalData.totDcAmt value: effectivePriceTotalData.totDcAmt
? formatCurrencyValue(effectivePriceTotalData.totDcAmt, currSign, currSignLoc, true) ? formatCurrencyValue(effectivePriceTotalData.totDcAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc, true)
: '-', : '-',
}, },
{ {
name: 'Shipping & Handling', name: 'Shipping & Handling',
value: formatCurrencyValue(effectivePriceTotalData.totDlvrAmt, currSign, currSignLoc), value: formatCurrencyValue(effectivePriceTotalData.totDlvrAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
}, },
{ {
name: 'TOTAL (before Tax)', name: 'TOTAL (before Tax)',
value: formatCurrencyValue(effectivePriceTotalData.ordPmtNoTaxAmt, currSign, currSignLoc), value: formatCurrencyValue(effectivePriceTotalData.ordPmtNoTaxAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc),
}, },
{ {
name: 'Estimated Sales Tax', 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(() => { const handleClickOrder = useCallback(() => {
console.log('[SummaryContainerMock] Place order clicked'); console.log('[SummaryContainerMock] Place order clicked');
if (doSendLogPaymentEntry) { doSendLogPaymentEntry();
doSendLogPaymentEntry();
}
setPlaceOrderPopup(true); setPlaceOrderPopup(true);
}, [doSendLogPaymentEntry, setPlaceOrderPopup]); }, [doSendLogPaymentEntry, setPlaceOrderPopup]);
console.log('[CheckOutPanel] SummaryContainerMock - items:', items);
const renderItemList = useCallback( const renderItemList = useCallback(
() => () =>
items.map((item, index) => ( items.map((item, index) => (
@@ -100,8 +134,8 @@ export default function SummaryContainerMock({
const estimatedTotal = useMemo(() => { const estimatedTotal = useMemo(() => {
console.log('[CheckOutPanel] SummaryContainerMock - estimatedTotal useMemo'); console.log('[CheckOutPanel] SummaryContainerMock - estimatedTotal useMemo');
return formatCurrencyValue(effectivePriceTotalData.ordPmtReqAmt, currSign, currSignLoc); return formatCurrencyValue(effectivePriceTotalData.ordPmtReqAmt, orderSummaryData.currency.currSign, orderSummaryData.currency.currSignLoc);
}, [effectivePriceTotalData, currSign, currSignLoc]); }, [effectivePriceTotalData, orderSummaryData.currency]);
const showAuctionNotice = productList?.auctProdYn === 'Y' && !productList.auctFinalPriceChgDt; const showAuctionNotice = productList?.auctProdYn === 'Y' && !productList.auctFinalPriceChgDt;

View File

@@ -533,6 +533,25 @@
// 배경색과 라운드는 FavoriteBtn 내부에서 처리하므로 제거 // 배경색과 라운드는 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 참고) // 액션 버튼들 (actionButtons 참고)
.actionButtonsWrapper { .actionButtonsWrapper {
width: 100%; width: 100%;

View File

@@ -110,23 +110,30 @@ const BuyOption = ({
// Mock Mode에서 옵션 데이터 처리 // Mock Mode에서 옵션 데이터 처리
const productOptionInfos = useMemo(() => { const productOptionInfos = useMemo(() => {
console.log('[BuyOption] productOptionInfos useMemo - isMockMode:', BUYNOW_CONFIG.isMockMode());
console.log('[BuyOption] productOptionInfos useMemo - reduxProductOptionInfos:', reduxProductOptionInfos);
// API Mode: 기존 로직 100% 유지 // API Mode: 기존 로직 100% 유지
if (!BUYNOW_CONFIG.isMockMode()) { if (!BUYNOW_CONFIG.isMockMode()) {
console.log('[BuyOption] API Mode - using reduxProductOptionInfos');
return reduxProductOptionInfos; return reduxProductOptionInfos;
} }
// Mock Mode: 옵션 데이터가 없면 Mock 데이터 생성 // Mock Mode: 옵션 데이터가 없거나 배열이 아니면 Mock 데이터 생성
if (!reduxProductOptionInfos || reduxProductOptionInfos.length === 0) { 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); const mockOptionData = createMockProductOptionData(productData);
// Mock 옵션 데이터 배열 반환 (기존 구조와 호환) console.log('[BuyOption] Mock Mode - createMockProductOptionData result:', mockOptionData);
return mockOptionData?.optionList || []; // Mock 옵션 데이터는 이미 배열 구조로 반환됨
return mockOptionData || [];
} }
// Mock Mode이고 옵션 데이터가 있으면 그대로 사용 // Mock Mode이고 유효한 옵션 데이터가 있으면 그대로 사용
console.log('[BuyOption] Mock Mode - using existing valid reduxProductOptionInfos');
return reduxProductOptionInfos; return reduxProductOptionInfos;
}, [reduxProductOptionInfos, productData]); }, [reduxProductOptionInfos, productData]); // logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용)
// logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용)
const logInfo = useMemo(() => { const logInfo = useMemo(() => {
if (productData) { if (productData) {
// productData가 있으면 SingleOption과 동일하게 처리 // productData가 있으면 SingleOption과 동일하게 처리
@@ -222,17 +229,50 @@ const BuyOption = ({
}, [selectedIndex, productOptionInfos, type]); }, [selectedIndex, productOptionInfos, type]);
// 옵션 자동 선택 로직 (SingleOption과 동일) // 옵션 자동 선택 로직 (SingleOption과 동일)
// Mock Mode: 항상 첫 번째 옵션을 자동으로 선택
useEffect(() => { useEffect(() => {
if ( console.log('[BuyOption] autoSelect useEffect - productOptionInfos:', productOptionInfos);
productOptionInfos && console.log('[BuyOption] autoSelect useEffect - selectedBtnOptIdx:', selectedBtnOptIdx);
selectedBtnOptIdx >= 0 && console.log('[BuyOption] autoSelect useEffect - isOptionValue:', isOptionValue);
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length === 1 &&
!isOptionValue if (!productOptionInfos || productOptionInfos.length === 0) {
) { console.log('[BuyOption] autoSelect - productOptionInfos is empty, returning');
setSelectedOptions(productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl[0]); 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); 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과 동일) // 필수 데이터 로드 (SingleOption과 동일)
// Mock Mode: API 호출 스킵 (Mock 데이터만 사용) // Mock Mode: API 호출 스킵 (Mock 데이터만 사용)
@@ -320,17 +360,17 @@ const BuyOption = ({
} }
} else if (response.retCode === 1001) { } else if (response.retCode === 1001) {
dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup)); dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup));
dispatch(changeAppStatus({ isLoading: false })); dispatch(changeAppStatus({ isLoading: false }));
} else { } else {
dispatch( dispatch(
showError( showError(
response.retCode, response.retCode,
response.retMsg, response.retMsg,
false, false,
response.retDetailCode, response.retDetailCode,
response.returnBindStrings response.returnBindStrings
) )
); );
dispatch(changeAppStatus({ isLoading: false })); dispatch(changeAppStatus({ isLoading: false }));
return; return;
} }
@@ -369,18 +409,18 @@ const BuyOption = ({
productOptionInfos.length > 1 && productOptionInfos.length > 1 &&
productInfo?.optProdYn === 'Y' productInfo?.optProdYn === 'Y'
) { ) {
if (selectFirstOptionIndex === 0) { if (selectFirstOptionIndex === 0) {
dispatch( dispatch(
showError( showError(
null, null,
"PLEASE SELECT OPTION", "PLEASE SELECT OPTION",
false, false,
null, null,
null null
) )
); );
return; return;
} }
} }
@@ -389,18 +429,18 @@ const BuyOption = ({
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 && productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 &&
productInfo?.optProdYn === 'Y' productInfo?.optProdYn === 'Y'
) { ) {
if (selectSecondOptionIndex === 0) { if (selectSecondOptionIndex === 0) {
dispatch( dispatch(
showError( showError(
null, null,
"PLEASE SELECT OPTION", "PLEASE SELECT OPTION",
false, false,
null, null,
null null
) )
); );
return; return;
} }
} }
@@ -408,10 +448,27 @@ const BuyOption = ({
if (userNumber && selectedPatnrId && selectedPrdtId && quantity) { if (userNumber && selectedPatnrId && selectedPrdtId && quantity) {
const { prodOptCval, priceInfo } = selectedOptions || {}; const { prodOptCval, priceInfo } = selectedOptions || {};
const { patncNm, brndNm, catNm, prdtNm, prdtId } = productInfo; const { patncNm, brndNm, catNm, prdtNm, prdtId } = productInfo;
const regularPrice = priceInfo?.split('|')[0]; console.log('[BuyOption] handleClickOrder - selectedOptions:', selectedOptions);
const discountPrice = priceInfo?.split('|')[1]; 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]; 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( dispatch(
sendLogTotalRecommend({ sendLogTotalRecommend({
nowMenu: nowMenu, nowMenu: nowMenu,
@@ -452,13 +509,44 @@ const BuyOption = ({
); );
} else { } else {
// Mock Mode: 체크아웃 페이지로 이동 (시뮬레이션) // Mock Mode: 체크아웃 페이지로 이동 (시뮬레이션)
// panelInfo의 logInfo에 선택한 상품 정보를 포함시켜 CheckOutPanel에서 사용
console.log('[BuyOption] Mock Mode - Simulating checkout'); console.log('[BuyOption] Mock Mode - Simulating checkout');
console.log('[BuyOption] logInfo:', logInfo); console.log('[BuyOption] logInfo:', logInfo);
console.log('[BuyOption] Dispatching pushPanel to CHECKOUT_PANEL'); console.log('[BuyOption] Dispatching pushPanel to CHECKOUT_PANEL');
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
dispatch(finishMediaPreview()); 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( dispatch(
pushPanel({ pushPanel({
name: Config.panel_names.CHECKOUT_PANEL, name: Config.panel_names.CHECKOUT_PANEL,
@@ -673,6 +761,11 @@ const BuyOption = ({
setFavoriteFlag(ev); setFavoriteFlag(ev);
}, []); }, []);
// 구매창이 뜰 때 콘솔 로그 출력 (상품 정보 포함 태그 [BuyOption] 붙임)
useEffect(() => {
console.log('[BuyOption]', '상품 정보:', JSON.stringify(productInfo));
}, []); // 컴포넌트 마운트 시 한 번만 출력
// hasOnClose 로직 (SingleOption과 동일) // hasOnClose 로직 (SingleOption과 동일)
const hasOnClose = useMemo(() => { const hasOnClose = useMemo(() => {
if (productOptionInfos && productOptionInfos.length > 0) { if (productOptionInfos && productOptionInfos.length > 0) {

View File

@@ -10,6 +10,7 @@ import {
} from 'react-redux'; } from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable';
import { import {
setHidePopup, setHidePopup,
@@ -28,6 +29,8 @@ import * as Config from '../../../utils/Config';
import { $L } from '../../../utils/helperMethods'; import { $L } from '../../../utils/helperMethods';
import css from './FavoriteBtn.module.less'; import css from './FavoriteBtn.module.less';
const SpottableDiv = Spottable('div');
export default function FavoriteBtn({ export default function FavoriteBtn({
selectedPatnrId, selectedPatnrId,
selectedPrdtId, selectedPrdtId,
@@ -80,7 +83,7 @@ export default function FavoriteBtn({
}, [dispatch, favoriteFlag, onFavoriteFlagChanged]); }, [dispatch, favoriteFlag, onFavoriteFlagChanged]);
return ( return (
<div <SpottableDiv
className={classNames( className={classNames(
css.favorBtnContainer, css.favorBtnContainer,
kind === "item_detail" ? css.smallSize : "" kind === "item_detail" ? css.smallSize : ""
@@ -110,6 +113,6 @@ export default function FavoriteBtn({
onClick={PopUpOnClick} onClick={PopUpOnClick}
/> />
)} )}
</div> </SpottableDiv>
); );
} }

View File

@@ -22,8 +22,7 @@
.favorUnableBtn { .favorUnableBtn {
min-width: 60px; min-width: 60px;
height: 60px; height: 60px;
background-image: url(../../../../assets/images/icons/ic-heart-nor@3x.png); background-color: rgba(68, 68, 68, 0.5); // 다른 버튼들과 동일한 배경색
background-color: #4f172c; // 색상 추가.
.imgElement(54px, 54px, center, center); .imgElement(54px, 54px, center, center);
} }
} }

View File

@@ -10,7 +10,12 @@ const DEBUG_MODE = true;
class ErrorBoundary extends Component { class ErrorBoundary extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { hasError: false }; this.state = {
hasError: false,
error: null,
errorInfo: null,
errorTimestamp: null,
};
} }
static getDerivedStateFromError(error) { static getDerivedStateFromError(error) {
@@ -18,11 +23,43 @@ class ErrorBoundary extends Component {
} }
componentDidCatch(error, errorInfo) { 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) { if (DEBUG_MODE) {
// Development mode: log error details instead of reloading // Development mode: log detailed error information
console.error("Error Stack:", error.stack); console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.error("Component Stack:", errorInfo.componentStack); 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) { componentDidUpdate(prevProps, prevState) {
@@ -37,32 +74,221 @@ class ErrorBoundary extends Component {
render() { render() {
if (this.state.hasError) { 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) { if (DEBUG_MODE) {
const { error, errorInfo, errorTimestamp } = this.state;
return ( return (
<div style={{ <div style={{
padding: '20px', display: 'flex',
flexDirection: 'column',
width: '100vw',
height: '100vh',
backgroundColor: '#f8d7da', backgroundColor: '#f8d7da',
color: '#721c24', color: '#721c24',
borderRadius: '4px', overflow: 'hidden',
margin: '20px', position: 'fixed',
fontFamily: 'monospace', top: 0,
whiteSpace: 'pre-wrap', left: 0,
overflow: 'auto' zIndex: 999999,
pointerEvents: 'auto'
}}> }}>
<h2> Error Caught by ErrorBoundary (DEBUG_MODE)</h2> {/* Header - Fixed */}
<p>Check browser console for detailed error information.</p> <div style={{
<button onClick={() => window.location.reload()} style={{ flexShrink: 0,
padding: '10px 20px', padding: '30px',
marginTop: '10px', borderBottom: '4px solid #721c24',
cursor: 'pointer', backgroundColor: '#f8d7da',
backgroundColor: '#721c24', pointerEvents: 'auto'
color: 'white',
border: 'none',
borderRadius: '4px'
}}> }}>
Reload Page <h1 style={{ margin: '0', fontSize: '48px', pointerEvents: 'auto' }}>
</button> 🚨 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> </div>
); );
} }