Files
shoptime/com.twin.app.shoptime/HOTEL_UI_HANDLING_REPORT.md
optrader ec5829eebe [251122] fix: DetailPanel Theme,Hotels - 1
🕐 커밋 시간: 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 서비스 레이어 개선
  • 개발 문서 및 가이드 개선
2025-11-22 21:21:32 +09:00

390 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (
<div className={css.container}>
{themeType === "theme" && (
<ShowProduct {...props} />
)}
{themeType === "hotel" && (
<HotelProduct {...props} />
)}
</div>
);
}
```
---
## 🏨 호텔 상품 UI 처리 (HotelProduct.jsx)
### 3. HotelProduct 컴포넌트 구조
#### A. 이미지 갤러리 영역
```jsx
<ThemeIndicator
themeProductInfos={hotelInfos}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
thumbnailUrls={hotelInfos[selectedIndex]?.hotelDetailInfo.imgUrls}
/>
```
- 호텔 이미지들을 인디케이터로 표시
- 선택된 인덱스에 따라 이미지 변경
#### B. 주소/위치 정보
```jsx
<IndicatorOptions
address={hotelInfos[selectedIndex]?.hotelDetailInfo.hotelAddr}
/>
```
#### C. 호텔 정보 카드 영역
```jsx
<div className={css.optionContainer}>
{/* 1. 상단 레이어: 로고 + 별점 + 등급 */}
<div className={css.topLayer}>
<img src={hotelData?.hotelInfo.patncLogoPath} alt="" />
<div className={css.rating}>
<StarRating
rating={hotelInfos[selectedIndex]?.hotelDetailInfo.revwGrd}
/>
<span className={css.line} />
<div>{label}</div>
</div>
</div>
{/* 2. 제목: [호텔타입] 호텔명 */}
<div className={css.title}>
[{hotelInfos[selectedIndex]?.hotelDetailInfo.hotelType}]&nbsp;
{hotelInfos[selectedIndex]?.hotelNm}
</div>
{/* 3. 편의시설 그리드 (최대 10개) */}
<div className={css.amenitiesCotainer}>
{amenitiesInfos && amenitiesInfos.map((item) => (
<div className={css.amenitiesBox} key={item.amntId}>
<img src={item.lgAmntImgUrl} alt="" />
<p>{item.lgAmntNm}</p>
</div>
))}
</div>
{/* 4. 하단 레이어: 예약 정보 + 가격 + QR코드 */}
<div className={css.bottomLayer}>
<div>
<div className={css.today}>
{nights}Nights {adultsCount}Adults
</div>
<div className={css.roomType}>
{hotelInfos[selectedIndex]?.hotelDetailInfo.roomType}
</div>
<div className={css.price}>
<div>From</div>
<p>
{hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign}
{hotelInfos[selectedIndex]?.hotelDetailInfo.price}
</p>
</div>
</div>
<div className={css.qrcodeContainer}>
<TQRCode
text={hotelInfos[selectedIndex]?.qrcodeUrl}
width="160"
height="160"
/>
</div>
</div>
{/* 5. CTA 버튼: "SEE MORE" */}
<TButton
className={css.tbutton}
size="extra"
onClick={handleSMSClick}
spotlightId="shopbymobile_Btn"
>
{$L("SEE MORE")}
</TButton>
</div>
```
---
## 🎯 데이터 매핑 상세 설명
### 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 연동으로 호텔 정보를 공유할 수 있습니다.