83 Commits

Author SHA1 Message Date
0cd22f4989 [251216] fix: panelHistoryMiddleware log
🕐 커밋 시간: 2025. 12. 16. 10:09:15

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

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

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 코드 정리 및 최적화
2025-12-16 10:09:15 +09:00
junghoon86.park
cb3a4e9bc7 [live영상] now playing노출 관련 수정
- currentVideoVisible 추가
2025-12-15 15:22:09 +09:00
junghoon86.park
3ce4398e67 [상품상세] 폰트수정
- 폰트수정
2025-12-15 14:31:13 +09:00
junghoon86.park
78153bae0c [상품상세] 폰트수정
- 글자크기가 디자인과 맞지 않아 이부분 수정.
2025-12-15 14:29:54 +09:00
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
99 changed files with 4530 additions and 690 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 { Job } from '@enact/core/util';
import platform from '@enact/core/platform'; import platform from '@enact/core/platform';
import { ThemeDecorator } from '@enact/sandstone/ThemeDecorator'; import { ThemeDecorator } from '@enact/sandstone/ThemeDecorator';
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
// import "../../../assets/fontello/css/fontello.css"; // import "../../../assets/fontello/css/fontello.css";
@@ -406,8 +407,6 @@ Spotlight.silentlyFocus = function (...args) {
return ret; return ret;
}; };
// Spotlight Focus 추적 로그 [251115] // Spotlight Focus 추적 로그 [251115]
// DOM 이벤트 리스너로 대체 // 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 dispatch = useDispatch();
const httpHeader = useSelector((state) => state.common.httpHeader); const httpHeader = useSelector((state) => state.common.httpHeader);
const httpHeaderRef = useRef(httpHeader); const httpHeaderRef = useRef(httpHeader);
@@ -628,7 +627,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
clearLaunchParams(); clearLaunchParams();
} }
}, },
[dispatch], [dispatch]
); );
const handleRelaunchEvent = useCallback(() => { const handleRelaunchEvent = useCallback(() => {
@@ -682,7 +681,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
if (typeof window === 'object' && window.PalmSystem) { if (typeof window === 'object' && window.PalmSystem) {
window.PalmSystem.activate(); window.PalmSystem.activate();
} }
}, [initService, introTermsAgreeRef]); }, [initService, introTermsAgreeRef, dispatch]);
const visibilityChanged = useCallback(() => { const visibilityChanged = useCallback(() => {
// console.log('document is hidden', document.hidden); // console.log('document is hidden', document.hidden);
@@ -726,7 +725,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
}, [dispatch]); }, [dispatch]);
useEffect(() => { useEffect(() => {
const keyDownEvent = (_event /* eslint-disable-line no-unused-vars */) => { const keyDownEvent = (event) => {
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
Spotlight.setPointerMode(false); Spotlight.setPointerMode(false);
}; };
@@ -735,7 +734,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
let lastMoveTime = 0; let lastMoveTime = 0;
const THROTTLE_MS = 100; const THROTTLE_MS = 100;
const mouseMoveEvent = (_event /* eslint-disable-line no-unused-vars */) => { const mouseMoveEvent = (event) => {
const now = Date.now(); const now = Date.now();
if (now - lastMoveTime < THROTTLE_MS) { if (now - lastMoveTime < THROTTLE_MS) {
// throttle 기간 내에는 hideCursor만 재시작 // throttle 기간 내에는 hideCursor만 재시작
@@ -788,7 +787,9 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
let userDataChanged = false; let userDataChanged = false;
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) { if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
userDataChanged = true; userDataChanged = true;
} else if (userDataChanged || httpHeaderRef.current === null) { }
if (!httpHeader || !deviceId) {
} else if (userDataChanged || httpHeaderRef.current === null) {
//계정정보 변경시 또는 초기 로딩시 //계정정보 변경시 또는 초기 로딩시
if (!httpHeader) { if (!httpHeader) {
dispatch( dispatch(
@@ -888,7 +889,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
/> />
)} )}
<ToastContainer /> <ToastContainer />
{/* <GlobalPopup /> */} <GlobalPopup />
</ErrorBoundary> </ErrorBoundary>
); );
} }

View File

@@ -130,11 +130,16 @@ export const types = {
GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO', GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO',
GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM', GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM',
GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED', 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_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING',
SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO', SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO',
RESET_BRAND_STATE: 'RESET_BRAND_STATE', RESET_BRAND_STATE: 'RESET_BRAND_STATE',
RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO', RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO',
RESET_BRAND_LAYOUT_INFO: 'RESET_BRAND_LAYOUT_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 // main actions
GET_SUB_CATEGORY: 'GET_SUB_CATEGORY', GET_SUB_CATEGORY: 'GET_SUB_CATEGORY',

View File

@@ -37,10 +37,12 @@ export const getBrandList = () => (dispatch, getState) => {
export const getBrandLayoutInfo = (props) => (dispatch, getState) => { export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
const { patnrId } = props; const { patnrId } = props;
// console.log("[getBrandLayoutInfo] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })); dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => { const onSuccess = (response) => {
// dlog("getBrandLayoutInfo onSuccess ", response.data); // console.log("[getBrandLayoutInfo] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({ dispatch({
type: types.GET_BRAND_LAYOUT_INFO, type: types.GET_BRAND_LAYOUT_INFO,
@@ -53,6 +55,7 @@ export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
}; };
const onFail = (error) => { const onFail = (error) => {
// console.log("[getBrandLayoutInfo] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandLayoutInfo onFail ', error); derror('getBrandLayoutInfo onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}; };
@@ -336,10 +339,15 @@ export const getBrandCategoryProductInfo = (props) => (dispatch, getState) => {
export const getBrandBestSeller = (props) => (dispatch, getState) => { export const getBrandBestSeller = (props) => (dispatch, getState) => {
const { patnrId } = props; const { patnrId } = props;
// console.log("[getBrandBestSeller] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })); dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => { 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({ dispatch({
type: types.GET_BRAND_BEST_SELLER, type: types.GET_BRAND_BEST_SELLER,
@@ -352,6 +360,7 @@ export const getBrandBestSeller = (props) => (dispatch, getState) => {
}; };
const onFail = (error) => { const onFail = (error) => {
// console.log("[getBrandBestSeller] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandBestSeller onFail ', error); derror('getBrandBestSeller onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); 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); 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 // Featured Brands Recently Aired 조회 IF-LGSP-373
export const getBrandRecentlyAired = (props) => (dispatch, getState) => { export const getBrandRecentlyAired = (props) => (dispatch, getState) => {
const { patnrId } = props; 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 '); dlog('getSystemSettings ');
lunaSend.getSystemSettings( lunaSend.getSystemSettings(
{ category: 'caption', keys: ['captionEnable'] }, { 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 '); dlog('getHttpHeaderForServiceRequest ');
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings; const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
lunaSend.getHttpHeaderForServiceRequest({ lunaSend.getHttpHeaderForServiceRequest({
@@ -285,7 +285,7 @@ export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
}); });
}; };
export const getDeviceId = (onComplete) => (dispatch) => { export const getDeviceId = (onComplete) => (dispatch, getState) => {
lunaSend.getDeviceId( lunaSend.getDeviceId(
{ idType: ['LGUDID'] }, { idType: ['LGUDID'] },
{ {
@@ -463,7 +463,7 @@ export const setFocus = (spotlightId) => ({
payload: spotlightId, payload: spotlightId,
}); });
export const focusElement = (spotlightId) => (dispatch) => { export const focusElement = (spotlightId) => (dispatch, getState) => {
dispatch(setFocus(spotlightId)); dispatch(setFocus(spotlightId));
if (typeof window === 'object') { if (typeof window === 'object') {
@@ -485,7 +485,7 @@ export const cancelFocusElement = () => () => {
let broadcastTimer = null; let broadcastTimer = null;
export const sendBroadCast = export const sendBroadCast =
({ type, moreInfo }) => ({ type, moreInfo }) =>
(dispatch) => { (dispatch, getState) => {
clearTimeout(broadcastTimer); clearTimeout(broadcastTimer);
dispatch(changeBroadcastEvent({ type, moreInfo })); dispatch(changeBroadcastEvent({ type, moreInfo }));
broadcastTimer = setTimeout(() => { broadcastTimer = setTimeout(() => {
@@ -542,7 +542,7 @@ export const addReservation = (data) => (dispatch) => {
}); });
}; };
export const deleteReservationCallback = (scheduleIdList) => () => { export const deleteReservationCallback = (scheduleIdList) => (dispatch) => {
lunaSend.deleteReservationCallback(scheduleIdList, { lunaSend.deleteReservationCallback(scheduleIdList, {
onSuccess: (res) => { onSuccess: (res) => {
// dispatch(alertToast("success" + JSON.stringify(res))); // dispatch(alertToast("success" + JSON.stringify(res)));
@@ -680,7 +680,7 @@ let updateNetworkStateJob = new Job((dispatch, connected) => {
dispatch(changeAppStatus({ isInternetConnected: connected })); dispatch(changeAppStatus({ isInternetConnected: connected }));
}); });
export const getConnectionStatus = () => (dispatch) => { export const getConnectionStatus = () => (dispatch, getState) => {
lunaSend.getConnectionStatus({ lunaSend.getConnectionStatus({
onSuccess: (res) => { onSuccess: (res) => {
dlog('lunasend getConnectionStatus', res); dlog('lunasend getConnectionStatus', res);
@@ -709,7 +709,7 @@ export const getConnectionStatus = () => (dispatch) => {
}; };
// macAddress // macAddress
export const getConnectionInfo = () => (dispatch) => { export const getConnectionInfo = () => (dispatch, getState) => {
lunaSend.getConnectionInfo({ lunaSend.getConnectionInfo({
onSuccess: (res) => { onSuccess: (res) => {
dlog('lunasend getConnectionStatus', res); dlog('lunasend getConnectionStatus', res);
@@ -731,7 +731,7 @@ export const getConnectionInfo = () => (dispatch) => {
}); });
}; };
export const disableNotification = () => { export const disableNotification = () => (dispatch, getState) => {
lunaSend.disableNotification({ lunaSend.disableNotification({
onSuccess: (res) => { onSuccess: (res) => {
dlog('lunasend disable notification success', res); dlog('lunasend disable notification success', res);
@@ -745,7 +745,7 @@ export const disableNotification = () => {
}); });
}; };
export const enableNotification = () => { export const enableNotification = () => (dispatch, getState) => {
lunaSend.enableNotification({ lunaSend.enableNotification({
onSuccess: (res) => { onSuccess: (res) => {
dlog('lunasend enable notification success', res); dlog('lunasend enable notification success', res);

View File

@@ -4,6 +4,8 @@ import { types } from './actionTypes';
import { changeAppStatus, getTermsAgreeYn } from './commonActions'; import { changeAppStatus, getTermsAgreeYn } from './commonActions';
import { collectBannerPositions } from '../utils/domUtils'; import { collectBannerPositions } from '../utils/domUtils';
import { createDebugHelpers } from '../utils/debug'; import { createDebugHelpers } from '../utils/debug';
import { setHidePopup, setShowPopup } from './commonActions';
import { ACTIVE_POPUP } from '../utils/Config';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = false;
@@ -75,6 +77,38 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
const onFail = (error) => { const onFail = (error) => {
derror('getHomeTerms 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( TAxios(

View File

@@ -6,7 +6,7 @@ import { updateHomeInfo } from './homeActions';
import { createDebugHelpers } from '../utils/debug'; import { createDebugHelpers } from '../utils/debug';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = true;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// 시작 메뉴 추적을 위한 상수 // 시작 메뉴 추적을 위한 상수
@@ -39,10 +39,19 @@ export const pushPanel = (panel, duplicatable = false) => ({
duplicatable: duplicatable, duplicatable: duplicatable,
}); });
export const popPanel = (panelName) => ({ export const popPanel = (panelName) => {
type: types.POP_PANEL, if (DEBUG_MODE) {
payload: panelName, 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) => ({ export const updatePanel = (panelInfo) => ({
type: types.UPDATE_PANEL, type: types.UPDATE_PANEL,
@@ -93,6 +102,11 @@ export const navigateToDetail = ({
...additionalInfo, ...additionalInfo,
}; };
const state = getState();
const panels = state.panels.panels;
// 선택적 파라미터들 추가 // 선택적 파라미터들 추가
if (curationId) panelInfo.curationId = curationId; if (curationId) panelInfo.curationId = curationId;
if (nowShelf) panelInfo.nowShelf = nowShelf; if (nowShelf) panelInfo.nowShelf = nowShelf;
@@ -176,8 +190,21 @@ export const navigateToDetail = ({
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false; 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 비디오가 재생 중이면 정지 // HomeBanner의 modal=true 비디오가 재생 중이면 정지
if (isCurrentBannerVideoPlaying) { 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'); const { finishVideoPreview } = require('./playActions');
@@ -204,11 +231,15 @@ export const navigateToDetail = ({
}) })
); );
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제) // 비디오 상태 저장 후 정지
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
} else { } else {
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장 // 비디오가 재생 중이 아니어도 HomePanel 상태 저장
console.log('[Detail-BG] ⏭️ navigateToDetail - HomeBanner video is NOT playing (modal === false or undefined)', {
playerPanelModal: playerPanelInfo.panelInfo?.modal,
sourceMenu,
timestamp: Date.now(),
});
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.HOME_PANEL, name: panel_names.HOME_PANEL,
@@ -267,10 +298,34 @@ export const navigateToDetail = ({
case SOURCE_MENUS.PLAYER_MEDIA: { case SOURCE_MENUS.PLAYER_MEDIA: {
// PlayerPanel에서 온 경우 // PlayerPanel에서 온 경우
const { hidePlayerOverlays } = require('./videoPlayActions'); 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 오버레이 숨김 // DetailPanel push 전에 VideoPlayer 오버레이 숨김
dispatch(hidePlayerOverlays()); 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) { if (Object.keys(focusSnapshot).length > 0) {
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId; panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
@@ -458,11 +513,28 @@ export const restoreVideoOnBack = () => {
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL); const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore; 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) { 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; 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'); 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( dispatch(
updatePanel({ updatePanel({

View File

@@ -73,6 +73,16 @@ export const startVideoPlayer =
...rest ...rest
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim();
console.log('[PTRACE-SP] startVideoPlayer call', {
modal,
modalContainerId,
modalClassName,
videoId,
showUrl,
caller,
});
dlog( dlog(
'[startVideoPlayer] ✅ START - videoId:', '[startVideoPlayer] ✅ START - videoId:',
videoId, videoId,
@@ -105,6 +115,9 @@ export const startVideoPlayer =
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { 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'); dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
clearAllVideoTimers(); clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL)); dispatch(popPanel(panel_names.PLAYER_PANEL));
@@ -182,6 +195,17 @@ export const startVideoPlayerNew =
...rest ...rest
}) => }) =>
(dispatch, getState) => { (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( dlog(
'[startVideoPlayerNew] *** ✅ START - bannerId:', '[startVideoPlayerNew] *** ✅ START - bannerId:',
bannerId, bannerId,
@@ -215,6 +239,9 @@ export const startVideoPlayerNew =
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { 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'); dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
clearAllVideoTimers(); clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL)); dispatch(popPanel(panel_names.PLAYER_PANEL));
@@ -325,6 +352,12 @@ export const finishVideoPreview = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) { 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) { if (startVideoFocusTimer) {
clearTimeout(startVideoFocusTimer); clearTimeout(startVideoFocusTimer);
startVideoFocusTimer = null; startVideoFocusTimer = null;
@@ -384,6 +417,13 @@ export const pauseModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal (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 (modalPlayerPanel) {
if (DEBUG_MODE === true) { if (DEBUG_MODE === true) {
dlog('[pauseModalVideo] Pausing modal video'); 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 (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 (modalPlayerPanel && modalPlayerPanel.panelInfo?.isPaused) {
if (DEBUG_MODE === true) { if (DEBUG_MODE === true) {
dlog('[resumeModalVideo] Resuming modal video'); 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 (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) { if (fullscreenPlayerPanel) {
dispatch( dispatch(
updatePanel({ 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) => { export const resumeFullscreenVideo = () => (dispatch, getState) => {
const panels = getState().panels.panels; 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인 패널) // 전체화면 PlayerPanel 찾기 (modal이 false인 패널)
const fullscreenPlayerPanel = panels.find( const fullscreenPlayerPanel = panels.find(
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
); );
// console.log('[BgVideo] resumeFullscreenVideo - fullscreenPlayerPanel found:', !!fullscreenPlayerPanel); console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
// console.log('[BgVideo] resumeFullscreenVideo - isPaused:', fullscreenPlayerPanel?.panelInfo?.isPaused); found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(),
});
if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) { if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) {
// console.log('[BgVideo] resumeFullscreenVideo - dispatching updatePanel with isPaused: false');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, 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 { } 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) { if (DEBUG_MODE === true) {
dlog('[BgVideo] resumeFullscreenVideo - Not resuming (not found or not paused)'); 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) => { 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로 변경하여 전체화면으로 전환 // 공유 PlayerPanel의 'modal' 상태를 false로 변경하여 전체화면으로 전환
dispatch( dispatch(
updatePanel({ 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 ...rest
} = videoInfo; } = 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; const videoIdentifier = videoId || showUrl || bannerId;
if (videoIdentifier) { if (videoIdentifier) {
@@ -1190,11 +1295,21 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
// 기존 PlayerPanel이 있으면 초기화 // 기존 PlayerPanel이 있으면 초기화
if (existingPlayerPanel) { if (existingPlayerPanel) {
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
existingModalStatus: existingPlayerPanel.panelInfo?.modal,
timestamp: Date.now(),
});
clearAllVideoTimers(); clearAllVideoTimers();
dispatch(popPanel(panel_names.PLAYER_PANEL)); dispatch(popPanel(panel_names.PLAYER_PANEL));
} }
// 새로운 PlayerPanel push // 새로운 PlayerPanel push
console.log('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', {
modal,
modalContainerId,
timestamp: Date.now(),
});
dispatch( dispatch(
pushPanel( 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'); dlog('[startBannerVideo] ✨ Panel action dispatched');
}; };

View File

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

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import { createQueryString } from "../utils/helperMethods"; import { createQueryString } from "../utils/helperMethods";
import { getUrl } from "./apiConfig"; import { getUrl } from "./apiConfig";
import { DEBUG_LOG_MODE, sendToLogServer } from "./logServerClient";
export const TLogEvent = ( export const TLogEvent = (
dispatch, dispatch,
@@ -68,6 +69,23 @@ export const TLogEvent = (
prodCd, 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; let axiosInstance;
switch (type) { switch (type) {

View File

@@ -22,7 +22,7 @@ export const URLS = {
// cart controller // cart controller
ADD_TO_CART: "/lgsp/v1/myinfo/cart/add.lge", ADD_TO_CART: "/lgsp/v1/myinfo/cart/add.lge",
REMOVE_FROM_CART: "/lgsp/v1/myinfo/cart/remove.lge", REMOVE_FROM_CART: "/lgsp/v1/myinfo/cart/remove.lge",
UPDATE_CART_ITEM: "/lgsp/v1/myinfo/cart/update.lge", UPDATE_CART_ITEM: "/lgsp/v1/myinfo/cart/update.lge",
// cart api // cart api
GET_MY_INFO_CART_SEARCH: "/lgsp/v1/myinfo/cart/search.lge", GET_MY_INFO_CART_SEARCH: "/lgsp/v1/myinfo/cart/search.lge",
INSERT_MY_INFO_CART: "/lgsp/v1/myinfo/cart/add.lge", INSERT_MY_INFO_CART: "/lgsp/v1/myinfo/cart/add.lge",
@@ -55,6 +55,8 @@ export const URLS = {
GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge", GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge",
GET_BRAND_SHOWROOM: "/lgsp/v1/brand/showroom.lge", GET_BRAND_SHOWROOM: "/lgsp/v1/brand/showroom.lge",
GET_BRAND_RECENTLY_AIRED: "/lgsp/v1/brand/recently/aired.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 //on-sale controller
GET_ON_SALE_INFO: "/lgsp/v1/onsale/onsale.lge", GET_ON_SALE_INFO: "/lgsp/v1/onsale/onsale.lge",
@@ -146,9 +148,8 @@ export const URLS = {
UPDATE_ORDER_PARTIAL_CANCEL: "/lgsp/v1/myinfo/order/orderPartialCancel.lge", UPDATE_ORDER_PARTIAL_CANCEL: "/lgsp/v1/myinfo/order/orderPartialCancel.lge",
PAYMENT_TOTAL_CANCEL: "/lgsp/v1/myinfo/order/paymentTotalCancel.lge", PAYMENT_TOTAL_CANCEL: "/lgsp/v1/myinfo/order/paymentTotalCancel.lge",
// foryou controller // foryou controller
JUSTFORYOU: "/lgsp/v1/recommend/justforyou.lge", JUSTFORYOU: "/lgsp/v1/recommend/justforyou.lge",
// emp controller // emp controller
GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge", GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge",
@@ -272,11 +273,11 @@ const getRicCode = (country, ricCodeSetting) => {
if (ricCodeSetting !== "system") { if (ricCodeSetting !== "system") {
return ricCodeSetting; return ricCodeSetting;
} }
if (country == "US") { if (country === "US") {
return "aic"; return "aic";
} else if (country == "DE" || country == "GB") { } else if (country === "DE" || country === "GB") {
return "eic"; return "eic";
} else if (country == "RU") { } else if (country === "RU") {
return "ruc"; return "ruc";
} }
return null; 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, { import React, {
useCallback, useCallback,
useEffect,
useMemo, useMemo,
} from 'react'; } from 'react';
@@ -14,10 +15,13 @@ import {
useSelector, useSelector,
} from 'react-redux'; } from 'react-redux';
import Spotlight from '@enact/spotlight';
import { setHidePopup } from '../../actions/commonActions'; import { setHidePopup } from '../../actions/commonActions';
import { getPopupConfig } from '../../constants/popupConfig'; import { getPopupConfig } from '../../constants/popupConfig';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import TPopUp from '../TPopUp/TPopUp'; import TPopUp from '../TPopUp/TPopUp';
import TopBannerPopup from '../../views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup';
// 커스텀 훅: 팝업 상태 관리 // 커스텀 훅: 팝업 상태 관리
const useGlobalPopupState = () => { const useGlobalPopupState = () => {
@@ -128,9 +132,28 @@ const GlobalPopup = () => {
secondaryData secondaryData
} = useGlobalPopupState(); } = useGlobalPopupState();
const [imageDimensions, setImageDimensions] = React.useState({ width: 0, height: 0 });
const handlers = usePopupCloseHandlers(); const handlers = usePopupCloseHandlers();
const previousPopupVisible = usePrevious(popupVisible); 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(() => { const currentConfig = useMemo(() => {
if (!activePopup) return null; if (!activePopup) return null;
@@ -214,6 +237,53 @@ const GlobalPopup = () => {
return null; 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) { if (!currentConfig) {
console.warn(`No configuration found for popup type: ${activePopup}`); console.warn(`No configuration found for popup type: ${activePopup}`);

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TNewPopUp from '../TPopUp/TNewPopUp'; // TNewPopUp 컴포넌트의 정확한 경로를 확인해주세요. import TNewPopUp from '../TPopUp/TNewPopUp'; // TNewPopUp 컴포넌트의 정확한 경로를 확인해주세요.
import css from './OptionalConfirm.module.less'; import css from './OptionalConfirm.module.less';
@@ -16,7 +18,7 @@ const OptionalConfirm = ({
onOptionalTermsClick, // 약관 자세히 보기 버튼 클릭 핸들러 onOptionalTermsClick, // 약관 자세히 보기 버튼 클릭 핸들러
onOptionalAgreeClick, // 동의 버튼 클릭 핸들러 onOptionalAgreeClick, // 동의 버튼 클릭 핸들러
onOptionalDeclineClick, // 거절 또는 다음에 하기 버튼 클릭 핸들러 onOptionalDeclineClick, // 거절 또는 다음에 하기 버튼 클릭 핸들러
customPosition, customPosition,
position, position,
}) => { }) => {
return ( return (
@@ -31,7 +33,7 @@ const OptionalConfirm = ({
onOptionalAgreeClick={onOptionalAgreeClick} onOptionalAgreeClick={onOptionalAgreeClick}
onOptionalDeclineClick={onOptionalDeclineClick} onOptionalDeclineClick={onOptionalDeclineClick}
customPosition={customPosition} customPosition={customPosition}
position={position} position={position}
/> />
); );
}; };

View File

@@ -1,14 +1,26 @@
// src/components/Optional/OptionalTermsConfirm.jsx // src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback, useState } from 'react'; import React, {
import { useDispatch, useSelector } from 'react-redux'; useCallback,
import TPopUp from '../TPopUp/TPopUp'; useState,
import TButton from '../TButton/TButton'; } from 'react';
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
import TButtonScroller from '../TButtonScroller/TButtonScroller'; import {
import { $L, scaleH, scaleW } from '../../utils/helperMethods'; useDispatch,
useSelector,
} from 'react-redux';
import { setHidePopup } from '../../actions/commonActions'; import { setHidePopup } from '../../actions/commonActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions'; 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'; import css from './OptionalTermsConfirm.module.less';
const OptionalTermsConfirm = ({ open }) => { const OptionalTermsConfirm = ({ open }) => {
@@ -17,9 +29,8 @@ const OptionalTermsConfirm = ({ open }) => {
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false); const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false); const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
const optionalTermsData = useSelector((state) => const optionalTermsData = useSelector((state) =>
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
); );
@@ -46,7 +57,7 @@ const OptionalTermsConfirm = ({ open }) => {
if (isChecked) { if (isChecked) {
// 약관 동의할 항목들 (string array) // 약관 동의할 항목들 (string array)
const termsList = ["TID0000222", "TID0000223", "TID0000232"]; const termsList = ["TID0000222", "TID0000223", "TID0000232"];
// 동의하지 않을 항목들 (빈 배열) // 동의하지 않을 항목들 (빈 배열)
const notTermsList = []; const notTermsList = [];
@@ -67,15 +78,15 @@ const OptionalTermsConfirm = ({ open }) => {
setIsWarningPopupVisible(true); setIsWarningPopupVisible(true);
} }
}, [isChecked, dispatch]); }, [isChecked, dispatch]);
const handleCloseWarningPopup = useCallback(() => { const handleCloseWarningPopup = useCallback(() => {
setIsWarningPopupVisible(false); setIsWarningPopupVisible(false);
}, []); }, []);
const handleDontAskAgain = () => { const handleDontAskAgain = useCallback(() => {
console.log("Don't Ask Again 처리 필요"); console.log("Don't Ask Again 처리 필요");
dispatch(setHidePopup()); dispatch(setHidePopup());
}; },[dispatch]);
if (isTermsPopupVisible) { if (isTermsPopupVisible) {
return ( return (
@@ -144,7 +155,7 @@ const OptionalTermsConfirm = ({ open }) => {
<div className={css.mainContent}> <div className={css.mainContent}>
<div className={css.checkboxSection}> <div className={css.checkboxSection}>
<div className={css.checkboxArea}> <div className={css.checkboxArea}>
<TCheckBoxSquare <TCheckBoxSquare
selected={isChecked} selected={isChecked}
onToggle={handleCheckboxToggle} onToggle={handleCheckboxToggle}
spotlightId="optional-checkbox" spotlightId="optional-checkbox"
@@ -157,34 +168,34 @@ const OptionalTermsConfirm = ({ open }) => {
type="terms" type="terms"
ariaLabel={$L("View Optional Terms")} ariaLabel={$L("View Optional Terms")}
> >
<div className={css.termTitle}>Optional Terms</div> <div className={css.termTitle}>Optional Terms</div>
</TButton> </TButton>
</div> </div>
</div> </div>
<div className={css.descriptionSection}> <div className={css.descriptionSection}>
<div className={css.description}> <div className={css.description}>
Get recommendations, special offers, and ads tailored just for you. Get recommendations, special offers, and ads tailored just for you.
</div> </div>
</div> </div>
<div className={css.buttonSection}> <div className={css.buttonSection}>
<div className={css.buttonGroup}> <div className={css.buttonGroup}>
<TButton <TButton
onClick={handleAgree} onClick={handleAgree}
spotlightId="agree-button" spotlightId="agree-button"
className={css.agreeButton} className={css.agreeButton}
> >
{$L('Agree')} {$L('Agree')}
</TButton> </TButton>
<TButton <TButton
onClick={handleMainPopupClose} onClick={handleMainPopupClose}
spotlightId="not-now-button" spotlightId="not-now-button"
className={css.notNowButton} className={css.notNowButton}
> >
{$L('Not Now')} {$L('Not Now')}
</TButton> </TButton>
<TButton <TButton
onClick={handleDontAskAgain} onClick={handleDontAskAgain}
spotlightId="dont-ask-button" spotlightId="dont-ask-button"
className={css.dontAskButton} className={css.dontAskButton}

View File

@@ -1,17 +1,28 @@
// src/components/Optional/OptionalTermsConfirm.jsx // src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback, useState } from 'react'; import React, {
import { useDispatch, useSelector } from 'react-redux'; useCallback,
import TNewPopUp from '../TPopUp/TNewPopUp'; useEffect,
import TButton from '../TButton/TButton'; useState,
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare'; } from 'react';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import { $L, scaleH, scaleW } from '../../utils/helperMethods'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import { setHidePopup } from '../../actions/commonActions'; import { setHidePopup } from '../../actions/commonActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions'; 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 css from './OptionalTermsConfirmBottom.module.less';
import cssPopup from '../TPopUp/TNewPopUp.module.less';
import Spotlight from "@enact/spotlight";
const OptionalTermsConfirm = ({ open }) => { const OptionalTermsConfirm = ({ open }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -19,9 +30,9 @@ const OptionalTermsConfirm = ({ open }) => {
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false); const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false); const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
const optionalTermsData = useSelector((state) =>
const optionalTermsData = useSelector((state) =>
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
); );
@@ -37,13 +48,13 @@ const OptionalTermsConfirm = ({ open }) => {
} }
}, [open, isTermsPopupVisible, isWarningPopupVisible]); }, [open, isTermsPopupVisible, isWarningPopupVisible]);
const handleMainPopupClose = useCallback(() => { // const handleMainPopupClose = useCallback(() => {
dispatch(setHidePopup()); // dispatch(setHidePopup());
}, [dispatch]); // }, [dispatch]);
const handleCheckboxToggle = useCallback(({ selected }) => { // const handleCheckboxToggle = useCallback(({ selected }) => {
setIsChecked(selected); // setIsChecked(selected);
}, []); // }, []);
const handleViewTermsClick = useCallback(() => { const handleViewTermsClick = useCallback(() => {
setIsTermsPopupVisible(true); setIsTermsPopupVisible(true);
@@ -66,20 +77,20 @@ const OptionalTermsConfirm = ({ open }) => {
const handleAgreeTest = useCallback(() => { const handleAgreeTest = useCallback(() => {
console.log("handleAgreeTest"); console.log("handleAgreeTest");
Spotlight.pause(); Spotlight.pause();
setIsTermsPopupVisible(false); setIsTermsPopupVisible(false);
// 상태 업데이트 후 DOM이 완전히 렌더링될 때까지 기다린 후 포커스 // 상태 업데이트 후 DOM이 완전히 렌더링될 때까지 기다린 후 포커스
setTimeout(() => { setTimeout(() => {
Spotlight.resume(); Spotlight.resume();
Spotlight.focus("optional-terms-confirm-popup"); Spotlight.focus("optional-terms-confirm-popup");
}, 500); // 50ms에서 100ms로 증가 }, 500); // 50ms에서 100ms로 증가
}, []); }, []);
const handleAgree = useCallback(() => { const handleAgree = useCallback(() => {
if (isChecked) { if (isChecked) {
// 약관 동의할 항목들 (string array) // 약관 동의할 항목들 (string array)
const termsList = ["TID0000222", "TID0000223", "TID0000232"]; const termsList = ["TID0000222", "TID0000223", "TID0000232"];
// 동의하지 않을 항목들 (빈 배열) // 동의하지 않을 항목들 (빈 배열)
const notTermsList = []; const notTermsList = [];
@@ -163,7 +174,7 @@ const OptionalTermsConfirm = ({ open }) => {
return ( return (
<TNewPopUp <TNewPopUp
kind="optionalConfirm" kind="optionalConfirm"
open={open} open={open}
spotlightId="optional-terms-confirm-popup" spotlightId="optional-terms-confirm-popup"
spotlightRestrict="self-only" spotlightRestrict="self-only"
className={css.optionalConfirmPopup} className={css.optionalConfirmPopup}

View File

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

View File

@@ -10,7 +10,7 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
position: relative;
.title { .title {
width: 1788px; width: 1788px;
font-size: 42px; font-size: 42px;
@@ -42,3 +42,24 @@
box-shadow: 0px 6px 30px 0 rgba(0, 0, 0, 0.4); 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 { > img {
.size(@w: inherit, @h: inherit); .size(@w: inherit, @h: inherit);
object-fit: cover; object-fit: cover;
border: solid 1px #f0f0f0; // border: solid 1px #f0f0f0;
} }
// discount rate // discount rate
@@ -144,7 +144,7 @@
> img { > img {
.size(@w: 288px, @h: 288px); .size(@w: 288px, @h: 288px);
object-fit: contain; object-fit: contain;
border: solid 1px #f0f0f0; // border: solid 1px #f0f0f0;
} }
// discount rate // 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 { getDeviceAdditionInfo } from '../../actions/deviceActions';
import { scaleH, scaleW } from "../../utils/helperMethods"; import {
scaleH,
scaleW,
} from '../../utils/helperMethods';
export default function TQRCode({ export default function TQRCode({
isBillingProductVisible, isBillingProductVisible,
@@ -51,6 +60,6 @@ export default function TQRCode({
correctLevel: window.QRCode.CorrectLevel.L, correctLevel: window.QRCode.CorrectLevel.L,
}); });
} }
}, [text, deviceInfo, entryMenu, nowMenu]); }, [text, deviceInfo, entryMenu, nowMenu, width, height]);
return <div aria-label={ariaLabel} ref={qrcodeRef} />; 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 classNames from 'classnames';
import compose from 'ramda/src/compose'; import compose from 'ramda/src/compose';
import { useDispatch } from 'react-redux';
import { Job } from '@enact/core/util'; 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 Spottable from '@enact/spotlight/Spottable';
import css from './TabItemSub.module.less';
import { sendLogTotalRecommend } from '../../actions/logActions'; import { sendLogTotalRecommend } from '../../actions/logActions';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from '../../utils/Config'; import {
import { useDispatch } from 'react-redux'; LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
} from '../../utils/Config';
import css from './TabItemSub.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
@@ -122,11 +133,11 @@ const TabItemBase = ({
<> <>
{subtitle && ( {subtitle && (
<div className={css.textWithIcon}> <div className={css.textWithIcon}>
{IconComponent && ( {/* {IconComponent && (
<span className={css.iconWrapper}> <span className={css.iconWrapper}>
<IconComponent iconType={focused ? 'focused' : selected ? 'selected' : 'normal'} /> <IconComponent iconType={focused ? 'focused' : selected ? 'selected' : 'normal'} />
</span> </span>
)} )} */}
<Marquee <Marquee
marqueeDisabled={!focused} marqueeDisabled={!focused}
marqueeOn={'focus'} marqueeOn={'focus'}

View File

@@ -172,23 +172,8 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
break; break;
//브랜드 //브랜드
case 10300: case 10300:
result = [ result =
// NBCU 브랜드 (하드코딩) data?.shortFeaturedBrands?.map((item) => ({
{
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) => ({
icons: FeaturedBrandIcon, icons: FeaturedBrandIcon,
id: item.patnrId, id: item.patnrId,
path: item.patncLogoPath, path: item.patncLogoPath,
@@ -200,8 +185,7 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
panelInfo: { from: 'gnb', patnrId: item.patnrId }, panelInfo: { from: 'gnb', patnrId: item.patnrId },
}, },
], ],
})) || []), })) || [];
];
break; break;
// //
case 10600: 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 // 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. // 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) { if (this.props.autoCloseTimeout && !this.props.sideContentsVisible) {
this.autoCloseJob.startAfter(this.props.autoCloseTimeout); this.autoCloseJob.startAfter(this.props.autoCloseTimeout);
} }

View File

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

View File

@@ -16,7 +16,7 @@ import { calculateIsPanelOnTop } from '../utils/panelUtils'; // 🎯 isOnTop 유
// DEBUG_MODE - true인 경우에만 로그 출력 // DEBUG_MODE - true인 경우에만 로그 출력
// ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단 // ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단
const DEBUG_MODE = false; const DEBUG_MODE = true;
/** /**
* Panel history middleware * Panel history middleware
@@ -106,7 +106,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After PUSH_PANEL:', { console.log(`[PANEL_HISTORY] PUSH_PANEL: ${panelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -125,6 +125,14 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
// POP 후 top panel을 기록 (이전 패널로 돌아감) // POP 후 top panel을 기록 (이전 패널로 돌아감)
if (panels.length > 0) { if (panels.length > 0) {
const topPanel = panels[panels.length - 1]; 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) { if (topPanel && topPanel.name) {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산
@@ -154,7 +162,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After POP_PANEL:', { console.log(`[PANEL_HISTORY] POP_PANEL: ${topPanel.name}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -202,7 +210,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After UPDATE_PANEL:', { console.log(`[PANEL_HISTORY] UPDATE_PANEL: ${panelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -284,7 +292,10 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After RESET_PANELS:', { const resetPanelName = (action.payload && action.payload.length > 0)
? action.payload[0].name
: 'homepanel';
console.log(`[PANEL_HISTORY] RESET_PANELS: ${resetPanelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });

View File

@@ -44,6 +44,17 @@ const initialState = {
brandRecentlyAiredData: { brandRecentlyAiredData: {
data: {}, data: {},
}, },
brandShopByShowData: {
data: {},
},
brandTopBannerData: {
data: {},
},
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 저장소
brandShopByShowCategoryGroups: {},
}; };
export const brandReducer = (state = initialState, action) => { export const brandReducer = (state = initialState, action) => {
@@ -155,6 +166,51 @@ export const brandReducer = (state = initialState, action) => {
brandRecentlyAiredData: action.payload, 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: case types.SET_BRAND_LIVE_CHANNEL_UPCOMING:
return { return {
...state, ...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: default:
return state; return state;
} }

View File

@@ -23,7 +23,7 @@ const initialState = {
* Cart Reducer * Cart Reducer
* 장바구니 관련 상태를 관리합니다. * 장바구니 관련 상태를 관리합니다.
*/ */
export const cartReducer = (state = initialState, action) => { export const cartReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
// 장바구니 조회 - API에서 가져온 전체 목록 // 장바구니 조회 - API에서 가져온 전체 목록
case types.GET_MY_INFO_CART_SEARCH: case types.GET_MY_INFO_CART_SEARCH:
@@ -37,9 +37,9 @@ export const cartReducer = (state = initialState, action) => {
}; };
// 장바구니에 상품 추가 // 장바구니에 상품 추가
case types.INSERT_MY_INFO_CART: case types.INSERT_MY_INFO_CART:
return { return {
...state, ...state,
selectCart: { selectCart: {
...state.selectCart, ...state.selectCart,
cartList: [...state.selectCart.cartList, action.payload], cartList: [...state.selectCart.cartList, action.payload],
@@ -51,22 +51,22 @@ export const cartReducer = (state = initialState, action) => {
case types.TOGGLE_CHECK_CART: { case types.TOGGLE_CHECK_CART: {
const checkedItem = action.payload.item; const checkedItem = action.payload.item;
const isChecked = action.payload.isChecked; const isChecked = action.payload.isChecked;
let updatedCheckedList = state.selectCart?.checkedItems || []; let updatedCheckedList = state.selectCart?.checkedItems || [];
if (isChecked) { if (isChecked) {
const itemExists = updatedCheckedList.some( const itemExists = updatedCheckedList.some(
item => item.prodSno === checkedItem.prodSno item => item.prodSno === checkedItem.prodSno
); );
if (!itemExists) { if (!itemExists) {
updatedCheckedList = [...updatedCheckedList, checkedItem]; updatedCheckedList = [...updatedCheckedList, checkedItem];
} }
} else { } else {
updatedCheckedList = updatedCheckedList.filter( updatedCheckedList = updatedCheckedList.filter(
item => item.prodSno !== checkedItem.prodSno item => item.prodSno !== checkedItem.prodSno
); );
} }
return { return {
...state, ...state,
selectCart: { selectCart: {
@@ -78,7 +78,7 @@ export const cartReducer = (state = initialState, action) => {
} }
// 장바구니에서 상품 삭제 // 장바구니에서 상품 삭제
case types.DELETE_MY_INFO_CART: case types.DELETE_MY_INFO_CART:
return { return {
...state, ...state,
getMyinfoCartSearch: { getMyinfoCartSearch: {
@@ -96,7 +96,7 @@ export const cartReducer = (state = initialState, action) => {
getMyinfoCartSearch: { getMyinfoCartSearch: {
...state.getMyinfoCartSearch, ...state.getMyinfoCartSearch,
cartList: (state.getMyinfoCartSearch.cartList || []).map(item => cartList: (state.getMyinfoCartSearch.cartList || []).map(item =>
item.prodSno === action.payload.prodSno item.prodSno === action.payload.prodSno
? { ...item, ...action.payload } ? { ...item, ...action.payload }
: item : item
), ),

View File

@@ -132,6 +132,60 @@ export const panelsReducer = (state = initialState, action) => {
case types.UPDATE_PANEL: { case types.UPDATE_PANEL: {
let lastIndex = -1; let lastIndex = -1;
let lastAction = 'update'; 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--) { for (let i = state.panels.length - 1; i >= 0; i--) {
if (state.panels[i].name === action.payload.name) { if (state.panels[i].name === action.payload.name) {
@@ -143,7 +197,7 @@ export const panelsReducer = (state = initialState, action) => {
index === lastIndex index === lastIndex
? { ? {
...panel, ...panel,
panelInfo: { ...panel.panelInfo, ...action.payload.panelInfo }, panelInfo: { ...panel.panelInfo, ...nextPanelInfo },
} }
: panel : panel
); );

View File

@@ -20,7 +20,10 @@ import {
setShowPopup, setShowPopup,
} from '../../../actions/commonActions'; } from '../../../actions/commonActions';
import { sendLogTotalRecommend } from '../../../actions/logActions'; import { sendLogTotalRecommend } from '../../../actions/logActions';
import { popPanel } from '../../../actions/panelActions'; import {
popPanel,
pushPanel,
} from '../../../actions/panelActions';
import TButton from '../../../components/TButton/TButton'; import TButton from '../../../components/TButton/TButton';
import TPopUp from '../../../components/TPopUp/TPopUp'; import TPopUp from '../../../components/TPopUp/TPopUp';
import TQRCode from '../../../components/TQRCode/TQRCode'; import TQRCode from '../../../components/TQRCode/TQRCode';
@@ -249,13 +252,25 @@ export default function InformationContainer({
); );
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
dispatch(setHidePopup()); // dispatch(setHidePopup());
}, [dispatch]); const { patnrId, prdtId } = checkoutData.productList[0];
const handleDone = useCallback(() => {
dispatch(setHidePopup());
dispatch(popPanel()); 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 ( return (
<> <>
@@ -282,8 +297,8 @@ export default function InformationContainer({
> >
ADD/EDIT ADD/EDIT
</TButton> </TButton>
{checkoutData?.shippingAddressList && ( {shippingAddressList && (
<ShippingAddressCard list={checkoutData.shippingAddressList} onFocus={handleFocus} /> <ShippingAddressCard list={shippingAddressList} onFocus={handleFocus} />
)} )}
</div> </div>
<div className={css.listBox}> <div className={css.listBox}>
@@ -300,8 +315,8 @@ export default function InformationContainer({
{/* <div style={{ padding: '10px', textAlign: 'center', color: '#999' }}> {/* <div style={{ padding: '10px', textAlign: 'center', color: '#999' }}>
Mock Billing Address Mock Billing Address
</div> */} </div> */}
{checkoutData?.billingAddressList && ( {billingAddressList && (
<BillingAddressCard list={checkoutData.billingAddressList} onFocus={handleFocus} /> <BillingAddressCard list={billingAddressList} onFocus={handleFocus} />
)} )}
</div> </div>
<div className={css.listBox}> <div className={css.listBox}>
@@ -314,7 +329,7 @@ export default function InformationContainer({
> >
ADD/EDIT ADD/EDIT
</TButton> </TButton>
{checkoutData?.cardInfo && <PaymentCard list={checkoutData.cardInfo} />} {cardInfo && <PaymentCard list={cardInfo} />}
</div> </div>
<div className={css.listBox}> <div className={css.listBox}>
<Subject title="OFFERS & PROMOTION" /> <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' '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> </h3>
<TButton className={css.popupBtn} onClick={handleDone}> <TButton className={css.popupBtn} onClick={handleCancel}>
{$L('OK')} {$L('OK')}
</TButton> </TButton>
</div> </div>

View File

@@ -303,6 +303,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel; const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu; 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되는 시점 // DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount:', { console.log('[DetailPanel] unmount:', {
sourcePanel, sourcePanel,
@@ -323,6 +331,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
detailPanelClosedAt: Date.now(), // ✅ 시점 기록 detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처 detailPanelClosedFromSource: sourceMenu, // ✅ 출처
lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달 lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달
lockModalFalse: false, // Detail 종료 시 lock 해제
}, },
}) })
); );
@@ -1025,34 +1034,56 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
// ProductAllSection에 비디오가 있는지 확인 // ProductAllSection에 비디오가 있는지 확인
const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)(); 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, hasPlayerPanel,
isModal, playerPanelModalStatus: isModal,
hasProductVideo, hasProductVideo,
sourceMenu: panelInfo?.sourceMenu, sourceMenu: panelInfo?.sourceMenu,
productDataUrl: productData?.prdtMediaUrl,
timestamp: Date.now(),
}); });
// PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤 // PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
if (hasPlayerPanel && hasProductVideo) { 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) { if (isModal) {
dispatch(pauseModalVideo()); dispatch(pauseModalVideo());
} else { } else {
dispatch(pauseFullscreenVideo()); dispatch(pauseFullscreenVideo());
} }
} else { } 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 () => { return () => {
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개 // DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
if (hasPlayerPanel && hasProductVideo) { 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) { if (isModal) {
dispatch(resumeModalVideo()); dispatch(resumeModalVideo());
} else { } else {
dispatch(resumeFullscreenVideo()); 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 // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1080,12 +1111,33 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const hasPlayerPanel = panels.some( const hasPlayerPanel = panels.some(
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true
); );
const launchedFromPlayer = panelInfo?.fromPlayer || panelInfo?.sourcePanel === panel_names.PLAYER_PANEL;
if (hasPlayerPanel) { if (hasPlayerPanel && !launchedFromPlayer) {
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview'); console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview (non-player source)');
dispatch(finishVideoPreview()); 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 ( return (
<div ref={containerRef}> <div ref={containerRef}>

View File

@@ -112,6 +112,8 @@ import {
} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx'; } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo import ProductVideo
from '../ProductContentSection/ProductVideo/ProductVideo.v3'; from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import SeeMoreProducts
from '../ProductContentSection/SeeMoreProducts/SeeMoreProducts';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews'; import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton'; // import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
import YouMayAlsoLike import YouMayAlsoLike
@@ -267,6 +269,10 @@ export default function ProductAllSection({
const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData); const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
const { popupVisible, activePopup } = useSelector((state) => state.common.popup); const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible); const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible);
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터
const brandShopByShowCategoryGroups = useSelector(
(state) => state.brand.brandShopByShowCategoryGroups
);
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략) // ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(1); const [productVideoVersion, setProductVideoVersion] = useState(1);
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화) // 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
@@ -274,7 +280,6 @@ export default function ProductAllSection({
// const [currentHeight, setCurrentHeight] = useState(0); // const [currentHeight, setCurrentHeight] = useState(0);
//하단부분까지 갔을때 체크용 //하단부분까지 갔을때 체크용
const [documentHeight, setDocumentHeight] = useState(0);
const [isBottom, setIsBottom] = useState(false); const [isBottom, setIsBottom] = useState(false);
//qr코드 노출용 //qr코드 노출용
@@ -283,6 +288,8 @@ export default function ProductAllSection({
// sendLogGNB용 entryMenu // sendLogGNB용 entryMenu
const entryMenuRef = useRef(null); const entryMenuRef = useRef(null);
const lastProductDetailLogKeyRef = useRef(null);
const lastGnbLogKeyRef = useRef(null);
// 출처 정보 통합 (향후 확장성 대비) // 출처 정보 통합 (향후 확장성 대비)
// YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지 // YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지
@@ -298,6 +305,7 @@ export default function ProductAllSection({
// 모든 timeout/timer를 추적하기 위한 ref // 모든 timeout/timer를 추적하기 위한 ref
const timersRef = useRef([]); const timersRef = useRef([]);
const contentHeightRef = useRef(0);
// handleScrollToImages의 timeout을 추적하기 위한 ref // handleScrollToImages의 timeout을 추적하기 위한 ref
const scrollToImagesTimeoutRef = useRef(null); const scrollToImagesTimeoutRef = useRef(null);
@@ -322,15 +330,20 @@ export default function ProductAllSection({
if (!container) return; if (!container) return;
if (typeof container.scrollTo === 'function') { if (typeof container.scrollTo === 'function') {
scrollTop({ y: 0, animate: true }); scrollTop({ y: 0, animate: true });
const timeOut = setTimeout(()=>{ setTimeout(()=>{
Spotlight.focus("product-detail-container-0"); if(hasVideo){
Spotlight.focus("product-video-player");
} else {
Spotlight.focus("product-detail-container-0");
}
},100); },100);
} }
}, [ }, [
scrollTop scrollTop,
hasVideo
]); ]);
useEffect(() => { const fetchCouponData = useCallback(() => {
dispatch( dispatch(
getProductCouponSearch({ getProductCouponSearch({
patnrId: selectedPatnrId, patnrId: selectedPatnrId,
@@ -338,7 +351,14 @@ export default function ProductAllSection({
mbrNo: userNumber, mbrNo: userNumber,
}) })
); );
}, [dispatch]); }, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]);
useEffect(() => {
// 필수 값이 모두 있을 때만 호출
if (selectedPatnrId && selectedPrdtId) {
fetchCouponData();
}
}, [fetchCouponData]);
const getCouponCode = () => { const getCouponCode = () => {
const snoArray = []; const snoArray = [];
@@ -378,7 +398,7 @@ export default function ProductAllSection({
setCouponTypes(idx); setCouponTypes(idx);
dispatch(setShowPopup(Config.ACTIVE_POPUP.couponPopup)); dispatch(setShowPopup(Config.ACTIVE_POPUP.couponPopup));
}, },
[dispatch, popupVisible, promotions, userNumber] [dispatch, userNumber, partnerCoupon]
); );
const handleCouponTotDownload = useCallback(() => { const handleCouponTotDownload = useCallback(() => {
@@ -507,7 +527,7 @@ export default function ProductAllSection({
)} )}
aria-label="Download Button" aria-label="Download Button"
> >
{downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) {downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && duplDwldYn === 'N'
? $L('DOWNLOAD COMPLETED') ? $L('DOWNLOAD COMPLETED')
: $L('DOWNLOAD')} : $L('DOWNLOAD')}
</div> </div>
@@ -550,6 +570,21 @@ export default function ProductAllSection({
[productType, themeProductInfo, themeProducts, selectedIndex, productInfo] [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와 동일한 로직 // 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
const isBillingProductVisible = useMemo(() => { const isBillingProductVisible = useMemo(() => {
// API Mode: 기존 로직 100% 유지 (절대 수정 안 함) // API Mode: 기존 로직 100% 유지 (절대 수정 안 함)
@@ -721,6 +756,7 @@ export default function ProductAllSection({
// sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴 // sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴
useEffect(() => { useEffect(() => {
if (!productData?.prdtId) return;
if (!entryMenuRef.current) entryMenuRef.current = nowMenu; if (!entryMenuRef.current) entryMenuRef.current = nowMenu;
// BUY NOW 버튼 활성화 상태에 따른 메뉴 결정 (Source SingleProduct vs UnableProduct 패턴) // BUY NOW 버튼 활성화 상태에 따른 메뉴 결정 (Source SingleProduct vs UnableProduct 패턴)
@@ -742,55 +778,88 @@ export default function ProductAllSection({
? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}` ? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}`
: baseMenu; : baseMenu;
const logKey = `${productData?.patnrId || ''}-${productData?.prdtId || ''}`;
if (lastGnbLogKeyRef.current === logKey) {
return;
}
lastGnbLogKeyRef.current = logKey;
dispatch(sendLogGNB(menu)); dispatch(sendLogGNB(menu));
}, [
isBillingProductVisible,
isGroupProductVisible,
isTravelProductVisible,
fromPanel?.fromYouMayLike,
productData?.patnrId,
productData?.prdtId,
nowMenu,
]);
// sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화) // sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화)
if (fromPanel?.fromYouMayLike === true) { useEffect(() => {
dispatch(updatePanel({ if (fromPanel?.fromYouMayLike === true) {
name: panel_names.DETAIL_PANEL, dispatch(updatePanel({
panelInfo: { name: panel_names.DETAIL_PANEL,
...panelInfo, panelInfo: {
fromPanel: { ...panelInfo,
fromYouMayLike: false // 플래그 초기화 fromPanel: {
fromYouMayLike: false // 플래그 초기화
}
} }
} }));
})); }
} }, [fromPanel?.fromYouMayLike, dispatch, panelInfo]);
}, [fromPanel?.fromYouMayLike, isBillingProductVisible, isUnavailableProductVisible, isGroupProductVisible, isTravelProductVisible]); // BUY NOW 상태 변경 시 재실행
// sendLogProductDetail 로깅 - Source의 productData 변경 감지와 동일한 패턴 // sendLogProductDetail 로깅 - Source의 productData 변경 감지와 동일한 패턴
useEffect(() => { useEffect(() => {
if (productData && Object.keys(productData).length > 0) { if (!productData || Object.keys(productData).length === 0) {
const params = { return;
befPrice: productData?.priceInfo?.split("|")[0],
curationId: productData?.curationId ?? "",
curationNm: productData?.curationNm ?? "",
entryMenu: entryMenuRef.current,
expsOrd: "1",
inDt: formatGMTString(new Date()),
lastPrice: productData?.priceInfo?.split("|")[1],
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,
patncNm: productData?.patncNm ?? "",
patnrId: productData?.patnrId ?? "",
prdtId: productData?.prdtId ?? "",
prdtNm: productData?.prdtNm ?? "",
revwGrd: productData?.revwGrd ?? "",
rewdAplyFlag: productData.priceInfo?.split("|")[2],
tsvFlag: productData?.todaySpclFlag ?? "",
};
return () => dispatch(sendLogProductDetail(params));
} }
}, [productData, entryMenuRef.current, panelInfo?.linkTpCd, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]); // productData 변경 시 재실행
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 ?? "",
curationNm: productData?.curationNm ?? "",
entryMenu: entryMenuRef.current,
expsOrd: "1",
inDt: formatGMTString(new Date()),
lastPrice: productData?.priceInfo?.split("|")[1],
lgCatCd: productData?.catCd ?? "",
lgCatNm: productData?.catNm ?? "",
linkTpCd: panelInfo?.linkTpCd ?? "",
logTpNo,
patncNm: productData?.patncNm ?? "",
patnrId: productData?.patnrId ?? "",
prdtId: productData?.prdtId ?? "",
prdtNm: productData?.prdtNm ?? "",
revwGrd: productData?.revwGrd ?? "",
rewdAplyFlag: productData.priceInfo?.split("|")[2],
tsvFlag: productData?.todaySpclFlag ?? "",
};
dispatch(sendLogProductDetail(params));
}, [
productData,
panelInfo?.linkTpCd,
isBillingProductVisible,
isGroupProductVisible,
isTravelProductVisible,
dispatch,
]); // productData 변경 시 재실행
// [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로 // [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음 // ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
@@ -951,6 +1020,9 @@ export default function ProductAllSection({
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false); const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = 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; const reviewTotalCount = stats.totalReviews;
@@ -1060,6 +1132,7 @@ export default function ProductAllSection({
const descriptionRef = useRef(null); const descriptionRef = useRef(null);
const reviewRef = useRef(null); const reviewRef = useRef(null);
const youMayAlsoLikelRef = useRef(null); const youMayAlsoLikelRef = useRef(null);
const seeMoreProductsRef = useRef(null);
const prevMediaPanelModalStateRef = useRef(null); // MediaPanel의 이전 modal 상태 추적 const prevMediaPanelModalStateRef = useRef(null); // MediaPanel의 이전 modal 상태 추적
// 동영상과 이미지를 통합한 렌더링 아이템 리스트 생성 (Indicator.jsx 로직 기반) // 동영상과 이미지를 통합한 렌더링 아이템 리스트 생성 (Indicator.jsx 로직 기반)
@@ -1106,35 +1179,32 @@ export default function ProductAllSection({
return hasVideo && productVideoVersion === 1; return hasVideo && productVideoVersion === 1;
}, [hasVideo, productVideoVersion]); }, [hasVideo, productVideoVersion]);
const handleShopByMobileOpen = useCallback( const handleShopByMobileOpen = useCallback(() => {
pipe(() => { // sendLogShopByMobile - Source와 동일한 로깅 추가
// sendLogShopByMobile - Source와 동일한 로깅 추가 if (productData && Object.keys(productData).length > 0) {
if (productData && Object.keys(productData).length > 0) { const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData;
const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData; const regularPrice = priceInfo?.split("|")[0];
const regularPrice = priceInfo?.split("|")[0]; const discountPrice = priceInfo?.split("|")[1];
const discountPrice = priceInfo?.split("|")[1]; const discountRate = priceInfo?.split("|")[4];
const discountRate = priceInfo?.split("|")[4];
const logParams = { const logParams = {
prdtId, prdtId,
patnrId, patnrId,
prdtNm, prdtNm,
patncNm, patncNm,
brndNm, brndNm,
catNm, catNm,
regularPrice, regularPrice,
discountPrice, discountPrice,
discountRate, discountRate,
shopByMobileTime: new Date().toISOString(), shopByMobileTime: new Date().toISOString(),
}; };
dispatch(sendLogShopByMobile(logParams)); dispatch(sendLogShopByMobile(logParams));
} }
setMobileSendPopupOpen(true); // 팝업 열기 setMobileSendPopupOpen(true); // 팝업 열기
}, setMobileSendPopupOpen), }, [productData, dispatch]);
[]
);
const shopByMobileId = useMemo( const shopByMobileId = useMemo(
() => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile', () => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
@@ -1219,6 +1289,116 @@ export default function ProductAllSection({
Spotlight.focus("detail_youMayAlsoLike_area") Spotlight.focus("detail_youMayAlsoLike_area")
},100); },100);
}, [scrollToSection, dispatch]); }, [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 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정 // 헤더 Back 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정
useEffect(() => { useEffect(() => {
const firstId = stackOrder[0]; const firstId = stackOrder[0];
@@ -1235,6 +1415,13 @@ export default function ProductAllSection({
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적 const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
const scrollExpandTimerRef = useRef(null); // 스크롤 확장 타이머 const scrollExpandTimerRef = useRef(null); // 스크롤 확장 타이머
const mediaMinimizedRef = useRef(false); const mediaMinimizedRef = useRef(false);
const getTotalContentHeight = useCallback(() => {
const measuredHeight =
contentHeightRef.current ||
scrollContainerRef.current?.scrollHeight ||
0;
return measuredHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
}, []);
const handleArrowClickAlternative = useCallback(() => { const handleArrowClickAlternative = useCallback(() => {
dispatch(minimizeModalMedia()); dispatch(minimizeModalMedia());
@@ -1247,14 +1434,14 @@ export default function ProductAllSection({
animate: true, animate: true,
}); });
// documentHeight를 활용하여 반복 계산 제거 // 캐시된 콘텐츠 높이를 활용하여 반복 계산 최소화
const totalHeight = documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0); const totalHeight = getTotalContentHeight();
const isAtBottom = scrollPositionRef.current + 1100 >= totalHeight; const isAtBottom = scrollPositionRef.current + 1100 >= totalHeight;
if (isAtBottom) { if (isAtBottom) {
setIsBottom(isAtBottom); setIsBottom(isAtBottom);
} }
}, [documentHeight, scrollTop, dispatch]); }, [scrollTop, dispatch, getTotalContentHeight]);
const handleScroll = useCallback( const handleScroll = useCallback(
(e) => { (e) => {
@@ -1265,12 +1452,12 @@ export default function ProductAllSection({
const prevScrollTop = prevScrollTopRef.current; const prevScrollTop = prevScrollTopRef.current;
scrollPositionRef.current = currentScrollTop; scrollPositionRef.current = currentScrollTop;
contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
// 기존 bottom 체크 로직 유지 // 기존 bottom 체크 로직 유지
if (documentHeight) { const totalHeight = getTotalContentHeight();
const isAtBottom = if (totalHeight) {
scrollPositionRef.current + 944 >= const isAtBottom = scrollPositionRef.current + 944 >= totalHeight;
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
if (isAtBottom !== isBottom) { if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom); setIsBottom(isAtBottom);
} }
@@ -1323,7 +1510,7 @@ export default function ProductAllSection({
} }
// v2: onScrollStop에서 처리 (기존 로직 유지) // v2: onScrollStop에서 처리 (기존 로직 유지)
}, },
[documentHeight, isBottom, productVideoVersion, isVideoPlaying, dispatch] [isBottom, productVideoVersion, isVideoPlaying, dispatch, getTotalContentHeight]
); );
// 스크롤 멈추었을 때만 호출 (성능 최적화) // 스크롤 멈추었을 때만 호출 (성능 최적화)
@@ -1331,10 +1518,10 @@ export default function ProductAllSection({
(e) => { (e) => {
const currentScrollTop = e.scrollTop; const currentScrollTop = e.scrollTop;
scrollPositionRef.current = currentScrollTop; scrollPositionRef.current = currentScrollTop;
if (documentHeight) { contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
const isAtBottom = const totalHeight = getTotalContentHeight();
currentScrollTop + 944 >= if (totalHeight) {
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0); const isAtBottom = currentScrollTop + 944 >= totalHeight;
if (isAtBottom !== isBottom) { if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom); setIsBottom(isAtBottom);
} }
@@ -1349,7 +1536,7 @@ export default function ProductAllSection({
return shouldMinimize; return shouldMinimize;
}); });
}, },
[documentHeight, isBottom] [getTotalContentHeight]
); );
useEffect(() => { useEffect(() => {
@@ -1374,14 +1561,6 @@ export default function ProductAllSection({
setActiveButton(null); setActiveButton(null);
}, []); }, []);
useEffect(() => {
setDocumentHeight(
(productDetailRef.current?.scrollHeight || 0) +
(descriptionRef.current?.scrollHeight || 0) +
(reviewRef.current?.scrollHeight || 0)
);
}, [productDetailRef.current, descriptionRef.current, reviewRef.current]);
// 스크롤 위치에 따른 MediaPanel 제어 (비디오 재생 중에는 자동 제어 안함 - unmount 시에만 정리) // 스크롤 위치에 따른 MediaPanel 제어 (비디오 재생 중에는 자동 제어 안함 - unmount 시에만 정리)
// useEffect(() => { // useEffect(() => {
// console.log('📍 [ProductAllSection] useEffect 실행 - shouldMinimizeMedia:', shouldMinimizeMedia); // console.log('📍 [ProductAllSection] useEffect 실행 - shouldMinimizeMedia:', shouldMinimizeMedia);
@@ -1422,6 +1601,8 @@ export default function ProductAllSection({
}; };
}, []); }, []);
const sponserImage = useMemo(() => productData?.spnsrImgUrl, [productData]);
// WebOS TV focus-within 대체 로직 // WebOS TV focus-within 대체 로직
// useEffect(() => { // useEffect(() => {
// const detailAreaElement = document.querySelector('.detailArea'); // const detailAreaElement = document.querySelector('.detailArea');
@@ -1700,7 +1881,7 @@ export default function ProductAllSection({
</TButton> </TButton>
</> </>
)} )}
{hasYouMayAlsoLike && ( {(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
<TButton <TButton
className={classNames( className={classNames(
css.youMayLikeButton, css.youMayLikeButton,
@@ -1711,6 +1892,19 @@ export default function ProductAllSection({
{$L('YOU MAY ALSO LIKE')} {$L('YOU MAY ALSO LIKE')}
</TButton> </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 버튼 렌더링 상태 로그 */} {/* YouMayLike 버튼 렌더링 상태 로그 */}
{/* {(() => { {/* {(() => {
console.log('[YouMayLike] 버튼 렌더링 체크:', { console.log('[YouMayLike] 버튼 렌더링 체크:', {
@@ -1784,7 +1978,7 @@ export default function ProductAllSection({
id="product-details-section" id="product-details-section"
ref={productDetailRef} ref={productDetailRef}
onFocus={() => handleButtonFocus('product')} onFocus={() => handleButtonFocus('product')}
onBlur={handleButtonBlur} onBlur={handleButtonBlur}
> >
{/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */} {/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */}
{hasVideo && renderItems[0].type === 'video' && productVideoVersion !== 3 && ( {hasVideo && renderItems[0].type === 'video' && productVideoVersion !== 3 && (
@@ -1873,7 +2067,7 @@ export default function ProductAllSection({
</div> </div>
<div ref={youMayAlsoLikelRef}> <div ref={youMayAlsoLikelRef}>
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div> <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"> <div id="you-may-also-like-section">
{/* {(() => { {/* {(() => {
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', { console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
@@ -1894,6 +2088,22 @@ export default function ProductAllSection({
</div> </div>
)} )}
</div> </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}> <div className={css.topButtonBox}>
<TButton <TButton

View File

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

View File

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

View File

@@ -39,7 +39,7 @@
} }
.thumbnailWrapper .productImage { .thumbnailWrapper .productImage {
transform: scale(1.015); // 가로세로 10px 정도 확대 효과 transform: scale(1.015); // 가로세로 10px 정도 확대 효과
} }
} }
} }
@@ -70,14 +70,15 @@
.thumbnailWrapper { .thumbnailWrapper {
position: relative; position: relative;
width: 658px; // width: 658px;
width:100%;
height: 610px; height: 610px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.productImage { .productImage {
width: 100%; width: 658px;
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -89,4 +90,20 @@
transition: all 0.2s ease; 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]); }, [onNext, currentIndex, totalReviews]);
useEffect(() => {
console.log("####currentIndex", currentIndex);
}, [currentIndex]);
// 리뷰 데이터가 없을 때 처리 // 리뷰 데이터가 없을 때 처리
if (!currentReview) { if (!currentReview) {
return ( return (

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
@import "../../../../../style/CommonStyle.module.less"; @import '../../../../../style/CommonStyle.module.less';
@import "../../../../../style/utils.module.less"; @import '../../../../../style/utils.module.less';
.wrapper { .wrapper {
height: 100%; height: 100%;
@@ -30,7 +30,7 @@
} }
.name { .name {
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 30px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
width: 100%; width: 100%;
} }

View File

@@ -30,7 +30,7 @@
} }
.name { .name {
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 30px;
color: @COLOR_GRAY07; color: @COLOR_GRAY07;
} }
.btmLayer { .btmLayer {
@@ -188,9 +188,11 @@
.productNm { .productNm {
width: 100%; width: 100%;
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 38px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
flex:none; flex:none;
.elip(2); .elip(2);
margin-bottom: 20px; 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 { $L } from '../../../../../utils/helperMethods';
import css from './ShopByMobilePriceDisplay.module.less'; 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({ export default function ShopByMobilePriceDisplay({
priceData, priceData,
priceInfo, priceInfo,
@@ -26,6 +32,10 @@ export default function ShopByMobilePriceDisplay({
orderPhnNo, orderPhnNo,
} = priceData; } = priceData;
// 파트너명 정규화
const cleanPatncNm = useMemo(() => extractPartnerName(patncNm), [patncNm]);
const cleanPatnrName = useMemo(() => extractPartnerName(patnrName), [patnrName]);
const { const {
discountRate, discountRate,
rewardFlag, rewardFlag,
@@ -50,6 +60,8 @@ export default function ShopByMobilePriceDisplay({
[isOriginalPriceEmpty, isDiscountedPriceEmpty, price5, offerInfo] [isOriginalPriceEmpty, isDiscountedPriceEmpty, price5, offerInfo]
); );
const renderPriceItem = useCallback(() => { const renderPriceItem = useCallback(() => {
if (priceData && !promotionCode) { if (priceData && !promotionCode) {
if (rewd) { if (rewd) {
@@ -57,9 +69,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}> <div className={css.wrapper}>
<div className={css.rewdTopLayer}> <div className={css.rewdTopLayer}>
<span> <span>
{patncNm {cleanPatncNm
? patncNm + " " + $L("Price") + " " ? cleanPatncNm + " " + $L("Price") + " "
: patnrName + " " + $L("Price") + " "} : cleanPatnrName + " " + $L("Price") + " "}
</span> </span>
<span className={css.partnerPrc}> <span className={css.partnerPrc}>
{TYPE_CASE.case5 || TYPE_CASE.case8 {TYPE_CASE.case5 || TYPE_CASE.case8
@@ -95,9 +107,9 @@ export default function ShopByMobilePriceDisplay({
return ( return (
<div className={css.wrapper}> <div className={css.wrapper}>
<span className={css.name}> <span className={css.name}>
{patncNm {cleanPatncNm
? patncNm + " " + $L("Price") ? cleanPatncNm + " " + $L("Price")
: patnrName + " " + $L("Price")} : cleanPatnrName + " " + $L("Price")}
</span> </span>
<div className={css.btmLayer}> <div className={css.btmLayer}>
<span className={classNames(css.price, css.case01)}> <span className={classNames(css.price, css.case01)}>
@@ -111,9 +123,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}> <div className={css.wrapper}>
<div className={css.topLayer}> <div className={css.topLayer}>
<span className={css.name}> <span className={css.name}>
{patncNm {cleanPatncNm
? patncNm + " " + $L("Price") ? cleanPatncNm + " " + $L("Price")
: patnrName + $L("Price")} : cleanPatnrName + $L("Price")}
</span> </span>
</div> </div>
{discountRate && Number(discountRate.replace("%", "")) > 4 && ( {discountRate && Number(discountRate.replace("%", "")) > 4 && (
@@ -140,9 +152,9 @@ export default function ShopByMobilePriceDisplay({
<div className={css.wrapper}> <div className={css.wrapper}>
<div className={css.topLayer}> <div className={css.topLayer}>
<span className={css.name}> <span className={css.name}>
{patncNm {cleanPatncNm
? patncNm + " " + $L("Price") ? cleanPatncNm + " " + $L("Price")
: patnrName + " " + $L("Price")} : cleanPatnrName + " " + $L("Price")}
</span> </span>
</div> </div>
{discountRate && Number(discountRate.replace("%", "")) > 4 && ( {discountRate && Number(discountRate.replace("%", "")) > 4 && (
@@ -194,7 +206,7 @@ export default function ShopByMobilePriceDisplay({
return ( return (
<div className={css.wrapper}> <div className={css.wrapper}>
<span className={css.partnerName}> <span className={css.partnerName}>
{patncNm ? patncNm + " " + $L("Price") : patnrName + $L("Price")} {cleanPatncNm ? cleanPatncNm + " " + $L("Price") : cleanPatnrName + $L("Price")}
</span> </span>
<span <span
className={css.offerInfo} className={css.offerInfo}
@@ -204,7 +216,8 @@ export default function ShopByMobilePriceDisplay({
); );
} }
}, [ }, [
patnrName, cleanPatnrName,
cleanPatncNm,
priceInfo, priceInfo,
isOriginalPriceEmpty, isOriginalPriceEmpty,
isDiscountedPriceEmpty, isDiscountedPriceEmpty,
@@ -214,4 +227,4 @@ export default function ShopByMobilePriceDisplay({
]); ]);
return <div>{renderPriceItem()}</div>; return <div>{renderPriceItem()}</div>;
} }

View File

@@ -1,5 +1,5 @@
@import "../../../../../style/CommonStyle.module.less"; @import '../../../../../style/CommonStyle.module.less';
@import "../../../../../style/utils.module.less"; @import '../../../../../style/utils.module.less';
.wrapper { .wrapper {
height: 100%; height: 100%;
@@ -30,7 +30,7 @@
} }
.name { .name {
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 30px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
width: 100%; width: 100%;
} }
@@ -40,7 +40,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.btmLayer2 { .btmLayer2 {
margin: 5px 0; margin: 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;

View File

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

View File

@@ -72,6 +72,7 @@ const CustomDropDown = ({
const selectedOption = normalizedOptions[selectedIndex]; const selectedOption = normalizedOptions[selectedIndex];
const selectedLabel = selectedOption?.label || placeholder; const selectedLabel = selectedOption?.label || placeholder;
const selectedId = selectedOption?.prodOptCdCval ? selectedOption?.prodOptCdCval : null;
const selectedImage = selectedOption?.imageUrl; const selectedImage = selectedOption?.imageUrl;
return ( return (
@@ -93,7 +94,7 @@ const CustomDropDown = ({
className={styles.custom_dropdown__image} 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>
<div className={styles.custom_dropdown__icon}> <div className={styles.custom_dropdown__icon}>
<img src={iconDownArrow} alt="dropdown arrow" /> <img src={iconDownArrow} alt="dropdown arrow" />
@@ -108,7 +109,6 @@ const CustomDropDown = ({
.map((option, reverseIndex) => { .map((option, reverseIndex) => {
const originalIndex = normalizedOptions.length - 1 - reverseIndex; const originalIndex = normalizedOptions.length - 1 - reverseIndex;
const isOptionDisabled = option.disabled; const isOptionDisabled = option.disabled;
return ( return (
<SpottableDiv <SpottableDiv
key={originalIndex} key={originalIndex}
@@ -133,7 +133,7 @@ const CustomDropDown = ({
/> />
)} )}
<span className={styles.custom_dropdown__optname}> <span className={styles.custom_dropdown__optname}>
{option.label} {option.label} {option.prodOptCdCval ? `ID : ${option.prodOptCdCval}` : ''}
</span> </span>
{isOptionDisabled ? ( {isOptionDisabled ? (
<span className={styles.custom_dropdown__lasttxt}> <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 Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../../../assets/images/bg/qvc_new.png'; import qvc from '../../../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../../../assets/images/bg/shoplc_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'; import css from './DetailPanelBackground.module.less';
/** /**
@@ -38,6 +39,7 @@ export default function DetailPanelBackground({
11: shoplc, 11: shoplc,
16: koreaKiosk, 16: koreaKiosk,
19: Pinkfong, 19: Pinkfong,
21: nbcu,
}; };
const detailPanelBg = useMemo(() => { const detailPanelBg = useMemo(() => {
@@ -75,26 +77,32 @@ export default function DetailPanelBackground({
useEffect(() => { useEffect(() => {
// launchedFromPlayer가 true이면 배경 이미지를 로드하지 않음 (PlayerPanel 비디오 보이도록) // launchedFromPlayer가 true이면 배경 이미지를 로드하지 않음 (PlayerPanel 비디오 보이도록)
if (launchedFromPlayer) { 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); setImageReady(false);
return; return;
} }
// launchedFromPlayer가 false일 때만 배경 이미지 로드 // 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)) { if (ImagePreloader.isLoaded(detailPanelBg)) {
// console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg); console.log('[Detail-BG] ✅ DetailPanelBackground - Using preloaded image:', detailPanelBg);
setImageReady(true); setImageReady(true);
} else { } 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) ImagePreloader.preloadImage(detailPanelBg)
.then(() => { .then(() => {
// console.log('[DetailPanelBackground] On-demand image loaded:', detailPanelBg); console.log('[Detail-BG] ✅ DetailPanelBackground - On-demand image loaded:', detailPanelBg);
setImageReady(true); setImageReady(true);
}) })
.catch((e) => { .catch((e) => {
// console.error('[DetailPanelBackground] On-demand image load failed:', e); console.error('[Detail-BG] ❌ DetailPanelBackground - On-demand image load failed:', e);
// 실패해도 이미지를 표시해야 함 // 실패해도 이미지를 표시해야 함
setImageReady(true); 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 Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../../../assets/images/bg/qvc_new.png'; import qvc from '../../../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../../../assets/images/bg/shoplc_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'; import css from './DetailPanelBackground.module.less';
// ==================== 로깅 함수들 ==================== // ==================== 로깅 함수들 ====================
@@ -65,6 +66,7 @@ export default function DetailPanelBackgroundV2({
11: shoplc, // SHOPLC 11: shoplc, // SHOPLC
16: koreaKiosk, // KOREA KIOSK 16: koreaKiosk, // KOREA KIOSK
19: Pinkfong, // PINKFONG 19: Pinkfong, // PINKFONG
21: nbcu, // NBCU
}), }),
[] []
); );
@@ -200,7 +202,7 @@ export function PreloadedBackgroundImages({
launchedFromPlayer = false, launchedFromPlayer = false,
}) { }) {
// 모든 파트너사 ID 목록 // 모든 파트너사 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가 있을 때만 배경 표시 // ✅ 원래 로직 복원: HomePanel이 onTop이 아니고 selectedPatnrId가 있을 때만 배경 표시
const shouldShowBackground = !isHomePanelOnTop && selectedPatnrId; const shouldShowBackground = !isHomePanelOnTop && selectedPatnrId;

View File

@@ -77,7 +77,7 @@ export default function THeaderCustom({
if(onBackButtonFocus){ if(onBackButtonFocus){
onBackButtonFocus(); onBackButtonFocus();
} }
},[onBackButtonFocus]) },[onBackButtonFocus])
return ( return (
<Container className={classNames(css.tHeaderCustom, className)} {...rest}> <Container className={classNames(css.tHeaderCustom, className)} {...rest}>
@@ -113,7 +113,7 @@ export default function THeaderCustom({
marqueeDisabled={marqueeDisabled} marqueeDisabled={marqueeDisabled}
aria-label={ariaLabel} aria-label={ariaLabel}
> >
{prdtId && <span className={css.prdtId}>ID : {prdtId}</span>} {(prdtId && patnrId !== "21") && <span className={css.prdtId}>ID : {prdtId}</span>}
{convertedTitle && ( {convertedTitle && (
<span dangerouslySetInnerHTML={{ __html: 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 IcPartnersDefault from "../../../../assets/images/ic-tab-partners-default@3x.png";
import CustomImage from "../../../components/CustomImage/CustomImage"; import CustomImage from "../../../components/CustomImage/CustomImage";
import TopBannerImage from "../TopBannerImage/TopBannerImage";
import css from "./Banner.module.less"; import css from "./Banner.module.less";
export default memo(function Banner({ export default memo(function Banner({
brandInfo, brandInfo,
brandTopImgInfo, brandTopImgInfo,
brandTopBannerInfo,
panelPatnrId, panelPatnrId,
selectedPatnrId,
}) { }) {
const selectedBrandInfo = const selectedBrandInfo =
brandInfo?.find(({ patnrId }) => panelPatnrId === patnrId) || {}; brandInfo?.find(({ patnrId }) => panelPatnrId === patnrId) || {};
const { patncLogoPath, patncNm } = selectedBrandInfo; 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 ( return (
<div className={css.container}> <div className={css.container}>
<figure> <figure>
@@ -26,7 +62,25 @@ export default memo(function Banner({
/> />
<figcaption>{patncNm}</figcaption> <figcaption>{patncNm}</figcaption>
</figure> </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> </div>
); );
}); });

View File

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

View File

@@ -19,6 +19,7 @@ const Container = SpotlightContainerDecorator(
const FeaturedBestSeller = ({ const FeaturedBestSeller = ({
brandBestSellerInfo, brandBestSellerInfo,
brandBestSellerTitle,
handleItemFocus, handleItemFocus,
order, order,
shelfOrder, shelfOrder,
@@ -28,6 +29,12 @@ const FeaturedBestSeller = ({
}) => { }) => {
const [firstChk, setFirstChk] = useState(0); 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(() => { const _handleItemFocus = useCallback(() => {
if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder); if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder);
@@ -65,7 +72,7 @@ const FeaturedBestSeller = ({
spotlightId={spotlightId} spotlightId={spotlightId}
> >
<SectionTitle <SectionTitle
title={$L(STRING_CONF.BEST_SELLER)} title={displayTitle}
data-title="best-seller" data-title="best-seller"
label="best-seller Heading 1" label="best-seller Heading 1"
/> />

View File

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

View File

@@ -11,6 +11,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Job } from "@enact/core/util"; import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight"; import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { types } from "../../actions/actionTypes"; import { types } from "../../actions/actionTypes";
import { import {
@@ -23,8 +24,10 @@ import {
getBrandLiveChannelInfo, getBrandLiveChannelInfo,
getBrandRecommendedShowInfo, getBrandRecommendedShowInfo,
getBrandSeriesInfo, getBrandSeriesInfo,
getBrandShopByShow,
getBrandShowroom, getBrandShowroom,
getBrandTSVInfo, getBrandTSVInfo,
getBrandTopBanner,
} from "../../actions/brandActions"; } from "../../actions/brandActions";
import { changeAppStatus, setHidePopup } from "../../actions/commonActions"; import { changeAppStatus, setHidePopup } from "../../actions/commonActions";
import { import {
@@ -63,6 +66,7 @@ import LiveChannels from "./LiveChannels/LiveChannels";
import QuickMenu from "./QuickMenu/QuickMenu"; import QuickMenu from "./QuickMenu/QuickMenu";
import RecommendedShows from "./RecommendedShows/RecommendedShows"; import RecommendedShows from "./RecommendedShows/RecommendedShows";
import Series from "./Series/Series"; import Series from "./Series/Series";
import ShopByShow from "./ShopByShow/ShopByShow";
import Showroom from "./Showroom/Showroom"; import Showroom from "./Showroom/Showroom";
import TodaysDeals from "./TodaysDeals/TodaysDeals"; import TodaysDeals from "./TodaysDeals/TodaysDeals";
import UpComing from "./UpComing/UpComing"; import UpComing from "./UpComing/UpComing";
@@ -90,6 +94,7 @@ const TEMPLATE_CODE_CONF = {
SERIES: "BRD00107", SERIES: "BRD00107",
CATEGORY: "BRD00108", CATEGORY: "BRD00108",
SHOWROOM: "BRD00109", SHOWROOM: "BRD00109",
NBCU: "BRD00110",
}; };
const DISPATCH_MAP = Object.freeze({ const DISPATCH_MAP = Object.freeze({
@@ -101,10 +106,22 @@ const DISPATCH_MAP = Object.freeze({
[TEMPLATE_CODE_CONF.SERIES]: getBrandSeriesInfo, [TEMPLATE_CODE_CONF.SERIES]: getBrandSeriesInfo,
[TEMPLATE_CODE_CONF.CATEGORY]: getBrandCategoryInfo, [TEMPLATE_CODE_CONF.CATEGORY]: getBrandCategoryInfo,
[TEMPLATE_CODE_CONF.SHOWROOM]: getBrandShowroom, [TEMPLATE_CODE_CONF.SHOWROOM]: getBrandShowroom,
[TEMPLATE_CODE_CONF.NBCU]: getBrandShopByShow,
}); });
const TOP_MARGIN = 36; 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) => const hasTemplateCodeWithValue = (array, value) =>
array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false; array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false;
@@ -233,6 +250,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
const brandBestSellerInfo = useSelector( const brandBestSellerInfo = useSelector(
(state) => state.brand.brandBestSellerData.data.brandBestSellerInfo (state) => state.brand.brandBestSellerData.data.brandBestSellerInfo
); );
const brandBestSellerTitle = useSelector(
(state) => state.brand.brandBestSellerData.data.brandBestSellerTitle
);
const brandRecommendedShowCategoryInfo = useSelector( const brandRecommendedShowCategoryInfo = useSelector(
(state) => (state) =>
state.brand.brandRecommendedShowInfoData.data state.brand.brandRecommendedShowInfoData.data
@@ -263,6 +283,19 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
const brandShowroomInfo = useSelector( const brandShowroomInfo = useSelector(
(state) => state.brand.brandShowroomData.data.brandShowroomInfo (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 [displayTopButton, setDisplayTopButton] = useState(false);
const [focusedContainerId, setFocusedContainerId] = useState(null); const [focusedContainerId, setFocusedContainerId] = useState(null);
@@ -293,6 +326,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
}); });
const renderedShelfCountRef = useRef(0); const renderedShelfCountRef = useRef(0);
// 🆕 [251210] patnrId=21 카테고리 그룹 조회 상태 추적
const fetchedCategoryGroupsRef = useRef(new Set());
const fromDetail = panelInfo?.from && panelInfo.from === "detail"; const fromDetail = panelInfo?.from && panelInfo.from === "detail";
const fromGNB = panelInfo?.from && panelInfo.from === "gnb"; const fromGNB = panelInfo?.from && panelInfo.from === "gnb";
const fromUpcoming = panelInfo?.from && panelInfo.from === "upcoming"; const fromUpcoming = panelInfo?.from && panelInfo.from === "upcoming";
@@ -412,9 +448,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
); );
const renderPageItem = useCallback(() => { const renderPageItem = useCallback(() => {
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo length:", sortedBrandLayoutInfo.length);
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo items:", sortedBrandLayoutInfo.map(el => el.shptmBrndOptTpCd));
return ( return (
<> <>
{sortedBrandLayoutInfo.map((el, idx) => { {sortedBrandLayoutInfo.map((el, idx) => {
// console.log("[FeaturedBrandsPanel] Processing template code:", el.shptmBrndOptTpCd);
switch (el.shptmBrndOptTpCd) { switch (el.shptmBrndOptTpCd) {
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: { case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
return ( return (
@@ -485,6 +524,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
} }
case TEMPLATE_CODE_CONF.BEST_SELLER: { 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 ( return (
<React.Fragment key={el.shptmBrndOptTpCd}> <React.Fragment key={el.shptmBrndOptTpCd}>
{hasTemplateCodeWithValue( {hasTemplateCodeWithValue(
@@ -492,15 +535,19 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
TEMPLATE_CODE_CONF.BEST_SELLER TEMPLATE_CODE_CONF.BEST_SELLER
) && ) &&
shouldRenderComponent(brandBestSellerInfo) && ( shouldRenderComponent(brandBestSellerInfo) && (
<FeaturedBestSeller <>
brandBestSellerInfo={brandBestSellerInfo} {/* {console.log("[FeaturedBrandsPanel] Rendering FeaturedBestSeller for patnrId:", selectedPatnrId)} */}
handleItemFocus={handleItemFocus} <FeaturedBestSeller
order={idx + 1} brandBestSellerInfo={brandBestSellerInfo}
shelfOrder={el.expsOrd} brandBestSellerTitle={brandBestSellerTitle}
shelfTitle={el.shptmBrndOptTpNm} handleItemFocus={handleItemFocus}
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER} order={idx + 1}
selectedPatnrId={selectedPatnrId} shelfOrder={el.expsOrd}
/> shelfTitle={el.shptmBrndOptTpNm}
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER}
selectedPatnrId={selectedPatnrId}
/>
</>
)} )}
</React.Fragment> </React.Fragment>
); );
@@ -650,12 +697,43 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
</React.Fragment> </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, brandBestSellerInfo,
brandBestSellerTitle,
brandCategoryInfo, brandCategoryInfo,
brandCategoryProductInfo, brandCategoryProductInfo,
brandChanInfo, brandChanInfo,
@@ -668,6 +746,8 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
brandSeriesGroupInfo, brandSeriesGroupInfo,
brandSeriesInfo, brandSeriesInfo,
brandShowroomInfo, brandShowroomInfo,
brandShopByShowContsList,
brandShopByShowContsInfo,
brandTsvInfo, brandTsvInfo,
fromGNB, fromGNB,
fromQuickMenu, fromQuickMenu,
@@ -711,6 +791,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: layout information fetching due to partner id change // effect: layout information fetching due to partner id change
useEffect(() => { useEffect(() => {
if (!fromDetail) { if (!fromDetail) {
// console.log("[FeaturedBrandsPanel] Layout Info Effect - patnrId:", panelInfo?.patnrId);
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO }); dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId })); dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
setIsInitialFocusOccurred(false); setIsInitialFocusOccurred(false);
@@ -720,30 +801,98 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: set selectedPatnrId and selectedPatncNm // effect: set selectedPatnrId and selectedPatncNm
useEffect(() => { useEffect(() => {
if (brandInfo) { if (brandInfo && panelInfo?.patnrId) {
const patnrId = panelInfo?.patnrId; const patnrId = panelInfo.patnrId;
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId).patncNm; const patncNm = brandInfo.find((b) => b?.patnrId === patnrId)?.patncNm;
setSelectedPatncNm(patncNm); if (patncNm) {
setSelectedPatncNm(patncNm);
}
if (!fromDetail) setSelectedPatnrId(patnrId); // Detail에서 돌아와도 patnrId가 비어 있으면 다시 설정해 API 호출이 정상 동작하도록 보완
if (!fromDetail || !selectedPatnrId) {
setSelectedPatnrId(patnrId);
}
} }
}, [brandInfo, panelInfo?.patnrId]); }, [brandInfo, panelInfo?.patnrId, selectedPatnrId, fromDetail]);
// effect: data fetching based on brandLayoutInfo and selectedPatnrId // effect: data fetching based on brandLayoutInfo and selectedPatnrId
useEffect(() => { 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) { if (sortedBrandLayoutInfo && selectedPatnrId) {
// console.log("[FB-PANEL-DATA-FETCH] Fetching data - patnrId:", selectedPatnrId);
Object.entries(DISPATCH_MAP) // Object.entries(DISPATCH_MAP) //
.forEach(([templateCode, action]) => { .forEach(([templateCode, action]) => {
if (hasTemplateCodeWithValue(sortedBrandLayoutInfo, templateCode)) { 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 })); 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(); resetStates();
} }
}, [sortedBrandLayoutInfo, selectedPatnrId]); }, [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(() => { useEffect(() => {
if (selectedCatCd) { if (selectedCatCd) {
dispatch( dispatch(
@@ -905,6 +1054,18 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
} }
}, [isLogGNBSent, isInitialFocusOccurred, selectedPatnrId, selectedPatncNm]); }, [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 // effect: unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -959,21 +1120,49 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
/> />
)} )}
{brandInfo && brandTopImgInfo && ( {/* 🆕 [251211] patnrId=21일 때: Banner + Category + BestSeller를 통합 Container로 관리 */}
<Banner {(selectedPatnrId === 21 || selectedPatnrId === "21") ? (
brandInfo={brandInfo} <NbcuIntegratedContainer className={css.nbcuIntegratedContainer}>
brandTopImgInfo={brandTopImgInfo} {brandInfo && (brandTopImgInfo || brandTopBannerInfo) && (
panelPatnrId={panelInfo?.patnrId} <Banner
/> brandInfo={brandInfo}
)} brandTopImgInfo={brandTopImgInfo}
brandTopBannerInfo={brandTopBannerInfo}
panelPatnrId={panelInfo?.patnrId}
selectedPatnrId={selectedPatnrId}
/>
)}
{sortedBrandLayoutInfo && ( {sortedBrandLayoutInfo && (
<div <div
className={css.orderableFlexContainer} className={css.orderableFlexContainer}
ref={orderableFlexContainerRef} ref={orderableFlexContainerRef}
> >
{renderPageItem()} {renderPageItem()}
</div> </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 && ( {displayTopButton && (

View File

@@ -49,10 +49,13 @@ const QuickMenuItem = ({
}, [handleStopScrolling, itemIndex]); }, [handleStopScrolling, itemIndex]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
console.log("[QuickMenuItem] Click - patnrId:", patnrId, "currentPatnrId:", selectedPatnrId ?? panelInfo?.patnrId);
if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) { if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) {
console.log("[QuickMenuItem] Already selected, returning");
return; return;
} }
console.log("[QuickMenuItem] Switching to patnrId:", patnrId);
const from = "menu"; const from = "menu";
const name = panel_names.FEATURED_BRANDS_PANEL; 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 bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
const popupVisible = useSelector((state) => state.common.popup.popupVisible); const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const panels = useSelector((state) => state.panels.panels);
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리 // 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
const focusHistory = useFocusHistory({ const focusHistory = useFocusHistory({
enableLogging: true, enableLogging: true,
@@ -163,7 +164,10 @@ export default function HomeBanner({
videoData = targetBannerData.bannerDetailInfos?.[0]; 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); console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
dispatch( dispatch(
@@ -175,12 +179,13 @@ export default function HomeBanner({
shptmBanrTpNm: videoData.shptmBanrTpNm, shptmBanrTpNm: videoData.shptmBanrTpNm,
lgCatCd: videoData.lgCatCd, lgCatCd: videoData.lgCatCd,
chanId: videoData.brdcChnlId, chanId: videoData.brdcChnlId,
// 기본: 배너는 modal=true로 재생
modal: true, modal: true,
modalContainerId: defaultFocus, modalContainerId: defaultFocus,
}) })
); );
} }
}, [bannerDataList, defaultFocus, dispatch]); }, [bannerDataList, defaultFocus, dispatch, panels]);
const renderItem = useCallback( const renderItem = useCallback(
(index, isHorizontal) => { (index, isHorizontal) => {

View File

@@ -1,31 +1,52 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
// 디버그 모드 설정 - true일 때만 console.log 출력 useEffect,
const DEBUG_MODE = false; useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container'; import { getContainerId } from '@enact/spotlight/src/container';
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png'; import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png'; import defaultLogoImg
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png'; from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png'; import emptyHorImage
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png'; 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 liveShow from '../../../../assets/images/tag-liveshow.png';
import { changeAppStatus } from '../../../actions/commonActions'; import { changeAppStatus } from '../../../actions/commonActions';
import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions'; import {
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions'; setVideoTransitionLock,
import { pushPanel, navigateFromRandomUnit, SOURCE_MENUS } from '../../../actions/panelActions'; updateHomeInfo,
} from '../../../actions/homeActions';
import {
sendLogTopContents,
sendLogTotalRecommend,
} from '../../../actions/logActions';
import {
navigateFromRandomUnit,
pushPanel,
SOURCE_MENUS,
} from '../../../actions/panelActions';
import { import {
finishVideoPreview, finishVideoPreview,
hideModalVideo,
startVideoPlayer, startVideoPlayer,
startVideoPlayerNew, startVideoPlayerNew,
hideModalVideo,
} from '../../../actions/playActions'; } from '../../../actions/playActions';
import CustomImage from '../../../components/CustomImage/CustomImage'; import CustomImage from '../../../components/CustomImage/CustomImage';
import usePriceInfo from '../../../hooks/usePriceInfo'; import usePriceInfo from '../../../hooks/usePriceInfo';
@@ -36,11 +57,19 @@ import {
LOG_TP_NO, LOG_TP_NO,
panel_names, panel_names,
} from '../../../utils/Config'; } from '../../../utils/Config';
import { selectIsPlaying } from '../../../utils/playerState/playerStateSelectors'; import {
import { $L, formatGMTString } from '../../../utils/helperMethods'; $L,
formatGMTString,
} from '../../../utils/helperMethods';
import {
selectIsPlaying,
} from '../../../utils/playerState/playerStateSelectors';
import { TEMPLATE_CODE_CONF } from '../HomePanel'; import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from './RandomUnit.module.less'; import css from './RandomUnit.module.less';
// 디버그 모드 설정 - true일 때만 console.log 출력
const DEBUG_MODE = false;
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, '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 homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd); const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
const foryouInfos = useSelector((state) => state.foryou.recommendInfo.recommendProduct);
// 현재 재생 중인 비디오 배너 ID 가져오기 // 현재 재생 중인 비디오 배너 ID 가져오기
const currentVideoBannerId = useSelector((state) => { const currentVideoBannerId = useSelector((state) => {
@@ -489,6 +519,14 @@ export default function RandomUnit({
}, },
}; };
break; break;
case 'DSP00510':
linkInfo = {
name: panel_names.JUST_FOR_YOU_TEST_PANEL,
panelInfo: {
},
};
break;
default: default:
linkInfo = { linkInfo = {
@@ -643,6 +681,29 @@ export default function RandomUnit({
// 투데이즈 딜 가격 정보 // 투데이즈 딜 가격 정보
const { originalPrice, discountedPrice, discountRate, offerInfo } = const { originalPrice, discountedPrice, discountRate, offerInfo } =
usePriceInfo(priceInfos) || {}; 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(() => { useEffect(() => {
@@ -999,7 +1060,72 @@ export default function RandomUnit({
/> />
</div> </div>
</SpottableComponent> </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> </Container>
</> </>
); );

View File

@@ -233,6 +233,165 @@
left: -4px; 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 { .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 classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container'; import { getContainerId } from '@enact/spotlight/src/container';
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png'; import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png'; import defaultLogoImg
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png'; from '../../../../assets/images/ic-tab-partners-default@3x.png';
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png'; import emptyHorImage
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png'; 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 liveShow from '../../../../assets/images/tag-liveshow.png';
import { setBannerIndex, updateHomeInfo } from '../../../actions/homeActions'; import {
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions'; setBannerIndex,
import { pushPanel, SOURCE_MENUS } from '../../../actions/panelActions'; updateHomeInfo,
import { startVideoPlayer, finishVideoPreview } from '../../../actions/playActions'; } 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 CustomImage from '../../../components/CustomImage/CustomImage';
import usePriceInfo from '../../../hooks/usePriceInfo'; import usePriceInfo from '../../../hooks/usePriceInfo';
import { import {
@@ -27,7 +53,10 @@ import {
LOG_TP_NO, LOG_TP_NO,
panel_names, panel_names,
} from '../../../utils/Config'; } from '../../../utils/Config';
import { $L, formatGMTString } from '../../../utils/helperMethods'; import {
$L,
formatGMTString,
} from '../../../utils/helperMethods';
import { TEMPLATE_CODE_CONF } from '../HomePanel'; import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from './RollingUnit.module.less'; import css from './RollingUnit.module.less';
@@ -81,6 +110,7 @@ export default function RollingUnit({
const introTermsAgree = useSelector((state) => state.common.optionalTermsAgree); const introTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory); const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd); 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); const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
@@ -119,7 +149,7 @@ export default function RollingUnit({
// filteredRollingDataRef 업데이트 // filteredRollingDataRef 업데이트
useEffect(() => { useEffect(() => {
filteredRollingDataRef.current = filteredRollingData; filteredRollingDataRef.current = filteredRollingData;
}, [filteredRollingData]); }, [filteredRollingData]);
const topContentsLogInfo = useMemo(() => { const topContentsLogInfo = useMemo(() => {
@@ -333,6 +363,29 @@ export default function RollingUnit({
const { originalPrice, discountedPrice, discountRate, offerInfo } = const { originalPrice, discountedPrice, discountRate, offerInfo } =
usePriceInfo(filteredRollingData.length > 0 ? filteredRollingData[startIndex].priceInfo : {}) || {}; 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( const handlePushPanel = useCallback(
(name, panelInfo) => { (name, panelInfo) => {
const isDetailPanel = name === panel_names.DETAIL_PANEL; const isDetailPanel = name === panel_names.DETAIL_PANEL;
@@ -582,7 +635,8 @@ export default function RollingUnit({
/> />
) : null} ) : 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 <SpottableComponent
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)} className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
onClick={imageBannerClick} onClick={imageBannerClick}
@@ -700,7 +754,7 @@ export default function RollingUnit({
)} )}
</div> </div>
</SpottableComponent> </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 <SpottableComponent
className={classNames( className={classNames(
css.itemBox, css.itemBox,
@@ -754,6 +808,75 @@ export default function RollingUnit({
/> />
</div> </div>
</SpottableComponent> </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} ) : null}
{filteredRollingData.length !== 1 ? ( {filteredRollingData.length !== 1 ? (

View File

@@ -237,6 +237,165 @@
left: -4px; 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 { .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 classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import {
import { types } from '../../actions/actionTypes'; useDispatch,
useSelector,
} from 'react-redux';
import { applyMiddleware } from 'redux'; import { applyMiddleware } from 'redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
@@ -11,53 +19,6 @@ import {
setContainerLastFocusedElement, setContainerLastFocusedElement,
} from '@enact/spotlight/src/container'; } 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 // DetailPanelBackground 이미지 imports for preloading
import hsn from '../../../assets/images/bg/hsn_new.png'; import hsn from '../../../assets/images/bg/hsn_new.png';
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_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 Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../assets/images/bg/qvc_new.png'; import qvc from '../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../assets/images/bg/shoplc_new.png'; import shoplc from '../../../assets/images/bg/shoplc_new.png';
import nbcu from '../../../assets/images/bg/nbcu_new.png';
// 파트너사별 배경 이미지 맵 import { types } from '../../actions/actionTypes';
const BACKGROUND_IMAGES = { import {
1: qvc, // QVC changeAppStatus,
2: hsn, // HSN setDeepLink,
4: ontv4u, // ONTV setExitApp,
9: lgelectronics, // LG ELECTRONICS setHidePopup,
11: shoplc, // SHOPLC setOptionalTermsPopupShown,
16: koreaKiosk, // KOREA KIOSK setShowPopup,
19: Pinkfong, // PINKFONG 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 관련 코드 주석 처리 - 향후 사용 검토 필요 // [COMMENTED OUT] useVideoMove 관련 코드 주석 처리 - 향후 사용 검토 필요
// import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove'; // import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove';
// ======= // =======
@@ -100,7 +98,9 @@ import {
LOG_MESSAGE_ID, LOG_MESSAGE_ID,
panel_names, panel_names,
} from '../../utils/Config'; } from '../../utils/Config';
import { createDebugHelpers } from '../../utils/debug';
import { $L } from '../../utils/helperMethods'; import { $L } from '../../utils/helperMethods';
import ImagePreloader from '../../utils/ImagePreloader';
import { SpotlightIds } from '../../utils/SpotlightIds'; import { SpotlightIds } from '../../utils/SpotlightIds';
import BestSeller from '../HomePanel/BestSeller/BestSeller'; import BestSeller from '../HomePanel/BestSeller/BestSeller';
import HomeBanner from '../HomePanel/HomeBanner/HomeBanner'; import HomeBanner from '../HomePanel/HomeBanner/HomeBanner';
@@ -111,6 +111,22 @@ import SubCategory from '../HomePanel/SubCategory/SubCategory';
import EventPopUpBanner from './EventPopUpBanner/EventPopUpBanner'; import EventPopUpBanner from './EventPopUpBanner/EventPopUpBanner';
import PickedForYou from './PickedForYou/PickedForYou'; 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 = { export const TEMPLATE_CODE_CONF = {
TOP: 'DSP00101', TOP: 'DSP00101',
CATEGORY_ITEM: 'DSP00102', CATEGORY_ITEM: 'DSP00102',
@@ -399,6 +415,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
optionalTerms: 'Y', optionalTerms: 'Y',
}, },
}); });
setTimeout(()=>{
Spotlight.focus('home_tbody');
},100)
}, [handleOptionalAgree, dispatch, currentTermsFlag]); }, [handleOptionalAgree, dispatch, currentTermsFlag]);
const handleOptionalDeclineClick = useCallback(() => { const handleOptionalDeclineClick = useCallback(() => {
@@ -407,6 +426,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} }
dispatch(updateOptionalTermsAgreement(false)); dispatch(updateOptionalTermsAgreement(false));
setIsOptionalConfirmVisible(false); setIsOptionalConfirmVisible(false);
setTimeout(()=>{
Spotlight.focus('home_tbody');
},100)
}, [dispatch]); }, [dispatch]);
const handleTermsPopupClosed = useCallback(() => { const handleTermsPopupClosed = useCallback(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1161,6 +1161,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
} }
if (!panelInfo.modal) { if (!panelInfo.modal) {
console.log('[PlayerPanel] popPanel - closeButtonHandler');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
@@ -1191,13 +1192,27 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
//todo if(modal) //todo if(modal)
return () => { return () => {
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 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()); dispatch(PanelActions.popPanel());
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
} }
}; };
}, [panelInfo?.modal, isOnTop]); }, [panelInfo?.modal, isOnTop, panels]);
useEffect(() => { useEffect(() => {
if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') { if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') {
@@ -1810,9 +1825,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo?.modalContainerId panelInfo?.modalContainerId
) { ) {
// case: Featured Brands // case: Featured Brands
if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) { // if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
dispatch(PanelActions.popPanel()); // dispatch(PanelActions.popPanel());
} // }
console.log('[PlayerPanel] Condition 4: Handling video error in fullscreen mode');
} }
}, [ }, [
broadcast?.type, broadcast?.type,
@@ -2384,6 +2400,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
Spotlight.pause(); Spotlight.pause();
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
console.log('[PlayerPanel] popPanel - VIDEO_END_ACTION_DELAY');
Spotlight.resume(); Spotlight.resume();
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
}, VIDEO_END_ACTION_DELAY); }, VIDEO_END_ACTION_DELAY);
@@ -2955,7 +2972,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
disabled={panelInfo.modal} disabled={panelInfo.modal}
onEnded={onEnded} onEnded={onEnded}
noAutoPlay={cannotPlay} noAutoPlay={cannotPlay}
autoCloseTimeout={6000} autoCloseTimeout={10000}
onBackButton={onClickBack} onBackButton={onClickBack}
spotlightDisabled={panelInfo.modal} spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube} isYoutube={isYoutube}

View File

@@ -4,16 +4,22 @@ import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import { sendLogTotalRecommend } from '../../../../actions/logActions';
// <<<<<<< HEAD // <<<<<<< HEAD
import { updatePanel } from '../../../../actions/panelActions'; import { updatePanel } from '../../../../actions/panelActions';
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods'; import { $L } from '../../../../utils/helperMethods';
import PlayerItemCard, { TYPES } from '../../PlayerItemCard/PlayerItemCard'; import PlayerItemCard, { TYPES } from '../../PlayerItemCard/PlayerItemCard';
import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents'; import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents';
import css from './LiveChannelContents.module.less'; import css from './LiveChannelContents.module.less';
import cssV2 from './LiveChannelContents.v2.module.less'; import cssV2 from './LiveChannelContents.v2.module.less';
import { sendLogTotalRecommend } from '../../../../actions/logActions';
// ======= // =======
// import { updatePanel } from "../../../../actions/panelActions"; // import { updatePanel } from "../../../../actions/panelActions";
// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList"; // import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
@@ -40,7 +46,7 @@ export default function LiveChannelContents({
handleItemFocus, handleItemFocus,
tabTitle, tabTitle,
panelInfo, panelInfo,
// <<<<<<< HEAD // <<<<<<< HEAD
direction = 'vertical', direction = 'vertical',
version = 1, version = 1,
isFilteredByPatnr19, isFilteredByPatnr19,
@@ -49,13 +55,13 @@ export default function LiveChannelContents({
const isClickBlocked = useRef(false); const isClickBlocked = useRef(false);
const blockTimeoutRef = useRef(null); const blockTimeoutRef = useRef(null);
// ======= // =======
// isFilteredByPatnr19, // isFilteredByPatnr19,
// }) { // }) {
// const dispatch = useDispatch(); // const dispatch = useDispatch();
// const isClickBlocked = useRef(false); // const isClickBlocked = useRef(false);
const scrollToRef = useRef(null); const scrollToRef = useRef(null);
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
const handleFocus = useCallback( const handleFocus = useCallback(
() => () => { () => () => {
if (handleItemFocus) { if (handleItemFocus) {
@@ -181,15 +187,24 @@ export default function LiveChannelContents({
startDt={strtDt} startDt={strtDt}
endDt={endDt} endDt={endDt}
currentTime={currentTime} currentTime={currentTime}
// <<<<<<< HEAD currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
// <<<<<<< HEAD
version={version} version={version}
// ======= // =======
// currentVideoVisible={currentVideoShowId === liveInfos[index].showId} // currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
/> />
); );
}, },
[liveInfos, currentTime, currentVideoShowId, isClickBlocked, dispatch, handleFocus, version] [
liveInfos,
currentTime,
currentVideoShowId,
isClickBlocked,
dispatch,
handleFocus,
version,
]
); );
const containerClass = version === 2 ? cssV2.container : css.container; const containerClass = version === 2 ? cssV2.container : css.container;

View File

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

View File

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

View File

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

View File

@@ -7,27 +7,28 @@ import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; 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 { 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 DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// import icon_arrow_right from '../../../../../assets/images/icons'; const Container = SpotlightContainerDecorator(
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png'; { enterTo: 'last-focused' },
import icon_shop_now from '../../../../../assets/images/player/icon_tabcontainer_shopnow.png'; 'div'
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 SpottableDiv = Spottable('div'); const SpottableDiv = Spottable('div');
@@ -50,18 +51,22 @@ export default function TabContainerV2({
onTabClose, // 탭 닫기 콜백 함수 onTabClose, // 탭 닫기 콜백 함수
tabVisible, tabVisible,
}) { }) {
const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos); const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos);
// 다음 재생 가능한 쇼 찾기 // 다음 재생 가능한 쇼 찾기
const findNextPlayableShow = useCallback((currentPlayList, currentIndex) => { const findNextPlayableShow = useCallback((currentPlayList, currentIndex) => {
if (!currentPlayList || currentPlayList.length === 0) return null; 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 initialIndex = nextIndex;
let attempts = 0; let attempts = 0;
// 유효한 showId를 가진 다음 쇼 찾기 // 유효한 showId를 가진 다음 쇼 찾기
while (!currentPlayList[nextIndex]?.showId && attempts < currentPlayList.length) { while (
!currentPlayList[nextIndex]?.showId &&
attempts < currentPlayList.length
) {
nextIndex = nextIndex === currentPlayList.length - 1 ? 0 : nextIndex + 1; nextIndex = nextIndex === currentPlayList.length - 1 ? 0 : nextIndex + 1;
attempts++; attempts++;
if (nextIndex === initialIndex) break; if (nextIndex === initialIndex) break;
@@ -87,13 +92,18 @@ export default function TabContainerV2({
data: youmaylikeInfos, data: youmaylikeInfos,
shopNowInfo_length: shopNowInfo?.length, shopNowInfo_length: shopNowInfo?.length,
shouldShowYouMayAlso: shouldShowYouMayAlso:
shopNowInfo && shopNowInfo.length < 3 && youmaylikeInfos && youmaylikeInfos.length > 0, shopNowInfo &&
shopNowInfo.length < 3 &&
youmaylikeInfos &&
youmaylikeInfos.length > 0,
}); });
}, [youmaylikeInfos, shopNowInfo]); }, [youmaylikeInfos, shopNowInfo]);
const tabList = [ const tabList = [
$L('SHOP NOW'), $L('SHOP NOW'),
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'), panelInfo?.shptmBanrTpNm === 'LIVE'
? $L('LIVE CHANNEL')
: $L('FEATURED SHOWS'),
]; ];
useEffect(() => { useEffect(() => {
@@ -105,7 +115,9 @@ export default function TabContainerV2({
if (tabIndex === 1) { if (tabIndex === 1) {
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE'; 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) { if (nowMenu) {
@@ -160,7 +172,9 @@ export default function TabContainerV2({
// 하나의 함수에서 모든 tabIndex 변화 처리 // 하나의 함수에서 모든 tabIndex 변화 처리
const handleTabIndexChange = useCallback((newTabIndex, oldTabIndex) => { 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) { if (newTabIndex === 0) {
// tabIndex = 0 (ShopNow) // tabIndex = 0 (ShopNow)
@@ -220,8 +234,18 @@ export default function TabContainerV2({
Spotlight.focus('shop-now-item-0'); Spotlight.focus('shop-now-item-0');
}} }}
> >
<div className={css.shopNowIconWrapper}> <div
<img src={icon_shop_now} alt="shop now icon" className={css.shopNowIcon} /> className={classNames(
css.shopNowIconWrapper,
playListInfo[selectedIndex]?.patncNm === 'QVC' &&
css.shopNowQvcIconWrapper
)}
>
<img
src={playListInfo[selectedIndex]?.patncLogoPath}
alt="shop now icon"
className={css.shopNowIcon}
/>
</div> </div>
<div className={css.shopNowHeaderText}>SHOP NOW</div> <div className={css.shopNowHeaderText}>SHOP NOW</div>
<div className={css.arrowIcon}> <div className={css.arrowIcon}>
@@ -250,7 +274,9 @@ export default function TabContainerV2({
youmaylikeInfos && youmaylikeInfos &&
youmaylikeInfos.length > 0 && ( youmaylikeInfos.length > 0 && (
<div className={css.youMayAlsoLikeHeader}> <div className={css.youMayAlsoLikeHeader}>
<div className={css.youMayAlsoLikeText}>You may also like</div> <div className={css.youMayAlsoLikeText}>
You may also like
</div>
</div> </div>
)} )}
</div> </div>
@@ -273,7 +299,11 @@ export default function TabContainerV2({
<SpottableDiv <SpottableDiv
className={css.liveChannelButton} className={css.liveChannelButton}
onClick={onLiveChannelButtonClick} 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} onSpotlightUp={handleSpotlightUpToBackButton}
onSpotlightDown={(e) => { onSpotlightDown={(e) => {
// 첫 번째 PlayerItem으로 포커스 이동 // 첫 번째 PlayerItem으로 포커스 이동

View File

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

View File

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