# Theme Product UI 처리 상세 분석 보고서 ## 📋 개요 **Theme Product** (쇼/공연 상품)는 DetailPanel에서 `panelInfo.type === "theme"`일 때 렌더링되며, **ShowProduct 컴포넌트**를 통해 UI를 구성합니다. --- ## 🔄 데이터 흐름 ``` DetailPanel (type: "theme" 감지) ↓ getThemeCurationDetailInfo() 액션 디스패치 ↓ Redux Reducer: GET_THEME_CURATION_DETAIL_INFO ↓ state.home.themeCurationDetailInfoData[] (상품 배열) state.home.productData (테마 정보) ↓ ShowProduct 컴포넌트 렌더링 ``` --- ## 🏗️ Redux 상태 구조 ### A. 상품 목록 데이터 (themeCurationDetailInfoData) ```javascript // state.home.themeCurationDetailInfoData: array of products [ { // 기본 정보 prdtId: "PROD001", prdtNm: "Opera Show", patncNm: "Broadway Partners", patnrId: "PARTNER001", patncLogoPath: "http://logo-url.png", // 이미지 imgUrls600: ["url1", "url2", "url3"], thumbnailUrl960: "thumbnail-url", // 가격 정보 ("|" 구분자로 여러 정보 포함) priceInfo: "299|199|Y|discount10|10%", // ↑ ↑ ↑ ↑ ↑ // 원가 할인가 보상적용 할인코드 할인율 // 상품 상태 soldoutFlag: "N", // "Y" or "N" pmtSuptYn: "Y", // 결제 지원 여부 // 분류 catCd: "CATE001", catNm: "Performance", // 평점 revwGrd: 4.5, // 비디오/미디어 prdtMediaUrl: "http://video-url", prdtMediaSubtitlUrl: "http://subtitle-url", // 특수 정보 todaySpclFlag: "Y", // 오늘의 특가 showId: "SHOW001", showNm: "Opera Night", orderPhnNo: "1-800-123-4567", disclaimer: "Disclaimer text", // QR 코드 qrcodeUrl: "qr-data-string" }, // ... 여러 쇼/상품 ] ``` ### B. 테마 정보 (productData) ```javascript // state.home.productData { themeInfo: [ { curationId: "CURATION001", curationNm: "Theater Theme", patnrId: "PARTNER001", patncNm: "Broadway Partners", brndNm: "Broadway", priceInfo: "299|199|Y|discount10|10%" } ] } ``` --- ## 🎨 ShowProduct 컴포넌트 구조 ### 렌더링 흐름 ```jsx ShowProduct ├── Container (Spotlight) │ ├── ThemeIndicator │ │ ├── 선택된 상품 이미지 표시 (메인) │ │ ├── 비디오 자동 재생 (옵션) │ │ └── 이미지 썸네일 스크롤 │ └── IndicatorOptions │ ├── 상품명, 로고, 평점 │ ├── 설명 버튼 │ ├── SMS 버튼 │ └── QR 코드 └── optionContainer ├── ShowSingleOption (결제 가능) │ └── ProductOption │ └── SingleOption └── ShowUnableOption (결제 불가) └── ProductOption └── UnableOption ``` --- ## 🖼️ ThemeIndicator 컴포넌트 ### 주요 기능 #### 1. 이미지 선택 관리 ```javascript const [imageSelectedIndex, setImageSelectedIndex] = useState(0); const [selectedImage, setSelectedImage] = useState(null); useEffect(() => { if (thumbnailUrls) { // 비디오가 있으면 [0] = 비디오, [1]부터 이미지 if (getProductMediaUrlStatus) { const image = thumbnailUrls[imageSelectedIndex - 1]; return setSelectedImage(image); } else { // 비디오 없으면 [0]부터 이미지 const image = thumbnailUrls[imageSelectedIndex]; setSelectedImage(image); } } }, [thumbnailUrls, getProductMediaUrlStatus, imageSelectedIndex]); ``` #### 2. 비디오 자동 재생 ```javascript const canPlayVideo = useMemo(() => { return themeProductInfo?.prdtMediaUrl && imageSelectedIndex === 0; }, [themeProductInfo, imageSelectedIndex]); useEffect(() => { if (!launchedFromPlayer && autoPlaying && themeProductInfo?.prdtMediaUrl) { dispatch( startVideoPlayer({ showUrl: themeProductInfo?.prdtMediaUrl, showNm: themeProductInfo?.prdtNm, subtitle: themeProductInfo?.prdtMediaSubtitlUrl, thumbnailUrl: themeProductInfo?.thumbnailUrl960, // ... 더 많은 정보 }) ); } }, [dispatch, autoPlaying, imageSelectedIndex]); ``` #### 3. 이미지 스크롤 (TVirtualGridList) ```jsx ``` --- ## 📝 IndicatorOptions 컴포넌트 ### 구성 요소 #### 1. 상단 정보 영역 ```jsx
{productInfo?.expsPrdtNo && (
ID: {productInfo?.expsPrdtNo}
)}
{productInfo?.prdtNm}
``` #### 2. 버튼 영역 ```jsx {isBillingProductVisible && ( descriptionClick("DESCRIPTION", description)} spotlightId="description_Btn" > {$L("DESCRIPTION")} descriptionClick("RETURNS & EXCHANGES", exchangeInfo)} spotlightId="return_Exchanges_Btn" > {$L("RETURNS & EXCHANGES")} )} {$L("SHOP BY MOBILE")} ``` #### 3. QR 코드 ```jsx
Please check for more detailed information
``` #### 4. 설명 팝업 ```jsx const renderPopUp = () => { return (

{productInfo?.prdtNm}

); }; ``` --- ## 💳 결제 여부에 따른 UI 분기 ### ShowSingleOption (pmtSuptYn === "Y" && webOSVersion >= "6.0") ```jsx ``` **구성:** ```jsx // ShowOptions/ShowSingleOption.jsx ``` **SingleOption 렌더링:** - 상품 옵션 선택 (이용일자, 좌석 등) - 수량 선택 - 가격 표시 - "구매" 버튼 --- ### ShowUnableOption (결제 불가능) **조건:** - `pmtSuptYn === "N"` OR - `webOSVersion < "6.0"` ```jsx ``` **구성:** ```jsx // ShowOptions/ShowUnableOption.jsx ``` **UnableOption 렌더링:** - 구매 불가 이유 표시 - SMS로 상품 정보 공유 버튼 - 로고, 상품명, 가격 - "SEE MORE" 또는 "SHOP BY MOBILE" 버튼 --- ## 📊 선택 인덱스 관리 ```javascript // ShowProduct.jsx에서 selectedIndex 상태 관리 const showProductInfo = useMemo(() => { if (productData && productInfo) { const themeInfo = productData?.themeInfo[0]; if (themeInfo) { return { ...productInfo[selectedIndex], // ← selectedIndex로 배열 접근 curationId: themeInfo.curationId, curationNm: themeInfo.curationNm, expsOrd: `${selectedIndex + 1}`, // 순번 (1부터) }; } } return {}; }, [productData, productInfo, selectedIndex]); ``` **UI에서 selectedIndex 변경:** ```jsx ``` --- ## 🔍 데이터 매핑 상세 ### priceInfo 파싱 ```javascript // priceInfo 형식: "원가|할인가|보상적용|할인코드|할인율" const priceInfo = "299|199|Y|discount10|10%"; const befPrice = priceInfo.split("|")[0]; // "299" (원가) const lastPrice = priceInfo.split("|")[1]; // "199" (할인가) const rewdAplyFlag = priceInfo.split("|")[2]; // "Y" (보상 적용) const discountCode = priceInfo.split("|")[3]; // "discount10" const discountRate = priceInfo.split("|")[4]; // "10%" ``` **로그 전송 시:** ```javascript const params = { befPrice: showProductInfo?.priceInfo?.split("|")[0], lastPrice: showProductInfo?.priceInfo?.split("|")[1], rewdAplyFlag: showProductInfo?.priceInfo?.split("|")[2], // ... }; ``` --- ## 📱 비디오 처리 ### 자동 재생 조건 ```javascript const [autoPlaying, setAutoPlaying] = useState( !launchedFromPlayer && themeProductInfo?.prdtMediaUrl ); // - Player에서 오지 않았고 (launchedFromPlayer = false) // - 비디오 URL이 있을 때만 자동 재생 ``` ### 이미지/비디오 순서 ```javascript // 비디오가 있는 경우 썸네일 구성: // [0] = 비디오 플레이 버튼 // [1] = 첫 번째 이미지 // [2] = 두 번째 이미지 // ... // imageSelectedIndex = 0 → 비디오 재생 // imageSelectedIndex = 1 → 첫 번째 이미지 표시 ``` --- ## 🔔 로깅 시스템 ### 1. 상세 정보 조회 로그 (500ms 딜레이) ```javascript useEffect(() => { if (showProductInfo && Object.keys(showProductInfo).length > 0) { const params = { befPrice: showProductInfo?.priceInfo?.split("|")[0], curationId: showProductInfo?.curationId, curationNm: showProductInfo?.curationNm, expsOrd: showProductInfo?.expsOrd, // 1부터 시작하는 순번 lgCatCd: showProductInfo?.catCd, lgCatNm: showProductInfo?.catNm, logTpNo: LOG_TP_NO.PRODUCT.PRODUCT_DETAIL_IMAGE, prdtId: showProductInfo?.prdtId, prdtNm: showProductInfo?.prdtNm, revwGrd: showProductInfo?.revwGrd, tsvFlag: showProductInfo?.todaySpclFlag, }; timerRef.current = setTimeout( () => dispatch(sendLogProductDetail(params)), 1000 ); } }, [showProductInfo]); ``` ### 2. 설명 버튼 클릭 로그 ```javascript const handleIndicatorOptions = useCallback(() => { if (productData && Object.keys(productData).length > 0) { const params = { ...detailLogParamsRef.current, logTpNo: LOG_TP_NO.DETAIL.DETAIL_BUTTON_CLICK, // 버튼 클릭 }; dispatch(sendLogDetail(params)); dispatch(sendLogTotalRecommend({ menu: LOG_MENU.DETAIL_PAGE_THEME_DETAIL, buttonTitle: "DESCRIPTION", messageId: LOG_MESSAGE_ID.BUTTONCLICK, })); } }, [productData]); ``` ### 3. SMS 버튼 클릭 로그 ```javascript const handleMobileSendPopupOpen = useCallback(() => { // ... SMS 팝업 로그 const params = { patncNm: showProductInfo?.patncNm, prdtId: showProductInfo?.prdtId, prdtNm: showProductInfo?.prdtNm, shopByMobileFlag: "Y", shopTpNm: "product", // ← 테마 상품 구분 showId: showProductInfo?.showId, showNm: showProductInfo?.showNm, // ... }; dispatch(sendLogShopByMobile(params)); }, [showProductInfo]); ``` --- ## 🎯 가시성(Visibility) 조건 ### 결제 가능 상품 표시 ```javascript const isBillingProductVisible = useMemo(() => { return ( productInfo && productInfo[selectedIndex]?.pmtSuptYn === "Y" && webOSVersion >= "6.0" ); }, [productData, webOSVersion, selectedIndex]); ``` ### 결제 불가능 상품 표시 ```javascript const isUnavailableProductVisible = useMemo(() => { return ( showProductInfo && productInfo && (productInfo[selectedIndex]?.pmtSuptYn === "N" || webOSVersion < "6.0") ); }, [showProductInfo, productInfo, webOSVersion, selectedIndex]); ``` --- ## 📊 상품 매진 상태 ```javascript const isProductSoldOut = () => { if ( productInfo && productInfo.length > selectedIndex && selectedIndex >= 0 ) { return productInfo[selectedIndex]?.soldoutFlag === "Y"; } return false; }; const isSoldout = isProductSoldOut(); // UI에서 사용 ``` --- ## 🎬 ProductOption 래퍼 모든 옵션(SingleOption, UnableOption)은 ProductOption으로 감싸짐: ```jsx ``` **ProductOption 역할:** ```jsx export default function ProductOption({ children, productInfo }) { return ( {productInfo && (
{/* 로고 */} {/* 상품 ID */} {productInfo?.expsPrdtNo && (
ID: {productInfo?.expsPrdtNo}
)} {/* 상품명 */}
{productInfo?.prdtNm}
{/* 평점 + 태그 */}
)} {children}
); } ``` --- ## ⚙️ DetailPanel과의 통합 ```javascript // DetailPanel.backup.jsx에서 Theme Product 처리 {isTravelProductVisible && ( )} const isTravelProductVisible = useMemo(() => { return panelInfo?.curationId && (hotelInfos || themeData); }, [panelInfo?.curationId, hotelInfos, themeData]); ``` --- ## 📈 상태 관리 흐름 ``` 사용자 액션 ↓ 화살표 키 (▲▼◄►) 입력 ↓ setSelectedIndex() 호출 ↓ selectedIndex 상태 변경 ↓ showProductInfo 리메모이제이션 ↓ useEffect: 이미지/가격/상품정보 업데이트 ↓ UI 리렌더링 (이미지, 가격, 옵션) ↓ 로그 전송 (500ms 후) ``` --- ## 📝 요약표 | 항목 | 설명 | |------|------| | **데이터 소스** | `state.home.themeCurationDetailInfoData[]` | | **테마 정보** | `state.home.productData.themeInfo[0]` | | **선택 관리** | `selectedIndex` (배열 인덱스) | | **이미지 배열** | `productInfo[i].imgUrls600[]` | | **비디오** | `productInfo[i].prdtMediaUrl` (선택사항) | | **가격 형식** | `"원가\|할인가\|보상\|코드\|할인율"` | | **매진 여부** | `soldoutFlag: "Y"/"N"` | | **결제 여부** | `pmtSuptYn: "Y"/"N"` | | **평점** | `revwGrd: 0~5` | | **QR 코드** | `qrcodeUrl` (상품별) 또는 `detailQRCodeUrl` (결제용) | | **SMS 타입** | `"APP00204"` (테마 상품) | | **로그 타입** | `shopTpNm: "product"` (테마) vs `"hotel"` (호텔) | --- ## 🔗 관련 컴포넌트 - **ThemeProduct.jsx** : 타입 분기 (ShowProduct 호출) - **ShowProduct.jsx** : 메인 렌더링 컴포넌트 - **ThemeIndicator.jsx** : 이미지 갤러리 + 비디오 - **IndicatorOptions.jsx** : 상품 정보 + 버튼 - **ProductOption.jsx** : 로고 + 상품명 래퍼 - **ShowSingleOption.jsx** : 결제가능 상품 - **ShowUnableOption.jsx** : 결제불가 상품 - **SingleOption** : 실제 구매 UI - **UnableOption** : 구매불가 UI --- ## 🎓 핵심 포인트 1. **다중 상품 선택**: `selectedIndex`로 배열 내 상품 관리 2. **비디오 통합**: 첫 번째 이미지 위치에 비디오 플레이 버튼 3. **가격 정보**: 파이프(|)로 구분된 복합 정보 (원가, 할인가, 할인율) 4. **조건부 렌더링**: `pmtSuptYn` & `webOSVersion`으로 UI 분기 5. **상세 로깅**: 모든 사용자 상호작용 추적 (클릭, 선택, 노출) 6. **SMS 공유**: 결제불가 상품도 SMS로 정보 공유 가능 (`shopTpNm: "product"`)