111 Commits

Author SHA1 Message Date
5ef0d8afae [251217] merge: gitlab develop_si 변경사항 병합 2025-12-17 14:21:27 +09:00
f6073d78c1 [251217] fix: LiveChannelContents Navigation
🕐 커밋 시간: 2025. 12. 17. 14:17:24

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 14:17:24 +09:00
junghoon86.park
0223499e12 [영상]
- 라이브 채널 next부분 관련해서 버튼부분의 배경 색상 제거
2025-12-17 13:46:23 +09:00
3fd3b66cb3 [251217] fix: PlayerPanel activity check 추가
🕐 커밋 시간: 2025. 12. 17. 13:43:33

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

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

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-12-17 13:43:33 +09:00
be9b1faeec [251217] fix: 비디오배너 클릭 방어로직추가
🕐 커밋 시간: 2025. 12. 17. 12:11:04

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx
2025-12-17 12:11:04 +09:00
07a042cca6 [251217] fix: PlayerPanel 배너동영상 위치 검증추가
🕐 커밋 시간: 2025. 12. 17. 12:03:44

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 12:03:44 +09:00
junghoon86.park
d93960f40a [영상]
- 프로그레스바 크기변경
 - 타임 노출부분 변경
 - cc 버튼 위치변경
2025-12-17 10:38:36 +09:00
4dfa15b4c0 [251217] fix: TabContainer.v2.jsx ShopNowButton 포커스 10ms
🕐 커밋 시간: 2025. 12. 17. 09:13:23

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx
2025-12-17 09:13:23 +09:00
d83e9d38f0 [251217] fix: ProductAllSection PRODUCT DETAIL버튼 조건부 렌더링
🕐 커밋 시간: 2025. 12. 17. 08:58:40

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-17 08:58:40 +09:00
8589cde061 [251216] fix: FeaturedBrandsPanel TItemCard to DetailPanel
🕐 커밋 시간: 2025. 12. 16. 17:29:43

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-16 17:29:44 +09:00
92964a5063 [251216] fix: DetailPanel skeleton비활성화
🕐 커밋 시간: 2025. 12. 16. 17:11:50

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

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

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-16 17:11:50 +09:00
dba79789a8 [251216] fix: DeepLink 처리 isDeepLinkEntry
🕐 커밋 시간: 2025. 12. 16. 17:00:52

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/deepLinkHandler.js
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
2025-12-16 17:00:52 +09:00
junghoon86.park
8a882c28ca [영상 노출에 따른 부분 수정]
- 영상 상하 블랙라인 관련 하여 스타일 수정
 - discimir부분 노출관련하여 처리.
2025-12-16 16:27:49 +09:00
61f67708a9 [251216] fix: 로그정리,PlayerPanel ShopNowContents
🕐 커밋 시간: 2025. 12. 16. 16:07:17

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ 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/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/utils/lodashFpEx.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선
2025-12-16 16:07:19 +09:00
c9b2e5daf5 [251216] fix: TrendingNowPanel DetailPanel MediaPanel popPanel fix
🕐 커밋 시간: 2025. 12. 16. 15:21:13

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-16 15:21:13 +09:00
4f4887ebdb [251216] fix: TrendingNowPanel PlayerPanel DetailPanel Bg Video Pause
🕐 커밋 시간: 2025. 12. 16. 14:39:08

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

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

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-16 14:39:08 +09:00
9d8cafc0a9 [251216] fix: TrendingNowPanel PlayerPanel Bg
🕐 커밋 시간: 2025. 12. 16. 14:29:22

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/TrendingNowPanel/TrendingNowPanel.jsx
2025-12-16 14:29:22 +09:00
junghoon86.park
929a9020a1 [youmayalsolike]
- updatePanel이 정상적으로 먹지 않는 문제가있어 pushpanel로 변경처리.
2025-12-16 14:20:25 +09:00
junghoon86.park
83905a092d [상품상세] description 클릭시 팝업 노출 처리
- 너무 긴경우 포커스가 넘어가는문제가 있어 클릭시에는 팝업이 노출되어 전체 영역을 확인할수있도록 수정.
2025-12-16 13:55:51 +09:00
e4a64644dd [251216] merge: resolve develop_si branch conflicts
Merge remote changes from gitlab/develop_si.

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-16 12:41:38 +09:00
55af96bd00 [251216] fix: Panel logs update - 1
🕐 커밋 시간: 2025. 12. 16. 12:39:11

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

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

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-16 12:39:12 +09:00
junghoon86.park
8325070138 [Tscrolldetail] 상세에서 오른쪽 스크롤 포커스 가지않도록 변경.
- focusableScrollbar를 false 로 강제로 줌.
2025-12-16 12:22:30 +09:00
junghoon86.park
bbb9e64120 [TheaderCustom]
- theme 상품 상세에서 theme명 노출부분 변경 처리.
2025-12-16 12:20:23 +09:00
99ea3e6595 [251216] fix: TrendingNowPanel 3-layer add
🕐 커밋 시간: 2025. 12. 16. 10:26:10

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
2025-12-16 10:26:10 +09:00
3dc4699479 [251216] fix: panelHistoryMiddleware log
🕐 커밋 시간: 2025. 12. 16. 10:15:57

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

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

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 코드 정리 및 최적화
2025-12-16 10:15:57 +09:00
junghoon86.park
8a3bcc1f9c [테마 아이템카드] 목업 데이터 제거
- 목업 데이터 제거.
2025-12-16 09:31:34 +09:00
junghoon86.park
486fb5efd5 [상품 상세] panelInfo?.patnrId 내려오는 부분 수정
- string 으로 내려와서 number로 넣어둔부분은 제거.
2025-12-15 17:01:27 +09:00
junghoon86.park
802484debd [검색] 음성검색관련 마이크 노출 주석처리
- 음성 검색우선 제외로 인하여 마이크 버튼주석처리.
2025-12-15 16:23:54 +09:00
junghoon86.park
c540378cb5 [foryou] 로그인, 비로그인시 노출 차이 수정
- 포유 아이콘 관련 처리부분 비로그인시 노출안되도록 처리.
 - 픽포유는 홈패널에서 노출로 처리.
