[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:
2025-11-02 11:19:42 +09:00
parent 0f755cac53
commit e3c7ff18d3
5 changed files with 213 additions and 82 deletions

View File

@@ -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 {
// 새 상품 추가

View File

@@ -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 고품질 이미지)

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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(() => {