Compare commits
106 Commits
develop_do
...
backup-202
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a042cca6 | |||
|
|
d93960f40a | ||
| 4dfa15b4c0 | |||
| d83e9d38f0 | |||
| 8589cde061 | |||
| 92964a5063 | |||
| dba79789a8 | |||
|
|
8a882c28ca | ||
| 61f67708a9 | |||
| c9b2e5daf5 | |||
| 4f4887ebdb | |||
| 9d8cafc0a9 | |||
|
|
929a9020a1 | ||
|
|
83905a092d | ||
| e4a64644dd | |||
| 55af96bd00 | |||
|
|
8325070138 | ||
|
|
bbb9e64120 | ||
| 99ea3e6595 | |||
| 3dc4699479 | |||
|
|
8a3bcc1f9c | ||
|
|
486fb5efd5 | ||
|
|
802484debd | ||
|
|
c540378cb5 | ||
|
|
cb3a4e9bc7 | ||
|
|
3ce4398e67 | ||
|
|
78153bae0c | ||
| 2c681bab68 | |||
| ab2dd7385b | |||
| ac5414a5fe | |||
|
|
f46090863f | ||
|
|
1e9c9bee40 | ||
|
|
f514e2468c | ||
|
|
1305158113 | ||
|
|
e97172fad5 | ||
| 42f58bf10c | |||
| e424ab761c | |||
| f62ccef420 | |||
| 1ee664e8c1 | |||
|
|
16a09b2e2b | ||
|
|
4fcd87da7d | ||
|
|
9c2ecbaa57 | ||
|
|
ad8fc598b4 | ||
|
|
ccc91ec662 | ||
|
|
b3b1151a1d | ||
|
|
4a70f321ed | ||
| ddd5d5c7ba | |||
| 9681eb42e1 | |||
| a3fe60ca70 | |||
|
|
0593f54d6e | ||
| d903610709 | |||
|
|
bc6119f902 | ||
| 38fad5ffe2 | |||
|
|
c16724f245 | ||
|
|
013055692f | ||
| 98df524ecf | |||
| 3e300749a0 | |||
| f5621b0c55 | |||
|
|
7971bbc1db | ||
| d640bb74ef | |||
|
|
cf27ed3846 | ||
| a85710421c | |||
| 05f5bf4d33 | |||
| 8057021d1c | |||
| cbdf1b89f8 | |||
|
|
4fe3c94b1e | ||
|
|
07e5d5c6de | ||
| bc8317483f | |||
| a2b29d219a | |||
| bf7af5aa2e | |||
| bc7a999cf1 | |||
| d6656848a2 | |||
|
|
d545a4de0c | ||
|
|
39d1b42ec4 | ||
|
|
53aa879ee5 | ||
| db7bc4b2ed | |||
|
|
a6275a63e9 | ||
| a6eee92641 | |||
| 844f374abb | |||
| 77987711d0 | |||
| 18c3ac3ad5 | |||
| aa1f9630e6 | |||
| 407b4c7751 | |||
| f2ab9dbdd4 | |||
| ce7916d7b0 | |||
| 0db5a72c63 | |||
| a46d34b776 | |||
| 85c44cdd8b | |||
| 2a1cda560c | |||
|
|
c7f6bf00b9 | ||
| 255b3bb2b7 | |||
| 4a6473e1e5 | |||
|
|
7f7b413aa5 | ||
| 439e5f46e3 | |||
|
|
92ee225dd1 | ||
|
|
c0223176f2 | ||
|
|
80c593e6f0 | ||
|
|
b2807c5a39 | ||
|
|
e00763f0da | ||
|
|
b040dd8c1c | ||
|
|
7507f81c34 | ||
|
|
b6bcc7dadc | ||
|
|
2627a7ac68 | ||
|
|
d164630200 | ||
|
|
6c00f6bd7d | ||
| f51e8bbfc5 |
BIN
com.twin.app.shoptime/assets/images/bg/nbcu_new.png
Normal file
BIN
com.twin.app.shoptime/assets/images/bg/nbcu_new.png
Normal file
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 |
126
com.twin.app.shoptime/log_keys.txt
Normal file
126
com.twin.app.shoptime/log_keys.txt
Normal 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 - 앱 상태
|
||||
150
com.twin.app.shoptime/shopByShow.response.json
Normal file
150
com.twin.app.shoptime/shopByShow.response.json
Normal 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": "Women’s 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import Spotlight from '@enact/spotlight';
|
||||
import { Job } from '@enact/core/util';
|
||||
import platform from '@enact/core/platform';
|
||||
import { ThemeDecorator } from '@enact/sandstone/ThemeDecorator';
|
||||
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
|
||||
|
||||
// import "../../../assets/fontello/css/fontello.css";
|
||||
|
||||
@@ -406,8 +407,6 @@ Spotlight.silentlyFocus = function (...args) {
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Spotlight Focus 추적 로그 [251115]
|
||||
// DOM 이벤트 리스너로 대체
|
||||
|
||||
@@ -426,7 +425,7 @@ Spotlight.silentlyFocus = function (...args) {
|
||||
// });
|
||||
// }
|
||||
|
||||
function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
function AppBase(props) {
|
||||
const dispatch = useDispatch();
|
||||
const httpHeader = useSelector((state) => state.common.httpHeader);
|
||||
const httpHeaderRef = useRef(httpHeader);
|
||||
@@ -628,7 +627,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
clearLaunchParams();
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleRelaunchEvent = useCallback(() => {
|
||||
@@ -682,7 +681,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
if (typeof window === 'object' && window.PalmSystem) {
|
||||
window.PalmSystem.activate();
|
||||
}
|
||||
}, [initService, introTermsAgreeRef]);
|
||||
}, [initService, introTermsAgreeRef, dispatch]);
|
||||
|
||||
const visibilityChanged = useCallback(() => {
|
||||
// console.log('document is hidden', document.hidden);
|
||||
@@ -726,7 +725,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||
const keyDownEvent = (event) => {
|
||||
dispatch(changeAppStatus({ cursorVisible: false }));
|
||||
Spotlight.setPointerMode(false);
|
||||
};
|
||||
@@ -735,7 +734,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
let lastMoveTime = 0;
|
||||
const THROTTLE_MS = 100;
|
||||
|
||||
const mouseMoveEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||
const mouseMoveEvent = (event) => {
|
||||
const now = Date.now();
|
||||
if (now - lastMoveTime < THROTTLE_MS) {
|
||||
// throttle 기간 내에는 hideCursor만 재시작
|
||||
@@ -788,7 +787,9 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
let userDataChanged = false;
|
||||
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
|
||||
userDataChanged = true;
|
||||
} else if (userDataChanged || httpHeaderRef.current === null) {
|
||||
}
|
||||
if (!httpHeader || !deviceId) {
|
||||
} else if (userDataChanged || httpHeaderRef.current === null) {
|
||||
//계정정보 변경시 또는 초기 로딩시
|
||||
if (!httpHeader) {
|
||||
dispatch(
|
||||
@@ -888,7 +889,7 @@ function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
/>
|
||||
)}
|
||||
<ToastContainer />
|
||||
{/* <GlobalPopup /> */}
|
||||
<GlobalPopup />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
|
||||
patnrId: patnrId,
|
||||
chanId: chanId,
|
||||
shptmBanrTpNm: "LIVE",
|
||||
modal: false, // DeepLink 진입 시 fullscreen으로 재생
|
||||
// expsOrd: expsOrd,
|
||||
};
|
||||
break;
|
||||
@@ -101,6 +102,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
|
||||
patnrId: patnrId,
|
||||
showId: showId,
|
||||
shptmBanrTpNm: "VOD",
|
||||
modal: false, // DeepLink 진입 시 fullscreen으로 재생
|
||||
// expsOrd: expsOrd,
|
||||
};
|
||||
break;
|
||||
@@ -274,6 +276,18 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
|
||||
const action =
|
||||
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(
|
||||
action({
|
||||
name: panelName,
|
||||
|
||||
@@ -130,11 +130,16 @@ export const types = {
|
||||
GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO',
|
||||
GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM',
|
||||
GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED',
|
||||
GET_BRAND_SHOP_BY_SHOW: 'GET_BRAND_SHOP_BY_SHOW',
|
||||
GET_BRAND_TOP_BANNER: 'GET_BRAND_TOP_BANNER',
|
||||
SET_BRAND_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING',
|
||||
SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO',
|
||||
RESET_BRAND_STATE: 'RESET_BRAND_STATE',
|
||||
RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO',
|
||||
RESET_BRAND_LAYOUT_INFO: 'RESET_BRAND_LAYOUT_INFO',
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 관리
|
||||
SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: 'SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS',
|
||||
RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: 'RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS',
|
||||
|
||||
// main actions
|
||||
GET_SUB_CATEGORY: 'GET_SUB_CATEGORY',
|
||||
|
||||
@@ -37,10 +37,12 @@ export const getBrandList = () => (dispatch, getState) => {
|
||||
export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
|
||||
const { patnrId } = props;
|
||||
|
||||
// console.log("[getBrandLayoutInfo] Called - patnrId:", patnrId);
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
||||
|
||||
const onSuccess = (response) => {
|
||||
// dlog("getBrandLayoutInfo onSuccess ", response.data);
|
||||
// console.log("[getBrandLayoutInfo] onSuccess - patnrId:", patnrId, "data:", response.data.data);
|
||||
|
||||
dispatch({
|
||||
type: types.GET_BRAND_LAYOUT_INFO,
|
||||
@@ -53,6 +55,7 @@ export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
// console.log("[getBrandLayoutInfo] onFail - patnrId:", patnrId, "error:", error);
|
||||
derror('getBrandLayoutInfo onFail ', error);
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
@@ -336,10 +339,15 @@ export const getBrandCategoryProductInfo = (props) => (dispatch, getState) => {
|
||||
export const getBrandBestSeller = (props) => (dispatch, getState) => {
|
||||
const { patnrId } = props;
|
||||
|
||||
// console.log("[getBrandBestSeller] Called - patnrId:", patnrId);
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
||||
|
||||
const onSuccess = (response) => {
|
||||
// dlog("getBrandBestSeller onSuccess ", response.data);
|
||||
// console.log("[getBrandBestSeller] onSuccess - patnrId:", patnrId);
|
||||
// console.log("[getBrandBestSeller] Full response:", response.data.data);
|
||||
// console.log("[getBrandBestSeller] brandBestSellerInfo:", response.data.data.brandBestSellerInfo);
|
||||
// console.log("[getBrandBestSeller] brandBestSellerTitle in response:", response.data.data.brandBestSellerTitle);
|
||||
|
||||
dispatch({
|
||||
type: types.GET_BRAND_BEST_SELLER,
|
||||
@@ -352,6 +360,7 @@ export const getBrandBestSeller = (props) => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
// console.log("[getBrandBestSeller] onFail - patnrId:", patnrId, "error:", error);
|
||||
derror('getBrandBestSeller onFail ', error);
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
@@ -386,6 +395,79 @@ export const getBrandShowroom = (props) => (dispatch, getState) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOWROOM, { patnrId }, {}, onSuccess, onFail);
|
||||
};
|
||||
|
||||
// Featured Brands SHOP BY SHOW 정보 조회 IF-LGSP-376
|
||||
export const getBrandShopByShow = (props) => (dispatch, getState) => {
|
||||
const { patnrId, contsId } = props;
|
||||
|
||||
// console.log("[getBrandShopByShow] Called - patnrId:", patnrId, "contsId:", contsId);
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
||||
|
||||
const onSuccess = (response) => {
|
||||
// console.log("[getBrandShopByShow] onSuccess - patnrId:", patnrId, "data:", response.data.data);
|
||||
|
||||
dispatch({
|
||||
type: types.GET_BRAND_SHOP_BY_SHOW,
|
||||
payload: {
|
||||
data: response.data.data,
|
||||
patnrId,
|
||||
contsId,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
// console.log("[getBrandShopByShow] onFail - patnrId:", patnrId, "error:", error);
|
||||
derror('getBrandShopByShow onFail ', error);
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
// patnrId: 필수, contsId: 선택
|
||||
const params = contsId ? { patnrId, contsId } : { patnrId };
|
||||
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOP_BY_SHOW, params, {}, onSuccess, onFail);
|
||||
};
|
||||
|
||||
// Featured Brands Top Banner 정보 조회 IF-LGSP-377 (NBCU 전용)
|
||||
export const getBrandTopBanner = (props) => (dispatch, getState) => {
|
||||
const { patnrId } = props;
|
||||
|
||||
// console.log("[BRAND-TOP-BANNER-API] Called - patnrId:", patnrId);
|
||||
|
||||
// NBCU(patnrId: 21)가 아니면 호출하지 않음
|
||||
if (patnrId !== 21 && patnrId !== "21") {
|
||||
console.log("[BRAND-TOP-BANNER-API] Skip - patnrId is not 21 (NBCU), patnrId:", patnrId, "(type:", typeof patnrId, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
|
||||
|
||||
const onSuccess = (response) => {
|
||||
// console.log("[BRAND-TOP-BANNER-API] onSuccess - patnrId:", patnrId);
|
||||
// console.log("[BRAND-TOP-BANNER-API] Full response data:", response.data.data);
|
||||
// console.log("[BRAND-TOP-BANNER-API] brandTopBannerInfo:", response.data.data.brandTopBannerInfo);
|
||||
|
||||
dispatch({
|
||||
type: types.GET_BRAND_TOP_BANNER,
|
||||
payload: {
|
||||
data: response.data.data,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
// console.log("[BRAND-TOP-BANNER-API] onFail - patnrId:", patnrId, "error:", error);
|
||||
derror('getBrandTopBanner onFail ', error);
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_TOP_BANNER, { patnrId }, {}, onSuccess, onFail);
|
||||
};
|
||||
|
||||
// Featured Brands Recently Aired 조회 IF-LGSP-373
|
||||
export const getBrandRecentlyAired = (props) => (dispatch, getState) => {
|
||||
const { patnrId } = props;
|
||||
|
||||
@@ -122,7 +122,7 @@ export const alertToast = (payload) => (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSettings = () => (dispatch) => {
|
||||
export const getSystemSettings = () => (dispatch, getState) => {
|
||||
dlog('getSystemSettings ');
|
||||
lunaSend.getSystemSettings(
|
||||
{ category: 'caption', keys: ['captionEnable'] },
|
||||
@@ -146,7 +146,7 @@ export const getSystemSettings = () => (dispatch) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
|
||||
export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getState) => {
|
||||
dlog('getHttpHeaderForServiceRequest ');
|
||||
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
|
||||
lunaSend.getHttpHeaderForServiceRequest({
|
||||
@@ -285,7 +285,7 @@ export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeviceId = (onComplete) => (dispatch) => {
|
||||
export const getDeviceId = (onComplete) => (dispatch, getState) => {
|
||||
lunaSend.getDeviceId(
|
||||
{ idType: ['LGUDID'] },
|
||||
{
|
||||
@@ -463,7 +463,7 @@ export const setFocus = (spotlightId) => ({
|
||||
payload: spotlightId,
|
||||
});
|
||||
|
||||
export const focusElement = (spotlightId) => (dispatch) => {
|
||||
export const focusElement = (spotlightId) => (dispatch, getState) => {
|
||||
dispatch(setFocus(spotlightId));
|
||||
|
||||
if (typeof window === 'object') {
|
||||
@@ -485,7 +485,7 @@ export const cancelFocusElement = () => () => {
|
||||
let broadcastTimer = null;
|
||||
export const sendBroadCast =
|
||||
({ type, moreInfo }) =>
|
||||
(dispatch) => {
|
||||
(dispatch, getState) => {
|
||||
clearTimeout(broadcastTimer);
|
||||
dispatch(changeBroadcastEvent({ type, moreInfo }));
|
||||
broadcastTimer = setTimeout(() => {
|
||||
@@ -542,7 +542,7 @@ export const addReservation = (data) => (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteReservationCallback = (scheduleIdList) => () => {
|
||||
export const deleteReservationCallback = (scheduleIdList) => (dispatch) => {
|
||||
lunaSend.deleteReservationCallback(scheduleIdList, {
|
||||
onSuccess: (res) => {
|
||||
// dispatch(alertToast("success" + JSON.stringify(res)));
|
||||
@@ -680,7 +680,7 @@ let updateNetworkStateJob = new Job((dispatch, connected) => {
|
||||
dispatch(changeAppStatus({ isInternetConnected: connected }));
|
||||
});
|
||||
|
||||
export const getConnectionStatus = () => (dispatch) => {
|
||||
export const getConnectionStatus = () => (dispatch, getState) => {
|
||||
lunaSend.getConnectionStatus({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend getConnectionStatus', res);
|
||||
@@ -709,7 +709,7 @@ export const getConnectionStatus = () => (dispatch) => {
|
||||
};
|
||||
|
||||
// macAddress
|
||||
export const getConnectionInfo = () => (dispatch) => {
|
||||
export const getConnectionInfo = () => (dispatch, getState) => {
|
||||
lunaSend.getConnectionInfo({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend getConnectionStatus', res);
|
||||
@@ -731,7 +731,7 @@ export const getConnectionInfo = () => (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const disableNotification = () => {
|
||||
export const disableNotification = () => (dispatch, getState) => {
|
||||
lunaSend.disableNotification({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend disable notification success', res);
|
||||
@@ -745,7 +745,7 @@ export const disableNotification = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const enableNotification = () => {
|
||||
export const enableNotification = () => (dispatch, getState) => {
|
||||
lunaSend.enableNotification({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend enable notification success', res);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { types } from './actionTypes';
|
||||
import { changeAppStatus, getTermsAgreeYn } from './commonActions';
|
||||
import { collectBannerPositions } from '../utils/domUtils';
|
||||
import { createDebugHelpers } from '../utils/debug';
|
||||
import { setHidePopup, setShowPopup } from './commonActions';
|
||||
import { ACTIVE_POPUP } from '../utils/Config';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
@@ -75,6 +77,38 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
|
||||
|
||||
const onFail = (error) => {
|
||||
derror('getHomeTerms onFail ', error);
|
||||
|
||||
// TODO: 임시 디버그용 팝업 (재현 후 제거하세요)
|
||||
const retCode = error?.data?.retCode ?? error?.retCode ?? 'unknown';
|
||||
dispatch(
|
||||
setShowPopup(ACTIVE_POPUP.toast, {
|
||||
button1Text: `getHomeTerms onFail retCode=${retCode}`,
|
||||
button2Text: 'OK',
|
||||
})
|
||||
);
|
||||
setTimeout(() => dispatch(setHidePopup()), 1500);
|
||||
|
||||
// 약관 미동의(retCode 501)로 GET_HOME_TERMS가 실패하면
|
||||
// introTermsAgree를 명시적으로 false로 내려 앱이 IntroPanel을 띄우도록 한다.
|
||||
if (retCode === 501) {
|
||||
dispatch({
|
||||
type: types.GET_TERMS_AGREE_YN_SUCCESS,
|
||||
payload: {
|
||||
privacyTerms: 'N',
|
||||
serviceTerms: 'N',
|
||||
purchaseTerms: 'N',
|
||||
paymentTerms: 'N',
|
||||
optionalTerms: 'N',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 실패 시 로딩 패널을 반드시 내려 백화 상태를 방지
|
||||
dispatch(
|
||||
changeAppStatus({
|
||||
showLoadingPanel: { show: false },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
TAxios(
|
||||
|
||||
@@ -99,17 +99,25 @@ export const finishMediaPreview = () => (dispatch, getState) => {
|
||||
export const finishModalMediaForce = () => (dispatch, getState) => {
|
||||
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(
|
||||
(panel) =>
|
||||
panel.name === panel_names.MEDIA_PANEL &&
|
||||
(panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player')
|
||||
);
|
||||
|
||||
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] hasProductVideoPanel:', hasProductVideoPanel);
|
||||
|
||||
if (hasProductVideoPanel) {
|
||||
if (startMediaFocusTimer) {
|
||||
clearTimeout(startMediaFocusTimer);
|
||||
startMediaFocusTimer = null;
|
||||
}
|
||||
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] Calling popPanel(panel_names.MEDIA_PANEL)');
|
||||
dispatch(popPanel(panel_names.MEDIA_PANEL));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,10 +39,27 @@ export const pushPanel = (panel, duplicatable = false) => ({
|
||||
duplicatable: duplicatable,
|
||||
});
|
||||
|
||||
export const popPanel = (panelName) => ({
|
||||
type: types.POP_PANEL,
|
||||
payload: panelName,
|
||||
});
|
||||
export const popPanel = (panelName) => {
|
||||
const stack = new Error().stack;
|
||||
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) => ({
|
||||
type: types.UPDATE_PANEL,
|
||||
@@ -93,6 +110,11 @@ export const navigateToDetail = ({
|
||||
...additionalInfo,
|
||||
};
|
||||
|
||||
const state = getState();
|
||||
const panels = state.panels.panels;
|
||||
|
||||
|
||||
|
||||
// 선택적 파라미터들 추가
|
||||
if (curationId) panelInfo.curationId = curationId;
|
||||
if (nowShelf) panelInfo.nowShelf = nowShelf;
|
||||
@@ -176,8 +198,21 @@ export const navigateToDetail = ({
|
||||
|
||||
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false;
|
||||
|
||||
console.log('[Detail-BG] 🎯 navigateToDetail - Checking HomeBanner video status:', {
|
||||
playerPanelModalValue: playerPanelInfo.panelInfo?.modal,
|
||||
isCurrentBannerVideoPlaying,
|
||||
sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// HomeBanner의 modal=true 비디오가 재생 중이면 정지
|
||||
if (isCurrentBannerVideoPlaying) {
|
||||
console.log('[Detail-BG] 🎬 navigateToDetail - HomeBanner video is playing (modal !== false)', {
|
||||
playerPanelModal: playerPanelInfo.panelInfo?.modal,
|
||||
sourceMenu,
|
||||
action: 'finishVideoPreview',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// 🔽 비디오 상태 저장 후 정지
|
||||
const { finishVideoPreview } = require('./playActions');
|
||||
|
||||
@@ -204,11 +239,15 @@ export const navigateToDetail = ({
|
||||
})
|
||||
);
|
||||
|
||||
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제)
|
||||
|
||||
// 비디오 상태 저장 후 정지
|
||||
dispatch(finishVideoPreview());
|
||||
} else {
|
||||
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장
|
||||
console.log('[Detail-BG] ⏭️ navigateToDetail - HomeBanner video is NOT playing (modal === false or undefined)', {
|
||||
playerPanelModal: playerPanelInfo.panelInfo?.modal,
|
||||
sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.HOME_PANEL,
|
||||
@@ -267,10 +306,34 @@ export const navigateToDetail = ({
|
||||
case SOURCE_MENUS.PLAYER_MEDIA: {
|
||||
// PlayerPanel에서 온 경우
|
||||
const { hidePlayerOverlays } = require('./videoPlayActions');
|
||||
const statePanels = panels || getState().panels.panels;
|
||||
const playerPanelEntry =
|
||||
[...statePanels].reverse().find(
|
||||
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
|
||||
) || null;
|
||||
|
||||
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
|
||||
dispatch(hidePlayerOverlays());
|
||||
|
||||
// PlayerPanel이 modal=true라면 풀스크린 백그라운드로 전환 + lockModalFalse 설정 (Detail 동안 modal 복귀 방지)
|
||||
if (playerPanelEntry) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: playerPanelEntry.name,
|
||||
panelInfo: {
|
||||
...playerPanelEntry.panelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
lockModalFalse: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 현재 포커스된 요소 저장
|
||||
if (Object.keys(focusSnapshot).length > 0) {
|
||||
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
|
||||
@@ -458,11 +521,28 @@ export const restoreVideoOnBack = () => {
|
||||
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
|
||||
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore;
|
||||
|
||||
console.log('[Detail-BG] 🔍 restoreVideoOnBack - Checking video restore state:', {
|
||||
hasVideoStateToRestore: !!videoStateToRestore,
|
||||
restoreOnBack: videoStateToRestore?.restoreOnBack,
|
||||
sourceMenu: videoStateToRestore?.sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) {
|
||||
console.log('[Detail-BG] ⏭️ restoreVideoOnBack - No video state to restore (skipping)', {
|
||||
reason: !videoStateToRestore ? 'no videoStateToRestore' : 'restoreOnBack is false',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 비디오 복원 시작 (로그는 개발 시 필요 시 주석 해제)
|
||||
console.log('[Detail-BG] ▶️ restoreVideoOnBack - Starting video restore', {
|
||||
sourceMenu: videoStateToRestore.sourceMenu,
|
||||
patnrId: videoStateToRestore.patnrId,
|
||||
showId: videoStateToRestore.showId,
|
||||
modal: true,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 비디오 상태 복원
|
||||
const { startVideoPlayerNew } = require('./playActions');
|
||||
@@ -489,6 +569,11 @@ export const restoreVideoOnBack = () => {
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ restoreVideoOnBack - Video restore dispatched', {
|
||||
restoredWithModal: restoreInfo.modal,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 복원 상태 정리
|
||||
dispatch(
|
||||
updatePanel({
|
||||
|
||||
@@ -73,6 +73,16 @@ export const startVideoPlayer =
|
||||
...rest
|
||||
}) =>
|
||||
(dispatch, getState) => {
|
||||
const caller = new Error().stack?.split('\n')[2]?.trim();
|
||||
dlog('[PTRACE-SP] startVideoPlayer call', {
|
||||
modal,
|
||||
modalContainerId,
|
||||
modalClassName,
|
||||
videoId,
|
||||
showUrl,
|
||||
caller,
|
||||
});
|
||||
|
||||
dlog(
|
||||
'[startVideoPlayer] ✅ START - videoId:',
|
||||
videoId,
|
||||
@@ -105,6 +115,9 @@ export const startVideoPlayer =
|
||||
|
||||
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
|
||||
if (existingPlayerPanel) {
|
||||
dlog('[PTRACE-SP] startVideoPlayer: popping existing player before push', {
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
@@ -135,14 +148,14 @@ export const startVideoPlayer =
|
||||
|
||||
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
|
||||
// if (modal && modalContainerId && !spotlightDisable) {
|
||||
// console.log('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId);
|
||||
// dlog('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId);
|
||||
// Spotlight.setPointerMode(false);
|
||||
// startVideoFocusTimer = setTimeout(() => {
|
||||
// console.log('[startVideoPlayer] 🔍 Spotlight.focus called');
|
||||
// dlog('[startVideoPlayer] 🔍 Spotlight.focus called');
|
||||
// Spotlight.focus(modalContainerId);
|
||||
// }, 0);
|
||||
// } 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');
|
||||
@@ -182,6 +195,17 @@ export const startVideoPlayerNew =
|
||||
...rest
|
||||
}) =>
|
||||
(dispatch, getState) => {
|
||||
const caller = new Error().stack?.split('\n')[2]?.trim();
|
||||
dlog('[PTRACE-SPN] startVideoPlayerNew call', {
|
||||
bannerId,
|
||||
modal,
|
||||
modalContainerId,
|
||||
modalClassName,
|
||||
videoId,
|
||||
showUrl,
|
||||
caller,
|
||||
});
|
||||
|
||||
dlog(
|
||||
'[startVideoPlayerNew] *** ✅ START - bannerId:',
|
||||
bannerId,
|
||||
@@ -215,6 +239,9 @@ export const startVideoPlayerNew =
|
||||
|
||||
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
|
||||
if (existingPlayerPanel) {
|
||||
dlog('[PTRACE-SPN] popping existing player before push', {
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
@@ -305,14 +332,14 @@ export const startVideoPlayerNew =
|
||||
|
||||
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
|
||||
// if (modal && modalContainerId && !spotlightDisable) {
|
||||
// console.log('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId);
|
||||
// dlog('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId);
|
||||
// Spotlight.setPointerMode(false);
|
||||
// startVideoFocusTimer = setTimeout(() => {
|
||||
// console.log('[startVideoPlayerNew] *** 🔍 Spotlight.focus called');
|
||||
// dlog('[startVideoPlayerNew] *** 🔍 Spotlight.focus called');
|
||||
// Spotlight.focus(modalContainerId);
|
||||
// }, 0);
|
||||
// } 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');
|
||||
@@ -325,6 +352,12 @@ export const finishVideoPreview = () => (dispatch, getState) => {
|
||||
const panels = getState().panels.panels;
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) {
|
||||
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) {
|
||||
clearTimeout(startVideoFocusTimer);
|
||||
startVideoFocusTimer = null;
|
||||
@@ -384,6 +417,13 @@ export const pauseModalVideo = () => (dispatch, getState) => {
|
||||
(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 (DEBUG_MODE === true) {
|
||||
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
|
||||
);
|
||||
|
||||
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 (DEBUG_MODE === true) {
|
||||
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
|
||||
);
|
||||
|
||||
dlog('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', {
|
||||
found: !!fullscreenPlayerPanel,
|
||||
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (fullscreenPlayerPanel) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
@@ -444,6 +516,14 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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) => {
|
||||
const panels = getState().panels.panels;
|
||||
|
||||
// console.log('[BgVideo] resumeFullscreenVideo called - panels:', {
|
||||
// panelsCount: panels?.length,
|
||||
// panels: panels?.map(p => ({ name: p.name, modal: p.panelInfo?.modal, isPaused: p.panelInfo?.isPaused }))
|
||||
// });
|
||||
|
||||
// 전체화면 PlayerPanel 찾기 (modal이 false인 패널)
|
||||
const fullscreenPlayerPanel = panels.find(
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
|
||||
);
|
||||
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - fullscreenPlayerPanel found:', !!fullscreenPlayerPanel);
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - isPaused:', fullscreenPlayerPanel?.panelInfo?.isPaused);
|
||||
dlog('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
|
||||
found: !!fullscreenPlayerPanel,
|
||||
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) {
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - dispatching updatePanel with isPaused: false');
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
@@ -475,7 +553,16 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
dlog('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} 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) {
|
||||
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,
|
||||
// modalStyle: panelInfo.modalStyle,
|
||||
// });
|
||||
@@ -951,6 +1038,12 @@ export const resumePlayerControl = (ownerId) => (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로 변경하여 전체화면으로 전환
|
||||
dispatch(
|
||||
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
|
||||
} = 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;
|
||||
if (videoIdentifier) {
|
||||
@@ -1190,11 +1295,21 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
|
||||
// 기존 PlayerPanel이 있으면 초기화
|
||||
if (existingPlayerPanel) {
|
||||
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
dlog('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
|
||||
existingModalStatus: existingPlayerPanel.panelInfo?.modal,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
}
|
||||
|
||||
// 새로운 PlayerPanel push
|
||||
dlog('[Detail-BG] ➕ startBannerVideo - Pushing new PlayerPanel with modal status', {
|
||||
modal,
|
||||
modalContainerId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
dispatch(
|
||||
pushPanel(
|
||||
{
|
||||
@@ -1216,6 +1331,10 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
|
||||
)
|
||||
);
|
||||
|
||||
dlog('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
dlog('[startBannerVideo] ✨ Panel action dispatched');
|
||||
};
|
||||
|
||||
|
||||
@@ -185,8 +185,14 @@ export const TAxios = (
|
||||
return;
|
||||
}
|
||||
|
||||
// 약관 미동의(501): 토큰 재발급 큐에 넣지 않고 바로 실패 처리
|
||||
if (res?.data?.retCode === 501) {
|
||||
if (onFail) onFail(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// RefreshToken 만료
|
||||
if (res?.data?.retCode === 402 || res?.data?.retCode === 501) {
|
||||
if (res?.data?.retCode === 402) {
|
||||
if (baseUrl === URLS.GET_RE_AUTHENTICATION_CODE) {
|
||||
dispatch(getAuthenticationCode());
|
||||
} else {
|
||||
@@ -349,10 +355,10 @@ export const TAxiosAdvancedPromise = (
|
||||
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
|
||||
|
||||
// Check if the error is due to token expiration
|
||||
// TAxios already handles token refresh and queueing for these codes (401, 402, 501)
|
||||
// TAxios already handles token refresh and queueing for 401/402 (501은 제외)
|
||||
// So we should NOT retry immediately in this loop, but let TAxios handle it.
|
||||
const retCode = error?.data?.retCode;
|
||||
const isTokenError = retCode === 401 || retCode === 402 || retCode === 501;
|
||||
const isTokenError = retCode === 401 || retCode === 402;
|
||||
|
||||
// 재시도 로직
|
||||
if (attempts < maxAttempts && !isTokenError) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from "axios";
|
||||
|
||||
import { createQueryString } from "../utils/helperMethods";
|
||||
import { getUrl } from "./apiConfig";
|
||||
import { DEBUG_LOG_MODE, sendToLogServer } from "./logServerClient";
|
||||
|
||||
export const TLogEvent = (
|
||||
dispatch,
|
||||
@@ -68,6 +69,23 @@ export const TLogEvent = (
|
||||
prodCd,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== DEBUG_LOG_MODE: 로그서버로 데이터 전송 =====
|
||||
if (DEBUG_LOG_MODE) {
|
||||
sendToLogServer({
|
||||
deviceId: dvcId,
|
||||
cntryCd,
|
||||
platCd,
|
||||
prodCd,
|
||||
appVersion,
|
||||
deviceLang,
|
||||
logModel: model,
|
||||
apiUrl: url,
|
||||
httpMethod: type,
|
||||
totalLogFlag,
|
||||
});
|
||||
}
|
||||
|
||||
let axiosInstance;
|
||||
|
||||
switch (type) {
|
||||
|
||||
@@ -55,6 +55,8 @@ export const URLS = {
|
||||
GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge",
|
||||
GET_BRAND_SHOWROOM: "/lgsp/v1/brand/showroom.lge",
|
||||
GET_BRAND_RECENTLY_AIRED: "/lgsp/v1/brand/recently/aired.lge",
|
||||
GET_BRAND_SHOP_BY_SHOW: "/lgsp/v1/brand/shopByShow.lge",
|
||||
GET_BRAND_TOP_BANNER: "/lgsp/v1/brand/top/banner.lge",
|
||||
|
||||
//on-sale controller
|
||||
GET_ON_SALE_INFO: "/lgsp/v1/onsale/onsale.lge",
|
||||
@@ -149,7 +151,6 @@ export const URLS = {
|
||||
// foryou controller
|
||||
JUSTFORYOU: "/lgsp/v1/recommend/justforyou.lge",
|
||||
|
||||
|
||||
// emp controller
|
||||
GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge",
|
||||
SET_MYPAGE_TERMS_AGREE: "/lgsp/v1/mypage/terms/agree.lge",
|
||||
@@ -272,11 +273,11 @@ const getRicCode = (country, ricCodeSetting) => {
|
||||
if (ricCodeSetting !== "system") {
|
||||
return ricCodeSetting;
|
||||
}
|
||||
if (country == "US") {
|
||||
if (country === "US") {
|
||||
return "aic";
|
||||
} else if (country == "DE" || country == "GB") {
|
||||
} else if (country === "DE" || country === "GB") {
|
||||
return "eic";
|
||||
} else if (country == "RU") {
|
||||
} else if (country === "RU") {
|
||||
return "ruc";
|
||||
}
|
||||
return null;
|
||||
|
||||
85
com.twin.app.shoptime/src/api/logServerClient.js
Normal file
85
com.twin.app.shoptime/src/api/logServerClient.js
Normal 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) {
|
||||
// 함수 자체의 오류는 무시 (조용하게 처리)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
@@ -14,10 +15,13 @@ import {
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import { setHidePopup } from '../../actions/commonActions';
|
||||
import { getPopupConfig } from '../../constants/popupConfig';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import TPopUp from '../TPopUp/TPopUp';
|
||||
import TopBannerPopup from '../../views/FeaturedBrandsPanel/TopBannerImage/TopBannerPopup';
|
||||
|
||||
// 커스텀 훅: 팝업 상태 관리
|
||||
const useGlobalPopupState = () => {
|
||||
@@ -128,9 +132,28 @@ const GlobalPopup = () => {
|
||||
secondaryData
|
||||
} = useGlobalPopupState();
|
||||
|
||||
const [imageDimensions, setImageDimensions] = React.useState({ width: 0, height: 0 });
|
||||
|
||||
const handlers = usePopupCloseHandlers();
|
||||
const previousPopupVisible = usePrevious(popupVisible);
|
||||
|
||||
const handleImageLoad = useCallback((dimensions) => {
|
||||
console.log("[GLOBAL-POPUP] Image dimensions received:", dimensions);
|
||||
setImageDimensions(dimensions);
|
||||
}, []);
|
||||
|
||||
// Spotlight 제어: 팝업 오픈/클로즈 시 포커스 트래핑
|
||||
useEffect(() => {
|
||||
if (popupVisible && activePopup === 'topBannerImagePopup') {
|
||||
console.log("[GLOBAL-POPUP] Pausing Spotlight for modal popup");
|
||||
Spotlight.pause();
|
||||
return () => {
|
||||
console.log("[GLOBAL-POPUP] Resuming Spotlight after modal close");
|
||||
Spotlight.resume();
|
||||
};
|
||||
}
|
||||
}, [popupVisible, activePopup]);
|
||||
|
||||
// 현재 팝업 설정
|
||||
const currentConfig = useMemo(() => {
|
||||
if (!activePopup) return null;
|
||||
@@ -214,6 +237,53 @@ const GlobalPopup = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TopBannerImagePopup 특수 처리
|
||||
if (activePopup === 'topBannerImagePopup') {
|
||||
// Figma 디자인 기반 고정 크기
|
||||
// 너비: 1060px
|
||||
// 높이: 헤더(110px) + 이미지(556px) + 푸터(138px) = 804px
|
||||
const popupWidth = '1060px';
|
||||
const popupHeight = '804px';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999
|
||||
}}
|
||||
onClick={handlers.handleClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: popupWidth,
|
||||
height: popupHeight,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<TopBannerPopup
|
||||
title={popupData?.pupBanrImgNm || 'Popup'}
|
||||
imageUrl={popupData?.pupBanrImgUrl}
|
||||
imageAlt={popupData?.pupBanrImgNm || 'Popup Banner'}
|
||||
onImageLoad={handleImageLoad}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 설정이 없으면 기본 팝업도 렌더링하지 않음
|
||||
if (!currentConfig) {
|
||||
console.warn(`No configuration found for popup type: ${activePopup}`);
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
padding: @slider-padding-v 0;
|
||||
height: @sand-mediaplayer-slider-height;
|
||||
right: 154px;
|
||||
width: 1466px;
|
||||
width: 1558px;
|
||||
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
position: absolute;
|
||||
font-family: @baseFont;
|
||||
width: 100%;
|
||||
right: 90px;
|
||||
right: 20px;
|
||||
bottom: -5px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
@@ -16,12 +16,12 @@
|
||||
letter-spacing: -1px;
|
||||
.separator {
|
||||
position: absolute;
|
||||
right: 105px;
|
||||
right: 95px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.currentTime {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
right: 120px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.totalTime {
|
||||
|
||||
@@ -4,75 +4,75 @@ import React, {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
} from 'react';
|
||||
|
||||
import classNames from "classnames";
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
AsYouTypeFormatter,
|
||||
PhoneNumberFormat,
|
||||
PhoneNumberUtil,
|
||||
} from "google-libphonenumber";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
} from 'google-libphonenumber';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { off, on } from "@enact/core/dispatcher";
|
||||
import spotlight, { Spotlight } from "@enact/spotlight";
|
||||
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import { Spottable } from "@enact/spotlight/Spottable";
|
||||
import { off, on } from '@enact/core/dispatcher';
|
||||
import spotlight, { Spotlight } from '@enact/spotlight';
|
||||
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import { Spottable } from '@enact/spotlight/Spottable';
|
||||
|
||||
import defaultImage from "../../../assets/images/img-thumb-empty-144@3x.png";
|
||||
import { types } from "../../actions/actionTypes";
|
||||
import { clearSMS, sendSms } from "../../actions/appDataActions";
|
||||
import defaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import { types } from '../../actions/actionTypes';
|
||||
import { clearSMS, sendSms } from '../../actions/appDataActions';
|
||||
import {
|
||||
changeLocalSettings,
|
||||
setHidePopup,
|
||||
setShowPopup,
|
||||
} from "../../actions/commonActions";
|
||||
} from '../../actions/commonActions';
|
||||
import {
|
||||
clearRegisterDeviceInfo,
|
||||
getDeviceAdditionInfo,
|
||||
registerDeviceInfo,
|
||||
} from "../../actions/deviceActions";
|
||||
} from '../../actions/deviceActions';
|
||||
import {
|
||||
clearCurationCoupon,
|
||||
setEventIssueReq,
|
||||
} from "../../actions/eventActions";
|
||||
} from '../../actions/eventActions';
|
||||
import {
|
||||
sendLogShopByMobile,
|
||||
sendLogTotalRecommend,
|
||||
} from "../../actions/logActions";
|
||||
} from '../../actions/logActions';
|
||||
import {
|
||||
ACTIVE_POPUP,
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MESSAGE_ID,
|
||||
LOG_TP_NO,
|
||||
} from "../../utils/Config";
|
||||
} from '../../utils/Config';
|
||||
import {
|
||||
$L,
|
||||
decryptPhoneNumber,
|
||||
encryptPhoneNumber,
|
||||
formatLocalDateTime,
|
||||
} from "../../utils/helperMethods";
|
||||
import CustomImage from "../CustomImage/CustomImage";
|
||||
import TButton from "../TButton/TButton";
|
||||
import TPopUp from "../TPopUp/TPopUp";
|
||||
import HistoryPhoneNumber from "./HistoryPhoneNumber/HistoryPhoneNumber";
|
||||
import css from "./MobileSendPopUp.module.less";
|
||||
import PhoneInputSection from "./PhoneInputSection";
|
||||
import SMSNumKeyPad from "./SMSNumKeyPad";
|
||||
} from '../../utils/helperMethods';
|
||||
import CustomImage from '../CustomImage/CustomImage';
|
||||
import TButton from '../TButton/TButton';
|
||||
import TPopUp from '../TPopUp/TPopUp';
|
||||
import HistoryPhoneNumber from './HistoryPhoneNumber/HistoryPhoneNumber';
|
||||
import css from './MobileSendPopUp.module.less';
|
||||
import PhoneInputSection from './PhoneInputSection';
|
||||
import SMSNumKeyPad from './SMSNumKeyPad';
|
||||
|
||||
const SECRET_KEY = "fy7BTKuM9eeTQqEC9sF3Iw5qG43Aaip";
|
||||
const SECRET_KEY = 'fy7BTKuM9eeTQqEC9sF3Iw5qG43Aaip';
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
{ enterTo: 'last-focused' },
|
||||
'div'
|
||||
);
|
||||
|
||||
const InputContainer = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
{ enterTo: 'last-focused' },
|
||||
'div'
|
||||
);
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
export default function MobileSendPopUp({
|
||||
open,
|
||||
@@ -116,39 +116,40 @@ export default function MobileSendPopUp({
|
||||
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
|
||||
|
||||
const [inputDisabled, setInputDisabled] = useState(true);
|
||||
const [mobileNumber, setMobileNumber] = useState("");
|
||||
const [mobileNumber, setMobileNumber] = useState('');
|
||||
const [recentSentNumber, setRecentSentNumber] = useState([]);
|
||||
const [keyPadOff, setKeyPadOff] = useState(false);
|
||||
const [smsRetCode, setSmsRetCode] = useState(undefined);
|
||||
|
||||
const agreeBtnClickedRef = useRef(false);
|
||||
|
||||
const deviceCountryCode = httpHeader["X-Device-Country"];
|
||||
const deviceCountryCode = httpHeader['X-Device-Country'];
|
||||
|
||||
const mobileSendPopUpSpotlightId = useMemo(() => {
|
||||
return !keyPadOff && recentSentNumber.length <= 0
|
||||
? "keypad-number-1"
|
||||
: "agreeAndSend";
|
||||
? 'keypad-number-1'
|
||||
: 'agreeAndSend';
|
||||
}, [keyPadOff, recentSentNumber]);
|
||||
|
||||
const getMaxNum = useCallback((_deviceCountryCode) => {
|
||||
if (_deviceCountryCode === "DE" || _deviceCountryCode === "GB") {
|
||||
if (_deviceCountryCode === 'DE' || _deviceCountryCode === 'GB') {
|
||||
return 11;
|
||||
} else if (_deviceCountryCode === "KR") {
|
||||
} else if (_deviceCountryCode === 'KR') {
|
||||
return 12;
|
||||
} else return 10;
|
||||
}, []);
|
||||
|
||||
const MSG_SUCCESS_SENT = $L("Text Send to") + " " + mobileNumber;
|
||||
const MSG_SEND_LINK = $L("Send a purchase link for this item via SMS");
|
||||
const MSG_SUCCESS_SENT = $L('Text Send to') + ' ' + mobileNumber;
|
||||
const MSG_SEND_LINK = $L('Send a purchase link for this item via SMS');
|
||||
|
||||
const handleClickSelect = (_phoneNumber) => {
|
||||
setKeyPadOff((state) => !state);
|
||||
setMobileNumber(_phoneNumber);
|
||||
|
||||
setTimeout(() => {
|
||||
Spotlight.focus("keypad-number-1");
|
||||
Spotlight.focus('keypad-number-1');
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@@ -161,9 +162,9 @@ export default function MobileSendPopUp({
|
||||
|
||||
const getRawPhoneNumber = useCallback(
|
||||
(key) => {
|
||||
let rawPhoneNumber = `${mobileNumber}${key}`.replace(/\D/g, "");
|
||||
let rawPhoneNumber = `${mobileNumber}${key}`.replace(/\D/g, '');
|
||||
if (rawPhoneNumber.length === getMaxNum(deviceCountryCode)) {
|
||||
Spotlight.focus("agreeAndSend");
|
||||
Spotlight.focus('agreeAndSend');
|
||||
}
|
||||
// 테스트용: 12자리까지 허용
|
||||
if (rawPhoneNumber.length > 12) {
|
||||
@@ -182,11 +183,11 @@ export default function MobileSendPopUp({
|
||||
numberProto,
|
||||
PhoneNumberFormat.NATIONAL
|
||||
);
|
||||
if (deviceCountryCode === "RU" && rawPhoneNumber.startsWith("8")) {
|
||||
if (deviceCountryCode === 'RU' && rawPhoneNumber.startsWith('8')) {
|
||||
rawPhoneNumber = rawPhoneNumber.substring(1);
|
||||
}
|
||||
} else {
|
||||
let formattedNumber = "";
|
||||
let formattedNumber = '';
|
||||
|
||||
for (let i = 0; i < rawPhoneNumber.length; i++) {
|
||||
formattedNumber = asYouTypeFormatter.inputDigit(
|
||||
@@ -206,7 +207,7 @@ export default function MobileSendPopUp({
|
||||
);
|
||||
|
||||
const getBackspaceRawNumber = useCallback(() => {
|
||||
let rawPhoneNumber = mobileNumber.replace(/\D/g, "").slice(0, -1);
|
||||
let rawPhoneNumber = mobileNumber.replace(/\D/g, '').slice(0, -1);
|
||||
const phoneUtil = PhoneNumberUtil.getInstance();
|
||||
const asYouTypeFormatter = new AsYouTypeFormatter(deviceCountryCode);
|
||||
|
||||
@@ -221,7 +222,7 @@ export default function MobileSendPopUp({
|
||||
PhoneNumberFormat.NATIONAL
|
||||
);
|
||||
} else {
|
||||
let formattedNumber = "";
|
||||
let formattedNumber = '';
|
||||
|
||||
for (let i = 0; i < rawPhoneNumber.length; i++) {
|
||||
formattedNumber = asYouTypeFormatter.inputDigit(
|
||||
@@ -242,7 +243,7 @@ export default function MobileSendPopUp({
|
||||
(ev) => {
|
||||
if (ev && ev.key >= 0 && ev.key <= 9) {
|
||||
getRawPhoneNumber(ev.key);
|
||||
} else if (ev.key === "Backspace") {
|
||||
} else if (ev.key === 'Backspace') {
|
||||
getBackspaceRawNumber();
|
||||
}
|
||||
},
|
||||
@@ -250,9 +251,9 @@ export default function MobileSendPopUp({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
on("keydown", handleKeydown);
|
||||
on('keydown', handleKeydown);
|
||||
return () => {
|
||||
off("keydown", handleKeydown);
|
||||
off('keydown', handleKeydown);
|
||||
};
|
||||
}, [handleKeydown]);
|
||||
|
||||
@@ -264,7 +265,7 @@ export default function MobileSendPopUp({
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() =>
|
||||
setInputDisabled(mobileSendPopUpSpotlightId === "keypad-number-1")
|
||||
setInputDisabled(mobileSendPopUpSpotlightId === 'keypad-number-1')
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
@@ -277,16 +278,16 @@ export default function MobileSendPopUp({
|
||||
setMobileNumber(recentSentNumber[0]);
|
||||
} else {
|
||||
setKeyPadOff(false);
|
||||
setMobileNumber("");
|
||||
setMobileNumber('');
|
||||
}
|
||||
}
|
||||
}, [recentSentNumber]);
|
||||
|
||||
const numKeypadClicked = useCallback(
|
||||
(key) => {
|
||||
if (key === "clear") {
|
||||
setMobileNumber("");
|
||||
} else if (key == "backspace") {
|
||||
if (key === 'clear') {
|
||||
setMobileNumber('');
|
||||
} else if (key == 'backspace') {
|
||||
getBackspaceRawNumber();
|
||||
} else {
|
||||
getRawPhoneNumber(key);
|
||||
@@ -315,7 +316,7 @@ export default function MobileSendPopUp({
|
||||
};
|
||||
|
||||
const handleAgreeSendClick = useCallback(() => {
|
||||
let naturalNumber = mobileNumber.replace(/\D/g, "");
|
||||
let naturalNumber = mobileNumber.replace(/\D/g, '');
|
||||
|
||||
// 테스트용: 길이 체크를 더 유연하게 (10자리 또는 11자리 허용)
|
||||
if (
|
||||
@@ -327,8 +328,8 @@ export default function MobileSendPopUp({
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceCountryCode === "KR") {
|
||||
naturalNumber = "82" + naturalNumber;
|
||||
if (deviceCountryCode === 'KR') {
|
||||
naturalNumber = '82' + naturalNumber;
|
||||
}
|
||||
|
||||
if (recentSentNumber && recentSentNumber.length > 0) {
|
||||
@@ -394,22 +395,22 @@ export default function MobileSendPopUp({
|
||||
};
|
||||
|
||||
// 호텔일 경우 날려야 하는 경우
|
||||
if (smsTpCd === "APP00205") {
|
||||
if (smsTpCd === 'APP00205') {
|
||||
params = { ...params, hotelId, hotelNm, hotelDtlUrl, curationId };
|
||||
}
|
||||
if (smsTpCd === "APP00204") {
|
||||
if (smsTpCd === 'APP00204') {
|
||||
params = { ...params, curationId };
|
||||
}
|
||||
dispatch(sendSms(params));
|
||||
}
|
||||
// EVT00101 & APP00207(welcome) EVT00103 & APP00209 (welcome+Prizes) : smsTpCd 값을 받지 않음
|
||||
if (evntTpCd === "EVT00101" || evntTpCd === "EVT00103") {
|
||||
if (evntTpCd === 'EVT00101' || evntTpCd === 'EVT00103') {
|
||||
dispatch(
|
||||
registerDeviceInfo({
|
||||
evntTpCd,
|
||||
evntId,
|
||||
evntApplcnFlag: "Y",
|
||||
entryMenu: "TermsPop",
|
||||
evntApplcnFlag: 'Y',
|
||||
entryMenu: 'TermsPop',
|
||||
mbphNo: naturalNumber,
|
||||
})
|
||||
);
|
||||
@@ -434,7 +435,7 @@ export default function MobileSendPopUp({
|
||||
onClose();
|
||||
dispatch(setShowPopup(ACTIVE_POPUP.smsPopup));
|
||||
|
||||
setTimeout(() => Spotlight.focus("agreeAndSend"));
|
||||
setTimeout(() => Spotlight.focus('agreeAndSend'));
|
||||
},
|
||||
[dispatch, smsRetCode]
|
||||
);
|
||||
@@ -450,7 +451,7 @@ export default function MobileSendPopUp({
|
||||
curationCouponSuccess === 0
|
||||
) {
|
||||
const logParams = {
|
||||
status: "send",
|
||||
status: 'send',
|
||||
nowMenu: nowMenu,
|
||||
partner: patncNm ?? shopByMobileLogRef?.current?.patncNm,
|
||||
productId: prdtId ?? shopByMobileLogRef?.current?.prdtId,
|
||||
@@ -465,8 +466,8 @@ export default function MobileSendPopUp({
|
||||
...shopByMobileLogRef.current,
|
||||
locDt: formatLocalDateTime(new Date()),
|
||||
logTpNo: LOG_TP_NO.SHOP_BY_MOBILE.AGREE_AND_SEND,
|
||||
mbphNoFlag: "Y",
|
||||
trmsAgrFlag: "Y",
|
||||
mbphNoFlag: 'Y',
|
||||
trmsAgrFlag: 'Y',
|
||||
};
|
||||
|
||||
dispatch(sendLogShopByMobile(params));
|
||||
@@ -497,24 +498,24 @@ export default function MobileSendPopUp({
|
||||
}, [dispatch]);
|
||||
|
||||
const getSmsErrorMsg = useMemo(() => {
|
||||
const SMS_ERROR_502 = $L("The event information has not been registered");
|
||||
const SMS_ERROR_903 = $L("You have exceeded the daily text limit.");
|
||||
const SMS_ERROR_502 = $L('The event information has not been registered');
|
||||
const SMS_ERROR_903 = $L('You have exceeded the daily text limit.');
|
||||
const SMS_ERROR_904 = $L(
|
||||
"You have exceeded the text limit for this product."
|
||||
'You have exceeded the text limit for this product.'
|
||||
);
|
||||
const SMS_ERROR_905 = $L(
|
||||
"This number is currently blocked. To receive a message, please send UNSTOP to the number below. 07860 064195"
|
||||
'This number is currently blocked. To receive a message, please send UNSTOP to the number below. 07860 064195'
|
||||
);
|
||||
const SMS_ERROR_906 = $L("Sorry. This item is sold out.");
|
||||
const SMS_ERROR_600 = $L("This device had received first time coupon.");
|
||||
const SMS_ERROR_601 = $L("There is no coupon.");
|
||||
const SMS_ERROR_900 = $L("Failed to send text to {mobileNumber}").replace(
|
||||
"{mobileNumber}",
|
||||
const SMS_ERROR_906 = $L('Sorry. This item is sold out.');
|
||||
const SMS_ERROR_600 = $L('This device had received first time coupon.');
|
||||
const SMS_ERROR_601 = $L('There is no coupon.');
|
||||
const SMS_ERROR_900 = $L('Failed to send text to {mobileNumber}').replace(
|
||||
'{mobileNumber}',
|
||||
mobileNumber
|
||||
);
|
||||
const SMS_ERROR_907 = $L(
|
||||
"Only {length} digits is permitted. Please check again"
|
||||
).replace("{length}", getMaxNum(deviceCountryCode));
|
||||
'Only {length} digits is permitted. Please check again'
|
||||
).replace('{length}', getMaxNum(deviceCountryCode));
|
||||
|
||||
switch (smsRetCode) {
|
||||
case 502:
|
||||
@@ -542,12 +543,12 @@ export default function MobileSendPopUp({
|
||||
|
||||
const getEvntErrorMsg = useMemo(() => {
|
||||
if (curationCouponSuccess === 600) {
|
||||
return $L("This device had received first time coupon.");
|
||||
return $L('This device had received first time coupon.');
|
||||
} else if (curationCouponSuccess === 601) {
|
||||
return $L("There is no coupon.");
|
||||
return $L('There is no coupon.');
|
||||
} else {
|
||||
return $L("Failed to sent text to {mobileNumber}").replace(
|
||||
"{mobileNumber}",
|
||||
return $L('Failed to sent text to {mobileNumber}').replace(
|
||||
'{mobileNumber}',
|
||||
mobileNumber
|
||||
);
|
||||
}
|
||||
@@ -571,7 +572,7 @@ export default function MobileSendPopUp({
|
||||
regDeviceInfoRetCode === undefined &&
|
||||
curationCouponSuccess === undefined && (
|
||||
<TPopUp
|
||||
kind={"mobileSendPopup"}
|
||||
kind={'mobileSendPopup'}
|
||||
className={css.container}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
@@ -602,16 +603,22 @@ export default function MobileSendPopUp({
|
||||
<div className={css.headerTopRow}>
|
||||
{brandLogo && (
|
||||
<img
|
||||
className={css.headerTopRow__brandLogo}
|
||||
className={classNames(
|
||||
css.headerTopRow__brandLogo,
|
||||
patnrId === '1' && css.headerTopRow__brandLogo__qvc
|
||||
)}
|
||||
src={brandLogo}
|
||||
alt="Brand"
|
||||
/>
|
||||
)}
|
||||
{productId && (
|
||||
{productId && patnrId !== '21' && (
|
||||
<div className={css.headerTopRow__productId}>
|
||||
ID: {productId}
|
||||
</div>
|
||||
)}
|
||||
{patnrId === '21' && (
|
||||
<div className={css.headerTopRow__productId}>{patncNm}</div>
|
||||
)}
|
||||
</div>
|
||||
{subTitle && (
|
||||
<div
|
||||
@@ -639,8 +646,8 @@ export default function MobileSendPopUp({
|
||||
onClick={handleInputClick}
|
||||
spotlightDisabled={inputDisabled}
|
||||
>
|
||||
{deviceCountryCode && deviceCountryCode === "RU" && (
|
||||
<span className={css.rucInput}>{"+7 "}</span>
|
||||
{deviceCountryCode && deviceCountryCode === 'RU' && (
|
||||
<span className={css.rucInput}>{'+7 '}</span>
|
||||
)}
|
||||
<span>{mobileNumber}</span>
|
||||
</SpottableComponent>
|
||||
@@ -662,10 +669,10 @@ export default function MobileSendPopUp({
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${$L(
|
||||
"By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes. By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply."
|
||||
'By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes. By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply.'
|
||||
)}`,
|
||||
}}
|
||||
className={deviceCountryCode === "RU" && css.instructionRu}
|
||||
className={deviceCountryCode === 'RU' && css.instructionRu}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -698,11 +705,11 @@ export default function MobileSendPopUp({
|
||||
<Container className={css.container__btnContainer}>
|
||||
<TButton
|
||||
onClick={handleAgreeSendClick}
|
||||
spotlightId={"agreeAndSend"}
|
||||
spotlightId={'agreeAndSend'}
|
||||
>
|
||||
{$L("Agree and Send")}
|
||||
{$L('Agree and Send')}
|
||||
</TButton>
|
||||
<TButton onClick={onClose}>{$L("Cancel")}</TButton>
|
||||
<TButton onClick={onClose}>{$L('Cancel')}</TButton>
|
||||
</Container>
|
||||
</TPopUp>
|
||||
)}
|
||||
@@ -714,7 +721,7 @@ export default function MobileSendPopUp({
|
||||
text={smsTpCd ? getSmsErrorMsg : getEvntErrorMsg}
|
||||
onClick={_onClose}
|
||||
hasButton
|
||||
button1Text={$L("OK")}
|
||||
button1Text={$L('OK')}
|
||||
/>
|
||||
)}
|
||||
{(smsRetCode === 0 ||
|
||||
@@ -727,7 +734,7 @@ export default function MobileSendPopUp({
|
||||
text={MSG_SUCCESS_SENT}
|
||||
onClick={onClose}
|
||||
hasButton
|
||||
button1Text={$L("OK")}
|
||||
button1Text={$L('OK')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../style/CommonStyle.module.less";
|
||||
@import "../../style/utils.module.less";
|
||||
@import '../../style/CommonStyle.module.less';
|
||||
@import '../../style/utils.module.less';
|
||||
|
||||
/* 🆕 [NEW] Figma 디자인용 타이틀 헤드 스타일 */
|
||||
.titleHead {
|
||||
@@ -14,7 +14,7 @@
|
||||
text-align: left; // center → left
|
||||
color: black;
|
||||
font-size: 32px;
|
||||
font-family: "LG Smart UI";
|
||||
font-family: 'LG Smart UI';
|
||||
font-weight: 700;
|
||||
line-height: 42px;
|
||||
word-wrap: break-word;
|
||||
@@ -32,12 +32,15 @@
|
||||
height: 50px;
|
||||
margin-right: 15px; // TV 호환: gap 대신 margin 사용
|
||||
border-radius: 100%;
|
||||
&.headerTopRow__brandLogo__qvc {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.headerTopRow__productId {
|
||||
color: #808080;
|
||||
font-size: 24px;
|
||||
font-family: "LG Smart UI";
|
||||
font-family: 'LG Smart UI';
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
word-wrap: break-word;
|
||||
@@ -57,7 +60,7 @@
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
display: flex;
|
||||
> .container__header__productImg,
|
||||
@@ -178,7 +181,7 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap:wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.instruction {
|
||||
width: 492.5px; // 고정 너비
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TNewPopUp from '../TPopUp/TNewPopUp'; // TNewPopUp 컴포넌트의 정확한 경로를 확인해주세요.
|
||||
import css from './OptionalConfirm.module.less';
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
// src/components/Optional/OptionalTermsConfirm.jsx
|
||||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TPopUp from '../TPopUp/TPopUp';
|
||||
import TButton from '../TButton/TButton';
|
||||
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
|
||||
import TButtonScroller from '../TButtonScroller/TButtonScroller';
|
||||
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import { setHidePopup } from '../../actions/commonActions';
|
||||
import { setMyPageTermsAgree } from '../../actions/myPageActions';
|
||||
import {
|
||||
$L,
|
||||
scaleH,
|
||||
scaleW,
|
||||
} from '../../utils/helperMethods';
|
||||
import TButton from '../TButton/TButton';
|
||||
import TButtonScroller from '../TButtonScroller/TButtonScroller';
|
||||
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
|
||||
import TPopUp from '../TPopUp/TPopUp';
|
||||
import css from './OptionalTermsConfirm.module.less';
|
||||
|
||||
const OptionalTermsConfirm = ({ open }) => {
|
||||
@@ -18,7 +30,6 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
|
||||
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
|
||||
|
||||
|
||||
const optionalTermsData = useSelector((state) =>
|
||||
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
|
||||
);
|
||||
@@ -72,10 +83,10 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
setIsWarningPopupVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleDontAskAgain = () => {
|
||||
const handleDontAskAgain = useCallback(() => {
|
||||
console.log("Don't Ask Again 처리 필요");
|
||||
dispatch(setHidePopup());
|
||||
};
|
||||
},[dispatch]);
|
||||
|
||||
if (isTermsPopupVisible) {
|
||||
return (
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
// src/components/Optional/OptionalTermsConfirm.jsx
|
||||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TNewPopUp from '../TPopUp/TNewPopUp';
|
||||
import TButton from '../TButton/TButton';
|
||||
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
|
||||
import TButtonScroller from '../TButtonScroller/TButtonScroller';
|
||||
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import { setHidePopup } from '../../actions/commonActions';
|
||||
import { setMyPageTermsAgree } from '../../actions/myPageActions';
|
||||
import {
|
||||
$L,
|
||||
scaleH,
|
||||
scaleW,
|
||||
} from '../../utils/helperMethods';
|
||||
import TButtonScroller from '../TButtonScroller/TButtonScroller';
|
||||
import TNewPopUp from '../TPopUp/TNewPopUp';
|
||||
import css from './OptionalTermsConfirmBottom.module.less';
|
||||
import cssPopup from '../TPopUp/TNewPopUp.module.less';
|
||||
import Spotlight from "@enact/spotlight";
|
||||
|
||||
const OptionalTermsConfirm = ({ open }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -37,13 +48,13 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
}
|
||||
}, [open, isTermsPopupVisible, isWarningPopupVisible]);
|
||||
|
||||
const handleMainPopupClose = useCallback(() => {
|
||||
dispatch(setHidePopup());
|
||||
}, [dispatch]);
|
||||
// const handleMainPopupClose = useCallback(() => {
|
||||
// dispatch(setHidePopup());
|
||||
// }, [dispatch]);
|
||||
|
||||
const handleCheckboxToggle = useCallback(({ selected }) => {
|
||||
setIsChecked(selected);
|
||||
}, []);
|
||||
// const handleCheckboxToggle = useCallback(({ selected }) => {
|
||||
// setIsChecked(selected);
|
||||
// }, []);
|
||||
|
||||
const handleViewTermsClick = useCallback(() => {
|
||||
setIsTermsPopupVisible(true);
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function THeader({
|
||||
ariaLabel,
|
||||
children,
|
||||
kind,
|
||||
sponserImage,
|
||||
...rest
|
||||
}) {
|
||||
const convertedTitle = useMemo(() => {
|
||||
@@ -86,6 +87,17 @@ export default function THeader({
|
||||
</Marquee>
|
||||
|
||||
{children}
|
||||
{sponserImage &&(
|
||||
<div className={css.sponserImgBox}>
|
||||
<CustomImage
|
||||
src={sponserImage}
|
||||
className={css.sponserImg}
|
||||
/>
|
||||
<div className={css.sponserTextBox}>
|
||||
SPONSORED BY
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
.title {
|
||||
width: 1788px;
|
||||
font-size: 42px;
|
||||
@@ -42,3 +42,24 @@
|
||||
box-shadow: 0px 6px 30px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.sponserImgBox {
|
||||
position:absolute;
|
||||
right:0;
|
||||
top:0;
|
||||
height:30px;
|
||||
display:flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.sponserImg {
|
||||
height:30px;
|
||||
}
|
||||
.sponserTextBox {
|
||||
padding:3px;
|
||||
background-color: #474747;
|
||||
color:rgba(255, 255, 255, 0.5);
|
||||
font-size:14px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
> img {
|
||||
.size(@w: inherit, @h: inherit);
|
||||
object-fit: cover;
|
||||
border: solid 1px #f0f0f0;
|
||||
// border: solid 1px #f0f0f0;
|
||||
}
|
||||
|
||||
// discount rate
|
||||
@@ -144,7 +144,7 @@
|
||||
> img {
|
||||
.size(@w: 288px, @h: 288px);
|
||||
object-fit: contain;
|
||||
border: solid 1px #f0f0f0;
|
||||
// border: solid 1px #f0f0f0;
|
||||
}
|
||||
|
||||
// discount rate
|
||||
|
||||
@@ -483,7 +483,7 @@
|
||||
.default-style();
|
||||
|
||||
.scrollInfo {
|
||||
width: 900px;
|
||||
width: 850px;
|
||||
background-color: @BG_COLOR_01;
|
||||
color: @COLOR_GRAY03;
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import { getDeviceAdditionInfo } from "../../actions/deviceActions";
|
||||
import { scaleH, scaleW } from "../../utils/helperMethods";
|
||||
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
|
||||
import {
|
||||
scaleH,
|
||||
scaleW,
|
||||
} from '../../utils/helperMethods';
|
||||
|
||||
export default function TQRCode({
|
||||
isBillingProductVisible,
|
||||
@@ -51,6 +60,6 @@ export default function TQRCode({
|
||||
correctLevel: window.QRCode.CorrectLevel.L,
|
||||
});
|
||||
}
|
||||
}, [text, deviceInfo, entryMenu, nowMenu]);
|
||||
}, [text, deviceInfo, entryMenu, nowMenu, width, height]);
|
||||
return <div aria-label={ariaLabel} ref={qrcodeRef} />;
|
||||
}
|
||||
|
||||
102
com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.jsx
Normal file
102
com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.jsx
Normal 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} />;
|
||||
}
|
||||
@@ -1,16 +1,27 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import compose from 'ramda/src/compose';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Job } from '@enact/core/util';
|
||||
import { Marquee, MarqueeController } from '@enact/sandstone/Marquee';
|
||||
import {
|
||||
Marquee,
|
||||
MarqueeController,
|
||||
} from '@enact/sandstone/Marquee';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import css from './TabItemSub.module.less';
|
||||
import { sendLogTotalRecommend } from '../../actions/logActions';
|
||||
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from '../../utils/Config';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MESSAGE_ID,
|
||||
} from '../../utils/Config';
|
||||
import css from './TabItemSub.module.less';
|
||||
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
@@ -122,11 +133,11 @@ const TabItemBase = ({
|
||||
<>
|
||||
{subtitle && (
|
||||
<div className={css.textWithIcon}>
|
||||
{IconComponent && (
|
||||
{/* {IconComponent && (
|
||||
<span className={css.iconWrapper}>
|
||||
<IconComponent iconType={focused ? 'focused' : selected ? 'selected' : 'normal'} />
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
<Marquee
|
||||
marqueeDisabled={!focused}
|
||||
marqueeOn={'focus'}
|
||||
|
||||
@@ -172,23 +172,8 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
||||
break;
|
||||
//브랜드
|
||||
case 10300:
|
||||
result = [
|
||||
// NBCU 브랜드 (하드코딩)
|
||||
{
|
||||
icons: FeaturedBrandIcon,
|
||||
id: 'nbcu-brand',
|
||||
path: 'assets/images/featuredBrands/nbcu.svg',
|
||||
patncNm: 'NBCU',
|
||||
spotlightId: 'spotlight_featuredbrand_nbcu',
|
||||
target: [
|
||||
{
|
||||
name: panel_names.FEATURED_BRANDS_PANEL,
|
||||
panelInfo: { from: 'gnb', patnrId: 'NBCU' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// API에서 가져온 기존 브랜드들
|
||||
...(data?.shortFeaturedBrands?.map((item) => ({
|
||||
result =
|
||||
data?.shortFeaturedBrands?.map((item) => ({
|
||||
icons: FeaturedBrandIcon,
|
||||
id: item.patnrId,
|
||||
path: item.patncLogoPath,
|
||||
@@ -200,8 +185,7 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
||||
panelInfo: { from: 'gnb', patnrId: item.patnrId },
|
||||
},
|
||||
],
|
||||
})) || []),
|
||||
];
|
||||
})) || [];
|
||||
break;
|
||||
//
|
||||
case 10600:
|
||||
|
||||
@@ -1157,11 +1157,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
// detection of when "more" is pressed vs when the state is updated is mismatched. Using an
|
||||
// instance variable that's only set and used for this express purpose seems cleanest.
|
||||
|
||||
// TabContainerV2가 표시 중이면 자동으로 닫지 않음
|
||||
if (this.props.tabContainerVersion === 2 && this.props.belowContentsVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.autoCloseTimeout && !this.props.sideContentsVisible) {
|
||||
this.autoCloseJob.startAfter(this.props.autoCloseTimeout);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
export default function useScrollTo() {
|
||||
export default function useScrollTo({ skipAutoScrollTop = false } = {}) {
|
||||
const scrollTo = useRef();
|
||||
|
||||
const scrollTop = useCallback(
|
||||
@@ -23,8 +23,10 @@ export default function useScrollTo() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollTop();
|
||||
}, []);
|
||||
if (!skipAutoScrollTop) {
|
||||
scrollTop();
|
||||
}
|
||||
}, [skipAutoScrollTop]);
|
||||
|
||||
return { getScrollTo, scrollLeft, scrollTop, scrollToRef: scrollTo };
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
(action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY')
|
||||
) {
|
||||
const caller = new Error().stack.split('\n')[1]?.trim();
|
||||
console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
|
||||
console.log(' Payload:', action.payload);
|
||||
// console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
|
||||
// console.log(' Payload:', action.payload);
|
||||
}
|
||||
|
||||
// GNB 호출 식별을 위한 helper 함수
|
||||
@@ -81,7 +81,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const isGNB = isGNBCall();
|
||||
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
|
||||
if (DEBUG_MODE)
|
||||
console.log('[PANEL] PUSH_PANEL:', {
|
||||
console.log(`[PANEL] PUSH_PANEL: ${panelName}`, {
|
||||
panelName,
|
||||
panelInfo,
|
||||
isGNB,
|
||||
@@ -106,7 +106,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
const panelsAfter = stateAfter.panels.panels;
|
||||
console.log('[PANEL_HISTORY] After PUSH_PANEL:', {
|
||||
console.log(`[PANEL_HISTORY] PUSH_PANEL: ${panelName}`, {
|
||||
panelHistory: panelHistoryAfter,
|
||||
panels: panelsAfter,
|
||||
});
|
||||
@@ -125,11 +125,19 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
// POP 후 top panel을 기록 (이전 패널로 돌아감)
|
||||
if (panels.length > 0) {
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (DEBUG_MODE) {
|
||||
console.log(`[PANEL-TRACE] POP_PANEL middleware stack: ${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) {
|
||||
const isGNB = isGNBCall();
|
||||
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산
|
||||
if (DEBUG_MODE)
|
||||
console.log('[PANEL] POP_PANEL:', {
|
||||
console.log(`[PANEL] POP_PANEL: ${topPanel.name}`, {
|
||||
panelName: topPanel.name,
|
||||
panelInfo: topPanel.panelInfo || {},
|
||||
isGNB,
|
||||
@@ -154,7 +162,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
const panelsAfter = stateAfter.panels.panels;
|
||||
console.log('[PANEL_HISTORY] After POP_PANEL:', {
|
||||
console.log(`[PANEL_HISTORY] POP_PANEL: ${topPanel.name}`, {
|
||||
panelHistory: panelHistoryAfter,
|
||||
panels: panelsAfter,
|
||||
});
|
||||
@@ -177,7 +185,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const isGNB = isGNBCall();
|
||||
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
|
||||
if (DEBUG_MODE)
|
||||
console.log('[PANEL] UPDATE_PANEL:', {
|
||||
console.log(`[PANEL] UPDATE_PANEL: ${panelName}`, {
|
||||
panelName,
|
||||
panelInfo,
|
||||
isGNB,
|
||||
@@ -202,7 +210,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
const stateAfter = store.getState();
|
||||
const panelHistoryAfter = stateAfter.panelHistory;
|
||||
const panelsAfter = stateAfter.panels.panels;
|
||||
console.log('[PANEL_HISTORY] After UPDATE_PANEL:', {
|
||||
console.log(`[PANEL_HISTORY] UPDATE_PANEL: ${panelName}`, {
|
||||
panelHistory: panelHistoryAfter,
|
||||
panels: panelsAfter,
|
||||
});
|
||||
@@ -218,11 +226,15 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
|
||||
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화
|
||||
case types.RESET_PANELS: {
|
||||
if (DEBUG_MODE)
|
||||
console.log('[PANEL] RESET_PANELS:', {
|
||||
if (DEBUG_MODE) {
|
||||
const resetPanelNameForLog = (action.payload && action.payload.length > 0)
|
||||
? action.payload[0].name
|
||||
: 'homepanel';
|
||||
console.log(`[PANEL] RESET_PANELS: ${resetPanelNameForLog}`, {
|
||||
payload: action.payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (DEBUG_MODE)
|
||||
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 panelHistoryAfter = stateAfter.panelHistory;
|
||||
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,
|
||||
panels: panelsAfter,
|
||||
});
|
||||
|
||||
@@ -44,6 +44,17 @@ const initialState = {
|
||||
brandRecentlyAiredData: {
|
||||
data: {},
|
||||
},
|
||||
|
||||
brandShopByShowData: {
|
||||
data: {},
|
||||
},
|
||||
|
||||
brandTopBannerData: {
|
||||
data: {},
|
||||
},
|
||||
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 저장소
|
||||
brandShopByShowCategoryGroups: {},
|
||||
};
|
||||
|
||||
export const brandReducer = (state = initialState, action) => {
|
||||
@@ -155,6 +166,51 @@ export const brandReducer = (state = initialState, action) => {
|
||||
brandRecentlyAiredData: action.payload,
|
||||
};
|
||||
|
||||
case types.GET_BRAND_SHOP_BY_SHOW: {
|
||||
// 일부 응답은 리스트 없이 내려와 기존 데이터를 덮어 지우는 문제가 있어 조건부 병합
|
||||
const prevData = state.brandShopByShowData?.data || {};
|
||||
const nextData = action.payload?.data || {};
|
||||
const hasNextList = Array.isArray(nextData.brandShopByShowContsList);
|
||||
|
||||
// 리스트가 없으면 이전 리스트 유지
|
||||
const mergedData = hasNextList
|
||||
? nextData
|
||||
: { ...prevData, ...nextData, brandShopByShowContsList: prevData.brandShopByShowContsList };
|
||||
|
||||
// 🆕 [251210] patnrId=21인 경우 그룹 데이터 별도 저장
|
||||
const updatedCategoryGroups = { ...state.brandShopByShowCategoryGroups };
|
||||
|
||||
if (action.payload?.patnrId === 21 || action.payload?.patnrId === "21") {
|
||||
const patnrId = String(action.payload.patnrId);
|
||||
|
||||
// patnrId별 그룹 데이터가 없으면 초기화
|
||||
if (!updatedCategoryGroups[patnrId]) {
|
||||
updatedCategoryGroups[patnrId] = {};
|
||||
}
|
||||
|
||||
// 현재 contsId에 대한 그룹 정보 저장
|
||||
if (nextData.brandShopByShowContsInfo?.contsId) {
|
||||
const contsId = nextData.brandShopByShowContsInfo.contsId;
|
||||
updatedCategoryGroups[patnrId][contsId] = nextData.brandShopByShowContsInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
brandShopByShowData: {
|
||||
...action.payload,
|
||||
data: mergedData,
|
||||
},
|
||||
brandShopByShowCategoryGroups: updatedCategoryGroups,
|
||||
};
|
||||
}
|
||||
|
||||
case types.GET_BRAND_TOP_BANNER:
|
||||
return {
|
||||
...state,
|
||||
brandTopBannerData: action.payload,
|
||||
};
|
||||
|
||||
case types.SET_BRAND_LIVE_CHANNEL_UPCOMING:
|
||||
return {
|
||||
...state,
|
||||
@@ -184,6 +240,25 @@ export const brandReducer = (state = initialState, action) => {
|
||||
};
|
||||
}
|
||||
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 설정
|
||||
case types.SET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: {
|
||||
return {
|
||||
...state,
|
||||
brandShopByShowCategoryGroups: {
|
||||
...state.brandShopByShowCategoryGroups,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터 초기화
|
||||
case types.RESET_BRAND_SHOP_BY_SHOW_CATEGORY_GROUPS: {
|
||||
return {
|
||||
...state,
|
||||
brandShopByShowCategoryGroups: {},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,12 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
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', {
|
||||
targetPanel: action.payload || 'last_panel',
|
||||
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);
|
||||
}
|
||||
|
||||
// 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', {
|
||||
resultPanels: resultPanels.map((p) => p.name),
|
||||
lastAction,
|
||||
@@ -132,6 +145,60 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
case types.UPDATE_PANEL: {
|
||||
let lastIndex = -1;
|
||||
let lastAction = 'update';
|
||||
const hasDetailPanel = state.panels.some((p) => p.name === panel_names.DETAIL_PANEL);
|
||||
const isPlayerPanel =
|
||||
action.payload.name === panel_names.PLAYER_PANEL ||
|
||||
action.payload.name === panel_names.PLAYER_PANEL_NEW;
|
||||
const existingPanel = state.panels.find((p) => p.name === action.payload.name);
|
||||
let nextPanelInfo = action.payload.panelInfo || {};
|
||||
|
||||
// lockModalFalse 플래그 처리: DetailPanel이 스택에 있거나 lock이 이미 true면 modal=true 업데이트를 차단
|
||||
if (isPlayerPanel && existingPanel) {
|
||||
const lockFlag =
|
||||
existingPanel.panelInfo?.lockModalFalse === true || nextPanelInfo.lockModalFalse === true;
|
||||
|
||||
// unlock 명시 시 그대로 진행
|
||||
if (nextPanelInfo.lockModalFalse === false) {
|
||||
// do nothing
|
||||
} else if (lockFlag && nextPanelInfo.modal === true) {
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
lockModalFalse: true,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
} else if (lockFlag && nextPanelInfo.modal === undefined && hasDetailPanel) {
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal:
|
||||
existingPanel.panelInfo?.modal === true ? false : existingPanel.panelInfo?.modal,
|
||||
modalContainerId:
|
||||
existingPanel.panelInfo?.modal === true
|
||||
? undefined
|
||||
: existingPanel.panelInfo?.modalContainerId,
|
||||
lockModalFalse: true,
|
||||
modalStyle: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalStyle,
|
||||
modalScale: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalScale,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
} else if (hasDetailPanel && nextPanelInfo.modal === true) {
|
||||
// DetailPanel 존재 시 modal=true 업데이트 차단
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
// 배열의 끝에서부터 시작하여 조건에 맞는 마지막 인덱스 찾기
|
||||
for (let i = state.panels.length - 1; i >= 0; i--) {
|
||||
if (state.panels[i].name === action.payload.name) {
|
||||
@@ -143,7 +210,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
index === lastIndex
|
||||
? {
|
||||
...panel,
|
||||
panelInfo: { ...panel.panelInfo, ...action.payload.panelInfo },
|
||||
panelInfo: { ...panel.panelInfo, ...nextPanelInfo },
|
||||
}
|
||||
: panel
|
||||
);
|
||||
|
||||
@@ -114,6 +114,7 @@ export const ACTIVE_POPUP = {
|
||||
optionalConfirm: 'optionalConfirm',
|
||||
energyPopup: 'energyPopup',
|
||||
addCartPopup: 'addCartPopup',
|
||||
scrollPopup: 'scrollPopup',
|
||||
};
|
||||
export const DEBUG_VIDEO_SUBTITLE_TEST = false;
|
||||
export const AUTO_SCROLL_DELAY = 600;
|
||||
|
||||
@@ -458,7 +458,7 @@ const tap = fp.curry((fn, value) => {
|
||||
* @param {*} value 대상 값
|
||||
*/
|
||||
const trace = fp.curry((label, value) => {
|
||||
console.log(label, value);
|
||||
// console.log(label, value);
|
||||
return value;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
setShowPopup,
|
||||
} from '../../../actions/commonActions';
|
||||
import { sendLogTotalRecommend } from '../../../actions/logActions';
|
||||
import { popPanel } from '../../../actions/panelActions';
|
||||
import {
|
||||
popPanel,
|
||||
pushPanel,
|
||||
} from '../../../actions/panelActions';
|
||||
import TButton from '../../../components/TButton/TButton';
|
||||
import TPopUp from '../../../components/TPopUp/TPopUp';
|
||||
import TQRCode from '../../../components/TQRCode/TQRCode';
|
||||
@@ -249,13 +252,25 @@ export default function InformationContainer({
|
||||
);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dispatch(setHidePopup());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleDone = useCallback(() => {
|
||||
dispatch(setHidePopup());
|
||||
// dispatch(setHidePopup());
|
||||
const { patnrId, prdtId } = checkoutData.productList[0];
|
||||
dispatch(popPanel());
|
||||
}, [dispatch]);
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: Config.panel_names.DETAIL_PANEL,
|
||||
panelInfo: { patnrId, prdtId },
|
||||
})
|
||||
);
|
||||
}, [dispatch, checkoutData]);
|
||||
|
||||
const { shippingAddressList, billingAddressList, cardInfo } = checkoutData || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!shippingAddressList || !billingAddressList || !cardInfo) {
|
||||
// if (shippingAddressList || billingAddressList || cardInfo) { //확인용도로 반대로 테스트중.
|
||||
dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup));
|
||||
}
|
||||
}, [shippingAddressList, billingAddressList, cardInfo, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -282,8 +297,8 @@ export default function InformationContainer({
|
||||
>
|
||||
ADD/EDIT
|
||||
</TButton>
|
||||
{checkoutData?.shippingAddressList && (
|
||||
<ShippingAddressCard list={checkoutData.shippingAddressList} onFocus={handleFocus} />
|
||||
{shippingAddressList && (
|
||||
<ShippingAddressCard list={shippingAddressList} onFocus={handleFocus} />
|
||||
)}
|
||||
</div>
|
||||
<div className={css.listBox}>
|
||||
@@ -300,8 +315,8 @@ export default function InformationContainer({
|
||||
{/* <div style={{ padding: '10px', textAlign: 'center', color: '#999' }}>
|
||||
Mock Billing Address
|
||||
</div> */}
|
||||
{checkoutData?.billingAddressList && (
|
||||
<BillingAddressCard list={checkoutData.billingAddressList} onFocus={handleFocus} />
|
||||
{billingAddressList && (
|
||||
<BillingAddressCard list={billingAddressList} onFocus={handleFocus} />
|
||||
)}
|
||||
</div>
|
||||
<div className={css.listBox}>
|
||||
@@ -314,7 +329,7 @@ export default function InformationContainer({
|
||||
>
|
||||
ADD/EDIT
|
||||
</TButton>
|
||||
{checkoutData?.cardInfo && <PaymentCard list={checkoutData.cardInfo} />}
|
||||
{cardInfo && <PaymentCard list={cardInfo} />}
|
||||
</div>
|
||||
<div className={css.listBox}>
|
||||
<Subject title="OFFERS & PROMOTION" />
|
||||
@@ -355,7 +370,7 @@ export default function InformationContainer({
|
||||
'Please update your information and complete the payment on your mobile. By clicking the OK button, you will be redirected to the product details page'
|
||||
)}
|
||||
</h3>
|
||||
<TButton className={css.popupBtn} onClick={handleDone}>
|
||||
<TButton className={css.popupBtn} onClick={handleCancel}>
|
||||
{$L('OK')}
|
||||
</TButton>
|
||||
</div>
|
||||
|
||||
@@ -280,6 +280,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] DetailPanel cleanup - calling finishModalMediaForce');
|
||||
dispatch(finishModalMediaForce());
|
||||
};
|
||||
}, [dispatch]);
|
||||
@@ -303,18 +304,25 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const sourcePanel = panelInfo?.sourcePanel;
|
||||
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되는 시점
|
||||
console.log('[DetailPanel] unmount:', {
|
||||
sourcePanel,
|
||||
sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel unmount details:', {
|
||||
// sourcePanel,
|
||||
// sourceMenu,
|
||||
// timestamp: Date.now(),
|
||||
// });
|
||||
|
||||
// sourcePanel에 따른 상태 업데이트
|
||||
switch (sourcePanel) {
|
||||
case panel_names.PLAYER_PANEL: {
|
||||
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
|
||||
console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
|
||||
console.log('[PANEL][DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
@@ -323,6 +331,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
|
||||
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
|
||||
lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달
|
||||
lockModalFalse: false, // Detail 종료 시 lock 해제
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -376,6 +385,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const sourcePanel = panelInfo?.sourcePanel;
|
||||
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(
|
||||
() => {
|
||||
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
|
||||
@@ -384,7 +401,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
switch (sourcePanel) {
|
||||
case panel_names.PLAYER_PANEL:
|
||||
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
|
||||
console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행');
|
||||
console.log('[🟠UNIQUE_DETAIL_BACK🟠] PlayerPanel 출신: 모달 정리만 수행');
|
||||
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
|
||||
dispatch(finishVideoPreview());
|
||||
break;
|
||||
@@ -393,17 +410,18 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
case panel_names.SEARCH_PANEL:
|
||||
default:
|
||||
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
|
||||
console.log(
|
||||
'[DetailPanel] onBackClick - source panel:',
|
||||
sourcePanel,
|
||||
'백그라운드 비디오 일시 중지'
|
||||
);
|
||||
// console.log(
|
||||
// '[🟠UNIQUE_DETAIL_BACK🟠] source panel:',
|
||||
// sourcePanel,
|
||||
// '백그라운드 비디오 일시 중지'
|
||||
// );
|
||||
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
|
||||
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
|
||||
dispatch(finishVideoPreview());
|
||||
break;
|
||||
}
|
||||
|
||||
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] Calling popPanel(DETAIL_PANEL)');
|
||||
dispatch(popPanel(panel_names.DETAIL_PANEL));
|
||||
},
|
||||
() => {
|
||||
@@ -425,7 +443,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
if (shouldUpdatePanel) {
|
||||
console.log(
|
||||
'[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'
|
||||
'[PANEL][DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'
|
||||
);
|
||||
dispatch(
|
||||
updatePanel({
|
||||
@@ -1025,34 +1043,56 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// ProductAllSection에 비디오가 있는지 확인
|
||||
const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)();
|
||||
|
||||
console.log('[BgVideo] DetailPanel - Video Control Check:', {
|
||||
console.log('[Detail-BG] 🎬 DetailPanel - Video Control Check (mount/update):', {
|
||||
hasPlayerPanel,
|
||||
isModal,
|
||||
playerPanelModalStatus: isModal,
|
||||
hasProductVideo,
|
||||
sourceMenu: panelInfo?.sourceMenu,
|
||||
productDataUrl: productData?.prdtMediaUrl,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
|
||||
if (hasPlayerPanel && hasProductVideo) {
|
||||
console.log('[BgVideo] DetailPanel - Pausing video');
|
||||
console.log('[Detail-BG] ⏸️ DetailPanel - Pausing PlayerPanel video (match: playerPanel + productVideo)', {
|
||||
isModalVideo: isModal,
|
||||
action: isModal ? 'pauseModalVideo' : 'pauseFullscreenVideo',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (isModal) {
|
||||
dispatch(pauseModalVideo());
|
||||
} else {
|
||||
dispatch(pauseFullscreenVideo());
|
||||
}
|
||||
} else {
|
||||
console.log('[BgVideo] DetailPanel - Skipping pause');
|
||||
}
|
||||
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping pause (no playerPanel or no productVideo)', {
|
||||
hasPlayerPanel,
|
||||
hasProductVideo,
|
||||
reason: !hasPlayerPanel ? 'no playerPanel' : 'no productVideo',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
|
||||
if (hasPlayerPanel && hasProductVideo) {
|
||||
console.log('[BgVideo] DetailPanel - Resuming video');
|
||||
console.log('[Detail-BG] ▶️ DetailPanel - Resuming PlayerPanel video (unmount cleanup)', {
|
||||
isModalVideo: isModal,
|
||||
action: isModal ? 'resumeModalVideo' : 'resumeFullscreenVideo',
|
||||
sourceMenu: panelInfo?.sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (isModal) {
|
||||
dispatch(resumeModalVideo());
|
||||
} else {
|
||||
dispatch(resumeFullscreenVideo());
|
||||
}
|
||||
} else {
|
||||
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping resume on unmount', {
|
||||
hasPlayerPanel,
|
||||
hasProductVideo,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -1080,12 +1120,33 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const hasPlayerPanel = panels.some(
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true
|
||||
);
|
||||
const launchedFromPlayer = panelInfo?.fromPlayer || panelInfo?.sourcePanel === panel_names.PLAYER_PANEL;
|
||||
|
||||
if (hasPlayerPanel) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview');
|
||||
if (hasPlayerPanel && !launchedFromPlayer) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview (non-player source)');
|
||||
dispatch(finishVideoPreview());
|
||||
} else if (hasPlayerPanel && launchedFromPlayer) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - launched from Player, skip finishVideoPreview');
|
||||
|
||||
// Detail 동안 modal=true로 바뀌지 않도록 lockModalFalse 설정
|
||||
const playerPanelEntry = panels.find(
|
||||
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
|
||||
);
|
||||
if (playerPanelEntry?.panelInfo?.modal === true) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: playerPanelEntry.name,
|
||||
panelInfo: {
|
||||
...playerPanelEntry.panelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
lockModalFalse: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [panels, dispatch]);
|
||||
}, [panels, dispatch, panelInfo?.fromPlayer, panelInfo?.sourcePanel]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -680,7 +680,8 @@
|
||||
|
||||
.productDetailsButton,
|
||||
.userReviewsButton,
|
||||
.youMayLikeButton {
|
||||
.youMayLikeButton,
|
||||
.seeMoreProductButton {
|
||||
align-self: stretch;
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경
|
||||
@@ -708,7 +709,8 @@
|
||||
|
||||
.productDetailsButton,
|
||||
.userReviewsButton,
|
||||
.youMayLikeButton {
|
||||
.youMayLikeButton,
|
||||
.seeMoreProductButton {
|
||||
align-self: stretch;
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
import React, { useCallback } from "react";
|
||||
import css from "./ProductDescription.module.less";
|
||||
import { $L, removeSpecificTags } from "../../../../utils/helperMethods";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
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와 충돌 문제로 인해
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
@@ -19,11 +42,22 @@ const Container = SpotlightContainerDecorator(
|
||||
);
|
||||
|
||||
export default function ProductDescription({ productInfo }) {
|
||||
const { popupVisible, activePopup } = useSelector(
|
||||
(state) => state.common.popup
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
const productDescription = useCallback(() => {
|
||||
const sanitizedString = removeSpecificTags(productInfo?.prdtDesc);
|
||||
return { __html: sanitizedString };
|
||||
}, [productInfo?.prdtDesc]);
|
||||
|
||||
const productDescriptionText = useMemo(() => {
|
||||
return removeSpecificTags(productInfo?.prdtDesc);
|
||||
}, [productInfo?.prdtDesc]);
|
||||
|
||||
// 왼쪽 화살표 키 이벤트 처리
|
||||
const handleKeyDown = useCallback((ev) => {
|
||||
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 직접 사용 패턴
|
||||
// prdtDesc가 없으면 렌더링하지 않음
|
||||
if (!productInfo?.prdtDesc) {
|
||||
@@ -41,36 +89,61 @@ export default function ProductDescription({ productInfo }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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}
|
||||
<>
|
||||
<Container
|
||||
className={css.descriptionContainer}
|
||||
spotlightId="product-description-container"
|
||||
>
|
||||
<div
|
||||
className={css.productDescription}
|
||||
dangerouslySetInnerHTML={productDescription()}
|
||||
/>
|
||||
</SpottableComponent>
|
||||
</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")}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,3 +51,8 @@
|
||||
|
||||
}
|
||||
}
|
||||
.scrollContainer {
|
||||
padding: 31px;
|
||||
font-size: 26px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import css from "./ProductDetail.new.module.less";
|
||||
import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import CustomImage from "../../../../components/CustomImage/CustomImage";
|
||||
import indicatorDefaultImage from "../../../../../assets/images/img-thumb-empty-144@3x.png";
|
||||
import useScrollTo from "../../../../hooks/useScrollTo";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import indicatorDefaultImage
|
||||
from '../../../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import TVirtualGridList
|
||||
from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import useScrollTo from '../../../../hooks/useScrollTo';
|
||||
// TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해
|
||||
import { removeSpecificTags } from "../../../../utils/helperMethods";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import { removeSpecificTags } from '../../../../utils/helperMethods';
|
||||
import css from './ProductDetail.new.module.less';
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{
|
||||
@@ -59,7 +70,8 @@ export default function ProductDetail({ productInfo }) {
|
||||
const image = listImages[0] || indicatorDefaultImage;
|
||||
const imageIndex = productInfo?.imageIndex ?? 0;
|
||||
const totalImages = productInfo?.totalImages ?? listImages.length;
|
||||
|
||||
const sponserImage = productInfo?.spnsrImgUrl;
|
||||
const spnsrNm = productInfo?.spnsrNm;
|
||||
return (
|
||||
<div className={css.thumbnailWrapper}>
|
||||
<CustomImage
|
||||
@@ -68,6 +80,19 @@ export default function ProductDetail({ productInfo }) {
|
||||
fallbackSrc={indicatorDefaultImage}
|
||||
className={css.productImage}
|
||||
/>
|
||||
{imageIndex === 0 && sponserImage &&(
|
||||
<div className={css.sponserImgBox}>
|
||||
<CustomImage
|
||||
src={sponserImage}
|
||||
alt={spnsrNm}
|
||||
fallbackSrc={indicatorDefaultImage}
|
||||
className={css.sponserImg}
|
||||
/>
|
||||
<div className={css.sponserTextBox}>
|
||||
SPONSORED BY
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [listImages, productInfo?.imageIndex, productInfo?.totalImages]);
|
||||
|
||||
@@ -70,14 +70,15 @@
|
||||
|
||||
.thumbnailWrapper {
|
||||
position: relative;
|
||||
width: 658px;
|
||||
// width: 658px;
|
||||
width:100%;
|
||||
height: 610px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.productImage {
|
||||
width: 100%;
|
||||
width: 658px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -89,4 +90,20 @@
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
.sponserImgBox {
|
||||
position:absolute;
|
||||
right:20px;
|
||||
top:0px;
|
||||
.sponserImg {
|
||||
width:145px;
|
||||
}
|
||||
.sponserTextBox {
|
||||
padding: 3px;
|
||||
background-color: #474747;
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
position: relative;
|
||||
width: 1114px; // ProductDetail과 동일한 고정 크기
|
||||
max-width: 1114px;
|
||||
height: 740px; // ProductDetail과 동일한 고정 높이
|
||||
margin-bottom: 30px; // ProductDetail과 동일한 간격
|
||||
height: 632px !important; // ProductDetail과 동일한 고정 높이
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
border-radius: 12px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-sizing: border-box;
|
||||
padding: 6px; // 포커스 테두리를 위한 공간
|
||||
overflow: hidden;
|
||||
@@ -80,7 +79,7 @@
|
||||
z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임
|
||||
border: 6px solid @PRIMARY_COLOR_RED;
|
||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||
border-radius: 12px;
|
||||
border-radius: 12px 12px 0px 0px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@@ -217,30 +216,28 @@
|
||||
}
|
||||
|
||||
.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%;
|
||||
height: 54px;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -311,6 +311,7 @@ export default function ProductVideo({
|
||||
if (!canPlayVideo) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpottableComponent
|
||||
className={css.videoContainer}
|
||||
onClick={handleVideoClick}
|
||||
@@ -330,12 +331,13 @@ export default function ProductVideo({
|
||||
<img src={playImg} alt="재생" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.notice}>
|
||||
<Marquee className={css.marquee} marqueeOn="render">
|
||||
<img src={ic_warning} alt={disclaimer} />
|
||||
<span>{disclaimer}</span>
|
||||
</Marquee>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
<div className={css.notice}>
|
||||
<Marquee className={css.marquee} marqueeOn="render">
|
||||
<img src={ic_warning} alt={disclaimer} />
|
||||
<span>{disclaimer}</span>
|
||||
</Marquee>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,6 @@ export default function UserReviewDetail({
|
||||
}
|
||||
}, [onNext, currentIndex, totalReviews]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("####currentIndex", currentIndex);
|
||||
}, [currentIndex]);
|
||||
|
||||
// 리뷰 데이터가 없을 때 처리
|
||||
if (!currentReview) {
|
||||
return (
|
||||
|
||||
@@ -48,10 +48,7 @@
|
||||
.averageOverallRating {
|
||||
width: 176px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
background-size: cover;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
@@ -104,36 +105,29 @@ export default function UserReviewsPopup({
|
||||
// All-Images 및 User-Reviews 모드를 위한 상태
|
||||
const [currentReviewIndex, setCurrentReviewIndex] = useState(0);
|
||||
|
||||
// 모드별 리뷰 인덱스 초기화
|
||||
// ✅ selectedImageIndex 변경 감지 추가: 클릭한 리뷰가 팝업에 표시되도록
|
||||
// 모드 변경 시 초기화
|
||||
useEffect(() => {
|
||||
console.log('[UserReviewsPopup] Mode or selectedImageIndex changed:', {
|
||||
mode,
|
||||
selectedImageIndex,
|
||||
allReviewsLength: allReviews?.length || 0,
|
||||
});
|
||||
|
||||
if (mode === "all-images" && images && images[selectedImageIndex]) {
|
||||
const selectedImage = images[selectedImageIndex];
|
||||
const reviewIndex = reviewsWithImages.findIndex(
|
||||
(review) => review.rvwId === selectedImage.reviewId
|
||||
);
|
||||
if (reviewIndex !== -1) {
|
||||
console.log('[UserReviewsPopup] all-images mode - found review index:', reviewIndex);
|
||||
setCurrentReviewIndex(reviewIndex);
|
||||
if (mode === "all-images" && selectedImageIndex !== undefined) {
|
||||
const selectedImage = images?.[selectedImageIndex];
|
||||
if (selectedImage) {
|
||||
const reviewIndex = reviewsWithImages.findIndex(
|
||||
(review) => review.rvwId === selectedImage.reviewId
|
||||
);
|
||||
if (reviewIndex !== -1) {
|
||||
console.log('[UserReviewsPopup] all-images mode - found review index:', reviewIndex);
|
||||
setCurrentReviewIndex(reviewIndex);
|
||||
}
|
||||
}
|
||||
} else if (mode === "user-reviews") {
|
||||
// User-Reviews 모드: selectedImageIndex를 그대로 사용
|
||||
} else if (mode === "user-reviews" && selectedImageIndex !== undefined) {
|
||||
console.log('[UserReviewsPopup] user-reviews mode - setting index to:', selectedImageIndex);
|
||||
setCurrentReviewIndex(selectedImageIndex);
|
||||
}
|
||||
}, [
|
||||
mode,
|
||||
selectedImageIndex, // ✅ 추가: 선택된 이미지 인덱스 변경 감지
|
||||
images, // ✅ 추가: 이미지 데이터 변경 감지
|
||||
reviewsWithImages, // ✅ 추가: 이미지 있는 리뷰 변경 감지
|
||||
allReviews, // ✅ 추가: allReviews 변경 감지
|
||||
]);
|
||||
}, [mode, selectedImageIndex]); // selectedImageIndex는 명시적 변경 시만
|
||||
|
||||
// 리뷰 네비게이션 핸들러 (All-Images 및 User-Reviews 모드)
|
||||
const handlePreviousReview = useCallback(() => {
|
||||
@@ -182,6 +176,7 @@ export default function UserReviewsPopup({
|
||||
// 모드가 변경되면 이미지 로딩 상태 초기화
|
||||
useEffect(() => {
|
||||
setImageLoadStates({});
|
||||
Spotlight.focus("review-popup-container");
|
||||
}, [mode]);
|
||||
|
||||
// 모든 이미지 표시
|
||||
@@ -216,6 +211,7 @@ export default function UserReviewsPopup({
|
||||
? "user-review-detail-prev"
|
||||
: "user-review-image-0"
|
||||
}
|
||||
spotlightId="review-popup-container"
|
||||
>
|
||||
{mode === "customer-images" && (
|
||||
<TScrollerDetail
|
||||
|
||||
@@ -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 SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
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 {
|
||||
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 TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import TVerticalPagenator
|
||||
from '../../../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import TVirtualGridList
|
||||
from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
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 css from './YouMayAlsoLike.module.less';
|
||||
|
||||
@@ -158,7 +178,7 @@ export default function YouMayAlsoLike({
|
||||
// DetailPanel을 언마운트하지 않고 상품 정보만 업데이트
|
||||
// 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨
|
||||
dispatch(
|
||||
updatePanel({
|
||||
pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
showNm: panelInfo?.showNm,
|
||||
@@ -170,8 +190,8 @@ export default function YouMayAlsoLike({
|
||||
launchedFromPlayer: launchedFromPlayer,
|
||||
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
|
||||
fromPanel: {
|
||||
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
|
||||
}, // 출처 정보 통합 객체
|
||||
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
|
||||
}, // 출처 정보 통합 객체
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import TQRCode from '../../../../components/TQRCode/TQRCode';
|
||||
import TQRCodeNew from '../../../../components/TQRCode/TQRCodeNew';
|
||||
import QRCodePatnr21 from './QRCodePatnr21';
|
||||
import { getQRCodeUrl } from '../../../../utils/helperMethods';
|
||||
import css from './QRCode.module.less';
|
||||
|
||||
@@ -56,13 +58,37 @@ export default function QRCode({
|
||||
return detailUrl;
|
||||
}, [productInfo, isShopByMobile, detailUrl]);
|
||||
|
||||
// patnrId === 21인 경우 qrImgUrl 처리
|
||||
const isPatnrId21 = productInfo?.patnrId === 21 || productInfo?.patnrId === "21";
|
||||
const qrImgUrl = isPatnrId21 ? productInfo?.qrImgUrl : null;
|
||||
|
||||
return (
|
||||
<div className={classNames(css.qrcode, kind ? css.detailQrcode : "")}>
|
||||
{/* {qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />} */}
|
||||
{kind === "detail" ? (
|
||||
<TQRCode text={qrCodeUrl} width="240" height="240" />
|
||||
isPatnrId21 ? (
|
||||
<QRCodePatnr21
|
||||
qrImgUrl={qrImgUrl}
|
||||
fallbackText={qrCodeUrl}
|
||||
width="240"
|
||||
height="240"
|
||||
/>
|
||||
) : (
|
||||
<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 릴리즈 후 */}
|
||||
<div className={css.tooltip}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export default function BuyNowPriceDisplay({
|
||||
display: "inline-flex",
|
||||
height: "60px",
|
||||
lineHeight: "60px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span className={css.price}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../../../../style/CommonStyle.module.less";
|
||||
@import "../../../../../style/utils.module.less";
|
||||
@import '../../../../../style/CommonStyle.module.less';
|
||||
@import '../../../../../style/utils.module.less';
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 36px;
|
||||
font-size: 30px;
|
||||
color: @COLOR_WHITE;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 36px;
|
||||
font-size: 30px;
|
||||
color: @COLOR_GRAY07;
|
||||
}
|
||||
.btmLayer {
|
||||
@@ -188,9 +188,11 @@
|
||||
.productNm {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-size: 36px;
|
||||
font-size: 38px;
|
||||
color: @COLOR_WHITE;
|
||||
flex:none;
|
||||
.elip(2);
|
||||
margin-bottom: 20px;
|
||||
height:76px;
|
||||
line-height:38px;
|
||||
}
|
||||
@@ -9,6 +9,12 @@ import usePriceInfo from '../../../../../hooks/usePriceInfo';
|
||||
import { $L } from '../../../../../utils/helperMethods';
|
||||
import css from './ShopByMobilePriceDisplay.module.less';
|
||||
|
||||
// 파트너명에서 "Peacock | Shop The Moment" 형식일 때 "Peacock"만 추출
|
||||
const extractPartnerName = (name) => {
|
||||
if (!name) return name;
|
||||
return name.includes(' | ') ? name.split(' | ')[0].trim() : name;
|
||||
};
|
||||
|
||||
export default function ShopByMobilePriceDisplay({
|
||||
priceData,
|
||||
priceInfo,
|
||||
@@ -26,6 +32,10 @@ export default function ShopByMobilePriceDisplay({
|
||||
orderPhnNo,
|
||||
} = priceData;
|
||||
|
||||
// 파트너명 정규화
|
||||
const cleanPatncNm = useMemo(() => extractPartnerName(patncNm), [patncNm]);
|
||||
const cleanPatnrName = useMemo(() => extractPartnerName(patnrName), [patnrName]);
|
||||
|
||||
const {
|
||||
discountRate,
|
||||
rewardFlag,
|
||||
@@ -50,6 +60,8 @@ export default function ShopByMobilePriceDisplay({
|
||||
[isOriginalPriceEmpty, isDiscountedPriceEmpty, price5, offerInfo]
|
||||
);
|
||||
|
||||
|
||||
|
||||
const renderPriceItem = useCallback(() => {
|
||||
if (priceData && !promotionCode) {
|
||||
if (rewd) {
|
||||
@@ -57,9 +69,9 @@ export default function ShopByMobilePriceDisplay({
|
||||
<div className={css.wrapper}>
|
||||
<div className={css.rewdTopLayer}>
|
||||
<span>
|
||||
{patncNm
|
||||
? patncNm + " " + $L("Price") + " "
|
||||
: patnrName + " " + $L("Price") + " "}
|
||||
{cleanPatncNm
|
||||
? cleanPatncNm + " " + $L("Price") + " "
|
||||
: cleanPatnrName + " " + $L("Price") + " "}
|
||||
</span>
|
||||
<span className={css.partnerPrc}>
|
||||
{TYPE_CASE.case5 || TYPE_CASE.case8
|
||||
@@ -95,9 +107,9 @@ export default function ShopByMobilePriceDisplay({
|
||||
return (
|
||||
<div className={css.wrapper}>
|
||||
<span className={css.name}>
|
||||
{patncNm
|
||||
? patncNm + " " + $L("Price")
|
||||
: patnrName + " " + $L("Price")}
|
||||
{cleanPatncNm
|
||||
? cleanPatncNm + " " + $L("Price")
|
||||
: cleanPatnrName + " " + $L("Price")}
|
||||
</span>
|
||||
<div className={css.btmLayer}>
|
||||
<span className={classNames(css.price, css.case01)}>
|
||||
@@ -111,9 +123,9 @@ export default function ShopByMobilePriceDisplay({
|
||||
<div className={css.wrapper}>
|
||||
<div className={css.topLayer}>
|
||||
<span className={css.name}>
|
||||
{patncNm
|
||||
? patncNm + " " + $L("Price")
|
||||
: patnrName + $L("Price")}
|
||||
{cleanPatncNm
|
||||
? cleanPatncNm + " " + $L("Price")
|
||||
: cleanPatnrName + $L("Price")}
|
||||
</span>
|
||||
</div>
|
||||
{discountRate && Number(discountRate.replace("%", "")) > 4 && (
|
||||
@@ -140,9 +152,9 @@ export default function ShopByMobilePriceDisplay({
|
||||
<div className={css.wrapper}>
|
||||
<div className={css.topLayer}>
|
||||
<span className={css.name}>
|
||||
{patncNm
|
||||
? patncNm + " " + $L("Price")
|
||||
: patnrName + " " + $L("Price")}
|
||||
{cleanPatncNm
|
||||
? cleanPatncNm + " " + $L("Price")
|
||||
: cleanPatnrName + " " + $L("Price")}
|
||||
</span>
|
||||
</div>
|
||||
{discountRate && Number(discountRate.replace("%", "")) > 4 && (
|
||||
@@ -194,7 +206,7 @@ export default function ShopByMobilePriceDisplay({
|
||||
return (
|
||||
<div className={css.wrapper}>
|
||||
<span className={css.partnerName}>
|
||||
{patncNm ? patncNm + " " + $L("Price") : patnrName + $L("Price")}
|
||||
{cleanPatncNm ? cleanPatncNm + " " + $L("Price") : cleanPatnrName + $L("Price")}
|
||||
</span>
|
||||
<span
|
||||
className={css.offerInfo}
|
||||
@@ -204,7 +216,8 @@ export default function ShopByMobilePriceDisplay({
|
||||
);
|
||||
}
|
||||
}, [
|
||||
patnrName,
|
||||
cleanPatnrName,
|
||||
cleanPatncNm,
|
||||
priceInfo,
|
||||
isOriginalPriceEmpty,
|
||||
isDiscountedPriceEmpty,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../../../../style/CommonStyle.module.less";
|
||||
@import "../../../../../style/utils.module.less";
|
||||
@import '../../../../../style/CommonStyle.module.less';
|
||||
@import '../../../../../style/utils.module.less';
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 36px;
|
||||
font-size: 30px;
|
||||
color: @COLOR_WHITE;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -78,35 +78,35 @@ export default function ThemeItemCard({
|
||||
const { originalPrice, discountedPrice, discountRate } =
|
||||
usePriceInfo(priceInfo) || {};
|
||||
|
||||
const mockEnergyLabel = [
|
||||
{
|
||||
"enrgLblExpsOrd": "0",
|
||||
"enrgLblTpCd": "EL_TYPE_05",
|
||||
"enrgLblCd": "MNLC",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"enrgLblExpsOrd": "0",
|
||||
"enrgLblTpCd": "EL_TYPE_05",
|
||||
"enrgLblCd": "MNLC",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"enrgLblExpsOrd": "0",
|
||||
"enrgLblTpCd": "EL_TYPE_05",
|
||||
"enrgLblCd": "MNLC",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
];
|
||||
// const mockEnergyLabel = [
|
||||
// {
|
||||
// "enrgLblExpsOrd": "0",
|
||||
// "enrgLblTpCd": "EL_TYPE_05",
|
||||
// "enrgLblCd": "MNLC",
|
||||
// "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",
|
||||
// "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"
|
||||
// },
|
||||
// {
|
||||
// "enrgLblExpsOrd": "0",
|
||||
// "enrgLblTpCd": "EL_TYPE_05",
|
||||
// "enrgLblCd": "MNLC",
|
||||
// "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",
|
||||
// "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"
|
||||
// },
|
||||
// {
|
||||
// "enrgLblExpsOrd": "0",
|
||||
// "enrgLblTpCd": "EL_TYPE_05",
|
||||
// "enrgLblCd": "MNLC",
|
||||
// "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",
|
||||
// "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"
|
||||
// }
|
||||
// ];
|
||||
|
||||
const setEnactFitZIndex = (zIndexValue) => {
|
||||
const target = document.getElementById("floatLayer");
|
||||
@@ -211,7 +211,7 @@ export default function ThemeItemCard({
|
||||
))}
|
||||
</div>
|
||||
)} */}
|
||||
{mockEnergyLabel && mockEnergyLabel.length > 0 && (
|
||||
{/* {mockEnergyLabel && mockEnergyLabel.length > 0 && (
|
||||
<div className={css.energyLabels}>
|
||||
{mockEnergyLabel.map((label, labelIndex) => (
|
||||
<SpottableTemp
|
||||
@@ -228,7 +228,7 @@ export default function ThemeItemCard({
|
||||
</SpottableTemp>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</SpottableDiv>
|
||||
{(() => {
|
||||
|
||||
@@ -1425,8 +1425,6 @@ const BuyOption = ({
|
||||
setTimeout(() => {
|
||||
Spotlight.focus('buy-option-buy-now-button');
|
||||
});
|
||||
|
||||
dispatch(setHidePopup());
|
||||
dispatch(launchMembershipApp());
|
||||
} else {
|
||||
dispatch(setShowPopup(Config.ACTIVE_POPUP.qrPopup));
|
||||
@@ -1525,6 +1523,7 @@ const handleCartMove = useCallback(() => {
|
||||
disabled: detail.optStkQty <= 0,
|
||||
imageUrl: detail.optImgUrl || null,
|
||||
price: detail.priceInfo.split('|')[1],
|
||||
prodOptCdCval: detail.prodOptCdCval,
|
||||
})) || []),
|
||||
]}
|
||||
selectedIndex={selectedOptionItemIndex}
|
||||
|
||||
@@ -72,6 +72,7 @@ const CustomDropDown = ({
|
||||
|
||||
const selectedOption = normalizedOptions[selectedIndex];
|
||||
const selectedLabel = selectedOption?.label || placeholder;
|
||||
const selectedId = selectedOption?.prodOptCdCval ? selectedOption?.prodOptCdCval : null;
|
||||
const selectedImage = selectedOption?.imageUrl;
|
||||
|
||||
return (
|
||||
@@ -93,7 +94,7 @@ const CustomDropDown = ({
|
||||
className={styles.custom_dropdown__image}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.custom_dropdown__text}>{selectedLabel}</div>
|
||||
<div className={styles.custom_dropdown__text}>{selectedLabel} {selectedId ? `ID : ${selectedId}` : ''}</div>
|
||||
</div>
|
||||
<div className={styles.custom_dropdown__icon}>
|
||||
<img src={iconDownArrow} alt="dropdown arrow" />
|
||||
@@ -108,7 +109,6 @@ const CustomDropDown = ({
|
||||
.map((option, reverseIndex) => {
|
||||
const originalIndex = normalizedOptions.length - 1 - reverseIndex;
|
||||
const isOptionDisabled = option.disabled;
|
||||
|
||||
return (
|
||||
<SpottableDiv
|
||||
key={originalIndex}
|
||||
@@ -133,7 +133,7 @@ const CustomDropDown = ({
|
||||
/>
|
||||
)}
|
||||
<span className={styles.custom_dropdown__optname}>
|
||||
{option.label}
|
||||
{option.label} {option.prodOptCdCval ? `ID : ${option.prodOptCdCval}` : ''}
|
||||
</span>
|
||||
{isOptionDisabled ? (
|
||||
<span className={styles.custom_dropdown__lasttxt}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ontv4u from '../../../../../assets/images/bg/ontv4u_new.png';
|
||||
import Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
|
||||
import qvc from '../../../../../assets/images/bg/qvc_new.png';
|
||||
import shoplc from '../../../../../assets/images/bg/shoplc_new.png';
|
||||
import nbcu from '../../../../../assets/images/bg/nbcu_new.png';
|
||||
import css from './DetailPanelBackground.module.less';
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,7 @@ export default function DetailPanelBackground({
|
||||
11: shoplc,
|
||||
16: koreaKiosk,
|
||||
19: Pinkfong,
|
||||
21: nbcu,
|
||||
};
|
||||
|
||||
const detailPanelBg = useMemo(() => {
|
||||
@@ -75,26 +77,32 @@ export default function DetailPanelBackground({
|
||||
useEffect(() => {
|
||||
// launchedFromPlayer가 true이면 배경 이미지를 로드하지 않음 (PlayerPanel 비디오 보이도록)
|
||||
if (launchedFromPlayer) {
|
||||
// console.log('[DetailPanelBackground] Skip background image loading - launchedFromPlayer=true (showing PlayerPanel video)');
|
||||
console.log('[Detail-BG] 🔵 DetailPanelBackground - Skip background image (launchedFromPlayer=true, showing PlayerPanel video)', {
|
||||
launchedFromPlayer,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
setImageReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// launchedFromPlayer가 false일 때만 배경 이미지 로드
|
||||
// console.log('[DetailPanelBackground] Loading background image - launchedFromPlayer=false');
|
||||
console.log('[Detail-BG] 🟢 DetailPanelBackground - Loading background image (launchedFromPlayer=false)', {
|
||||
patnrId,
|
||||
imagePath: detailPanelBg,
|
||||
});
|
||||
if (ImagePreloader.isLoaded(detailPanelBg)) {
|
||||
// console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg);
|
||||
console.log('[Detail-BG] ✅ DetailPanelBackground - Using preloaded image:', detailPanelBg);
|
||||
setImageReady(true);
|
||||
} else {
|
||||
// 프리로드되지 않았다면 즉시 로드 시도
|
||||
// console.log('[DetailPanelBackground] Image not preloaded, loading on-demand:', detailPanelBg);
|
||||
console.log('[Detail-BG] 📥 DetailPanelBackground - Image not preloaded, loading on-demand:', detailPanelBg);
|
||||
ImagePreloader.preloadImage(detailPanelBg)
|
||||
.then(() => {
|
||||
// console.log('[DetailPanelBackground] On-demand image loaded:', detailPanelBg);
|
||||
console.log('[Detail-BG] ✅ DetailPanelBackground - On-demand image loaded:', detailPanelBg);
|
||||
setImageReady(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
// console.error('[DetailPanelBackground] On-demand image load failed:', e);
|
||||
console.error('[Detail-BG] ❌ DetailPanelBackground - On-demand image load failed:', e);
|
||||
// 실패해도 이미지를 표시해야 함
|
||||
setImageReady(true);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import ontv4u from '../../../../../assets/images/bg/ontv4u_new.png';
|
||||
import Pinkfong from '../../../../../assets/images/bg/Pinkfong_new.png';
|
||||
import qvc from '../../../../../assets/images/bg/qvc_new.png';
|
||||
import shoplc from '../../../../../assets/images/bg/shoplc_new.png';
|
||||
import nbcu from '../../../../../assets/images/bg/nbcu_new.png';
|
||||
import css from './DetailPanelBackground.module.less';
|
||||
|
||||
// ==================== 로깅 함수들 ====================
|
||||
@@ -65,6 +66,7 @@ export default function DetailPanelBackgroundV2({
|
||||
11: shoplc, // SHOPLC
|
||||
16: koreaKiosk, // KOREA KIOSK
|
||||
19: Pinkfong, // PINKFONG
|
||||
21: nbcu, // NBCU
|
||||
}),
|
||||
[]
|
||||
);
|
||||
@@ -200,7 +202,7 @@ export function PreloadedBackgroundImages({
|
||||
launchedFromPlayer = false,
|
||||
}) {
|
||||
// 모든 파트너사 ID 목록
|
||||
const allPatnrIds = useMemo(() => [1, 2, 4, 9, 11, 16, 19], []);
|
||||
const allPatnrIds = useMemo(() => [1, 2, 4, 9, 11, 16, 19, 21], []);
|
||||
|
||||
// ✅ 원래 로직 복원: HomePanel이 onTop이 아니고 selectedPatnrId가 있을 때만 배경 표시
|
||||
const shouldShowBackground = !isHomePanelOnTop && selectedPatnrId;
|
||||
|
||||
@@ -93,9 +93,6 @@ export default function THeaderCustom({
|
||||
role="button"
|
||||
/>
|
||||
)}
|
||||
{type === "theme" && themeTitle && (
|
||||
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: themeTitle }} />
|
||||
)}
|
||||
{kind ? (
|
||||
""
|
||||
) : (
|
||||
@@ -107,13 +104,16 @@ export default function THeaderCustom({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === "theme" && themeTitle && (
|
||||
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: `[${themeTitle}]` }} />
|
||||
)}
|
||||
<Marquee
|
||||
marqueeOn="render"
|
||||
className={css.title}
|
||||
marqueeDisabled={marqueeDisabled}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{prdtId && <span className={css.prdtId}>ID : {prdtId}</span>}
|
||||
{(prdtId && patnrId !== "21") && <span className={css.prdtId}>ID : {prdtId}</span>}
|
||||
{convertedTitle && (
|
||||
<span dangerouslySetInnerHTML={{ __html: convertedTitle }} />
|
||||
)}
|
||||
|
||||
@@ -61,4 +61,5 @@
|
||||
color: #eaeaea;
|
||||
width: max-content;
|
||||
margin-right: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -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 { 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 Scroller from '@enact/sandstone/Scroller';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import AutoScrollAreaDetail, { POSITION } from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
|
||||
import AutoScrollAreaDetail, {
|
||||
POSITION,
|
||||
} from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
|
||||
import css from './TScrollerDetail.module.less';
|
||||
|
||||
/**
|
||||
@@ -206,7 +218,8 @@ const TScrollerDetail = forwardRef(
|
||||
onScrollStop={_onScrollStop}
|
||||
onScroll={_onScroll}
|
||||
scrollMode={scrollMode || 'translate'}
|
||||
focusableScrollbar={focusableScrollbar}
|
||||
// focusableScrollbar={focusableScrollbar}
|
||||
focusableScrollbar={false}
|
||||
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
|
||||
direction={direction}
|
||||
horizontalScrollbar={horizontalScrollbar}
|
||||
@@ -220,7 +233,7 @@ const TScrollerDetail = forwardRef(
|
||||
}}
|
||||
noScrollByWheel={noScrollByWheel}
|
||||
noScrollByDrag
|
||||
// rest props에서 ref만 제외하고 전달
|
||||
// rest props에서 ref만 제외하고 전달
|
||||
{...(rest.ref ? { ...rest, ref: undefined } : rest)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,19 +2,55 @@ import React, { memo } from "react";
|
||||
|
||||
import IcPartnersDefault from "../../../../assets/images/ic-tab-partners-default@3x.png";
|
||||
import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||
import TopBannerImage from "../TopBannerImage/TopBannerImage";
|
||||
import css from "./Banner.module.less";
|
||||
|
||||
export default memo(function Banner({
|
||||
brandInfo,
|
||||
brandTopImgInfo,
|
||||
brandTopBannerInfo,
|
||||
panelPatnrId,
|
||||
selectedPatnrId,
|
||||
}) {
|
||||
const selectedBrandInfo =
|
||||
brandInfo?.find(({ patnrId }) => panelPatnrId === patnrId) || {};
|
||||
|
||||
const { patncLogoPath, patncNm } = selectedBrandInfo;
|
||||
const { topImgAlt, topImgPath } = brandTopImgInfo;
|
||||
const { topImgAlt, topImgPath } = brandTopImgInfo || {};
|
||||
|
||||
// NBCU(patnrId: 21)인 경우 Top Banner 정보 사용
|
||||
const isNBCU = selectedPatnrId === 21 || selectedPatnrId === "21";
|
||||
// console.log("[FB-BANNER-COMP] ===== BANNER COMPONENT START =====");
|
||||
// console.log("[FB-BANNER-COMP] selectedPatnrId:", selectedPatnrId, "(type:", typeof selectedPatnrId, ")");
|
||||
// console.log("[FB-BANNER-COMP] panelPatnrId:", panelPatnrId, "(type:", typeof panelPatnrId, ")");
|
||||
// console.log("[FB-BANNER-COMP] isNBCU:", isNBCU);
|
||||
// console.log("[FB-BANNER-COMP] brandTopBannerInfo:", brandTopBannerInfo);
|
||||
|
||||
// Top Banner 정보에서 필요한 필드 추출
|
||||
const {
|
||||
banrImgUrl, // 배너 이미지 URL
|
||||
banrImgNm, // 배너 이미지 이름
|
||||
pupBanrImgUrl, // 팝업 배너 이미지 URL
|
||||
pupBanrImgNm, // 팝업 배너 이미지 이름
|
||||
pupBanrTtl, // 팝업 배너 타이틀
|
||||
banrNm // 배너 이름
|
||||
} = brandTopBannerInfo || {};
|
||||
|
||||
// 현재는 Top Banner 이미지를 사용하지 않고 기존 Top 이미지만 사용
|
||||
// TODO: 향후 Top Banner 이미지를 사용하려면 아래 주석 처리된 부분을 활성화
|
||||
const bannerImageSrc = topImgPath; // isNBCU ? banrImgUrl : topImgPath;
|
||||
const bannerImageAlt = topImgAlt; // isNBCU ? banrImgNm || banrNm : topImgAlt;
|
||||
|
||||
// console.log("[FB-BANNER-COMP] Top Banner 이미지는 현재 비활성화됨");
|
||||
// if (isNBCU && brandTopBannerInfo) {
|
||||
// console.log("[FB-BANNER-COMP] NBCU Top Banner 데이터 수신 (사용하지 않음):");
|
||||
// console.log("[FB-BANNER-COMP] - banrImgUrl:", banrImgUrl);
|
||||
// console.log("[FB-BANNER-COMP] - pupBanrImgUrl:", pupBanrImgUrl);
|
||||
// console.log("[FB-BANNER-COMP] - pupBanrTtl:", pupBanrTtl);
|
||||
// }
|
||||
// console.log("[FB-BANNER-COMP] Using original Top Image");
|
||||
// console.log("[FB-BANNER-COMP] bannerImageSrc:", bannerImageSrc);
|
||||
// console.log("[FB-BANNER-COMP] ===== BANNER COMPONENT END =====");
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<figure>
|
||||
@@ -26,7 +62,25 @@ export default memo(function Banner({
|
||||
/>
|
||||
<figcaption>{patncNm}</figcaption>
|
||||
</figure>
|
||||
<CustomImage src={topImgPath} alt={topImgAlt} ariaLabel={topImgAlt} />
|
||||
{bannerImageSrc && (
|
||||
<CustomImage
|
||||
src={bannerImageSrc}
|
||||
alt={bannerImageAlt}
|
||||
ariaLabel={bannerImageAlt}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* NBCU Top Banner Image */}
|
||||
{isNBCU && brandTopBannerInfo?.banrImgUrl && (
|
||||
<TopBannerImage
|
||||
banrImgUrl={banrImgUrl}
|
||||
banrImgNm={banrImgNm}
|
||||
banrNm={banrNm}
|
||||
pupBanrImgUrl={pupBanrImgUrl}
|
||||
pupBanrImgNm={pupBanrImgNm}
|
||||
spotlightId="nbcu-top-banner-image"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
// position: relative; // changed from fixed to relative for absolute positioning of children
|
||||
.flex(@justifyCenter: flex-start, @alignCenter: flex-end);
|
||||
.size(@w: 100%, @h: 108px);
|
||||
margin-bottom: 58px;
|
||||
|
||||
@@ -19,6 +19,7 @@ const Container = SpotlightContainerDecorator(
|
||||
|
||||
const FeaturedBestSeller = ({
|
||||
brandBestSellerInfo,
|
||||
brandBestSellerTitle,
|
||||
handleItemFocus,
|
||||
order,
|
||||
shelfOrder,
|
||||
@@ -28,6 +29,12 @@ const FeaturedBestSeller = ({
|
||||
}) => {
|
||||
const [firstChk, setFirstChk] = useState(0);
|
||||
|
||||
// brandBestSellerTitle 우선 사용, 없으면 shelfTitle, 없으면 기본값
|
||||
const displayTitle = brandBestSellerTitle || shelfTitle || $L(STRING_CONF.BEST_SELLER);
|
||||
|
||||
// console.log("[FeaturedBestSeller] brandBestSellerTitle:", brandBestSellerTitle);
|
||||
// console.log("[FeaturedBestSeller] displayTitle:", displayTitle);
|
||||
|
||||
const _handleItemFocus = useCallback(() => {
|
||||
if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder);
|
||||
|
||||
@@ -65,7 +72,7 @@ const FeaturedBestSeller = ({
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<SectionTitle
|
||||
title={$L(STRING_CONF.BEST_SELLER)}
|
||||
title={displayTitle}
|
||||
data-title="best-seller"
|
||||
label="best-seller Heading 1"
|
||||
/>
|
||||
|
||||
@@ -60,6 +60,10 @@ export default function FeaturedBestSellerList({
|
||||
|
||||
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
|
||||
|
||||
// console.log("[FeaturedBestSellerList] Mounted - selectedPatnrId:", selectedPatnrId);
|
||||
// console.log("[FeaturedBestSellerList] brandBestSellerInfo:", brandBestSellerInfo);
|
||||
// console.log("[FeaturedBestSellerList] Data count:", brandBestSellerInfo?.length || 0);
|
||||
|
||||
const cursorVisible = useSelector(
|
||||
(state) => state.common.appStatus.cursorVisible
|
||||
);
|
||||
@@ -155,6 +159,9 @@ export default function FeaturedBestSellerList({
|
||||
lgCatNm,
|
||||
euEnrgLblInfos,
|
||||
} = brandBestSellerInfo[index];
|
||||
|
||||
// console.log("[FeaturedBestSellerList] renderItem - index:", index, "patnrId:", patnrId, "rankOrd:", rankOrd, "prdtNm:", prdtNm);
|
||||
|
||||
const rankText =
|
||||
rankOrd === 1
|
||||
? rankOrd + "st,"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Job } from "@enact/core/util";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
|
||||
import { types } from "../../actions/actionTypes";
|
||||
import {
|
||||
@@ -23,8 +24,10 @@ import {
|
||||
getBrandLiveChannelInfo,
|
||||
getBrandRecommendedShowInfo,
|
||||
getBrandSeriesInfo,
|
||||
getBrandShopByShow,
|
||||
getBrandShowroom,
|
||||
getBrandTSVInfo,
|
||||
getBrandTopBanner,
|
||||
} from "../../actions/brandActions";
|
||||
import { changeAppStatus, setHidePopup } from "../../actions/commonActions";
|
||||
import {
|
||||
@@ -63,6 +66,7 @@ import LiveChannels from "./LiveChannels/LiveChannels";
|
||||
import QuickMenu from "./QuickMenu/QuickMenu";
|
||||
import RecommendedShows from "./RecommendedShows/RecommendedShows";
|
||||
import Series from "./Series/Series";
|
||||
import ShopByShow from "./ShopByShow/ShopByShow";
|
||||
import Showroom from "./Showroom/Showroom";
|
||||
import TodaysDeals from "./TodaysDeals/TodaysDeals";
|
||||
import UpComing from "./UpComing/UpComing";
|
||||
@@ -90,6 +94,7 @@ const TEMPLATE_CODE_CONF = {
|
||||
SERIES: "BRD00107",
|
||||
CATEGORY: "BRD00108",
|
||||
SHOWROOM: "BRD00109",
|
||||
NBCU: "BRD00110",
|
||||
};
|
||||
|
||||
const DISPATCH_MAP = Object.freeze({
|
||||
@@ -101,10 +106,22 @@ const DISPATCH_MAP = Object.freeze({
|
||||
[TEMPLATE_CODE_CONF.SERIES]: getBrandSeriesInfo,
|
||||
[TEMPLATE_CODE_CONF.CATEGORY]: getBrandCategoryInfo,
|
||||
[TEMPLATE_CODE_CONF.SHOWROOM]: getBrandShowroom,
|
||||
[TEMPLATE_CODE_CONF.NBCU]: getBrandShopByShow,
|
||||
});
|
||||
|
||||
const TOP_MARGIN = 36;
|
||||
|
||||
// 🆕 [251211] NBCU(patnrId=21) 전용 통합 Container
|
||||
// Banner + Category + BestSeller를 같은 Spotlight 계층 내에서 관리
|
||||
const NbcuIntegratedContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
spotlightDirection: 'vertical',
|
||||
enterTo: 'last-focused',
|
||||
preserveld: true,
|
||||
},
|
||||
'div'
|
||||
);
|
||||
|
||||
const hasTemplateCodeWithValue = (array, value) =>
|
||||
array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false;
|
||||
|
||||
@@ -233,6 +250,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
const brandBestSellerInfo = useSelector(
|
||||
(state) => state.brand.brandBestSellerData.data.brandBestSellerInfo
|
||||
);
|
||||
const brandBestSellerTitle = useSelector(
|
||||
(state) => state.brand.brandBestSellerData.data.brandBestSellerTitle
|
||||
);
|
||||
const brandRecommendedShowCategoryInfo = useSelector(
|
||||
(state) =>
|
||||
state.brand.brandRecommendedShowInfoData.data
|
||||
@@ -263,6 +283,19 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
const brandShowroomInfo = useSelector(
|
||||
(state) => state.brand.brandShowroomData.data.brandShowroomInfo
|
||||
);
|
||||
const brandShopByShowContsList = useSelector(
|
||||
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsList
|
||||
);
|
||||
const brandShopByShowContsInfo = useSelector(
|
||||
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsInfo
|
||||
);
|
||||
const brandTopBannerInfo = useSelector(
|
||||
(state) => state.brand.brandTopBannerData.data.brandTopBannerInfo
|
||||
);
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터
|
||||
const brandShopByShowCategoryGroups = useSelector(
|
||||
(state) => state.brand.brandShopByShowCategoryGroups
|
||||
);
|
||||
|
||||
const [displayTopButton, setDisplayTopButton] = useState(false);
|
||||
const [focusedContainerId, setFocusedContainerId] = useState(null);
|
||||
@@ -293,6 +326,9 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
});
|
||||
const renderedShelfCountRef = useRef(0);
|
||||
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 조회 상태 추적
|
||||
const fetchedCategoryGroupsRef = useRef(new Set());
|
||||
|
||||
const fromDetail = panelInfo?.from && panelInfo.from === "detail";
|
||||
const fromGNB = panelInfo?.from && panelInfo.from === "gnb";
|
||||
const fromUpcoming = panelInfo?.from && panelInfo.from === "upcoming";
|
||||
@@ -412,9 +448,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
);
|
||||
|
||||
const renderPageItem = useCallback(() => {
|
||||
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo length:", sortedBrandLayoutInfo.length);
|
||||
// console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo items:", sortedBrandLayoutInfo.map(el => el.shptmBrndOptTpCd));
|
||||
return (
|
||||
<>
|
||||
{sortedBrandLayoutInfo.map((el, idx) => {
|
||||
// console.log("[FeaturedBrandsPanel] Processing template code:", el.shptmBrndOptTpCd);
|
||||
switch (el.shptmBrndOptTpCd) {
|
||||
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
|
||||
return (
|
||||
@@ -485,6 +524,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
}
|
||||
|
||||
case TEMPLATE_CODE_CONF.BEST_SELLER: {
|
||||
// console.log("[FeaturedBrandsPanel] BEST_SELLER - patnrId:", selectedPatnrId);
|
||||
// console.log("[FeaturedBrandsPanel] BEST_SELLER - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.BEST_SELLER));
|
||||
// console.log("[FeaturedBrandsPanel] BEST_SELLER - shouldRender:", shouldRenderComponent(brandBestSellerInfo));
|
||||
// console.log("[FeaturedBrandsPanel] BEST_SELLER - data:", brandBestSellerInfo);
|
||||
return (
|
||||
<React.Fragment key={el.shptmBrndOptTpCd}>
|
||||
{hasTemplateCodeWithValue(
|
||||
@@ -492,15 +535,19 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
TEMPLATE_CODE_CONF.BEST_SELLER
|
||||
) &&
|
||||
shouldRenderComponent(brandBestSellerInfo) && (
|
||||
<FeaturedBestSeller
|
||||
brandBestSellerInfo={brandBestSellerInfo}
|
||||
handleItemFocus={handleItemFocus}
|
||||
order={idx + 1}
|
||||
shelfOrder={el.expsOrd}
|
||||
shelfTitle={el.shptmBrndOptTpNm}
|
||||
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
/>
|
||||
<>
|
||||
{/* {console.log("[FeaturedBrandsPanel] Rendering FeaturedBestSeller for patnrId:", selectedPatnrId)} */}
|
||||
<FeaturedBestSeller
|
||||
brandBestSellerInfo={brandBestSellerInfo}
|
||||
brandBestSellerTitle={brandBestSellerTitle}
|
||||
handleItemFocus={handleItemFocus}
|
||||
order={idx + 1}
|
||||
shelfOrder={el.expsOrd}
|
||||
shelfTitle={el.shptmBrndOptTpNm}
|
||||
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -650,12 +697,43 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
case TEMPLATE_CODE_CONF.NBCU: {
|
||||
// console.log("[FeaturedBrandsPanel] NBCU - patnrId:", selectedPatnrId);
|
||||
// console.log("[FeaturedBrandsPanel] NBCU - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.NBCU));
|
||||
// console.log("[FeaturedBrandsPanel] NBCU - shouldRender:", shouldRenderComponent(brandShopByShowContsList));
|
||||
// console.log("[FeaturedBrandsPanel] NBCU - data:", brandShopByShowContsList);
|
||||
return (
|
||||
<React.Fragment key={el.shptmBrndOptTpCd}>
|
||||
{hasTemplateCodeWithValue(
|
||||
sortedBrandLayoutInfo,
|
||||
TEMPLATE_CODE_CONF.NBCU
|
||||
) &&
|
||||
shouldRenderComponent(brandShopByShowContsList) && (
|
||||
<>
|
||||
{/* {console.log("[FeaturedBrandsPanel] Rendering ShopByShow for patnrId:", selectedPatnrId)} */}
|
||||
<ShopByShow
|
||||
brandShopByShowContsList={brandShopByShowContsList}
|
||||
brandShopByShowContsInfo={brandShopByShowContsInfo}
|
||||
handleItemFocus={handleItemFocus}
|
||||
order={idx + 1}
|
||||
shelfOrder={el.expsOrd}
|
||||
shelfTitle={el.shptmBrndOptTpNm}
|
||||
spotlightId={TEMPLATE_CODE_CONF.NBCU}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
brandBestSellerInfo,
|
||||
brandBestSellerTitle,
|
||||
brandCategoryInfo,
|
||||
brandCategoryProductInfo,
|
||||
brandChanInfo,
|
||||
@@ -668,6 +746,8 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
brandSeriesGroupInfo,
|
||||
brandSeriesInfo,
|
||||
brandShowroomInfo,
|
||||
brandShopByShowContsList,
|
||||
brandShopByShowContsInfo,
|
||||
brandTsvInfo,
|
||||
fromGNB,
|
||||
fromQuickMenu,
|
||||
@@ -711,6 +791,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
// effect: layout information fetching due to partner id change
|
||||
useEffect(() => {
|
||||
if (!fromDetail) {
|
||||
// console.log("[FeaturedBrandsPanel] Layout Info Effect - patnrId:", panelInfo?.patnrId);
|
||||
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
|
||||
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
|
||||
setIsInitialFocusOccurred(false);
|
||||
@@ -720,30 +801,98 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
|
||||
// effect: set selectedPatnrId and selectedPatncNm
|
||||
useEffect(() => {
|
||||
if (brandInfo) {
|
||||
const patnrId = panelInfo?.patnrId;
|
||||
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId).patncNm;
|
||||
if (brandInfo && panelInfo?.patnrId) {
|
||||
const patnrId = panelInfo.patnrId;
|
||||
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId)?.patncNm;
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
// console.log("[FB-PANEL-DATA-FETCH] Effect triggered");
|
||||
// console.log("[FB-PANEL-DATA-FETCH] sortedBrandLayoutInfo:", sortedBrandLayoutInfo);
|
||||
// console.log("[FB-PANEL-DATA-FETCH] selectedPatnrId:", selectedPatnrId);
|
||||
|
||||
// 🆕 [251210] patnrId 변경 시 조회 상태 초기화
|
||||
if (selectedPatnrId) {
|
||||
const patnrIdString = String(selectedPatnrId);
|
||||
// 이전 patnrId와 다르면 ref 초기화
|
||||
const currentFetchKeys = Array.from(fetchedCategoryGroupsRef.current).filter(key => key.startsWith(patnrIdString));
|
||||
if (currentFetchKeys.length === 0) {
|
||||
// console.log("[FB-PANEL-DATA-FETCH] patnrId changed, clearing category group fetch status");
|
||||
// 다른 patnrId로 전환 시 ref 초기화
|
||||
fetchedCategoryGroupsRef.current.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (sortedBrandLayoutInfo && selectedPatnrId) {
|
||||
// console.log("[FB-PANEL-DATA-FETCH] Fetching data - patnrId:", selectedPatnrId);
|
||||
Object.entries(DISPATCH_MAP) //
|
||||
.forEach(([templateCode, action]) => {
|
||||
if (hasTemplateCodeWithValue(sortedBrandLayoutInfo, templateCode)) {
|
||||
// Detail 복귀 시 ShopByShow 데이터가 이미 있으면 재호출을 건너뛰어 선택 상태가 초기화되는 것을 방지
|
||||
if (
|
||||
templateCode === TEMPLATE_CODE_CONF.NBCU &&
|
||||
fromDetail &&
|
||||
shouldRenderComponent(brandShopByShowContsList)
|
||||
) {
|
||||
// console.log("[FB-PANEL-DATA-FETCH] Skip re-fetch ShopByShow on return from detail");
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("[FB-PANEL-DATA-FETCH] Dispatching for template:", templateCode, "patnrId:", selectedPatnrId);
|
||||
dispatch(action({ patnrId: selectedPatnrId }));
|
||||
}
|
||||
});
|
||||
|
||||
// NBCU(patnrId: 21)인 경우 Top Banner API 호출
|
||||
if (selectedPatnrId === 21 || selectedPatnrId === "21") {
|
||||
// console.log("[FB-PANEL-TOP-BANNER] NBCU(patnrId=21) detected - calling Top Banner API");
|
||||
// console.log("[FB-PANEL-TOP-BANNER] selectedPatnrId:", selectedPatnrId, "(type:", typeof selectedPatnrId, ")");
|
||||
// console.log("[FB-PANEL-TOP-BANNER] Before API call - brandTopBannerInfo:", brandTopBannerInfo);
|
||||
dispatch(getBrandTopBanner({ patnrId: selectedPatnrId }));
|
||||
}
|
||||
|
||||
resetStates();
|
||||
}
|
||||
}, [sortedBrandLayoutInfo, selectedPatnrId]);
|
||||
|
||||
// 🆕 [251210] patnrId=21인 경우 모든 카테고리 그룹 데이터 미리 조회
|
||||
useEffect(() => {
|
||||
if (selectedPatnrId === 21 || selectedPatnrId === "21") {
|
||||
// console.log("[FB-PANEL-CATEGORY-GROUPS] patnrId=21 detected - fetching all category group data");
|
||||
// console.log("[FB-PANEL-CATEGORY-GROUPS] brandShopByShowContsList:", brandShopByShowContsList);
|
||||
|
||||
// 각 카테고리(contsId)별 그룹 데이터 조회
|
||||
if (brandShopByShowContsList && brandShopByShowContsList.length > 0) {
|
||||
brandShopByShowContsList.forEach((conts) => {
|
||||
const fetchKey = `${selectedPatnrId}-${conts.contsId}`;
|
||||
|
||||
// useRef로 이미 조회된 contsId 추적 (무한루프 방지)
|
||||
if (!fetchedCategoryGroupsRef.current.has(fetchKey)) {
|
||||
// console.log("[FB-PANEL-CATEGORY-GROUPS] Fetching category group for contsId:", conts.contsId);
|
||||
fetchedCategoryGroupsRef.current.add(fetchKey); // 조회 상태 기록
|
||||
dispatch(getBrandShopByShow({
|
||||
patnrId: selectedPatnrId,
|
||||
contsId: conts.contsId
|
||||
}));
|
||||
} else {
|
||||
// console.log("[FB-PANEL-CATEGORY-GROUPS] Category group already fetched for contsId:", conts.contsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedPatnrId, brandShopByShowContsList, dispatch]); // brandShopByShowCategoryGroups 제거
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCatCd) {
|
||||
dispatch(
|
||||
@@ -905,6 +1054,18 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
}
|
||||
}, [isLogGNBSent, isInitialFocusOccurred, selectedPatnrId, selectedPatncNm]);
|
||||
|
||||
// effect: partners log for NBCU (patnrId=21)
|
||||
useEffect(() => {
|
||||
if (selectedPatnrId === 21 && selectedPatncNm) {
|
||||
dispatch(
|
||||
sendLogPartners({
|
||||
patncNm: selectedPatncNm,
|
||||
patnrId: selectedPatnrId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [selectedPatnrId, selectedPatncNm]);
|
||||
|
||||
// effect: unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -959,21 +1120,49 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{brandInfo && brandTopImgInfo && (
|
||||
<Banner
|
||||
brandInfo={brandInfo}
|
||||
brandTopImgInfo={brandTopImgInfo}
|
||||
panelPatnrId={panelInfo?.patnrId}
|
||||
/>
|
||||
)}
|
||||
{/* 🆕 [251211] patnrId=21일 때: Banner + Category + BestSeller를 통합 Container로 관리 */}
|
||||
{(selectedPatnrId === 21 || selectedPatnrId === "21") ? (
|
||||
<NbcuIntegratedContainer className={css.nbcuIntegratedContainer}>
|
||||
{brandInfo && (brandTopImgInfo || brandTopBannerInfo) && (
|
||||
<Banner
|
||||
brandInfo={brandInfo}
|
||||
brandTopImgInfo={brandTopImgInfo}
|
||||
brandTopBannerInfo={brandTopBannerInfo}
|
||||
panelPatnrId={panelInfo?.patnrId}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sortedBrandLayoutInfo && (
|
||||
<div
|
||||
className={css.orderableFlexContainer}
|
||||
ref={orderableFlexContainerRef}
|
||||
>
|
||||
{renderPageItem()}
|
||||
</div>
|
||||
{sortedBrandLayoutInfo && (
|
||||
<div
|
||||
className={css.orderableFlexContainer}
|
||||
ref={orderableFlexContainerRef}
|
||||
>
|
||||
{renderPageItem()}
|
||||
</div>
|
||||
)}
|
||||
</NbcuIntegratedContainer>
|
||||
) : (
|
||||
<>
|
||||
{brandInfo && (brandTopImgInfo || (selectedPatnrId === 21 && brandTopBannerInfo)) && (
|
||||
<Banner
|
||||
brandInfo={brandInfo}
|
||||
brandTopImgInfo={brandTopImgInfo}
|
||||
brandTopBannerInfo={brandTopBannerInfo}
|
||||
panelPatnrId={panelInfo?.patnrId}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sortedBrandLayoutInfo && (
|
||||
<div
|
||||
className={css.orderableFlexContainer}
|
||||
ref={orderableFlexContainerRef}
|
||||
>
|
||||
{renderPageItem()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayTopButton && (
|
||||
|
||||
@@ -49,10 +49,13 @@ const QuickMenuItem = ({
|
||||
}, [handleStopScrolling, itemIndex]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
console.log("[QuickMenuItem] Click - patnrId:", patnrId, "currentPatnrId:", selectedPatnrId ?? panelInfo?.patnrId);
|
||||
if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) {
|
||||
console.log("[QuickMenuItem] Already selected, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[QuickMenuItem] Switching to patnrId:", patnrId);
|
||||
const from = "menu";
|
||||
const name = panel_names.FEATURED_BRANDS_PANEL;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { Job } from "@enact/core/util";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
|
||||
import { updatePanel } from "../../../../../actions/panelActions";
|
||||
import { startVideoPlayer } from "../../../../../actions/playActions";
|
||||
import { pushPanel, updatePanel } from "../../../../../actions/panelActions";
|
||||
// import { startVideoPlayer } from "../../../../../actions/playActions";
|
||||
import TItemCard, {
|
||||
removeDotAndColon,
|
||||
} from "../../../../../components/TItemCard/TItemCard";
|
||||
@@ -113,27 +113,36 @@ export default function RecommendedShowsProductList({
|
||||
);
|
||||
}
|
||||
|
||||
let y =
|
||||
index < 2
|
||||
? 0
|
||||
: index === 2
|
||||
? scaleH(208)
|
||||
: scaleH(index * 248 - 248 - 40);
|
||||
|
||||
// 🆕 DetailPanel로 이동 (ShopByShow 방식)
|
||||
dispatch(
|
||||
startVideoPlayer({
|
||||
modal: false,
|
||||
patnrId,
|
||||
prdtId,
|
||||
showId,
|
||||
shptmBanrTpNm: "VOD",
|
||||
thumbnail: videoThumbnail,
|
||||
targetId: "spotlightId-" + prdtId,
|
||||
y,
|
||||
pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: { patnrId, prdtId },
|
||||
})
|
||||
);
|
||||
|
||||
// 🔴 기존 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(() => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@ import Spotlight from '@enact/spotlight';
|
||||
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
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 SectionTitle from '../../../components/SectionTitle/SectionTitle';
|
||||
import Tag from '../../../components/TItemCard/Tag';
|
||||
@@ -15,13 +19,20 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
|
||||
import TScroller from '../../../components/TScroller/TScroller';
|
||||
import useScrollReset from '../../../hooks/useScrollReset';
|
||||
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 { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||
import css from './BestSeller.module.less';
|
||||
|
||||
const SpottableComponent = Spottable('div');
|
||||
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: 'last-focused' },
|
||||
'div'
|
||||
);
|
||||
|
||||
const BestSeller = ({
|
||||
order,
|
||||
@@ -33,15 +44,26 @@ const BestSeller = ({
|
||||
shelfTitle,
|
||||
}) => {
|
||||
const { getScrollTo, scrollLeft } = useScrollTo();
|
||||
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true);
|
||||
const { handleScrollReset, handleStopScrolling } = useScrollReset(
|
||||
scrollLeft,
|
||||
true
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
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 [firstChk, setFirstChk] = useState(0);
|
||||
@@ -51,37 +73,45 @@ const BestSeller = ({
|
||||
|
||||
useEffect(() => {
|
||||
setBestInfos(
|
||||
bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // 기본값으로 빈 배열 설정
|
||||
bestSellerNewDatas?.filter(
|
||||
(item) => item.recommendTpCd === 'BESTSELLER'
|
||||
) || [] // 기본값으로 빈 배열 설정
|
||||
);
|
||||
}, [bestSellerNewDatas]);
|
||||
|
||||
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 =
|
||||
bestSellerDatas?.map((item) => ({
|
||||
...item,
|
||||
foryou: false,
|
||||
foryou: recommendedPrdtIds.has(item.prdtId),
|
||||
})) || [];
|
||||
|
||||
setBestItemNewData(baseData);
|
||||
return;
|
||||
} else {
|
||||
setBestItemNewData(bestSellerDatas);
|
||||
}
|
||||
|
||||
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]);
|
||||
}, [bestSellerDatas, bestInfos, userNumber]);
|
||||
|
||||
const orderStyle = useMemo(() => ({ order: order }), [order]);
|
||||
|
||||
@@ -144,7 +174,10 @@ const BestSeller = ({
|
||||
if (c) {
|
||||
let cAriaLabel = c.getAttribute('aria-label');
|
||||
if (cAriaLabel) {
|
||||
const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', '');
|
||||
const newcAriaLabel = cAriaLabel.replace(
|
||||
'Best Seller, Heading 1,',
|
||||
''
|
||||
);
|
||||
c.setAttribute('aria-label', newcAriaLabel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ export default function HomeBanner({
|
||||
const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
|
||||
|
||||
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
const isDeepLinkEntry = useSelector(
|
||||
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
|
||||
);
|
||||
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
|
||||
const focusHistory = useFocusHistory({
|
||||
enableLogging: true,
|
||||
@@ -163,7 +167,11 @@ export default function HomeBanner({
|
||||
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);
|
||||
|
||||
dispatch(
|
||||
@@ -175,12 +183,13 @@ export default function HomeBanner({
|
||||
shptmBanrTpNm: videoData.shptmBanrTpNm,
|
||||
lgCatCd: videoData.lgCatCd,
|
||||
chanId: videoData.brdcChnlId,
|
||||
// 기본: 배너는 modal=true로 재생
|
||||
modal: true,
|
||||
modalContainerId: defaultFocus,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [bannerDataList, defaultFocus, dispatch]);
|
||||
}, [bannerDataList, defaultFocus, dispatch, panels, isDeepLinkEntry]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(index, isHorizontal) => {
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// 디버그 모드 설정 - true일 때만 console.log 출력
|
||||
const DEBUG_MODE = false;
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { getContainerId } from '@enact/spotlight/src/container';
|
||||
|
||||
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png';
|
||||
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png';
|
||||
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png';
|
||||
import defaultLogoImg
|
||||
from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||
import emptyHorImage
|
||||
from '../../../../assets/images/img-home-banner-empty-hor.png';
|
||||
import emptyVerImage
|
||||
from '../../../../assets/images/img-home-banner-empty-ver.png';
|
||||
import defaultImageItem
|
||||
from '../../../../assets/images/img-thumb-empty-product@3x.png';
|
||||
import liveShow from '../../../../assets/images/tag-liveshow.png';
|
||||
import { changeAppStatus } from '../../../actions/commonActions';
|
||||
import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions';
|
||||
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions';
|
||||
import { pushPanel, navigateFromRandomUnit, SOURCE_MENUS } from '../../../actions/panelActions';
|
||||
import {
|
||||
setVideoTransitionLock,
|
||||
updateHomeInfo,
|
||||
} from '../../../actions/homeActions';
|
||||
import {
|
||||
sendLogTopContents,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../../actions/logActions';
|
||||
import {
|
||||
navigateFromRandomUnit,
|
||||
pushPanel,
|
||||
SOURCE_MENUS,
|
||||
} from '../../../actions/panelActions';
|
||||
import {
|
||||
finishVideoPreview,
|
||||
hideModalVideo,
|
||||
startVideoPlayer,
|
||||
startVideoPlayerNew,
|
||||
hideModalVideo,
|
||||
} from '../../../actions/playActions';
|
||||
import CustomImage from '../../../components/CustomImage/CustomImage';
|
||||
import usePriceInfo from '../../../hooks/usePriceInfo';
|
||||
@@ -36,11 +57,19 @@ import {
|
||||
LOG_TP_NO,
|
||||
panel_names,
|
||||
} from '../../../utils/Config';
|
||||
import { selectIsPlaying } from '../../../utils/playerState/playerStateSelectors';
|
||||
import { $L, formatGMTString } from '../../../utils/helperMethods';
|
||||
import {
|
||||
$L,
|
||||
formatGMTString,
|
||||
} from '../../../utils/helperMethods';
|
||||
import {
|
||||
selectIsPlaying,
|
||||
} from '../../../utils/playerState/playerStateSelectors';
|
||||
import { TEMPLATE_CODE_CONF } from '../HomePanel';
|
||||
import css from './RandomUnit.module.less';
|
||||
|
||||
// 디버그 모드 설정 - true일 때만 console.log 출력
|
||||
const DEBUG_MODE = false;
|
||||
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
@@ -66,6 +95,7 @@ export default function RandomUnit({
|
||||
|
||||
const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
|
||||
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
|
||||
const foryouInfos = useSelector((state) => state.foryou.recommendInfo.recommendProduct);
|
||||
|
||||
// 현재 재생 중인 비디오 배너 ID 가져오기
|
||||
const currentVideoBannerId = useSelector((state) => {
|
||||
@@ -490,6 +520,14 @@ export default function RandomUnit({
|
||||
};
|
||||
break;
|
||||
|
||||
case 'DSP00510':
|
||||
linkInfo = {
|
||||
name: panel_names.JUST_FOR_YOU_TEST_PANEL,
|
||||
panelInfo: {
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
linkInfo = {
|
||||
name: panel_names.HOME_PANEL,
|
||||
@@ -644,6 +682,29 @@ export default function RandomUnit({
|
||||
const { originalPrice, discountedPrice, discountRate, offerInfo } =
|
||||
usePriceInfo(priceInfos) || {};
|
||||
|
||||
// Just For You 데이터에서 첫 번째 상품 추출
|
||||
const justForYouProduct = useMemo(() => {
|
||||
if (foryouInfos && foryouInfos.length > 0) {
|
||||
const justForYouShelf = foryouInfos.find(
|
||||
(shelf) => shelf.recommendTpCd === 'JUSTFORYOU'
|
||||
);
|
||||
if (justForYouShelf && justForYouShelf.productInfos && justForYouShelf.productInfos.length > 0) {
|
||||
return justForYouShelf.productInfos[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [foryouInfos]);
|
||||
|
||||
// Just For You 상품의 가격 정보
|
||||
const justForYouPriceInfo = usePriceInfo(
|
||||
justForYouProduct && justForYouProduct.priceInfo ? justForYouProduct.priceInfo : ''
|
||||
) || {
|
||||
originalPrice: '',
|
||||
discountedPrice: '',
|
||||
discountRate: null,
|
||||
offerInfo: '',
|
||||
};
|
||||
|
||||
// 로그
|
||||
useEffect(() => {
|
||||
let _nowMenu = nowMenu;
|
||||
@@ -999,7 +1060,72 @@ export default function RandomUnit({
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : null}
|
||||
)
|
||||
: randomData?.shptmBanrTpNm == "Just For You" ? (
|
||||
{/* <SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
css.justforyou,
|
||||
countryCode === 'RU' ? css.ru : '',
|
||||
countryCode === 'DE' ? css.de : '',
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onClick={todayDealClick}
|
||||
spotlightId={spotlightId}
|
||||
aria-label={justForYouProduct?.prdtNm ? justForYouProduct?.prdtNm : randomData.tmnlImgNm}
|
||||
>
|
||||
<div className={css.productInfo}>
|
||||
<div className={css.justforyouTitle}>{$L("Just For You")}</div>
|
||||
<div
|
||||
className={css.textBox}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${justForYouProduct?.prdtNm || randomData.prdtNm}`,
|
||||
}}
|
||||
/>
|
||||
<div className={css.accBox}>
|
||||
|
||||
{justForYouProduct ? (
|
||||
<>
|
||||
{parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') === 0
|
||||
? justForYouProduct.offerInfo
|
||||
: justForYouPriceInfo.discountRate
|
||||
? justForYouPriceInfo.discountedPrice
|
||||
: justForYouPriceInfo.originalPrice}
|
||||
{justForYouPriceInfo.discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{parseFloat(originalPrice?.replace('$', '') || '0') === 0
|
||||
? randomData?.offerInfo
|
||||
: discountRate
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
{discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{isHorizontal && justForYouProduct && parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') !== 0 && (
|
||||
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.itemImgBox}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={justForYouProduct?.imgUrl || randomData.tmnlImgPath}
|
||||
animationSpeed="fast"
|
||||
fallbackSrc={defaultImageItem}
|
||||
ariaLabel={justForYouProduct?.prdtNm || randomData.tmnlImgNm}
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent> */}
|
||||
)
|
||||
: null}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -233,6 +233,165 @@
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
&.justforyou {
|
||||
background-image: url(../../../../assets/images/img-home-banner-jfy-ver@3x.png);
|
||||
background-size: 486px 858px;
|
||||
background-position: left top;
|
||||
border-radius: 10px;
|
||||
padding: 75px 51px 0;
|
||||
&.ru {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 58px;
|
||||
line-height: 60px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.de {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 74px !important;
|
||||
line-height: 63px !important;
|
||||
letter-spacing: -1px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.productInfo {
|
||||
margin-bottom: 31px;
|
||||
.justforyouTitle {
|
||||
.size(@w:100%,@h:132px);
|
||||
font-family: Arial;
|
||||
font-weight: bold;
|
||||
font-size: 80px;
|
||||
word-break: break-word;
|
||||
font-stretch: normal;
|
||||
color: #151515;
|
||||
text-align: center;
|
||||
line-height: 76px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
.textBox {
|
||||
.size(@w: 100%, @h: 80px);
|
||||
margin-top: 71px;
|
||||
.elip(@clamp:2);
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
color: @COLOR_GRAY06;
|
||||
line-height: 1.27;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.accBox {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 42px;
|
||||
color: @PRIMARY_COLOR_RED;
|
||||
line-height: 1.14;
|
||||
display: inline-block;
|
||||
.elip(@clamp:1);
|
||||
> strong {
|
||||
width: 260px;
|
||||
font-size: 30px;
|
||||
line-height: 1.27;
|
||||
display: block;
|
||||
.elip(@clamp:2);
|
||||
}
|
||||
.saleAccBox {
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
color: @COLOR_GRAY04;
|
||||
vertical-align: middle;
|
||||
text-decoration: line-through;
|
||||
margin-left: 9px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.itemImgBox {
|
||||
> img {
|
||||
.size(@w: 356px, @h: 356px);
|
||||
border-radius: 12px;
|
||||
border:6px solid #DCB9A1;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
&.isHorizontal {
|
||||
background-image: url(../../../../assets/images/img-home-banner-jfy-hor@3x.png);
|
||||
background-size: 744px 420px;
|
||||
background-position: center center;
|
||||
display: flex;
|
||||
padding: 0 30px 0 0;
|
||||
border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
-o-border-radius: 10px;
|
||||
> div {
|
||||
flex: none;
|
||||
}
|
||||
&.ru {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 58px;
|
||||
line-height: 60px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.de {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 59px !important;
|
||||
line-height: 63px !important;
|
||||
letter-spacing: -2px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.productInfo {
|
||||
margin-bottom: 0;
|
||||
.justforyouTitle {
|
||||
.size(@w:305px,@h:114px);
|
||||
margin-top: 53px;
|
||||
margin-left: 49px;
|
||||
font-size: 66px;
|
||||
word-break: break-word;
|
||||
color: #151515;
|
||||
text-align: left;
|
||||
line-height: 57px;
|
||||
font-family: @arialBlack;
|
||||
}
|
||||
.textBox {
|
||||
.size(@w: 294px, @h: 80px);
|
||||
margin: 67px 0 5px 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.accBox {
|
||||
.size(@w: 320px, @h: 50px);
|
||||
margin-left: 50px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
.elip(@clamp:1);
|
||||
}
|
||||
.saleAccBox {
|
||||
color: #767676;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: 5px 0 0 55px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
.itemImgBox {
|
||||
.position(@position: absolute, @top: 47px, @left: 389px);
|
||||
.size(@w: 326px, @h: 326px);
|
||||
> img {
|
||||
.size(@w: inherit, @h: inherit);
|
||||
border-radius: 12px;
|
||||
border:6px solid #DCB9A1;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user