2025-12-15 16:15:51 +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
126 changed files with 6155 additions and 1334 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

@@ -85,6 +85,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
chanId: chanId, chanId: chanId,
shptmBanrTpNm: "LIVE", shptmBanrTpNm: "LIVE",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -101,6 +102,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
showId: showId, showId: showId,
shptmBanrTpNm: "VOD", shptmBanrTpNm: "VOD",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -274,6 +276,18 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
const action = const action =
panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel; panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel;
// 🔽 LS(Live Show) 또는 VS(VOD Show)인 경우 DeepLink 진입 플래그 설정
if ((type === 'LS' || type === 'VS') && action === pushPanel) {
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
isDeepLinkEntry: true, // DeepLink PlayerPanel 진입 플래그
},
})
);
}
dispatch( dispatch(
action({ action({
name: panelName, name: panelName,

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

@@ -99,17 +99,25 @@ export const finishMediaPreview = () => (dispatch, getState) => {
export const finishModalMediaForce = () => (dispatch, getState) => { export const finishModalMediaForce = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] finishModalMediaForce called', {
// panelCount: panels.length,
// panelNames: panels.map((p) => p.name),
// });
const hasProductVideoPanel = panels.some( const hasProductVideoPanel = panels.some(
(panel) => (panel) =>
panel.name === panel_names.MEDIA_PANEL && panel.name === panel_names.MEDIA_PANEL &&
(panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player') (panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player')
); );
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] hasProductVideoPanel:', hasProductVideoPanel);
if (hasProductVideoPanel) { if (hasProductVideoPanel) {
if (startMediaFocusTimer) { if (startMediaFocusTimer) {
clearTimeout(startMediaFocusTimer); clearTimeout(startMediaFocusTimer);
startMediaFocusTimer = null; startMediaFocusTimer = null;
} }
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] Calling popPanel(panel_names.MEDIA_PANEL)');
dispatch(popPanel(panel_names.MEDIA_PANEL)); dispatch(popPanel(panel_names.MEDIA_PANEL));
} }
}; };

View File

@@ -39,10 +39,27 @@ export const pushPanel = (panel, duplicatable = false) => ({
duplicatable: duplicatable, duplicatable: duplicatable,
}); });
export const popPanel = (panelName) => ({ export const popPanel = (panelName) => {
type: types.POP_PANEL, const stack = new Error().stack;
payload: panelName, const stackLines = stack?.split('\n') || [];
});
// console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action dispatcher - REMOVING PANEL:', {
// panelName,
// timestamp: Date.now(),
// fullStack: stackLines.slice(1, 6).map((line) => line.trim()),
// });
if (DEBUG_MODE) {
console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action creator stack:', {
panelName,
caller: stackLines[2]?.trim(),
});
}
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 +110,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 +198,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 +239,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 +306,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 +521,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 +569,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();
dlog('[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) {
dlog('[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));
@@ -135,14 +148,14 @@ export const startVideoPlayer =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// console.log('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId); // dlog('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// console.log('[startVideoPlayer] 🔍 Spotlight.focus called'); // dlog('[startVideoPlayer] 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// console.log('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // dlog('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayer] ✅ END'); dlog('[startVideoPlayer] ✅ END');
@@ -182,6 +195,17 @@ export const startVideoPlayerNew =
...rest ...rest
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim();
dlog('[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) {
dlog('[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));
@@ -305,14 +332,14 @@ export const startVideoPlayerNew =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// console.log('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId); // dlog('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// console.log('[startVideoPlayerNew] *** 🔍 Spotlight.focus called'); // dlog('[startVideoPlayerNew] *** 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// console.log('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // dlog('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayerNew] *** ✅ END'); dlog('[startVideoPlayerNew] *** ✅ END');
@@ -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) {
dlog('[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
); );
dlog('[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) => {
}, },
}) })
); );
dlog('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', {
timestamp: Date.now(),
});
} else {
dlog('[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
); );
dlog('[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) => {
}, },
}) })
); );
dlog('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', {
timestamp: Date.now(),
});
} else {
dlog('[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
); );
dlog('[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) => {
}, },
}) })
); );
dlog('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', {
timestamp: Date.now(),
});
} else {
dlog('[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); dlog('[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) => {
}, },
}) })
); );
dlog('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
timestamp: Date.now(),
});
} else { } else {
dlog('[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)');
} }
@@ -514,7 +601,7 @@ export const hideModalVideo = () => (dispatch, getState) => {
}, },
}; };
// console.log('[HomePanel] hideModalVideo: saving shrinkInfo', { // dlog('[HomePanel] hideModalVideo: saving shrinkInfo', {
// shrinkInfo: updatedPlayerState.shrinkInfo, // shrinkInfo: updatedPlayerState.shrinkInfo,
// modalStyle: panelInfo.modalStyle, // modalStyle: panelInfo.modalStyle,
// }); // });
@@ -951,6 +1038,12 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다. * 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
*/ */
export const goToFullScreen = () => (dispatch, getState) => { export const goToFullScreen = () => (dispatch, getState) => {
dlog('[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) => {
}, },
}) })
); );
dlog('[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;
dlog('[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');
dlog('[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
dlog('[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) => {
) )
); );
dlog('[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

@@ -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",
@@ -149,7 +151,6 @@ export const URLS = {
// 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",
SET_MYPAGE_TERMS_AGREE: "/lgsp/v1/mypage/terms/agree.lge", SET_MYPAGE_TERMS_AGREE: "/lgsp/v1/mypage/terms/agree.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

@@ -34,7 +34,7 @@
padding: @slider-padding-v 0; padding: @slider-padding-v 0;
height: @sand-mediaplayer-slider-height; height: @sand-mediaplayer-slider-height;
right: 154px; right: 154px;
width: 1466px; width: 1558px;
// Add a tap area that extends to the edges of the screen, to make the slider more accessible // Add a tap area that extends to the edges of the screen, to make the slider more accessible
&::before { &::before {
content: ""; content: "";

View File

@@ -7,7 +7,7 @@
position: absolute; position: absolute;
font-family: @baseFont; font-family: @baseFont;
width: 100%; width: 100%;
right: 90px; right: 20px;
bottom: -5px; bottom: -5px;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@@ -16,12 +16,12 @@
letter-spacing: -1px; letter-spacing: -1px;
.separator { .separator {
position: absolute; position: absolute;
right: 105px; right: 95px;
bottom: -5px; bottom: -5px;
} }
.currentTime { .currentTime {
position: absolute; position: absolute;
right: 130px; right: 120px;
bottom: -5px; bottom: -5px;
} }
.totalTime { .totalTime {

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';

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 }) => {
@@ -18,7 +30,6 @@ const OptionalTermsConfirm = ({ open }) => {
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")
); );
@@ -72,10 +83,10 @@ const OptionalTermsConfirm = ({ open }) => {
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 (

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();
@@ -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);

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

@@ -483,7 +483,7 @@
.default-style(); .default-style();
.scrollInfo { .scrollInfo {
width: 900px; width: 850px;
background-color: @BG_COLOR_01; background-color: @BG_COLOR_01;
color: @COLOR_GRAY03; color: @COLOR_GRAY03;
display: flex; display: flex;

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

@@ -33,8 +33,8 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
(action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY') (action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY')
) { ) {
const caller = new Error().stack.split('\n')[1]?.trim(); const caller = new Error().stack.split('\n')[1]?.trim();
console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`); // console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
console.log(' Payload:', action.payload); // console.log(' Payload:', action.payload);
} }
// GNB 호출 식별을 위한 helper 함수 // GNB 호출 식별을 위한 helper 함수
@@ -81,7 +81,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] PUSH_PANEL:', { console.log(`[PANEL] PUSH_PANEL: ${panelName}`, {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -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,11 +125,19 @@ 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: ${topPanel?.name}`, {
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 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] POP_PANEL:', { console.log(`[PANEL] POP_PANEL: ${topPanel.name}`, {
panelName: topPanel.name, panelName: topPanel.name,
panelInfo: topPanel.panelInfo || {}, panelInfo: topPanel.panelInfo || {},
isGNB, isGNB,
@@ -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,
}); });
@@ -177,7 +185,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] UPDATE_PANEL:', { console.log(`[PANEL] UPDATE_PANEL: ${panelName}`, {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -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,
}); });
@@ -218,11 +226,15 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화 // RESET_PANELS: GNB 네비게이션 또는 완전 초기화
case types.RESET_PANELS: { case types.RESET_PANELS: {
if (DEBUG_MODE) if (DEBUG_MODE) {
console.log('[PANEL] RESET_PANELS:', { const resetPanelNameForLog = (action.payload && action.payload.length > 0)
? action.payload[0].name
: 'homepanel';
console.log(`[PANEL] RESET_PANELS: ${resetPanelNameForLog}`, {
payload: action.payload, payload: action.payload,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory); console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory);
@@ -284,7 +296,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

@@ -94,6 +94,12 @@ export const panelsReducer = (state = initialState, action) => {
} }
case types.POP_PANEL: { case types.POP_PANEL: {
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer START', {
// targetPanel: action.payload || 'last_panel',
// currentPanels: state.panels.map((p) => p.name),
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL START', { dlog('[panelReducer] 🔴 POP_PANEL START', {
targetPanel: action.payload || 'last_panel', targetPanel: action.payload || 'last_panel',
currentPanels: state.panels.map((p) => p.name), currentPanels: state.panels.map((p) => p.name),
@@ -118,6 +124,13 @@ export const panelsReducer = (state = initialState, action) => {
resultPanels = state.panels.slice(0, state.panels.length - 1); resultPanels = state.panels.slice(0, state.panels.length - 1);
} }
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer END', {
// resultPanels: resultPanels.map((p) => p.name),
// panelCount: resultPanels.length,
// lastAction,
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL END', { dlog('[panelReducer] 🔴 POP_PANEL END', {
resultPanels: resultPanels.map((p) => p.name), resultPanels: resultPanels.map((p) => p.name),
lastAction, lastAction,
@@ -132,6 +145,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 +210,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

@@ -114,6 +114,7 @@ export const ACTIVE_POPUP = {
optionalConfirm: 'optionalConfirm', optionalConfirm: 'optionalConfirm',
energyPopup: 'energyPopup', energyPopup: 'energyPopup',
addCartPopup: 'addCartPopup', addCartPopup: 'addCartPopup',
scrollPopup: 'scrollPopup',
}; };
export const DEBUG_VIDEO_SUBTITLE_TEST = false; export const DEBUG_VIDEO_SUBTITLE_TEST = false;
export const AUTO_SCROLL_DELAY = 600; export const AUTO_SCROLL_DELAY = 600;

View File

@@ -458,7 +458,7 @@ const tap = fp.curry((fn, value) => {
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const trace = fp.curry((label, value) => { const trace = fp.curry((label, value) => {
console.log(label, value); // console.log(label, value);
return value; return value;
}); });

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

@@ -280,6 +280,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
useEffect(() => { useEffect(() => {
return () => { return () => {
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] DetailPanel cleanup - calling finishModalMediaForce');
dispatch(finishModalMediaForce()); dispatch(finishModalMediaForce());
}; };
}, [dispatch]); }, [dispatch]);
@@ -303,18 +304,25 @@ 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('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel cleanup/unmount triggered', {
// sourcePanel,
// sourceMenu,
// panelsSnapshot: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
// DetailPanel이 unmount되는 시점 // DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount:', { // console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel unmount details:', {
sourcePanel, // sourcePanel,
sourceMenu, // sourceMenu,
timestamp: Date.now(), // timestamp: Date.now(),
}); // });
// sourcePanel에 따른 상태 업데이트 // sourcePanel에 따른 상태 업데이트
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: { case panel_names.PLAYER_PANEL: {
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달'); console.log('[PANEL][DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -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 해제
}, },
}) })
); );
@@ -376,6 +385,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('[🟠UNIQUE_DETAIL_BACK🟠] onBackClick triggered', {
// sourcePanel,
// sourceMenu,
// isCancelClick,
// currentPanels: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
fp.pipe( fp.pipe(
() => { () => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
@@ -384,7 +401,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: case panel_names.PLAYER_PANEL:
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리 // PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행'); console.log('[🟠UNIQUE_DETAIL_BACK🟠] PlayerPanel 출신: 모달 정리만 수행');
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
@@ -393,17 +410,18 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
case panel_names.SEARCH_PANEL: case panel_names.SEARCH_PANEL:
default: default:
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지 // HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
console.log( // console.log(
'[DetailPanel] onBackClick - source panel:', // '[🟠UNIQUE_DETAIL_BACK🟠] source panel:',
sourcePanel, // sourcePanel,
'백그라운드 비디오 일시 중지' // '백그라운드 비디오 일시 중지'
); // );
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
} }
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] Calling popPanel(DETAIL_PANEL)');
dispatch(popPanel(panel_names.DETAIL_PANEL)); dispatch(popPanel(panel_names.DETAIL_PANEL));
}, },
() => { () => {
@@ -425,7 +443,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
if (shouldUpdatePanel) { if (shouldUpdatePanel) {
console.log( console.log(
'[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달' '[PANEL][DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'
); );
dispatch( dispatch(
updatePanel({ updatePanel({
@@ -1025,34 +1043,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 +1120,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

@@ -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,9 +1,32 @@
import React, { useCallback } from "react"; import React, {
import css from "./ProductDescription.module.less"; useCallback,
import { $L, removeSpecificTags } from "../../../../utils/helperMethods"; useMemo,
import Spottable from "@enact/spotlight/Spottable"; } from 'react';
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import Spotlight from "@enact/spotlight"; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import {
setHidePopup,
setShowPopup,
} from '../../../../actions/commonActions';
import TButtonScroller
from '../../../../components/TButtonScroller/TButtonScroller';
import TNewPopUp from '../../../../components/TPopUp/TNewPopUp';
import * as Config from '../../../../utils/Config';
import {
$L,
removeSpecificTags,
} from '../../../../utils/helperMethods';
import css from './ProductDescription.module.less';
// TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해 // TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
@@ -19,11 +42,22 @@ const Container = SpotlightContainerDecorator(
); );
export default function ProductDescription({ productInfo }) { export default function ProductDescription({ productInfo }) {
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const dispatch = useDispatch();
const productDescription = useCallback(() => { const productDescription = useCallback(() => {
const sanitizedString = removeSpecificTags(productInfo?.prdtDesc); const sanitizedString = removeSpecificTags(productInfo?.prdtDesc);
return { __html: sanitizedString }; return { __html: sanitizedString };
}, [productInfo?.prdtDesc]); }, [productInfo?.prdtDesc]);
const productDescriptionText = useMemo(() => {
return removeSpecificTags(productInfo?.prdtDesc);
}, [productInfo?.prdtDesc]);
// 왼쪽 화살표 키 이벤트 처리 // 왼쪽 화살표 키 이벤트 처리
const handleKeyDown = useCallback((ev) => { const handleKeyDown = useCallback((ev) => {
if (ev.keyCode === 37) { // 왼쪽 화살표 키 if (ev.keyCode === 37) { // 왼쪽 화살표 키
@@ -34,6 +68,20 @@ export default function ProductDescription({ productInfo }) {
} }
}, []); }, []);
const descriptionClick = useCallback(() => {
dispatch(setShowPopup(Config.ACTIVE_POPUP.scrollPopup));
},
[dispatch]
);
const _onClose = useCallback(()=>{
dispatch(setHidePopup());
// Restore focus to the description content after popup closes
setTimeout(() => {
Spotlight.focus('product-description-content');
}, 100);
},[dispatch])
// ProductDescription: Container 직접 사용 패턴 // ProductDescription: Container 직접 사용 패턴
// prdtDesc가 없으면 렌더링하지 않음 // prdtDesc가 없으면 렌더링하지 않음
if (!productInfo?.prdtDesc) { if (!productInfo?.prdtDesc) {
@@ -41,36 +89,61 @@ export default function ProductDescription({ productInfo }) {
} }
return ( return (
<Container <>
className={css.descriptionContainer} <Container
spotlightId="product-description-container" className={css.descriptionContainer}
> spotlightId="product-description-container"
{/* <SpottableComponent
className={css.titleWrapper}
spotlightId="product-description-title"
onClick={() => console.log("[ProductDescription] Title clicked")}
onFocus={() => console.log("[ProductDescription] Title focused")}
onBlur={() => console.log("[ProductDescription] Title blurred")}
> */}
<div className={css.titleWrapper}>
<div className={css.title}>{$L("DESCRIPTION")}</div>
</div>
{/* </SpottableComponent> */}
<SpottableComponent
className={css.descriptionWrapper}
spotlightId="product-description-content"
onClick={() => console.log("[ProductDescription] Content clicked")}
onFocus={() => console.log("[ProductDescription] Content focused")}
onBlur={() => console.log("[ProductDescription] Content blurred")}
onKeyDown={handleKeyDown}
> >
<div {/* <SpottableComponent
className={css.productDescription} className={css.titleWrapper}
dangerouslySetInnerHTML={productDescription()} spotlightId="product-description-title"
/> onClick={() => console.log("[ProductDescription] Title clicked")}
</SpottableComponent> onFocus={() => console.log("[ProductDescription] Title focused")}
</Container> onBlur={() => console.log("[ProductDescription] Title blurred")}
> */}
<div className={css.titleWrapper}>
<div className={css.title}>{$L("DESCRIPTION")}</div>
</div>
{/* </SpottableComponent> */}
<SpottableComponent
className={css.descriptionWrapper}
spotlightId="product-description-content"
// onClick={() => console.log("[ProductDescription] Content clicked")}
onClick={descriptionClick}
onFocus={() => console.log("[ProductDescription] Content focused")}
onBlur={() => console.log("[ProductDescription] Content blurred")}
onKeyDown={handleKeyDown}
>
<div
className={css.productDescription}
dangerouslySetInnerHTML={productDescription()}
/>
</SpottableComponent>
</Container>
{activePopup === Config.ACTIVE_POPUP.scrollPopup && (
<TNewPopUp
kind="scrollPopup"
open={popupVisible}
hasText
title={$L("DESCRIPTION")}
onClick={_onClose}
hasButton
button1Text={$L("OK")}
>
<TButtonScroller
boxHeight={460}
width={844}
kind={"figmaTermsPopup"}
>
<div
className={css.scrollContainer}
dangerouslySetInnerHTML={{ __html: productDescriptionText }}
/>
</TButtonScroller>
</TNewPopUp>
)}
</>
); );
} }

View File

@@ -51,3 +51,8 @@
} }
} }
.scrollContainer {
padding: 31px;
font-size: 26px;
line-height: 1.5;
}

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

@@ -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

@@ -5,11 +5,10 @@
position: relative; position: relative;
width: 1114px; // ProductDetail과 동일한 고정 크기 width: 1114px; // ProductDetail과 동일한 고정 크기
max-width: 1114px; max-width: 1114px;
height: 740px; // ProductDetail과 동일한 고정 높이 height: 632px !important; // ProductDetail과 동일한 고정 높이
margin-bottom: 30px; // ProductDetail과 동일한 간격
cursor: pointer; cursor: pointer;
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
border-radius: 12px; border-radius: 12px 12px 0 0;
box-sizing: border-box; box-sizing: border-box;
padding: 6px; // 포커스 테두리를 위한 공간 padding: 6px; // 포커스 테두리를 위한 공간
overflow: hidden; overflow: hidden;
@@ -80,7 +79,7 @@
z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임 z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임
border: 6px solid @PRIMARY_COLOR_RED; border: 6px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
border-radius: 12px; border-radius: 12px 12px 0px 0px;
content: ""; content: "";
} }
@@ -217,30 +216,28 @@
} }
.notice { .notice {
width: calc(100% - 10px);
height: 54px;
background: #000000;
.flex(@justifyCenter:flex-start);
padding: 6px 18px 18px 18px;
border-radius: 0 0 12px 12px;
margin-bottom: 30px; // ProductDetail과 동일한 간격
.marquee {
width: 100%; width: 100%;
height: 54px; height: 100%;
background: #000000;
.flex(@justifyCenter:flex-start);
padding: 6px 18px 18px 18px;
position: absolute;
bottom: 0;
border-radius: 0 0 12px 12px;
.marquee {
width: 100%;
height: 100%;
}
img {
width: 18px;
height: 18px;
margin: 10px 12px 0 0;
object-fit: contain;
}
span {
line-height: normal;
letter-spacing: normal;
text-align: left;
.font(@fontFamily:@baseFont, @fontSize:20px);
color: @COLOR_GRAY04;
}
} }
img {
width: 18px;
height: 18px;
margin: 10px 12px 0 0;
object-fit: contain;
}
span {
line-height: normal;
letter-spacing: normal;
text-align: left;
.font(@fontFamily:@baseFont, @fontSize:20px);
color: @COLOR_GRAY04;
}
}

View File

@@ -311,6 +311,7 @@ export default function ProductVideo({
if (!canPlayVideo) return null; if (!canPlayVideo) return null;
return ( return (
<>
<SpottableComponent <SpottableComponent
className={css.videoContainer} className={css.videoContainer}
onClick={handleVideoClick} onClick={handleVideoClick}
@@ -330,12 +331,13 @@ export default function ProductVideo({
<img src={playImg} alt="재생" /> <img src={playImg} alt="재생" />
</div> </div>
</div> </div>
<div className={css.notice}>
<Marquee className={css.marquee} marqueeOn="render">
<img src={ic_warning} alt={disclaimer} />
<span>{disclaimer}</span>
</Marquee>
</div>
</SpottableComponent> </SpottableComponent>
<div className={css.notice}>
<Marquee className={css.marquee} marqueeOn="render">
<img src={ic_warning} alt={disclaimer} />
<span>{disclaimer}</span>
</Marquee>
</div>
</>
); );
} }

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

@@ -1,21 +1,41 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
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 { clearThemeDetail } from '../../../../actions/homeActions'; import { clearThemeDetail } from '../../../../actions/homeActions';
import { finishModalMediaForce } from '../../../../actions/mediaActions'; import { finishModalMediaForce } from '../../../../actions/mediaActions';
import { popPanel, pushPanel, updatePanel } from '../../../../actions/panelActions'; import {
popPanel,
pushPanel,
updatePanel,
} from '../../../../actions/panelActions';
import { finishVideoPreview } from '../../../../actions/playActions'; import { finishVideoPreview } from '../../../../actions/playActions';
import THeader from '../../../../components/THeader/THeader'; import THeader from '../../../../components/THeader/THeader';
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new'; import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
import TVerticalPagenator from '../../../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; from '../../../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList
from '../../../../components/TVirtualGridList/TVirtualGridList';
import useScrollTo from '../../../../hooks/useScrollTo'; import useScrollTo from '../../../../hooks/useScrollTo';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods'; import { $L } from '../../../../utils/helperMethods';
import css from './YouMayAlsoLike.module.less'; import css from './YouMayAlsoLike.module.less';
@@ -158,7 +178,7 @@ export default function YouMayAlsoLike({
// DetailPanel을 언마운트하지 않고 상품 정보만 업데이트 // DetailPanel을 언마운트하지 않고 상품 정보만 업데이트
// 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨 // 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨
dispatch( dispatch(
updatePanel({ pushPanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
panelInfo: { panelInfo: {
showNm: panelInfo?.showNm, showNm: panelInfo?.showNm,
@@ -170,8 +190,8 @@ export default function YouMayAlsoLike({
launchedFromPlayer: launchedFromPlayer, launchedFromPlayer: launchedFromPlayer,
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지 bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
fromPanel: { fromPanel: {
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시 fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
}, // 출처 정보 통합 객체 }, // 출처 정보 통합 객체
}, },
}) })
); );

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,

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

@@ -78,35 +78,35 @@ export default function ThemeItemCard({
const { originalPrice, discountedPrice, discountRate } = const { originalPrice, discountedPrice, discountRate } =
usePriceInfo(priceInfo) || {}; usePriceInfo(priceInfo) || {};
const mockEnergyLabel = [ // const mockEnergyLabel = [
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "A", // "enrgClasCd": "A",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
}, // },
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "D", // "enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
}, // },
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "D", // "enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
} // }
]; // ];
const setEnactFitZIndex = (zIndexValue) => { const setEnactFitZIndex = (zIndexValue) => {
const target = document.getElementById("floatLayer"); const target = document.getElementById("floatLayer");
@@ -211,7 +211,7 @@ export default function ThemeItemCard({
))} ))}
</div> </div>
)} */} )} */}
{mockEnergyLabel && mockEnergyLabel.length > 0 && ( {/* {mockEnergyLabel && mockEnergyLabel.length > 0 && (
<div className={css.energyLabels}> <div className={css.energyLabels}>
{mockEnergyLabel.map((label, labelIndex) => ( {mockEnergyLabel.map((label, labelIndex) => (
<SpottableTemp <SpottableTemp
@@ -228,7 +228,7 @@ export default function ThemeItemCard({
</SpottableTemp> </SpottableTemp>
))} ))}
</div> </div>
)} )} */}
</div> </div>
</SpottableDiv> </SpottableDiv>
{(() => { {(() => {

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

@@ -93,9 +93,6 @@ export default function THeaderCustom({
role="button" role="button"
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: themeTitle }} />
)}
{kind ? ( {kind ? (
"" ""
) : ( ) : (
@@ -107,13 +104,16 @@ export default function THeaderCustom({
}} }}
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: `[${themeTitle}]` }} />
)}
<Marquee <Marquee
marqueeOn="render" marqueeOn="render"
className={css.title} className={css.title}
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

@@ -61,4 +61,5 @@
color: #eaeaea; color: #eaeaea;
width: max-content; width: max-content;
margin-right: 20px; margin-right: 20px;
margin-left: 10px;
} }

View File

@@ -1,14 +1,26 @@
import React, { useCallback, useEffect, useRef, useState, useMemo, forwardRef } from 'react'; import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { off, on } from '@enact/core/dispatcher'; import {
off,
on,
} from '@enact/core/dispatcher';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
import Scroller from '@enact/sandstone/Scroller'; import Scroller from '@enact/sandstone/Scroller';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import AutoScrollAreaDetail, { POSITION } from '../AutoScrollAreaDetail/AutoScrollAreaDetail'; import AutoScrollAreaDetail, {
POSITION,
} from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
import css from './TScrollerDetail.module.less'; import css from './TScrollerDetail.module.less';
/** /**
@@ -206,7 +218,8 @@ const TScrollerDetail = forwardRef(
onScrollStop={_onScrollStop} onScrollStop={_onScrollStop}
onScroll={_onScroll} onScroll={_onScroll}
scrollMode={scrollMode || 'translate'} scrollMode={scrollMode || 'translate'}
focusableScrollbar={focusableScrollbar} // focusableScrollbar={focusableScrollbar}
focusableScrollbar={false}
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)} className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
direction={direction} direction={direction}
horizontalScrollbar={horizontalScrollbar} horizontalScrollbar={horizontalScrollbar}
@@ -220,7 +233,7 @@ const TScrollerDetail = forwardRef(
}} }}
noScrollByWheel={noScrollByWheel} noScrollByWheel={noScrollByWheel}
noScrollByDrag noScrollByDrag
// rest props에서 ref만 제외하고 전달 // rest props에서 ref만 제외하고 전달
{...(rest.ref ? { ...rest, ref: undefined } : rest)} {...(rest.ref ? { ...rest, ref: undefined } : rest)}
> >
{children} {children}

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

@@ -5,8 +5,8 @@ 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 { updatePanel } from "../../../../../actions/panelActions"; import { pushPanel, updatePanel } from "../../../../../actions/panelActions";
import { startVideoPlayer } from "../../../../../actions/playActions"; // import { startVideoPlayer } from "../../../../../actions/playActions";
import TItemCard, { import TItemCard, {
removeDotAndColon, removeDotAndColon,
} from "../../../../../components/TItemCard/TItemCard"; } from "../../../../../components/TItemCard/TItemCard";
@@ -113,27 +113,36 @@ export default function RecommendedShowsProductList({
); );
} }
let y = // 🆕 DetailPanel로 이동 (ShopByShow 방식)
index < 2
? 0
: index === 2
? scaleH(208)
: scaleH(index * 248 - 248 - 40);
dispatch( dispatch(
startVideoPlayer({ pushPanel({
modal: false, name: panel_names.DETAIL_PANEL,
patnrId, panelInfo: { patnrId, prdtId },
prdtId,
showId,
shptmBanrTpNm: "VOD",
thumbnail: videoThumbnail,
targetId: "spotlightId-" + prdtId,
y,
}) })
); );
// 🔴 기존 PlayerPanel 로직 (주석처리)
// let y =
// index < 2
// ? 0
// : index === 2
// ? scaleH(208)
// : scaleH(index * 248 - 248 - 40);
//
// dispatch(
// startVideoPlayer({
// modal: false,
// patnrId,
// prdtId,
// showId,
// shptmBanrTpNm: "VOD",
// thumbnail: videoThumbnail,
// targetId: "spotlightId-" + prdtId,
// y,
// })
// );
}, },
[catCd, dispatch, patnrId, showId, videoThumbnail] [catCd, dispatch, patnrId]
); );
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {

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

@@ -6,7 +6,11 @@ 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 { pushPanel, updatePanel, navigateFromBestSeller } from '../../../actions/panelActions'; import {
navigateFromBestSeller,
pushPanel,
updatePanel,
} from '../../../actions/panelActions';
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions'; import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
@@ -15,13 +19,20 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller'; import TScroller from '../../../components/TScroller/TScroller';
import useScrollReset from '../../../hooks/useScrollReset'; import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../utils/Config';
import { $L, scaleW } from '../../../utils/helperMethods'; import { $L, scaleW } from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './BestSeller.module.less'; import css from './BestSeller.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const Container = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
const BestSeller = ({ const BestSeller = ({
order, order,
@@ -33,15 +44,26 @@ const BestSeller = ({
shelfTitle, shelfTitle,
}) => { }) => {
const { getScrollTo, scrollLeft } = useScrollTo(); const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true); const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { cursorVisible } = useSelector((state) => state.common.appStatus); const { cursorVisible } = useSelector((state) => state.common.appStatus);
const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller); const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
);
const bestSellerNewDatas = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct); const bestSellerNewDatas = useSelector(
(state) => state.foryou?.recommendInfo?.recommendProduct
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const [drawChk, setDrawChk] = useState(false); const [drawChk, setDrawChk] = useState(false);
const [firstChk, setFirstChk] = useState(0); const [firstChk, setFirstChk] = useState(0);
@@ -51,37 +73,45 @@ const BestSeller = ({
useEffect(() => { useEffect(() => {
setBestInfos( setBestInfos(
bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // 기본값으로 빈 배열 설정 bestSellerNewDatas?.filter(
(item) => item.recommendTpCd === 'BESTSELLER'
) || [] // 기본값으로 빈 배열 설정
); );
}, [bestSellerNewDatas]); }, [bestSellerNewDatas]);
useEffect(() => { useEffect(() => {
if (!bestInfos || bestInfos.length === 0) { if (userNumber) {
if (!bestInfos || bestInfos.length === 0) {
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: false,
})) || [];
setBestItemNewData(baseData);
return;
}
const recommendedData =
bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(
recommendedData.map((item) => item.prdtId)
);
const baseData = const baseData =
bestSellerDatas?.map((item) => ({ bestSellerDatas?.map((item) => ({
...item, ...item,
foryou: false, foryou: recommendedPrdtIds.has(item.prdtId),
})) || []; })) || [];
setBestItemNewData(baseData); setBestItemNewData(baseData);
return; } else {
setBestItemNewData(bestSellerDatas);
} }
}, [bestSellerDatas, bestInfos, userNumber]);
const recommendedData =
bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
setBestItemNewData(baseData);
}, [bestSellerDatas, bestInfos]);
const orderStyle = useMemo(() => ({ order: order }), [order]); const orderStyle = useMemo(() => ({ order: order }), [order]);
@@ -144,7 +174,10 @@ const BestSeller = ({
if (c) { if (c) {
let cAriaLabel = c.getAttribute('aria-label'); let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) { if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', ''); const newcAriaLabel = cAriaLabel.replace(
'Best Seller, Heading 1,',
''
);
c.setAttribute('aria-label', newcAriaLabel); c.setAttribute('aria-label', newcAriaLabel);
} }
} }

View File

@@ -53,6 +53,10 @@ 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);
const isDeepLinkEntry = useSelector(
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
);
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리 // 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
const focusHistory = useFocusHistory({ const focusHistory = useFocusHistory({
enableLogging: true, enableLogging: true,
@@ -163,7 +167,11 @@ export default function HomeBanner({
videoData = targetBannerData.bannerDetailInfos?.[0]; videoData = targetBannerData.bannerDetailInfos?.[0];
} }
if (videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) { // 🔽 [251221] DetailPanel이나 DeepLink PlayerPanel이 떠 있으면 배너 자동 재생 스킵
const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL);
const hasPlayerPanel = panels.some((p) => p.name === panel_names.PLAYER_PANEL);
if (!hasDetailPanel && !hasPlayerPanel && !isDeepLinkEntry && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus); console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
dispatch( dispatch(
@@ -175,12 +183,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, isDeepLinkEntry]);
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) => {
@@ -490,6 +520,14 @@ export default function RandomUnit({
}; };
break; break;
case 'DSP00510':
linkInfo = {
name: panel_names.JUST_FOR_YOU_TEST_PANEL,
panelInfo: {
},
};
break;
default: default:
linkInfo = { linkInfo = {
name: panel_names.HOME_PANEL, name: panel_names.HOME_PANEL,
@@ -579,6 +617,15 @@ export default function RandomUnit({
// 비디오 클릭 // 비디오 클릭
const videoClick = useCallback(() => { const videoClick = useCallback(() => {
// 🔽 비디오가 다른 배너에서 modal=true로 이미 재생 중이면 클릭 무시
if (playerPanelInfo?.modal === true && currentVideoBannerId && currentVideoBannerId !== spotlightId) {
console.log('[RandomUnit] videoClick 무시: 다른 배너에서 modal=true로 재생 중', {
currentVideoBannerId,
clickedBannerId: spotlightId,
});
return;
}
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent()); const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
if (lastFocusedTargetId) { if (lastFocusedTargetId) {
@@ -636,6 +683,7 @@ export default function RandomUnit({
sendBannerLog, sendBannerLog,
onBlur, onBlur,
playerPanelInfo?.modal, playerPanelInfo?.modal,
currentVideoBannerId,
dispatch, dispatch,
handleStartVideo, handleStartVideo,
]); ]);
@@ -644,6 +692,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(() => {
let _nowMenu = nowMenu; let _nowMenu = nowMenu;
@@ -999,7 +1070,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 {

Some files were not shown because too many files have changed in this diff Show More