From 95b709c513984256429341b527cfa836b85b99b9 Mon Sep 17 00:00:00 2001 From: optrader Date: Wed, 5 Nov 2025 07:35:14 +0900 Subject: [PATCH] [251105] fix: BuyNow Option-1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 커밋 시간: 2025. 11. 05. 07:35:13 📊 변경 통계: • 총 파일: 4개 • 추가: +94줄 • 삭제: -70줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx 🔧 주요 변경 내용: • 공통 유틸리티 함수 최적화 • UI 컴포넌트 아키텍처 개선 • 소규모 기능 개선 • 코드 정리 및 최적화 --- .../src/utils/BuyNowDataManipulator.js | 100 +++++++++++------- .../src/views/DetailPanel/DetailPanel.jsx | 1 + .../DetailPanel/components/BuyOption.jsx | 61 +++++------ .../VoiceInputOverlay/VoiceInputOverlay.jsx | 2 +- 4 files changed, 94 insertions(+), 70 deletions(-) diff --git a/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js b/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js index 5c786f35..52332cf5 100644 --- a/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js +++ b/com.twin.app.shoptime/src/utils/BuyNowDataManipulator.js @@ -12,43 +12,69 @@ import { BUYNOW_CONFIG } from './BuyNowConfig'; * @param {Object} originalProductData - 원본 상품 데이터 * @returns {Object|null} Mock 옵션 데이터 또는 null */ -export const createMockProductOptionData = (originalProductData) => { - // API Mode: null 반환 - if (!BUYNOW_CONFIG.isMockMode() || !originalProductData) { - return null; - } - - // 원본 데이터에서 필요한 정보 추출 - const prdtId = originalProductData.prdtId || 'MOCK_PRODUCT'; - const prdtNm = originalProductData.prdtNm || 'Mock Product'; - - // 실제 상품 가격 정보 사용 (originalProductData의 priceInfo 또는 기본값) - // priceInfo 포맷: "원가|할인가|할인액|할인율|할인율숫자" - const priceInfo = originalProductData.priceInfo || '99999|99999|0|0%|0'; - - console.log('[BuyNowDataManipulator] createMockProductOptionData - priceInfo:', priceInfo); - - // 옵션 상세 객체 - const optionDetail = { - prodOptCval: `${prdtNm}`, - priceInfo: priceInfo, - stockCnt: 999, - soldOutYn: 'N', - }; - - // Mock 옵션 데이터 생성 (BuyOption에서 기대하는 구조) - // 배열 구조로 반환 - BuyOption에서 productOptionInfos[0]에 접근 - return [ - { - // 옵션 ID (기본 값) - prodOptSno: `MOCK_OPT_${prdtId}_1`, - prodOptTpCdCval: 'BASIC', - - // 옵션 상세 목록 (기본값 1개) - prdtOptDtl 배열 구조 - prdtOptDtl: [optionDetail], - } - ]; -}; +export const createMockProductOptionData = (originalProductData) => { + // API Mode: null 반환 + if (!BUYNOW_CONFIG.isMockMode() || !originalProductData) { + return null; + } + + // 원본 데이터에서 필요한 정보 추출 + const prdtId = originalProductData.prdtId || 'MOCK_PRODUCT'; + const prdtNm = originalProductData.prdtNm || 'Mock Product'; + + // 실제 상품 가격 정보 사용 (originalProductData의 priceInfo 또는 기본값) + // priceInfo 포맷: "원가|할인가|할인액|할인율|할인율숫자" + const priceInfo = originalProductData.priceInfo || '99999|99999|0|0%|0'; + const [regularPrice = '99999', salePrice = regularPrice] = priceInfo.split('|'); + + // 옵션명/옵션 값은 기존 데이터에서 최대한 활용하고, 없으면 기본값 사용 + const baseOptionName = + originalProductData.optNm || + originalProductData.optTpNm || + `${prdtNm} Option`; + const optionValueName = + originalProductData.optValueNm || + originalProductData.optVlNm || + `${prdtNm} 기본 구성`; + + const optionCode = + originalProductData.prodOptCdCval || `MOCK_OPT_${prdtId}_1`; + const optionImage = + originalProductData.thumbnailUrl960 || + originalProductData.imgUrl || + originalProductData.thumbnailUrl || + null; + + console.log('[BuyNowDataManipulator] createMockProductOptionData - priceInfo:', priceInfo); + + // 옵션 상세 객체 + const optionDetail = { + prodOptCdCval: optionCode, + prodOptCval: optionValueName, + optNm: optionValueName, + optPrc: salePrice, + optStkQty: 999, + optImgUrl: optionImage, + priceInfo: priceInfo, + stockCnt: 999, + soldOutYn: 'N', + }; + + // Mock 옵션 데이터 생성 (BuyOption에서 기대하는 구조) + // 배열 구조로 반환 - BuyOption에서 productOptionInfos[0]에 접근 + return [ + { + // 옵션 ID (기본 값) + prodOptSno: optionCode, + prodOptTpCdCval: originalProductData.prodOptTpCdCval || 'BASIC', + optNm: baseOptionName, + optTpNm: originalProductData.optTpNm || baseOptionName, + + // 옵션 상세 목록 (기본값 1개) - prdtOptDtl 배열 구조 + prdtOptDtl: [optionDetail], + } + ]; +}; /** * Mock Mode에서 원본 상품 데이터를 기반으로 Mock 장바구니 데이터 생성 diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index e5d9c913..ffe8e2f7 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -141,6 +141,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { (isCancelClick) => (ev) => { fp.pipe( () => { + dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 dispatch(finishVideoPreview()); dispatch(popPanel(panel_names.DETAIL_PANEL)); }, diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx index a53fc481..9b3e3ced 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx @@ -136,14 +136,15 @@ const BuyOption = ({ // 상품 정보 (props 우선, 없으면 productInfo에서) const selectedPatnrId = propsSelectedPatnrId || productInfo?.patnrId; const selectedPrdtId = propsSelectedPrdtId || productInfo?.prdtId; + const isMockMode = BUYNOW_CONFIG.isMockMode(); // Mock Mode에서 옵션 데이터 처리 const productOptionInfos = useMemo(() => { - console.log('[BuyOption] productOptionInfos useMemo - isMockMode:', BUYNOW_CONFIG.isMockMode()); + console.log('[BuyOption] productOptionInfos useMemo - isMockMode:', isMockMode); console.log('[BuyOption] productOptionInfos useMemo - reduxProductOptionInfos:', reduxProductOptionInfos); // API Mode: 기존 로직 100% 유지 - if (!BUYNOW_CONFIG.isMockMode()) { + if (!isMockMode) { console.log('[BuyOption] API Mode - using reduxProductOptionInfos'); return reduxProductOptionInfos; } @@ -162,7 +163,9 @@ const BuyOption = ({ // Mock Mode이고 유효한 옵션 데이터가 있으면 그대로 사용 console.log('[BuyOption] Mock Mode - using existing valid reduxProductOptionInfos'); return reduxProductOptionInfos; - }, [reduxProductOptionInfos, productData]); // logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용) + }, [reduxProductOptionInfos, productData, isMockMode]); // logInfo 생성 (SingleOption과 동일한 로직, productData 우선 사용) + const hasMockOptions = + isMockMode && Array.isArray(productOptionInfos) && productOptionInfos.length > 0; const logInfo = useMemo(() => { if (productData) { // productData가 있으면 SingleOption과 동일하게 처리 @@ -307,7 +310,7 @@ const BuyOption = ({ // Mock Mode: API 호출 스킵 (Mock 데이터만 사용) useEffect(() => { // API Mode: 실제 API 호출 - if (!BUYNOW_CONFIG.isMockMode()) { + if (!isMockMode) { dispatch( getProductOption({ patnrId: selectedPatnrId, @@ -324,41 +327,35 @@ const BuyOption = ({ ); } // Mock Mode: API 호출 하지 않음 - }, [dispatch, selectedPatnrId, selectedPrdtId, userNumber]); + }, [dispatch, selectedPatnrId, selectedPrdtId, userNumber, isMockMode]); // 포커스 관리 로직 (SingleOption과 유사) useEffect(() => { - // if (!isSpotlight) { - // // isSpotlight이 false면 일반적인 BuyOption 포커스 - // console.log('[BuyOption] Component mounted - focusing BUY NOW button'); + const shouldHandleOptionFocus = + productInfo?.optProdYn === 'Y' || hasMockOptions; - // Spotlight.focus('buy-option-buy-now-button'); - - // return; - // } - - // isSpotlight이 true이고 SingleOption 동작이 필요한 경우 요부분 - if (productInfo?.optProdYn === 'N') { + if (!shouldHandleOptionFocus) { Spotlight.focus('buy-option-buy-now-button'); - } else if (productInfo?.optProdYn === 'Y') { - if( - productOptionInfos && - productOptionInfos?.length > 1 - ){ - Spotlight.focus('buy-option-first-dropdown'); - } else if ( - productOptionInfos?.legnth > 0 && - productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl && - productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 - ){ - Spotlight.focus('buy-option-second-dropdown'); - } else { - Spotlight.focus('buy-option-buy-now-button'); - } + return; + } + + if (productOptionInfos && productOptionInfos.length > 1) { + Spotlight.focus('buy-option-first-dropdown'); + } else if ( + productOptionInfos && + productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl && + productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 + ) { + Spotlight.focus('buy-option-second-dropdown'); } else { Spotlight.focus('buy-option-buy-now-button'); } - }, [productOptionInfos?.length, productInfo?.length]); + }, [ + productOptionInfos, + productInfo?.optProdYn, + hasMockOptions, + selectedBtnOptIdx, + ]); // checkOutValidate 콜백 함수 (SingleOption과 동일한 로직) function checkOutValidate(response) { @@ -1318,7 +1315,7 @@ const BuyOption = ({ {/* 동적 옵션 렌더링 */} {productOptionInfos && productOptionInfos?.length > 0 && - productInfo?.optProdYn === 'Y' && ( + (productInfo?.optProdYn === 'Y' || hasMockOptions) && ( <> {/* 첫번째 옵션 (여러 옵션이 있을 때만) */} {productOptionInfos?.length > 1 && ( diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx index d5d80869..ed8904ce 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx @@ -213,7 +213,7 @@ const VoiceInputOverlay = ({ const [isSilenceCheckActive, setIsSilenceCheckActive] = useState(false); const [hasReached5Chars, setHasReached5Chars] = useState(false); // 처음 5글자 도달 추적 // 💬 Bubble 버튼 상태 (true: 버튼 O, false: 텍스트만) - const [isBubbleButton, setIsBubbleButton] = useState(true); // 첫 발화때는 true (Try Saying) + const [isBubbleButton, setIsBubbleButton] = useState(false); // 첫 발화때는 true (Try Saying) // useSearchHistory Hook 적용 (음성검색 기록 관리) const { addVoiceSearch } = useSearchHistory();