diff --git a/com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md b/com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md
new file mode 100644
index 00000000..75658a6a
--- /dev/null
+++ b/com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md
@@ -0,0 +1,389 @@
+# DetailPanel 컴포넌트의 Hotels/여행상품(Theme) UI 처리 분석 보고서
+
+## 📋 개요
+
+DetailPanel 컴포넌트는 **3가지 상품 타입**을 지원합니다:
+1. **Single Product** (결제가능 상품)
+2. **Group Product** (그룹상품)
+3. **Travel/Theme Product** (여행상품/테마)
+ - `type === "theme"` : ShowProduct (테마상품)
+ - `type === "hotel"` : HotelProduct (호텔상품)
+
+---
+
+## 🏗️ 아키텍처 흐름
+
+### 1. 데이터 로딩 흐름 (DetailPanel.backup.jsx)
+
+```javascript
+// panelInfo.type에 따른 데이터 로딩
+useEffect(() => {
+ if (panelInfo?.type === "hotel") {
+ // 호텔 상세정보 요청
+ dispatch(getThemeHotelDetailInfo({
+ patnrId: panelInfo?.patnrId,
+ curationId: panelInfo?.curationId,
+ }));
+ }
+
+ if (panelInfo?.type === "theme") {
+ // 테마 상세정보 요청
+ dispatch(getThemeCurationDetailInfo({
+ patnrId: panelInfo?.patnrId,
+ curationId: panelInfo?.curationId,
+ bgImgNo: panelInfo?.bgImgNo,
+ }));
+ }
+}, [panelInfo.type, panelInfo.curationId, ...]);
+```
+
+**Redux State 구조:**
+```javascript
+// homeReducer.js
+const initialState = {
+ themeCurationDetailInfoData: [], // 테마 상품 데이터
+ themeCurationHotelDetailData: [], // 호텔 상세 데이터
+ hotelData: {}, // 호텔 통합 정보
+ productData: {}, // 테마 정보
+};
+```
+
+---
+
+## 🎨 UI 렌더링 로직
+
+### 2. ThemeProduct 컴포넌트 (라우팅)
+
+```jsx
+// ThemeProduct.jsx - 타입에 따른 조건부 렌더링
+export default function ThemeProduct({
+ themeType, // "theme" 또는 "hotel"
+ selectedIndex,
+ setSelectedIndex,
+ ...
+}) {
+ return (
+
+ {themeType === "theme" && (
+
+ )}
+ {themeType === "hotel" && (
+
+ )}
+
+ );
+}
+```
+
+---
+
+## 🏨 호텔 상품 UI 처리 (HotelProduct.jsx)
+
+### 3. HotelProduct 컴포넌트 구조
+
+#### A. 이미지 갤러리 영역
+```jsx
+
+```
+- 호텔 이미지들을 인디케이터로 표시
+- 선택된 인덱스에 따라 이미지 변경
+
+#### B. 주소/위치 정보
+```jsx
+
+```
+
+#### C. 호텔 정보 카드 영역
+
+```jsx
+
+ {/* 1. 상단 레이어: 로고 + 별점 + 등급 */}
+
+

+
+
+
+ {/* 2. 제목: [호텔타입] 호텔명 */}
+
+ [{hotelInfos[selectedIndex]?.hotelDetailInfo.hotelType}]
+ {hotelInfos[selectedIndex]?.hotelNm}
+
+
+ {/* 3. 편의시설 그리드 (최대 10개) */}
+
+ {amenitiesInfos && amenitiesInfos.map((item) => (
+
+

+
{item.lgAmntNm}
+
+ ))}
+
+
+ {/* 4. 하단 레이어: 예약 정보 + 가격 + QR코드 */}
+
+
+
+ {nights}Nights {adultsCount}Adults
+
+
+ {hotelInfos[selectedIndex]?.hotelDetailInfo.roomType}
+
+
+
From
+
+ {hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign}
+ {hotelInfos[selectedIndex]?.hotelDetailInfo.price}
+
+
+
+
+
+
+
+
+ {/* 5. CTA 버튼: "SEE MORE" */}
+
+ {$L("SEE MORE")}
+
+
+```
+
+---
+
+## 🎯 데이터 매핑 상세 설명
+
+### 4. 호텔 데이터 구조 (Redux에서)
+
+```javascript
+hotelInfos: [
+ {
+ hotelId: "string",
+ hotelNm: "Hotel Name",
+ hotelImgUrl: "url",
+ imgUrls600: ["url1", "url2", ...],
+ qrcodeUrl: "qr-code-data",
+ hotelDetailInfo: {
+ hotelAddr: "Address",
+ hotelType: "Luxury/Budget/etc",
+ price: "299",
+ currencySign: "$",
+ revwGrd: 4.5, // 평점
+ nights: 2,
+ adultsCount: 2,
+ roomType: "Double Room",
+ amenities: ["amntId1", "amntId2", ...],
+ imgUrls: ["url1", "url2", ...]
+ }
+ },
+ // ... 여러 호텔
+]
+
+hotelData: {
+ hotelInfo: {
+ curationId: "string",
+ curationNm: "Theme Name",
+ patncNm: "Partner Name",
+ patnrId: "string",
+ patncLogoPath: "url"
+ },
+ amenities: [
+ {
+ amntId: "1",
+ lgAmntNm: "Free WiFi",
+ lgAmntImgUrl: "icon-url"
+ },
+ // ... 편의시설 목록
+ ]
+}
+```
+
+---
+
+## ⭐ 별점 등급 매핑
+
+```javascript
+useEffect(() => {
+ let label = "";
+ let rating = hotelInfos[selectedIndex]?.hotelDetailInfo.revwGrd;
+
+ if (rating !== undefined) {
+ if (rating <= 2.4) label = $L("Fair");
+ else if (rating >= 2.5 && rating <= 3.4) label = $L("Good");
+ else if (rating >= 3.5 && rating <= 4.4) label = $L("Very Good");
+ else if (rating >= 4.5 && rating <= 5) label = $L("Excellent");
+ }
+ setLabel(label);
+}, [selectedIndex, hotelInfos]);
+```
+
+---
+
+## 🛏️ 편의시설 처리 로직
+
+```javascript
+const getAmenitiesInfo = () => {
+ const matchedData = new Set();
+ const amenitiesMap = new Map();
+
+ // 1. 전체 편의시설을 맵으로 변환
+ hotelData.amenities.forEach((item) => {
+ amenitiesMap.set(item.amntId, item);
+ });
+
+ // 2. 현재 호텔에 포함된 편의시설 필터링 (최대 10개)
+ hotelInfos[selectedIndex]?.hotelDetailInfo.amenities.forEach((amntId) => {
+ if (amenitiesMap.has(amntId) && matchedData.size < 10) {
+ matchedData.add(amenitiesMap.get(amntId));
+ }
+ });
+
+ // 3. 같은 카테고리의 중복 제거
+ let amenitiesArr = Array.from(matchedData);
+ amenitiesArr = amenitiesArr.filter((item, index, self) => {
+ return index === self.findIndex((t) => t.lgAmntNm === item.lgAmntNm);
+ });
+
+ setAmenitiesInfos(amenitiesArr);
+};
+```
+
+---
+
+## 📊 스타일링 (HotelProduct.module.less)
+
+### 레이아웃 구성:
+
+| 영역 | 크기 | 설명 |
+|------|------|------|
+| **themeContainer** | 774px × 930px | 이미지 갤러리 컨테이너 |
+| **optionContainer** | 1026px × 990px | 호텔 정보 카드 |
+| **topLayer** | 100% × auto | 로고 + 별점 섹션 |
+| **title** | 100% × 84px | 호텔명 (2줄 말줄임) |
+| **amenitiesCotainer** | 846px × 344px | 편의시설 그리드 |
+| **amenitiesBox** | 138px × 138px | 개별 편의시설 |
+| **bottomLayer** | auto × auto | 예약정보 + 가격 + QR코드 |
+| **price** | auto × auto | 가격 표시 (큰 크기, 분홍색) |
+| **qrcodeContainer** | 192px × 192px | QR코드 영역 |
+
+---
+
+## 🔄 선택 인덱스 관리
+
+```javascript
+useEffect(() => {
+ // URL 파라미터로 특정 호텔이 지정된 경우
+ if (hotelInfos && hotelInfos.length > 0 && panelInfo?.themeHotelId) {
+ for (let i = 0; i < hotelInfos.length; i++) {
+ if (hotelInfos[i].hotelId === panelInfo?.themeHotelId) {
+ setSelectedIndex(i);
+ }
+ }
+ }
+}, [hotelInfos, panelInfo?.themeHotelId]);
+```
+
+---
+
+## 📱 이미지 길이 업데이트
+
+```javascript
+useEffect(() => {
+ if ((hotelInfos && hotelInfos.length > 0) && selectedIndex !== undefined) {
+ if (panelInfo?.type === "hotel") {
+ const imgUrls600 = hotelInfos[selectedIndex]?.imgUrls600 || [];
+ dispatch(getProductImageLength({ imageLength: imgUrls600.length }));
+ }
+ }
+}, [dispatch, selectedIndex, hotelInfos]);
+```
+
+---
+
+## 🔔 로깅 및 추적
+
+### A. 상세정보 조회 로그
+```javascript
+const params = {
+ befPrice: selectedHotelInfo?.hotelInfo?.hotelDetailInfo?.price,
+ curationId: selectedHotelInfo?.curationId,
+ curationNm: selectedHotelInfo?.curationNm,
+ expsOrd: selectedIndex + 1,
+ logTpNo: LOG_TP_NO.PRODUCT.PRODUCT_DETAIL_IMAGE,
+ prdtId: selectedHotelInfo?.hotelInfo?.hotelId,
+ prdtNm: selectedHotelInfo?.hotelInfo?.hotelNm,
+ revwGrd: selectedHotelInfo?.hotelInfo?.hotelDetailInfo?.revwGrd,
+ // ... 더 많은 필드
+};
+dispatch(sendLogProductDetail(params));
+```
+
+### B. SMS 보내기 버튼 로그
+```javascript
+const params = {
+ patncNm: selectedHotelInfo.patncNm,
+ prdtId: selectedHotelInfo.hotelInfo?.hotelDetailInfo?.hotelId,
+ prdtNm: selectedHotelInfo.hotelInfo?.hotelDetailInfo?.hotelNm,
+ shopTpNm: "hotel", // 호텔 타입 마킹
+ shopByMobileFlag: "Y",
+ price: selectedHotelInfo.hotelInfo?.hotelDetailInfo?.price,
+ // ...
+};
+dispatch(sendLogShopByMobile(params));
+```
+
+---
+
+## 💡 주요 특징
+
+| 기능 | 구현 |
+|------|------|
+| **동적 인덱스 선택** | 화살표 키로 호텔 선택 변경 가능 |
+| **이미지 갤러리** | ThemeIndicator 컴포넌트로 여러 이미지 표시 |
+| **평점 표시** | StarRating 컴포넌트로 시각화 |
+| **편의시설 필터링** | 최대 10개, 중복 제거 |
+| **가격 표시** | 통화 기호 + 숫자 (분홍색 강조) |
+| **예약 정보** | 숙박일 수 + 성인 수 자동 포맷팅 |
+| **QR코드** | 호텔 상세정보 링크 제공 |
+| **SMS 기능** | "SEE MORE" 버튼으로 SMS 팝업 오픈 |
+| **Spotlight** | 키보드 네비게이션 지원 |
+
+---
+
+## 🔗 관련 컴포넌트
+
+- **ThemeProduct.jsx** : 라우팅 (theme vs hotel)
+- **HotelProduct.jsx** : 호텔 UI 렌더링
+- **ShowProduct.jsx** : 테마 상품 UI 렌더링
+- **ThemeIndicator** : 이미지 갤러리
+- **IndicatorOptions** : 주소/위치 표시
+- **StarRating** : 별점 표시
+- **TQRCode** : QR코드 생성
+- **TButton** : 호텔/테마 액션 버튼
+
+---
+
+## 📝 요약
+
+DetailPanel의 호텔 UI 처리는 **Redux 상태 기반의 선언적 렌더링**으로, 선택된 인덱스(`selectedIndex`)에 따라 해당 호텔의 이미지, 정보, 가격, 편의시설을 동적으로 표시합니다. 모든 상호작용(이미지 변경, 버튼 클릭)은 상세한 로깅을 통해 추적되며, SMS 연동으로 호텔 정보를 공유할 수 있습니다.
diff --git a/com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md b/com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md
new file mode 100644
index 00000000..d613175d
--- /dev/null
+++ b/com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md
@@ -0,0 +1,409 @@
+# DetailPanel 호텔/여행상품 UI 처리 - 시각적 가이드
+
+## 🔄 데이터 흐름 다이어그램
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ DetailPanel (Main) │
+└────────────────┬────────────────────────────────────────────────┘
+ │
+ ┌────────┴─────────┐
+ │ panelInfo.type │
+ └────────┬─────────┘
+ │
+ ┌───────────┼───────────┐
+ │ │ │
+ "theme" "hotel" "product"
+ │ │ │
+ ▼ ▼ ▼
+┌─────────┐ ┌────────┐ ┌─────────┐
+│ShowPrdt │ │HotelPdt│ │SinglePdt│
+└────┬────┘ └────┬───┘ └────┬────┘
+ │ │ │
+ ▼ ▼ ▼
+Redux Actions Redux Actions Redux Actions
+ ┌─────────────────────────────────────────┐
+ │ getThemeCurationDetailInfo() [THEME] │
+ │ getThemeHotelDetailInfo() [HOTEL] │
+ │ getMainCategoryDetail() [PRODUCT] │
+ └──────────────┬──────────────────────────┘
+ │
+ ▼
+ ┌────────────────────┐
+ │ Redux Reducer │
+ │ │
+ │ - productData │
+ │ - hotelData │
+ │ - hotelInfos[] │
+ │ - themeInfos[] │
+ └────────────────────┘
+ │
+ ▼
+ ┌────────────────────┐
+ │ UI Components │
+ │ │
+ │ - ThemeIndicator │
+ │ - HotelProduct │
+ │ - ShowProduct │
+ └────────────────────┘
+```
+
+---
+
+## 🏨 HotelProduct UI 구조
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ HotelProduct Component │
+└──────────────────┬───────────────────────────────────────────────┘
+ │
+ ┌──────────┴──────────┐
+ │ │
+ ▼ ▼
+ ┌─────────────┐ ┌──────────────────┐
+ │ThemeIndicator │ │OptionContainer │
+ ├─────────────┤ ├──────────────────┤
+ │ 이미지 갤러리 │ │ 호텔 정보 카드 │
+ │ 774×930px │ │ 1026×990px │
+ │ │ │ │
+ │ ┌─────────┐ │ │ ┌────────────┐ │
+ │ │ 이미지1 │ │ │ │ 로고 + 별점 │ │
+ │ │ │ │ │ ├────────────┤ │
+ │ │ ▲ ▼ │ │ │ │ 호텔명 │ │
+ │ │ [•••] │ │ │ │ [타입]호텔 │ │
+ │ │ │ │ │ ├────────────┤ │
+ │ │ ◀ ───► │ │ │ │ 편의시설 │ │
+ │ │ 이미지2 │ │ │ │ [아이콘] │ │
+ │ │ 이미지3 │ │ │ │ [아이콘] │ │
+ │ └─────────┘ │ │ ├────────────┤ │
+ │ │ │ │ 예약정보 │ │
+ └─────────────┘ │ │ N Nights │ │
+ │ │ M Adults │ │
+ │ │ Room Type │ │
+ │ ├────────────┤ │
+ │ │ From │ │
+ │ │ $299 ◀─────┼──┼→ QR Code
+ │ │ (분홍색) │ │ 160×160px
+ │ ├────────────┤ │
+ │ │[SEE MORE]btn│ │
+ │ └────────────┘ │
+ └──────────────────┘
+```
+
+---
+
+## 📊 컴포넌트 계층도 (DetailPanel)
+
+```
+DetailPanel
+├── TPanel
+│ ├── THeader (제목: 호텔명/테마명)
+│ └── TBody (스크롤 가능 영역)
+│ └── ThemeProduct (조건부 렌더링)
+│ ├── ShowProduct
+│ │ ├── Container (Spotlight)
+│ │ │ ├── ThemeIndicator
+│ │ │ └── IndicatorOptions
+│ │ └── optionContainer
+│ │ ├── ShowSingleOption
+│ │ └── ShowUnableOption
+│ │
+│ └── HotelProduct ◀── 호텔 전용
+│ ├── Container (Spotlight)
+│ │ ├── ThemeIndicator
+│ │ └── IndicatorOptions
+│ └── optionContainer
+│ ├── topLayer
+│ │ ├── img (로고)
+│ │ └── rating (별점)
+│ ├── title
+│ ├── amenitiesCotainer
+│ │ └── amenitiesBox[] (반복)
+│ ├── bottomLayer
+│ │ ├── 예약정보
+│ │ ├── 가격
+│ │ └── QR코드
+│ └── TButton (SEE MORE)
+│
+├── YouMayLike (조건부)
+└── MobileSendPopUp (조건부)
+```
+
+---
+
+## 🎯 선택 인덱스 상태 관리
+
+```javascript
+HotelProduct
+ │
+ ├─ selectedIndex (props)
+ │ └─ 현재 선택된 호텔의 배열 인덱스
+ │ ├─ hotelInfos[selectedIndex] → 호텔 데이터
+ │ ├─ hotelInfos[selectedIndex].hotelNm → 호텔명
+ │ ├─ hotelInfos[selectedIndex].hotelDetailInfo.price → 가격
+ │ └─ hotelInfos[selectedIndex].hotelDetailInfo.amenities → 편의시설 ID 배열
+ │
+ └─ setSelectedIndex (callback)
+ └─ ThemeIndicator 화살표 클릭 시 호출
+ ├─ UP / LEFT → selectedIndex - 1
+ └─ DOWN / RIGHT → selectedIndex + 1
+```
+
+---
+
+## 💾 Redux 상태 구조 (호텔 데이터)
+
+```javascript
+state.home = {
+ // 호텔 목록 (배열)
+ themeCurationHotelDetailData: [
+ {
+ hotelId: "HOTEL001",
+ hotelNm: "Luxury Hotel",
+ hotelImgUrl: "http://...",
+ imgUrls600: ["url1", "url2", "url3"],
+ qrcodeUrl: "qrcode-data",
+ hotelDetailInfo: {
+ hotelAddr: "123 Main St, NYC",
+ hotelType: "5-star",
+ price: "299.99",
+ currencySign: "$",
+ revwGrd: 4.8, // 평점
+ nights: 2, // 숙박일 수
+ adultsCount: 2, // 성인 수
+ roomType: "Deluxe Double Room",
+ amenities: ["AMN001", "AMN002", "AMN005"], // 편의시설 ID
+ imgUrls: ["url1", "url2", "url3"]
+ }
+ },
+ { /* 다음 호텔 */ }
+ ],
+
+ // 호텔 통합 정보
+ hotelData: {
+ hotelInfo: {
+ curationId: "CURATION001",
+ curationNm: "Dubai Vacation",
+ patncNm: "Travel Partner",
+ patnrId: "PARTNER001",
+ patncLogoPath: "http://logo-url"
+ },
+ amenities: [
+ {
+ amntId: "AMN001",
+ lgAmntNm: "Free WiFi",
+ lgAmntImgUrl: "http://icon-wifi.png"
+ },
+ {
+ amntId: "AMN002",
+ lgAmntNm: "Swimming Pool",
+ lgAmntImgUrl: "http://icon-pool.png"
+ },
+ {
+ amntId: "AMN005",
+ lgAmntNm: "Spa",
+ lgAmntImgUrl: "http://icon-spa.png"
+ }
+ // ... 더 많은 편의시설
+ ]
+ }
+}
+```
+
+---
+
+## 🎨 스타일 적용 흐름
+
+```
+HotelProduct.module.less
+│
+├─ .themeContainer
+│ └─ ThemeIndicator 감싸기
+│ ├─ width: 774px
+│ └─ height: 930px
+│
+├─ .optionContainer
+│ └─ 호텔 정보 카드
+│ ├─ width: 1026px
+│ ├─ height: 990px
+│ ├─ padding: 30px 120px 60px 60px
+│ └─ background: #f9f9f9
+│
+├─ .topLayer
+│ └─ display: flex
+│ ├─ justify-content: space-between
+│ └─ img (로고 42×42px)
+│ rating (별점 + 텍스트)
+│
+├─ .title
+│ ├─ font-size: 36px
+│ ├─ font-weight: bold
+│ ├─ overflow: ellipsis (2줄 말줄임)
+│ └─ color: #333
+│
+├─ .amenitiesCotainer
+│ ├─ width: 846px
+│ ├─ height: 344px
+│ ├─ display: flex
+│ ├─ flex-wrap: wrap
+│ ├─ background: #f2f2f2
+│ └─ border: 1px solid #dadada
+│ │
+│ └─ .amenitiesBox (반복)
+│ ├─ width: 138px
+│ ├─ height: 138px
+│ ├─ background: white
+│ ├─ border-radius: 6px
+│ ├─ display: flex (column)
+│ └─ img: 36×32px, p: 18px font
+│
+├─ .bottomLayer
+│ ├─ display: flex
+│ ├─ justify-content: space-between
+│ │
+│ ├─ 좌측 (예약정보 + 가격)
+│ │ ├─ .today: 24px, bold, #222
+│ │ ├─ .roomType: 24px, #808080
+│ │ └─ .price
+│ │ ├─ title: 36px, #222
+│ │ └─ value: 44px, bold, #c70850 (분홍색)
+│ │
+│ └─ 우측 (QR코드)
+│ └─ .qrcodeContainer
+│ ├─ width: 192px
+│ ├─ height: 192px
+│ ├─ box-shadow: 0px 3px 6px rgba(0,0,0,0.1)
+│ └─ border: 1px solid #dadada
+│
+└─ .tbutton
+ ├─ size: extra
+ ├─ background: 테마 색상
+ └─ margin-top: auto
+```
+
+---
+
+## 🔄 이벤트 흐름
+
+```
+사용자 입력
+│
+├─ 화살표 키 (▲▼◄►)
+│ └─ Spotlight 포커스 변경
+│ └─ setSelectedIndex() 호출
+│ └─ hotelInfos[newIndex] 데이터 로드
+│ └─ UI 업데이트 (이미지, 가격, 편의시설)
+│
+└─ [SEE MORE] 버튼 클릭
+ └─ handleSMSClick()
+ ├─ 로깅 (sendLogTotalRecommend, sendLogShopByMobile)
+ └─ MobileSendPopUp 오픈
+ └─ SMS로 호텔 정보 전송
+```
+
+---
+
+## 📱 반응형 고려사항
+
+### 이미지 크기
+- **thumbnailUrls**: `imgUrls600[]` (600px 기준)
+- **갤러리**: 774×930px 고정
+- **카드**: 1026×990px 고정
+- **QR코드**: 160×160px 표시용
+
+### 텍스트 보호
+```javascript
+// 호텔명 2줄 말줄임
+.title {
+ .elip(@clamp:2); // max 2 lines
+}
+
+// 방 타입 3줄 말줄임
+.roomType {
+ .elip(@clamp:3); // max 3 lines
+}
+```
+
+---
+
+## ⚠️ 주의사항
+
+1. **QR 코드 이슈**: 호텔 QR 코드가 서버에서 다르게 내려와 에러 발생
+ ```javascript
+ // TODO: 해결되면 주석제거
+
+ ```
+
+2. **편의시설 필터링**:
+ - 최대 10개로 제한
+ - 같은 카테고리명 중복 제거
+
+3. **평점 없음 처리**:
+ ```javascript
+ if (rating !== undefined) {
+ // 평점 등급 처리
+ }
+ ```
+
+4. **이미지 없음 처리**:
+ ```javascript
+ const imgUrls600 = hotelInfos[selectedIndex]?.imgUrls600 || [];
+ ```
+
+---
+
+## 📊 상태 관리 체크리스트
+
+- ✅ 호텔 데이터 로드 (useEffect with panelInfo.type)
+- ✅ 선택 인덱스 초기화 (panelInfo?.themeHotelId 기반)
+- ✅ 이미지 길이 업데이트 (이미지 스와이프 관련)
+- ✅ 편의시설 정렬 (최대 10개, 중복 제거)
+- ✅ 평점 등급 계산
+- ✅ SMS 로깅 (shopByMobileFlag: "Y", shopTpNm: "hotel")
+- ✅ 상세 정보 로깅 (logTpNo: PRODUCT.PRODUCT_DETAIL_IMAGE)
+- ✅ cleanup (clearThemeDetail, clearProductDetail)
+
+---
+
+## 🔗 관련 액션 (Redux Actions)
+
+```javascript
+// src/actions/homeActions.js
+
+// 호텔 데이터 조회
+getThemeHotelDetailInfo({ patnrId, curationId })
+ → dispatch(GET_THEME_HOTEL_DETAIL_INFO, hotelData)
+
+// 테마 데이터 조회
+getThemeCurationDetailInfo({ patnrId, curationId, bgImgNo })
+ → dispatch(GET_THEME_CURATION_DETAIL_INFO, themeData)
+
+// 상세 정보 초기화
+clearThemeDetail()
+ → 호텔/테마 데이터 리셋
+```
+
+---
+
+## 📝 필드 매핑 요약
+
+| UI 요소 | 데이터 경로 | 설명 |
+|---------|-----------|------|
+| 로고 | `hotelData.hotelInfo.patncLogoPath` | 파트너 로고 |
+| 별점 | `hotelInfos[i].hotelDetailInfo.revwGrd` | 0~5 평점 |
+| 별점 등급 | 계산됨 (2.5~3.4=Good 등) | 동적 매핑 |
+| 호텔명 | `hotelInfos[i].hotelNm` | 호텔 이름 |
+| 호텔 타입 | `hotelInfos[i].hotelDetailInfo.hotelType` | 5-Star, Luxury 등 |
+| 이미지 | `hotelInfos[i].hotelDetailInfo.imgUrls[]` | 갤러리 이미지 |
+| 주소 | `hotelInfos[i].hotelDetailInfo.hotelAddr` | 지도 위치 |
+| 편의시설 | `hotelInfos[i].hotelDetailInfo.amenities[]` | ID 배열 → 매핑 |
+| 숙박일 | `hotelInfos[i].hotelDetailInfo.nights` | N Nights |
+| 성인 수 | `hotelInfos[i].hotelDetailInfo.adultsCount` | M Adults |
+| 방 타입 | `hotelInfos[i].hotelDetailInfo.roomType` | Deluxe Double Room |
+| 가격 | `hotelInfos[i].hotelDetailInfo.price` | 숫자 (문자열) |
+| 통화 | `hotelInfos[i].hotelDetailInfo.currencySign` | $ / € / ¥ |
+| QR 코드 | `hotelInfos[i].qrcodeUrl` | QR 데이터 |
diff --git a/com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md b/com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md
new file mode 100644
index 00000000..cf0fbf25
--- /dev/null
+++ b/com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md
@@ -0,0 +1,692 @@
+# 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"`)
diff --git a/com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md b/com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md
new file mode 100644
index 00000000..d627320d
--- /dev/null
+++ b/com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md
@@ -0,0 +1,437 @@
+# Theme Product UI - 시각적 구조 가이드
+
+## 📊 ShowProduct 렌더링 구조
+
+```
+ShowProduct (Main Component)
+│
+├─ Container (Spotlight)
+│ └─ spotlight-IndicatorContainer
+│ │
+│ ├─ ThemeIndicator
+│ │ ├─ [메인 이미지 영역] 834×930px
+│ │ │ ├─ 비디오 플레이 버튼 (선택사항)
+│ │ │ ├─ 메인 이미지
+│ │ │ └─ [매진] 배지 (soldoutFlag="Y"일 때)
+│ │ │
+│ │ └─ [썸네일 스크롤] (TVirtualGridList)
+│ │ ├─ 이미지 높이: 152px 고정
+│ │ ├─ 상하 스크롤 네비게이션
+│ │ └─ 현재 선택 강조 표시
+│ │
+│ └─ IndicatorOptions
+│ ├─ 로고 + ID (상단)
+│ ├─ 상품명 (제목)
+│ ├─ 평점 + 태그
+│ ├─ 버튼 영역
+│ │ ├─ [DESCRIPTION] 버튼
+│ │ ├─ [RETURNS & EXCHANGES] 버튼
+│ │ └─ [SHOP BY MOBILE] 버튼
+│ ├─ QR 코드 (160×160px)
+│ └─ 설명 팝업 (조건부)
+│
+└─ optionContainer (하단)
+ ├─ ShowSingleOption (결제가능 상품)
+ │ └─ ProductOption (래퍼)
+ │ └─ SingleOption
+ │ ├─ 옵션 선택 (좌석, 이용일 등)
+ │ ├─ 수량 선택
+ │ ├─ 가격 표시
+ │ └─ [구매] 버튼
+ │
+ └─ ShowUnableOption (결제불가 상품)
+ └─ ProductOption (래퍼)
+ └─ UnableOption
+ ├─ 구매 불가 메시지
+ ├─ SMS 공유 옵션
+ └─ [SHOP BY MOBILE] 버튼
+```
+
+---
+
+## 🖼️ ThemeIndicator 상세 구조
+
+```
+ThemeIndicator Container
+│
+├─ [메인 디스플레이 영역]
+│ │
+│ ├─ 비디오 지원 여부 감지
+│ │ ├─ YES → [비디오 플레이 버튼]
+│ │ │ (아래 이미지들이 [1]부터 시작)
+│ │ │
+│ │ └─ NO → [이미지 [0]부터 표시]
+│ │
+│ └─ 선택된 이미지/비디오
+│ └─ imageSelectedIndex 기반 렌더링
+│
+├─ [썸네일 스크롤 영역]
+│ │
+│ └─ TVirtualGridList
+│ ├─ 항목 높이: 152px (scaleH 적용)
+│ ├─ 아이템:
+│ │ ├─ [0] - 비디오 (있을 경우)
+│ │ ├─ [1] - 이미지 1
+│ │ ├─ [2] - 이미지 2
+│ │ └─ [n] - 이미지 n
+│ │
+│ ├─ 상하 화살표 네비게이션
+│ └─ 현재 선택 (selectedIndex) 강조
+│
+└─ [화살표 버튼] (선택사항)
+ ├─ UP/LEFT → selectedIndex - 1
+ └─ DOWN/RIGHT → selectedIndex + 1
+```
+
+---
+
+## 📐 레이아웃 치수
+
+```
+┌────────────────────────────────────────────────┐
+│ DetailPanel 레이아웃 │
+├────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────────┐ ┌──────────────────┐ │
+│ │ │ │ │ │
+│ │ ThemeIndicator │ │ IndicatorOptions │ │
+│ │ 834 × 930px │ │ 정보 + 버튼 │ │
+│ │ │ │ │ │
+│ │ ┌─────────────┐ │ ├──────────────────┤ │
+│ │ │메인 이미지 │ │ │ 로고 + ID │ │
+│ │ │ 600×600px │ │ │ 상품명 │ │
+│ │ │ │ │ │ 평점 + 태그 │ │
+│ │ └─────────────┘ │ │ │ │
+│ │ │ │ [설명][교환] │ │
+│ │ ┌─────────────┐ │ │ [SMS] │ │
+│ │ │ 썸네일 │ │ │ │ │
+│ │ │ 150×150px │ │ │ QR코드 160×160 │ │
+│ │ │ (반복) │ │ │ │ │
+│ │ └─────────────┘ │ └──────────────────┘ │
+│ │ │ │
+│ └─────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────┐ │
+│ │ ShowSingleOption / ShowUnableOption │ │
+│ │ (ProductOption으로 감싸짐) │ │
+│ │ │ │
+│ │ ProductOption (헤더) │ │
+│ │ ├─ 로고 아이콘 │ │
+│ │ ├─ 상품명 │ │
+│ │ ├─ 평점 + 태그 │ │
+│ │ └───────────────────────────────── │ │
+│ │ │ │
+│ │ SingleOption / UnableOption (본체) │ │
+│ │ ├─ [옵션 선택 / 구매불가 메시지] │ │
+│ │ ├─ 수량 / SMS 공유 │ │
+│ │ ├─ 가격 │ │
+│ │ └─ [구매 / SEE MORE] 버튼 │ │
+│ │ │ │
+│ └──────────────────────────────────────────┘ │
+│ │
+└────────────────────────────────────────────────┘
+```
+
+---
+
+## 🔄 데이터 흐름 (Theme Product)
+
+```
+Redux State
+│
+├─ state.home.themeCurationDetailInfoData
+│ └─ Array of Products
+│ ├─ [0] { prdtId, prdtNm, imgUrls600[], priceInfo, ... }
+│ ├─ [1] { ... }
+│ └─ [n] { ... }
+│
+├─ state.home.productData
+│ └─ { themeInfo[0] { curationId, curationNm, ... } }
+│
+└─ selectedIndex (상태 변수)
+ └─ 0, 1, 2, ... n
+ └─ productInfo[selectedIndex] ← 현재 상품
+```
+
+**선택된 상품 데이터 조립:**
+```javascript
+showProductInfo = {
+ ...productInfo[selectedIndex], // 모든 상품 정보
+ curationId: themeInfo.curationId, // 테마 ID 추가
+ curationNm: themeInfo.curationNm, // 테마 이름 추가
+ expsOrd: `${selectedIndex + 1}`, // 순번 (1부터)
+}
+```
+
+---
+
+## 📋 priceInfo 구조
+
+```javascript
+// priceInfo = "299|199|Y|discount10|10%"
+
+┌──────────────────────────────────────────────────────┐
+│ 원가 │ 할인가 │ 보상적용 │ 할인코드 │ 할인율 │
+│ $299 │ $199 │ Y │ discount10 │ 10% │
+└──────────────────────────────────────────────────────┘
+ [0] [1] [2] [3] [4]
+
+// 파싱:
+split("|")[0] → befPrice (원가)
+split("|")[1] → lastPrice (할인가)
+split("|")[2] → rewdAplyFlag (보상적용여부)
+split("|")[3] → 할인코드 (일반적으로 로그에는 미사용)
+split("|")[4] → discountRate (할인율%)
+```
+
+---
+
+## 🎬 비디오/이미지 선택 로직
+
+### Scenario A: 비디오 O + 이미지 3개
+
+```
+서버 응답:
+imgUrls600 = [
+ "play-button.png", // [0] 비디오 플레이 아이콘
+ "image1.png", // [1] 첫 번째 이미지
+ "image2.png", // [2] 두 번째 이미지
+ "image3.png" // [3] 세 번째 이미지
+]
+
+UI 표시:
+- imageSelectedIndex = 0 → [비디오 플레이] 표시 + 자동 재생
+- imageSelectedIndex = 1 → imgUrls600[0] = image1 표시
+- imageSelectedIndex = 2 → imgUrls600[1] = image2 표시
+- imageSelectedIndex = 3 → imgUrls600[2] = image3 표시
+```
+
+### Scenario B: 비디오 X + 이미지 3개
+
+```
+서버 응답:
+imgUrls600 = [
+ "image1.png",
+ "image2.png",
+ "image3.png"
+]
+
+UI 표시:
+- imageSelectedIndex = 0 → imgUrls600[0] = image1 표시
+- imageSelectedIndex = 1 → imgUrls600[1] = image2 표시
+- imageSelectedIndex = 2 → imgUrls600[2] = image3 표시
+```
+
+---
+
+## 🎛️ 조건부 렌더링 조건
+
+```
+결제 가능 상품 (ShowSingleOption)
+├─ pmtSuptYn === "Y"
+└─ webOSVersion >= "6.0"
+ └─ YES → 구매 옵션 + 수량 + 버튼
+ └─ NO → 다음 조건 확인
+
+
+결제 불가능 상품 (ShowUnableOption)
+├─ pmtSuptYn === "N" OR
+└─ webOSVersion < "6.0"
+ └─ YES → "구매불가" 메시지 + SMS 공유
+```
+
+---
+
+## 🔔 이벤트 흐름
+
+```
+사용자 입력
+│
+├─ 화살표 키 (▲▼)
+│ └─ setSelectedIndex(newIndex) 호출
+│ └─ showProductInfo 리메모이제이션
+│ └─ useEffect 발동
+│ ├─ 이미지 변경 (imageSelectedIndex 업데이트)
+│ ├─ 가격 업데이트
+│ ├─ 옵션 업데이트
+│ └─ 로그 전송 (1초 후)
+│
+├─ [설명] 버튼
+│ └─ descriptionClick(label, description)
+│ ├─ setTabLabel([label])
+│ ├─ setDescription(description)
+│ ├─ dispatch(setShowPopup(descriptionPopup))
+│ ├─ handleIndicatorOptions() 호출
+│ └─ 로그 전송
+│
+├─ [SMS] 버튼
+│ └─ handleSMSClick()
+│ ├─ dispatch(setShowPopup(smsPopup))
+│ ├─ handleMobileSendPopupOpen() 호출
+│ └─ 로그 전송
+│
+└─ 이미지 클릭 (썸네일)
+ └─ setImageSelectedIndex(index)
+ └─ 해당 이미지 확대 표시
+```
+
+---
+
+## 📍 ProductOption 래퍼 역할
+
+```
+ProductOption (공통 헤더)
+│
+├─ contentHeader
+│ │
+│ ├─ topLayer
+│ │ ├─ 파트너 로고 (CustomImage)
+│ │ └─ 상품 ID (expsPrdtNo)
+│ │
+│ ├─ title
+│ │ └─ 상품명 (prdtNm)
+│ │ (HTML 태그 제거 후 표시)
+│ │
+│ └─ bottomLayer
+│ ├─ StarRating (revwGrd)
+│ └─ ProductTag (상품 태그)
+│
+└─ children
+ └─ SingleOption 또는 UnableOption
+ (본체 컴포넌트)
+```
+
+---
+
+## 🏪 SingleOption vs UnableOption
+
+### SingleOption (결제 가능)
+```
+┌─────────────────────────────────┐
+│ ProductOption (공통 헤더) │
+├─────────────────────────────────┤
+│ │
+│ ┌─ 옵션 선택 영역 │
+│ │ ├─ 이용일자 선택 │
+│ │ ├─ 좌석/등급 선택 │
+│ │ └─ 기타 옵션 │
+│ │ │
+│ ├─ 수량 선택 │
+│ │ ├─ [−] 1 [+] │
+│ │ └─ 수량: 1 │
+│ │ │
+│ ├─ 가격 영역 │
+│ │ ├─ 원가: $299 │
+│ │ └─ 할인가: $199 │
+│ │ │
+│ └─ [구매] 버튼 │
+│ └─ spotlight: "buy_Btn" │
+│ │
+└─────────────────────────────────┘
+```
+
+### UnableOption (결제 불가)
+```
+┌─────────────────────────────────┐
+│ ProductOption (공통 헤더) │
+├─────────────────────────────────┤
+│ │
+│ ┌─ 상태 메시지 │
+│ │ └─ "결제 불가능합니다" │
+│ │ (또는 버전 미지원) │
+│ │ │
+│ ├─ 대체 옵션 │
+│ │ ├─ 가격 표시 │
+│ │ └─ SMS 공유 옵션 │
+│ │ │
+│ └─ [SHOP BY MOBILE] 버튼 │
+│ └─ SMS로 정보 전송 │
+│ smsTpCd: "APP00204" │
+│ │
+└─────────────────────────────────┘
+```
+
+---
+
+## 📊 로깅 데이터 매핑
+
+### 상품 상세 로그 (productDetailImage)
+```javascript
+{
+ expsOrd: "1", // 상품 순번 (1부터)
+ prdtId: "PROD001",
+ prdtNm: "Opera Show",
+ catCd: "CATE001",
+ catNm: "Performance",
+ befPrice: "299", // 원가
+ lastPrice: "199", // 할인가
+ rewdAplyFlag: "Y", // 보상 적용
+ revwGrd: 4.5, // 평점
+ tsvFlag: "Y", // 오늘의 특가
+ logTpNo: 2301, // PRODUCT.PRODUCT_DETAIL_IMAGE
+}
+```
+
+### SMS 전송 로그 (shopByMobile)
+```javascript
+{
+ shopTpNm: "product", // 상품 타입
+ shopByMobileFlag: "Y", // SMS 전송 플래그
+ prdtId: "PROD001",
+ prdtNm: "Opera Show",
+ showId: "SHOW001", // 쇼 ID (테마 전용)
+ showNm: "Opera Night", // 쇼 이름 (테마 전용)
+ patncNm: "Broadway Partners",
+ price: "199",
+ logTpNo: 1401, // SHOP_BY_MOBILE.SHOP_BY_MOBILE
+}
+```
+
+---
+
+## 🔗 상태 관리 체크리스트
+
+- ✅ 상품 목록 로드 (getThemeCurationDetailInfo)
+- ✅ 선택 인덱스 관리 (selectedIndex)
+- ✅ 이미지 선택 관리 (imageSelectedIndex)
+- ✅ 비디오 자동 재생 (autoPlaying)
+- ✅ 매진 상태 확인 (soldoutFlag)
+- ✅ 결제 가능 여부 (pmtSuptYn + webOSVersion)
+- ✅ 설명 팝업 상태 (popupVisible, activePopup)
+- ✅ 로그 전송 (1초 딜레이)
+- ✅ Spotlight 포커스 관리
+
+---
+
+## 📝 필드 매핑 테이블
+
+| UI 요소 | 데이터 경로 | 타입 | 설명 |
+|---------|-----------|------|------|
+| 메인 이미지 | `imgUrls600[imageSelectedIndex]` | URL | 선택된 이미지 표시 |
+| 썸네일 | `imgUrls600[]` | 배열 | 스크롤 갤러리 |
+| 상품명 | `prdtNm` | 문자열 | HTML 태그 제거 후 표시 |
+| 파트너 로고 | `patncLogoPath` | URL | 폴백: 기본 로고 |
+| 평점 | `revwGrd` | 숫자 | 0~5 |
+| 상품 ID | `expsPrdtNo` | 문자열 | 선택사항 |
+| 원가 | `priceInfo.split(\"|\"[0]` | 숫자 | 할인 전 가격 |
+| 할인가 | `priceInfo.split(\"|\"[1]` | 숫자 | 최종 가격 |
+| 할인율 | `priceInfo.split(\"|\"[4]` | 문자열 | "10%" 형식 |
+| 매진 여부 | `soldoutFlag` | "Y"/"N" | UI 배지 표시 |
+| 결제 가능 | `pmtSuptYn` | "Y"/"N" | 조건부 렌더링 |
+| 비디오 URL | `prdtMediaUrl` | URL | 선택사항 |
+| 비디오 자막 | `prdtMediaSubtitlUrl` | URL | 선택사항 |
+| QR 코드 | `qrcodeUrl` | 데이터 | 상품 공유용 |
+| 쇼 ID | `showId` | 문자열 | 테마 상품 고유 |
+| 카테고리 | `catCd` / `catNm` | 문자열 | 분류 정보 |
+
+---
+
+## 🎓 주요 특징
+
+1. **멀티 이미지 갤러리**: 비디오 + 이미지 혼합 지원
+2. **자동 비디오 재생**: 조건 만족 시 자동으로 재생 (launchedFromPlayer 제외)
+3. **스마트 인덱싱**: 비디오 유무에 따라 이미지 인덱스 자동 조정
+4. **조건부 구매 UI**: 결제 여부 + OS 버전에 따른 분기
+5. **상세 로깅**: 노출, 선택, 클릭 모두 추적 (매장 분석용)
+6. **SMS 공유**: 구매불가 상품도 정보 공유 가능
+7. **Spotlight 지원**: 전자제품 원격 제어 네비게이션 완벽 지원
diff --git a/com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md b/com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md
new file mode 100644
index 00000000..0c4d16d4
--- /dev/null
+++ b/com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md
@@ -0,0 +1,498 @@
+# DetailPanel - Theme Product vs Hotel Product 비교 분석
+
+## 📊 전체 개요
+
+| 구분 | Theme Product (쇼/공연) | Hotel Product (숙박) |
+|------|------------------------|----------------------|
+| **데이터 소스** | `themeCurationDetailInfoData[]` | `themeCurationHotelDetailData[]` |
+| **메인 컴포넌트** | ShowProduct | HotelProduct |
+| **패널 타입** | `panelInfo.type === "theme"` | `panelInfo.type === "hotel"` |
+| **서버 API** | getThemeCurationDetailInfo | getThemeHotelDetailInfo |
+| **Redux 액션** | GET_THEME_CURATION_DETAIL_INFO | GET_THEME_HOTEL_DETAIL_INFO |
+
+---
+
+## 🎯 핵심 차이점
+
+### 1. 데이터 구조
+
+#### Theme Product
+```javascript
+themeCurationDetailInfoData: [
+ {
+ prdtId, prdtNm, // 상품 ID, 이름
+ imgUrls600: [], // 이미지 배열
+ priceInfo: "299|199|Y|...", // 가격 정보 (파이프 구분)
+ prdtMediaUrl, // 비디오 URL
+ showId, showNm, // 쇼 정보
+ catCd, catNm, // 카테고리
+ soldoutFlag, // 매진 여부
+ pmtSuptYn, // 결제 지원
+ revwGrd, // 평점
+ }
+]
+```
+
+#### Hotel Product
+```javascript
+themeCurationHotelDetailData: [
+ {
+ hotelId, hotelNm, // 호텔 ID, 이름
+ hotelImgUrl, // 호텔 썸네일
+ imgUrls600: [], // 이미지 배열
+ qrcodeUrl, // QR 코드
+ hotelDetailInfo: {
+ price, // 가격 (단순 숫자)
+ currencySign, // 통화 기호
+ revwGrd, // 평점
+ hotelType, // 호텔 타입
+ hotelAddr, // 주소
+ nights, adultsCount, // 숙박일, 성인 수
+ roomType, // 방 타입
+ amenities: [], // 편의시설 ID 배열
+ imgUrls: [] // 이미지
+ }
+ }
+]
+
+hotelData: {
+ hotelInfo: { curationId, curationNm, ... },
+ amenities: [
+ { amntId, lgAmntNm, lgAmntImgUrl },
+ // ...
+ ]
+}
+```
+
+---
+
+### 2. UI 구조
+
+#### Theme Product
+```
+ShowProduct
+├─ ThemeIndicator
+│ ├─ [메인 이미지] 또는 [비디오]
+│ └─ [썸네일 스크롤]
+├─ IndicatorOptions
+│ ├─ [설명] [교환정책] [SMS]
+│ └─ QR 코드
+└─ ShowSingleOption / ShowUnableOption
+ └─ 구매 옵션 또는 구매불가 메시지
+```
+
+#### Hotel Product
+```
+HotelProduct
+├─ ThemeIndicator
+│ ├─ [메인 이미지]
+│ └─ [썸네일 스크롤]
+├─ IndicatorOptions
+│ └─ [주소 정보]
+└─ optionContainer
+ ├─ [로고 + 별점]
+ ├─ [호텔명 + 타입]
+ ├─ [편의시설 그리드] (최대 10개)
+ ├─ [예약정보 + 가격 + QR]
+ └─ [SEE MORE] 버튼
+```
+
+---
+
+### 3. 가격 처리
+
+#### Theme Product
+```javascript
+// priceInfo = "299|199|Y|discount10|10%"
+const befPrice = priceInfo.split("|")[0]; // 원가
+const lastPrice = priceInfo.split("|")[1]; // 할인가
+const rewdAplyFlag = priceInfo.split("|")[2]; // 보상 적용
+const discountRate = priceInfo.split("|")[4]; // 할인율
+
+// 로그 전송
+params.befPrice = befPrice;
+params.lastPrice = lastPrice;
+```
+
+#### Hotel Product
+```javascript
+// price = "299.99" (단순 숫자)
+// currencySign = "$"
+
+// UI 표시
+
+ {hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign}
+ {hotelInfos[selectedIndex]?.hotelDetailInfo.price}
+
+
+// 로그 전송
+params.price = selectedHotelInfo.hotelInfo?.hotelDetailInfo?.price;
+```
+
+---
+
+### 4. 특수 기능
+
+#### Theme Product
+✅ **비디오 자동 재생**
+- `prdtMediaUrl` 존재 시 첫 이미지 위치에 비디오
+- `launchedFromPlayer = false`일 때만 자동 재생
+- 비디오 자막 지원 (`prdtMediaSubtitlUrl`)
+
+✅ **상세 정보 팝업**
+- [DESCRIPTION] 버튼: 상품 설명
+- [RETURNS & EXCHANGES] 버튼: 반품/교환 정책
+- HTML 마크업 지원 (`dangerouslySetInnerHTML`)
+
+✅ **결제 옵션 선택**
+- 옵션 선택 (이용일자, 좌석 등)
+- 수량 선택
+- 옵션별 가격 계산
+
+❌ **편의시설 표시 없음**
+
+---
+
+#### Hotel Product
+✅ **편의시설 그리드 표시**
+- 최대 10개 편의시설 아이콘 + 텍스트
+- 같은 카테고리 중복 제거
+- 편의시설별 이미지 및 설명
+
+✅ **예약 정보 표시**
+- 숙박 기간 (nights)
+- 성인 수 (adultsCount)
+- 방 타입 (roomType)
+- 자동 포맷팅 (예: "2 Nights 2 Adults")
+
+✅ **별점 등급 시스템**
+- 평점 수치 → 문자 등급 변환
+- Fair (≤2.4) / Good (2.5~3.4) / Very Good (3.5~4.4) / Excellent (≥4.5)
+
+❌ **비디오 지원 없음**
+❌ **옵션 선택 없음** (숙박 정보는 미리 정해짐)
+
+---
+
+### 5. 결제 여부 판단
+
+#### 공통점
+```javascript
+// 결제 OS 버전 체크
+webOSVersion >= "6.0" // TRUE일 때만 결제 UI 표시
+```
+
+#### Theme Product
+```javascript
+const isBillingProductVisible = (
+ productInfo[selectedIndex]?.pmtSuptYn === "Y" &&
+ webOSVersion >= "6.0"
+);
+
+// YES → ShowSingleOption (구매 옵션)
+// NO → ShowUnableOption (구매불가)
+```
+
+#### Hotel Product
+```javascript
+// 호텔은 pmtSuptYn 체크 없음 (모두 구매불가)
+// SMS "SEE MORE" 버튼만 제공
+
+// 모든 호텔 상품이 UnableOption 구조
+```
+
+---
+
+### 6. SMS 타입 코드
+
+#### Theme Product
+```javascript
+smsTpCd = "APP00204" // 테마/쇼 상품
+```
+
+#### Hotel Product
+```javascript
+smsTpCd = "APP00205" // 호텔 상품
+```
+
+---
+
+### 7. 로그 필드
+
+#### 공통 필드
+```javascript
+{
+ curationId, curationNm, // 테마/큐레이션
+ patnrId, patncNm, // 파트너
+ prdtId, prdtNm, // 상품 ID, 이름
+ revwGrd, // 평점
+ expsOrd, // 상품 순번
+}
+```
+
+#### Theme Product 추가 필드
+```javascript
+{
+ showId, showNm, // 쇼 정보
+ catCd, catNm, // 카테고리
+ befPrice, lastPrice, // 원가, 할인가
+ rewdAplyFlag, // 보상 적용
+ tsvFlag, // 오늘의 특가
+ shopTpNm: "product", // 상품 타입
+}
+```
+
+#### Hotel Product 추가 필드
+```javascript
+{
+ hotelId, // 호텔 ID (prdtId 대체)
+ price, // 가격 (단순)
+ shopTpNm: "hotel", // 상품 타입
+}
+```
+
+---
+
+### 8. 선택 인덱스 처리
+
+#### Theme Product
+```javascript
+// URL 파라미터로 특정 상품 지정
+if (panelInfo?.themePrdtId) {
+ for (let i = 0; i < themeProductInfos.length; i++) {
+ if (themeProductInfos[i].prdtId === panelInfo?.themePrdtId) {
+ setSelectedIndex(i); // ← 해당 상품으로 이동
+ }
+ }
+}
+```
+
+#### Hotel Product
+```javascript
+// URL 파라미터로 특정 호텔 지정
+if (panelInfo?.themeHotelId) {
+ for (let i = 0; i < hotelInfos.length; i++) {
+ if (hotelInfos[i].hotelId === panelInfo?.themeHotelId) {
+ setSelectedIndex(i); // ← 해당 호텔로 이동
+ }
+ }
+}
+```
+
+---
+
+## 📐 레이아웃 비교
+
+### Theme Product
+| 요소 | 크기 |
+|------|------|
+| ThemeIndicator | 834×930px |
+| 메인 이미지 | 600×600px |
+| 썸네일 | 150×150px (반복) |
+| QR 코드 | 160×160px |
+
+### Hotel Product
+| 요소 | 크기 |
+|------|------|
+| ThemeIndicator | 774×930px |
+| 메인 이미지 | 600×600px |
+| optionContainer | 1026×990px |
+| 편의시설 박스 | 138×138px (반복, 최대 10개) |
+| QR 코드 | 160×160px |
+
+---
+
+## 🔄 컴포넌트 재사용
+
+### 공유 컴포넌트
+```
+ThemeIndicator
+├─ Theme Product에서 사용
+└─ Hotel Product에서도 사용
+ (비디오 재생은 X)
+
+IndicatorOptions
+├─ Theme Product에서 사용
+└─ Hotel Product에서도 사용
+ (주소 정보만 표시)
+
+StarRating
+├─ Theme Product에서 사용
+└─ Hotel Product에서도 사용
+```
+
+### 전용 컴포넌트
+```
+Theme Product:
+├─ ShowProduct.jsx
+├─ ShowSingleOption.jsx
+├─ ShowUnableOption.jsx
+└─ SingleOption / UnableOption (기존)
+
+Hotel Product:
+├─ HotelProduct.jsx
+└─ StarRating (호텔 등급용)
+```
+
+---
+
+## 📊 정리 테이블
+
+| 기능 | Theme | Hotel |
+|------|-------|-------|
+| **비디오 지원** | ✅ | ❌ |
+| **자동 재생** | ✅ | ❌ |
+| **편의시설 표시** | ❌ | ✅ (최대 10개) |
+| **예약 정보** | ❌ | ✅ (숙박일, 성인 수) |
+| **등급 변환** | ❌ | ✅ (Fair/Good/Very Good/Excellent) |
+| **옵션 선택** | ✅ | ❌ |
+| **수량 선택** | ✅ | ❌ |
+| **결제 가능** | 조건부 (pmtSuptYn) | ❌ (항상 불가) |
+| **상세 팝업** | ✅ (설명/교환) | ❌ |
+| **가격 형식** | `"299\|199\|Y\|..."` | `"299.99"` |
+| **통화 기호** | 미사용 | ✅ (`$`, `€`, `¥` 등) |
+| **SMS 타입** | APP00204 | APP00205 |
+| **QR 코드** | ✅ | ✅ |
+| **로그 추적** | 상세 (가격, 할인율) | 기본 (가격만) |
+
+---
+
+## 🎯 사용 시나리오
+
+### Theme Product 사용 예
+```
+사용자가 "오페라 공연" 클릭
+ ↓
+DetailPanel 로드 (type: "theme")
+ ↓
+ShowProduct 렌더링
+ ↓
+공연 비디오 자동 재생
+ ↓
+사용자가 설명 버튼 클릭
+ ↓
+상품 설명 팝업 표시
+ ↓
+사용자가 SMS 버튼 클릭
+ ↓
+친구에게 공연 정보 전송
+```
+
+### Hotel Product 사용 예
+```
+사용자가 "두바이 호텔" 클릭
+ ↓
+DetailPanel 로드 (type: "hotel")
+ ↓
+HotelProduct 렌더링
+ ↓
+호텔 사진, 편의시설, 가격 표시
+ ↓
+사용자가 화살표로 다른 호텔 선택
+ ↓
+별점/편의시설/가격 업데이트
+ ↓
+사용자가 SEE MORE 버튼 클릭
+ ↓
+호텔 상세 정보 SMS 전송
+```
+
+---
+
+## 💡 개발 가이드
+
+### 새로운 상품 타입 추가 시
+1. **DetailPanel에 분기 추가**
+ ```javascript
+ if (panelInfo?.type === "newtype") {
+ dispatch(getNewTypeDetailInfo(...));
+ }
+ ```
+
+2. **Redux Action/Reducer 생성**
+ ```javascript
+ GET_NEWTYPE_DETAIL_INFO action 추가
+ state.home.newTypeData 상태 추가
+ ```
+
+3. **메인 컴포넌트 생성** (ShowProduct/HotelProduct 참고)
+ - ThemeIndicator 재사용 (또는 커스터마이징)
+ - IndicatorOptions 재사용 (또는 커스터마이징)
+ - 세부 UI 컴포넌트 작성
+
+4. **ThemeProduct에 라우팅 추가**
+ ```javascript
+ {themeType === "newtype" && (
+
+ )}
+ ```
+
+---
+
+## 🔗 관련 파일 구조
+
+```
+src/views/DetailPanel/
+├─ DetailPanel.jsx / DetailPanel.backup.jsx
+├─ ThemeProduct/
+│ ├─ ThemeProduct.jsx (라우팅)
+│ ├─ ShowProduct.jsx (테마 상품)
+│ ├─ HotelProduct.jsx (호텔 상품)
+│ ├─ ShowOptions/
+│ │ ├─ ShowSingleOption.jsx
+│ │ └─ ShowUnableOption.jsx
+│ └─ *.module.less
+├─ components/
+│ ├─ ProductOption.jsx (래퍼)
+│ ├─ indicator/
+│ │ ├─ ThemeIndicator.jsx (공유)
+│ │ ├─ IndicatorOptions.jsx (공유)
+│ │ └─ *.module.less
+│ ├─ StarRating.jsx (공유)
+│ └─ ...
+├─ SingleProduct/
+│ └─ SingleOption.jsx (테마용)
+├─ UnableProduct/
+│ └─ UnableOption.jsx (테마용)
+└─ ...
+
+src/actions/
+├─ homeActions.js
+│ ├─ getThemeCurationDetailInfo()
+│ └─ getThemeHotelDetailInfo()
+└─ ...
+
+src/reducers/
+└─ homeReducer.js
+ ├─ GET_THEME_CURATION_DETAIL_INFO case
+ └─ GET_THEME_HOTEL_DETAIL_INFO case
+```
+
+---
+
+## ✅ 최종 체크리스트
+
+### Theme Product
+- ✅ 비디오 자동 재생 (조건부)
+- ✅ 상세 정보 팝업 (설명/교환)
+- ✅ 옵션 선택 (날짜/좌석)
+- ✅ 수량 선택
+- ✅ 가격 할인율 표시
+- ✅ 결제 가능 여부 판단
+- ✅ SMS 공유 (모든 상품)
+- ✅ 상세 로깅
+
+### Hotel Product
+- ✅ 호텔 이미지 갤러리
+- ✅ 편의시설 그리드 (최대 10개)
+- ✅ 별점 등급 변환
+- ✅ 예약 정보 표시 (숙박일, 성인 수)
+- ✅ 가격 + 통화 기호
+- ✅ 주소 정보
+- ✅ QR 코드
+- ✅ SMS 공유 (SEE MORE)
+- ✅ 로깅
+
+---
+
+이 문서를 통해 Theme Product와 Hotel Product의 차이점을 명확히 이해할 수 있으며, 향후 유사한 상품 타입 추가 시 참고할 수 있습니다.
diff --git a/com.twin.app.shoptime/docs/todo/251122-detailpanel-diff.md b/com.twin.app.shoptime/docs/todo/251122-detailpanel-diff.md
new file mode 100644
index 00000000..905fa1d7
--- /dev/null
+++ b/com.twin.app.shoptime/docs/todo/251122-detailpanel-diff.md
@@ -0,0 +1,13 @@
+# 251122 DetailPanel 기능 이관 점검 (backup 대비 누락 가능성)
+
+백업본(`DetailPanel.backup.jsx`)에는 있었지만 현재 `DetailPanel.jsx + ProductAllSection.jsx`로 리팩토링하면서 빠졌을 수 있는 항목들. 유지해야 하는 기능이면 재이관 필요.
+
+## 백업에만 있고 현행에는 없는 것
+- 호텔/여행형 상품 처리: `hotelData`/`hotelInfos` 기반 가격 표시(Price), 테마/호텔 정보 렌더링, SMS 팝업용 필드 등. 현행 DetailPanel에는 호텔 관련 로직이 모두 없음.
+- 최근 본 상품 저장: `saveToLocalSettings`로 `changeLocalSettings` dispatch. 현행에는 “필요하면 구현” 주석만 존재.
+- 이미지 길이 설정: 테마/호텔 이미지 개수를 `getProductImageLength`로 Redux 반영. 현행에는 없음.
+- 언마운트 정리 범위 축소: 백업은 `clearProductDetail`, `clearThemeDetail`, `clearCouponInfo`, `setContainerLastFocusedElement(null, ['indicator-GridListContainer'])` 모두 호출. 현행은 `clearProductDetail`과 `setContainerLastFocusedElement`만.
+
+## 참고
+- MobileSend 팝업, YouMayLike 요청, OptionId 초기화 등은 다른 컴포넌트(ProductAllSection/DetailMobileSendPopUp 등)로 분리되어 있음.
+- 위 네 가지가 실제로 필요하면 ProductAllSection/DetailPanel 측에 재연결이 필요.
diff --git a/com.twin.app.shoptime/src/api/TAxios.js b/com.twin.app.shoptime/src/api/TAxios.js
index a16fa9e7..9cda4614 100644
--- a/com.twin.app.shoptime/src/api/TAxios.js
+++ b/com.twin.app.shoptime/src/api/TAxios.js
@@ -25,7 +25,7 @@ let tokenRefreshing = false;
const axiosQueue = [];
export const setTokenRefreshing = (value) => {
- console.log('TAxios setTokenRefreshing ', value);
+ // console.log('TAxios setTokenRefreshing ', value);
tokenRefreshing = value;
};
export const runDelayedAction = (dispatch, getState) => {
diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
index a1eb17b7..5b05e028 100644
--- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
+++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
@@ -80,6 +80,29 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
() => fp.pipe(() => panelInfo, fp.get('bgVideoInfo'), fp.defaultTo(null))(),
[panelInfo]
);
+ const hasThemeContents = useMemo(
+ () =>
+ fp.pipe(
+ () => ({ panelType, themeData }),
+ ({ panelType, themeData }) =>
+ panelType === 'theme' &&
+ fp.pipe(
+ () => themeData,
+ fp.get('productInfos'),
+ (list) => Array.isArray(list) && list.length > 0
+ )()
+ )(),
+ [panelType, themeData]
+ );
+ const themeProducts = useMemo(
+ () =>
+ fp.pipe(
+ () => themeData,
+ fp.get('productInfos'),
+ (list) => (Array.isArray(list) ? list : [])
+ )(),
+ [themeData]
+ );
const panelShouldReload = useMemo(
() => fp.pipe(() => panelInfo, fp.get('shouldReload'), fp.defaultTo(false))(),
[panelInfo]
@@ -775,6 +798,21 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
}
}, [getProductType, productData, themeData, panelType]);
+ // Theme 콘텐츠 진입 시 전달된 themePrdtId를 초기 선택으로 반영
+ useEffect(() => {
+ if (panelType !== 'theme') return;
+
+ if (Array.isArray(themeProducts) && panelInfo?.themePrdtId) {
+ const matchedIndex = themeProducts.findIndex(
+ (item) => item?.prdtId === panelInfo.themePrdtId
+ );
+
+ if (matchedIndex >= 0) {
+ setSelectedIndex(matchedIndex);
+ }
+ }
+ }, [panelType, themeProducts, panelInfo?.themePrdtId, setSelectedIndex]);
+
// themeProductInfo 업데이트 - selectedIndex 변경 시마다 실행
useEffect(() => {
if (themeData?.productInfos && selectedIndex !== undefined) {
@@ -965,6 +1003,8 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
productType={productType}
productInfo={productDataSource}
panelInfo={panelInfo}
+ hasThemeContents={hasThemeContents}
+ themeProducts={themeProducts}
selectedIndex={selectedIndex}
selectedPatnrId={panelPatnrId}
selectedPrdtId={panelPrdtId}
diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
index aa5146b2..7200a019 100644
--- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
+++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
@@ -116,7 +116,7 @@ const HorizontalContainer = SpotlightContainerDecorator(
const ShopByMobileContainer = SpotlightContainerDecorator(
{
spotlightDirection: 'horizontal',
- enterTo: 'default-element',
+ enterTo: 'last-focused',
restrict: 'self-only',
defaultElement: SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
},
@@ -126,7 +126,7 @@ const ShopByMobileContainer = SpotlightContainerDecorator(
const BuyNowContainer = SpotlightContainerDecorator(
{
spotlightDirection: 'horizontal',
- enterTo: 'default-element',
+ enterTo: 'last-focused',
restrict: 'self-only',
defaultElement: 'detail-buy-now-button',
},
@@ -154,15 +154,24 @@ const ButtonStackContainer = SpotlightContainerDecorator(
const SpottableComponent = Spottable('div');
-const getProductData = curry((productType, themeProductInfo, productInfo) =>
- pipe(
- when(
- () => isVal(productType) && productType === 'theme' && isVal(themeProductInfo),
- () => themeProductInfo
- ),
- defaultTo(productInfo),
- defaultTo({}) // 빈 객체라도 반환하여 컴포넌트가 렌더링되도록 함
- )(productInfo)
+const getProductData = curry(
+ (productType, themeProductInfo, themeProducts, selectedIndex, productInfo) =>
+ pipe(
+ when(
+ () => isVal(productType) && productType === 'theme',
+ () => {
+ if (isVal(themeProductInfo)) {
+ return themeProductInfo;
+ }
+ if (Array.isArray(themeProducts) && isVal(selectedIndex)) {
+ return themeProducts[selectedIndex];
+ }
+ return null;
+ }
+ ),
+ defaultTo(productInfo),
+ defaultTo({}) // 빈 객체라도 반환하여 컴포넌트가 렌더링되도록 함
+ )(productInfo)
);
const deriveFavoriteFlag = curry((favoriteOverride, productData) => {
@@ -183,6 +192,8 @@ export default function ProductAllSection({
productType,
productInfo,
panelInfo,
+ hasThemeContents,
+ themeProducts,
launchedFromPlayer,
selectedIndex,
selectedPatnrId,
@@ -461,8 +472,8 @@ export default function ProductAllSection({
const [activeButton, setActiveButton] = useState(null);
const productData = useMemo(
- () => getProductData(productType, themeProductInfo, productInfo),
- [productType, themeProductInfo, productInfo]
+ () => getProductData(productType, themeProductInfo, themeProducts, selectedIndex, productInfo),
+ [productType, themeProductInfo, themeProducts, selectedIndex, productInfo]
);
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
@@ -1116,14 +1127,17 @@ export default function ProductAllSection({
// 초기 로딩 후 Skeleton 숨기기
useEffect(() => {
- if (productType && productData && isInitialLoading) {
+ const hasDataReady =
+ productType === 'theme' ? hasThemeContents && !!productData : !!productData;
+
+ if (productType && hasDataReady && isInitialLoading) {
const timer = setTimeout(() => {
setIsInitialLoading(false);
}, 150); // 150ms 후에 Skeleton 숨김
return () => clearTimeout(timer);
}
- }, [productType, productData, isInitialLoading]);
+ }, [productType, productData, hasThemeContents, isInitialLoading]);
// 컴포넌트 unmount 시 초기 상태로 복원
useEffect(() => {
@@ -1644,4 +1658,6 @@ export default function ProductAllSection({
ProductAllSection.propTypes = {
productType: PropTypes.oneOf(['buyNow', 'shopByMobile', 'theme']).isRequired,
+ hasThemeContents: PropTypes.bool,
+ themeProducts: PropTypes.array,
};
diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less
index 110debd2..568cc2c9 100644
--- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less
+++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less
@@ -30,12 +30,13 @@
// 2. Info Section - 645px
.infoSection {
width: 650px;
- height: 100%;
+ height: 884px; // 100% → 884px로 변경
padding: 0;
margin: 0;
display: flex;
- justify-content: flex-start;
- align-items: flex-start;
+ justify-content: flex-start; // 그대로 유지
+ align-items: flex-start; // 그대로 유지
+ overflow: hidden; // 내용이 넘칠 경우 방지
> div {
height: auto; /* 고정 높이 339px 제거 - 콘텐츠에 맞게 자동 조정 (Chromium 68 호환) */
min-height: 339px; /* 최소 높이만 유지 */