From ec5829eebe0c9e117f26281ae605e9cdc776d168 Mon Sep 17 00:00:00 2001 From: optrader Date: Sat, 22 Nov 2025 21:21:32 +0900 Subject: [PATCH] [251122] fix: DetailPanel Theme,Hotels - 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕐 컀밋 시간: 2025. 11. 22. 21:21:31 📊 변겜 통계: • 쎝 파음: 10개 • 추가: +76쀄 • 삭제: -19쀄 📁 추가된 파음: + com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md + com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md + com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md + com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md + com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md + com.twin.app.shoptime/docs/todo/251122-detailpanel-diff.md 📝 수정된 파음: ~ com.twin.app.shoptime/src/api/TAxios.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less 🔧 핚수 변겜 낎용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): ✅ Added: extractProductMeta() 🔄 Modified: SpotlightContainerDecorator() ❌ Deleted: SpotlightContainerDecorator() 📄 com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md (md파음): ✅ Added: useEffect(), dispatch(), ThemeProduct(), setLabel(), Map(), forEach(), filter(), findIndex(), setAmenitiesInfos(), setSelectedIndex() 📄 com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md (md파음): ✅ Added: DetailPanel(), getThemeCurationDetailInfo(), getThemeHotelDetailInfo(), getMainCategoryDetail(), THeader(), TBody(), ThemeProduct(), Container(), rating(), TButton(), YouMayLike(), MobileSendPopUp(), selectedIndex(), setSelectedIndex(), ellipsis(), amenitiesBox(), c70850(), handleSMSClick(), dispatch(), clearThemeDetail() 📄 com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md (md파음): ✅ Added: getThemeCurationDetailInfo(), productData(), Container(), ShowSingleOption(), ShowUnableOption(), useState(), useEffect(), setSelectedImage(), useMemo(), dispatch(), startVideoPlayer(), productDescription(), setTimeout(), useCallback(), isProductSoldOut(), ProductOption(), setSelectedIndex() 📄 com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md (md파음): ✅ Added: ShowProduct(), Container(), optionContainer(), ShowSingleOption(), ProductOption(), ShowUnableOption(), UnableOption(), selectedIndex(), setSelectedIndex(), descriptionClick(), setTabLabel(), setDescription(), dispatch(), handleIndicatorOptions(), handleSMSClick(), handleMobileSendPopupOpen(), setImageSelectedIndex(), StarRating(), ProductTag(), SingleOption() 📄 com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md (md파음): ✅ Added: Product(), UnableOption(), StarRating(), dispatch(), getThemeCurationDetailInfo(), getThemeHotelDetailInfo() 🔧 죌요 변겜 낎용: • API 서비슀 레읎얎 개선 • 개발 묞서 및 가읎드 개선 --- .../HOTEL_UI_HANDLING_REPORT.md | 389 ++++++++++ .../HOTEL_UI_VISUAL_GUIDE.md | 409 +++++++++++ .../THEME_PRODUCT_UI_ANALYSIS.md | 692 ++++++++++++++++++ .../THEME_PRODUCT_VISUAL_GUIDE.md | 437 +++++++++++ .../THEME_VS_HOTEL_COMPARISON.md | 498 +++++++++++++ .../docs/todo/251122-detailpanel-diff.md | 13 + com.twin.app.shoptime/src/api/TAxios.js | 2 +- .../src/views/DetailPanel/DetailPanel.jsx | 40 + .../ProductAllSection/ProductAllSection.jsx | 46 +- .../ProductAllSection.module.less | 7 +- 10 files changed, 2514 insertions(+), 19 deletions(-) create mode 100644 com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md create mode 100644 com.twin.app.shoptime/HOTEL_UI_VISUAL_GUIDE.md create mode 100644 com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md create mode 100644 com.twin.app.shoptime/THEME_PRODUCT_VISUAL_GUIDE.md create mode 100644 com.twin.app.shoptime/THEME_VS_HOTEL_COMPARISON.md create mode 100644 com.twin.app.shoptime/docs/todo/251122-detailpanel-diff.md 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. 상닚 레읎얎: 로고 + 별점 + 등꞉ */} +
+ +
+ + +
{label}
+
+
+ + {/* 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; /* 최소 높읎만 유지 */