[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 서비스 레이어 개선 • 개발 문서 및 가이드 개선
This commit is contained in:
692
com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md
Normal file
692
com.twin.app.shoptime/THEME_PRODUCT_UI_ANALYSIS.md
Normal file
@@ -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
|
||||
<TVirtualGridList
|
||||
// 썸네일 목록 표시
|
||||
// 상하 스크롤로 네비게이션
|
||||
itemSize={IMAGE_HEIGHT} // 152px (scaleH 적용)
|
||||
items={/* 이미지 배열 */}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 IndicatorOptions 컴포넌트
|
||||
|
||||
### 구성 요소
|
||||
|
||||
#### 1. 상단 정보 영역
|
||||
```jsx
|
||||
<div className={css.topLayer}>
|
||||
<CustomImage
|
||||
src={productInfo?.patncLogoPath}
|
||||
fallbackSrc={defaultLogoImg}
|
||||
/>
|
||||
{productInfo?.expsPrdtNo && (
|
||||
<div>ID: {productInfo?.expsPrdtNo}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.title}>
|
||||
{productInfo?.prdtNm}
|
||||
</div>
|
||||
|
||||
<div className={css.bottomLayer}>
|
||||
<StarRating rating={productInfo?.revwGrd} />
|
||||
<ProductTag productInfo={productInfo} />
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 2. 버튼 영역
|
||||
```jsx
|
||||
{isBillingProductVisible && (
|
||||
<TButtonScroller>
|
||||
<TButtonTab
|
||||
onClick={() => descriptionClick("DESCRIPTION", description)}
|
||||
spotlightId="description_Btn"
|
||||
>
|
||||
{$L("DESCRIPTION")}
|
||||
</TButtonTab>
|
||||
<TButtonTab
|
||||
onClick={() => descriptionClick("RETURNS & EXCHANGES", exchangeInfo)}
|
||||
spotlightId="return_Exchanges_Btn"
|
||||
>
|
||||
{$L("RETURNS & EXCHANGES")}
|
||||
</TButtonTab>
|
||||
</TButtonScroller>
|
||||
)}
|
||||
|
||||
<TButtonTab
|
||||
onClick={handleSMSClick}
|
||||
spotlightId="shopbymobile_Btn"
|
||||
>
|
||||
{$L("SHOP BY MOBILE")}
|
||||
</TButtonTab>
|
||||
```
|
||||
|
||||
#### 3. QR 코드
|
||||
```jsx
|
||||
<div className={css.qrcodeContainer}>
|
||||
<TQRCode
|
||||
text={qrCodeUrl} // 결제가능 상품은 detailUrl 사용
|
||||
width="140"
|
||||
height="140"
|
||||
/>
|
||||
<div className={css.tooltip}>
|
||||
<div className={css.tooltipBody}>
|
||||
Please check for more detailed information
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 4. 설명 팝업
|
||||
```jsx
|
||||
const renderPopUp = () => {
|
||||
return (
|
||||
<TPopUp
|
||||
kind="descriptionPopup"
|
||||
open={popupVisible}
|
||||
onClose={handleSMSonClose}
|
||||
>
|
||||
<div className={css.popUpHeader}>
|
||||
<img src={thumbnailUrl} alt="" />
|
||||
<img src={productInfo?.patncLogoPath} alt="" />
|
||||
<h3>{productInfo?.prdtNm}</h3>
|
||||
<StarRating rating={productInfo?.revwGrd} />
|
||||
</div>
|
||||
<div
|
||||
className={css.popUpBody}
|
||||
dangerouslySetInnerHTML={productDescription()}
|
||||
/>
|
||||
</TPopUp>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💳 결제 여부에 따른 UI 분기
|
||||
|
||||
### ShowSingleOption (pmtSuptYn === "Y" && webOSVersion >= "6.0")
|
||||
|
||||
```jsx
|
||||
<ShowSingleOption
|
||||
productData={productData}
|
||||
productInfo={productInfo}
|
||||
selectedIndex={selectedIndex}
|
||||
soldoutFlag={isSoldout}
|
||||
logMenu={logMenu}
|
||||
/>
|
||||
```
|
||||
|
||||
**구성:**
|
||||
```jsx
|
||||
// ShowOptions/ShowSingleOption.jsx
|
||||
<ProductOption productInfo={productInfo[selectedIndex]}>
|
||||
<SingleOption
|
||||
type="theme"
|
||||
selectedPatnrId={productData?.themeInfo[0]?.patnrId}
|
||||
selectedPrdtId={productInfo[selectedIndex]?.prdtId}
|
||||
productInfo={productInfo[selectedIndex]}
|
||||
patncNm={productData?.themeInfo[0]?.patncNm}
|
||||
soldoutFlag={soldoutFlag}
|
||||
// ...
|
||||
/>
|
||||
</ProductOption>
|
||||
```
|
||||
|
||||
**SingleOption 렌더링:**
|
||||
- 상품 옵션 선택 (이용일자, 좌석 등)
|
||||
- 수량 선택
|
||||
- 가격 표시
|
||||
- "구매" 버튼
|
||||
|
||||
---
|
||||
|
||||
### ShowUnableOption (결제 불가능)
|
||||
|
||||
**조건:**
|
||||
- `pmtSuptYn === "N"` OR
|
||||
- `webOSVersion < "6.0"`
|
||||
|
||||
```jsx
|
||||
<ShowUnableOption
|
||||
productInfo={showProductInfo}
|
||||
productData={productData}
|
||||
soldoutFlag={isSoldout}
|
||||
selectedCurationId={selectedCurationId}
|
||||
selectedCurationNm={selectedCurationNm}
|
||||
handleMobileSendPopupOpen={handleMobileSendPopupOpen}
|
||||
logMenu={logMenu}
|
||||
/>
|
||||
```
|
||||
|
||||
**구성:**
|
||||
```jsx
|
||||
// ShowOptions/ShowUnableOption.jsx
|
||||
<ProductOption productInfo={productInfo}>
|
||||
<UnableOption
|
||||
selectedPatnrId={productData?.themeInfo[0]?.patnrId}
|
||||
selectedPrdtId={productInfo?.prdtId}
|
||||
productInfo={productInfo}
|
||||
soldoutFlag={soldoutFlag}
|
||||
smsTpCd="APP00204"
|
||||
handleMobileSendPopupOpen={handleMobileSendPopupOpen}
|
||||
// ...
|
||||
/>
|
||||
</ProductOption>
|
||||
```
|
||||
|
||||
**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
|
||||
<ThemeIndicator
|
||||
themeProductInfos={productInfo}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex} // ← 화살표로 변경
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 데이터 매핑 상세
|
||||
|
||||
### 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에서 사용
|
||||
<ThemeIndicator
|
||||
soldoutFlag={isSoldout}
|
||||
// 매진 상태면 이미지에 매진 배지 표시
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 ProductOption 래퍼
|
||||
|
||||
모든 옵션(SingleOption, UnableOption)은 ProductOption으로 감싸짐:
|
||||
|
||||
```jsx
|
||||
<ProductOption productInfo={productInfo[selectedIndex]}>
|
||||
<SingleOption {...props} />
|
||||
</ProductOption>
|
||||
```
|
||||
|
||||
**ProductOption 역할:**
|
||||
```jsx
|
||||
export default function ProductOption({ children, productInfo }) {
|
||||
return (
|
||||
<Container className={css.optionContainer}>
|
||||
{productInfo && (
|
||||
<div className={css.contentHeader}>
|
||||
{/* 로고 */}
|
||||
<CustomImage src={productInfo?.patncLogoPath} />
|
||||
|
||||
{/* 상품 ID */}
|
||||
{productInfo?.expsPrdtNo && (
|
||||
<div>ID: {productInfo?.expsPrdtNo}</div>
|
||||
)}
|
||||
|
||||
{/* 상품명 */}
|
||||
<div className={css.title}>
|
||||
{productInfo?.prdtNm}
|
||||
</div>
|
||||
|
||||
{/* 평점 + 태그 */}
|
||||
<StarRating rating={productInfo?.revwGrd} />
|
||||
<ProductTag productInfo={productInfo} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ DetailPanel과의 통합
|
||||
|
||||
```javascript
|
||||
// DetailPanel.backup.jsx에서 Theme Product 처리
|
||||
{isTravelProductVisible && (
|
||||
<ThemeProduct
|
||||
themeType="theme" // ← 테마 타입 지정
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
panelInfo={panelInfo}
|
||||
selectedCurationId={panelInfo?.curationId}
|
||||
selectedCurationNm={panelInfo?.curationNm}
|
||||
selectedPatnrId={panelInfo?.patnrId}
|
||||
shopByMobileLogRef={shopByMobileLogRef}
|
||||
isYouMayLikeOpened={isYouMayLikeOpened}
|
||||
/>
|
||||
)}
|
||||
|
||||
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"`)
|
||||
Reference in New Issue
Block a user