[251102] fix: CartPanel mock-3
🕐 커밋 시간: 2025. 11. 02. 11:19:39 📊 변경 통계: • 총 파일: 5개 • 추가: +215줄 • 삭제: -84줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/reducers/mockCartReducer.js ~ com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js ~ com.twin.app.shoptime/src/views/CartPanel/CartPanel.jsx ~ com.twin.app.shoptime/src/views/CartPanel/CartProduct.jsx ~ com.twin.app.shoptime/src/views/CartPanel/CartSidebar.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • 대규모 기능 개발
This commit is contained in:
@@ -140,11 +140,17 @@ export const mockCartReducer = (state = loadFromLocalStorage(), action) => {
|
||||
|
||||
let updatedItems;
|
||||
if (isDuplicate) {
|
||||
// 중복 상품이면 수량 증가
|
||||
// 중복 상품이면 수량 증가 (BuyOption에서 전달된 수량 존중)
|
||||
updatedItems = [...currentItems];
|
||||
const currentQty = updatedItems[index].prodQty || updatedItems[index].qty || 1;
|
||||
const newQty = item.prodQty || item.qty || 1;
|
||||
|
||||
console.log('[MockCartReducer] Quantity update - Current:', currentQty, 'Adding:', newQty);
|
||||
|
||||
updatedItems[index] = {
|
||||
...updatedItems[index],
|
||||
prodQty: (updatedItems[index].prodQty || 1) + (item.prodQty || 1)
|
||||
prodQty: currentQty + newQty,
|
||||
qty: currentQty + newQty // qty 필드도 동기화
|
||||
};
|
||||
} else {
|
||||
// 새 상품 추가
|
||||
|
||||
@@ -79,6 +79,7 @@ export const createMockCartData = (productData, optionInfo = {}, quantity = 1) =
|
||||
patnrId: productData.patnrId,
|
||||
patncNm: productData.patncNm,
|
||||
qty: quantity,
|
||||
prodQty: quantity, // prodQty 필드도 추가 (동기화용)
|
||||
price: productData.prdtPrice || 0,
|
||||
|
||||
// ✅ 모든 이미지 필드 보존 (CartProduct 호환성 유지 + ProductAllSection 고품질 이미지)
|
||||
|
||||
@@ -45,10 +45,14 @@ export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }
|
||||
return isMockMode ? mockCartData : cartData;
|
||||
}, [isMockMode, mockCartData, cartData]);
|
||||
|
||||
// PlayerPanel/MediaPanel 충돌 방지 로직
|
||||
// PlayerPanel/MediaPanel 충돌 방지 로직 (DEBUG_LOG가 true일 때만 로깅)
|
||||
useEffect(() => {
|
||||
console.log('[CartPanel] Component mounted - checking for panel conflicts');
|
||||
console.log('[CartPanel] Current panels:', panels?.map((p) => ({ name: p.name, hasModal: !!p.panelInfo?.modal })));
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] Component mounted - checking for panel conflicts');
|
||||
console.log('[CartPanel] Current panels:', panels?.map((p) => ({ name: p.name, hasModal: !!p.panelInfo?.modal })));
|
||||
}
|
||||
|
||||
// PlayerPanel 충돌 방지: PlayerPanel이 있고 modal 상태면 비활성화
|
||||
const playerPanelIndex = panels?.findIndex(p =>
|
||||
@@ -58,12 +62,15 @@ export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }
|
||||
);
|
||||
|
||||
if (playerPanelIndex >= 0) {
|
||||
console.log('[CartPanel] 🚨 PlayerPanel/MediaPanel detected at index:', playerPanelIndex);
|
||||
console.log('[CartPanel] PlayerPanel info:', panels[playerPanelIndex]);
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] 🚨 PlayerPanel/MediaPanel detected at index:', playerPanelIndex);
|
||||
}
|
||||
|
||||
// PlayerPanel/MediaPanel 상태를 비활성화하여 CartPanel과의 충돌 방지
|
||||
if (panels[playerPanelIndex].panelInfo?.modal) {
|
||||
console.log('[CartPanel] 🔄 Disabling modal PlayerPanel to prevent conflicts');
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] 🔄 Disabling modal PlayerPanel to prevent conflicts');
|
||||
}
|
||||
// 필요하다면 여기서 PlayerPanel 상태를 비활성화하는 액션을 디스패치할 수 있음
|
||||
// dispatch(updatePanel({
|
||||
// name: panels[playerPanelIndex].name,
|
||||
@@ -73,7 +80,9 @@ export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }
|
||||
}
|
||||
|
||||
return () => {
|
||||
console.log('[CartPanel] 🔄 Component unmounting - cleaning up');
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] 🔄 Component unmounting - cleaning up');
|
||||
}
|
||||
};
|
||||
}, [panels]);
|
||||
|
||||
@@ -81,6 +90,12 @@ export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }
|
||||
dispatch(popPanel());
|
||||
}, [dispatch]);
|
||||
|
||||
// 최적화된 렌더링 방지 함수
|
||||
const shouldRerender = useCallback((prevProps, nextProps) => {
|
||||
// cartInfo가 변경되지 않았으면 리렌더링 방지
|
||||
return JSON.stringify(prevProps.cartInfo) === JSON.stringify(nextProps.cartInfo);
|
||||
}, []);
|
||||
|
||||
// 장바구니 데이터 로드
|
||||
useEffect(() => {
|
||||
console.log('[CartPanel] Component mounted - isMockMode:', isMockMode, 'panelInfo:', panelInfo);
|
||||
@@ -114,15 +129,21 @@ export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }
|
||||
}
|
||||
}, [dispatch, userNumber, isMockMode, panelInfo]);
|
||||
|
||||
// Mock 장바구니 데이터 변경 감지 (디버깅용)
|
||||
// Mock 장바구니 데이터 변경 감지 (디버깅용 - DEBUG_LOG가 true일 때만)
|
||||
useEffect(() => {
|
||||
console.log('[CartPanel] mockCartData changed:', mockCartData?.length, 'items');
|
||||
console.log('[CartPanel] mockCartData:', JSON.stringify(mockCartData));
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] mockCartData changed:', mockCartData?.length, 'items');
|
||||
console.log('[CartPanel] mockCartData:', JSON.stringify(mockCartData));
|
||||
}
|
||||
}, [mockCartData]);
|
||||
|
||||
// displayCartData 변경 감지 (디버깅용)
|
||||
// displayCartData 변경 감지 (디버깅용 - DEBUG_LOG가 true일 때만)
|
||||
useEffect(() => {
|
||||
console.log('[CartPanel] displayCartData changed:', displayCartData?.length, 'items');
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartPanel] displayCartData changed:', displayCartData?.length, 'items');
|
||||
}
|
||||
}, [displayCartData]);
|
||||
|
||||
const {
|
||||
|
||||
@@ -10,6 +10,52 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { updateSelectedItems } from '../../actions/mockCartActions';
|
||||
|
||||
// Debounce 유틸리티 함수
|
||||
const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
// 간단한 이미지 최적화 컴포넌트
|
||||
const OptimizedImage = ({ src, alt, className, fallbackSrc }) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
// 이미지 로드 핸들러
|
||||
const handleLoad = () => {
|
||||
setIsLoaded(true);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setHasError(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={{ backgroundColor: '#f5f5f5' }}>
|
||||
<img
|
||||
src={hasError ? fallbackSrc : src}
|
||||
alt={alt}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
opacity: isLoaded ? 1 : 0.7,
|
||||
transition: 'opacity 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
@@ -65,8 +111,11 @@ const CartProduct = ({ cartInfo }) => {
|
||||
}, [cartData]);
|
||||
|
||||
useEffect(()=>{
|
||||
console.log("###groupedCartData",groupedCartData);
|
||||
},[groupedCartData])
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
if (DEBUG_LOG) {
|
||||
console.log("###groupedCartData", groupedCartData);
|
||||
}
|
||||
}, [groupedCartData])
|
||||
|
||||
// 파트너사별 총합 계산
|
||||
const calculatePartnerTotal = (items) => {
|
||||
@@ -132,21 +181,41 @@ const CartProduct = ({ cartInfo }) => {
|
||||
}
|
||||
}, [dispatch, isMockMode]);
|
||||
|
||||
// 체크박스 선택 핸들러
|
||||
const handleCheckboxChange = useCallback((prodSno, isChecked) => {
|
||||
if (isMockMode) {
|
||||
let newSelectedItems;
|
||||
if (isChecked) {
|
||||
// 상품 선택
|
||||
newSelectedItems = [...selectedItems, prodSno];
|
||||
} else {
|
||||
// 상품 선택 해제
|
||||
newSelectedItems = selectedItems.filter(id => id !== prodSno);
|
||||
}
|
||||
// 체크박스 선택 핸들러 (TCheckBoxSquare onToggle 형식에 맞춤) - debounce 적용
|
||||
const debouncedUpdateSelectedItems = useCallback(
|
||||
debounce((newSelectedItems) => {
|
||||
dispatch(updateSelectedItems(newSelectedItems));
|
||||
console.log('[CartProduct] Checkbox changed - prodSno:', prodSno, 'isChecked:', isChecked, 'selectedItems:', newSelectedItems);
|
||||
}
|
||||
}, [dispatch, isMockMode, selectedItems]);
|
||||
}, 100), // 100ms debounce
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleCheckboxToggle = useCallback((prodSno) => {
|
||||
return ({ selected: isChecked }) => {
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartProduct] handleCheckboxToggle called - prodSno:', prodSno, 'selected:', isChecked);
|
||||
}
|
||||
|
||||
if (isMockMode) {
|
||||
let newSelectedItems;
|
||||
if (isChecked) {
|
||||
// 상품 선택
|
||||
newSelectedItems = [...selectedItems, prodSno];
|
||||
} else {
|
||||
// 상품 선택 해제
|
||||
newSelectedItems = selectedItems.filter(id => id !== prodSno);
|
||||
}
|
||||
|
||||
// debounced 호출 사용
|
||||
debouncedUpdateSelectedItems(newSelectedItems);
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartProduct] Checkbox toggled - prodSno:', prodSno, 'isChecked:', isChecked, 'selectedItems:', newSelectedItems);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [dispatch, isMockMode, selectedItems, debouncedUpdateSelectedItems]);
|
||||
|
||||
// 상품이 선택되었는지 확인
|
||||
const isItemSelected = useCallback((prodSno) => {
|
||||
@@ -221,8 +290,8 @@ const CartProduct = ({ cartInfo }) => {
|
||||
<TCheckBoxSquare
|
||||
className={css.customeCheckbox}
|
||||
spotlightId={`productCheckbox-${item.prodSno}`}
|
||||
checked={isItemSelected(item.prodSno)}
|
||||
onChange={(isChecked) => handleCheckboxChange(item.prodSno, isChecked)}
|
||||
selected={isItemSelected(item.prodSno)}
|
||||
onToggle={handleCheckboxToggle(item.prodSno)}
|
||||
/>
|
||||
<span className={css.productId}>
|
||||
ID : {item.prdtId}
|
||||
@@ -230,7 +299,7 @@ const CartProduct = ({ cartInfo }) => {
|
||||
</div>
|
||||
<div className={css.productInfo}>
|
||||
<div className={css.leftSection}>
|
||||
<CustomImage
|
||||
<OptimizedImage
|
||||
className={css.productImage}
|
||||
src={imageSrc}
|
||||
fallbackSrc={defaultImage}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { memo } from 'react';
|
||||
|
||||
import TButton from '../../components/TButton/TButton';
|
||||
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
|
||||
@@ -23,93 +24,126 @@ const CartSidebar = ({ cartInfo }) => {
|
||||
|
||||
// 선택된 상품들만 필터링 - 항상 선택된 상품들만 반환
|
||||
const getSelectedItems = useCallback((items) => {
|
||||
console.log('[CartSidebar] getSelectedItems called - isMockMode:', isMockMode, 'selectedItems:', selectedItems);
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] getSelectedItems called - isMockMode:', isMockMode, 'selectedItems:', selectedItems);
|
||||
}
|
||||
|
||||
if (!items || !Array.isArray(items)) {
|
||||
console.log('[CartSidebar] No items provided, returning empty array');
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] No items provided, returning empty array');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isMockMode) {
|
||||
console.log('[CartSidebar] API Mode - returning all items');
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] API Mode - returning all items');
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
console.log('[CartSidebar] No items selected, returning empty array');
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] No items selected, returning empty array');
|
||||
}
|
||||
return []; // 선택된 상품이 없으면 빈 배열 반환
|
||||
}
|
||||
|
||||
const filtered = items.filter(item => {
|
||||
const itemId = item.prodSno || item.cartId;
|
||||
const isSelected = selectedItems.includes(itemId);
|
||||
console.log('[CartSidebar] Item filter:', {
|
||||
itemName: item.prdtNm,
|
||||
itemId,
|
||||
isSelected
|
||||
});
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] Item filter:', {
|
||||
itemName: item.prdtNm,
|
||||
itemId,
|
||||
isSelected
|
||||
});
|
||||
}
|
||||
return isSelected;
|
||||
});
|
||||
|
||||
console.log('[CartSidebar] Filtered selected items:', filtered.length, 'out of', items.length);
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] Filtered selected items:', filtered.length, 'out of', items.length);
|
||||
}
|
||||
return filtered;
|
||||
}, [isMockMode, selectedItems]);
|
||||
|
||||
// Mock 데이터 또는 실제 데이터 계산 (선택된 상품만) - CheckOutPanel 방식 적용
|
||||
// 개별 상품 가격 캐싱 (성능 최적화)
|
||||
const itemPriceCache = useMemo(() => {
|
||||
const cache = new Map();
|
||||
|
||||
if (isMockMode && displayCartInfo) {
|
||||
displayCartInfo.forEach(item => {
|
||||
if (!cache.has(item.prodSno || item.cartId)) {
|
||||
const orderSummary = calculateOrderSummaryFromProductInfo(item);
|
||||
cache.set(item.prodSno || item.cartId, {
|
||||
price: orderSummary.items,
|
||||
coupon: orderSummary.couponSavings,
|
||||
shipping: orderSummary.shipping
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return cache;
|
||||
}, [isMockMode, displayCartInfo]);
|
||||
|
||||
// Mock 데이터 또는 실제 데이터 계산 (선택된 상품만) - 최적화 버전
|
||||
const calculatedData = useMemo(() => {
|
||||
const DEBUG_LOG = false; // 수동 설정: false로 비활성화
|
||||
|
||||
if (isMockMode) {
|
||||
// Mock Mode: 선택된 상품들로 개별 가격 계산
|
||||
// Mock Mode: 선택된 상품들로 개별 가격 계산 (캐시 사용)
|
||||
if (displayCartInfo && Array.isArray(displayCartInfo) && displayCartInfo.length > 0) {
|
||||
const selectedCartItems = getSelectedItems(displayCartInfo);
|
||||
console.log('[CartSidebar] Selected items for calculation:', selectedCartItems);
|
||||
|
||||
// CheckOutPanel 방식: 각 상품의 정확한 가격 추출
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] Selected items for calculation:', selectedCartItems);
|
||||
}
|
||||
|
||||
// 캐시된 가격 정보 사용
|
||||
let totalItems = 0;
|
||||
let totalCoupon = 0;
|
||||
let totalShipping = 0;
|
||||
let totalQuantity = 0;
|
||||
|
||||
selectedCartItems.forEach((item) => {
|
||||
console.log('[CartSidebar] Processing item:', item.prdtNm, 'price fields:', {
|
||||
price2: item.price2,
|
||||
price3: item.price3,
|
||||
price5: item.price5,
|
||||
finalPrice: item.finalPrice,
|
||||
discountPrice: item.discountPrice,
|
||||
});
|
||||
const itemId = item.prodSno || item.cartId;
|
||||
const cachedPrice = itemPriceCache.get(itemId);
|
||||
|
||||
// CheckOutPanel의 calculateOrderSummaryFromProductInfo 로직 사용
|
||||
const orderSummary = calculateOrderSummaryFromProductInfo(item);
|
||||
if (cachedPrice) {
|
||||
const qty = item.prodQty || item.qty || 1;
|
||||
totalItems += cachedPrice.price * qty;
|
||||
totalCoupon += cachedPrice.coupon * qty;
|
||||
totalShipping += cachedPrice.shipping * qty;
|
||||
totalQuantity += qty;
|
||||
|
||||
const qty = item.prodQty || item.qty || 1;
|
||||
const itemPrice = orderSummary.items; // 개별 상품 가격
|
||||
const itemCoupon = orderSummary.couponSavings; // 개별 상품 쿠폰
|
||||
const itemShipping = orderSummary.shipping; // 개별 상품 배송비
|
||||
|
||||
totalItems += itemPrice * qty;
|
||||
totalCoupon += itemCoupon * qty;
|
||||
totalShipping += itemShipping * qty; // 배송비도 수량만큼 계산
|
||||
totalQuantity += qty;
|
||||
|
||||
console.log('[CartSidebar] Item calculation:', {
|
||||
name: item.prdtNm,
|
||||
qty,
|
||||
itemPrice,
|
||||
itemCoupon,
|
||||
itemShipping,
|
||||
runningTotal: totalItems
|
||||
});
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] Item calculation (cached):', {
|
||||
name: item.prdtNm,
|
||||
qty,
|
||||
itemPrice: cachedPrice.price,
|
||||
itemCoupon: cachedPrice.coupon,
|
||||
itemShipping: cachedPrice.shipping,
|
||||
runningTotal: totalItems
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const subtotal = Math.max(0, totalItems - totalCoupon + totalShipping);
|
||||
|
||||
console.log('[CartSidebar] Final calculation for selected items:', {
|
||||
totalQuantity,
|
||||
totalItems,
|
||||
totalCoupon,
|
||||
totalShipping,
|
||||
subtotal,
|
||||
});
|
||||
if (DEBUG_LOG) {
|
||||
console.log('[CartSidebar] Final calculation for selected items:', {
|
||||
totalQuantity,
|
||||
totalItems,
|
||||
totalCoupon,
|
||||
totalShipping,
|
||||
subtotal,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
itemCount: totalQuantity,
|
||||
@@ -160,7 +194,7 @@ const CartSidebar = ({ cartInfo }) => {
|
||||
orderTotalBeforeTax: 0,
|
||||
};
|
||||
}
|
||||
}, [isMockMode, displayCartInfo, getSelectedItems]);
|
||||
}, [isMockMode, displayCartInfo, getSelectedItems, itemPriceCache]);
|
||||
|
||||
// 체크아웃 버튼 클릭 핸들러
|
||||
const handleCheckoutClick = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user