79 Commits

Author SHA1 Message Date
2c681bab68 [251215] merge: Soft merge gitlab/develop_si
다음 변경사항들을 merge:
- [영상내 shopnow 수정]
- [라이브 채널 넥스트 버튼 수정]

충돌 없이 성공적으로 merge 완료
2025-12-15 13:16:08 +09:00
ab2dd7385b fix: DetailPanel above PlayerPanel , FeaturedBrandsPanel-2 2025-12-15 13:10:55 +09:00
ac5414a5fe fix: DetailPanel above PlayerPanel , FeaturedBrandsPanel 2025-12-15 12:38:20 +09:00
junghoon86.park
f46090863f [영상내 shopnow 수정]
- shopnowIcon이미지 변경. qvc로 고정되어있었던부분 영상에 맞게 노출되도록 수정.
 - qvc영상일때 이미지는 border-radius먹지않도록 변경.
2025-12-15 11:02:52 +09:00
junghoon86.park
1e9c9bee40 [라이브 채널 넥스트 버튼 수정]
- qvc가 짤려보이는문제 관련하여 수정.
2025-12-15 10:12:47 +09:00
junghoon86.park
f514e2468c [nbcu 상품에서의 모바일 샌드팝업 수정]
- 기존 처리 했던 useselector제거하고 patnrId 로 변경처리.
 - nbcu상품에서는 상품 id가 노출되지않고 patncNm가 노출되도록 수정.
2025-12-15 10:00:27 +09:00
junghoon86.park
1305158113 [영상노출 관련 스타일 수정]
- 영상재생시에 하단에 노출되는 livechannel부분에 qvc는 border-radius 0으로 작업.
2025-12-15 09:52:31 +09:00
junghoon86.park
e97172fad5 [모바일 샌드 팝업] qvc일때 원형으로 짤리는부분 수정
- qvc에서는 border-radius제거.
 - theme item일때 에러나는부분때문에 옵셔널체이닝 처리.
2025-12-15 09:43:22 +09:00
42f58bf10c [251215] fix: FeaturedBrandsPanel Log추가
🕐 커밋 시간: 2025. 12. 15. 09:38:59

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +30줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/api/logServerClient.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/api/TLogEvent.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx

🔧 주요 변경 내용:
  • API 서비스 레이어 개선
  • 소규모 기능 개선
2025-12-15 09:39:00 +09:00
e424ab761c [251212] fix: FeaturedBrandsPanel Debugging - 2
🕐 커밋 시간: 2025. 12. 12. 17:17:53

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +6줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
2025-12-12 17:17:54 +09:00
f62ccef420 [251212] fix: FeaturedBrandsPanel Debugging - 1
🕐 커밋 시간: 2025. 12. 12. 17:12:43

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +145줄
  • 삭제: -13줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/actions/playActions.js
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 중간 규모 기능 개선
2025-12-12 17:12:43 +09:00
1ee664e8c1 [251212] fix: FeaturedBrandsPanel관련 로그 정리
🕐 커밋 시간: 2025. 12. 12. 14:41:05

📊 변경 통계:
  • 총 파일: 9개
  • 추가: +118줄
  • 삭제: -87줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBestSeller/FeaturedBestSeller.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBestSeller/FeaturedBestSellerList/FeaturedBestSellerList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • 중간 규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선
2025-12-12 14:41:06 +09:00
junghoon86.park
16a09b2e2b [워닝제거] cartReducer 공백제거.
- 공백만 제거.
2025-12-12 13:15:52 +09:00
junghoon86.park
4fcd87da7d [워닝 제거] tqrcode
- 선언후 사용하지않는 useMemo, useState제거
 - tqrcode에서는 width, height 의존성배열 추가.
2025-12-12 13:12:33 +09:00
junghoon86.park
9c2ecbaa57 [워닝 제거]
- 공백 워닝, == 와 ===의 워닝처리건.
2025-12-12 13:08:09 +09:00
junghoon86.park
ad8fc598b4 [쿠폰 다운로드] 중복다운로드 가능하면 download complete 뜨지않도록
- 현재 쿠폰이 보이지않아 확인은 못함.(확인필요)
2025-12-12 13:05:37 +09:00
junghoon86.park
ccc91ec662 [워닝제거] 큰범위 내에서 사용하지않는것, 공백 부분만 처리
- optionalConfirm, optionalTermsConfirm, optionalTermsConfirmBottom 3파일 우선처리.
2025-12-12 11:01:33 +09:00
junghoon86.park
b3b1151a1d [저스트포유] 랜덤 배너에도 추가
- 현재 데이터가 롤링유닛에만 내려와서 확인이 불가능한 상황이라 우선 실제 노출소스는 주석처리.
 - 추후 확인후 주석해제하여 사용하겠습니다.
2025-12-12 10:39:29 +09:00
junghoon86.park
4a70f321ed [상품 상세] 금익부분이 너무 클경우 아래로 떨어지지않고 이상하게 노출되는부분 수정. 2025-12-11 18:02:48 +09:00
ddd5d5c7ba [251211] fix: PlayerPanel autoClose activityCheck
🕐 커밋 시간: 2025. 12. 11. 17:59:30

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +1줄
  • 삭제: -6줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-11 17:59:31 +09:00
9681eb42e1 [251211] fix: IntroPanel.new.jsx 포커스 조정-3
🕐 커밋 시간: 2025. 12. 11. 17:20:46

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +11줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx
2025-12-11 17:20:46 +09:00
a3fe60ca70 [251211] fix: IntroPanel.new.jsx 포커스 조정-2
🕐 커밋 시간: 2025. 12. 11. 17:08:47

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +8줄
  • 삭제: -13줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-11 17:08:47 +09:00
junghoon86.park
0593f54d6e [tabitemsub]
- 서브 카테고리 들어가면 icon이 이상하게 노출되는부분 관련하여 확인해보니 IconComponent가 돌아가고있음. 이부분 체크를 위해서 우선 주석처리.
2025-12-11 16:59:31 +09:00
d903610709 [251211] fix: IntroPanel.new.jsx 포커스 조정
🕐 커밋 시간: 2025. 12. 11. 16:47:19

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +3줄
  • 삭제: -3줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-11 16:47:19 +09:00
junghoon86.park
bc6119f902 [nbcu] 타이틀 배너 스타일 수정
- 타이틀 text-align:center제거
2025-12-11 15:37:12 +09:00
38fad5ffe2 [251211] fix: FeaturedBrandsPanel TopImageBanner Focus
🕐 커밋 시간: 2025. 12. 11. 15:21:58

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +55줄
  • 삭제: -17줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-11 15:21:59 +09:00
junghoon86.park
c16724f245 [nbcu] qr 스타일 수정
- nbcu전용 qr에 대해서 스타일 수정.
2025-12-11 15:12:25 +09:00
junghoon86.park
013055692f [nbcu 관련수정]
- youmayalsolike 파트너사 21일때는 노출하지않음.
 - see more product 쉘프에 스폰서 이미지 추가
 - see more product 쉘프에 쉘프명 수정.
2025-12-11 15:06:09 +09:00
98df524ecf [251211] fix: NBCU Detailpanel QRCodeNew->QRCode
🕐 커밋 시간: 2025. 12. 11. 14:54:31

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +15줄
  • 삭제: -3줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCodePatnr21.jsx

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx
2025-12-11 14:54:31 +09:00
3e300749a0 [251211] fix: ShopByShow PriceInfo add
🕐 커밋 시간: 2025. 12. 11. 14:36:53

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowProductList/ShopByShowProductList.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-11 14:36:53 +09:00
f5621b0c55 [251211] feat: FeaturedBrandsPanel , TopBannerImage Modal
🕐 커밋 시간: 2025. 12. 11. 14:04:14

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +63줄
  • 삭제: -25줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/GlobalPopup/GlobalPopup.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
2025-12-11 14:04:15 +09:00
junghoon86.park
7971bbc1db [nbcu] 상단 배너 포커스 관련 수정
- 스타일 수정
2025-12-11 14:01:56 +09:00
d640bb74ef [251211] feat: FeaturedBrandsPanel , TopBannerImage Focus
🕐 커밋 시간: 2025. 12. 11. 13:46:11

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +48줄
  • 삭제: -16줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.jsx
  + com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-11 13:46:12 +09:00
junghoon86.park
cf27ed3846 [상품 상세] 디테일 부분 포커스 및 스타일 수정
- nbcu seemoreproduct 버튼 포커스및 tiemcard내에 보더 제거
2025-12-11 12:53:05 +09:00
a85710421c [251211] feat: FeaturedBrandsPanel , See More Products - 3
🕐 커밋 시간: 2025. 12. 11. 12:29:51

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +25줄
  • 삭제: -5줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/SeeMoreProducts/SeeMoreProducts.jsx
  + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/SeeMoreProducts/SeeMoreProducts.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-11 12:29:51 +09:00
05f5bf4d33 [251211] feat: FeaturedBrandsPanel , See More Products - 2
🕐 커밋 시간: 2025. 12. 11. 09:07:19

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +93줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-11 09:07:19 +09:00
8057021d1c [251210] feat: FeaturedBrandsPanel , See More Products - 1
🕐 커밋 시간: 2025. 12. 10. 18:29:18

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +133줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/reducers/brandReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 중간 규모 기능 개선
2025-12-10 18:29:18 +09:00
cbdf1b89f8 [251210] fix: DetailPanel,ProductAllSection API최적화-2
🕐 커밋 시간: 2025. 12. 10. 17:30:18

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +85줄
  • 삭제: -52줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-10 17:30:18 +09:00
junghoon86.park
4fe3c94b1e [스폰서 이미지 위치 수정]
- 스폰서 이미지 위치 수정
2025-12-10 17:19:07 +09:00
junghoon86.park
07e5d5c6de [스폰서 이미지 추가]
- 상품 상세 첫번째 이미지에 스폰서 이미지 추가.
2025-12-10 17:16:39 +09:00
bc8317483f [251210] fix: DetailPanel,ProductAllSection API최적화-1
🕐 커밋 시간: 2025. 12. 10. 16:49:51

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +49줄
  • 삭제: -43줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-10 16:49:51 +09:00
a2b29d219a [251210] feat: FeaturedBrandsPanel-TopBannerImage Popup
🕐 커밋 시간: 2025. 12. 10. 16:33:37

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +69줄
  • 삭제: -5줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup.figma.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/GlobalPopup/GlobalPopup.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-10 16:33:38 +09:00
bf7af5aa2e [251210] feat: FeaturedBrandsPanel-TopBannerImage 추가
🕐 커밋 시간: 2025. 12. 10. 15:53:45

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +51줄
  • 삭제: -21줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.module.less
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-10 15:53:46 +09:00
bc7a999cf1 Merge resolve conflicts 2025-12-10 13:10:53 +09:00
d6656848a2 [251210] feat: featuredBrandTopBanner API추가 , nbcu bg추가
🕐 커밋 시간: 2025. 12. 10. 13:02:33

📊 변경 통계:
  • 총 파일: 10개
  • 추가: +100줄
  • 삭제: -4줄

📁 추가된 파일:
  + com.twin.app.shoptime/assets/images/bg/nbcu_new.png

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/api/apiConfig.js
  ~ com.twin.app.shoptime/src/reducers/brandReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.v2.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-10 13:02:34 +09:00
junghoon86.park
d545a4de0c [상품 상세]
- price 파트너사 붙는쪽에 nbcu일 경우 peacoke | ******* 로 들어오는 부분때문에 변경.
2025-12-10 12:53:48 +09:00
junghoon86.park
39d1b42ec4 [상세 헤더]
- 파트너 아이디가 21일경우 id 값 노출안되도록 변경(2025.12.10 오현주 팀장님 요청건)
2025-12-10 12:47:33 +09:00
junghoon86.park
53aa879ee5 [checkout panel]
- 배송정보, 주문정보, 카드정보 없을시 qr팝업 노출되도록 수정.
 - qr팝업 닫혔을때 상품 상세페이지로 이동하도록 처리.
2025-12-10 12:39:31 +09:00
db7bc4b2ed [251210] feat: brandBestSelleInfo 처리
🕐 커밋 시간: 2025. 12. 10. 12:28:02

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +17줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBestSeller/FeaturedBestSeller.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-10 12:28:03 +09:00
junghoon86.park
a6275a63e9 [justforyou] 이미지 겉 보더 색상 변경.
- 색상변경 및 추가.
2025-12-10 10:46:35 +09:00
a6eee92641 [251210] fix: 백화현상디버깅-App.js commonActions.js 원복
🕐 커밋 시간: 2025. 12. 10. 10:01:38

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +16줄
  • 삭제: -14줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/actions/commonActions.js

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/App/App.js (javascript):
     Added: AppBase(), keyDownEvent(), mouseMoveEvent()
    🔄 Modified: function()
  📄 com.twin.app.shoptime/src/actions/commonActions.js (javascript):
    🔄 Modified: alertToast(), setFocus(), cancelFocusElement(), addReservation(), Job()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-10 10:01:38 +09:00
844f374abb [251209] fix: 백화현상 디버깅-3 App.js initService dispatch의존성 추가
🕐 커밋 시간: 2025. 12. 09. 18:42:58

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +1줄
  • 삭제: -1줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 코드 정리 및 최적화
2025-12-09 18:42:58 +09:00
77987711d0 [251209] fix: 백화현상 디버깅-2 App.js GlobalPopup활성화
🕐 커밋 시간: 2025. 12. 09. 18:34:56

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -1줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-09 18:34:56 +09:00
18c3ac3ad5 [251209] fix: 백화현상 디버깅-1
🕐 커밋 시간: 2025. 12. 09. 18:18:51

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +28줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/homeActions.js
  ~ com.twin.app.shoptime/src/api/TAxios.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 소규모 기능 개선
2025-12-09 18:18:51 +09:00
aa1f9630e6 [251209] feat: ShopByShow-8
🕐 커밋 시간: 2025. 12. 09. 17:49:58

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +69줄
  • 삭제: -22줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowProductList/ShopByShowProductList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-09 17:49:58 +09:00
407b4c7751 [251209] feat: 약관미동의시 처리-1
🕐 커밋 시간: 2025. 12. 09. 17:17:23

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +16줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/homeActions.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-09 17:17:24 +09:00
f2ab9dbdd4 [251209] feat: NBCU-ShopByShow-7
🕐 커밋 시간: 2025. 12. 09. 17:04:16

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +16줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/reducers/brandReducer.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-09 17:04:16 +09:00
ce7916d7b0 [251209] feat: NBCU-ShopByShow-6
🕐 커밋 시간: 2025. 12. 09. 16:51:12

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +5줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowProductList/ShopByShowProductList.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-09 16:51:13 +09:00
0db5a72c63 [251209] feat: NBCU-ShopByShow-5
🕐 커밋 시간: 2025. 12. 09. 16:28:02

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +32줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-09 16:28:02 +09:00
a46d34b776 [251209] feat: NBCU-ShopByShow-4
🕐 커밋 시간: 2025. 12. 09. 16:19:54

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +62줄
  • 삭제: -139줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/hooks/useScrollTo.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.module.less

🗑️ 삭제된 파일:
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.jsx
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.module.less

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-09 16:19:55 +09:00
85c44cdd8b [251209] feat: NBCU-ShopByShow-3
🕐 커밋 시간: 2025. 12. 09. 16:05:46

📊 변경 통계:
  • 총 파일: 14개
  • 추가: +50줄
  • 삭제: -553줄

📁 추가된 파일:
  + com.twin.app.shoptime/shopByShow.response.json
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowImageCard/ShopByShowImageCard.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowImageCard/ShopByShowImageCard.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowProductList/ShopByShowProductList.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowProductList/ShopByShowProductList.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.module.less
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.jsx

🗑️ 삭제된 파일:
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowImageCard/ShopByShowImageCard.jsx
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowImageCard/ShopByShowImageCard.module.less
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowProductList/ShopByShowProductList.jsx
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowProductList/ShopByShowProductList.module.less
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowSection.jsx
  - com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowSection.module.less

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선

Performance: 코드 최적화로 성능 개선 기대
2025-12-09 16:05:48 +09:00
2a1cda560c [251209] feat: NBCU-ShopByShow-2
🕐 커밋 시간: 2025. 12. 09. 15:48:17

📊 변경 통계:
  • 총 파일: 11개
  • 추가: +25줄
  • 삭제: -114줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.figma.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowImageCard/ShopByShowImageCard.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowImageCard/ShopByShowImageCard.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowProductList/ShopByShowProductList.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowProductList/ShopByShowProductList.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowSection.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowSection/ShopByShowSection.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.module.less
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.module.less
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선

Performance: 코드 최적화로 성능 개선 기대
2025-12-09 15:48:19 +09:00
junghoon86.park
c7f6bf00b9 [상품 상세] shopbymobile 팝업 노출관련 수정
- 팝업이 정상적이지않아 의존성배열에 추가하여 수정.
2025-12-09 15:16:31 +09:00
255b3bb2b7 Merge remote-tracking branch 'gitlab/develop_si' into develop_si 2025-12-09 14:34:24 +09:00
4a6473e1e5 [251209] feat: NBCU-ShopByShow-1
🕐 커밋 시간: 2025. 12. 09. 14:33:20

📊 변경 통계:
  • 총 파일: 15개
  • 추가: +128줄
  • 삭제: -11줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/api/apiConfig.js
  ~ com.twin.app.shoptime/src/reducers/brandReducer.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBestSeller/FeaturedBestSellerList/FeaturedBestSellerList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenuItem/QuickMenuItem.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 중간 규모 기능 개선
  • 모듈 구조 개선
2025-12-09 14:33:21 +09:00
junghoon86.park
7f7b413aa5 [저스트포유 배너 변경]
- 기존 image banner로들어가는부분이 아닌 today deals와 같은 포맷으로 들어가도록 변경.
 - 이미지, 데이터 불러오는부분 추가
2025-12-09 13:54:06 +09:00
439e5f46e3 [251209] feat: log_keys.txt
🕐 커밋 시간: 2025. 12. 09. 13:00:08

📊 변경 통계:
  • 총 파일: 1개

📁 추가된 파일:
  + com.twin.app.shoptime/log_keys.txt

🔧 주요 변경 내용:
  • 로깅 시스템 개선
2025-12-09 13:00:08 +09:00
junghoon86.park
92ee225dd1 [리뷰 패널 필터 스타일 수정]
- 리뷰 패널 필터 스타일 수정(작동되고있는 필터 구분을 위해서 처리)
2025-12-09 09:45:32 +09:00
junghoon86.park
c0223176f2 [리뷰 팝업 노출 관련 수정]
- 리뷰 팝업 노출시 인디케이터로 이동후 포커스 벗어나면 초기화되는부분이 있어 이부분에 대한 수정.
2025-12-09 09:34:07 +09:00
junghoon86.park
80c593e6f0 [상품 상세]
- 상풉 옵션별 ID노출 처리.
2025-12-08 20:10:02 +09:00
junghoon86.park
b2807c5a39 [리뷰 팝업 포커스 사라짐 수정]
- 모드 변경시에 포커스 박스 안에서만 유지되도록 변경.
2025-12-08 19:38:04 +09:00
junghoon86.park
e00763f0da [리뷰 별점관련 스타일 수정]
- 티비 화면에서 확인필요
2025-12-08 19:25:42 +09:00
junghoon86.park
b040dd8c1c [buyoption] 로그인 관련 수정
- 뜰때 하이드 팝업 되는 문제로 인해서 멤버십이 안뜨는부분때문에 sethidepopup제거.
2025-12-08 18:44:57 +09:00
junghoon86.park
7507f81c34 [상품상세 포커스 수정]
- 영상 존재시 영상으로 포커스 , 이외에는 상품 이미지 첫번째로 포커스가도록 수정.
2025-12-08 17:05:50 +09:00
junghoon86.park
b6bcc7dadc [상품명 부분 노출수정]
- 기존 스타일 수정한것으로 확인하니 상품명이 너무 길경우 padding 으로 인해서 글자가 아래쪽에 보이는문제 발견하여 수정
2025-12-08 16:24:05 +09:00
junghoon86.park
2627a7ac68 [상품 상세]
- 글자 폰트 변경 및 노출관련하여 margin을 Padding으로 변경.
2025-12-08 15:10:29 +09:00
junghoon86.park
d164630200 [homepanel]
- 선택약관 동의, 비동의시 팝업 닫힌 뒤 포커스 이동 처리.
 - settimeout100으로 home_tbody로 포커스 줌.
2025-12-08 14:01:21 +09:00
junghoon86.park
6c00f6bd7d [detail]
- 하단 부분 scrolldown부분 노출 관련하여 isBottom이 들어가있을시 정상작동하지않아 제거.
2025-12-08 13:37:54 +09:00
f51e8bbfc5 [251208] fix: GNB-Featured Brands mock menu remove
🕐 커밋 시간: 2025. 12. 08. 12:25:30

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +3줄
  • 삭제: -19줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-08 12:25:30 +09:00
96 changed files with 4487 additions and 665 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -0,0 +1,126 @@
# 로그 시스템에서 사용되는 모든 JSON 키 목록
# 추출일: 2025-01-09
## 공통 키 (Common Keys)
- entryMenu - 진입 메뉴
- nowMenu - 현재 메뉴
- logTpNo - 로그 타입 번호
- inDt - 진입 시간
- outDt - 진출 시간
## 사용자/파트너 정보 (User/Partner Info)
- patncNm - 파트너 이름
- patnrId - 파트너 아이디
- usrNo - 사용자 번호
- lginTpNm - 로그인 타입 네임
- mbrNo - 멤버 번호
## 상품 정보 (Product Info)
- prdtId - 상품 ID
- prdtNm - 상품 이름
- befPrice - 이전 가격
- lastPrice - 최종 가격
- linkTpCd - 링크 타입 코드
- tsvFlag - TSV 여부
- cartTpSno - 카트 타입 시퀀스 번호
- qty - 수량
- prodId - 상품 ID (다른 표기)
- prodNm - 상품 이름 (다른 표기)
## 방송/콘텐츠 정보 (Show/Content Info)
- showId - 방송 ID
- showNm - 방송 이름
- vdoTpNm - 비디오 타입 네임
- cnttTpNm - 콘텐츠 타입 네임
- contId - 콘텐츠 ID
- contNm - 콘텐츠 이름
- banrNo - 배너 번호
- tmplCd - 템플릿 코드
- keywordList - 키워드 리스트
## 시청 정보 (Watch Info)
- watchStrtDt - 시청 시작 시간
- watchEndDt - 시청 종료 시간
## 카테고리 정보 (Category Info)
- lgCatCd - 대 카테고리 코드
- lgCatNm - 대 카테고리 이름
- catCd - 카테고리 코드
- catNm - 카테고리 이름
- catCdLv1 - 1단계 카테고리 코드
- catCdLv2 - 2단계 카테고리 코드
## 큐레이션/테마 정보 (Curation/Theme Info)
- curationId - 큐레이션 ID
- curationNm - 큐레이션 이름
- shelfId - 셸프 ID
- shelfNm - 셸프 이름
- expsOrd - 노출 순서
- sortTpNm - 정렬 타입 네임
## 브랜드/시리즈 정보 (Brand/Series Info)
- crtrId - 크리에이터 ID
- crtrNm - 크리에이터 이름
- srsId - 시리즈 ID
- srsNm - 시리즈 이름
## 검색 정보 (Search Info)
- keyword - 키워드
- inputFlag - 입력 플래그
- itemCnt - 상품 개수
- showCnt - 방송 개수
- themeCnt - 테마 개수
## 알림 정보 (Alarm Info)
- alarmDt - 알람 날짜
- alarmType - 알람 타입
- alertFlag - 알림 플래그
- clickFlag - 클릭 플래그
- cnt - 개수
- items - 아이템들
## 쿠폰 정보 (Coupon Info)
- cpnSno - 쿠폰 시퀀스 번호
- cpnTtl - 쿠폰 제목
## 결제 정보 (Payment Info)
- dcAftrPrc - 할인 후 가격
- dcBefPrc - 할인 전 가격
## 주문 정보 (Order Info)
- reqRsn - 요청 사유
- reqTpNm - 요청 타입 네임
## 마이페이지 정보 (MyPage Info)
- itemId - 아이템 ID
- title - 제목
- btnNm - 버튼 이름
## 모바일 쇼핑 정보 (Mobile Shopping Info)
- shopByMobileFlag - 모바일 쇼핑 플래그
- mbphNoFlag - 휴대폰 번호 플래그
- shopTpNm - 쇼핑 타입 네임
- trmsAgrFlag - 약관 동의 플래그
## DeepLink 정보
- deeplinkId - 딥링크 ID
- flag - 플래그
## 네트워크/시스템 정보 (Network/System Info)
- clientIP - 클라이언트 IP
- localMacAddress - 로컬 MAC 주소
- macAddress - MAC 주소
- macAddr - MAC 주소 (다른 표기)
- hstNm - 호스트 이름
- bgImgNo - 배경 이미지 번호
## 기타 키 (Other Keys)
- menuMovSno - 메뉴 이동 시퀀스 번호
- additionalInfo - 추가 정보
- fullVideolgCatCd - 풀영상 대 카테고리 코드
- totalLogFlag - 통합 로그 플래그
- secondLayerInfo - 세컨드 레이어 정보
- panelInfo - 패널 정보
- userNumber - 사용자 번호
- loginUserData - 로그인 사용자 데이터
- appStatus - 앱 상태

View File

@@ -0,0 +1,150 @@
{
"retCode": 0,
"retMsg": "Success",
"data": {
"brandShopByShowContsList": [
{
"patncNm": "Peacock | Shop The Moment",
"patnrId": 21,
"contsNm": "Below Deck Med",
"contsId": "SHBD12345",
"contsExpsOrd": 1
},
{
"patncNm": "Peacock | Shop The Moment",
"patnrId": 21,
"contsNm": "Top Chef",
"contsId": "SHTC12345",
"contsExpsOrd": 2
}
],
"brandShopByShowContsInfo": {
"contsId": "SHBD12345",
"contsNm": "Below Deck Med",
"patnrId": "21",
"patncNm": "Peacock | Shop The Moment",
"brandShopByShowClctInfos": [
{
"clctId": "mercury-below_deck_merch",
"clctNm": "Below Deck Merch",
"clctImgUrl": "https://nonprod-commerce.nbcuni.com/uat/content-manager-assets/nbcu-comcast/GdLF-BeUT1-Below Deck CollectionAsset 1.png",
"clctExpsOrd": "1",
"brandProductInfos": [
{
"prdtId": "8ad864e8-dc12-4f01-9f68-717ad115fd06",
"prdtNm": "Alarm clock",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/d2e3f3703a9c4e94b653-dcEJtWjC.jpeg",
"priceInfo": "$ 70.00|$ 70.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "Y",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "Cup-a-Bug"
},
{
"prdtId": "02b81061-e59c-47fd-b3ff-e3c743d17148",
"prdtNm": "Single Delay",
"revwGrd": null,
"prdtImgUrl": "https://8bf2164a2f18e0674bc4-c19fb008b43eea897ccae6fb0e59b195.ssl.cf1.rackcdn.com/022511005a3946a2b8e1-MKskNz4E.jpeg",
"priceInfo": "$ 123.00|$ 123.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "N",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "Required for sponsored collection"
},
{
"prdtId": "3058cdf6-e1b7-4bc9-912f-1ef29e70b7c6",
"prdtNm": "Womens Casual Long Sleeve Half Zip Pullover",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/2800f90b251540baa940-Az4jltNx.jpeg",
"priceInfo": "$ 58.00|$ 58.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "N",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": null
},
{
"prdtId": "7716e71a-4d22-415e-943a-89739ac9b685",
"prdtNm": "IIII",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/d9b7e7e8aa9b4051a2ce-Fm2Tq6SG.jpeg",
"priceInfo": "$ 2.00|$ 2.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "N",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "chair"
}
]
},
{
"clctId": "mercury-below_deck_garden",
"clctNm": "Below Deck Garden",
"clctImgUrl": "https://nonprod-commerce.nbcuni.com/uat/content-manager-assets/nbcu-comcast/-eTpg2tMOT-Below Deck CollectionAsset 3.png",
"clctExpsOrd": "2",
"brandProductInfos": [
{
"prdtId": "399d8a6c-773f-49df-93e8-44697a4248ef",
"prdtNm": "AiryWeight Eucalyptus Sheet Set v2",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/d80b6a8edc03406badff-5kaDvtaq.jpeg",
"priceInfo": "$ 185.00|$ 185.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "Y",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "BB SUP 9.23 - SK"
},
{
"prdtId": "4e495aa2-2b10-4120-86bd-bcc9f3843d32",
"prdtNm": "Cooling Towel",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/aec0a6c4770b437ba20a-nOXvmxoD.jpeg",
"priceInfo": "$ 20.00|$ 20.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "Y",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "Posh Pickler"
},
{
"prdtId": "07c2ca90-c730-4bed-8f5d-25a815c2de11",
"prdtNm": "Towel",
"revwGrd": null,
"prdtImgUrl": "https://images.cdn.us-central1.gcp.commercetools.com/eba1c230-c331-4b91-8952-38967e532e65/3cf8c0b692724bea96ec-3jTpXCmc.jpeg",
"priceInfo": "$ 50.00|$ 50.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "N",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "BB SUP 9.23 - SK"
},
{
"prdtId": "cc85a17a-e6b5-4d0e-ad3b-67194d5aeafb",
"prdtNm": "STAINLESS STEEL MEASURING CUPS",
"revwGrd": null,
"prdtImgUrl": "https://8bf2164a2f18e0674bc4-c19fb008b43eea897ccae6fb0e59b195.ssl.cf1.rackcdn.com/9dc9460fd125488582e4-5GTzqS_6.jpeg",
"priceInfo": "$ 8.00|$ 8.00|N||||",
"freeShippingFlag": "N",
"soldoutFlag": "N",
"offerInfo": null,
"lgCatCd": null,
"lgCatNm": null,
"brndNm": "TARGET"
}
]
}
]
}
}
}

View File

