🕐 커밋 시간: 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 서비스 레이어 개선 • 개발 문서 및 가이드 개선
390 lines
10 KiB
Markdown
390 lines
10 KiB
Markdown
# 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}]
|
||
{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 연동으로 호텔 정보를 공유할 수 있습니다.
|