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

10 KiB
Raw Permalink Blame History

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)

// 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 구조:

// homeReducer.js
const initialState = {
  themeCurationDetailInfoData: [],  // 테마 상품 데이터
  themeCurationHotelDetailData: [], // 호텔 상세 데이터
  hotelData: {},                    // 호텔 통합 정보
  productData: {},                  // 테마 정보
};

🎨 UI 렌더링 로직

2. ThemeProduct 컴포넌트 (라우팅)

// 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. 이미지 갤러리 영역

<ThemeIndicator
  themeProductInfos={hotelInfos}
  selectedIndex={selectedIndex}
  setSelectedIndex={setSelectedIndex}
  thumbnailUrls={hotelInfos[selectedIndex]?.hotelDetailInfo.imgUrls}
/>
  • 호텔 이미지들을 인디케이터로 표시
  • 선택된 인덱스에 따라 이미지 변경

B. 주소/위치 정보

<IndicatorOptions
  address={hotelInfos[selectedIndex]?.hotelDetailInfo.hotelAddr}
/>

C. 호텔 정보 카드 영역

<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에서)

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"
    },
    // ... 편의시설 목록
  ]
}

별점 등급 매핑

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]);

🛏️ 편의시설 처리 로직

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코드 영역

🔄 선택 인덱스 관리

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]);

📱 이미지 길이 업데이트

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. 상세정보 조회 로그

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 보내기 버튼 로그

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 연동으로 호텔 정보를 공유할 수 있습니다.