[251102] fix: CartPanel mock-1

🕐 커밋 시간: 2025. 11. 02. 08:49:45

📊 변경 통계:
  • 총 파일: 12개
  • 추가: +686줄
  • 삭제: -88줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/reducers/mockCartReducer.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx
  ~ com.twin.app.shoptime/src/store/store.js
  ~ com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js
  ~ com.twin.app.shoptime/src/utils/Config.js
  ~ com.twin.app.shoptime/src/utils/mockDataSafetyUtils.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/CartProductBar.jsx
  ~ com.twin.app.shoptime/src/views/CartPanel/CartSidebar.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 대규모 기능 개발
  • 모듈 구조 개선
This commit is contained in:
2025-11-02 08:49:51 +09:00
parent dda368ab65
commit efeb45823e
12 changed files with 964 additions and 94 deletions

View File

@@ -22,6 +22,7 @@ export default function TReactPlayer({
mediaEventsMap = handledMediaEventsMap,
videoRef,
url,
dispatch,
...rest
}) {
const playerRef = useRef(null);

View File

@@ -0,0 +1,272 @@
import { MOCK_CART_TYPES } from '../actions/mockCartActions';
// 브라우저 환경 확인
const isBrowser = typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
/**
* Mock Cart Reducer 초기 상태
* 실제 cartReducer와 유사한 구조 유지
*/
const initialState = {
// Mock 장바구니 목록
cartInfo: [],
// 마지막 실행된 액션 정보
lastAction: null,
// 에러 정보
error: null,
// 총 상품 수량
totalQuantity: 0,
// 총 가격
totalPrice: 0,
// 마지막 업데이트 시간
lastUpdated: null,
};
// localStorage 관련 유틸리티
const MOCK_CART_STORAGE_KEY = 'mockCartData';
/**
* Mock 장바구니 데이터를 localStorage에 저장
*/
const saveToLocalStorage = (state) => {
if (!isBrowser) return;
try {
const dataToSave = {
cartInfo: state.cartInfo,
totalQuantity: state.totalQuantity,
totalPrice: state.totalPrice,
lastUpdated: state.lastUpdated,
};
window.localStorage.setItem(MOCK_CART_STORAGE_KEY, JSON.stringify(dataToSave));
} catch (error) {
console.error('[MockCartReducer] localStorage 저장 실패:', error);
}
};
/**
* localStorage에서 Mock 장바구니 데이터 불러오기
*/
const loadFromLocalStorage = () => {
if (!isBrowser) return initialState;
try {
const savedData = window.localStorage.getItem(MOCK_CART_STORAGE_KEY);
if (savedData) {
const parsedData = JSON.parse(savedData);
return {
...initialState,
cartInfo: parsedData.cartInfo || [],
totalQuantity: parsedData.totalQuantity || 0,
totalPrice: parsedData.totalPrice || 0,
lastUpdated: parsedData.lastUpdated || Date.now(),
};
}
} catch (error) {
console.error('[MockCartReducer] localStorage 로드 실패:', error);
}
return initialState;
};
/**
* 장바구니 아이템 중복 확인
* @param {Array} cartItems - 현재 장바구니 아이템 목록
* @param {Object} newItem - 추가할 상품
* @returns {Object} 중복 여부와 기존 아이템 인덱스
*/
const findDuplicateItem = (cartItems, newItem) => {
const index = cartItems.findIndex(item =>
item.prdtId === newItem.prdtId &&
item.optNm === newItem.optNm
);
return {
isDuplicate: index !== -1,
index
};
};
/**
* 총 수량 및 총 가격 계산
* @param {Array} cartItems - 장바구니 아이템 목록
* @returns {Object} 총 수량과 총 가격
*/
const calculateTotals = (cartItems) => {
const totalQuantity = cartItems.reduce((sum, item) => sum + (item.prodQty || 1), 0);
const totalPrice = cartItems.reduce((sum, item) => {
const itemPrice = parseFloat(item.price3 || item.price2 || 0);
const optionPrice = parseFloat(item.price5 || 0);
const shippingPrice = parseFloat(item.shippingCharge || 0);
return sum + ((itemPrice + optionPrice) * (item.prodQty || 1) + shippingPrice);
}, 0);
return {
totalQuantity,
totalPrice: parseFloat(totalPrice.toFixed(2))
};
};
/**
* Mock Cart Reducer
* Mock Mode에서 장바구니 데이터를 관리합니다.
*/
export const mockCartReducer = (state = loadFromLocalStorage(), action) => {
switch (action.type) {
case MOCK_CART_TYPES.INIT_MOCK_CART: {
const { items = [] } = action.payload;
const newState = {
...state,
cartInfo: items,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(items),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
case MOCK_CART_TYPES.ADD_TO_MOCK_CART: {
const { item } = action.payload;
const currentItems = [...state.cartInfo];
// 중복 상품 확인
const { isDuplicate, index } = findDuplicateItem(currentItems, item);
let updatedItems;
if (isDuplicate) {
// 중복 상품이면 수량 증가
updatedItems = [...currentItems];
updatedItems[index] = {
...updatedItems[index],
prodQty: (updatedItems[index].prodQty || 1) + (item.prodQty || 1)
};
} else {
// 새 상품 추가
updatedItems = [...currentItems, item];
}
const newState = {
...state,
cartInfo: updatedItems,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(updatedItems),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
case MOCK_CART_TYPES.REMOVE_FROM_MOCK_CART: {
const { prodSno } = action.payload;
const updatedItems = state.cartInfo.filter(item => item.prodSno !== prodSno);
const newState = {
...state,
cartInfo: updatedItems,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(updatedItems),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
case MOCK_CART_TYPES.UPDATE_MOCK_CART_ITEM: {
const { prodSno, quantity } = action.payload;
const updatedItems = state.cartInfo.map(item => {
if (item.prodSno === prodSno) {
return {
...item,
prodQty: Math.max(1, quantity) // 최소 1개 보장
};
}
return item;
});
const newState = {
...state,
cartInfo: updatedItems,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(updatedItems),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
case MOCK_CART_TYPES.SET_MOCK_CART_QUANTITY: {
const { prodSno, quantity } = action.payload;
if (quantity <= 0) {
// 수량이 0이면 상품 제거
const updatedItems = state.cartInfo.filter(item => item.prodSno !== prodSno);
const newState = {
...state,
cartInfo: updatedItems,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(updatedItems),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
const updatedItems = state.cartInfo.map(item => {
if (item.prodSno === prodSno) {
return {
...item,
prodQty: quantity
};
}
return item;
});
const newState = {
...state,
cartInfo: updatedItems,
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
...calculateTotals(updatedItems),
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
case MOCK_CART_TYPES.CLEAR_MOCK_CART: {
const newState = {
...state,
cartInfo: [],
lastAction: action.payload.lastAction,
error: null,
lastUpdated: Date.now(),
totalQuantity: 0,
totalPrice: 0,
};
// localStorage에 저장
saveToLocalStorage(newState);
return newState;
}
default:
return state;
}
};

View File

@@ -22,6 +22,7 @@ import { foryouReducer } from '../reducers/forYouReducer';
import { homeReducer } from '../reducers/homeReducer';
import { localSettingsReducer } from '../reducers/localSettingsReducer';
import { mainReducer } from '../reducers/mainReducer';
import { mockCartReducer } from '../reducers/mockCartReducer';
import { myPageReducer } from '../reducers/myPageReducer';
import { onSaleReducer } from '../reducers/onSaleReducer';
import { orderReducer } from '../reducers/orderReducer';
@@ -47,6 +48,7 @@ const rootReducer = combineReducers({
home: homeReducer,
brand: brandReducer,
main: mainReducer,
mockCart: mockCartReducer,
myPage: myPageReducer,
onSale: onSaleReducer,
product: productReducer,

View File

@@ -63,6 +63,15 @@ export const createMockCartData = (productData, optionInfo = {}, quantity = 1) =
return null;
}
// ✅ 이미지 URL 처리 (ProductAllSection의 고품질 이미지 우선)
const imgUrl = productData.imgUrls600?.[0] || // ✅ ProductAllSection의 고품질 이미지
productData.thumbnailUrl960 || // ✅ 960px 썸네일
productData.imgUrl ||
productData.thumbnailUrl ||
productData.imgList?.[0]?.imgUrl ||
productData.imgUrls?.[0]?.imgUrl ||
'/assets/images/img-thumb-empty-144@3x.png';
return {
cartId: `MOCK_CART_${productData.prdtId}_${Date.now()}`,
prdtId: productData.prdtId,
@@ -71,12 +80,258 @@ export const createMockCartData = (productData, optionInfo = {}, quantity = 1) =
patncNm: productData.patncNm,
qty: quantity,
price: productData.prdtPrice || 0,
thumbnailUrl: productData.thumbnailUrl || null,
// ✅ 모든 이미지 필드 보존 (CartProduct 호환성 유지 + ProductAllSection 고품질 이미지)
imgUrl: imgUrl,
thumbnailUrl: productData.thumbnailUrl960 || productData.thumbnailUrl || imgUrl,
thumbnailUrl960: productData.thumbnailUrl960,
imgList: productData.imgList || [{ imgUrl: imgUrl }],
imgUrls: productData.imgUrls || [{ imgUrl: imgUrl }],
imgUrls600: productData.imgUrls600, // ✅ ProductAllSection의 고품질 이미지 배열
optionInfo: optionInfo,
addedAt: new Date().toISOString()
};
};
/**
* Mock Mode에서 초기 Mock 장바구니 목록 생성
*
* @param {Object} productData - 초기 상품 데이터 (可选)
* @param {Object} optionInfo - 옵션 정보 (可选)
* @param {number} quantity - 수량 (可选)
* @returns {Array} Mock 장바구니 데이터 목록
*/
export const createMockCartListData = (productData, optionInfo = {}, quantity = 1) => {
if (!BUYNOW_CONFIG.isMockMode()) {
return [];
}
// ✅ 기본 Mock 장바구니 데이터 (CartPanel 구조에 맞춰 개선 - 모든 이미지 필드 포함)
const defaultMockItems = [
// QVC 상품
{
prodSno: 'MOCK_CART_1',
prdtId: 'MOCK_QVC_001',
prdtNm: 'Mock Premium Wireless Headphones',
patnrId: 'QVC',
patncNm: 'QVC',
patncLogPath: '/assets/images/ic-partners-qvc@3x.png',
imgUrl: '/assets/images/img-thumb-empty-144@3x.png',
thumbnailUrl: '/assets/images/img-thumb-empty-144@3x.png',
imgList: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }],
imgUrls: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }], // CheckOutPanel 호환성
price2: '299.99', // 원가
price3: '199.99', // 할인가
price5: '29.99', // 옵션 할인가
optPrc: '29.99', // 옵션 가격
shippingCharge: '12.99',
prodQty: 1,
optNm: 'Color: Black, Warranty: 2 Years',
addedAt: new Date().toISOString()
},
// HSN 상품
{
prodSno: 'MOCK_CART_2',
prdtId: 'MOCK_HSN_001',
prdtNm: 'Mock Smart Watch Pro',
patnrId: 'HSN',
patncNm: 'HSN',
patncLogPath: '/assets/images/ic-partners-hsn@3x.png',
imgUrl: '/assets/images/img-thumb-empty-144@3x.png',
thumbnailUrl: '/assets/images/img-thumb-empty-144@3x.png',
imgList: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }],
imgUrls: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }], // CheckOutPanel 호환성
price2: '399.99',
price3: '299.99',
price5: '49.99',
optPrc: '49.99',
shippingCharge: '9.99',
prodQty: 2,
optNm: 'Color: Silver, Size: 42mm',
addedAt: new Date().toISOString()
},
// 다른 파트너사 상품
{
prodSno: 'MOCK_CART_3',
prdtId: 'MOCK_EVINE_001',
prdtNm: 'Mock Luxury Skincare Set',
patnrId: 'EVINE',
patncNm: 'EVINE',
patncLogPath: '/assets/images/ic-partners-evine@3x.png',
imgUrl: '/assets/images/img-thumb-empty-144@3x.png',
thumbnailUrl: '/assets/images/img-thumb-empty-144@3x.png',
imgList: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }],
imgUrls: [{ imgUrl: '/assets/images/img-thumb-empty-144@3x.png' }], // CheckOutPanel 호환성
price2: '149.99',
price3: '99.99',
price5: '19.99',
optPrc: '19.99',
shippingCharge: '7.99',
prodQty: 1,
optNm: 'Size: Full Set, Type: Anti-Aging',
addedAt: new Date().toISOString()
}
];
// 초기 상품 데이터가 있는 경우
if (productData) {
const newCartItem = createMockCartData(productData, optionInfo, quantity);
if (newCartItem) {
return [newCartItem, ...defaultMockItems];
}
}
return defaultMockItems;
};
// Mock 장바구니 데이터를 저장할 전역 변수
let mockCartItems = [];
/**
* Mock Mode에 따라 장바구니 데이터 설정
*
* @param {Array} cartData - 장바구니 데이터
*/
export const setMockCartData = (cartData) => {
if (BUYNOW_CONFIG.isMockMode()) {
mockCartItems = cartData || [];
}
};
/**
* 현재 Mock 장바구니 데이터 가져오기
*
* @returns {Array} 현재 Mock 장바구니 데이터
*/
export const getMockCartData = () => {
return BUYNOW_CONFIG.isMockMode() ? mockCartItems : [];
};
/**
* Mock 장바구니에 상품 추가
*
* @param {Object} productData - 상품 데이터
* @param {Object} optionInfo - 옵션 정보
* @param {number} quantity - 수량
* @returns {Object} 추가된 장바구니 아이템
*/
export const addMockCartItem = (productData, optionInfo = {}, quantity = 1) => {
if (!BUYNOW_CONFIG.isMockMode() || !productData) {
return null;
}
// ✅ 이미지 URL 처리 (ProductAllSection의 고품질 이미지 우선)
const imgUrl = productData.imgUrls600?.[0] || // ✅ ProductAllSection의 고품질 이미지
productData.thumbnailUrl960 || // ✅ 960px 썸네일
productData.imgUrl ||
productData.thumbnailUrl ||
productData.imgList?.[0]?.imgUrl ||
productData.imgUrls?.[0]?.imgUrl ||
'/assets/images/img-thumb-empty-144@3x.png';
const cartItem = {
prodSno: `MOCK_CART_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
prdtId: productData.prdtId || 'MOCK_PRODUCT',
prdtNm: productData.prdtNm || 'Mock Product',
patnrId: productData.patnrId || 'MOCK_PARTNER',
patncNm: productData.patncNm || 'Mock Partner',
patncLogPath: productData.patncLogPath || '/assets/images/ic-partners-qvc@3x.png',
// ✅ 이미지 정보 (모든 필드명 지원 - CartProduct/CheckOutPanel 호환성 + ProductAllSection 고품질 이미지)
imgUrl: imgUrl,
thumbnailUrl: productData.thumbnailUrl960 || productData.thumbnailUrl || imgUrl,
thumbnailUrl960: productData.thumbnailUrl960,
imgList: productData.imgList || [{ imgUrl: imgUrl }],
imgUrls: productData.imgUrls || [{ imgUrl: imgUrl }], // CheckOutPanel 호환성
imgUrls600: productData.imgUrls600, // ✅ ProductAllSection의 고품질 이미지 배열
// ✅ 가격 정보 (CartProduct의 calculatePartnerTotal에서 사용하는 필드들 모두 포함)
price2: productData.price2 || productData.originalPrice || '299.99', // 원가
price3: productData.price3 || productData.salePrice || productData.discountPrice || '199.99', // 할인가
price5: optionInfo.price || productData.price5 || '29.99', // 옵션 할인가
optPrc: optionInfo.price || productData.optPrc || productData.price5 || '29.99', // 옵션 가격
// 배송비
shippingCharge: productData.shippingCharge || productData.shippingFee || '12.99',
// 수량
prodQty: quantity || productData.prodQty || 1,
// 옵션명
optNm: optionInfo.name || productData.optNm || '',
// 기타 정보
addedAt: new Date().toISOString(),
soldoutFlag: productData.soldoutFlag || 'N',
};
// 중복 확인
const existingIndex = mockCartItems.findIndex(item =>
item.prdtId === cartItem.prdtId && item.optNm === cartItem.optNm
);
if (existingIndex !== -1) {
// 중복이면 수량 증가
mockCartItems[existingIndex].prodQty += cartItem.prodQty;
return mockCartItems[existingIndex];
} else {
// 새 상품 추가
mockCartItems.push(cartItem);
return cartItem;
}
};
/**
* Mock 장바구니에서 상품 제거
*
* @param {string} prodSno - 상품 고유번호
* @returns {Object|null} 제거된 상품 정보
*/
export const removeMockCartItem = (prodSno) => {
if (!BUYNOW_CONFIG.isMockMode() || !prodSno) {
return null;
}
const index = mockCartItems.findIndex(item => item.prodSno === prodSno);
if (index !== -1) {
const removedItem = mockCartItems.splice(index, 1)[0];
return removedItem;
}
return null;
};
/**
* Mock 장바구니 상품 수량 업데이트
*
* @param {string} prodSno - 상품 고유번호
* @param {number} quantity - 새로운 수량
* @returns {Object|null} 업데이트된 상품 정보
*/
export const updateMockCartItemQuantity = (prodSno, quantity) => {
if (!BUYNOW_CONFIG.isMockMode() || !prodSno || quantity <= 0) {
return null;
}
const item = mockCartItems.find(item => item.prodSno === prodSno);
if (item) {
item.prodQty = quantity;
return { ...item };
}
return null;
};
/**
* Mock 장바구니 초기화
*/
export const clearMockCartData = () => {
if (BUYNOW_CONFIG.isMockMode()) {
mockCartItems = [];
}
};
/**
* Mock Mode에서 가격 정보 포맷팅
* priceInfo는 "원가|할인가" 형식 (예: "100000|80000")

View File

@@ -43,6 +43,19 @@ export const panel_names = {
USER_REVIEW_PANEL: 'userreviewpanel',
};
// 단독으로 렌더링되어야 하는 패널 목록
// 이 패널들은 항상 isOnTop=true로 설정되고 다른 패널들과 함께 표시되지 않음
export const STANDALONE_PANELS = [
panel_names.CHECKOUT_PANEL,
panel_names.CART_PANEL,
// 향후 추가될 다른 단독 패널들 여기에 추가
];
// 단독 패널인지 확인하는 유틸리티 함수
export const isStandalonePanel = (panelName) => {
return STANDALONE_PANELS.includes(panelName);
};
//button
export const TBUTTON_PRESS_DELAY = 100;

View File

@@ -129,33 +129,46 @@ export const getSafeProductOptions = (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];
// 1순위: imgUrls600 배열 (ProductAllSection의 고품질 이미지)
const imgUrls600 = product?.imgUrls600;
if (Array.isArray(imgUrls600) && imgUrls600.length > 0) {
return imgUrls600[0];
}
// 2순위: imgUrl 직접 필드
if (product?.imgUrl) {
return product.imgUrl;
}
// 3순위: thumbnailUrl (productInfo의 썸네일)
if (product?.thumbnailUrl) {
return product.thumbnailUrl;
}
// 4순위: thumbnailUrl960 (productInfo의 960px 썸네일)
// ✅ 2순위: thumbnailUrl960 (ProductAllSection의 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';
// 3순위: imgUrl 직접 필드
if (product?.imgUrl) {
return product.imgUrl;
}
// 6순위: patncLogPath (파트너 로고)
// 4순위: thumbnailUrl (productInfo의 썸네일)
if (product?.thumbnailUrl) {
return product.thumbnailUrl;
}
// 5순위: imgUrls 배열 (productInfo의 imgUrls)
const imgUrls = product?.imgUrls;
if (Array.isArray(imgUrls) && imgUrls.length > 0) {
// imgUrls[0]이 객체면 imgUrl 필드 추출, 문자열이면 직접 사용
return typeof imgUrls[0] === 'string' ? imgUrls[0] : imgUrls[0]?.imgUrl;
}
// 6순위: imgList 배열의 imgUrl 필드
const imgList = product?.imgList;
if (Array.isArray(imgList) && imgList.length > 0) {
return imgList[0]?.imgUrl || '/mock/image.jpg';
}
// 7순위: patncLogPath (파트너 로고)
if (product?.patncLogPath) {
return product.patncLogPath;
}
// 7순위: 기본 이미지
// 8순위: 기본 이미지
return '/mock/image.jpg';
};

View File

@@ -1,6 +1,7 @@
import React, {
useCallback,
useEffect,
useMemo,
} from 'react';
import {
@@ -9,7 +10,10 @@ import {
} from 'react-redux';
import { getMyInfoCartSearch } from '../../actions/cartActions';
import { initializeMockCart, resetMockCart } from '../../actions/mockCartActions';
import { popPanel } from '../../actions/panelActions';
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
import * as Config from '../../utils/Config';
import TBody from '../../components/TBody/TBody';
import THeader from '../../components/THeader/THeader';
import TPanel from '../../components/TPanel/TPanel';
@@ -20,12 +24,58 @@ import css from './CartPanel.module.less';
import CartProductBar from './CartProductBar';
import CartSidebar from './CartSidebar';
export default function CartPanel({ spotlightId, scrollOptions = [] }) {
export default function CartPanel({ spotlightId, scrollOptions = [], panelInfo }) {
const dispatch = useDispatch();
// 실제 장바구니 데이터
const cartData = useSelector((state) => state.cart.getMyinfoCartSearch.cartInfo);
// Mock 장바구니 데이터
const mockCartData = useSelector((state) => state.mockCart.cartInfo);
// 패널 상태 확인 (충돌 방지용)
const panels = useSelector((state) => state.panels.panels);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const dispatch = useDispatch();
// Mock Mode 여부 확인 및 적절한 데이터 선택
const isMockMode = BUYNOW_CONFIG.isMockMode();
const displayCartData = useMemo(() => {
return isMockMode ? mockCartData : cartData;
}, [isMockMode, mockCartData, cartData]);
// PlayerPanel/MediaPanel 충돌 방지 로직
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 })));
// PlayerPanel 충돌 방지: PlayerPanel이 있고 modal 상태면 비활성화
const playerPanelIndex = panels?.findIndex(p =>
p.name === Config.panel_names.PLAYER_PANEL ||
p.name === Config.panel_names.PLAYER_PANEL_NEW ||
p.name === Config.panel_names.MEDIA_PANEL
);
if (playerPanelIndex >= 0) {
console.log('[CartPanel] 🚨 PlayerPanel/MediaPanel detected at index:', playerPanelIndex);
console.log('[CartPanel] PlayerPanel info:', panels[playerPanelIndex]);
// PlayerPanel/MediaPanel 상태를 비활성화하여 CartPanel과의 충돌 방지
if (panels[playerPanelIndex].panelInfo?.modal) {
console.log('[CartPanel] 🔄 Disabling modal PlayerPanel to prevent conflicts');
// 필요하다면 여기서 PlayerPanel 상태를 비활성화하는 액션을 디스패치할 수 있음
// dispatch(updatePanel({
// name: panels[playerPanelIndex].name,
// panelInfo: { ...panels[playerPanelIndex].panelInfo, isActive: false }
// }));
}
}
return () => {
console.log('[CartPanel] 🔄 Component unmounting - cleaning up');
};
}, [panels]);
const onBackClick = useCallback(() => {
dispatch(popPanel());
@@ -33,10 +83,32 @@ export default function CartPanel({ spotlightId, scrollOptions = [] }) {
// 장바구니 데이터 로드
useEffect(() => {
if (userNumber) {
console.log('[CartPanel] Component mounted - isMockMode:', isMockMode, 'panelInfo:', panelInfo);
if (isMockMode) {
// Mock Mode: panelInfo가 있으면 해당 상품 추가, 없으면 기본 Mock 데이터 설정
if (panelInfo?.productInfo) {
console.log('[CartPanel] Mock Mode - Adding product from panelInfo:', panelInfo.productInfo);
console.log('[CartPanel] Mock Mode - Option info:', panelInfo.optionInfo);
console.log('[CartPanel] Mock Mode - Quantity:', panelInfo.quantity);
// panelInfo의 상품 정보로 Mock 장바구니 초기화 (기존 데이터 유지)
dispatch(initializeMockCart(
panelInfo.productInfo,
panelInfo.optionInfo || {},
panelInfo.quantity || 1
));
} else {
// panelInfo가 없으면 기본 Mock 장바구니 데이터로 설정
console.log('[CartPanel] Mock Mode - Initializing with default mock data');
dispatch(resetMockCart());
}
} else if (userNumber) {
// API Mode: 실제 API 호출
console.log('[CartPanel] API Mode - Loading cart from API for user:', userNumber);
dispatch(getMyInfoCartSearch({ mbrNo: userNumber }));
}
}, [dispatch, userNumber]);
}, [dispatch, userNumber, isMockMode, panelInfo]);
const {
getScrollTo,
@@ -56,17 +128,17 @@ export default function CartPanel({ spotlightId, scrollOptions = [] }) {
/>
<div className={css.Wrap}>
<div className={css.leftSection}>
<CartSidebar cartInfo={cartData}/>
<CartSidebar cartInfo={displayCartData}/>
</div>
<div className={css.rightSection} cbScrollTo={getScrollToBody}>
{/* 오른쪽 상품 영역 */}
{cartData && cartData?.length > 0 ? (
{displayCartData && displayCartData?.length > 0 ? (
<TScroller
className={css.tScroller}
scrollTopBody={scrollTopBody}
{...scrollOptions}
>
<CartProductBar cartInfo={cartData} />
<CartProductBar cartInfo={displayCartData} />
</TScroller>
) : (
<CartEmpty />

View File

@@ -6,7 +6,7 @@ import React, {
} from 'react';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
@@ -17,11 +17,25 @@ import defaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
import CustomImage from '../../components/CustomImage/CustomImage';
import TButton from '../../components/TButton/TButton';
import TCheckBoxSquare from '../../components/TCheckBox/TCheckBoxSquare';
import { removeFromCart, updateCartItem } from '../../actions/cartActions';
import { removeFromMockCart, setMockCartItemQuantity } from '../../actions/mockCartActions';
import store from '../../store/store';
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
import { normalizeProductDataForDisplay } from '../../utils/mockDataSafetyUtils';
import css from './CartProduct.module.less';
const Container = SpotlightContainerDecorator("div");
const CartProduct = () => {
const cartData = useSelector((state) => state.cart.getMyinfoCartSearch.cartInfo);
const CartProduct = ({ cartInfo }) => {
const dispatch = useDispatch();
// 항상 호출되어야 하는 Hook들
const fallbackCartData = useSelector((state) => state.cart.getMyinfoCartSearch.cartInfo);
// 실제 장바구니 데이터와 Mock 데이터 중 선택
const cartData = cartInfo || fallbackCartData;
// Mock Mode 확인
const isMockMode = BUYNOW_CONFIG.isMockMode();
//카트 데이타 그룹화 - 수정된 부분
const groupedCartData = useMemo(() => {
@@ -71,23 +85,49 @@ const CartProduct = () => {
};
};
const [pdEa, setPdEa] = useState(1);
// 수량 조절 핸들러
const handleDecreseClick = useCallback((prodSno, currentQty) => {
if (currentQty > 1) {
const newQty = currentQty - 1;
const handleDecreseClick = useCallback(() => {
if (pdEa > 1) {
setPdEa(pdEa - 1);
if (isMockMode) {
dispatch(setMockCartItemQuantity(prodSno, newQty));
} else {
// 실제 API 호출을 위한 사용자 정보 필요
const { userNumber } = store.getState().common.appStatus.loginUserData;
if (userNumber) {
dispatch(updateCartItem({ mbrNo: userNumber, cartSno: prodSno, prodQty: newQty }));
}
}
}
}, [pdEa]);
}, [dispatch, isMockMode]);
const handleIncreseClick = useCallback(() => {
setPdEa(pdEa + 1);
}, [pdEa]);
const handleIncreseClick = useCallback((prodSno, currentQty) => {
const newQty = currentQty + 1;
useEffect(() => {
if (pdEa === 1) {
Spotlight.focus("pd_ea_increse");
if (isMockMode) {
dispatch(setMockCartItemQuantity(prodSno, newQty));
} else {
// 실제 API 호출을 위한 사용자 정보 필요
const { userNumber } = store.getState().common.appStatus.loginUserData;
if (userNumber) {
dispatch(updateCartItem({ mbrNo: userNumber, cartSno: prodSno, prodQty: newQty }));
}
}
}, [pdEa]);
}, [dispatch, isMockMode]);
// 상품 삭제 핸들러
const handleDeleteClick = useCallback((prodSno) => {
if (isMockMode) {
dispatch(removeFromMockCart(prodSno));
} else {
// 실제 API 호출을 위한 사용자 정보 필요
const { userNumber } = store.getState().common.appStatus.loginUserData;
if (userNumber) {
dispatch(removeFromCart({ mbrNo: userNumber, cartSno: prodSno }));
}
}
}, [dispatch, isMockMode]);
return (
<>
@@ -95,8 +135,8 @@ const CartProduct = () => {
const totals = calculatePartnerTotal(group.items);
return (
<Container className={css.productBox}>
<div key={partnerKey} className={css.partnerSection}>
<Container key={partnerKey} className={css.productBox}>
<div className={css.partnerSection}>
{/* 파트너사 정보 - 한 번만 표시 */}
<div className={css.productCompany}>
<div className={css.logo}>
@@ -135,7 +175,22 @@ const CartProduct = () => {
</div>
{/* 해당 파트너사의 상품 리스트 - map으로 반복 */}
{group.items.map((item) => (
{group.items.map((item) => {
// CheckOutPanel과 동일한 이미지 처리 방식 적용
const normalizedItem = normalizeProductDataForDisplay(item);
// ✅ 이미지 우선순위: ProductAllSection 고품질 이미지 → 기타 모든 이미지 필드
const imageSrc = (item.imgUrls600 && item.imgUrls600[0]) || // ✅ ProductAllSection의 고품질 이미지
(item.thumbnailUrl960) || // ✅ 960px 썸네일
(normalizedItem.imgUrls && normalizedItem.imgUrls[0]?.imgUrl) ||
(item.imgUrls && item.imgUrls[0]?.imgUrl) ||
(item.imgList && item.imgList[0]?.imgUrl) ||
normalizedItem.imgUrl ||
item.imgUrl ||
item.thumbnailUrl ||
defaultImage;
return (
<div key={item.prodSno} className={css.product}>
<div className={css.leftBox}>
<div className={css.checkBox}>
@@ -151,8 +206,9 @@ const CartProduct = () => {
<div className={css.leftSection}>
<CustomImage
className={css.productImage}
src={item.imgUrl}
src={imageSrc}
fallbackSrc={defaultImage}
alt={item.prdtNm || 'Product Image'}
/>
</div>
<div className={css.rightSection}>
@@ -191,13 +247,13 @@ const CartProduct = () => {
item.prodQty === 1 ? css.dimm : ""
)}
size="cartEa"
onClick={() => handleDecreseClick(item.prodSno)}
onClick={() => handleDecreseClick(item.prodSno, item.prodQty)}
spotlightId={"pd_ea_decrese"}
spotlightDisabled={item.prodQty === 1}
/>
<div className={css.ea}>{item.prodQty}</div>
<TButton
onClick={() => handleIncreseClick(item.prodSno)}
onClick={() => handleIncreseClick(item.prodSno, item.prodQty)}
className={css.plusBox}
spotlightId={"pd_ea_increse"}
size="cartEa"
@@ -210,11 +266,12 @@ const CartProduct = () => {
<TButton
className={css.trashImg}
size="cartTrash"
// onClick={() => handleDeleteClick(item.prodSno)}
onClick={() => handleDeleteClick(item.prodSno)}
/>
</div>
</div>
))}
);
})}
</div>
</Container>
);

View File

@@ -6,17 +6,12 @@ import SpotlightContainerDecorator
import CartProduct from './CartProduct';
import css from './CartProductBar.module.less';
const CartProductBar = ({ scrollOptions = {} }) => {
const CartProductBar = ({ cartInfo, scrollOptions = {} }) => {
const Container = SpotlightContainerDecorator("div");
// const randomCount = useMemo(() => Math.floor(Math.random() * 3) + 1, []);
return (
<Container className={css.productContainer}>
<CartProduct />
{/* {Array.from({ length: randomCount }, (_, index) => ( */}
{/* <CartProduct key={index} /> */}
{/* ))} */}
<CartProduct cartInfo={cartInfo} />
</Container>
);
};

View File

@@ -1,16 +1,84 @@
import React from 'react';
import React, { useMemo, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import TButton from '../../components/TButton/TButton';
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
import { pushPanel } from '../../actions/panelActions';
import Config from '../../utils/Config';
import css from './CartSidebar.module.less';
const CartSidebar = () => {
const mockData = {
itemCount: 3,
subtotal: 199.97,
optionTotal: 29.99,
shippingHandling: 19.99,
orderTotalBeforeTax: 249.95,
};
const CartSidebar = ({ cartInfo }) => {
const dispatch = useDispatch();
// Mock Mode 확인
const isMockMode = BUYNOW_CONFIG.isMockMode();
// 실제 장바구니 데이터 (API 모드일 때만 사용)
const fallbackCartInfo = useSelector((state) => state.cart.getMyinfoCartSearch.cartInfo);
// 사용할 장바구니 데이터 결정
const displayCartInfo = cartInfo || (isMockMode ? null : fallbackCartInfo);
// Mock 데이터 또는 실제 데이터 계산
const calculatedData = useMemo(() => {
if (isMockMode) {
// Mock Mode: 기본 mockData 사용
return {
itemCount: 3,
subtotal: 199.97,
optionTotal: 29.99,
shippingHandling: 19.99,
orderTotalBeforeTax: 249.95,
};
} else if (displayCartInfo && Array.isArray(displayCartInfo) && displayCartInfo.length > 0) {
// API Mode: 실제 장바구니 데이터로 계산
const itemCount = displayCartInfo.reduce((sum, item) => sum + (item.prodQty || 1), 0);
const subtotal = displayCartInfo.reduce((sum, item) => {
const price = parseFloat(item.price3 || item.price2 || 0);
return sum + (price * (item.prodQty || 1));
}, 0);
const optionTotal = displayCartInfo.reduce((sum, item) => {
const optionPrice = parseFloat(item.price5 || item.optPrc || 0);
return sum + (optionPrice * (item.prodQty || 1));
}, 0);
const shippingHandling = displayCartInfo.reduce((sum, item) =>
sum + parseFloat(item.shippingCharge || 0), 0
);
return {
itemCount,
subtotal,
optionTotal,
shippingHandling,
orderTotalBeforeTax: subtotal + optionTotal + shippingHandling,
};
} else {
// 데이터가 없는 경우
return {
itemCount: 0,
subtotal: 0,
optionTotal: 0,
shippingHandling: 0,
orderTotalBeforeTax: 0,
};
}
}, [isMockMode, displayCartInfo]);
// 체크아웃 버튼 클릭 핸들러
const handleCheckoutClick = useCallback(() => {
if (isMockMode) {
// Mock Mode: 바로 체크아웃 패널로 이동
console.log('[CartSidebar] Mock Mode - Going to Checkout');
dispatch(pushPanel({
name: Config.panel_names.CHECKOUT_PANEL,
}));
} else {
// API Mode: 실제 로직 (향후 구현)
console.log('[CartSidebar] API Mode - Checkout (to be implemented)');
}
}, [dispatch, isMockMode]);
const { itemCount, subtotal, optionTotal, shippingHandling, orderTotalBeforeTax } = calculatedData;
const formatPrice = (price) => {
return `$${price.toFixed(2)}`;
@@ -21,24 +89,24 @@ const CartSidebar = () => {
<div className={css.summarySection}>
<div className={css.header}>
<div className={css.title}>Subtotal</div>
<span className={css.itemCount}>{mockData.itemCount} Items</span>
<span className={css.itemCount}>{itemCount} Items</span>
</div>
<div className={css.borderLine} />
<div className={css.priceList}>
<div className={css.priceItem}>
<span className={css.label}>Subtotal</span>
<span className={css.value}>{formatPrice(mockData.subtotal)}</span>
<span className={css.value}>{formatPrice(subtotal)}</span>
</div>
<div className={css.priceItem}>
<span className={css.label}>Option</span>
<span className={css.value}>
{formatPrice(mockData.optionTotal)}
{formatPrice(optionTotal)}
</span>
</div>
<div className={css.priceItem}>
<span className={css.label}>S&H</span>
<span className={css.value}>
{formatPrice(mockData.shippingHandling)}
{formatPrice(shippingHandling)}
</span>
</div>
</div>
@@ -49,7 +117,7 @@ const CartSidebar = () => {
<span className={css.totalLabelSub}>(Before Tax)</span>
</span>
<span className={css.totalValue}>
{formatPrice(mockData.orderTotalBeforeTax)}
{formatPrice(orderTotalBeforeTax)}
</span>
</div>
</div>
@@ -70,7 +138,7 @@ const CartSidebar = () => {
<TButton
className={css.checkoutButton}
spotlightId="cart-checkout-button"
onClick={() => console.log("Checkout clicked")}
onClick={handleCheckoutClick}
>
Checkout
</TButton>

View File

@@ -16,6 +16,7 @@ import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import { addToCart } from '../../../actions/cartActions';
import { addToMockCart } from '../../../actions/mockCartActions';
import { getMyInfoCheckoutInfo } from '../../../actions/checkoutActions';
import {
changeAppStatus,
@@ -792,7 +793,7 @@ const BuyOption = ({
// })
// );
// Mock Mode: API 호출 스킵
// API Mode: 실제 API 호출 후 CartPanel로 이동
if (!BUYNOW_CONFIG.isMockMode()) {
// 장바구니에 추가
dispatch(
@@ -811,17 +812,138 @@ const BuyOption = ({
}
})
);
} else {
// Mock Mode: 로컬 장바구니 데이터 생성 후 CartPanel로 이동
console.log('[BuyOption] Mock Mode - Adding to cart (Mock)');
}
// CartPanel로 이동
dispatch(
pushPanel({
name: Config.panel_names.CART_PANEL,
})
);
// CartPanel로 이동 (productInfo 포함) - API에서는 이미 addToCart 호출됨
// 이미지 URL 구성 (CheckOutPanel과 동일한 방식)
// ✅ 이미지 URL 추출 (ProductAllSection의 고품질 이미지 우선)
const imgUrl = productInfo?.imgUrls600?.[0] || // ✅ ProductAllSection에서 사용하는 고품질 이미지
productInfo?.thumbnailUrl960 || // ✅ 960px 썸네일
productInfo?.imgList?.[0]?.imgUrl ||
productInfo?.thumbnailUrl ||
productInfo?.patncLogPath ||
'/assets/images/img-thumb-empty-144@3x.png';
// 가격 정보 구성
const regularPrice = productInfo?.regularPrice || 299.99;
const discountPrice = productInfo?.discountPrice || regularPrice;
const productInfoForCart = {
// 기본 정보
prdtId: selectedPrdtId,
prdtNm: productInfo?.prdtNm || 'Product',
patnrId: selectedPatnrId,
patncNm: productInfo?.patncNm || 'Partner',
patncLogPath: productInfo?.patncLogPath || '',
// ✅ 이미지 정보 (ProductAllSection의 고품질 이미지 포함)
imgUrl: imgUrl,
thumbnailUrl: productInfo?.thumbnailUrl960 || imgUrl,
thumbnailUrl960: productInfo?.thumbnailUrl960,
imgList: productInfo?.imgList || [{ imgUrl: imgUrl }],
imgUrls: productInfo?.imgUrls || [{ imgUrl: imgUrl }], // imgUrls 배열 구조 추가 (CheckOutPanel 호환성)
imgUrls600: productInfo?.imgUrls600, // ✅ 고품질 이미지 배열 포함
// 가격 정보 (문자열 형식으로)
price2: regularPrice.toFixed(2), // 원가
price3: discountPrice.toFixed(2), // 할인가/판매가
// 수량 정보
prodQty: quantity,
// 옵션 정보
optNm: productOptionInfos[0]?.optNm || '',
// 배송비 정보
shippingCharge: productInfo?.shippingFee || '12.99',
// 기타 정보
soldoutFlag: productInfo?.soldoutFlag || 'N',
};
const optionInfoForCart = {
name: productOptionInfos[0]?.optNm || '',
price: productOptionInfos[0]?.prdtOptDtl[0]?.optPrc || '0.00'
};
dispatch(
pushPanel({
name: Config.panel_names.CART_PANEL,
panelInfo: {
productInfo: productInfoForCart,
optionInfo: optionInfoForCart,
quantity: quantity
}
})
);
} else {
// Mock Mode: Mock 장바구니에 상품 추가 후 CartPanel로 이동
console.log('[BuyOption] Mock Mode - Adding to cart (Mock)');
// ✅ 이미지 URL 구성 (ProductAllSection의 고품질 이미지 우선)
const imgUrl = productInfo?.imgUrls600?.[0] || // ✅ ProductAllSection에서 사용하는 고품질 이미지
productInfo?.thumbnailUrl960 || // ✅ 960px 썸네일
productInfo?.imgList?.[0]?.imgUrl ||
productInfo?.thumbnailUrl ||
productInfo?.patncLogPath ||
'/assets/images/img-thumb-empty-144@3x.png';
// 가격 정보 구성 (CheckOutPanel과 동일한 방식)
const regularPrice = productInfo?.regularPrice || 299.99;
const discountPrice = productInfo?.discountPrice || regularPrice;
// Mock 상품 정보 구성 (CheckOutPanel 구조 참고)
const mockProductInfo = {
// 기본 정보
prdtId: selectedPrdtId,
prdtNm: productInfo?.prdtNm || 'Mock Product',
patnrId: selectedPatnrId,
patncNm: productInfo?.patncNm || 'Mock Partner',
patncLogPath: productInfo?.patncLogPath || '',
// ✅ 이미지 정보 (ProductAllSection의 고품질 이미지 포함)
imgUrl: imgUrl,
thumbnailUrl: productInfo?.thumbnailUrl960 || imgUrl,
thumbnailUrl960: productInfo?.thumbnailUrl960,
imgList: productInfo?.imgList || [{ imgUrl: imgUrl }], // imgList 배열 구조 추가
imgUrls: productInfo?.imgUrls || [{ imgUrl: imgUrl }], // imgUrls 배열 구조 추가 (CheckOutPanel 호환성)
imgUrls600: productInfo?.imgUrls600, // ✅ 고품질 이미지 배열 포함
// 가격 정보 (문자열 형식으로)
price2: regularPrice.toFixed(2), // 원가
price3: discountPrice.toFixed(2), // 할인가/판매가
// 수량 정보
prodQty: quantity,
// 옵션 정보 (필요시)
optNm: productOptionInfos[0]?.optNm || 'Default Option',
// 배송비 정보
shippingCharge: productInfo?.shippingFee || '12.99',
shippingFee: productInfo?.shippingFee || '12.99',
// 기타 정보
soldoutFlag: productInfo?.soldoutFlag || 'N',
};
// 옵션 정보 구성
const optionInfo = {
name: productOptionInfos[0]?.optNm || 'Default Option',
price: productOptionInfos[0]?.prdtOptDtl[0]?.optPrc || '0.00'
};
// CartPanel로 이동 (productInfo 포함) - CartPanel에서 직접 상품 추가
dispatch(
pushPanel({
name: Config.panel_names.CART_PANEL,
panelInfo: {
productInfo: mockProductInfo,
optionInfo: optionInfo,
quantity: quantity
}
})
);
}
}
dispatch(clearAllToasts());

View File

@@ -46,7 +46,7 @@ import TNewPopUp from '../../components/TPopUp/TNewPopUp';
import TPopUp from '../../components/TPopUp/TPopUp';
import usePrevious from '../../hooks/usePrevious';
import * as Config from '../../utils/Config';
import { panel_names } from '../../utils/Config';
import { panel_names, STANDALONE_PANELS, isStandalonePanel } from '../../utils/Config';
import { $L, getErrorMessage, getSpottableDescendants } from '../../utils/helperMethods';
import { BUYNOW_CONFIG } from '../../utils/BuyNowConfig';
import { SpotlightIds } from '../../utils/SpotlightIds';
@@ -200,10 +200,10 @@ export default function MainView({ className, initService }) {
const topPanel = panels[panels.length - 1];
// CheckOutPanel은 독립적으로 항상 단독 렌더링
if (topPanel?.name === Config.panel_names.CHECKOUT_PANEL) {
console.log('[MainView] CheckOutPanel detected - rendering independently');
renderingPanels = [topPanel]; // CheckOutPanel만 단독으로 렌더링
// 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들
if (isStandalonePanel(topPanel?.name)) {
console.log(`[MainView] Standalone panel detected: ${topPanel?.name} - rendering independently`);
renderingPanels = [topPanel]; // 단독 패널만 단독으로 렌더링
}
// 기존 3-layer 구조 체크: PlayerPanel + DetailPanel + MediaPanel(modal)
else {
@@ -248,10 +248,10 @@ export default function MainView({ className, initService }) {
const Component = panelMap[panel.name];
let isPanelOnTop = false;
// CheckOutPanel은 항상 onTop
if (panel.name === Config.panel_names.CHECKOUT_PANEL) {
// 단독 패널은 항상 onTop
if (isStandalonePanel(panel.name)) {
isPanelOnTop = true;
console.log('[MainView] CheckOutPanel is always onTop');
console.log(`[MainView] Standalone panel ${panel.name} is always onTop`);
}
// 3-layer 케이스: 중간 패널(DetailPanel)이 onTop
else if (renderingPanels.length === 3) {