@@ -13,6 +13,7 @@ import Spotlight from '@enact/spotlight';
import { Job } from '@enact/core/util';
import platform from '@enact/core/platform';
import { ThemeDecorator } from '@enact/sandstone/ThemeDecorator';
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
// import "../../../assets/fontello/css/fontello.css";
@@ -406,8 +407,6 @@ Spotlight.silentlyFocus = function (...args) {
return ret;
};
// Spotlight Focus 추적 로그 [251115]
// DOM 이벤트 리스너로 대체
@@ -426,7 +425,7 @@ Spotlight.silentlyFocus = function (...args) {
// });
// }
function AppBase(_props /* eslint-disable-line no-unused-vars */) {
function AppBase(props) {
const dispatch = useDispatch();
const httpHeader = useSelector((state) => state.common.httpHeader);
const httpHeaderRef = useRef(httpHeader);
@@ -628,7 +627,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
clearLaunchParams();
}
},
[dispatch],
[dispatch]
);
const handleRelaunchEvent = useCallback(() => {
@@ -682,7 +681,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
if (typeof window === 'object' && window.PalmSystem) {
window.PalmSystem.activate();
}
}, [initService, introTermsAgreeRef]);
}, [initService, introTermsAgreeRef, dispatch]);
const visibilityChanged = useCallback(() => {
// console.log('document is hidden', document.hidden);
@@ -726,7 +725,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
}, [dispatch]);
useEffect(() => {
const keyDownEvent = (_event /* eslint-disable-line no-unused-vars */) => {
const keyDownEvent = (event) => {
dispatch(changeAppStatus({ cursorVisible: false }));
Spotlight.setPointerMode(false);
};
@@ -735,7 +734,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
let lastMoveTime = 0;
const THROTTLE_MS = 100;
const mouseMoveEvent = (_event /* eslint-disable-line no-unused-vars */) => {
const mouseMoveEvent = (event) => {
const now = Date.now();
if (now - lastMoveTime < THROTTLE_MS) {
// throttle 기간 내에는 hideCursor만 재시작
@@ -788,6 +787,8 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
let userDataChanged = false;
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
userDataChanged = true;
}
if (!httpHeader || !deviceId) {
} else if (userDataChanged || httpHeaderRef.current === null) {
//계정정보 변경시 또는 초기 로딩시
if (!httpHeader) {
@@ -888,7 +889,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
/>
)}
<ToastContainer />
{/* <GlobalPopup /> */}
<GlobalPopup />
</ErrorBoundary>
);
}

View File

@@ -130,11 +130,16 @@ export const types = {
GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO',
GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM',
GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED',
GET_BRAND_SHOP_BY_SHOW: 'GET_BRAND_SHOP_BY_SHOW',
GET_BRAND_TOP_BANNER: 'GET_BRAND_TOP_BANNER',
SET_BRAND_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING',
SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO',
RESET_BRAND_STATE: 'RESET_BRAND_STATE',
RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO',
RESET_BRAND_LAYOUT_INFO: 'RESET_BRAND_LAYOUT_INFO',
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 관리
SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: 'SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS',
RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: 'RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS',
// main actions
GET_SUB_CATEGORY: 'GET_SUB_CATEGORY',

View File

@@ -37,10 +37,12 @@ export const getBrandList = () => (dispatch, getState) => {
export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
const { patnrId } = props;
// console.log("[getBrandLayoutInfo] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// dlog("getBrandLayoutInfo onSuccess ", response.data);
// console.log("[getBrandLayoutInfo] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({
type: types.GET_BRAND_LAYOUT_INFO,
@@ -53,6 +55,7 @@ export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
};
const onFail = (error) => {
// console.log("[getBrandLayoutInfo] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandLayoutInfo onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
@@ -336,10 +339,15 @@ export const getBrandCategoryProductInfo = (props) => (dispatch, getState) => {
export const getBrandBestSeller = (props) => (dispatch, getState) => {
const { patnrId } = props;
// console.log("[getBrandBestSeller] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// dlog("getBrandBestSeller onSuccess ", response.data);
// console.log("[getBrandBestSeller] onSuccess - patnrId:", patnrId);
// console.log("[getBrandBestSeller] Full response:", response.data.data);
// console.log("[getBrandBestSeller] brandBestSellerInfo:", response.data.data.brandBestSellerInfo);
// console.log("[getBrandBestSeller] brandBestSellerTitle in response:", response.data.data.brandBestSellerTitle);
dispatch({
type: types.GET_BRAND_BEST_SELLER,
@@ -352,6 +360,7 @@ export const getBrandBestSeller = (props) => (dispatch, getState) => {
};
const onFail = (error) => {
// console.log("[getBrandBestSeller] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandBestSeller onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
@@ -386,6 +395,79 @@ export const getBrandShowroom = (props) => (dispatch, getState) => {
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOWROOM, { patnrId }, {}, onSuccess, onFail);
};
// Featured Brands SHOP BY SHOW 정보 조회 IF-LGSP-376
export const getBrandShopByShow = (props) => (dispatch, getState) => {
const { patnrId, contsId } = props;
// console.log("[getBrandShopByShow] Called - patnrId:", patnrId, "contsId:", contsId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// console.log("[getBrandShopByShow] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({
type: types.GET_BRAND_SHOP_BY_SHOW,
payload: {
data: response.data.data,
patnrId,
contsId,
},
});
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
const onFail = (error) => {
// console.log("[getBrandShopByShow] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandShopByShow onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
// patnrId: 필수, contsId: 선택
const params = contsId ? { patnrId, contsId } : { patnrId };
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOP_BY_SHOW, params, {}, onSuccess, onFail);
};
// Featured Brands Top Banner 정보 조회 IF-LGSP-377 (NBCU 전용)
export const getBrandTopBanner = (props) => (dispatch, getState) => {
const { patnrId } = props;
// console.log("[BRAND-TOP-BANNER-API] Called - patnrId:", patnrId);
// NBCU(patnrId: 21)가 아니면 호출하지 않음
if (patnrId !== 21 && patnrId !== "21") {
console.log("[BRAND-TOP-BANNER-API] Skip - patnrId is not 21 (NBCU), patnrId:", patnrId, "(type:", typeof patnrId, ")");
return;
}
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// console.log("[BRAND-TOP-BANNER-API] onSuccess - patnrId:", patnrId);
// console.log("[BRAND-TOP-BANNER-API] Full response data:", response.data.data);
// console.log("[BRAND-TOP-BANNER-API] brandTopBannerInfo:", response.data.data.brandTopBannerInfo);
dispatch({
type: types.GET_BRAND_TOP_BANNER,
payload: {
data: response.data.data,
},
});
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
const onFail = (error) => {
// console.log("[BRAND-TOP-BANNER-API] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandTopBanner onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_TOP_BANNER, { patnrId }, {}, onSuccess, onFail);
};
// Featured Brands Recently Aired 조회 IF-LGSP-373
export const getBrandRecentlyAired = (props) => (dispatch, getState) => {
const { patnrId } = props;

View File

@@ -122,7 +122,7 @@ export const alertToast = (payload) => (dispatch) => {
}
};
export const getSystemSettings = () => (dispatch) => {
export const getSystemSettings = () => (dispatch, getState) => {
dlog('getSystemSettings ');
lunaSend.getSystemSettings(
{ category: 'caption', keys: ['captionEnable'] },
@@ -146,7 +146,7 @@ export const getSystemSettings = () => (dispatch) => {
);
};
export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getState) => {
dlog('getHttpHeaderForServiceRequest ');
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
lunaSend.getHttpHeaderForServiceRequest({
@@ -285,7 +285,7 @@ export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
});
};
export const getDeviceId = (onComplete) => (dispatch) => {
export const getDeviceId = (onComplete) => (dispatch, getState) => {
lunaSend.getDeviceId(
{ idType: ['LGUDID'] },
{
@@ -463,7 +463,7 @@ export const setFocus = (spotlightId) => ({
payload: spotlightId,
});
export const focusElement = (spotlightId) => (dispatch) => {
export const focusElement = (spotlightId) => (dispatch, getState) => {
dispatch(setFocus(spotlightId));
if (typeof window === 'object') {
@@ -485,7 +485,7 @@ export const cancelFocusElement = () => () => {
let broadcastTimer = null;
export const sendBroadCast =
({ type, moreInfo }) =>
(dispatch) => {
(dispatch, getState) => {
clearTimeout(broadcastTimer);
dispatch(changeBroadcastEvent({ type, moreInfo }));
broadcastTimer = setTimeout(() => {
@@ -542,7 +542,7 @@ export const addReservation = (data) => (dispatch) => {
});
};
export const deleteReservationCallback = (scheduleIdList) => () => {
export const deleteReservationCallback = (scheduleIdList) => (dispatch) => {
lunaSend.deleteReservationCallback(scheduleIdList, {
onSuccess: (res) => {
// dispatch(alertToast("success" + JSON.stringify(res)));
@@ -680,7 +680,7 @@ let updateNetworkStateJob = new Job((dispatch, connected) => {
dispatch(changeAppStatus({ isInternetConnected: connected }));
});
export const getConnectionStatus = () => (dispatch) => {
export const getConnectionStatus = () => (dispatch, getState) => {
lunaSend.getConnectionStatus({
onSuccess: (res) => {
dlog('lunasend getConnectionStatus', res);
@@ -709,7 +709,7 @@ export const getConnectionStatus = () => (dispatch) => {
};
// macAddress
export const getConnectionInfo = () => (dispatch) => {
export const getConnectionInfo = () => (dispatch, getState) => {
lunaSend.getConnectionInfo({
onSuccess: (res) => {
dlog('lunasend getConnectionStatus', res);
@@ -731,7 +731,7 @@ export const getConnectionInfo = () => (dispatch) => {
});
};
export const disableNotification = () => {
export const disableNotification = () => (dispatch, getState) => {
lunaSend.disableNotification({
onSuccess: (res) => {
dlog('lunasend disable notification success', res);
@@ -745,7 +745,7 @@ export const disableNotification = () => {
});
};
export const enableNotification = () => {
export const enableNotification = () => (dispatch, getState) => {
lunaSend.enableNotification({
onSuccess: (res) => {
dlog('lunasend enable notification success', res);

View File

@@ -4,6 +4,8 @@ import { types } from './actionTypes';
import { changeAppStatus, getTermsAgreeYn } from './commonActions';
import { collectBannerPositions } from '../utils/domUtils';
import { createDebugHelpers } from '../utils/debug';
import { setHidePopup, setShowPopup } from './commonActions';
import { ACTIVE_POPUP } from '../utils/Config';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
@@ -75,6 +77,38 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
const onFail = (error) => {
derror('getHomeTerms onFail ', error);
// TODO: 임시 디버그용 팝업 (재현 후 제거하세요)
const retCode = error?.data?.retCode ?? error?.retCode ?? 'unknown';
dispatch(
setShowPopup(ACTIVE_POPUP.toast, {
button1Text: `getHomeTerms onFail retCode=${retCode}`,
button2Text: 'OK',
})
);
setTimeout(() => dispatch(setHidePopup()), 1500);
// 약관 미동의(retCode 501)로 GET_HOME_TERMS가 실패하면
// introTermsAgree를 명시적으로 false로 내려 앱이 IntroPanel을 띄우도록 한다.
if (retCode === 501) {
dispatch({
type: types.GET_TERMS_AGREE_YN_SUCCESS,
payload: {
privacyTerms: 'N',
serviceTerms: 'N',
purchaseTerms: 'N',
paymentTerms: 'N',
optionalTerms: 'N',
},
});
}
// 실패 시 로딩 패널을 반드시 내려 백화 상태를 방지
dispatch(
changeAppStatus({
showLoadingPanel: { show: false },
})
);
};
TAxios(

View File

@@ -6,7 +6,7 @@ import { updateHomeInfo } from './homeActions';
import { createDebugHelpers } from '../utils/debug';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
const DEBUG_MODE = true;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// 시작 메뉴 추적을 위한 상수
@@ -39,10 +39,19 @@ export const pushPanel = (panel, duplicatable = false) => ({
duplicatable: duplicatable,
});
export const popPanel = (panelName) => ({
export const popPanel = (panelName) => {
if (DEBUG_MODE) {
console.log('[PANEL-TRACE] popPanel action creator', {
panelName,
caller: new Error().stack?.split('\n')[2]?.trim(),
});
console.trace('[PANEL-TRACE] popPanel stack trace');
}
return {
type: types.POP_PANEL,
payload: panelName,
});
};
};
export const updatePanel = (panelInfo) => ({
type: types.UPDATE_PANEL,
@@ -93,6 +102,11 @@ export const navigateToDetail = ({
...additionalInfo,
};
const state = getState();
const panels = state.panels.panels;
// 선택적 파라미터들 추가
if (curationId) panelInfo.curationId = curationId;
if (nowShelf) panelInfo.nowShelf = nowShelf;
@@ -176,8 +190,21 @@ export const navigateToDetail = ({
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false;
console.log('[Detail-BG] 🎯 navigateToDetail - Checking HomeBanner video status:', {
playerPanelModalValue: playerPanelInfo.panelInfo?.modal,
isCurrentBannerVideoPlaying,
sourceMenu,
timestamp: Date.now(),
});
// HomeBanner의 modal=true 비디오가 재생 중이면 정지
if (isCurrentBannerVideoPlaying) {
console.log('[Detail-BG] 🎬 navigateToDetail - HomeBanner video is playing (modal !== false)', {
playerPanelModal: playerPanelInfo.panelInfo?.modal,
sourceMenu,
action: 'finishVideoPreview',
timestamp: Date.now(),
});
// 🔽 비디오 상태 저장 후 정지
const { finishVideoPreview } = require('./playActions');
@@ -204,11 +231,15 @@ export const navigateToDetail = ({
})
);
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제)
// 비디오 상태 저장 후 정지
dispatch(finishVideoPreview());
} else {
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장
console.log('[Detail-BG] ⏭️ navigateToDetail - HomeBanner video is NOT playing (modal === false or undefined)', {
playerPanelModal: playerPanelInfo.panelInfo?.modal,
sourceMenu,
timestamp: Date.now(),
});
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
@@ -267,10 +298,34 @@ export const navigateToDetail = ({
case SOURCE_MENUS.PLAYER_MEDIA: {
// PlayerPanel에서 온 경우
const { hidePlayerOverlays } = require('./videoPlayActions');
const statePanels = panels || getState().panels.panels;
const playerPanelEntry =
[...statePanels].reverse().find(
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
) || null;
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
dispatch(hidePlayerOverlays());
// PlayerPanel이 modal=true라면 풀스크린 백그라운드로 전환 + lockModalFalse 설정 (Detail 동안 modal 복귀 방지)
if (playerPanelEntry) {
dispatch(
updatePanel({
name: playerPanelEntry.name,
panelInfo: {
...playerPanelEntry.panelInfo,
modal: false,
modalContainerId: undefined,
modalStyle: undefined,
modalScale: undefined,
shouldShrinkTo1px: false,
isHidden: false,
lockModalFalse: true,
},
})
);
}
// 현재 포커스된 요소 저장
if (Object.keys(focusSnapshot).length > 0) {
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
@@ -458,11 +513,28 @@ export const restoreVideoOnBack = () => {
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore;
console.log('[Detail-BG] 🔍 restoreVideoOnBack - Checking video restore state:', {
hasVideoStateToRestore: !!videoStateToRestore,
restoreOnBack: videoStateToRestore?.restoreOnBack,
sourceMenu: videoStateToRestore?.sourceMenu,
timestamp: Date.now(),
});
if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) {
console.log('[Detail-BG] ⏭️ restoreVideoOnBack - No video state to restore (skipping)', {
reason: !videoStateToRestore ? 'no videoStateToRestore' : 'restoreOnBack is false',
timestamp: Date.now(),
});
return;
}
// 비디오 복원 시작 (로그는 개발 시 필요 시 주석 해제)
console.log('[Detail-BG] ▶️ restoreVideoOnBack - Starting video restore', {
sourceMenu: videoStateToRestore.sourceMenu,
patnrId: videoStateToRestore.patnrId,
showId: videoStateToRestore.showId,
modal: true,
timestamp: Date.now(),
});
// 비디오 상태 복원
const { startVideoPlayerNew } = require('./playActions');
@@ -489,6 +561,11 @@ export const restoreVideoOnBack = () => {
})
);
console.log('[Detail-BG] ✅ restoreVideoOnBack - Video restore dispatched', {
restoredWithModal: restoreInfo.modal,
timestamp: Date.now(),
});
// 복원 상태 정리
dispatch(
updatePanel({

View File

@@ -73,6 +73,16 @@ export const startVideoPlayer =
...rest
}) =>
(dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim();
console.log('[PTRACE-SP] startVideoPlayer call', {
modal,
modalContainerId,
modalClassName,
videoId,
showUrl,
caller,
});
dlog(
'[startVideoPlayer] ✅ START - videoId:',
videoId,
@@ -105,6 +115,9 @@ export const startVideoPlayer =
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) {
console.log('[PTRACE-SP] startVideoPlayer: popping existing player before push', {
stack: panels.map((p) => p.name),
});
dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL));
@@ -182,6 +195,17 @@ export const startVideoPlayerNew =
...rest
}) =>
(dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim();
console.log('[PTRACE-SPN] startVideoPlayerNew call', {
bannerId,
modal,
modalContainerId,
modalClassName,
videoId,
showUrl,
caller,
});
dlog(
'[startVideoPlayerNew] *** ✅ START - bannerId:',
bannerId,
@@ -215,6 +239,9 @@ export const startVideoPlayerNew =
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) {
console.log('[PTRACE-SPN] popping existing player before push', {
stack: panels.map((p) => p.name),
});
dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL));
@@ -325,6 +352,12 @@ export const finishVideoPreview = () => (dispatch, getState) => {
const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1];
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) {
console.log('[PANEL-TRACE] finishVideoPreview: popping modal player', {
topPanelName: topPanel.name,
modal: topPanel.panelInfo.modal,
stack: panels.map((p) => p.name),
panelInfo: topPanel.panelInfo,
});
if (startVideoFocusTimer) {
clearTimeout(startVideoFocusTimer);
startVideoFocusTimer = null;
@@ -384,6 +417,13 @@ export const pauseModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
);
console.log('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', {
found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (modalPlayerPanel) {
if (DEBUG_MODE === true) {
dlog('[pauseModalVideo] Pausing modal video');
@@ -397,6 +437,14 @@ export const pauseModalVideo = () => (dispatch, getState) => {
},
})
);
console.log('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', {
timestamp: Date.now(),
});
} else {
console.log('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', {
timestamp: Date.now(),
});
}
};
@@ -409,6 +457,13 @@ export const resumeModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
);
console.log('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', {
found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (modalPlayerPanel && modalPlayerPanel.panelInfo?.isPaused) {
if (DEBUG_MODE === true) {
dlog('[resumeModalVideo] Resuming modal video');
@@ -422,6 +477,16 @@ export const resumeModalVideo = () => (dispatch, getState) => {
},
})
);
console.log('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', {
timestamp: Date.now(),
});
} else {
console.log('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', {
found: !!modalPlayerPanel,
isPaused: modalPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
}
};
@@ -434,6 +499,13 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
);
console.log('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', {
found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (fullscreenPlayerPanel) {
dispatch(
updatePanel({
@@ -444,6 +516,14 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
},
})
);
console.log('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', {
timestamp: Date.now(),
});
} else {
console.log('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', {
timestamp: Date.now(),
});
}
};
@@ -451,21 +531,19 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
export const resumeFullscreenVideo = () => (dispatch, getState) => {
const panels = getState().panels.panels;
// console.log('[BgVideo] resumeFullscreenVideo called - panels:', {
// panelsCount: panels?.length,
// panels: panels?.map(p => ({ name: p.name, modal: p.panelInfo?.modal, isPaused: p.panelInfo?.isPaused }))
// });
// 전체화면 PlayerPanel 찾기 (modal이 false인 패널)
const fullscreenPlayerPanel = panels.find(
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
);
// console.log('[BgVideo] resumeFullscreenVideo - fullscreenPlayerPanel found:', !!fullscreenPlayerPanel);
// console.log('[BgVideo] resumeFullscreenVideo - isPaused:', fullscreenPlayerPanel?.panelInfo?.isPaused);
console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) {
// console.log('[BgVideo] resumeFullscreenVideo - dispatching updatePanel with isPaused: false');
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
@@ -475,7 +553,16 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
},
})
);
console.log('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
timestamp: Date.now(),
});
} else {
console.log('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', {
found: !!fullscreenPlayerPanel,
isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (DEBUG_MODE === true) {
dlog('[BgVideo] resumeFullscreenVideo - Not resuming (not found or not paused)');
}
@@ -951,6 +1038,12 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
*/
export const goToFullScreen = () => (dispatch, getState) => {
console.log('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', {
targetModal: false,
action: 'updatePanel',
timestamp: Date.now(),
});
// 공유 PlayerPanel의 'modal' 상태를 false로 변경하여 전체화면으로 전환
dispatch(
updatePanel({
@@ -961,6 +1054,10 @@ export const goToFullScreen = () => (dispatch, getState) => {
},
})
);
console.log('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', {
timestamp: Date.now(),
});
};
/**
@@ -1171,6 +1268,14 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
...rest
} = videoInfo;
console.log('[Detail-BG] 🎥 startBannerVideo - Starting banner video', {
modalStatus: modal,
bannerId,
displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)',
videoId,
timestamp: Date.now(),
});
// 비디오 식별자 생성
const videoIdentifier = videoId || showUrl || bannerId;
if (videoIdentifier) {
@@ -1190,11 +1295,21 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
// 기존 PlayerPanel이 있으면 초기화
if (existingPlayerPanel) {
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
existingModalStatus: existingPlayerPanel.panelInfo?.modal,
timestamp: Date.now(),
});
clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL));
}
// 새로운 PlayerPanel push
console.log('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', {
modal,
modalContainerId,
timestamp: Date.now(),
});
dispatch(
pushPanel(
{
@@ -1216,6 +1331,10 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
)
);
console.log('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, {
timestamp: Date.now(),
});
dlog('[startBannerVideo] ✨ Panel action dispatched');
};

View File

@@ -185,8 +185,14 @@ export const TAxios = (
return;
}
// 약관 미동의(501): 토큰 재발급 큐에 넣지 않고 바로 실패 처리
if (res?.data?.retCode === 501) {
if (onFail) onFail(res);
return;
}
// RefreshToken 만료
if (res?.data?.retCode === 402 || res?.data?.retCode === 501) {
if (res?.data?.retCode === 402) {
if (baseUrl === URLS.GET_RE_AUTHENTICATION_CODE) {
dispatch(getAuthenticationCode());
} else {
@@ -349,10 +355,10 @@ export const TAxiosAdvancedPromise = (
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
// Check if the error is due to token expiration
// TAxios already handles token refresh and queueing for these codes (401, 402, 501)
// TAxios already handles token refresh and queueing for 401/402 (501은 제외)
// So we should NOT retry immediately in this loop, but let TAxios handle it.
const retCode = error?.data?.retCode;
const isTokenError = retCode === 401 || retCode === 402 || retCode === 501;
const isTokenError = retCode === 401 || retCode === 402;
// 재시도 로직
if (attempts < maxAttempts && !isTokenError) {

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import { createQueryString } from "../utils/helperMethods";
import { getUrl } from "./apiConfig";
import { DEBUG_LOG_MODE, sendToLogServer } from "./logServerClient";
export const TLogEvent = (
dispatch,
@@ -68,6 +69,23 @@ export const TLogEvent = (
prodCd,
};
}
// ===== DEBUG_LOG_MODE: 로그서버로 데이터 전송 =====
if (DEBUG_LOG_MODE) {
sendToLogServer({
deviceId: dvcId,
cntryCd,
platCd,
prodCd,
appVersion,
deviceLang,
logModel: model,
apiUrl: url,
httpMethod: type,
totalLogFlag,
});
}
let axiosInstance;
switch (type) {

View File

@@ -55,6 +55,8 @@ export const URLS = {
GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge",
GET_BRAND_SHOWROOM: "/lgsp/v1/brand/showroom.lge",
GET_BRAND_RECENTLY_AIRED: "/lgsp/v1/brand/recently/aired.lge",
GET_BRAND_SHOP_BY_SHOW: "/lgsp/v1/brand/shopByShow.lge",
GET_BRAND_TOP_BANNER: "/lgsp/v1/brand/top/banner.lge",
//on-sale controller
GET_ON_SALE_INFO: "/lgsp/v1/onsale/onsale.lge",
@@ -149,7 +151,6 @@ export const URLS = {
// foryou controller
JUSTFORYOU: "/lgsp/v1/recommend/justforyou.lge",
// emp controller
GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge",
SET_MYPAGE_TERMS_AGREE: "/lgsp/v1/mypage/terms/agree.lge",
@@ -272,11 +273,11 @@ const getRicCode = (country, ricCodeSetting) => {
if (ricCodeSetting !== "system") {
return ricCodeSetting;
}
if (country == "US") {
if (country === "US") {
return "aic";
} else if (country == "DE" || country == "GB") {
} else if (country === "DE" || country === "GB") {
return "eic";
} else if (country == "RU") {
} else if (country === "RU") {
return "ruc";
}
return null;

View File

@@ -0,0 +1,85 @@
import axios from 'axios';
// ===== DEBUG_LOG_MODE =====
// true: 로그서버로 데이터 전송
// false: 로그서버 전송 안함
export const DEBUG_LOG_MODE = false;
// ===== 로그서버 기본 설정 =====
const LOG_SERVER_URL = 'http://api.optsoft.store:55003/api/logs/realtime';
/**
* TLogEvent에서 보낸 데이터를 로그서버로 전송
*
* @param {Object} logData - TLogEvent에서 보낸 로그 데이터 (params + 추가 정보)
* @param {string} logData.deviceId - 디바이스 ID
* @param {string} logData.cntryCd - 국가 코드 (또는 countryCode)
* @param {string} logData.platCd - 플랫폼 코드
* @param {string} logData.prodCd - 제품 코드
* @param {string} logData.appVersion - 앱 버전
* @param {Object} logData.logModel - TLogEvent가 axios로 보낼 모델 객체
* @param {string} logData.apiUrl - API 엔드포인트
* @param {string} logData.httpMethod - HTTP 메서드 (get, post)
*
* @returns {Promise<void>}
*/
export async function sendToLogServer(logData) {
if (!DEBUG_LOG_MODE) {
return;
}
try {
// TLogEvent에서 전달된 messageId 사용, 없으면 null
const messageId = (logData.logModel && logData.logModel.messageId) || null;
// 로그서버에 전송할 데이터 구성
const logPayload = {
// ===== 필수 필드 =====
deviceId: logData.deviceId || logData.dvcId || 'unknown',
messageId: messageId,
logCreateTime: new Date().toISOString(),
// ===== 로그 기본 정보 =====
eventType: 'api_call',
apiUrl: logData.apiUrl || 'unknown',
httpMethod: logData.httpMethod || 'POST',
// ===== 디바이스 & 앱 정보 =====
countryCode: logData.countryCode || logData.cntryCd || 'unknown',
platformCode: logData.platformCode || logData.platCd || 'unknown',
platformVersion: logData.platformVersion || logData.prodCd || 'unknown',
appVersion: logData.appVersion || 'unknown',
deviceLang: logData.deviceLang || 'unknown',
// ===== 로그 타입별 데이터 =====
logTpNo: logData.logTpNo || logData.logType || 'unknown',
entryMenu: logData.entryMenu || 'unknown',
nowMenu: logData.nowMenu || 'unknown',
// ===== TLogEvent 원본 데이터 (model) =====
...(logData.logModel || {}),
};
// console.log('[logServerClient] Sending log to server - Full Payload:', logPayload);
// console.log('[logServerClient] Input logData:', logData);
// 로그서버로 전송 (비동기, 응답 대기 안함)
axios.post(LOG_SERVER_URL, logPayload, {
timeout: 5000, // 5초 타임아웃
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
// 성공 시 조용하게 처리
}).catch((error) => {
// 로그서버 전송 실패 시만 오류 로그 출력
console.error('[logServerClient] Failed to send log to server:', {
url: LOG_SERVER_URL,
error: error.message,
messageId: messageId,
});
});
} catch (error) {
// 함수 자체의 오류는 무시 (조용하게 처리)
}
}

View File

@@ -6,6 +6,7 @@
import React, {
useCallback,
useEffect,
useMemo,
} from 'react';
@@ -14,10 +15,13 @@ import {
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import { setHidePopup } from '../../actions/commonActions';
import { getPopupConfig } from '../../constants/popupConfig';
import usePrevious from '../../hooks/usePrevious';
import TPopUp from '../TPopUp/TPopUp';
import TopBannerPopup from '../../views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup';
// 커스텀 훅: 팝업 상태 관리
const useGlobalPopupState = () => {
@@ -128,9 +132,28 @@ const GlobalPopup = () => {
secondaryData
} = useGlobalPopupState();
const [imageDimensions, setImageDimensions] = React.useState({ width: 0, height: 0 });
const handlers = usePopupCloseHandlers();
const previousPopupVisible = usePrevious(popupVisible);
const handleImageLoad = useCallback((dimensions) => {
console.log("[GLOBAL-POPUP] Image dimensions received:", dimensions);
setImageDimensions(dimensions);
}, []);
// Spotlight 제어: 팝업 오픈/클로즈 시 포커스 트래핑
useEffect(() => {
if (popupVisible && activePopup === 'topBannerImagePopup') {
console.log("[GLOBAL-POPUP] Pausing Spotlight for modal popup");
Spotlight.pause();
return () => {
console.log("[GLOBAL-POPUP] Resuming Spotlight after modal close");
Spotlight.resume();
};
}
}, [popupVisible, activePopup]);
// 현재 팝업 설정
const currentConfig = useMemo(() => {
if (!activePopup) return null;
@@ -214,6 +237,53 @@ const GlobalPopup = () => {
return null;
}
// TopBannerImagePopup 특수 처리
if (activePopup === 'topBannerImagePopup') {
// Figma 디자인 기반 고정 크기
// 너비: 1060px
// 높이: 헤더(110px) + 이미지(556px) + 푸터(138px) = 804px
const popupWidth = '1060px';
const popupHeight = '804px';
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999
}}
onClick={handlers.handleClose}
>
<div
style={{
width: popupWidth,
height: popupHeight,
backgroundColor: 'white',
borderRadius: '12px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}
onClick={(e) => e.stopPropagation()}
>
<TopBannerPopup
title={popupData?.pupBanrImgNm || 'Popup'}
imageUrl={popupData?.pupBanrImgUrl}
imageAlt={popupData?.pupBanrImgNm || 'Popup Banner'}
onImageLoad={handleImageLoad}
/>
</div>
</div>
);
}
// 설정이 없으면 기본 팝업도 렌더링하지 않음
if (!currentConfig) {
console.warn(`No configuration found for popup type: ${activePopup}`);

View File

@@ -4,75 +4,75 @@ import React, {
useMemo,
useRef,
useState,
} from "react";
} from 'react';
import classNames from "classnames";
import classNames from 'classnames';
import {
AsYouTypeFormatter,
PhoneNumberFormat,
PhoneNumberUtil,
} from "google-libphonenumber";
import { useDispatch, useSelector } from "react-redux";
} from 'google-libphonenumber';
import { useDispatch, useSelector } from 'react-redux';
import { off, on } from "@enact/core/dispatcher";
import spotlight, { Spotlight } from "@enact/spotlight";
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import { Spottable } from "@enact/spotlight/Spottable";
import { off, on } from '@enact/core/dispatcher';
import spotlight, { Spotlight } from '@enact/spotlight';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import { Spottable } from '@enact/spotlight/Spottable';
import defaultImage from "../../../assets/images/img-thumb-empty-144@3x.png";
import { types } from "../../actions/actionTypes";
import { clearSMS, sendSms } from "../../actions/appDataActions";
import defaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
import { types } from '../../actions/actionTypes';
import { clearSMS, sendSms } from '../../actions/appDataActions';
import {
changeLocalSettings,
setHidePopup,
setShowPopup,
} from "../../actions/commonActions";
} from '../../actions/commonActions';
import {
clearRegisterDeviceInfo,
getDeviceAdditionInfo,
registerDeviceInfo,
} from "../../actions/deviceActions";
} from '../../actions/deviceActions';
import {
clearCurationCoupon,
setEventIssueReq,
} from "../../actions/eventActions";
} from '../../actions/eventActions';
import {
sendLogShopByMobile,
sendLogTotalRecommend,
} from "../../actions/logActions";
} from '../../actions/logActions';
import {
ACTIVE_POPUP,
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
LOG_TP_NO,
} from "../../utils/Config";
} from '../../utils/Config';
import {
$L,
decryptPhoneNumber,
encryptPhoneNumber,
formatLocalDateTime,
} from "../../utils/helperMethods";
import CustomImage from "../CustomImage/CustomImage";
import TButton from "../TButton/TButton";
import TPopUp from "../TPopUp/TPopUp";
import HistoryPhoneNumber from "./HistoryPhoneNumber/HistoryPhoneNumber";
import css from "./MobileSendPopUp.module.less";
import PhoneInputSection from "./PhoneInputSection";
import SMSNumKeyPad from "./SMSNumKeyPad";
} from '../../utils/helperMethods';
import CustomImage from '../CustomImage/CustomImage';
import TButton from '../TButton/TButton';
import TPopUp from '../TPopUp/TPopUp';
import HistoryPhoneNumber from './HistoryPhoneNumber/HistoryPhoneNumber';
import css from './MobileSendPopUp.module.less';
import PhoneInputSection from './PhoneInputSection';
import SMSNumKeyPad from './SMSNumKeyPad';
const SECRET_KEY = "fy7BTKuM9eeTQqEC9sF3Iw5qG43Aaip";
const SECRET_KEY = 'fy7BTKuM9eeTQqEC9sF3Iw5qG43Aaip';
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
{ enterTo: 'last-focused' },
'div'
);
const InputContainer = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
{ enterTo: 'last-focused' },
'div'
);
const SpottableComponent = Spottable("div");
const SpottableComponent = Spottable('div');
export default function MobileSendPopUp({
open,
@@ -116,39 +116,40 @@ export default function MobileSendPopUp({
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
const [inputDisabled, setInputDisabled] = useState(true);
const [mobileNumber, setMobileNumber] = useState("");
const [mobileNumber, setMobileNumber] = useState('');
const [recentSentNumber, setRecentSentNumber] = useState([]);
const [keyPadOff, setKeyPadOff] = useState(false);
const [smsRetCode, setSmsRetCode] = useState(undefined);
const agreeBtnClickedRef = useRef(false);
const deviceCountryCode = httpHeader["X-Device-Country"];
const deviceCountryCode = httpHeader['X-Device-Country'];
const mobileSendPopUpSpotlightId = useMemo(() => {
return !keyPadOff && recentSentNumber.length <= 0
? "keypad-number-1"
: "agreeAndSend";
? 'keypad-number-1'
: 'agreeAndSend';
}, [keyPadOff, recentSentNumber]);
const getMaxNum = useCallback((_deviceCountryCode) => {
if (_deviceCountryCode === "DE" || _deviceCountryCode === "GB") {
if (_deviceCountryCode === 'DE' || _deviceCountryCode === 'GB') {
return 11;
} else if (_deviceCountryCode === "KR") {
} else if (_deviceCountryCode === 'KR') {
return 12;
} else return 10;
}, []);
const MSG_SUCCESS_SENT = $L("Text Send to") + " " + mobileNumber;
const MSG_SEND_LINK = $L("Send a purchase link for this item via SMS");
const MSG_SUCCESS_SENT = $L('Text Send to') + ' ' + mobileNumber;
const MSG_SEND_LINK = $L('Send a purchase link for this item via SMS');
const handleClickSelect = (_phoneNumber) => {
setKeyPadOff((state) => !state);
setMobileNumber(_phoneNumber);
setTimeout(() => {
Spotlight.focus("keypad-number-1");
Spotlight.focus('keypad-number-1');
}, 0);
};
@@ -161,9 +162,9 @@ export default function MobileSendPopUp({
const getRawPhoneNumber = useCallback(
(key) => {
let rawPhoneNumber = `${mobileNumber}${key}`.replace(/\D/g, "");
let rawPhoneNumber = `${mobileNumber}${key}`.replace(/\D/g, '');
if (rawPhoneNumber.length === getMaxNum(deviceCountryCode)) {
Spotlight.focus("agreeAndSend");
Spotlight.focus('agreeAndSend');
}
// 테스트용: 12자리까지 허용
if (rawPhoneNumber.length > 12) {
@@ -182,11 +183,11 @@ export default function MobileSendPopUp({
numberProto,
PhoneNumberFormat.NATIONAL
);
if (deviceCountryCode === "RU" && rawPhoneNumber.startsWith("8")) {
if (deviceCountryCode === 'RU' && rawPhoneNumber.startsWith('8')) {
rawPhoneNumber = rawPhoneNumber.substring(1);
}
} else {
let formattedNumber = "";
let formattedNumber = '';
for (let i = 0; i < rawPhoneNumber.length; i++) {
formattedNumber = asYouTypeFormatter.inputDigit(
@@ -206,7 +207,7 @@ export default function MobileSendPopUp({
);
const getBackspaceRawNumber = useCallback(() => {
let rawPhoneNumber = mobileNumber.replace(/\D/g, "").slice(0, -1);
let rawPhoneNumber = mobileNumber.replace(/\D/g, '').slice(0, -1);
const phoneUtil = PhoneNumberUtil.getInstance();
const asYouTypeFormatter = new AsYouTypeFormatter(deviceCountryCode);
@@ -221,7 +222,7 @@ export default function MobileSendPopUp({
PhoneNumberFormat.NATIONAL
);
} else {
let formattedNumber = "";
let formattedNumber = '';
for (let i = 0; i < rawPhoneNumber.length; i++) {
formattedNumber = asYouTypeFormatter.inputDigit(
@@ -242,7 +243,7 @@ export default function MobileSendPopUp({
(ev) => {
if (ev && ev.key >= 0 && ev.key <= 9) {
getRawPhoneNumber(ev.key);
} else if (ev.key === "Backspace") {
} else if (ev.key === 'Backspace') {
getBackspaceRawNumber();
}
},
@@ -250,9 +251,9 @@ export default function MobileSendPopUp({
);
useEffect(() => {
on("keydown", handleKeydown);
on('keydown', handleKeydown);
return () => {
off("keydown", handleKeydown);
off('keydown', handleKeydown);
};
}, [handleKeydown]);
@@ -264,7 +265,7 @@ export default function MobileSendPopUp({
useEffect(() => {
const timer = setTimeout(() =>
setInputDisabled(mobileSendPopUpSpotlightId === "keypad-number-1")
setInputDisabled(mobileSendPopUpSpotlightId === 'keypad-number-1')
);
return () => clearTimeout(timer);
@@ -277,16 +278,16 @@ export default function MobileSendPopUp({
setMobileNumber(recentSentNumber[0]);
} else {
setKeyPadOff(false);
setMobileNumber("");
setMobileNumber('');
}
}
}, [recentSentNumber]);
const numKeypadClicked = useCallback(
(key) => {
if (key === "clear") {
setMobileNumber("");
} else if (key == "backspace") {
if (key === 'clear') {
setMobileNumber('');
} else if (key == 'backspace') {
getBackspaceRawNumber();
} else {
getRawPhoneNumber(key);
@@ -315,7 +316,7 @@ export default function MobileSendPopUp({
};
const handleAgreeSendClick = useCallback(() => {
let naturalNumber = mobileNumber.replace(/\D/g, "");
let naturalNumber = mobileNumber.replace(/\D/g, '');
// 테스트용: 길이 체크를 더 유연하게 (10자리 또는 11자리 허용)
if (
@@ -327,8 +328,8 @@ export default function MobileSendPopUp({
return;
}
if (deviceCountryCode === "KR") {
naturalNumber = "82" + naturalNumber;
if (deviceCountryCode === 'KR') {
naturalNumber = '82' + naturalNumber;
}
if (recentSentNumber && recentSentNumber.length > 0) {
@@ -394,22 +395,22 @@ export default function MobileSendPopUp({
};
// 호텔일 경우 날려야 하는 경우
if (smsTpCd === "APP00205") {
if (smsTpCd === 'APP00205') {
params = { ...params, hotelId, hotelNm, hotelDtlUrl, curationId };
}
if (smsTpCd === "APP00204") {
if (smsTpCd === 'APP00204') {
params = { ...params, curationId };
}
dispatch(sendSms(params));
}
// EVT00101 & APP00207(welcome) EVT00103 & APP00209 (welcome+Prizes) : smsTpCd 값을 받지 않음
if (evntTpCd === "EVT00101" || evntTpCd === "EVT00103") {
if (evntTpCd === 'EVT00101' || evntTpCd === 'EVT00103') {
dispatch(
registerDeviceInfo({
evntTpCd,
evntId,
evntApplcnFlag: "Y",
entryMenu: "TermsPop",
evntApplcnFlag: 'Y',
entryMenu: 'TermsPop',
mbphNo: naturalNumber,
})
);
@@ -434,7 +435,7 @@ export default function MobileSendPopUp({
onClose();
dispatch(setShowPopup(ACTIVE_POPUP.smsPopup));
setTimeout(() => Spotlight.focus("agreeAndSend"));
setTimeout(() => Spotlight.focus('agreeAndSend'));
},
[dispatch, smsRetCode]
);
@@ -450,7 +451,7 @@ export default function MobileSendPopUp({
curationCouponSuccess === 0
) {
const logParams = {
status: "send",
status: 'send',
nowMenu: nowMenu,
partner: patncNm ?? shopByMobileLogRef?.current?.patncNm,
productId: prdtId ?? shopByMobileLogRef?.current?.prdtId,
@@ -465,8 +466,8 @@ export default function MobileSendPopUp({
...shopByMobileLogRef.current,
locDt: formatLocalDateTime(new Date()),
logTpNo: LOG_TP_NO.SHOP_BY_MOBILE.AGREE_AND_SEND,
mbphNoFlag: "Y",
trmsAgrFlag: "Y",
mbphNoFlag: 'Y',
trmsAgrFlag: 'Y',
};
dispatch(sendLogShopByMobile(params));
@@ -497,24 +498,24 @@ export default function MobileSendPopUp({
}, [dispatch]);
const getSmsErrorMsg = useMemo(() => {
const SMS_ERROR_502 = $L("The event information has not been registered");
const SMS_ERROR_903 = $L("You have exceeded the daily text limit.");
const SMS_ERROR_502 = $L('The event information has not been registered');
const SMS_ERROR_903 = $L('You have exceeded the daily text limit.');
const SMS_ERROR_904 = $L(
"You have exceeded the text limit for this product."
'You have exceeded the text limit for this product.'
);
const SMS_ERROR_905 = $L(
"This number is currently blocked. To receive a message, please send UNSTOP to the number below. 07860 064195"
'This number is currently blocked. To receive a message, please send UNSTOP to the number below. 07860 064195'
);
const SMS_ERROR_906 = $L("Sorry. This item is sold out.");
const SMS_ERROR_600 = $L("This device had received first time coupon.");
const SMS_ERROR_601 = $L("There is no coupon.");
const SMS_ERROR_900 = $L("Failed to send text to {mobileNumber}").replace(
"{mobileNumber}",
const SMS_ERROR_906 = $L('Sorry. This item is sold out.');
const SMS_ERROR_600 = $L('This device had received first time coupon.');
const SMS_ERROR_601 = $L('There is no coupon.');
const SMS_ERROR_900 = $L('Failed to send text to {mobileNumber}').replace(
'{mobileNumber}',
mobileNumber
);
const SMS_ERROR_907 = $L(
"Only {length} digits is permitted. Please check again"
).replace("{length}", getMaxNum(deviceCountryCode));
'Only {length} digits is permitted. Please check again'
).replace('{length}', getMaxNum(deviceCountryCode));
switch (smsRetCode) {
case 502:
@@ -542,12 +543,12 @@ export default function MobileSendPopUp({
const getEvntErrorMsg = useMemo(() => {
if (curationCouponSuccess === 600) {
return $L("This device had received first time coupon.");
return $L('This device had received first time coupon.');
} else if (curationCouponSuccess === 601) {
return $L("There is no coupon.");
return $L('There is no coupon.');
} else {
return $L("Failed to sent text to {mobileNumber}").replace(
"{mobileNumber}",
return $L('Failed to sent text to {mobileNumber}').replace(
'{mobileNumber}',
mobileNumber
);
}
@@ -571,7 +572,7 @@ export default function MobileSendPopUp({
regDeviceInfoRetCode === undefined &&
curationCouponSuccess === undefined && (
<TPopUp
kind={"mobileSendPopup"}
kind={'mobileSendPopup'}
className={css.container}
open={open}
onClose={onClose}
@@ -602,16 +603,22 @@ export default function MobileSendPopUp({
<div className={css.headerTopRow}>
{brandLogo && (
<img
className={css.headerTopRow__brandLogo}
className={classNames(
css.headerTopRow__brandLogo,
patnrId === '1' && css.headerTopRow__brandLogo__qvc
)}
src={brandLogo}
alt="Brand"
/>
)}
{productId && (
{productId && patnrId !== '21' && (
<div className={css.headerTopRow__productId}>
ID: {productId}
</div>
)}
{patnrId === '21' && (
<div className={css.headerTopRow__productId}>{patncNm}</div>
)}
</div>
{subTitle && (
<div
@@ -639,8 +646,8 @@ export default function MobileSendPopUp({
onClick={handleInputClick}
spotlightDisabled={inputDisabled}
>
{deviceCountryCode && deviceCountryCode === "RU" && (
<span className={css.rucInput}>{"+7 "}</span>
{deviceCountryCode && deviceCountryCode === 'RU' && (
<span className={css.rucInput}>{'+7 '}</span>
)}
<span>{mobileNumber}</span>
</SpottableComponent>
@@ -662,10 +669,10 @@ export default function MobileSendPopUp({
<span
dangerouslySetInnerHTML={{
__html: `${$L(
"By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes. By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply."
'By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes. By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply.'
)}`,
}}
className={deviceCountryCode === "RU" && css.instructionRu}
className={deviceCountryCode === 'RU' && css.instructionRu}
/>
)}
</div>
@@ -698,11 +705,11 @@ export default function MobileSendPopUp({
<Container className={css.container__btnContainer}>
<TButton
onClick={handleAgreeSendClick}
spotlightId={"agreeAndSend"}
spotlightId={'agreeAndSend'}
>
{$L("Agree and Send")}
{$L('Agree and Send')}
</TButton>
<TButton onClick={onClose}>{$L("Cancel")}</TButton>
<TButton onClick={onClose}>{$L('Cancel')}</TButton>
</Container>
</TPopUp>
)}
@@ -714,7 +721,7 @@ export default function MobileSendPopUp({
text={smsTpCd ? getSmsErrorMsg : getEvntErrorMsg}
onClick={_onClose}
hasButton
button1Text={$L("OK")}
button1Text={$L('OK')}
/>
)}
{(smsRetCode === 0 ||
@@ -727,7 +734,7 @@ export default function MobileSendPopUp({
text={MSG_SUCCESS_SENT}
onClick={onClose}
hasButton
button1Text={$L("OK")}
button1Text={$L('OK')}
/>
)}
</>

View File

@@ -1,5 +1,5 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
@import '../../style/CommonStyle.module.less';
@import '../../style/utils.module.less';
/* 🆕 [NEW] Figma 디자인용 타이틀 헤드 스타일 */
.titleHead {
@@ -14,7 +14,7 @@
text-align: left; // center → left
color: black;
font-size: 32px;
font-family: "LG Smart UI";
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
@@ -32,12 +32,15 @@
height: 50px;
margin-right: 15px; // TV 호환: gap 대신 margin 사용
border-radius: 100%;
&.headerTopRow__brandLogo__qvc {
border-radius: 0;
}
}
.headerTopRow__productId {
color: #808080;
font-size: 24px;
font-family: "LG Smart UI";
font-family: 'LG Smart UI';
font-weight: 600;
line-height: 18px;
word-wrap: break-word;
@@ -57,7 +60,7 @@
top: 0;
z-index: 0;
content: "";
content: '';
}
display: flex;
> .container__header__productImg,

View File

@@ -1,5 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import TNewPopUp from '../TPopUp/TNewPopUp'; // TNewPopUp 컴포넌트의 정확한 경로를 확인해주세요.
import css from './OptionalConfirm.module.less';

View File

@@ -1,14 +1,26 @@
// src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TPopUp from '../TPopUp/TPopUp';
import TButton from '../TButton/TButton';
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
import React, {
useCallback,
useState,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { setHidePopup } from '../../actions/commonActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions';
import {
$L,
scaleH,
scaleW,
} from '../../utils/helperMethods';
import TButton from '../TButton/TButton';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
import TPopUp from '../TPopUp/TPopUp';
import css from './OptionalTermsConfirm.module.less';
const OptionalTermsConfirm = ({ open }) => {
@@ -18,7 +30,6 @@ const OptionalTermsConfirm = ({ open }) => {
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
const optionalTermsData = useSelector((state) =>
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
);
@@ -72,10 +83,10 @@ const OptionalTermsConfirm = ({ open }) => {
setIsWarningPopupVisible(false);
}, []);
const handleDontAskAgain = () => {
const handleDontAskAgain = useCallback(() => {
console.log("Don't Ask Again 처리 필요");
dispatch(setHidePopup());
};
},[dispatch]);
if (isTermsPopupVisible) {
return (

View File

@@ -1,17 +1,28 @@
// src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TNewPopUp from '../TPopUp/TNewPopUp';
import TButton from '../TButton/TButton';
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
import React, {
useCallback,
useEffect,
useState,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import { setHidePopup } from '../../actions/commonActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions';
import {
$L,
scaleH,
scaleW,
} from '../../utils/helperMethods';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import TNewPopUp from '../TPopUp/TNewPopUp';
import css from './OptionalTermsConfirmBottom.module.less';
import cssPopup from '../TPopUp/TNewPopUp.module.less';
import Spotlight from "@enact/spotlight";
const OptionalTermsConfirm = ({ open }) => {
const dispatch = useDispatch();
@@ -37,13 +48,13 @@ const OptionalTermsConfirm = ({ open }) => {
}
}, [open, isTermsPopupVisible, isWarningPopupVisible]);
const handleMainPopupClose = useCallback(() => {
dispatch(setHidePopup());
}, [dispatch]);
// const handleMainPopupClose = useCallback(() => {
// dispatch(setHidePopup());
// }, [dispatch]);
const handleCheckboxToggle = useCallback(({ selected }) => {
setIsChecked(selected);
}, []);
// const handleCheckboxToggle = useCallback(({ selected }) => {
// setIsChecked(selected);
// }, []);
const handleViewTermsClick = useCallback(() => {
setIsTermsPopupVisible(true);

View File

@@ -30,6 +30,7 @@ export default function THeader({
ariaLabel,
children,
kind,
sponserImage,
...rest
}) {
const convertedTitle = useMemo(() => {
@@ -86,6 +87,17 @@ export default function THeader({
</Marquee>
{children}
{sponserImage &&(
<div className={css.sponserImgBox}>
<CustomImage
src={sponserImage}
className={css.sponserImg}
/>
<div className={css.sponserTextBox}>
SPONSORED BY
</div>
</div>
)}
</Container>
);
}

View File

@@ -10,7 +10,7 @@
display: flex;
justify-content: flex-start;
align-items: center;
position: relative;
.title {
width: 1788px;
font-size: 42px;
@@ -42,3 +42,24 @@
box-shadow: 0px 6px 30px 0 rgba(0, 0, 0, 0.4);
}
}
.sponserImgBox {
position:absolute;
right:0;
top:0;
height:30px;
display:flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
.sponserImg {
height:30px;
}
.sponserTextBox {
padding:3px;
background-color: #474747;
color:rgba(255, 255, 255, 0.5);
font-size:14px;
border-radius: 6px;
}
}

View File

@@ -29,7 +29,7 @@
> img {
.size(@w: inherit, @h: inherit);
object-fit: cover;
border: solid 1px #f0f0f0;
// border: solid 1px #f0f0f0;
}
// discount rate
@@ -144,7 +144,7 @@
> img {
.size(@w: 288px, @h: 288px);
object-fit: contain;
border: solid 1px #f0f0f0;
// border: solid 1px #f0f0f0;
}
// discount rate

View File

@@ -1,9 +1,18 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, {
useEffect,
useRef,
} from 'react';
import { useDispatch, useSelector } from "react-redux";
import {
useDispatch,
useSelector,
} from 'react-redux';
import { getDeviceAdditionInfo } from "../../actions/deviceActions";
import { scaleH, scaleW } from "../../utils/helperMethods";
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
import {
scaleH,
scaleW,
} from '../../utils/helperMethods';
export default function TQRCode({
isBillingProductVisible,
@@ -51,6 +60,6 @@ export default function TQRCode({
correctLevel: window.QRCode.CorrectLevel.L,
});
}
}, [text, deviceInfo, entryMenu, nowMenu]);
}, [text, deviceInfo, entryMenu, nowMenu, width, height]);
return <div aria-label={ariaLabel} ref={qrcodeRef} />;
}

View File

@@ -0,0 +1,102 @@
import React, {
useEffect,
useRef,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
import {
scaleH,
scaleW,
} from '../../utils/helperMethods';
export default function TQRCodeNew({
isBillingProductVisible,
ariaLabel,
text,
width = "128",
height = "128",
}) {
const qrcodeRef = useRef(null);
const deviceInfo = useSelector((state) => state.device.deviceInfo);
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const dispatch = useDispatch();
useEffect(() => {
if (!deviceInfo) {
dispatch(getDeviceAdditionInfo());
}
}, [deviceInfo, dispatch]);
const applyCircularMask = (scaledWidth, scaledHeight) => {
if (!qrcodeRef.current) return;
const canvas = qrcodeRef.current.querySelector('canvas');
if (!canvas) return;
const radius = scaledWidth / 2;
// 원본 canvas 저장
const tempCanvas = document.createElement('canvas');
tempCanvas.width = scaledWidth;
tempCanvas.height = scaledHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(canvas, 0, 0);
// 원본 canvas 초기화
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
// 원형 마스크 적용
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, Math.PI * 2);
ctx.clip();
// 이미지 다시 그리기
ctx.drawImage(tempCanvas, 0, 0);
};
useEffect(() => {
if (typeof window === "object" && entryMenu && nowMenu) {
if (qrcodeRef.current) {
while (qrcodeRef.current.firstChild) {
qrcodeRef.current.removeChild(qrcodeRef.current.firstChild);
}
}
// nowMenu 데이터를 Base64로 인코딩
const encodedNowMenu = encodeURIComponent(nowMenu);
const encodeEntryMenu = encodeURIComponent(entryMenu);
let idx;
if (deviceInfo === null || !deviceInfo) {
idx = 0;
} else {
idx = deviceInfo?.dvcIndex;
}
const scaledWidth = scaleW(width);
const scaledHeight = scaleH(height);
const qrcode = new window.QRCode(qrcodeRef.current, {
text: isBillingProductVisible
? text
: `${text}&entryMenu=${encodeEntryMenu}&nowMenu=${encodedNowMenu}&idx=${idx}`,
width: scaledWidth,
height: scaledHeight,
correctLevel: window.QRCode.CorrectLevel.L,
});
// QR코드 생성 완료 후 원형 마스킹 적용
setTimeout(() => {
applyCircularMask(scaledWidth, scaledHeight);
}, 100);
}
}, [text, deviceInfo, entryMenu, nowMenu, isBillingProductVisible, width, height]);
return <div aria-label={ariaLabel} ref={qrcodeRef} />;
}

View File

@@ -1,16 +1,27 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import compose from 'ramda/src/compose';
import { useDispatch } from 'react-redux';
import { Job } from '@enact/core/util';
import { Marquee, MarqueeController } from '@enact/sandstone/Marquee';
import {
Marquee,
MarqueeController,
} from '@enact/sandstone/Marquee';
import Spottable from '@enact/spotlight/Spottable';
import css from './TabItemSub.module.less';
import { sendLogTotalRecommend } from '../../actions/logActions';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from '../../utils/Config';
import { useDispatch } from 'react-redux';
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
} from '../../utils/Config';
import css from './TabItemSub.module.less';
const SpottableComponent = Spottable('div');
@@ -122,11 +133,11 @@ const TabItemBase = ({
<>
{subtitle && (
<div className={css.textWithIcon}>
{IconComponent && (
{/* {IconComponent && (
<span className={css.iconWrapper}>
<IconComponent iconType={focused ? 'focused' : selected ? 'selected' : 'normal'} />
</span>
)}
)} */}
<Marquee
marqueeDisabled={!focused}
marqueeOn={'focus'}

View File

@@ -172,23 +172,8 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
break;
//브랜드
case 10300:
result = [
// NBCU 브랜드 (하드코딩)
{
icons: FeaturedBrandIcon,
id: 'nbcu-brand',
path: 'assets/images/featuredBrands/nbcu.svg',
patncNm: 'NBCU',
spotlightId: 'spotlight_featuredbrand_nbcu',
target: [
{
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: 'gnb', patnrId: 'NBCU' },
},
],
},
// API에서 가져온 기존 브랜드들
...(data?.shortFeaturedBrands?.map((item) => ({
result =
data?.shortFeaturedBrands?.map((item) => ({
icons: FeaturedBrandIcon,
id: item.patnrId,
path: item.patncLogoPath,
@@ -200,8 +185,7 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
panelInfo: { from: 'gnb', patnrId: item.patnrId },
},
],
})) || []),
];
})) || [];
break;
//
case 10600:

View File

@@ -1157,11 +1157,6 @@ const VideoPlayerBase = class extends React.Component {
// detection of when "more" is pressed vs when the state is updated is mismatched. Using an
// instance variable that's only set and used for this express purpose seems cleanest.
// TabContainerV2가 표시 중이면 자동으로 닫지 않음
if (this.props.tabContainerVersion === 2 && this.props.belowContentsVisible) {
return;
}
if (this.props.autoCloseTimeout && !this.props.sideContentsVisible) {
this.autoCloseJob.startAfter(this.props.autoCloseTimeout);
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef } from "react";
export default function useScrollTo() {
export default function useScrollTo({ skipAutoScrollTop = false } = {}) {
const scrollTo = useRef();
const scrollTop = useCallback(
@@ -23,8 +23,10 @@ export default function useScrollTo() {
}, []);
useEffect(() => {
if (!skipAutoScrollTop) {
scrollTop();
}, []);
}
}, [skipAutoScrollTop]);
return { getScrollTo, scrollLeft, scrollTop, scrollToRef: scrollTo };
}

View File

@@ -16,7 +16,7 @@ import { calculateIsPanelOnTop } from '../utils/panelUtils'; // 🎯 isOnTop 유
// DEBUG_MODE - true인 경우에만 로그 출력
// ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단
const DEBUG_MODE = false;
const DEBUG_MODE = true;
/**
* Panel history middleware
@@ -125,6 +125,14 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
// POP 후 top panel을 기록 (이전 패널로 돌아감)
if (panels.length > 0) {
const topPanel = panels[panels.length - 1];
if (DEBUG_MODE) {
console.log('[PANEL-TRACE] POP_PANEL middleware stack', {
stack: panels.map((p) => p.name),
topPanel: topPanel?.name,
payload: action.payload,
caller: new Error().stack?.split('\n')[2]?.trim(),
});
}
if (topPanel && topPanel.name) {
const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산

View File

@@ -44,6 +44,17 @@ const initialState = {
brandRecentlyAiredData: {
data: {},
},
brandShopByShowData: {
data: {},
},
brandTopBannerData: {
data: {},
},
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 저장소
brandShopByShowCategoryGroups: {},
};
export const brandReducer = (state = initialState, action) => {
@@ -155,6 +166,51 @@ export const brandReducer = (state = initialState, action) => {
brandRecentlyAiredData: action.payload,
};
case types.GET_BRAND_SHOP_BY_SHOW: {
// 일부 응답은 리스트 없이 내려와 기존 데이터를 덮어 지우는 문제가 있어 조건부 병합
const prevData = state.brandShopByShowData?.data || {};
const nextData = action.payload?.data || {};
const hasNextList = Array.isArray(nextData.brandShopByShowContsList);
// 리스트가 없으면 이전 리스트 유지
const mergedData = hasNextList
? nextData
: { ...prevData, ...nextData, brandShopByShowContsList: prevData.brandShopByShowContsList };
// 🆕 [251210] patnrId=21인 경우 그룹 데이터 별도 저장
const updatedCategoryGroups = { ...state.brandShopByShowCategoryGroups };
if (action.payload?.patnrId === 21 || action.payload?.patnrId === "21") {
const patnrId = String(action.payload.patnrId);
// patnrId별 그룹 데이터가 없으면 초기화
if (!updatedCategoryGroups[patnrId]) {
updatedCategoryGroups[patnrId] = {};
}
// 현재 contsId에 대한 그룹 정보 저장
if (nextData.brandShopByShowContsInfo?.contsId) {
const contsId = nextData.brandShopByShowContsInfo.contsId;
updatedCategoryGroups[patnrId][contsId] = nextData.brandShopByShowContsInfo;
}
}
return {
...state,
brandShopByShowData: {
...action.payload,
data: mergedData,
},
brandShopByShowCategoryGroups: updatedCategoryGroups,
};
}
case types.GET_BRAND_TOP_BANNER:
return {
...state,
brandTopBannerData: action.payload,
};
case types.SET_BRAND_LIVE_CHANNEL_UPCOMING:
return {
...state,
@@ -184,6 +240,25 @@ export const brandReducer = (state = initialState, action) => {
};
}
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 설정
case types.SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: {
return {
...state,
brandShopByShowCategoryGroups: {
...state.brandShopByShowCategoryGroups,
...action.payload,
},
};
}
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 초기화
case types.RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: {
return {
...state,
brandShopByShowCategoryGroups: {},
};
}
default:
return state;
}

View File

@@ -132,6 +132,60 @@ export const panelsReducer = (state = initialState, action) => {
case types.UPDATE_PANEL: {
let lastIndex = -1;
let lastAction = 'update';
const hasDetailPanel = state.panels.some((p) => p.name === panel_names.DETAIL_PANEL);
const isPlayerPanel =
action.payload.name === panel_names.PLAYER_PANEL ||
action.payload.name === panel_names.PLAYER_PANEL_NEW;
const existingPanel = state.panels.find((p) => p.name === action.payload.name);
let nextPanelInfo = action.payload.panelInfo || {};
// lockModalFalse 플래그 처리: DetailPanel이 스택에 있거나 lock이 이미 true면 modal=true 업데이트를 차단
if (isPlayerPanel && existingPanel) {
const lockFlag =
existingPanel.panelInfo?.lockModalFalse === true || nextPanelInfo.lockModalFalse === true;
// unlock 명시 시 그대로 진행
if (nextPanelInfo.lockModalFalse === false) {
// do nothing
} else if (lockFlag && nextPanelInfo.modal === true) {
nextPanelInfo = {
...nextPanelInfo,
modal: false,
modalContainerId: undefined,
lockModalFalse: true,
modalStyle: undefined,
modalScale: undefined,
shouldShrinkTo1px: false,
isHidden: false,
};
} else if (lockFlag && nextPanelInfo.modal === undefined && hasDetailPanel) {
nextPanelInfo = {
...nextPanelInfo,
modal:
existingPanel.panelInfo?.modal === true ? false : existingPanel.panelInfo?.modal,
modalContainerId:
existingPanel.panelInfo?.modal === true
? undefined
: existingPanel.panelInfo?.modalContainerId,
lockModalFalse: true,
modalStyle: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalStyle,
modalScale: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalScale,
shouldShrinkTo1px: false,
isHidden: false,
};
} else if (hasDetailPanel && nextPanelInfo.modal === true) {
// DetailPanel 존재 시 modal=true 업데이트 차단
nextPanelInfo = {
...nextPanelInfo,
modal: false,
modalContainerId: undefined,
modalStyle: undefined,
modalScale: undefined,
shouldShrinkTo1px: false,
isHidden: false,
};
}
}
// 배열의 끝에서부터 시작하여 조건에 맞는 마지막 인덱스 찾기
for (let i = state.panels.length - 1; i >= 0; i--) {
if (state.panels[i].name === action.payload.name) {
@@ -143,7 +197,7 @@ export const panelsReducer = (state = initialState, action) => {
index === lastIndex
? {
...panel,
panelInfo: { ...panel.panelInfo, ...action.payload.panelInfo },
panelInfo: { ...panel.panelInfo, ...nextPanelInfo },
}
: panel
);

View File

@@ -20,7 +20,10 @@ import {
setShowPopup,
} from '../../../actions/commonActions';
import { sendLogTotalRecommend } from '../../../actions/logActions';
import { popPanel } from '../../../actions/panelActions';
import {
popPanel,
pushPanel,
} from '../../../actions/panelActions';
import TButton from '../../../components/TButton/TButton';
import TPopUp from '../../../components/TPopUp/TPopUp';
import TQRCode from '../../../components/TQRCode/TQRCode';
@@ -249,13 +252,25 @@ export default function InformationContainer({
);
const handleCancel = useCallback(() => {
dispatch(setHidePopup());
}, [dispatch]);
const handleDone = useCallback(() => {
dispatch(setHidePopup());
// dispatch(setHidePopup());
const { patnrId, prdtId } = checkoutData.productList[0];
dispatch(popPanel());
}, [dispatch]);
dispatch(
pushPanel({
name: Config.panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId },
})
);
}, [dispatch, checkoutData]);
const { shippingAddressList, billingAddressList, cardInfo } = checkoutData || {};
useEffect(() => {
if (!shippingAddressList || !billingAddressList || !cardInfo) {
// if (shippingAddressList || billingAddressList || cardInfo) { //확인용도로 반대로 테스트중.
dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup));
}
}, [shippingAddressList, billingAddressList, cardInfo, dispatch])
return (
<>
@@ -282,8 +297,8 @@ export default function InformationContainer({
>
ADD/EDIT
</TButton>
{checkoutData?.shippingAddressList && (
<ShippingAddressCard list={checkoutData.shippingAddressList} onFocus={handleFocus} />
{shippingAddressList && (
<ShippingAddressCard list={shippingAddressList} onFocus={handleFocus} />
)}
</div>
<div className={css.listBox}>
@@ -300,8 +315,8 @@ export default function InformationContainer({
{/* <div style={{ padding: '10px', textAlign: 'center', color: '#999' }}>
Mock Billing Address
</div> */}
{checkoutData?.billingAddressList && (
<BillingAddressCard list={checkoutData.billingAddressList} onFocus={handleFocus} />
{billingAddressList && (
<BillingAddressCard list={billingAddressList} onFocus={handleFocus} />
)}
</div>
<div className={css.listBox}>
@@ -314,7 +329,7 @@ export default function InformationContainer({
>
ADD/EDIT
</TButton>
{checkoutData?.cardInfo && <PaymentCard list={checkoutData.cardInfo} />}
{cardInfo && <PaymentCard list={cardInfo} />}
</div>
<div className={css.listBox}>
<Subject title="OFFERS & PROMOTION" />
@@ -355,7 +370,7 @@ export default function InformationContainer({
'Please update your information and complete the payment on your mobile. By clicking the OK button, you will be redirected to the product details page'
)}
</h3>
<TButton className={css.popupBtn} onClick={handleDone}>
<TButton className={css.popupBtn} onClick={handleCancel}>
{$L('OK')}
</TButton>
</div>

View File

@@ -303,6 +303,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu;
console.log('[DP-TRACE] Detail unmount start', {
sourcePanel,
sourceMenu,
panelsSnapshot: panels.map((p) => p.name),
});
console.log('[Detail-BG] 306-line sourcePanel:', sourcePanel, 'sourceMenu:', sourceMenu);
// DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount:', {
sourcePanel,
@@ -323,6 +331,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달
lockModalFalse: false, // Detail 종료 시 lock 해제
},
})
);
@@ -1025,34 +1034,56 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
// ProductAllSection에 비디오가 있는지 확인
const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)();
console.log('[BgVideo] DetailPanel - Video Control Check:', {
console.log('[Detail-BG] 🎬 DetailPanel - Video Control Check (mount/update):', {
hasPlayerPanel,
isModal,
playerPanelModalStatus: isModal,
hasProductVideo,
sourceMenu: panelInfo?.sourceMenu,
productDataUrl: productData?.prdtMediaUrl,
timestamp: Date.now(),
});
// PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
if (hasPlayerPanel && hasProductVideo) {
console.log('[BgVideo] DetailPanel - Pausing video');
console.log('[Detail-BG] ⏸️ DetailPanel - Pausing PlayerPanel video (match: playerPanel + productVideo)', {
isModalVideo: isModal,
action: isModal ? 'pauseModalVideo' : 'pauseFullscreenVideo',
timestamp: Date.now(),
});
if (isModal) {
dispatch(pauseModalVideo());
} else {
dispatch(pauseFullscreenVideo());
}
} else {
console.log('[BgVideo] DetailPanel - Skipping pause');
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping pause (no playerPanel or no productVideo)', {
hasPlayerPanel,
hasProductVideo,
reason: !hasPlayerPanel ? 'no playerPanel' : 'no productVideo',
timestamp: Date.now(),
});
}
return () => {
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
if (hasPlayerPanel && hasProductVideo) {
console.log('[BgVideo] DetailPanel - Resuming video');
console.log('[Detail-BG] ▶️ DetailPanel - Resuming PlayerPanel video (unmount cleanup)', {
isModalVideo: isModal,
action: isModal ? 'resumeModalVideo' : 'resumeFullscreenVideo',
sourceMenu: panelInfo?.sourceMenu,
timestamp: Date.now(),
});
if (isModal) {
dispatch(resumeModalVideo());
} else {
dispatch(resumeFullscreenVideo());
}
} else {
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping resume on unmount', {
hasPlayerPanel,
hasProductVideo,
timestamp: Date.now(),
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1080,12 +1111,33 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const hasPlayerPanel = panels.some(
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true
);
const launchedFromPlayer = panelInfo?.fromPlayer || panelInfo?.sourcePanel === panel_names.PLAYER_PANEL;
if (hasPlayerPanel) {
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview');
if (hasPlayerPanel && !launchedFromPlayer) {
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview (non-player source)');
dispatch(finishVideoPreview());
} else if (hasPlayerPanel && launchedFromPlayer) {
console.log('[DetailPanel] PlayerPanel modal=true detected - launched from Player, skip finishVideoPreview');
// Detail 동안 modal=true로 바뀌지 않도록 lockModalFalse 설정
const playerPanelEntry = panels.find(
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
);
if (playerPanelEntry?.panelInfo?.modal === true) {
dispatch(
updatePanel({
name: playerPanelEntry.name,
panelInfo: {
...playerPanelEntry.panelInfo,
modal: false,
modalContainerId: undefined,
lockModalFalse: true,
},
})
);
}
}, [panels, dispatch]);
}
}, [panels, dispatch, panelInfo?.fromPlayer, panelInfo?.sourcePanel]);
return (
<div ref={containerRef}>

View File

@@ -112,6 +112,8 @@ import {
} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo
from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import SeeMoreProducts
from '../ProductContentSection/SeeMoreProducts/SeeMoreProducts';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
import YouMayAlsoLike
@@ -267,6 +269,10 @@ export default function ProductAllSection({
const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible);
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터
const brandShopByShowCategoryGroups = useSelector(
(state) => state.brand.brandShopByShowCategoryGroups
);
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(1);
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
@@ -274,7 +280,6 @@ export default function ProductAllSection({
// const [currentHeight, setCurrentHeight] = useState(0);
//하단부분까지 갔을때 체크용
const [documentHeight, setDocumentHeight] = useState(0);
const [isBottom, setIsBottom] = useState(false);
//qr코드 노출용
@@ -283,6 +288,8 @@ export default function ProductAllSection({
// sendLogGNB용 entryMenu
const entryMenuRef = useRef(null);
const lastProductDetailLogKeyRef = useRef(null);
const lastGnbLogKeyRef = useRef(null);
// 출처 정보 통합 (향후 확장성 대비)
// YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지
@@ -298,6 +305,7 @@ export default function ProductAllSection({
// 모든 timeout/timer를 추적하기 위한 ref
const timersRef = useRef([]);
const contentHeightRef = useRef(0);
// handleScrollToImages의 timeout을 추적하기 위한 ref
const scrollToImagesTimeoutRef = useRef(null);
@@ -322,15 +330,20 @@ export default function ProductAllSection({
if (!container) return;
if (typeof container.scrollTo === 'function') {
scrollTop({ y: 0, animate: true });
const timeOut = setTimeout(()=>{
setTimeout(()=>{
if(hasVideo){
Spotlight.focus("product-video-player");
} else {
Spotlight.focus("product-detail-container-0");
}
},100);
}
}, [
scrollTop
scrollTop,
hasVideo
]);
useEffect(() => {
const fetchCouponData = useCallback(() => {
dispatch(
getProductCouponSearch({
patnrId: selectedPatnrId,
@@ -338,7 +351,14 @@ export default function ProductAllSection({
mbrNo: userNumber,
})
);
}, [dispatch]);
}, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]);
useEffect(() => {
// 필수 값이 모두 있을 때만 호출
if (selectedPatnrId && selectedPrdtId) {
fetchCouponData();
}
}, [fetchCouponData]);
const getCouponCode = () => {
const snoArray = [];
@@ -378,7 +398,7 @@ export default function ProductAllSection({
setCouponTypes(idx);
dispatch(setShowPopup(Config.ACTIVE_POPUP.couponPopup));
},
[dispatch, popupVisible, promotions, userNumber]
[dispatch, userNumber, partnerCoupon]
);
const handleCouponTotDownload = useCallback(() => {
@@ -507,7 +527,7 @@ export default function ProductAllSection({
)}
aria-label="Download Button"
>
{downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno)
{downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && duplDwldYn === 'N'
? $L('DOWNLOAD COMPLETED')
: $L('DOWNLOAD')}
</div>
@@ -550,6 +570,21 @@ export default function ProductAllSection({
[productType, themeProductInfo, themeProducts, selectedIndex, productInfo]
);
// 🆕 [251211] patnrId=21인 경우 QR 데이터 확인
useEffect(() => {
if (productData?.patnrId === 21 || productData?.patnrId === "21") {
console.log('[QR-Data] patnrId=21 QR 데이터 확인:', {
patnrId: productData?.patnrId,
prdtId: productData?.prdtId,
qrImgUrl: productData?.qrImgUrl,
qrCodeUrl: productData?.qrCodeUrl,
hasQrImgUrl: !!productData?.qrImgUrl,
hasQrCodeUrl: !!productData?.qrCodeUrl,
allData: productData,
});
}
}, [productData]);
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
const isBillingProductVisible = useMemo(() => {
// API Mode: 기존 로직 100% 유지 (절대 수정 안 함)
@@ -721,6 +756,7 @@ export default function ProductAllSection({
// sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴
useEffect(() => {
if (!productData?.prdtId) return;
if (!entryMenuRef.current) entryMenuRef.current = nowMenu;
// BUY NOW 버튼 활성화 상태에 따른 메뉴 결정 (Source SingleProduct vs UnableProduct 패턴)
@@ -742,9 +778,25 @@ export default function ProductAllSection({
? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}`
: baseMenu;
const logKey = `${productData?.patnrId || ''}-${productData?.prdtId || ''}`;
if (lastGnbLogKeyRef.current === logKey) {
return;
}
lastGnbLogKeyRef.current = logKey;
dispatch(sendLogGNB(menu));
}, [
isBillingProductVisible,
isGroupProductVisible,
isTravelProductVisible,
fromPanel?.fromYouMayLike,
productData?.patnrId,
productData?.prdtId,
nowMenu,
]);
// sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화)
useEffect(() => {
if (fromPanel?.fromYouMayLike === true) {
dispatch(updatePanel({
name: panel_names.DETAIL_PANEL,
@@ -756,11 +808,28 @@ export default function ProductAllSection({
}
}));
}
}, [fromPanel?.fromYouMayLike, isBillingProductVisible, isUnavailableProductVisible, isGroupProductVisible, isTravelProductVisible]); // BUY NOW 상태 변경 시 재실행
}, [fromPanel?.fromYouMayLike, dispatch, panelInfo]);
// sendLogProductDetail 로깅 - Source의 productData 변경 감지와 동일한 패턴
useEffect(() => {
if (productData && Object.keys(productData).length > 0) {
if (!productData || Object.keys(productData).length === 0) {
return;
}
const logTpNo = isTravelProductVisible
? Config.LOG_TP_NO.PRODUCT.TRAVEL_DETAIL
: isGroupProductVisible
? Config.LOG_TP_NO.PRODUCT.GROUP_DETAIL
: isBillingProductVisible
? Config.LOG_TP_NO.PRODUCT.BILLING_PRODUCT_DETAIL
: Config.LOG_TP_NO.PRODUCT.PRODUCT_DETAIL;
const logKey = `${productData?.patnrId || ''}-${productData?.prdtId || ''}-${panelInfo?.linkTpCd || ''}-${logTpNo}`;
if (lastProductDetailLogKeyRef.current === logKey) {
return;
}
lastProductDetailLogKeyRef.current = logKey;
const params = {
befPrice: productData?.priceInfo?.split("|")[0],
curationId: productData?.curationId ?? "",
@@ -772,13 +841,7 @@ export default function ProductAllSection({
lgCatCd: productData?.catCd ?? "",
lgCatNm: productData?.catNm ?? "",
linkTpCd: panelInfo?.linkTpCd ?? "",
logTpNo: isTravelProductVisible
? Config.LOG_TP_NO.PRODUCT.TRAVEL_DETAIL
: isGroupProductVisible
? Config.LOG_TP_NO.PRODUCT.GROUP_DETAIL
: isBillingProductVisible
? Config.LOG_TP_NO.PRODUCT.BILLING_PRODUCT_DETAIL
: Config.LOG_TP_NO.PRODUCT.PRODUCT_DETAIL,
logTpNo,
patncNm: productData?.patncNm ?? "",
patnrId: productData?.patnrId ?? "",
prdtId: productData?.prdtId ?? "",
@@ -788,9 +851,15 @@ export default function ProductAllSection({
tsvFlag: productData?.todaySpclFlag ?? "",
};
return () => dispatch(sendLogProductDetail(params));
}
}, [productData, entryMenuRef.current, panelInfo?.linkTpCd, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]); // productData 변경 시 재실행
dispatch(sendLogProductDetail(params));
}, [
productData,
panelInfo?.linkTpCd,
isBillingProductVisible,
isGroupProductVisible,
isTravelProductVisible,
dispatch,
]); // productData 변경 시 재실행
// [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
@@ -951,6 +1020,9 @@ export default function ProductAllSection({
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = useState(false);
// 🆕 [251210] patnrId=21 SEE MORE PRODUCTS 버튼 표시 여부
const [hasSeeMoreProducts, setHasSeeMoreProducts] = useState(false);
const [seeMoreProductsData, setSeeMoreProductsData] = useState([]);
const reviewTotalCount = stats.totalReviews;
@@ -1060,6 +1132,7 @@ export default function ProductAllSection({
const descriptionRef = useRef(null);
const reviewRef = useRef(null);
const youMayAlsoLikelRef = useRef(null);
const seeMoreProductsRef = useRef(null);
const prevMediaPanelModalStateRef = useRef(null); // MediaPanel의 이전 modal 상태 추적
// 동영상과 이미지를 통합한 렌더링 아이템 리스트 생성 (Indicator.jsx 로직 기반)
@@ -1106,8 +1179,7 @@ export default function ProductAllSection({
return hasVideo && productVideoVersion === 1;
}, [hasVideo, productVideoVersion]);
const handleShopByMobileOpen = useCallback(
pipe(() => {
const handleShopByMobileOpen = useCallback(() => {
// sendLogShopByMobile - Source와 동일한 로깅 추가
if (productData && Object.keys(productData).length > 0) {
const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData;
@@ -1132,9 +1204,7 @@ export default function ProductAllSection({
}
setMobileSendPopupOpen(true); // 팝업 열기
}, setMobileSendPopupOpen),
[]
);
}, [productData, dispatch]);
const shopByMobileId = useMemo(
() => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
@@ -1219,6 +1289,116 @@ export default function ProductAllSection({
Spotlight.focus("detail_youMayAlsoLike_area")
},100);
}, [scrollToSection, dispatch]);
const handleSeeMoreProductsClick = useCallback(() => {
console.log('[SEE MORE PRODUCTS] Button clicked - just scroll to section');
// 버튼 클릭 시 스크롤만 처리 (데이터는 useEffect에서 처리)
scrollToSection('scroll-marker-see-more-products');
setTimeout(() => {
Spotlight.focus("detail_seeMoreProducts_area");
}, 100);
}, [scrollToSection]);
// 🆕 [251210] patnrId=21인 경우 그룹 상품 데이터 미리 처리
useEffect(() => {
if (panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") {
console.log('[SEE MORE PRODUCTS] patnrId=21 detected - processing group data on panel mount');
console.log('[SEE MORE PRODUCTS] panelInfo:', panelInfo);
console.log('[SEE MORE PRODUCTS] brandShopByShowCategoryGroups:', brandShopByShowCategoryGroups);
const patnrIdString = String(panelInfo.patnrId);
const currentPrdtId = panelInfo.prdtId;
const categoryGroups = brandShopByShowCategoryGroups[patnrIdString];
console.log('[SEE MORE PRODUCTS] patnrIdString:', patnrIdString);
console.log('[SEE MORE PRODUCTS] currentPrdtId:', currentPrdtId);
console.log('[SEE MORE PRODUCTS] categoryGroups:', categoryGroups);
let shouldShowButton = false; // 버튼 표시 여부
if (categoryGroups) {
let foundGroup = null;
let foundConts = null;
// 모든 contsId에서 현재 상품이 속한 그룹 찾기
for (const contsId in categoryGroups) {
const contsInfo = categoryGroups[contsId];
console.log(`[SEE MORE PRODUCTS] Checking contsId: ${contsId}, contsNm: ${contsInfo.contsNm}`);
if (contsInfo.brandShopByShowClctInfos) {
for (const group of contsInfo.brandShopByShowClctInfos) {
console.log(`[SEE MORE PRODUCTS] Checking group: ${group.clctNm} (${group.clctId})`);
if (group.brandProductInfos) {
const foundProduct = group.brandProductInfos.find(p => p.prdtId === currentPrdtId);
if (foundProduct) {
console.log('[SEE MORE PRODUCTS] 🎯 Found current product:', foundProduct);
foundGroup = group;
foundConts = contsInfo;
break;
}
}
}
if (foundGroup) break;
}
}
if (foundGroup && foundConts) {
console.log('[SEE MORE PRODUCTS] ✅ Found group info:');
console.log(' - Category:', foundConts.contsNm);
console.log(' - Group:', foundGroup.clctNm);
console.log(' - Group ID:', foundGroup.clctId);
// 현재 상품을 제외한 다른 상품들 확인
const otherProducts = foundGroup.brandProductInfos.filter(p => p.prdtId !== currentPrdtId);
console.log(' - Other products count:', otherProducts.length);
if (otherProducts.length > 0) {
// 다른 상품이 있을 때만 버튼 표시
shouldShowButton = true;
console.log('[SEE MORE PRODUCTS] 📦 Showing button - group has other products:');
otherProducts.forEach((product, index) => {
console.log(` ${index + 1}. ${product.prdtNm} (${product.prdtId})`);
console.log(` - Price: ${product.priceInfo}`);
console.log(` - Brand: ${product.brndNm || 'N/A'}`);
});
// 🆕 SeeMoreProducts 컴포넌트를 위한 데이터 변환
const formattedProducts = otherProducts.map(product => ({
prdtId: product.prdtId,
prdtNm: product.prdtNm,
priceInfo: product.priceInfo,
patncNm: foundConts.patncNm,
patnrId: foundConts.patnrId,
brndNm: product.brndNm,
imgUrl: product.prdtImgUrl,
lgCatCd: product.lgCatCd,
offerInfo: product.offerInfo,
groupNm: foundGroup.clctNm,
}));
// YouMayAlsoLike 데이터 형식으로 맞추기
setSeeMoreProductsData(formattedProducts);
} else {
console.log('[SEE MORE PRODUCTS] ❌ No other products in group - hiding button');
setSeeMoreProductsData([]);
}
} else {
console.log('[SEE MORE PRODUCTS] ❌ No group found for current product - hiding button');
}
} else {
console.log('[SEE MORE PRODUCTS] ❌ No category groups found for patnrId 21 - hiding button');
}
// 버튼 표시 여부 상태 설정
setHasSeeMoreProducts(shouldShowButton);
} else {
// patnrId=21이 아닌 경우 버튼 숨김
setHasSeeMoreProducts(false);
}
}, [panelInfo, brandShopByShowCategoryGroups]);
// 헤더 Back 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정
useEffect(() => {
const firstId = stackOrder[0];
@@ -1235,6 +1415,13 @@ export default function ProductAllSection({
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
const scrollExpandTimerRef = useRef(null); // 스크롤 확장 타이머
const mediaMinimizedRef = useRef(false);
const getTotalContentHeight = useCallback(() => {
const measuredHeight =
contentHeightRef.current ||
scrollContainerRef.current?.scrollHeight ||
0;
return measuredHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
}, []);
const handleArrowClickAlternative = useCallback(() => {
dispatch(minimizeModalMedia());
@@ -1247,14 +1434,14 @@ export default function ProductAllSection({
animate: true,
});
// documentHeight를 활용하여 반복 계산 제거
const totalHeight = documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
// 캐시된 콘텐츠 높이를 활용하여 반복 계산 최소화
const totalHeight = getTotalContentHeight();
const isAtBottom = scrollPositionRef.current + 1100 >= totalHeight;
if (isAtBottom) {
setIsBottom(isAtBottom);
}
}, [documentHeight, scrollTop, dispatch]);
}, [scrollTop, dispatch, getTotalContentHeight]);
const handleScroll = useCallback(
(e) => {
@@ -1265,12 +1452,12 @@ export default function ProductAllSection({
const prevScrollTop = prevScrollTopRef.current;
scrollPositionRef.current = currentScrollTop;
contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
// 기존 bottom 체크 로직 유지
if (documentHeight) {
const isAtBottom =
scrollPositionRef.current + 944 >=
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
const totalHeight = getTotalContentHeight();
if (totalHeight) {
const isAtBottom = scrollPositionRef.current + 944 >= totalHeight;
if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom);
}
@@ -1323,7 +1510,7 @@ export default function ProductAllSection({
}
// v2: onScrollStop에서 처리 (기존 로직 유지)
},
[documentHeight, isBottom, productVideoVersion, isVideoPlaying, dispatch]
[isBottom, productVideoVersion, isVideoPlaying, dispatch, getTotalContentHeight]
);
// 스크롤 멈추었을 때만 호출 (성능 최적화)
@@ -1331,10 +1518,10 @@ export default function ProductAllSection({
(e) => {
const currentScrollTop = e.scrollTop;
scrollPositionRef.current = currentScrollTop;
if (documentHeight) {
const isAtBottom =
currentScrollTop + 944 >=
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
const totalHeight = getTotalContentHeight();
if (totalHeight) {
const isAtBottom = currentScrollTop + 944 >= totalHeight;
if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom);
}
@@ -1349,7 +1536,7 @@ export default function ProductAllSection({
return shouldMinimize;
});
},
[documentHeight, isBottom]
[getTotalContentHeight]
);
useEffect(() => {
@@ -1374,14 +1561,6 @@ export default function ProductAllSection({
setActiveButton(null);
}, []);
useEffect(() => {
setDocumentHeight(
(productDetailRef.current?.scrollHeight || 0) +
(descriptionRef.current?.scrollHeight || 0) +
(reviewRef.current?.scrollHeight || 0)
);
}, [productDetailRef.current, descriptionRef.current, reviewRef.current]);
// 스크롤 위치에 따른 MediaPanel 제어 (비디오 재생 중에는 자동 제어 안함 - unmount 시에만 정리)
// useEffect(() => {
// console.log('📍 [ProductAllSection] useEffect 실행 - shouldMinimizeMedia:', shouldMinimizeMedia);
@@ -1422,6 +1601,8 @@ export default function ProductAllSection({
};
}, []);
const sponserImage = useMemo(() => productData?.spnsrImgUrl, [productData]);
// WebOS TV focus-within 대체 로직
// useEffect(() => {
// const detailAreaElement = document.querySelector('.detailArea');
@@ -1700,7 +1881,7 @@ export default function ProductAllSection({
</TButton>
</>
)}
{hasYouMayAlsoLike && (
{(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
<TButton
className={classNames(
css.youMayLikeButton,
@@ -1711,6 +1892,19 @@ export default function ProductAllSection({
{$L('YOU MAY ALSO LIKE')}
</TButton>
)}
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 버튼 */}
{(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
<TButton
className={classNames(
css.seeMoreProductButton,
activeButton === 'seemoreproducts' ? css.active : ''
)}
onClick={handleSeeMoreProductsClick}
spotlightId="see-more-products-button"
>
{$L('SEE MORE PRODUCTS')}
</TButton>
)}
{/* YouMayLike 버튼 렌더링 상태 로그 */}
{/* {(() => {
console.log('[YouMayLike] 버튼 렌더링 체크:', {
@@ -1873,7 +2067,7 @@ export default function ProductAllSection({
</div>
<div ref={youMayAlsoLikelRef}>
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div>
{hasYouMayAlsoLike && (
{(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
<div id="you-may-also-like-section">
{/* {(() => {
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
@@ -1895,6 +2089,22 @@ export default function ProductAllSection({
)}
</div>
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 섹션 */}
{(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
<div ref={seeMoreProductsRef}>
<div id="scroll-marker-see-more-products" className={css.scrollMarker}></div>
<div id="see-more-products-section" data-spotlight-id="see-more-products-area">
<SeeMoreProducts
groupProducts={seeMoreProductsData}
sponserImage={sponserImage}
panelInfo={panelInfo}
onFocus={() => handleButtonFocus('seemoreproducts')}
onBlur={handleButtonBlur}
/>
</div>
</div>
)}
<div className={css.topButtonBox}>
<TButton
className={css.tButton}

View File

@@ -680,7 +680,8 @@
.productDetailsButton,
.userReviewsButton,
.youMayLikeButton {
.youMayLikeButton,
.seeMoreProductButton {
align-self: stretch;
height: 60px;
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경
@@ -708,7 +709,8 @@
.productDetailsButton,
.userReviewsButton,
.youMayLikeButton {
.youMayLikeButton,
.seeMoreProductButton {
align-self: stretch;
height: 60px;
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경

View File

@@ -1,14 +1,25 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import css from "./ProductDetail.new.module.less";
import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
import Spottable from "@enact/spotlight/Spottable";
import CustomImage from "../../../../components/CustomImage/CustomImage";
import indicatorDefaultImage from "../../../../../assets/images/img-thumb-empty-144@3x.png";
import useScrollTo from "../../../../hooks/useScrollTo";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import indicatorDefaultImage
from '../../../../../assets/images/img-thumb-empty-144@3x.png';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import TVirtualGridList
from '../../../../components/TVirtualGridList/TVirtualGridList';
import useScrollTo from '../../../../hooks/useScrollTo';
// TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해
import { removeSpecificTags } from "../../../../utils/helperMethods";
import Spotlight from "@enact/spotlight";
import { removeSpecificTags } from '../../../../utils/helperMethods';
import css from './ProductDetail.new.module.less';
const Container = SpotlightContainerDecorator(
{
@@ -59,7 +70,8 @@ export default function ProductDetail({ productInfo }) {
const image = listImages[0] || indicatorDefaultImage;
const imageIndex = productInfo?.imageIndex ?? 0;
const totalImages = productInfo?.totalImages ?? listImages.length;
const sponserImage = productInfo?.spnsrImgUrl;
const spnsrNm = productInfo?.spnsrNm;
return (
<div className={css.thumbnailWrapper}>
<CustomImage
@@ -68,6 +80,19 @@ export default function ProductDetail({ productInfo }) {
fallbackSrc={indicatorDefaultImage}
className={css.productImage}
/>
{imageIndex === 0 && sponserImage &&(
<div className={css.sponserImgBox}>
<CustomImage
src={sponserImage}
alt={spnsrNm}
fallbackSrc={indicatorDefaultImage}
className={css.sponserImg}
/>
<div className={css.sponserTextBox}>
SPONSORED BY
</div>
</div>
)}
</div>
);
}, [listImages, productInfo?.imageIndex, productInfo?.totalImages]);

View File

@@ -70,14 +70,15 @@
.thumbnailWrapper {
position: relative;
width: 658px;
// width: 658px;
width:100%;
height: 610px;
display: flex;
align-items: center;
justify-content: center;
.productImage {
width: 100%;
width: 658px;
height: 100%;
margin: 0;
padding: 0;
@@ -89,4 +90,20 @@
transition: all 0.2s ease;
}
}
.sponserImgBox {
position:absolute;
right:20px;
top:0px;
.sponserImg {
width:145px;
}
.sponserTextBox {
padding: 3px;
background-color: #474747;
font-size:14px;
font-weight:bold;
color: rgba(255, 255, 255, 0.5);
border-radius: 12px;
}
}
}

View File

@@ -0,0 +1,219 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { Job } from '@enact/core/util';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { clearThemeDetail } from '../../../../actions/homeActions';
import { finishModalMediaForce } from '../../../../actions/mediaActions';
import {
popPanel,
pushPanel,
updatePanel,
} from '../../../../actions/panelActions';
import { finishVideoPreview } from '../../../../actions/playActions';
import THeader from '../../../../components/THeader/THeader';
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
import TVerticalPagenator
from '../../../../components/TVerticalPagenator/TVerticalPagenator';
import useScrollTo from '../../../../hooks/useScrollTo';
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods';
import css from './SeeMoreProducts.module.less';
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator(
{
enterTo: 'last-focused',
leaveFor: {
left: 'spotlight-product-info-section-container',
},
},
'div'
);
export default function SeeMoreProducts({
groupProducts,
sponserImage,
panelInfo,
onFocus,
onBlur,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const [newGroupProductData, setNewGroupProductData] = useState([]);
const dispatch = useDispatch();
const focusedContainerIdRef = useRef(null);
const panels = useSelector((state) => state.panels.panels);
const themeProductInfos = useSelector((state) => state.home.themeCurationDetailInfoData);
const launchedFromPlayer = useMemo(() => {
const detailPanelIndex = panels.findIndex(({ name }) => name === 'detailpanel');
const playerPanelIndex = panels.findIndex(({ name }) => name === 'playerpanel');
return detailPanelIndex - 1 === playerPanelIndex;
}, [panels]);
const onFocusedContainerId = useCallback((containerId) => {
focusedContainerIdRef.current = containerId;
}, []);
const _onFocus = useCallback(() => {
if (onFocus) {
onFocus();
}
}, [onFocus]);
const _onBlur = useCallback(() => {
if (onBlur) {
onBlur();
}
}, [onBlur]);
// 그룹 상품 데이터 처리 (YOU MAY ALSO LIKE와 동일 로직)
useEffect(() => {
console.log('[SeeMoreProducts] 그룹 상품 데이터 처리:', {
originalData: groupProducts,
originalLength: groupProducts?.length || 0,
hasData: !!(groupProducts && groupProducts.length > 0)
});
if (groupProducts && groupProducts.length > 0) {
// 최대 9개로 제한 (YOU MAY ALSO LIKE와 동일)
const processedData = groupProducts.length > 9
? groupProducts.slice(0, groupProducts.length - 1)
: groupProducts;
console.log('[SeeMoreProducts] 처리된 데이터 설정:', {
processedLength: processedData.length,
processedData
});
setNewGroupProductData(processedData);
} else {
console.log('[SeeMoreProducts] 데이터 없음 - 빈 배열 설정');
setNewGroupProductData([]);
}
}, [groupProducts]);
const cursorOpen = useRef(new Job((func) => func(), 1000));
return (
<div>
{newGroupProductData && newGroupProductData.length > 0 && (
<TVerticalPagenator
spotlightId={'detail_seeMoreProducts_area'}
data-wheel-point={true}
className={css.tVerticalPagenator}
defaultContainerId={panelInfo?.focusedContainerId}
onFocusedContainerId={onFocusedContainerId}
topMargin={36}
>
<Container className={css.container} onFocus={_onFocus} onBlur={_onBlur}>
<THeader title={newGroupProductData[0].groupNm} className={css.tHeader} sponserImage={sponserImage} />
<div className={css.renderCardContainer}>
{newGroupProductData?.map((product, index) => {
const {
imgUrl: prdtImgUrl, // 이미지 URL 변경
patnrId,
prdtId,
prdtNm,
priceInfo,
offerInfo,
patncNm,
brndNm,
lgCatCd,
euEnrgLblInfos,
} = product;
const handleItemClick = () => {
console.log('[SeeMoreProducts] 상품 클릭:', product);
// Promise 체이닝으로 순서 보장 (YOU MAY ALSO LIKE와 동일)
Promise.resolve()
.then(() => {
// 1. 기존 비디오 강제 종료
dispatch(finishVideoPreview());
dispatch(finishModalMediaForce());
if (themeProductInfos && themeProductInfos.length > 0) {
dispatch(clearThemeDetail());
}
})
.then(() => {
// 2. 비디오 종료 후 새로운 상품으로 업데이트
dispatch(
updatePanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
showNm: panelInfo?.showNm,
showId: panelInfo?.showId,
liveFlag: panelInfo?.liveFlag,
thumbnailUrl: panelInfo?.thumbnailUrl,
patnrId,
prdtId,
launchedFromPlayer: launchedFromPlayer,
fromPanel: {
fromSeeMoreProducts: true, // 🆕 SeeMoreProducts에서 선택된 상품임을 표시
},
},
})
);
cursorOpen.current.stop();
});
};
// prdtId가 없는 경우를 대비한 안정적인 key 생성
const itemKey = prdtId ? `${patnrId}-${prdtId}` : `see-more-product-${index}`;
// 🆕 [251210] TItemCardNew에 spotlightId와 spottable 설정 추가
return (
<TItemCardNew
key={itemKey}
className={css.itemCardNew}
contextName={LOG_CONTEXT_NAME.YOUMAYLIKE}
messageId={LOG_MESSAGE_ID.CONTENTCLICK}
productId={prdtId}
nowProductId={panelInfo?.prdtId}
nowProductTitle={panelInfo?.prdtNm}
nowCategory={panelInfo?.catNm}
catNm={lgCatCd}
patnerName={patncNm}
brandName={brndNm}
imageAlt={prdtId}
imageSource={prdtImgUrl}
priceInfo={priceInfo}
offerInfo={offerInfo}
productName={prdtNm}
onClick={handleItemClick}
label={index * 1 + 1 + ' of ' + newGroupProductData.length}
lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos}
/>
);
})}
</div>
</Container>
</TVerticalPagenator>
)}
</div>
);
}

View File

@@ -0,0 +1,105 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
// .container {
// .size(@w: 874px,@h:500px);
// .itemWrapper {
// .size(@w: 874px,@h:500px);
// .item {
// .size(@w: 300px,@h:300px);
// }
// }
// }
.tVerticalPagenator {
.size(@w: 1114px, @h: auto); // 마진 포함 전체 크기 (1054px + 60px)
max-width: 1114px;
// padding-left: 30px; // 좌측 30px 마진
// padding-right: 30px; // 우측 30px 마진
box-sizing: border-box;
// .sectionTitle {
// .font(@fontFamily: @baseFont, @fontSize: 30px);
// min-height: 56px;
// font-weight: 700;
// color: rgba(234, 234, 234, 1);
// // margin: 30px 0 20px 0;
// }
.tHeader {
background: transparent;
.size(@w: 100%, @h: 36px); // 마진 제외 콘텐츠 크기
margin-bottom: 20px;
position:relative;
> div{
padding:0;
}
.averageOverallRating {
.size(@w: 176px,@h:30px);
}
span {
font-size: 30px;
font-weight: 700;
height: 36px;
color: rgba(234, 234, 234, 1);
}
}
.container {
width: 100%;
.flex(@direction:column,@alignCenter:flex-start);
flex-wrap: wrap;
margin-top: 34px;
// > div {
// margin: 0 15px 15px 0;
// }
.renderCardContainer {
width: 1144px;
display: flex;
flex-wrap: wrap;
// margin-top: 34px;
> div.itemCardNew {
/* item card */
margin: 0 15px 15px 0;
.size(@w:360px,@h:494px);
background-color: rgba(51, 51, 51, 0.95);
border: none;
> div:nth-child(1) {
/* img wrapper*/
.size(@w:323px,@h:323px);
> img {
.size(@w:100%,@h:100%);
}
}
> div:nth-child(2) {
margin-top: 15px;
/* desc wrapper */
> div > div > h3 {
/* title */
color: rgba(234, 234, 234, 1);
.size(@w:100%,@h:64px);
line-height: 31px;
}
> p {
/* priceInfo */
height: 43px;
text-align: center;
> span {
font-size: 24px;
}
}
}
// width: 100%;
// padding-left: 60px;
// overflow: unset;
}
}
}
}

View File

@@ -38,10 +38,6 @@ export default function UserReviewDetail({
}
}, [onNext, currentIndex, totalReviews]);
useEffect(() => {
console.log("####currentIndex", currentIndex);
}, [currentIndex]);
// 리뷰 데이터가 없을 때 처리
if (!currentReview) {
return (

View File

@@ -48,10 +48,7 @@
.averageOverallRating {
width: 176px;
height: 30px;
display: flex;
align-items: center;
margin-left: auto;
background-size: cover;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
}

View File

@@ -7,6 +7,7 @@ import React, {
import classNames from 'classnames';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
@@ -104,17 +105,16 @@ export default function UserReviewsPopup({
// All-Images 및 User-Reviews 모드를 위한 상태
const [currentReviewIndex, setCurrentReviewIndex] = useState(0);
// 모드별 리뷰 인덱스 초기화
// ✅ selectedImageIndex 변경 감지 추가: 클릭한 리뷰가 팝업에 표시되도록
// 모드 변경 시 초기화
useEffect(() => {
console.log('[UserReviewsPopup] Mode or selectedImageIndex changed:', {
mode,
selectedImageIndex,
allReviewsLength: allReviews?.length || 0,
});
if (mode === "all-images" && images && images[selectedImageIndex]) {
const selectedImage = images[selectedImageIndex];
if (mode === "all-images" && selectedImageIndex !== undefined) {
const selectedImage = images?.[selectedImageIndex];
if (selectedImage) {
const reviewIndex = reviewsWithImages.findIndex(
(review) => review.rvwId === selectedImage.reviewId
);
@@ -122,18 +122,12 @@ export default function UserReviewsPopup({
console.log('[UserReviewsPopup] all-images mode - found review index:', reviewIndex);
setCurrentReviewIndex(reviewIndex);
}
} else if (mode === "user-reviews") {
// User-Reviews 모드: selectedImageIndex를 그대로 사용
}
} else if (mode === "user-reviews" && selectedImageIndex !== undefined) {
console.log('[UserReviewsPopup] user-reviews mode - setting index to:', selectedImageIndex);
setCurrentReviewIndex(selectedImageIndex);
}
}, [
mode,
selectedImageIndex, // ✅ 추가: 선택된 이미지 인덱스 변경 감지
images, // ✅ 추가: 이미지 데이터 변경 감지
reviewsWithImages, // ✅ 추가: 이미지 있는 리뷰 변경 감지
allReviews, // ✅ 추가: allReviews 변경 감지
]);
}, [mode, selectedImageIndex]); // selectedImageIndex는 명시적 변경 시만
// 리뷰 네비게이션 핸들러 (All-Images 및 User-Reviews 모드)
const handlePreviousReview = useCallback(() => {
@@ -182,6 +176,7 @@ export default function UserReviewsPopup({
// 모드가 변경되면 이미지 로딩 상태 초기화
useEffect(() => {
setImageLoadStates({});
Spotlight.focus("review-popup-container");
}, [mode]);
// 모든 이미지 표시
@@ -216,6 +211,7 @@ export default function UserReviewsPopup({
? "user-review-detail-prev"
: "user-review-image-0"
}
spotlightId="review-popup-container"
>
{mode === "customer-images" && (
<TScrollerDetail

View File

@@ -4,6 +4,8 @@ import classNames from 'classnames';
import { useSelector } from 'react-redux';
import TQRCode from '../../../../components/TQRCode/TQRCode';
import TQRCodeNew from '../../../../components/TQRCode/TQRCodeNew';
import QRCodePatnr21 from './QRCodePatnr21';
import { getQRCodeUrl } from '../../../../utils/helperMethods';
import css from './QRCode.module.less';
@@ -56,13 +58,37 @@ export default function QRCode({
return detailUrl;
}, [productInfo, isShopByMobile, detailUrl]);
// patnrId === 21인 경우 qrImgUrl 처리
const isPatnrId21 = productInfo?.patnrId === 21 || productInfo?.patnrId === "21";
const qrImgUrl = isPatnrId21 ? productInfo?.qrImgUrl : null;
return (
<div className={classNames(css.qrcode, kind ? css.detailQrcode : "")}>
{/* {qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />} */}
{kind === "detail" ? (
<TQRCode text={qrCodeUrl} width="240" height="240" />
isPatnrId21 ? (
<QRCodePatnr21
qrImgUrl={qrImgUrl}
fallbackText={qrCodeUrl}
width="240"
height="240"
/>
) : (
qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />
<TQRCode text={qrCodeUrl} width="240" height="240" />
)
) : (
qrCodeUrl && (
isPatnrId21 ? (
<QRCodePatnr21
qrImgUrl={qrImgUrl}
fallbackText={qrCodeUrl}
width="190"
height="190"
/>
) : (
<TQRCode text={qrCodeUrl} width="190" height="190" />
)
)
)}
{/* todo : 시나리오,UI 릴리즈 후 */}
<div className={css.tooltip}>

View File

@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import TQRCode from '../../../../components/TQRCode/TQRCode';
/**
* patnrId=21 전용 QR 이미지 처리 컴포넌트
* 서버에서 제공하는 qrImgUrl을 우선 표시하고,
* 로드 실패 시 TQRCode(QR 코드 생성)로 폴백합니다.
*
* @param {string} qrImgUrl - 서버 제공 QR 이미지 URL (productData.qrImgUrl)
* @param {string} fallbackText - TQRCode 생성 시 사용할 QR 텍스트 (qrCodeUrl)
* @param {string} width - 너비 ("190" 또는 "240")
* @param {string} height - 높이 ("190" 또는 "240")
*/
export default function QRCodePatnr21({
qrImgUrl,
fallbackText,
width = '190',
height = '190',
}) {
const [imageError, setImageError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
// 1. qrImgUrl이 없으면 TQRCode 폴백
if (!qrImgUrl) {
return <TQRCode text={fallbackText} width={width} height={height} />;
}
// 2. 이미지 로드 실패 → TQRCode 폴백
if (imageError) {
return <TQRCode text={fallbackText} width={width} height={height} />;
}
// 3. 이미지 로드 성공 → 이미지 표시 (기존 QRCode와 동일한 레이아웃)
const sizeInPx = `${width}px`;
return (
<div
style={{
width: sizeInPx,
height: sizeInPx,
background: 'transparent',
margin: '0 auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
border: 'none',
}}
>
<img
src={qrImgUrl}
alt="QR Code"
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
style={{
display: imageLoaded ? 'block' : 'none',
width: '100%',
height: '100%',
objectFit: 'contain',
}}
/>
{!imageLoaded && (
<div
style={{
fontSize: '12px',
color: '#999',
textAlign: 'center',
position: 'absolute',
}}
>
Loading...
</div>
)}
</div>
);
}

View File

@@ -52,6 +52,7 @@ export default function BuyNowPriceDisplay({
display: "inline-flex",
height: "60px",
lineHeight: "60px",
flexWrap: "wrap",
}}
>
<span className={css.price}>

View File

@@ -30,7 +30,7 @@
}
.name {
font-weight: bold;
font-size: 36px;
font-size: 30px;
color: @COLOR_GRAY07;
}
.btmLayer {
@@ -188,9 +188,11 @@
.productNm {
width: 100%;
font-weight: bold;
font-size: 36px;
font-size: 38px;
color: @COLOR_WHITE;
flex:none;
.elip(2);
margin-bottom: 20px;
height:76px;
line-height:38px;
}

View File

@@ -9,6 +9,12 @@ import usePriceInfo from '../../../../../hooks/usePriceInfo';
import { $L } from '../../../../../utils/helperMethods';
import css from './ShopByMobilePriceDisplay.module.less';
// 파트너명에서 "Peacock | Shop The Moment" 형식일 때 "Peacock"만 추출
const extractPartnerName = (name) => {
if (!name) return name;
return name.includes(' | ') ? name.split(' | ')[0].trim() : name;
};
export default function ShopByMobilePriceDisplay({
priceData,
priceInfo,
@@ -26,6 +32,10 @@ export default function ShopByMobilePriceDisplay({
orderPhnNo,
} = priceData;
// 파트너명 정규화
const cleanPatncNm = useMemo(() => extractPartnerName(patncNm), [patncNm]);
const cleanPatnrName = useMemo(() => extractPartnerName(patnrName), [patnrName]);
const {
discountRate,
rewardFlag,
@@ -50,6 +60,8 @@ export default function ShopByMobilePriceDisplay({
[isOriginalPriceEmpty, isDiscountedPriceEmpty, price5, offerInfo]
);
const renderPriceItem = useCallback(() => {
if (priceData && !promotionCode) {
if (rewd) {
@@ -57,9 +69,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}>
<div className={css.rewdTopLayer}>
<span>
{patncNm
? patncNm + " " + $L("Price") + " "
: patnrName + " " + $L("Price") + " "}
{cleanPatncNm
? cleanPatncNm + " " + $L("Price") + " "
: cleanPatnrName + " " + $L("Price") + " "}
</span>
<span className={css.partnerPrc}>
{TYPE_CASE.case5 || TYPE_CASE.case8
@@ -95,9 +107,9 @@ export default function ShopByMobilePriceDisplay({
return (
<div className={css.wrapper}>
<span className={css.name}>
{patncNm
? patncNm + " " + $L("Price")
: patnrName + " " + $L("Price")}
{cleanPatncNm
? cleanPatncNm + " " + $L("Price")
: cleanPatnrName + " " + $L("Price")}
</span>
<div className={css.btmLayer}>
<span className={classNames(css.price, css.case01)}>
@@ -111,9 +123,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}>
<div className={css.topLayer}>
<span className={css.name}>
{patncNm
? patncNm + " " + $L("Price")
: patnrName + $L("Price")}
{cleanPatncNm
? cleanPatncNm + " " + $L("Price")
: cleanPatnrName + $L("Price")}
</span>
</div>
{discountRate && Number(discountRate.replace("%", "")) > 4 && (
@@ -140,9 +152,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}>
<div className={css.topLayer}>
<span className={css.name}>
{patncNm
? patncNm + " " + $L("Price")
: patnrName + " " + $L("Price")}
{cleanPatncNm
? cleanPatncNm + " " + $L("Price")
: cleanPatnrName + " " + $L("Price")}
</span>
</div>
{discountRate && Number(discountRate.replace("%", "")) > 4 && (
@@ -194,7 +206,7 @@ export default function ShopByMobilePriceDisplay({
return (
<div className={css.wrapper}>
<span className={css.partnerName}>
{patncNm ? patncNm + " " + $L("Price") : patnrName + $L("Price")}
{cleanPatncNm ? cleanPatncNm + " " + $L("Price") : cleanPatnrName + $L("Price")}
</span>
<span
className={css.offerInfo}
@@ -204,7 +216,8 @@ export default function ShopByMobilePriceDisplay({
);
}
}, [
patnrName,
cleanPatnrName,
cleanPatncNm,
priceInfo,
isOriginalPriceEmpty,
isDiscountedPriceEmpty,

View File

@@ -1425,8 +1425,6 @@ const BuyOption = ({
setTimeout(() => {
Spotlight.focus('buy-option-buy-now-button');
});
dispatch(setHidePopup());
dispatch(launchMembershipApp());
} else {
dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup));
@@ -1525,6 +1523,7 @@ const handleCartMove = useCallback(() => {
disabled: detail.optStkQty <= 0,
imageUrl: detail.optImgUrl || null,
price: detail.priceInfo.split('|')[1],
prodOptCdCval: detail.prodOptCdCval,
})) || []),
]}
selectedIndex={selectedOptionItemIndex}

View File

@@ -72,6 +72,7 @@ const CustomDropDown = ({
const selectedOption = normalizedOptions[selectedIndex];
const selectedLabel = selectedOption?.label || placeholder;
const selectedId = selectedOption?.prodOptCdCval ? selectedOption?.prodOptCdCval : null;
const selectedImage = selectedOption?.imageUrl;
return (
@@ -93,7 +94,7 @@ const CustomDropDown = ({
className={styles.custom_dropdown__image}
/>
)}
<div className={styles.custom_dropdown__text}>{selectedLabel}</div>
<div className={styles.custom_dropdown__text}>{selectedLabel} {selectedId ? `ID : ${selectedId}` : ''}</div>
</div>
<div className={styles.custom_dropdown__icon}>
<img src={iconDownArrow} alt="dropdown arrow" />
@@ -108,7 +109,6 @@ const CustomDropDown = ({
.map((option, reverseIndex) => {
const originalIndex = normalizedOptions.length - 1 - reverseIndex;
const isOptionDisabled = option.disabled;
return (
<SpottableDiv
key={originalIndex}
@@ -133,7 +133,7 @@ const CustomDropDown = ({
/>
)}
<span className={styles.custom_dropdown__optname}>
{option.label}
{option.label} {option.prodOptCdCval ? `ID : ${option.prodOptCdCval}` : ''}
</span>
{isOptionDisabled ? (
<span className={styles.custom_dropdown__lasttxt}>

View File

@@ -10,6 +10,7 @@ import ontv4u from '../../../../../assets/images/bg/ontv4u_new.png';
import Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../../../assets/images/bg/shoplc_new.png';
import nbcu from '../../../../../assets/images/bg/nbcu_new.png';
import css from './DetailPanelBackground.module.less';
/**
@@ -38,6 +39,7 @@ export default function DetailPanelBackground({
11: shoplc,
16: koreaKiosk,
19: Pinkfong,
21: nbcu,
};
const detailPanelBg = useMemo(() => {
@@ -75,26 +77,32 @@ export default function DetailPanelBackground({
useEffect(() => {
// launchedFromPlayer가 true이면 배경 이미지를 로드하지 않음 (PlayerPanel 비디오 보이도록)
if (launchedFromPlayer) {
// console.log('[DetailPanelBackground] Skip background image loading - launchedFromPlayer=true (showing PlayerPanel video)');
console.log('[Detail-BG] 🔵 DetailPanelBackground - Skip background image (launchedFromPlayer=true, showing PlayerPanel video)', {
launchedFromPlayer,
timestamp: Date.now(),
});
setImageReady(false);
return;
}
// launchedFromPlayer가 false일 때만 배경 이미지 로드
// console.log('[DetailPanelBackground] Loading background image - launchedFromPlayer=false');
console.log('[Detail-BG] 🟢 DetailPanelBackground - Loading background image (launchedFromPlayer=false)', {
patnrId,
imagePath: detailPanelBg,
});
if (ImagePreloader.isLoaded(detailPanelBg)) {
// console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg);
console.log('[Detail-BG] ✅ DetailPanelBackground - Using preloaded image:', detailPanelBg);
setImageReady(true);
} else {
// 프리로드되지 않았다면 즉시 로드 시도
// console.log('[DetailPanelBackground] Image not preloaded, loading on-demand:', detailPanelBg);
console.log('[Detail-BG] 📥 DetailPanelBackground - Image not preloaded, loading on-demand:', detailPanelBg);
ImagePreloader.preloadImage(detailPanelBg)
.then(() => {
// console.log('[DetailPanelBackground] On-demand image loaded:', detailPanelBg);
console.log('[Detail-BG] ✅ DetailPanelBackground - On-demand image loaded:', detailPanelBg);
setImageReady(true);
})
.catch((e) => {
// console.error('[DetailPanelBackground] On-demand image load failed:', e);
console.error('[Detail-BG] ❌ DetailPanelBackground - On-demand image load failed:', e);
// 실패해도 이미지를 표시해야 함
setImageReady(true);
});

View File

@@ -9,6 +9,7 @@ import ontv4u from '../../../../../assets/images/bg/ontv4u_new.png';
import Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../../../assets/images/bg/shoplc_new.png';
import nbcu from '../../../../../assets/images/bg/nbcu_new.png';
import css from './DetailPanelBackground.module.less';
// ==================== 로깅 함수들 ====================
@@ -65,6 +66,7 @@ export default function DetailPanelBackgroundV2({
11: shoplc, // SHOPLC
16: koreaKiosk, // KOREA KIOSK
19: Pinkfong, // PINKFONG
21: nbcu, // NBCU
}),
[]
);
@@ -200,7 +202,7 @@ export function PreloadedBackgroundImages({
launchedFromPlayer = false,
}) {
// 모든 파트너사 ID 목록
const allPatnrIds = useMemo(() => [1, 2, 4, 9, 11, 16, 19], []);
const allPatnrIds = useMemo(() => [1, 2, 4, 9, 11, 16, 19, 21], []);
// ✅ 원래 로직 복원: HomePanel이 onTop이 아니고 selectedPatnrId가 있을 때만 배경 표시
const shouldShowBackground = !isHomePanelOnTop && selectedPatnrId;

View File

@@ -113,7 +113,7 @@ export default function THeaderCustom({
marqueeDisabled={marqueeDisabled}
aria-label={ariaLabel}
>
{prdtId && <span className={css.prdtId}>ID : {prdtId}</span>}
{(prdtId && patnrId !== "21") && <span className={css.prdtId}>ID : {prdtId}</span>}
{convertedTitle && (
<span dangerouslySetInnerHTML={{ __html: convertedTitle }} />
)}

View File

@@ -2,19 +2,55 @@ import React, { memo } from "react";
import IcPartnersDefault from "../../../../assets/images/ic-tab-partners-default@3x.png";
import CustomImage from "../../../components/CustomImage/CustomImage";
import TopBannerImage from "../TopBannerImage/TopBannerImage";
import css from "./Banner.module.less";
export default memo(function Banner({
brandInfo,
brandTopImgInfo,
brandTopBannerInfo,
panelPatnrId,
selectedPatnrId,
}) {
const selectedBrandInfo =
brandInfo?.find(({ patnrId }) => panelPatnrId === patnrId) || {};
const { patncLogoPath, patncNm } = selectedBrandInfo;
const { topImgAlt, topImgPath } = brandTopImgInfo;
const { topImgAlt, topImgPath } = brandTopImgInfo || {};
// NBCU(patnrId: 21)인 경우 Top Banner 정보 사용
const isNBCU = selectedPatnrId === 21 || selectedPatnrId === "21";
// console.log("[FB-BANNER-COMP] ===== BANNER COMPONENT START =====");
// console.log("[FB-BANNER-COMP] selectedPatnrId:", selectedPatnrId, "(type:", typeof selectedPatnrId, ")");
// console.log("[FB-BANNER-COMP] panelPatnrId:", panelPatnrId, "(type:", typeof panelPatnrId, ")");
// console.log("[FB-BANNER-COMP] isNBCU:", isNBCU);
// console.log("[FB-BANNER-COMP] brandTopBannerInfo:", brandTopBannerInfo);
// Top Banner 정보에서 필요한 필드 추출
const {
banrImgUrl, // 배너 이미지 URL
banrImgNm, // 배너 이미지 이름
pupBanrImgUrl, // 팝업 배너 이미지 URL
pupBanrImgNm, // 팝업 배너 이미지 이름
pupBanrTtl, // 팝업 배너 타이틀
banrNm // 배너 이름
} = brandTopBannerInfo || {};
// 현재는 Top Banner 이미지를 사용하지 않고 기존 Top 이미지만 사용
// TODO: 향후 Top Banner 이미지를 사용하려면 아래 주석 처리된 부분을 활성화
const bannerImageSrc = topImgPath; // isNBCU ? banrImgUrl : topImgPath;
const bannerImageAlt = topImgAlt; // isNBCU ? banrImgNm || banrNm : topImgAlt;
// console.log("[FB-BANNER-COMP] Top Banner 이미지는 현재 비활성화됨");
// if (isNBCU && brandTopBannerInfo) {
// console.log("[FB-BANNER-COMP] NBCU Top Banner 데이터 수신 (사용하지 않음):");
// console.log("[FB-BANNER-COMP] - banrImgUrl:", banrImgUrl);
// console.log("[FB-BANNER-COMP] - pupBanrImgUrl:", pupBanrImgUrl);
// console.log("[FB-BANNER-COMP] - pupBanrTtl:", pupBanrTtl);
// }
// console.log("[FB-BANNER-COMP] Using original Top Image");
// console.log("[FB-BANNER-COMP] bannerImageSrc:", bannerImageSrc);
// console.log("[FB-BANNER-COMP] ===== BANNER COMPONENT END =====");
return (
<div className={css.container}>
<figure>
@@ -26,7 +62,25 @@ export default memo(function Banner({
/>
<figcaption>{patncNm}</figcaption>
</figure>
<CustomImage src={topImgPath} alt={topImgAlt} ariaLabel={topImgAlt} />
{bannerImageSrc && (
<CustomImage
src={bannerImageSrc}
alt={bannerImageAlt}
ariaLabel={bannerImageAlt}
/>
)}
{/* NBCU Top Banner Image */}
{isNBCU && brandTopBannerInfo?.banrImgUrl && (
<TopBannerImage
banrImgUrl={banrImgUrl}
banrImgNm={banrImgNm}
banrNm={banrNm}
pupBanrImgUrl={pupBanrImgUrl}
pupBanrImgNm={pupBanrImgNm}
spotlightId="nbcu-top-banner-image"
/>
)}
</div>
);
});

View File

@@ -3,6 +3,7 @@
.container {
position: fixed;
// position: relative; // changed from fixed to relative for absolute positioning of children
.flex(@justifyCenter: flex-start, @alignCenter: flex-end);
.size(@w: 100%, @h: 108px);
margin-bottom: 58px;

View File

@@ -19,6 +19,7 @@ const Container = SpotlightContainerDecorator(
const FeaturedBestSeller = ({
brandBestSellerInfo,
brandBestSellerTitle,
handleItemFocus,
order,
shelfOrder,
@@ -28,6 +29,12 @@ const FeaturedBestSeller = ({
}) => {
const [firstChk, setFirstChk] = useState(0);
// brandBestSellerTitle 우선 사용, 없으면 shelfTitle, 없으면 기본값
const displayTitle = brandBestSellerTitle || shelfTitle || $L(STRING_CONF.BEST_SELLER);
// console.log("[FeaturedBestSeller] brandBestSellerTitle:", brandBestSellerTitle);
// console.log("[FeaturedBestSeller] displayTitle:", displayTitle);
const _handleItemFocus = useCallback(() => {
if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder);
@@ -65,7 +72,7 @@ const FeaturedBestSeller = ({
spotlightId={spotlightId}
>
<SectionTitle
title={$L(STRING_CONF.BEST_SELLER)}
title={displayTitle}
data-title="best-seller"
label="best-seller Heading 1"
/>

View File

@@ -60,6 +60,10 @@ export default function FeaturedBestSellerList({
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
// console.log("[FeaturedBestSellerList] Mounted - selectedPatnrId:", selectedPatnrId);
// console.log("[FeaturedBestSellerList] brandBestSellerInfo:", brandBestSellerInfo);
// console.log("[FeaturedBestSellerList] Data count:", brandBestSellerInfo?.length || 0);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
@@ -155,6 +159,9 @@ export default function FeaturedBestSellerList({
lgCatNm,
euEnrgLblInfos,
} = brandBestSellerInfo[index];
// console.log("[FeaturedBestSellerList] renderItem - index:", index, "patnrId:", patnrId, "rankOrd:", rankOrd, "prdtNm:", prdtNm);
const rankText =
rankOrd === 1
? rankOrd + "st,"

View File

@@ -11,6 +11,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { types } from "../../actions/actionTypes";
import {
@@ -23,8 +24,10 @@ import {
getBrandLiveChannelInfo,
getBrandRecommendedShowInfo,
getBrandSeriesInfo,
getBrandShopByShow,
getBrandShowroom,
getBrandTSVInfo,
getBrandTopBanner,
} from "../../actions/brandActions";
import { changeAppStatus, setHidePopup } from "../../actions/commonActions";
import {
@@ -63,6 +66,7 @@ import LiveChannels from "./LiveChannels/LiveChannels";
import QuickMenu from "./QuickMenu/QuickMenu";
import RecommendedShows from "./RecommendedShows/RecommendedShows";
import Series from "./Series/Series";
import ShopByShow from "./ShopByShow/ShopByShow";
import Showroom from "./Showroom/Showroom";
import TodaysDeals from "./TodaysDeals/TodaysDeals";
import UpComing from "./UpComing/UpComing";
@@ -90,6 +94,7 @@ const TEMPLATE_CODE_CONF = {
SERIES: "BRD00107",
CATEGORY: "BRD00108",
SHOWROOM: "BRD00109",
NBCU: "BRD00110",
};
const DISPATCH_MAP = Object.freeze({
@@ -101,10 +106,22 @@ const DISPATCH_MAP = Object.freeze({
[TEMPLATE_CODE_CONF.SERIES]: getBrandSeriesInfo,
[TEMPLATE_CODE_CONF.CATEGORY]: getBrandCategoryInfo,
[TEMPLATE_CODE_CONF.SHOWROOM]: getBrandShowroom,
[TEMPLATE_CODE_CONF.NBCU]: getBrandShopByShow,
});
const TOP_MARGIN = 36;
// 🆕 [251211] NBCU(patnrId=21) 전용 통합 Container
// Banner + Category + BestSeller를 같은 Spotlight 계층 내에서 관리
const NbcuIntegratedContainer = SpotlightContainerDecorator(
{
spotlightDirection: 'vertical',
enterTo: 'last-focused',
preserveld: true,
},
'div'
);
const hasTemplateCodeWithValue = (array, value) =>
array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false;
@@ -233,6 +250,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
const brandBestSellerInfo = useSelector(
(state) => state.brand.brandBestSellerData.data.brandBestSellerInfo
);
const brandBestSellerTitle = useSelector(
(state) => state.brand.brandBestSellerData.data.brandBestSellerTitle
);
const brandRecommendedShowCategoryInfo = useSelector(
(state) =>
state.brand.brandRecommendedShowInfoData.data
@@ -263,6 +283,19 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
const brandShowroomInfo = useSelector(
(state) => state.brand.brandShowroomData.data.brandShowroomInfo
);
const brandShopByShowContsList = useSelector(
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsList
);
const brandShopByShowContsInfo = useSelector(
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsInfo
);
const brandTopBannerInfo = useSelector(
(state) => state.brand.brandTopBannerData.data.brandTopBannerInfo
);
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터
const brandShopByShowCategoryGroups = useSelector(
(state) => state.brand.brandShopByShowCategoryGroups
);
const [displayTopButton, setDisplayTopButton] = useState(false);
const [focusedContainerId, setFocusedContainerId] = useState(null);
@@ -293,6 +326,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
});
const renderedShelfCountRef = useRef(0);
// 🆕 [251210] patnrId=21 카테고리 그룹 조회 상태 추적
const fetchedCategoryGroupsRef = useRef(new Set());
const fromDetail = panelInfo?.from && panelInfo.from === "detail";
const fromGNB = panelInfo?.from && panelInfo.from === "gnb";
const fromUpcoming = panelInfo?.from && panelInfo.from === "upcoming";
@@ -412,9 +448,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
);
const renderPageItem = useCallback(() => {
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo length:", sortedBrandLayoutInfo.length);
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo items:", sortedBrandLayoutInfo.map(el => el.shptmBrndOptTpCd));
return (
<>
{sortedBrandLayoutInfo.map((el, idx) => {
// console.log("[FeaturedBrandsPanel] Processing template code:", el.shptmBrndOptTpCd);
switch (el.shptmBrndOptTpCd) {
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
return (
@@ -485,6 +524,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
}
case TEMPLATE_CODE_CONF.BEST_SELLER: {
// console.log("[FeaturedBrandsPanel] BEST_SELLER - patnrId:", selectedPatnrId);
// console.log("[FeaturedBrandsPanel] BEST_SELLER - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.BEST_SELLER));
// console.log("[FeaturedBrandsPanel] BEST_SELLER - shouldRender:", shouldRenderComponent(brandBestSellerInfo));
// console.log("[FeaturedBrandsPanel] BEST_SELLER - data:", brandBestSellerInfo);
return (
<React.Fragment key={el.shptmBrndOptTpCd}>
{hasTemplateCodeWithValue(
@@ -492,8 +535,11 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
TEMPLATE_CODE_CONF.BEST_SELLER
) &&
shouldRenderComponent(brandBestSellerInfo) && (
<>
{/* {console.log("[FeaturedBrandsPanel] Rendering FeaturedBestSeller for patnrId:", selectedPatnrId)} */}
<FeaturedBestSeller
brandBestSellerInfo={brandBestSellerInfo}
brandBestSellerTitle={brandBestSellerTitle}
handleItemFocus={handleItemFocus}
order={idx + 1}
shelfOrder={el.expsOrd}
@@ -501,6 +547,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER}
selectedPatnrId={selectedPatnrId}
/>
</>
)}
</React.Fragment>
);
@@ -650,12 +697,43 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
</React.Fragment>
);
}
case TEMPLATE_CODE_CONF.NBCU: {
// console.log("[FeaturedBrandsPanel] NBCU - patnrId:", selectedPatnrId);
// console.log("[FeaturedBrandsPanel] NBCU - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.NBCU));
// console.log("[FeaturedBrandsPanel] NBCU - shouldRender:", shouldRenderComponent(brandShopByShowContsList));
// console.log("[FeaturedBrandsPanel] NBCU - data:", brandShopByShowContsList);
return (
<React.Fragment key={el.shptmBrndOptTpCd}>
{hasTemplateCodeWithValue(
sortedBrandLayoutInfo,
TEMPLATE_CODE_CONF.NBCU
) &&
shouldRenderComponent(brandShopByShowContsList) && (
<>
{/* {console.log("[FeaturedBrandsPanel] Rendering ShopByShow for patnrId:", selectedPatnrId)} */}
<ShopByShow
brandShopByShowContsList={brandShopByShowContsList}
brandShopByShowContsInfo={brandShopByShowContsInfo}
handleItemFocus={handleItemFocus}
order={idx + 1}
shelfOrder={el.expsOrd}
shelfTitle={el.shptmBrndOptTpNm}
spotlightId={TEMPLATE_CODE_CONF.NBCU}
selectedPatnrId={selectedPatnrId}
/>
</>
)}
</React.Fragment>
);
}
}
})}
</>
);
}, [
brandBestSellerInfo,
brandBestSellerTitle,
brandCategoryInfo,
brandCategoryProductInfo,
brandChanInfo,
@@ -668,6 +746,8 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
brandSeriesGroupInfo,
brandSeriesInfo,
brandShowroomInfo,
brandShopByShowContsList,
brandShopByShowContsInfo,
brandTsvInfo,
fromGNB,
fromQuickMenu,
@@ -711,6 +791,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: layout information fetching due to partner id change
useEffect(() => {
if (!fromDetail) {
// console.log("[FeaturedBrandsPanel] Layout Info Effect - patnrId:", panelInfo?.patnrId);
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
setIsInitialFocusOccurred(false);
@@ -720,30 +801,98 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: set selectedPatnrId and selectedPatncNm
useEffect(() => {
if (brandInfo) {
const patnrId = panelInfo?.patnrId;
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId).patncNm;
if (brandInfo && panelInfo?.patnrId) {
const patnrId = panelInfo.patnrId;
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId)?.patncNm;
if (patncNm) {
setSelectedPatncNm(patncNm);
if (!fromDetail) setSelectedPatnrId(patnrId);
}
}, [brandInfo, panelInfo?.patnrId]);
// Detail에서 돌아와도 patnrId가 비어 있으면 다시 설정해 API 호출이 정상 동작하도록 보완
if (!fromDetail || !selectedPatnrId) {
setSelectedPatnrId(patnrId);
}
}
}, [brandInfo, panelInfo?.patnrId, selectedPatnrId, fromDetail]);
// effect: data fetching based on brandLayoutInfo and selectedPatnrId
useEffect(() => {
// console.log("[FB-PANEL-DATA-FETCH] Effect triggered");
// console.log("[FB-PANEL-DATA-FETCH] sortedBrandLayoutInfo:", sortedBrandLayoutInfo);
// console.log("[FB-PANEL-DATA-FETCH] selectedPatnrId:", selectedPatnrId);
// 🆕 [251210] patnrId 변경 시 조회 상태 초기화
if (selectedPatnrId) {
const patnrIdString = String(selectedPatnrId);
// 이전 patnrId와 다르면 ref 초기화
const currentFetchKeys = Array.from(fetchedCategoryGroupsRef.current).filter(key => key.startsWith(patnrIdString));
if (currentFetchKeys.length === 0) {
// console.log("[FB-PANEL-DATA-FETCH] patnrId changed, clearing category group fetch status");
// 다른 patnrId로 전환 시 ref 초기화
fetchedCategoryGroupsRef.current.clear();
}
}
if (sortedBrandLayoutInfo && selectedPatnrId) {
// console.log("[FB-PANEL-DATA-FETCH] Fetching data - patnrId:", selectedPatnrId);
Object.entries(DISPATCH_MAP) //
.forEach(([templateCode, action]) => {
if (hasTemplateCodeWithValue(sortedBrandLayoutInfo, templateCode)) {
// Detail 복귀 시 ShopByShow 데이터가 이미 있으면 재호출을 건너뛰어 선택 상태가 초기화되는 것을 방지
if (
templateCode === TEMPLATE_CODE_CONF.NBCU &&
fromDetail &&
shouldRenderComponent(brandShopByShowContsList)
) {
// console.log("[FB-PANEL-DATA-FETCH] Skip re-fetch ShopByShow on return from detail");
return;
}
// console.log("[FB-PANEL-DATA-FETCH] Dispatching for template:", templateCode, "patnrId:", selectedPatnrId);
dispatch(action({ patnrId: selectedPatnrId }));
}
});
// NBCU(patnrId: 21)인 경우 Top Banner API 호출
if (selectedPatnrId === 21 || selectedPatnrId === "21") {
// console.log("[FB-PANEL-TOP-BANNER] NBCU(patnrId=21) detected - calling Top Banner API");
// console.log("[FB-PANEL-TOP-BANNER] selectedPatnrId:", selectedPatnrId, "(type:", typeof selectedPatnrId, ")");
// console.log("[FB-PANEL-TOP-BANNER] Before API call - brandTopBannerInfo:", brandTopBannerInfo);
dispatch(getBrandTopBanner({ patnrId: selectedPatnrId }));
}
resetStates();
}
}, [sortedBrandLayoutInfo, selectedPatnrId]);
// 🆕 [251210] patnrId=21인 경우 모든 카테고리 그룹 데이터 미리 조회
useEffect(() => {
if (selectedPatnrId === 21 || selectedPatnrId === "21") {
// console.log("[FB-PANEL-CATEGORY-GROUPS] patnrId=21 detected - fetching all category group data");
// console.log("[FB-PANEL-CATEGORY-GROUPS] brandShopByShowContsList:", brandShopByShowContsList);
// 각 카테고리(contsId)별 그룹 데이터 조회
if (brandShopByShowContsList && brandShopByShowContsList.length > 0) {
brandShopByShowContsList.forEach((conts) => {
const fetchKey = `${selectedPatnrId}-${conts.contsId}`;
// useRef로 이미 조회된 contsId 추적 (무한루프 방지)
if (!fetchedCategoryGroupsRef.current.has(fetchKey)) {
// console.log("[FB-PANEL-CATEGORY-GROUPS] Fetching category group for contsId:", conts.contsId);
fetchedCategoryGroupsRef.current.add(fetchKey); // 조회 상태 기록
dispatch(getBrandShopByShow({
patnrId: selectedPatnrId,
contsId: conts.contsId
}));
} else {
// console.log("[FB-PANEL-CATEGORY-GROUPS] Category group already fetched for contsId:", conts.contsId);
}
});
}
}
}, [selectedPatnrId, brandShopByShowContsList, dispatch]); // brandShopByShowCategoryGroups 제거
useEffect(() => {
if (selectedCatCd) {
dispatch(
@@ -905,6 +1054,18 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
}
}, [isLogGNBSent, isInitialFocusOccurred, selectedPatnrId, selectedPatncNm]);
// effect: partners log for NBCU (patnrId=21)
useEffect(() => {
if (selectedPatnrId === 21 && selectedPatncNm) {
dispatch(
sendLogPartners({
patncNm: selectedPatncNm,
patnrId: selectedPatnrId,
})
);
}
}, [selectedPatnrId, selectedPatncNm]);
// effect: unmount
useEffect(() => {
return () => {
@@ -959,11 +1120,16 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
/>
)}
{brandInfo && brandTopImgInfo && (
{/* 🆕 [251211] patnrId=21일 때: Banner + Category + BestSeller를 통합 Container로 관리 */}
{(selectedPatnrId === 21 || selectedPatnrId === "21") ? (
<NbcuIntegratedContainer className={css.nbcuIntegratedContainer}>
{brandInfo && (brandTopImgInfo || brandTopBannerInfo) && (
<Banner
brandInfo={brandInfo}
brandTopImgInfo={brandTopImgInfo}
brandTopBannerInfo={brandTopBannerInfo}
panelPatnrId={panelInfo?.patnrId}
selectedPatnrId={selectedPatnrId}
/>
)}
@@ -975,6 +1141,29 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
{renderPageItem()}
</div>
)}
</NbcuIntegratedContainer>
) : (
<>
{brandInfo && (brandTopImgInfo || (selectedPatnrId === 21 && brandTopBannerInfo)) && (
<Banner
brandInfo={brandInfo}
brandTopImgInfo={brandTopImgInfo}
brandTopBannerInfo={brandTopBannerInfo}
panelPatnrId={panelInfo?.patnrId}
selectedPatnrId={selectedPatnrId}
/>
)}
{sortedBrandLayoutInfo && (
<div
className={css.orderableFlexContainer}
ref={orderableFlexContainerRef}
>
{renderPageItem()}
</div>
)}
</>
)}
{displayTopButton && (
<TButton

View File

@@ -49,10 +49,13 @@ const QuickMenuItem = ({
}, [handleStopScrolling, itemIndex]);
const handleClick = useCallback(() => {
console.log("[QuickMenuItem] Click - patnrId:", patnrId, "currentPatnrId:", selectedPatnrId ?? panelInfo?.patnrId);
if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) {
console.log("[QuickMenuItem] Already selected, returning");
return;
}
console.log("[QuickMenuItem] Switching to patnrId:", patnrId);
const from = "menu";
const name = panel_names.FEATURED_BRANDS_PANEL;

View File

@@ -0,0 +1,79 @@
<div style={{width: '100%', height: '100%', paddingTop: 63, paddingLeft: 60, flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{width: 1800, height: 42, justifyContent: 'flex-start', alignItems: 'center', gap: 12, display: 'inline-flex'}}>
<div style={{width: 6, height: 36, background: '#C70850'}} />
<div style={{textAlign: 'center', color: 'black', fontSize: 42, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 42, wordWrap: 'break-word'}}>Chef Gadget's</div>
</div>
<div style={{alignSelf: 'stretch', paddingTop: 20, paddingBottom: 20, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 19, display: 'inline-flex'}}>
<div style={{width: 665, alignSelf: 'stretch', padding: 18, background: 'white', overflow: 'hidden', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'flex'}}>
<img style={{flex: '1 1 0', alignSelf: 'stretch', padding: 18, background: 'linear-gradient(180deg, #EC79B8 0%, #CD4F93 100%)', border: '1px rgba(218, 218, 218, 0.54) solid'}} src="https://placehold.co/629x402" alt="Chef Gadget's featured product" />
</div>
<div style={{width: 323, padding: 18, background: 'white', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<div style={{alignSelf: 'stretch', height: 287, position: 'relative'}}>
<img style={{width: 287, height: 287, left: 0, top: 0, position: 'absolute'}} src="https://placehold.co/287x287" alt="Bravo's Top Chef product" />
<div style={{width: 71, height: 72, left: 216, top: 215, position: 'absolute', background: '#EFEEF0'}} />
</div>
<div style={{alignSelf: 'stretch', height: 82, paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Bravo's Top Chef</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
<div style={{width: 323, padding: 18, background: 'white', boxShadow: '0px 0px 30px rgba(0, 0, 0, 0.45)', borderRadius: 12, outline: '2px #C70850 solid', outlineOffset: '-2px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<img style={{alignSelf: 'stretch', height: 287}} src="https://placehold.co/287x287" alt="Top Chef Knife Tote Bag" />
<div style={{alignSelf: 'stretch', height: 82, paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Top Chef Knife Tote Bag</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
<div style={{width: 323, padding: 18, background: 'white', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<div style={{alignSelf: 'stretch', height: 287, position: 'relative'}}>
<img style={{width: 287, height: 287, left: 0, top: 0, position: 'absolute'}} src="https://placehold.co/287x287" alt="Salt, Maldon Traditional product on sale 17%" />
<div style={{width: 60, height: 60, left: 219, top: 219, position: 'absolute', background: '#C70850', borderRadius: 1000, justifyContent: 'center', alignItems: 'center', display: 'inline-flex'}}>
<div style={{justifyContent: 'center', display: 'flex', flexDirection: 'column', color: 'white', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 26, wordWrap: 'break-word'}}>17%</div>
</div>
</div>
<div style={{alignSelf: 'stretch', height: 82, paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Salt, Maldon Traditional</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
<div style={{width: 323, padding: 18, background: 'white', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<img style={{alignSelf: 'stretch', height: 287}} src="https://placehold.co/287x287" alt="Fish Grill Pan" />
<div style={{alignSelf: 'stretch', height: 82, paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Fish Grill Pan</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
<div style={{width: 323, padding: 18, background: 'white', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<img style={{alignSelf: 'stretch', height: 287}} src="https://placehold.co/287x287" alt="Product" />
<div style={{alignSelf: 'stretch', paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Productl Nameytg Product Name Producthlyg()...</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
<div style={{width: 323, padding: 18, background: 'white', borderRadius: 12, outline: '1px #DADADA solid', outlineOffset: '-1px', flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'flex-start', display: 'inline-flex'}}>
<img style={{alignSelf: 'stretch', height: 287}} src="https://placehold.co/287x287" alt="Product" />
<div style={{alignSelf: 'stretch', paddingTop: 8, paddingBottom: 10, justifyContent: 'flex-start', alignItems: 'flex-start', gap: 10, display: 'inline-flex'}}>
<div style={{flex: '1 1 0', color: 'black', fontSize: 24, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 32, wordWrap: 'break-word'}}>Productl Nameytg Product Name Producthlyg()...</div>
</div>
<div style={{paddingBottom: 3, justifyContent: 'center', alignItems: 'center', gap: 4, display: 'inline-flex'}}>
<div style={{color: '#C70850', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>$32.98</div>
<div style={{color: '#808080', fontSize: 18, fontFamily: 'LG Smart UI', fontWeight: '400', textDecoration: 'line-through', lineHeight: 18, wordWrap: 'break-word'}}>$150.00</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,168 @@
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getBrandShopByShow } from "../../../actions/brandActions";
import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import useScrollTo from "../../../hooks/useScrollTo";
import SectionTitle from "../../../components/SectionTitle/SectionTitle";
import { $L } from "../../../utils/helperMethods";
import { panel_names } from "../../../utils/Config";
import css from "./ShopByShow.module.less";
import ShopByShowNav from "./ShopByShowList/ShopByShowNav/ShopByShowNav";
import ShopByShowContents from "./ShopByShowList/ShopByShowContents/ShopByShowContents";
import { updatePanel } from "../../../actions/panelActions";
const STRING_CONF = {
SHOP_BY_SHOW: "SHOP BY SHOW",
};
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"div"
);
const ShopByShow = ({
brandShopByShowContsList,
brandShopByShowContsInfo,
handleItemFocus,
order,
shelfOrder,
spotlightId,
selectedPatnrId,
shelfTitle,
}) => {
const [firstChk, setFirstChk] = useState(0);
const [selectedContsId, setSelectedContsId] = useState(null);
const dispatch = useDispatch();
const { getScrollTo, scrollLeft } = useScrollTo({ skipAutoScrollTop: true });
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const scrollLeftJob = useRef(new Job((func) => func(), 0));
const brandShopByShowClctInfos = brandShopByShowContsInfo?.brandShopByShowClctInfos || [];
// DetailPanel 복귀 시 panelInfo에 저장된 contsId로 네비게이션 선택을 복구
useEffect(() => {
if (panelInfo?.contsId && panelInfo.contsId !== selectedContsId) {
setSelectedContsId(panelInfo.contsId);
}
}, [panelInfo?.contsId, selectedContsId]);
const handleContsIdChange = useCallback(
(contsId) => {
// patnrId가 없으면 Detail 복귀 직후라 정상 호출 불가
if (!selectedPatnrId) return;
setSelectedContsId(contsId);
// 현재 선택된 contsId를 패널 상태에 저장해 복귀 시 복원
dispatch(
updatePanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: {
...(panelInfo || {}),
contsId,
},
})
);
// 'ALL' 버튼 클릭 시 (contsId === null) 첫 번째 contents 로드
const targetContsId = contsId || brandShopByShowContsList?.[0]?.contsId;
if (targetContsId) {
dispatch(getBrandShopByShow({ patnrId: selectedPatnrId, contsId: targetContsId }));
}
},
[selectedPatnrId, brandShopByShowContsList, dispatch]
);
useEffect(() => {
if (panelInfo?.section !== "shop-by-show" || !panelInfo?.x) {
return;
}
const scrollLeftJobValue = scrollLeftJob.current;
const { x } = panelInfo;
scrollLeftJobValue.start(() => scrollLeft({ x }));
return () => scrollLeftJobValue.stop();
}, [panelInfo, scrollLeft]);
useEffect(() => {
scrollLeft();
}, [scrollLeft, selectedPatnrId]);
const _handleItemFocus = useCallback(() => {
if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder);
const c = Spotlight.getCurrent();
if (firstChk === 0) {
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
if (cAriaLabel) {
cAriaLabel = "shop-by-show, Heading1," + cAriaLabel;
c.setAttribute("aria-label", cAriaLabel);
}
}
setFirstChk(1);
} else if (firstChk === 1) {
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace(
"shop-by-show, Heading1,",
""
);
c.setAttribute("aria-label", newcAriaLabel);
}
}
} else {
return;
}
}, [handleItemFocus, firstChk]);
return (
<Container
className={css.container}
data-shelf-order={order}
data-wheel-point
spotlightId={spotlightId}
>
<SectionTitle
title={$L(STRING_CONF.SHOP_BY_SHOW)}
data-title="shop-by-show"
label="shop-by-show Heading 1"
/>
<ShopByShowNav
brandShopByShowContsList={brandShopByShowContsList}
brandShopByShowContsInfo={brandShopByShowContsInfo}
handleItemFocus={_handleItemFocus}
onContsIdChange={handleContsIdChange}
selectedContsId={selectedContsId}
/>
{brandShopByShowClctInfos.map((collection, collIdx) => (
<ShopByShowContents
key={`${spotlightId}-${collIdx}`}
brandProductInfos={collection.brandProductInfos}
contentsIndex={collIdx}
handleItemFocus={_handleItemFocus}
clctNm={collection.clctNm}
clctImgUrl={collection.clctImgUrl}
contsId={brandShopByShowContsInfo?.contsId || selectedContsId}
patnrId={selectedPatnrId}
selectedPatnrId={selectedPatnrId}
shelfOrder={shelfOrder}
shelfTitle={shelfTitle}
spotlightId={spotlightId}
/>
))}
</Container>
);
};
export default memo(ShopByShow);

View File

@@ -0,0 +1,16 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.container {
width: 100%;
margin-bottom: 58px;
> h2 {
margin-bottom: 24px;
padding-left: 60px;
}
> nav {
margin-bottom: 30px;
}
}

View File

@@ -0,0 +1,65 @@
import React, { memo, useCallback } from "react";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import css from "./ShopByShowContents.module.less";
import ShopByShowImageCard from "./ShopByShowImageCard/ShopByShowImageCard";
import ShopByShowProductList from "./ShopByShowProductList/ShopByShowProductList";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: null },
"div"
);
export default memo(function ShopByShowContents({
brandProductInfos,
contentsIndex,
handleItemFocus,
clctNm,
clctImgUrl,
contsId,
patnrId,
selectedPatnrId,
shelfOrder,
shelfTitle,
spotlightId,
}) {
const handleFocus = useCallback(() => {
if (handleItemFocus) handleItemFocus();
}, [handleItemFocus]);
if (!brandProductInfos || brandProductInfos.length === 0) {
return null;
}
return (
<Container
className={css.container}
data-wheel-point
spotlightId={`${spotlightId}-${contentsIndex}`}
>
<h3 data-collection-subtitle={clctNm}>{clctNm}</h3>
<div>
<ShopByShowImageCard
imageAlt={clctNm}
imageSource={clctImgUrl}
clctNm={clctNm}
spotlightDisabled
ariaLabel={clctNm}
/>
<ShopByShowProductList
brandProductInfos={brandProductInfos}
contentsIndex={contentsIndex}
handleFocus={handleFocus}
contsId={contsId}
patnrId={patnrId}
selectedPatnrId={selectedPatnrId}
clctNm={clctNm}
shelfOrder={shelfOrder}
shelfTitle={shelfTitle}
spotlightId={spotlightId}
/>
</div>
</Container>
);
});

View File

@@ -0,0 +1,32 @@
@import "../../../../../style/CommonStyle.module.less";
@import "../../../../../style/utils.module.less";
.container {
padding-left: 60px;
> h3 {
position: relative;
.font(@fontFamily: @arialFontBold, @fontSize: 36px);
color: @COLOR_GRAY08;
}
> div:nth-child(2) {
.flex(@justifyCenter: flex-start);
.size(@w: 100%, @h: auto);
}
&.listContainer {
margin-bottom: 12px;
}
&.gridContainer {
> h3 {
position: relative;
margin-bottom: 22px;
}
}
}
.container:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,22 @@
import React, { memo } from "react";
import Spottable from "@enact/spotlight/Spottable";
import CustomImage from "../../../../../../components/CustomImage/CustomImage";
import css from "./ShopByShowImageCard.module.less";
const SpottableComponent = Spottable("figure");
export default memo(function ShopByShowImageCard({
imageAlt,
imageSource,
ariaLabel,
...rest
}) {
delete rest.clctNm;
return (
<SpottableComponent className={css.card} aria-label={ariaLabel} {...rest}>
<CustomImage src={imageSource} alt={imageAlt} />
</SpottableComponent>
);
});

View File

@@ -0,0 +1,22 @@
@import "../../../../../../style/CommonStyle.module.less";
@import "../../../../../../style/utils.module.less";
.card {
position: relative;
.size(@w: 663px, @h:438px);
padding: 18px;
background-color: @COLOR_WHITE;
border: solid 1px @COLOR_GRAY02;
border-radius: 12px;
img {
.size(@w: 627px, @h:402px);
object-fit: cover;
}
&:focus {
&::after {
.focused(@boxShadow: 22px, @borderRadius: 12px);
}
}
}

View File

@@ -0,0 +1,173 @@
import React, { useCallback, useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight";
import { pushPanel, updatePanel } from "../../../../../../actions/panelActions";
import TItemCardNew, {
removeDotAndColon,
} from "../../../../../../components/TItemCard/TItemCard.new";
import TVirtualGridList from "../../../../../../components/TVirtualGridList/TVirtualGridList";
import useScrollTo from "../../../../../../hooks/useScrollTo";
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from "../../../../../../utils/Config";
import { getTranslate3dValueByDirection } from "../../../../../../utils/helperMethods";
import css from "./ShopByShowProductList.module.less";
export default function ShopByShowProductList({
brandProductInfos,
contentsIndex,
handleFocus,
contsId,
patnrId,
selectedPatnrId,
clctNm,
shelfOrder,
shelfTitle,
spotlightId,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const dispatch = useDispatch();
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const scrollLeftJob = useRef(new Job((func) => func(), 0));
useEffect(() => {
if (
panelInfo?.section !== "shop-by-show" ||
!panelInfo?.x ||
panelInfo?.exprOrd !== contentsIndex + 1
) {
return;
}
const scrollLeftJobValue = scrollLeftJob.current;
const { x } = panelInfo;
scrollLeftJobValue.start(() => scrollLeft({ x }));
return () => scrollLeftJobValue.stop();
}, [panelInfo, scrollLeft, contentsIndex]);
useEffect(() => {
scrollLeft();
}, [scrollLeft, selectedPatnrId]);
const handleClick = useCallback(
(prdtId, productPatnrId) => (e) => {
const effectivePatnrId = productPatnrId || patnrId;
const tItemCard = e.currentTarget;
const lastFocusedTarget = Spotlight.getCurrent();
const lastFocusedTargetId =
lastFocusedTarget?.getAttribute("data-spotlight-id");
const exprOrd = parseInt(
lastFocusedTarget?.getAttribute("data-exposure-order")
);
const xContainer = tItemCard?.parentNode?.parentNode;
if (exprOrd && lastFocusedTargetId && xContainer) {
const section = "shop-by-show";
const x = getTranslate3dValueByDirection(xContainer);
dispatch(
updatePanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: {
exprOrd,
lastFocusedTargetId,
patnrId: effectivePatnrId,
section,
contsId,
x,
},
})
);
}
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId: effectivePatnrId, prdtId, contsId },
})
);
},
[dispatch, patnrId, contsId]
);
const renderItem = useCallback(
({ index, ...rest }) => {
const product = brandProductInfos[index];
const {
prdtImgUrl,
prdtOfferId,
patnrId: productPartnerId = "21",
prdtNm,
prdtId,
priceInfo,
patncNm,
brndNm,
lgCatNm,
euEnrgLblInfos,
} = product;
return (
<TItemCardNew
catNm={lgCatNm}
contextName={LOG_CONTEXT_NAME.FEATURED_BRANDS}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}
patnerName={patncNm || "Peacock | Shop The Moment"}
brandName={brndNm}
shelfId={spotlightId}
shelfLocation={shelfOrder}
shelfTitle={shelfTitle}
imageAlt={prdtNm}
imageSource={prdtImgUrl}
onClick={handleClick(prdtId, productPartnerId)}
onFocus={handleFocus}
offerInfo={prdtOfferId}
priceInfo={priceInfo}
productId={prdtId}
productName={prdtNm}
spotlightId={"shop-by-show-list-spotlightId-" + removeDotAndColon(prdtId)}
data-exposure-order={contentsIndex + 1}
label={index + 1 + " of " + brandProductInfos.length}
lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos}
{...rest}
/>
);
},
[
brandProductInfos,
contentsIndex,
handleClick,
handleFocus,
]
);
return (
<div className={css.container}>
{brandProductInfos && (
<TVirtualGridList
cbScrollTo={getScrollTo}
className={css.tVirtualGridList}
dataSize={brandProductInfos.length}
direction="horizontal"
itemHeight={438}
itemWidth={324}
spacing={18}
renderItem={renderItem}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,21 @@
@import "../../../../../../style/utils.module.less";
.container {
.flex();
overflow: hidden;
.size(@w: calc(100% - 663px), @h: 482px);
padding: 0 18px;
// tVirtualGridListContainer
> div:nth-child(1) {
.size(@w: 100%, @h: inherit);
> div:nth-child(1) {
padding: 22px 0;
}
> div:nth-child(3) {
right: -18px;
}
}
}

View File

@@ -0,0 +1,86 @@
import React, { memo, useCallback } from "react";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import TButton, { TYPES } from "../../../../../components/TButton/TButton";
import TScroller from "../../../../../components/TScroller/TScroller";
import useScrollTo from "../../../../../hooks/useScrollTo";
import { $L } from "../../../../../utils/helperMethods";
import css from "./ShopByShowNav.module.less";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"nav"
);
const STRING_CONF = {
ALL: "ALL",
};
export default memo(function ShopByShowNav({
brandShopByShowContsList,
brandShopByShowContsInfo,
handleItemFocus,
onContsIdChange,
selectedContsId,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const activeContsId = selectedContsId ?? brandShopByShowContsInfo?.contsId;
const handleClick = useCallback(
(contsId) => () => {
onContsIdChange(contsId);
},
[onContsIdChange]
);
const handleFocus = useCallback(() => {
if (handleItemFocus) {
handleItemFocus();
}
}, [handleItemFocus]);
const selectedText = !activeContsId ? "Selected " : "";
const allLabeltext = selectedText + "ALL 1 of " + (brandShopByShowContsList.length + 1);
return (
<Container className={css.nav} id="shop-by-show-nav-id" spotlightId="shop-by-show-nav-id">
<TScroller cbScrollTo={getScrollTo} direction="horizontal" noScrollByWheel>
<ul>
{/* 'ALL' 버튼 - 디자인에 없어서 주석 처리 */}
{/* <li>
<TButton
className={!selectedContsId && css.selected}
onClick={handleClick(null)}
onFocus={handleFocus}
selected={!selectedContsId}
type={TYPES.oneDepthCategory}
ariaLabel={allLabeltext}
>
{$L(STRING_CONF.ALL)}
</TButton>
</li> */}
{brandShopByShowContsList &&
brandShopByShowContsList.map(({ contsId, contsNm }, index) => (
<li key={"shop-by-show-conts-" + index}>
<TButton
className={activeContsId === contsId && css.selected}
onClick={handleClick(contsId)}
onFocus={handleFocus}
selected={activeContsId === contsId}
type={TYPES.oneDepthCategory}
ariaLabel={
activeContsId === contsId
? "Selected " + contsNm + " " + (index * 1 + 1) + " of " + brandShopByShowContsList.length
: "" + contsNm + " " + (index * 1 + 1) + " of " + brandShopByShowContsList.length
}
>
{contsNm}
</TButton>
</li>
))}
</ul>
</TScroller>
</Container>
);
});

View File

@@ -0,0 +1,55 @@
@import "../../../../../style/CommonStyle.module.less";
@import "../../../../../style/utils.module.less";
.nav {
position: relative;
.size(@w: 100%, @h: 162px);
margin-bottom: 30px;
padding-right: 1px;
z-index: 2;
> div:nth-child(1) {
.size(@w: inherit, @h: inherit);
}
&::before {
position: absolute;
top: 0;
left: 0;
.size(@w: 100%, @h: 144px);
background-color: #ddd;
content: "";
}
ul {
display: flex;
align-items: center;
height: inherit;
padding-left: 60px;
border-bottom: 18px solid transparent;
li {
flex: none;
margin-right: 12px;
> div {
position: relative;
&.selected {
&::before {
position: absolute;
bottom: -62px;
left: 50%;
transform: translateX(-50%);
.size(@w: 0, @h: 0);
border-top: 18px solid #ddd;
border-right: 18px solid transparent;
border-bottom: 18px solid transparent;
border-left: 18px solid transparent;
content: "";
}
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
import React, {
memo,
useCallback,
useState,
} from 'react';
import { useDispatch } from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import { setShowPopup } from '../../../actions/commonActions';
import CustomImage from '../../../components/CustomImage/CustomImage';
import css from './TopBannerImage.module.less';
const SpottableDiv = Spottable("div");
const TopBannerImage = memo(({ banrImgUrl, banrImgNm, banrNm, pupBanrImgUrl, pupBanrImgNm, spotlightId }) => {
// console.log("[TOP-BANNER-IMG] Rendering with URL:", banrImgUrl);
// console.log("[TOP-BANNER-IMG] spotlightId:", spotlightId);
const dispatch = useDispatch();
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
const handleClick = useCallback(() => {
// console.log("[TOP-BANNER-IMG] Clicked - Opening popup");
if (pupBanrImgUrl) {
// console.log("[TOP-BANNER-IMG] Dispatching topBannerImagePopup");
dispatch(setShowPopup({
activePopup: 'topBannerImagePopup',
data: {
pupBanrImgUrl,
pupBanrImgNm: pupBanrImgNm || banrImgNm || banrNm
}
}));
}
}, [dispatch, pupBanrImgUrl, pupBanrImgNm, banrImgNm, banrNm]);
const handleImageLoad = useCallback((e) => {
const img = e.target;
// console.log("[TOP-BANNER-IMG] Image loaded - dimensions:", img.naturalWidth, "x", img.naturalHeight);
// 원본 이미지 크기
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
setImageDimensions({
width: naturalWidth,
height: naturalHeight
});
}, []);
if (!banrImgUrl) {
// console.log("[TOP-BANNER-IMG] No image URL provided");
return null;
}
return (
<SpottableDiv
className={css.topBannerContainer}
spotlightId={spotlightId}
onClick={handleClick}
>
<img
src={banrImgUrl}
alt={banrImgNm || banrNm || "NBCU Top Banner"}
className={css.topBannerImage}
onLoad={handleImageLoad}
style={{
width: imageDimensions.width || 'auto',
height: imageDimensions.height || 'auto'
}}
/>
</SpottableDiv>
);
});
export default TopBannerImage;

View File

@@ -0,0 +1,33 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.topBannerContainer {
position: absolute;
right: 60px;
top: 48px;
// padding: 15px;
background-color: transparent;
cursor: pointer;
// Spotlight 포커스 스타일 (TItemCard 방식)
&:focus {
&::after {
.focused(@boxShadow: 10px, @borderRadius: 4px);
}
}
// 마우스 호버 스타일
&:hover {
outline: 2px solid #fff;
outline-offset: 2px;
background-color: rgba(255, 255, 255, 0.1);
}
}
.topBannerImage {
display: block;
pointer-events: none;
// 크기는 JavaScript에서 동적으로 설정
border-radius: 4px;
width:100%;
}

View File

@@ -0,0 +1,13 @@
<div style={{width: '100%', height: '100%', background: 'white', overflow: 'hidden', borderRadius: 12, flexDirection: 'column', justifyContent: 'center', alignItems: 'center', display: 'inline-flex'}}>
<div style={{alignSelf: 'stretch', height: 119, padding: 30, background: '#E7EBEF', justifyContent: 'flex-start', alignItems: 'center', gap: 15, display: 'inline-flex'}}>
<div style={{textAlign: 'center', color: 'black', fontSize: 42, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 42, wordWrap: 'break-word'}}>Wells Fargo Active Cash Credit Card</div>
</div>
<div style={{alignSelf: 'stretch', justifyContent: 'center', alignItems: 'center', display: 'inline-flex'}}>
<img style={{flex: '1 1 0', height: 555.51}} src="https://placehold.co/1060x556" />
</div>
<div style={{alignSelf: 'stretch', paddingLeft: 60, paddingRight: 60, paddingTop: 30, paddingBottom: 30, justifyContent: 'center', alignItems: 'center', gap: 10, display: 'inline-flex'}}>
<div style={{width: 300, height: 78, background: '#7A808D', borderRadius: 12, justifyContent: 'center', alignItems: 'center', gap: 10, display: 'flex'}}>
<div style={{textAlign: 'center', color: 'white', fontSize: 30, fontFamily: 'LG Smart UI', fontWeight: '700', lineHeight: 30, wordWrap: 'break-word'}}>CLOSE</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,75 @@
import React, { memo, useCallback, useState, useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { setHidePopup } from "../../../actions/commonActions";
import css from "./TopBannerPopup.module.less";
const TopBannerPopup = memo(({ title, imageUrl, imageAlt, onImageLoad }) => {
const dispatch = useDispatch();
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
const closeButtonRef = useRef(null);
const handleImageLoad = useCallback((e) => {
const img = e.target;
console.log("[TOP-BANNER-POPUP] Image loaded - dimensions:", img.naturalWidth, "x", img.naturalHeight);
const dimensions = {
width: img.naturalWidth,
height: img.naturalHeight
};
setImageDimensions(dimensions);
// 부모 컴포넌트에 크기 전달
if (onImageLoad) {
onImageLoad(dimensions);
}
}, [onImageLoad]);
const handleClose = useCallback(() => {
console.log("[TOP-BANNER-POPUP] Closing popup");
dispatch(setHidePopup());
}, [dispatch]);
// 팝업이 마운트되었을 때 Close 버튼에 포커스
useEffect(() => {
console.log("[TOP-BANNER-POPUP] Component mounted - focusing close button");
if (closeButtonRef.current) {
closeButtonRef.current.focus();
}
}, []);
return (
<div className={css.container}>
{/* Title Section */}
<div className={css.titleSection}>
<div className={css.titleText}>{title}</div>
</div>
{/* Image Section */}
<div className={css.imageSection}>
<img
src={imageUrl}
alt={imageAlt || "Popup Banner"}
className={css.popupImage}
onLoad={handleImageLoad}
/>
</div>
{/* Button Section */}
<div className={css.buttonSection}>
<button
ref={closeButtonRef}
className={css.closeButton}
onClick={handleClose}
aria-label="Close popup"
>
CLOSE
</button>
</div>
</div>
);
});
TopBannerPopup.displayName = "TopBannerPopup";
export default TopBannerPopup;

View File

@@ -0,0 +1,102 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.container {
width: 100%;
height: 100%;
background: white;
overflow: hidden;
border-radius: 12px;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
display: flex;
}
// 헤더: 높이 110px (상단 마진 30 + 내용 50 + 하단 마진 30)
.titleSection {
flex: 0 0 110px;
background: #E7EBEF;
padding-top: 30px;
padding-bottom: 30px;
padding-left: 30px;
padding-right: 30px;
justify-content: flex-start;
align-items: center;
gap: 15px;
display: flex;
}
.titleText {
flex: 1;
color: black;
font-size: 42px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
word-break: break-word;
}
// 이미지: 높이 556px
.imageSection {
flex: 0 0 556px;
justify-content: center;
align-items: center;
display: flex;
background: white;
overflow: hidden;
}
.popupImage {
width: 100%;
height: 100%;
object-fit: contain;
}
// 푸터: 높이 138px (상단 마진 30 + 버튼 78 + 하단 마진 30)
.buttonSection {
flex: 0 0 138px;
padding-left: 60px;
padding-right: 60px;
padding-top: 30px;
padding-bottom: 30px;
justify-content: center;
align-items: center;
gap: 10px;
display: flex;
}
.closeButton {
width: 300px;
height: 78px;
background: #7A808D;
border-radius: 12px;
border: none;
justify-content: center;
align-items: center;
gap: 10px;
display: flex;
cursor: pointer;
transition: background-color 0.2s ease;
color: white;
font-size: 30px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 30px;
text-align: center;
&:hover {
background: @PRIMARY_COLOR_RED;
}
&:focus {
background: @PRIMARY_COLOR_RED;
outline: none;
}
&:active {
background: #5a6268;
}
}

View File

@@ -53,6 +53,7 @@ export default function HomeBanner({
const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const panels = useSelector((state) => state.panels.panels);
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
const focusHistory = useFocusHistory({
enableLogging: true,
@@ -163,7 +164,10 @@ export default function HomeBanner({
videoData = targetBannerData.bannerDetailInfos?.[0];
}
if (videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
// DetailPanel이 떠 있는 동안에는 배너 자동 재생을 스킵 (PlayerPanel 모달 재설정 방지)
const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL);
if (!hasDetailPanel && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
dispatch(
@@ -175,12 +179,13 @@ export default function HomeBanner({
shptmBanrTpNm: videoData.shptmBanrTpNm,
lgCatCd: videoData.lgCatCd,
chanId: videoData.brdcChnlId,
// 기본: 배너는 modal=true로 재생
modal: true,
modalContainerId: defaultFocus,
})
);
}
}, [bannerDataList, defaultFocus, dispatch]);
}, [bannerDataList, defaultFocus, dispatch, panels]);
const renderItem = useCallback(
(index, isHorizontal) => {

View File

@@ -1,31 +1,52 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// 디버그 모드 설정 - true일 때만 console.log 출력
const DEBUG_MODE = false;
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container';
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png';
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png';
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png';
import defaultLogoImg
from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyHorImage
from '../../../../assets/images/img-home-banner-empty-hor.png';
import emptyVerImage
from '../../../../assets/images/img-home-banner-empty-ver.png';
import defaultImageItem
from '../../../../assets/images/img-thumb-empty-product@3x.png';
import liveShow from '../../../../assets/images/tag-liveshow.png';
import { changeAppStatus } from '../../../actions/commonActions';
import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions';
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions';
import { pushPanel, navigateFromRandomUnit, SOURCE_MENUS } from '../../../actions/panelActions';
import {
setVideoTransitionLock,
updateHomeInfo,
} from '../../../actions/homeActions';
import {
sendLogTopContents,
sendLogTotalRecommend,
} from '../../../actions/logActions';
import {
navigateFromRandomUnit,
pushPanel,
SOURCE_MENUS,
} from '../../../actions/panelActions';
import {
finishVideoPreview,
hideModalVideo,
startVideoPlayer,
startVideoPlayerNew,
hideModalVideo,
} from '../../../actions/playActions';
import CustomImage from '../../../components/CustomImage/CustomImage';
import usePriceInfo from '../../../hooks/usePriceInfo';
@@ -36,11 +57,19 @@ import {
LOG_TP_NO,
panel_names,
} from '../../../utils/Config';
import { selectIsPlaying } from '../../../utils/playerState/playerStateSelectors';
import { $L, formatGMTString } from '../../../utils/helperMethods';
import {
$L,
formatGMTString,
} from '../../../utils/helperMethods';
import {
selectIsPlaying,
} from '../../../utils/playerState/playerStateSelectors';
import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from './RandomUnit.module.less';
// 디버그 모드 설정 - true일 때만 console.log 출력
const DEBUG_MODE = false;
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
@@ -66,6 +95,7 @@ export default function RandomUnit({
const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
const foryouInfos = useSelector((state) => state.foryou.recommendInfo.recommendProduct);
// 현재 재생 중인 비디오 배너 ID 가져오기
const currentVideoBannerId = useSelector((state) => {
@@ -490,6 +520,14 @@ export default function RandomUnit({
};
break;
case 'DSP00510':
linkInfo = {
name: panel_names.JUST_FOR_YOU_TEST_PANEL,
panelInfo: {
},
};
break;
default:
linkInfo = {
name: panel_names.HOME_PANEL,
@@ -644,6 +682,29 @@ export default function RandomUnit({
const { originalPrice, discountedPrice, discountRate, offerInfo } =
usePriceInfo(priceInfos) || {};
// Just For You 데이터에서 첫 번째 상품 추출
const justForYouProduct = useMemo(() => {
if (foryouInfos && foryouInfos.length > 0) {
const justForYouShelf = foryouInfos.find(
(shelf) => shelf.recommendTpCd === 'JUSTFORYOU'
);
if (justForYouShelf && justForYouShelf.productInfos && justForYouShelf.productInfos.length > 0) {
return justForYouShelf.productInfos[0];
}
}
return null;
}, [foryouInfos]);
// Just For You 상품의 가격 정보
const justForYouPriceInfo = usePriceInfo(
justForYouProduct && justForYouProduct.priceInfo ? justForYouProduct.priceInfo : ''
) || {
originalPrice: '',
discountedPrice: '',
discountRate: null,
offerInfo: '',
};
// 로그
useEffect(() => {
let _nowMenu = nowMenu;
@@ -999,7 +1060,72 @@ export default function RandomUnit({
/>
</div>
</SpottableComponent>
) : null}
)
: randomData?.shptmBanrTpNm == "Just For You" ? (
{/* <SpottableComponent
className={classNames(
css.itemBox,
css.justforyou,
countryCode === 'RU' ? css.ru : '',
countryCode === 'DE' ? css.de : '',
isHorizontal && css.isHorizontal
)}
onClick={todayDealClick}
spotlightId={spotlightId}
aria-label={justForYouProduct?.prdtNm ? justForYouProduct?.prdtNm : randomData.tmnlImgNm}
>
<div className={css.productInfo}>
<div className={css.justforyouTitle}>{$L("Just For You")}</div>
<div
className={css.textBox}
dangerouslySetInnerHTML={{
__html: `${justForYouProduct?.prdtNm || randomData.prdtNm}`,
}}
/>
<div className={css.accBox}>
{justForYouProduct ? (
<>
{parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') === 0
? justForYouProduct.offerInfo
: justForYouPriceInfo.discountRate
? justForYouPriceInfo.discountedPrice
: justForYouPriceInfo.originalPrice}
{justForYouPriceInfo.discountRate && !isHorizontal && (
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
)}
</>
) : (
<>
{parseFloat(originalPrice?.replace('$', '') || '0') === 0
? randomData?.offerInfo
: discountRate
? discountedPrice
: originalPrice}
{discountRate && !isHorizontal && (
<span className={css.saleAccBox}>{originalPrice}</span>
)}
</>
)}
</div>
{isHorizontal && justForYouProduct && parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') !== 0 && (
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
)}
</div>
<div className={css.itemImgBox}>
<CustomImage
delay={0}
src={justForYouProduct?.imgUrl || randomData.tmnlImgPath}
animationSpeed="fast"
fallbackSrc={defaultImageItem}
ariaLabel={justForYouProduct?.prdtNm || randomData.tmnlImgNm}
/>
</div>
</SpottableComponent> */}
)
: null}
</Container>
</>
);

View File

@@ -233,6 +233,165 @@
left: -4px;
}
}
&.justforyou {
background-image: url(../../../../assets/images/img-home-banner-jfy-ver@3x.png);
background-size: 486px 858px;
background-position: left top;
border-radius: 10px;
padding: 75px 51px 0;
&.ru {
.productInfo {
.justforyouTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.justforyouTitle {
font-size: 74px !important;
line-height: 63px !important;
letter-spacing: -1px !important;
}
}
}
.productInfo {
margin-bottom: 31px;
.justforyouTitle {
.size(@w:100%,@h:132px);
font-family: Arial;
font-weight: bold;
font-size: 80px;
word-break: break-word;
font-stretch: normal;
color: #151515;
text-align: center;
line-height: 76px;
font-family: @arialFontBold;
}
.textBox {
.size(@w: 100%, @h: 80px);
margin-top: 71px;
.elip(@clamp:2);
font-weight: bold;
font-size: 30px;
color: @COLOR_GRAY06;
line-height: 1.27;
margin-bottom: 6px;
}
.accBox {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 42px;
color: @PRIMARY_COLOR_RED;
line-height: 1.14;
display: inline-block;
.elip(@clamp:1);
> strong {
width: 260px;
font-size: 30px;
line-height: 1.27;
display: block;
.elip(@clamp:2);
}
.saleAccBox {
font-weight: normal;
font-size: 24px;
color: @COLOR_GRAY04;
vertical-align: middle;
text-decoration: line-through;
margin-left: 9px;
display: inline-block;
}
}
}
.itemImgBox {
> img {
.size(@w: 356px, @h: 356px);
border-radius: 12px;
border:6px solid #DCB9A1;
box-sizing: content-box;
}
}
&.isHorizontal {
background-image: url(../../../../assets/images/img-home-banner-jfy-hor@3x.png);
background-size: 744px 420px;
background-position: center center;
display: flex;
padding: 0 30px 0 0;
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
-o-border-radius: 10px;
> div {
flex: none;
}
&.ru {
.productInfo {
.justforyouTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.justforyouTitle {
font-size: 59px !important;
line-height: 63px !important;
letter-spacing: -2px !important;
}
}
}
.productInfo {
margin-bottom: 0;
.justforyouTitle {
.size(@w:305px,@h:114px);
margin-top: 53px;
margin-left: 49px;
font-size: 66px;
word-break: break-word;
color: #151515;
text-align: left;
line-height: 57px;
font-family: @arialBlack;
}
.textBox {
.size(@w: 294px, @h: 80px);
margin: 67px 0 5px 50px;
text-align: left;
}
.accBox {
.size(@w: 320px, @h: 50px);
margin-left: 50px;
text-align: left;
display: block;
.elip(@clamp:1);
}
.saleAccBox {
color: #767676;
display: block;
text-align: left;
margin: 5px 0 0 55px;
text-decoration: line-through;
}
}
.itemImgBox {
.position(@position: absolute, @top: 47px, @left: 389px);
.size(@w: 326px, @h: 326px);
> img {
.size(@w: inherit, @h: inherit);
border-radius: 12px;
border:6px solid #DCB9A1;
box-sizing: content-box;
}
}
}
}
}
.arrow {

View File

@@ -1,23 +1,49 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container';
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png';
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png';
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png';
import defaultLogoImg
from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyHorImage
from '../../../../assets/images/img-home-banner-empty-hor.png';
import emptyVerImage
from '../../../../assets/images/img-home-banner-empty-ver.png';
import defaultImageItem
from '../../../../assets/images/img-thumb-empty-product@3x.png';
import liveShow from '../../../../assets/images/tag-liveshow.png';
import { setBannerIndex, updateHomeInfo } from '../../../actions/homeActions';
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions';
import { pushPanel, SOURCE_MENUS } from '../../../actions/panelActions';
import { startVideoPlayer, finishVideoPreview } from '../../../actions/playActions';
import {
setBannerIndex,
updateHomeInfo,
} from '../../../actions/homeActions';
import {
sendLogTopContents,
sendLogTotalRecommend,
} from '../../../actions/logActions';
import {
pushPanel,
SOURCE_MENUS,
} from '../../../actions/panelActions';
import {
finishVideoPreview,
startVideoPlayer,
} from '../../../actions/playActions';
import CustomImage from '../../../components/CustomImage/CustomImage';
import usePriceInfo from '../../../hooks/usePriceInfo';
import {
@@ -27,7 +53,10 @@ import {
LOG_TP_NO,
panel_names,
} from '../../../utils/Config';
import { $L, formatGMTString } from '../../../utils/helperMethods';
import {
$L,
formatGMTString,
} from '../../../utils/helperMethods';
import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from './RollingUnit.module.less';
@@ -81,6 +110,7 @@ export default function RollingUnit({
const introTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
const foryouInfos = useSelector((state) => state.foryou.recommendInfo.recommendProduct);
const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
@@ -333,6 +363,29 @@ export default function RollingUnit({
const { originalPrice, discountedPrice, discountRate, offerInfo } =
usePriceInfo(filteredRollingData.length > 0 ? filteredRollingData[startIndex].priceInfo : {}) || {};
// Just For You 데이터에서 첫 번째 상품 추출
const justForYouProduct = useMemo(() => {
if (foryouInfos && foryouInfos.length > 0) {
const justForYouShelf = foryouInfos.find(
(shelf) => shelf.recommendTpCd === 'JUSTFORYOU'
);
if (justForYouShelf && justForYouShelf.productInfos && justForYouShelf.productInfos.length > 0) {
return justForYouShelf.productInfos[0];
}
}
return null;
}, [foryouInfos]);
// Just For You 상품의 가격 정보
const justForYouPriceInfo = usePriceInfo(
justForYouProduct && justForYouProduct.priceInfo ? justForYouProduct.priceInfo : ''
) || {
originalPrice: '',
discountedPrice: '',
discountRate: null,
offerInfo: '',
};
const handlePushPanel = useCallback(
(name, panelInfo) => {
const isDetailPanel = name === panel_names.DETAIL_PANEL;
@@ -582,7 +635,8 @@ export default function RollingUnit({
/>
) : null}
{filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === 'Image Banner' ? (
{/* 일반 Image Banner (Just For You, Today's Deals 제외) */}
{filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === 'Image Banner' && filteredRollingData[startIndex].shptmLnkTpNm !== 'Just For You' && filteredRollingData[startIndex].shptmLnkTpNm !== "Today's Deals" ? (
<SpottableComponent
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
onClick={imageBannerClick}
@@ -700,7 +754,7 @@ export default function RollingUnit({
)}
</div>
</SpottableComponent>
) : filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === "Today's Deals" ? (
) : filteredRollingData && filteredRollingData.length > 0 && (filteredRollingData[startIndex].shptmBanrTpNm === "Today's Deals" || filteredRollingData[startIndex].shptmLnkTpNm === "Today's Deals") ? (
<SpottableComponent
className={classNames(
css.itemBox,
@@ -754,6 +808,75 @@ export default function RollingUnit({
/>
</div>
</SpottableComponent>
) : filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmLnkTpNm === "Just For You" ? (
<SpottableComponent
className={classNames(
css.itemBox,
css.justforyou,
countryCode === 'RU' ? css.ru : '',
countryCode === 'DE' ? css.de : '',
isHorizontal && css.isHorizontal
)}
onClick={imageBannerClick}
onFocus={onFocus}
onBlur={onBlur}
spotlightId={spotlightId}
spotlightDisabled={contentsFocus}
aria-label={
justForYouProduct?.prdtNm
? justForYouProduct.prdtNm
: filteredRollingData[startIndex].tmnlImgNm
}
>
<div className={css.productInfo}>
<div className={css.justforyouTitle}>{$L("Just For You")}</div>
<div
className={css.textBox}
dangerouslySetInnerHTML={{
__html: `${justForYouProduct?.prdtNm || filteredRollingData[startIndex].prdtNm}`,
}}
/>
<div className={css.accBox}>
{justForYouProduct ? (
<>
{parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') === 0
? justForYouProduct.offerInfo
: justForYouPriceInfo.discountRate
? justForYouPriceInfo.discountedPrice
: justForYouPriceInfo.originalPrice}
{justForYouPriceInfo.discountRate && !isHorizontal && (
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
)}
</>
) : (
<>
{parseFloat(originalPrice?.replace('$', '') || '0') === 0
? filteredRollingData[startIndex]?.offerInfo
: discountRate
? discountedPrice
: originalPrice}
{discountRate && !isHorizontal && (
<span className={css.saleAccBox}>{originalPrice}</span>
)}
</>
)}
</div>
{isHorizontal && justForYouProduct && parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') !== 0 && (
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
)}
</div>
<div className={css.itemImgBox}>
<CustomImage
alt=""
delay={0}
animationSpeed="fast"
src={justForYouProduct?.imgUrl || filteredRollingData[startIndex]?.tmnlImgPath}
fallbackSrc={defaultImageItem}
ariaLabel={justForYouProduct?.prdtNm || filteredRollingData[startIndex]?.tmnlImgNm}
/>
</div>
</SpottableComponent>
) : null}
{filteredRollingData.length !== 1 ? (

View File

@@ -237,6 +237,165 @@
left: -4px;
}
}
&.justforyou {
background-image: url(../../../../assets/images/img-home-banner-jfy-ver@3x.png);
background-size: 486px 858px;
background-position: left top;
border-radius: 10px;
padding: 75px 51px 0;
&.ru {
.productInfo {
.justforyouTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.justforyouTitle {
font-size: 74px !important;
line-height: 63px !important;
letter-spacing: -1px !important;
}
}
}
.productInfo {
margin-bottom: 31px;
.justforyouTitle {
.size(@w:100%,@h:132px);
font-family: Arial;
font-weight: bold;
font-size: 80px;
word-break: break-word;
font-stretch: normal;
color: #151515;
text-align: center;
line-height: 76px;
font-family: @arialFontBold;
}
.textBox {
.size(@w: 100%, @h: 80px);
margin-top: 71px;
.elip(@clamp:2);
font-weight: bold;
font-size: 30px;
color: @COLOR_GRAY06;
line-height: 1.27;
margin-bottom: 6px;
}
.accBox {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 42px;
color: @PRIMARY_COLOR_RED;
line-height: 1.14;
display: inline-block;
.elip(@clamp:1);
> strong {
width: 260px;
font-size: 30px;
line-height: 1.27;
display: block;
.elip(@clamp:2);
}
.saleAccBox {
font-weight: normal;
font-size: 24px;
color: @COLOR_GRAY04;
vertical-align: middle;
text-decoration: line-through;
margin-left: 9px;
display: inline-block;
}
}
}
.itemImgBox {
> img {
.size(@w: 356px, @h: 356px);
border-radius: 12px;
border:6px solid #DCB9A1;
box-sizing: content-box;
}
}
&.isHorizontal {
background-image: url(../../../../assets/images/img-home-banner-jfy-hor@3x.png);
background-size: 744px 420px;
background-position: center center;
display: flex;
padding: 0 30px 0 0;
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
-o-border-radius: 10px;
> div {
flex: none;
}
&.ru {
.productInfo {
.justforyouTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.justforyouTitle {
font-size: 59px !important;
line-height: 63px !important;
letter-spacing: -2px !important;
}
}
}
.productInfo {
margin-bottom: 0;
.justforyouTitle {
.size(@w:305px,@h:114px);
margin-top: 53px;
margin-left: 49px;
font-size: 66px;
word-break: break-word;
color: #151515;
text-align: left;
line-height: 57px;
font-family: @arialBlack;
}
.textBox {
.size(@w: 294px, @h: 80px);
margin: 67px 0 5px 50px;
text-align: left;
}
.accBox {
.size(@w: 320px, @h: 50px);
margin-left: 50px;
text-align: left;
display: block;
.elip(@clamp:1);
}
.saleAccBox {
color: #767676;
display: block;
text-align: left;
margin: 5px 0 0 55px;
text-decoration: line-through;
}
}
.itemImgBox {
.position(@position: absolute, @top: 47px, @left: 389px);
.size(@w: 326px, @h: 326px);
> img {
.size(@w: inherit, @h: inherit);
border-radius: 12px;
border:6px solid #DCB9A1;
box-sizing: content-box;
}
}
}
}
}
.arrow {

View File

@@ -1,8 +1,16 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { types } from '../../actions/actionTypes';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { applyMiddleware } from 'redux';
import Spotlight from '@enact/spotlight';
@@ -11,53 +19,6 @@ import {
setContainerLastFocusedElement,
} from '@enact/spotlight/src/container';
import {
changeAppStatus,
setDeepLink,
setExitApp,
setHidePopup,
setShowPopup,
setOptionalTermsPopupShown,
updateOptionalTermsAgreement,
} from '../../actions/commonActions';
import { getWelcomeEventInfo } from '../../actions/eventActions';
import {
checkEnterThroughGNB,
getHomeLayout,
getHomeMainContents,
updateHomeInfo,
// <<<<<<< HEAD
} from '../../actions/homeActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions';
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
import { getSubCategory, getTop20Show } from '../../actions/mainActions';
import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
import { updatePanel } from '../../actions/panelActions';
import {
showModalVideo,
finishVideoPreview,
hideModalVideo,
startVideoPlayerNew,
} from '../../actions/playActions';
import { getBestSeller } from '../../actions/productActions';
import TBody from '../../components/TBody/TBody';
import TButton, { TYPES } from '../../components/TButton/TButton';
import OptionalConfirm from '../../components/Optional/OptionalConfirm';
import TNewPopUp from '../../components/TPopUp/TNewPopUp';
import TPanel from '../../components/TPanel/TPanel';
import TPopUp from '../../components/TPopUp/TPopUp';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import useDebugKey from '../../hooks/useDebugKey';
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
import usePrevious from '../../hooks/usePrevious';
import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay';
import ImagePreloader from '../../utils/ImagePreloader';
import { createDebugHelpers } from '../../utils/debug';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// DetailPanelBackground 이미지 imports for preloading
import hsn from '../../../assets/images/bg/hsn_new.png';
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png';
@@ -66,17 +27,54 @@ import ontv4u from '../../../assets/images/bg/ontv4u_new.png';
import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../assets/images/bg/shoplc_new.png';
// 파트너사별 배경 이미지 맵
const BACKGROUND_IMAGES = {
1: qvc, // QVC
2: hsn, // HSN
4: ontv4u, // ONTV
9: lgelectronics, // LG ELECTRONICS
11: shoplc, // SHOPLC
16: koreaKiosk, // KOREA KIOSK
19: Pinkfong, // PINKFONG
};
import nbcu from '../../../assets/images/bg/nbcu_new.png';
import { types } from '../../actions/actionTypes';
import {
changeAppStatus,
setDeepLink,
setExitApp,
setHidePopup,
setOptionalTermsPopupShown,
setShowPopup,
updateOptionalTermsAgreement,
} from '../../actions/commonActions';
import { getWelcomeEventInfo } from '../../actions/eventActions';
import {
checkEnterThroughGNB,
getHomeLayout,
getHomeMainContents,
updateHomeInfo,
} from '../../actions/homeActions';
import {
sendLogGNB,
sendLogTotalRecommend,
} from '../../actions/logActions';
import {
getSubCategory,
getTop20Show,
} from '../../actions/mainActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions';
import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
import { updatePanel } from '../../actions/panelActions';
import {
finishVideoPreview,
hideModalVideo,
showModalVideo,
startVideoPlayerNew,
} from '../../actions/playActions';
import { getBestSeller } from '../../actions/productActions';
import OptionalConfirm from '../../components/Optional/OptionalConfirm';
import TBody from '../../components/TBody/TBody';
import TButton, { TYPES } from '../../components/TButton/TButton';
import TPanel from '../../components/TPanel/TPanel';
import TNewPopUp from '../../components/TPopUp/TNewPopUp';
import TPopUp from '../../components/TPopUp/TPopUp';
import TVerticalPagenator
from '../../components/TVerticalPagenator/TVerticalPagenator';
import useDebugKey from '../../hooks/useDebugKey';
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
import usePrevious from '../../hooks/usePrevious';
import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay';
// [COMMENTED OUT] useVideoMove 관련 코드 주석 처리 - 향후 사용 검토 필요
// import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove';
// =======
@@ -100,7 +98,9 @@ import {
LOG_MESSAGE_ID,
panel_names,
} from '../../utils/Config';
import { createDebugHelpers } from '../../utils/debug';
import { $L } from '../../utils/helperMethods';
import ImagePreloader from '../../utils/ImagePreloader';
import { SpotlightIds } from '../../utils/SpotlightIds';
import BestSeller from '../HomePanel/BestSeller/BestSeller';
import HomeBanner from '../HomePanel/HomeBanner/HomeBanner';
@@ -111,6 +111,22 @@ import SubCategory from '../HomePanel/SubCategory/SubCategory';
import EventPopUpBanner from './EventPopUpBanner/EventPopUpBanner';
import PickedForYou from './PickedForYou/PickedForYou';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// 파트너사별 배경 이미지 맵
const BACKGROUND_IMAGES = {
1: qvc, // QVC
2: hsn, // HSN
4: ontv4u, // ONTV
9: lgelectronics, // LG ELECTRONICS
11: shoplc, // SHOPLC
16: koreaKiosk, // KOREA KIOSK
19: Pinkfong, // PINKFONG
21: nbcu, // NBCU
};
export const TEMPLATE_CODE_CONF = {
TOP: 'DSP00101',
CATEGORY_ITEM: 'DSP00102',
@@ -399,6 +415,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
optionalTerms: 'Y',
},
});
setTimeout(()=>{
Spotlight.focus('home_tbody');
},100)
}, [handleOptionalAgree, dispatch, currentTermsFlag]);
const handleOptionalDeclineClick = useCallback(() => {
@@ -407,6 +426,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
}
dispatch(updateOptionalTermsAgreement(false));
setIsOptionalConfirmVisible(false);
setTimeout(()=>{
Spotlight.focus('home_tbody');
},100)
}, [dispatch]);
const handleTermsPopupClosed = useCallback(() => {

View File

@@ -53,7 +53,7 @@ import { types } from "../../actions/actionTypes";
import { focusById } from "../../utils/spotlight-utils";
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
{ enterTo: "#selectAllCheckbox" },
"div",
);
@@ -282,7 +282,7 @@ function IntroPanelWithOptional({
useEffect(() => {
const focusTimer = setTimeout(() => {
focusById("selectAllCheckbox");
}, 500);
}, 0);
return () => {
clearTimeout(focusTimer);
@@ -844,27 +844,29 @@ function IntroPanelWithOptional({
//20250903 pjh
//selectAll에서 포커스 올렸을때 처리 변경.
const onSelectAllSpotlightUp = () => {
const focusTimer = setTimeout(() => {
Spotlight.focus("optionalCheckbox");
}, 100);
return () => {
clearTimeout(focusTimer);
};
const onSelectAllSpotlightUp = (event) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
setTimeout(() => {
focusById("optionalCheckbox");
}, 10);
};
//Optional terms 체크박스가 체크가 되어있을때 아래로 내리면(3개전부 체크기준) agree로 가고, 아닐경우 select all로 포커스이동해야함.
const onOptionalTermSpotlightDown = () => {
const focusTimer = setTimeout(() => {
if (termsChecked && privacyChecked && optionalChecked) {
Spotlight.focus("agreeButton");
} else {
Spotlight.focus("selectAllCheckbox");
const onOptionalTermSpotlightDown = (event) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
}, 100);
return () => {
clearTimeout(focusTimer);
};
setTimeout(() => {
if (termsChecked && privacyChecked && optionalChecked) {
focusById("agreeButton");
} else {
focusById("selectAllCheckbox");
}
}, 10);
};
// useEffect(() => {
@@ -905,7 +907,7 @@ function IntroPanelWithOptional({
handleCancel={onCancel}
spotlightId={spotlightId}
>
<Container {...rest} className={css.introLayout}>
<Container className={css.introLayout} defaultElement="#selectAllCheckbox" {...rest}>
{/* 첫 번째 영역: 헤더 섹션 */}
<div className={css.headerSection}>
<div className={css.titleContainer}>

View File

@@ -206,7 +206,9 @@ export default function MainView({ className, initService }) {
let renderingPanels = [];
const topPanel = panels[panels.length - 1];
const hasFeaturedBrandsPanel = panels.some(
(panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL
);
// 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들
if (DEBUG_MODE) {
console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`);
@@ -248,7 +250,7 @@ export default function MainView({ className, initService }) {
'[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel'
);
}
renderingPanels = panels.slice(-3);
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-4) : panels.slice(-3);
} else if (
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
@@ -256,12 +258,17 @@ export default function MainView({ className, initService }) {
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL
) {
renderingPanels = panels.slice(-2);
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-3) : panels.slice(-2);
} else {
renderingPanels = panels.slice(-1);
}
}
// DetailPanel 위치 확인 (있으면 항상 onTop 처리)
const detailPanelIndex = renderingPanels.findIndex(
(panel) => panel.name === Config.panel_names.DETAIL_PANEL
);
return (
<>
{(isHomeOnTop ||
@@ -287,18 +294,13 @@ export default function MainView({ className, initService }) {
console.log(`[MainView] Standalone panel ${panel.name} is always onTop`);
}
}
// 3-layer 케이스: 중간 패널(DetailPanel)이 onTop
else if (renderingPanels.length === 3) {
if (index === 1) {
// DetailPanel (중간)
isPanelOnTop = true;
if (DEBUG_MODE) {
console.log('[MainView] 3-layer: DetailPanel is onTop');
// DetailPanel이 포함되어 있으면 항상 onTop
else if (detailPanelIndex >= 0) {
isPanelOnTop = index === detailPanelIndex;
if (DEBUG_MODE && isPanelOnTop) {
console.log('[MainView] DetailPanel set to onTop');
}
}
// PlayerPanel (index 0): isOnTop = false (백그라운드)
// MediaPanel (index 2): isOnTop = false (modal overlay)
}
// 2-layer 케이스: modal이면 첫 번째가 onTop
else if (
index === 0 &&

View File

@@ -106,7 +106,10 @@ export default memo(function PlayerItemCard({
}
}, [onFocus]);
const progressStyle = useMemo(() => ({ width: `${percentGauge}%` }), [percentGauge]);
const progressStyle = useMemo(
() => ({ width: `${percentGauge}%` }),
[percentGauge]
);
const ariaLabel = 'Selected, ' + patnerName + ' ' + productName();
const css = version === 2 ? css2 : css1;
@@ -121,13 +124,17 @@ export default memo(function PlayerItemCard({
onBlur={_onBlur}
onClick={_onClick}
onFocus={_onFocus}
spotlightId={productId ? 'spotlightId-' + removeDotAndColon(productId) : spotlightId}
spotlightId={
productId ? 'spotlightId-' + removeDotAndColon(productId) : spotlightId
}
aria-label={ariaLabel}
{...rest}
>
<div className={css.imageWrap}>
<CustomImage alt={imageAlt} delay={0} src={imageSource} />
{soldoutFlag && soldoutFlag === 'Y' && <div>{$L(STRING_CONF.SOLD_OUT)}</div>}
{soldoutFlag && soldoutFlag === 'Y' && (
<div>{$L(STRING_CONF.SOLD_OUT)}</div>
)}
{currentVideoVisible && (
<div className={css.nowPlay}>
<h3>{$L('Now Playing')}</h3>
@@ -136,7 +143,14 @@ export default memo(function PlayerItemCard({
</div>
<div className={css.descWrap}>
<div className={css.patnerWrap}>
<CustomImage className={css.logo} src={logo} fallbackSrc={defaultLogoImg} />
<CustomImage
className={classNames(
css.logo,
(patnerName === 'QVC' || patnerName === 'qvc') && css.qvcLogo
)}
src={logo}
fallbackSrc={defaultLogoImg}
/>
<h3 className={css.brandName}>{patnerName}</h3>
</div>

View File

@@ -1,5 +1,5 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
@import '../../../style/CommonStyle.module.less';
@import '../../../style/utils.module.less';
/* liveHorizontal */
.liveHorizontal {
@@ -86,10 +86,13 @@
max-width: 80%;
max-height: 80%;
object-fit: contain;
&.qvcLogo {
border-radius: 0;
}
}
.brandName {
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 700;
@@ -100,7 +103,7 @@
.title {
width: 100%;
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 400;
@@ -124,16 +127,16 @@
// track 역할
&::before {
content: "";
content: '';
.size(@w: 100%, @h: 5px);
.position(@position: absolute, @top: 0, @left: 0);
background: #D4D4D4;
background: #d4d4d4;
}
.gauge {
height: 5px;
.position(@position: absolute, @top: 0, @left: 0);
background: #7D848C;
background: #7d848c;
transition: width 0.3s ease;
&.focused {
@@ -158,7 +161,7 @@
border-color: @PRIMARY_COLOR_RED;
&::after {
content: "";
content: '';
position: absolute;
top: 0;
left: 0;
@@ -259,7 +262,7 @@
}
.brandName {
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 700;
@@ -270,7 +273,7 @@
.title {
width: 100%;
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 400;
@@ -289,7 +292,7 @@
border-color: @PRIMARY_COLOR_RED;
&::after {
content: "";
content: '';
position: absolute;
top: 0;
left: 0;

View File

@@ -1161,6 +1161,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
}
if (!panelInfo.modal) {
console.log('[PlayerPanel] popPanel - closeButtonHandler');
dispatch(PanelActions.popPanel());
dispatch(changeAppStatus({ cursorVisible: false }));
@@ -1191,13 +1192,27 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
//todo if(modal)
return () => {
// 패널이 2개 존재할때만 popPanel 진행
if (panelInfo.modal && !isOnTop) {
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
console.log('[PP-TRACE] cleanup start', {
modal: panelInfo.modal,
isOnTop,
topPanel: panels[panels.length - 1]?.name,
stack: panels.map((p) => p.name),
});
const topPanelName = panels[panels.length - 1]?.name;
if (
panelInfo.modal &&
!isOnTop &&
topPanelName === panel_names.PLAYER_PANEL &&
panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지)
) {
console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)');
dispatch(PanelActions.popPanel());
} else {
Spotlight.focus('tbody');
}
};
}, [panelInfo?.modal, isOnTop]);
}, [panelInfo?.modal, isOnTop, panels]);
useEffect(() => {
if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') {
@@ -1810,9 +1825,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo?.modalContainerId
) {
// case: Featured Brands
if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
dispatch(PanelActions.popPanel());
}
// if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
// dispatch(PanelActions.popPanel());
// }
console.log('[PlayerPanel] Condition 4: Handling video error in fullscreen mode');
}
}, [
broadcast?.type,
@@ -2384,6 +2400,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
);
Spotlight.pause();
timeoutRef.current = setTimeout(() => {
console.log('[PlayerPanel] popPanel - VIDEO_END_ACTION_DELAY');
Spotlight.resume();
dispatch(PanelActions.popPanel());
}, VIDEO_END_ACTION_DELAY);
@@ -2955,7 +2972,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
disabled={panelInfo.modal}
onEnded={onEnded}
noAutoPlay={cannotPlay}
autoCloseTimeout={6000}
autoCloseTimeout={10000}
onBackButton={onClickBack}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}

View File

@@ -292,7 +292,7 @@ export default function ShopNowContents({
// ===== navigateToDetail 방식 (handleYouMayLikeItemClick 참고) =====
console.log(
"[ShopNowContents] ShopNow DetailPanel 진입 - sourceMenu:",
"[DetailPanel-BG][ShopNowContents] ShopNow DetailPanel 진입 - sourceMenu:",
SOURCE_MENUS.PLAYER_SHOP_NOW
);

View File

@@ -1,16 +1,13 @@
import React from 'react';
import classNames from 'classnames';
import { compose } from 'ramda/src/compose';
import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable';
import {
Marquee,
MarqueeController,
} from '@enact/ui/Marquee';
import { Marquee, MarqueeController } from '@enact/ui/Marquee';
import icon_arrow_dwon
from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import { SpotlightIds } from '../../../../utils/SpotlightIds';
import css from './LiveChannelNext.module.less';
@@ -55,9 +52,19 @@ export default function LiveChannelNext({
onSpotlightRight={handleSpotlightRight}
>
<div className={css.logoWrapper}>
<div className={css.logoBackground} style={{ background: backgroundColor }}>
<div
className={css.logoBackground}
style={{ background: backgroundColor }}
>
{channelLogo ? (
<CustomImage src={channelLogo} alt={channelName} className={css.logoImage} />
<CustomImage
src={channelLogo}
alt={channelName}
className={classNames(
css.logoImage,
channelName === 'QVC' && css.qvcLogoImg
)}
/>
) : (
<div className={css.logoPlaceholder} />
)}

View File

@@ -1,5 +1,5 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
@import '../../../../style/CommonStyle.module.less';
@import '../../../../style/utils.module.less';
.container {
position: fixed;
@@ -51,13 +51,16 @@
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(180deg, #284998 0%, #06B0EE 100%);
}
.logoImage {
width: 100%;
height: 100%;
object-fit: cover;
&.qvcLogoImg {
width: 70%;
height: 70%;
}
}
.logoPlaceholder {
@@ -69,7 +72,7 @@
.channelName {
color: #fcfcfc;
font-size: 25px;
font-family: "LG Smart UI";
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
white-space: nowrap;
@@ -81,7 +84,7 @@
.programName {
color: rgba(234, 234, 234, 0.7);
font-size: 25px;
font-family: "LG Smart UI";
font-family: 'LG Smart UI';
font-weight: 600;
line-height: 35px;
white-space: nowrap;
@@ -91,7 +94,6 @@
max-width: 180px; // 최대 너비 제한 완화
}
.arrowIcon {
display: flex;
align-items: center;

View File

@@ -7,27 +7,28 @@ import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
// import icon_arrow_right from '../../../../../assets/images/icons';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import usePrevious from '../../../../hooks/usePrevious';
import { LOG_MENU } from '../../../../utils/Config';
import { createDebugHelpers } from '../../../../utils/debug';
import { $L } from '../../../../utils/helperMethods';
import { SpotlightIds } from '../../../../utils/SpotlightIds';
import FeaturedShowContents from '../TabContents/FeaturedShowContents';
import LiveChannelContents from '../TabContents/LiveChannelContents';
import ShopNowContents from '../TabContents/ShopNowContents';
import LiveChannelNext from './LiveChannelNext';
import ShopNowButton from './ShopNowButton';
import css from './TabContainer.v2.module.less';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// import icon_arrow_right from '../../../../../assets/images/icons';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import icon_shop_now from '../../../../../assets/images/player/icon_tabcontainer_shopnow.png';
import { LOG_MENU } from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods';
import { SpotlightIds } from '../../../../utils/SpotlightIds';
import usePrevious from '../../../../hooks/usePrevious';
import LiveChannelContents from '../TabContents/LiveChannelContents';
import FeaturedShowContents from '../TabContents/FeaturedShowContents';
import ShopNowContents from '../TabContents/ShopNowContents';
import ShopNowButton from './ShopNowButton';
import LiveChannelNext from './LiveChannelNext';
import css from './TabContainer.v2.module.less';
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
const Container = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
const SpottableDiv = Spottable('div');
@@ -56,12 +57,16 @@ export default function TabContainerV2({
const findNextPlayableShow = useCallback((currentPlayList, currentIndex) => {
if (!currentPlayList || currentPlayList.length === 0) return null;
let nextIndex = currentIndex === currentPlayList.length - 1 ? 0 : currentIndex + 1;
let nextIndex =
currentIndex === currentPlayList.length - 1 ? 0 : currentIndex + 1;
let initialIndex = nextIndex;
let attempts = 0;
// 유효한 showId를 가진 다음 쇼 찾기
while (!currentPlayList[nextIndex]?.showId && attempts < currentPlayList.length) {
while (
!currentPlayList[nextIndex]?.showId &&
attempts < currentPlayList.length
) {
nextIndex = nextIndex === currentPlayList.length - 1 ? 0 : nextIndex + 1;
attempts++;
if (nextIndex === initialIndex) break;
@@ -87,13 +92,18 @@ export default function TabContainerV2({
data: youmaylikeInfos,
shopNowInfo_length: shopNowInfo?.length,
shouldShowYouMayAlso:
shopNowInfo && shopNowInfo.length < 3 && youmaylikeInfos && youmaylikeInfos.length > 0,
shopNowInfo &&
shopNowInfo.length < 3 &&
youmaylikeInfos &&
youmaylikeInfos.length > 0,
});
}, [youmaylikeInfos, shopNowInfo]);
const tabList = [
$L('SHOP NOW'),
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'),
panelInfo?.shptmBanrTpNm === 'LIVE'
? $L('LIVE CHANNEL')
: $L('FEATURED SHOWS'),
];
useEffect(() => {
@@ -105,7 +115,9 @@ export default function TabContainerV2({
if (tabIndex === 1) {
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE';
nowMenu = isLive ? LOG_MENU.FULL_LIVE_CHANNELS : LOG_MENU.FULL_FEATURED_SHOWS;
nowMenu = isLive
? LOG_MENU.FULL_LIVE_CHANNELS
: LOG_MENU.FULL_FEATURED_SHOWS;
}
if (nowMenu) {
@@ -160,7 +172,9 @@ export default function TabContainerV2({
// 하나의 함수에서 모든 tabIndex 변화 처리
const handleTabIndexChange = useCallback((newTabIndex, oldTabIndex) => {
console.log(`[TabIndexChange] Tab changed from ${oldTabIndex} to ${newTabIndex}`);
console.log(
`[TabIndexChange] Tab changed from ${oldTabIndex} to ${newTabIndex}`
);
if (newTabIndex === 0) {
// tabIndex = 0 (ShopNow)
@@ -220,8 +234,18 @@ export default function TabContainerV2({
Spotlight.focus('shop-now-item-0');
}}
>
<div className={css.shopNowIconWrapper}>
<img src={icon_shop_now} alt="shop now icon" className={css.shopNowIcon} />
<div
className={classNames(
css.shopNowIconWrapper,
playListInfo[selectedIndex]?.patncNm === 'QVC' &&
css.shopNowQvcIconWrapper
)}
>
<img
src={playListInfo[selectedIndex]?.patncLogoPath}
alt="shop now icon"
className={css.shopNowIcon}
/>
</div>
<div className={css.shopNowHeaderText}>SHOP NOW</div>
<div className={css.arrowIcon}>
@@ -250,7 +274,9 @@ export default function TabContainerV2({
youmaylikeInfos &&
youmaylikeInfos.length > 0 && (
<div className={css.youMayAlsoLikeHeader}>
<div className={css.youMayAlsoLikeText}>You may also like</div>
<div className={css.youMayAlsoLikeText}>
You may also like
</div>
</div>
)}
</div>
@@ -273,7 +299,11 @@ export default function TabContainerV2({
<SpottableDiv
className={css.liveChannelButton}
onClick={onLiveChannelButtonClick}
spotlightId={panelInfo?.shptmBanrTpNm === 'LIVE' ? 'below-tab-live-channel-button' : 'below-tab-featured-show-button'}
spotlightId={
panelInfo?.shptmBanrTpNm === 'LIVE'
? 'below-tab-live-channel-button'
: 'below-tab-featured-show-button'
}
onSpotlightUp={handleSpotlightUpToBackButton}
onSpotlightDown={(e) => {
// 첫 번째 PlayerItem으로 포커스 이동

View File

@@ -1,5 +1,5 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
@import '../../../../style/CommonStyle.module.less';
@import '../../../../style/utils.module.less';
.tabContainer {
.position(@position: fixed, @bottom: 0, @left: 0);
@@ -61,7 +61,7 @@
}
&::before {
content: "";
content: '';
.position(@position: absolute, @top: 0, @left: 0);
.size(@w: 100%, @h: 100%);
background: linear-gradient(
@@ -113,10 +113,17 @@
background: white;
border-radius: 100px;
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
&.shopNowQvcIconWrapper {
border-radius: 0;
.shopNowIcon {
.size(@w: 40px, @h: 40px);
object-fit: contain;
}
}
}
.shopNowIcon {
.size(@w: 20.67px, @h: 20.67px);
.size(@w: 40px, @h: 40px);
object-fit: contain;
}
@@ -265,7 +272,7 @@
.youMayAlsoLikeText {
margin-right: 15px;
color: #EAEAEA;
color: #eaeaea;
font-size: 24px;
font-family: @baseFont;
font-weight: 700;

View File

@@ -45,7 +45,7 @@
padding: 20px;
flex-direction: column;
align-items: flex-start;
background: #4a4c50 !important; // 기본 배경색과 동일
background: #4F172C !important; // 기본 배경색과 동일
border-radius: 100px;
border: 1px solid #585858 !important;
white-space: nowrap;