Compare commits
82 Commits
develop_do
...
develop_si
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,7 +6,7 @@ import { updateHomeInfo } from './homeActions';
|
||||
import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const DEBUG_MODE = true;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// 시작 메뉴 추적을 위한 상수
|
||||
@@ -39,10 +39,19 @@ export const pushPanel = (panel, duplicatable = false) => ({
|
||||
duplicatable: duplicatable,
|
||||
});
|
||||
|
||||
export const popPanel = (panelName) => ({
|
||||
type: types.POP_PANEL,
|
||||
payload: panelName,
|
||||
});
|
||||
export const popPanel = (panelName) => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[PANEL-TRACE] popPanel action creator', {
|
||||
panelName,
|
||||
caller: new Error().stack?.split('\n')[2]?.trim(),
|
||||
});
|
||||
console.trace('[PANEL-TRACE] popPanel stack trace');
|
||||
}
|
||||
return {
|
||||
type: types.POP_PANEL,
|
||||
payload: panelName,
|
||||
};
|
||||
};
|
||||
|
||||
export const updatePanel = (panelInfo) => ({
|
||||
type: types.UPDATE_PANEL,
|
||||
@@ -93,6 +102,11 @@ export const navigateToDetail = ({
|
||||
...additionalInfo,
|
||||
};
|
||||
|
||||
const state = getState();
|
||||
const panels = state.panels.panels;
|
||||
|
||||
|
||||
|
||||
// 선택적 파라미터들 추가
|
||||
if (curationId) panelInfo.curationId = curationId;
|
||||
if (nowShelf) panelInfo.nowShelf = nowShelf;
|
||||
@@ -176,8 +190,21 @@ export const navigateToDetail = ({
|
||||
|
||||
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false;
|
||||
|
||||
console.log('[Detail-BG] 🎯 navigateToDetail - Checking HomeBanner video status:', {
|
||||
playerPanelModalValue: playerPanelInfo.panelInfo?.modal,
|
||||
isCurrentBannerVideoPlaying,
|
||||
sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// HomeBanner의 modal=true 비디오가 재생 중이면 정지
|
||||
if (isCurrentBannerVideoPlaying) {
|
||||
console.log('[Detail-BG] 🎬 navigateToDetail - HomeBanner video is playing (modal !== false)', {
|
||||
playerPanelModal: playerPanelInfo.panelInfo?.modal,
|
||||
sourceMenu,
|
||||
action: 'finishVideoPreview',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// 🔽 비디오 상태 저장 후 정지
|
||||
const { finishVideoPreview } = require('./playActions');
|
||||
|
||||
@@ -204,11 +231,15 @@ export const navigateToDetail = ({
|
||||
})
|
||||
);
|
||||
|
||||
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제)
|
||||
|
||||
// 비디오 상태 저장 후 정지
|
||||
dispatch(finishVideoPreview());
|
||||
} else {
|
||||
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장
|
||||
console.log('[Detail-BG] ⏭️ navigateToDetail - HomeBanner video is NOT playing (modal === false or undefined)', {
|
||||
playerPanelModal: playerPanelInfo.panelInfo?.modal,
|
||||
sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.HOME_PANEL,
|
||||
@@ -267,10 +298,34 @@ export const navigateToDetail = ({
|
||||
case SOURCE_MENUS.PLAYER_MEDIA: {
|
||||
// PlayerPanel에서 온 경우
|
||||
const { hidePlayerOverlays } = require('./videoPlayActions');
|
||||
const statePanels = panels || getState().panels.panels;
|
||||
const playerPanelEntry =
|
||||
[...statePanels].reverse().find(
|
||||
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
|
||||
) || null;
|
||||
|
||||
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
|
||||
dispatch(hidePlayerOverlays());
|
||||
|
||||
// PlayerPanel이 modal=true라면 풀스크린 백그라운드로 전환 + lockModalFalse 설정 (Detail 동안 modal 복귀 방지)
|
||||
if (playerPanelEntry) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: playerPanelEntry.name,
|
||||
panelInfo: {
|
||||
...playerPanelEntry.panelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
lockModalFalse: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 현재 포커스된 요소 저장
|
||||
if (Object.keys(focusSnapshot).length > 0) {
|
||||
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
|
||||
@@ -458,11 +513,28 @@ export const restoreVideoOnBack = () => {
|
||||
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
|
||||
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore;
|
||||
|
||||
console.log('[Detail-BG] 🔍 restoreVideoOnBack - Checking video restore state:', {
|
||||
hasVideoStateToRestore: !!videoStateToRestore,
|
||||
restoreOnBack: videoStateToRestore?.restoreOnBack,
|
||||
sourceMenu: videoStateToRestore?.sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) {
|
||||
console.log('[Detail-BG] ⏭️ restoreVideoOnBack - No video state to restore (skipping)', {
|
||||
reason: !videoStateToRestore ? 'no videoStateToRestore' : 'restoreOnBack is false',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 비디오 복원 시작 (로그는 개발 시 필요 시 주석 해제)
|
||||
console.log('[Detail-BG] ▶️ restoreVideoOnBack - Starting video restore', {
|
||||
sourceMenu: videoStateToRestore.sourceMenu,
|
||||
patnrId: videoStateToRestore.patnrId,
|
||||
showId: videoStateToRestore.showId,
|
||||
modal: true,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 비디오 상태 복원
|
||||
const { startVideoPlayerNew } = require('./playActions');
|
||||
@@ -489,6 +561,11 @@ export const restoreVideoOnBack = () => {
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ restoreVideoOnBack - Video restore dispatched', {
|
||||
restoredWithModal: restoreInfo.modal,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 복원 상태 정리
|
||||
dispatch(
|
||||
updatePanel({
|
||||
|
||||
@@ -73,6 +73,16 @@ export const startVideoPlayer =
|
||||
...rest
|
||||
}) =>
|
||||
(dispatch, getState) => {
|
||||
const caller = new Error().stack?.split('\n')[2]?.trim();
|
||||
console.log('[PTRACE-SP] startVideoPlayer call', {
|
||||
modal,
|
||||
modalContainerId,
|
||||
modalClassName,
|
||||
videoId,
|
||||
showUrl,
|
||||
caller,
|
||||
});
|
||||
|
||||
dlog(
|
||||
'[startVideoPlayer] ✅ START - videoId:',
|
||||
videoId,
|
||||
@@ -105,6 +115,9 @@ export const startVideoPlayer =
|
||||
|
||||
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
|
||||
if (existingPlayerPanel) {
|
||||
console.log('[PTRACE-SP] startVideoPlayer: popping existing player before push', {
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
@@ -182,6 +195,17 @@ export const startVideoPlayerNew =
|
||||
...rest
|
||||
}) =>
|
||||
(dispatch, getState) => {
|
||||
const caller = new Error().stack?.split('\n')[2]?.trim();
|
||||
console.log('[PTRACE-SPN] startVideoPlayerNew call', {
|
||||
bannerId,
|
||||
modal,
|
||||
modalContainerId,
|
||||
modalClassName,
|
||||
videoId,
|
||||
showUrl,
|
||||
caller,
|
||||
});
|
||||
|
||||
dlog(
|
||||
'[startVideoPlayerNew] *** ✅ START - bannerId:',
|
||||
bannerId,
|
||||
@@ -215,6 +239,9 @@ export const startVideoPlayerNew =
|
||||
|
||||
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
|
||||
if (existingPlayerPanel) {
|
||||
console.log('[PTRACE-SPN] popping existing player before push', {
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
@@ -325,6 +352,12 @@ export const finishVideoPreview = () => (dispatch, getState) => {
|
||||
const panels = getState().panels.panels;
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) {
|
||||
console.log('[PANEL-TRACE] finishVideoPreview: popping modal player', {
|
||||
topPanelName: topPanel.name,
|
||||
modal: topPanel.panelInfo.modal,
|
||||
stack: panels.map((p) => p.name),
|
||||
panelInfo: topPanel.panelInfo,
|
||||
});
|
||||
if (startVideoFocusTimer) {
|
||||
clearTimeout(startVideoFocusTimer);
|
||||
startVideoFocusTimer = null;
|
||||
@@ -384,6 +417,13 @@ export const pauseModalVideo = () => (dispatch, getState) => {
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', {
|
||||
found: !!modalPlayerPanel,
|
||||
playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (modalPlayerPanel) {
|
||||
if (DEBUG_MODE === true) {
|
||||
dlog('[pauseModalVideo] Pausing modal video');
|
||||
@@ -397,6 +437,14 @@ export const pauseModalVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.log('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -409,6 +457,13 @@ export const resumeModalVideo = () => (dispatch, getState) => {
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', {
|
||||
found: !!modalPlayerPanel,
|
||||
playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (modalPlayerPanel && modalPlayerPanel.panelInfo?.isPaused) {
|
||||
if (DEBUG_MODE === true) {
|
||||
dlog('[resumeModalVideo] Resuming modal video');
|
||||
@@ -422,6 +477,16 @@ export const resumeModalVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.log('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', {
|
||||
found: !!modalPlayerPanel,
|
||||
isPaused: modalPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -434,6 +499,13 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', {
|
||||
found: !!fullscreenPlayerPanel,
|
||||
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (fullscreenPlayerPanel) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
@@ -444,6 +516,14 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.log('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,21 +531,19 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
|
||||
export const resumeFullscreenVideo = () => (dispatch, getState) => {
|
||||
const panels = getState().panels.panels;
|
||||
|
||||
// console.log('[BgVideo] resumeFullscreenVideo called - panels:', {
|
||||
// panelsCount: panels?.length,
|
||||
// panels: panels?.map(p => ({ name: p.name, modal: p.panelInfo?.modal, isPaused: p.panelInfo?.isPaused }))
|
||||
// });
|
||||
|
||||
// 전체화면 PlayerPanel 찾기 (modal이 false인 패널)
|
||||
const fullscreenPlayerPanel = panels.find(
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
|
||||
);
|
||||
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - fullscreenPlayerPanel found:', !!fullscreenPlayerPanel);
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - isPaused:', fullscreenPlayerPanel?.panelInfo?.isPaused);
|
||||
console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
|
||||
found: !!fullscreenPlayerPanel,
|
||||
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
|
||||
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) {
|
||||
// console.log('[BgVideo] resumeFullscreenVideo - dispatching updatePanel with isPaused: false');
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
@@ -475,7 +553,16 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.log('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', {
|
||||
found: !!fullscreenPlayerPanel,
|
||||
isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (DEBUG_MODE === true) {
|
||||
dlog('[BgVideo] resumeFullscreenVideo - Not resuming (not found or not paused)');
|
||||
}
|
||||
@@ -951,6 +1038,12 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
|
||||
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
|
||||
*/
|
||||
export const goToFullScreen = () => (dispatch, getState) => {
|
||||
console.log('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', {
|
||||
targetModal: false,
|
||||
action: 'updatePanel',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 공유 PlayerPanel의 'modal' 상태를 false로 변경하여 전체화면으로 전환
|
||||
dispatch(
|
||||
updatePanel({
|
||||
@@ -961,6 +1054,10 @@ export const goToFullScreen = () => (dispatch, getState) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1171,6 +1268,14 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
|
||||
...rest
|
||||
} = videoInfo;
|
||||
|
||||
console.log('[Detail-BG] 🎥 startBannerVideo - Starting banner video', {
|
||||
modalStatus: modal,
|
||||
bannerId,
|
||||
displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)',
|
||||
videoId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 비디오 식별자 생성
|
||||
const videoIdentifier = videoId || showUrl || bannerId;
|
||||
if (videoIdentifier) {
|
||||
@@ -1190,11 +1295,21 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
|
||||
// 기존 PlayerPanel이 있으면 초기화
|
||||
if (existingPlayerPanel) {
|
||||
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
|
||||
console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
|
||||
existingModalStatus: existingPlayerPanel.panelInfo?.modal,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
clearAllVideoTimers();
|
||||
dispatch(popPanel(panel_names.PLAYER_PANEL));
|
||||
}
|
||||
|
||||
// 새로운 PlayerPanel push
|
||||
console.log('[Detail-BG] ➕ startBannerVideo - Pushing new PlayerPanel with modal status', {
|
||||
modal,
|
||||
modalContainerId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
dispatch(
|
||||
pushPanel(
|
||||
{
|
||||
@@ -1216,6 +1331,10 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
|
||||
)
|
||||
);
|
||||
|
||||
console.log('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
dlog('[startBannerVideo] ✨ Panel action dispatched');
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,7 @@ export const URLS = {
|
||||
// cart controller
|
||||
ADD_TO_CART: "/lgsp/v1/myinfo/cart/add.lge",
|
||||
REMOVE_FROM_CART: "/lgsp/v1/myinfo/cart/remove.lge",
|
||||
UPDATE_CART_ITEM: "/lgsp/v1/myinfo/cart/update.lge",
|
||||
UPDATE_CART_ITEM: "/lgsp/v1/myinfo/cart/update.lge",
|
||||
// cart api
|
||||
GET_MY_INFO_CART_SEARCH: "/lgsp/v1/myinfo/cart/search.lge",
|
||||
INSERT_MY_INFO_CART: "/lgsp/v1/myinfo/cart/add.lge",
|
||||
@@ -55,6 +55,8 @@ export const URLS = {
|
||||
GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge",
|
||||
GET_BRAND_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",
|
||||
@@ -146,9 +148,8 @@ export const URLS = {
|
||||
UPDATE_ORDER_PARTIAL_CANCEL: "/lgsp/v1/myinfo/order/orderPartialCancel.lge",
|
||||
PAYMENT_TOTAL_CANCEL: "/lgsp/v1/myinfo/order/paymentTotalCancel.lge",
|
||||
|
||||
// foryou controller
|
||||
// foryou controller
|
||||
JUSTFORYOU: "/lgsp/v1/recommend/justforyou.lge",
|
||||
|
||||
|
||||
// emp controller
|
||||
GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.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}`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -16,7 +18,7 @@ const OptionalConfirm = ({
|
||||
onOptionalTermsClick, // 약관 자세히 보기 버튼 클릭 핸들러
|
||||
onOptionalAgreeClick, // 동의 버튼 클릭 핸들러
|
||||
onOptionalDeclineClick, // 거절 또는 다음에 하기 버튼 클릭 핸들러
|
||||
customPosition,
|
||||
customPosition,
|
||||
position,
|
||||
}) => {
|
||||
return (
|
||||
@@ -31,7 +33,7 @@ const OptionalConfirm = ({
|
||||
onOptionalAgreeClick={onOptionalAgreeClick}
|
||||
onOptionalDeclineClick={onOptionalDeclineClick}
|
||||
customPosition={customPosition}
|
||||
position={position}
|
||||
position={position}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
@@ -17,9 +29,8 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
|
||||
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
|
||||
|
||||
|
||||
const optionalTermsData = useSelector((state) =>
|
||||
const optionalTermsData = useSelector((state) =>
|
||||
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
|
||||
);
|
||||
|
||||
@@ -46,7 +57,7 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
if (isChecked) {
|
||||
// 약관 동의할 항목들 (string array)
|
||||
const termsList = ["TID0000222", "TID0000223", "TID0000232"];
|
||||
|
||||
|
||||
// 동의하지 않을 항목들 (빈 배열)
|
||||
const notTermsList = [];
|
||||
|
||||
@@ -67,15 +78,15 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
setIsWarningPopupVisible(true);
|
||||
}
|
||||
}, [isChecked, dispatch]);
|
||||
|
||||
|
||||
const handleCloseWarningPopup = useCallback(() => {
|
||||
setIsWarningPopupVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleDontAskAgain = () => {
|
||||
const handleDontAskAgain = useCallback(() => {
|
||||
console.log("Don't Ask Again 처리 필요");
|
||||
dispatch(setHidePopup());
|
||||
};
|
||||
},[dispatch]);
|
||||
|
||||
if (isTermsPopupVisible) {
|
||||
return (
|
||||
@@ -144,7 +155,7 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
<div className={css.mainContent}>
|
||||
<div className={css.checkboxSection}>
|
||||
<div className={css.checkboxArea}>
|
||||
<TCheckBoxSquare
|
||||
<TCheckBoxSquare
|
||||
selected={isChecked}
|
||||
onToggle={handleCheckboxToggle}
|
||||
spotlightId="optional-checkbox"
|
||||
@@ -157,34 +168,34 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
type="terms"
|
||||
ariaLabel={$L("View Optional Terms")}
|
||||
>
|
||||
<div className={css.termTitle}>Optional Terms</div>
|
||||
<div className={css.termTitle}>Optional Terms</div>
|
||||
</TButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={css.descriptionSection}>
|
||||
<div className={css.description}>
|
||||
Get recommendations, special offers, and ads tailored just for you.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={css.buttonSection}>
|
||||
<div className={css.buttonGroup}>
|
||||
<TButton
|
||||
<TButton
|
||||
onClick={handleAgree}
|
||||
spotlightId="agree-button"
|
||||
className={css.agreeButton}
|
||||
>
|
||||
{$L('Agree')}
|
||||
</TButton>
|
||||
<TButton
|
||||
<TButton
|
||||
onClick={handleMainPopupClose}
|
||||
spotlightId="not-now-button"
|
||||
className={css.notNowButton}
|
||||
>
|
||||
{$L('Not Now')}
|
||||
</TButton>
|
||||
<TButton
|
||||
<TButton
|
||||
onClick={handleDontAskAgain}
|
||||
spotlightId="dont-ask-button"
|
||||
className={css.dontAskButton}
|
||||
|
||||
@@ -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();
|
||||
@@ -19,9 +30,9 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false);
|
||||
const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false);
|
||||
|
||||
|
||||
const optionalTermsData = useSelector((state) =>
|
||||
|
||||
const optionalTermsData = useSelector((state) =>
|
||||
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -66,20 +77,20 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
const handleAgreeTest = useCallback(() => {
|
||||
console.log("handleAgreeTest");
|
||||
Spotlight.pause();
|
||||
setIsTermsPopupVisible(false);
|
||||
setIsTermsPopupVisible(false);
|
||||
// 상태 업데이트 후 DOM이 완전히 렌더링될 때까지 기다린 후 포커스
|
||||
setTimeout(() => {
|
||||
Spotlight.resume();
|
||||
Spotlight.focus("optional-terms-confirm-popup");
|
||||
}, 500); // 50ms에서 100ms로 증가
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
const handleAgree = useCallback(() => {
|
||||
if (isChecked) {
|
||||
// 약관 동의할 항목들 (string array)
|
||||
const termsList = ["TID0000222", "TID0000223", "TID0000232"];
|
||||
|
||||
|
||||
// 동의하지 않을 항목들 (빈 배열)
|
||||
const notTermsList = [];
|
||||
|
||||
@@ -163,7 +174,7 @@ const OptionalTermsConfirm = ({ open }) => {
|
||||
return (
|
||||
<TNewPopUp
|
||||
kind="optionalConfirm"
|
||||
open={open}
|
||||
open={open}
|
||||
spotlightId="optional-terms-confirm-popup"
|
||||
spotlightRestrict="self-only"
|
||||
className={css.optionalConfirmPopup}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { calculateIsPanelOnTop } from '../utils/panelUtils'; // 🎯 isOnTop 유
|
||||
|
||||
// DEBUG_MODE - true인 경우에만 로그 출력
|
||||
// ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단
|
||||
const DEBUG_MODE = false;
|
||||
const DEBUG_MODE = true;
|
||||
|
||||
/**
|
||||
* Panel history middleware
|
||||
@@ -125,6 +125,14 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
|
||||
// POP 후 top panel을 기록 (이전 패널로 돌아감)
|
||||
if (panels.length > 0) {
|
||||
const topPanel = panels[panels.length - 1];
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[PANEL-TRACE] POP_PANEL middleware stack', {
|
||||
stack: panels.map((p) => p.name),
|
||||
topPanel: topPanel?.name,
|
||||
payload: action.payload,
|
||||
caller: new Error().stack?.split('\n')[2]?.trim(),
|
||||
});
|
||||
}
|
||||
if (topPanel && topPanel.name) {
|
||||
const isGNB = isGNBCall();
|
||||
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const initialState = {
|
||||
* Cart Reducer
|
||||
* 장바구니 관련 상태를 관리합니다.
|
||||
*/
|
||||
export const cartReducer = (state = initialState, action) => {
|
||||
export const cartReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
// 장바구니 조회 - API에서 가져온 전체 목록
|
||||
case types.GET_MY_INFO_CART_SEARCH:
|
||||
@@ -37,9 +37,9 @@ export const cartReducer = (state = initialState, action) => {
|
||||
};
|
||||
|
||||
// 장바구니에 상품 추가
|
||||
case types.INSERT_MY_INFO_CART:
|
||||
case types.INSERT_MY_INFO_CART:
|
||||
return {
|
||||
...state,
|
||||
...state,
|
||||
selectCart: {
|
||||
...state.selectCart,
|
||||
cartList: [...state.selectCart.cartList, action.payload],
|
||||
@@ -51,22 +51,22 @@ export const cartReducer = (state = initialState, action) => {
|
||||
case types.TOGGLE_CHECK_CART: {
|
||||
const checkedItem = action.payload.item;
|
||||
const isChecked = action.payload.isChecked;
|
||||
|
||||
|
||||
let updatedCheckedList = state.selectCart?.checkedItems || [];
|
||||
|
||||
if (isChecked) {
|
||||
|
||||
if (isChecked) {
|
||||
const itemExists = updatedCheckedList.some(
|
||||
item => item.prodSno === checkedItem.prodSno
|
||||
);
|
||||
if (!itemExists) {
|
||||
updatedCheckedList = [...updatedCheckedList, checkedItem];
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
updatedCheckedList = updatedCheckedList.filter(
|
||||
item => item.prodSno !== checkedItem.prodSno
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectCart: {
|
||||
@@ -78,7 +78,7 @@ export const cartReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
// 장바구니에서 상품 삭제
|
||||
case types.DELETE_MY_INFO_CART:
|
||||
case types.DELETE_MY_INFO_CART:
|
||||
return {
|
||||
...state,
|
||||
getMyinfoCartSearch: {
|
||||
@@ -96,7 +96,7 @@ export const cartReducer = (state = initialState, action) => {
|
||||
getMyinfoCartSearch: {
|
||||
...state.getMyinfoCartSearch,
|
||||
cartList: (state.getMyinfoCartSearch.cartList || []).map(item =>
|
||||
item.prodSno === action.payload.prodSno
|
||||
item.prodSno === action.payload.prodSno
|
||||
? { ...item, ...action.payload }
|
||||
: item
|
||||
),
|
||||
|
||||
@@ -132,6 +132,60 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
case types.UPDATE_PANEL: {
|
||||
let lastIndex = -1;
|
||||
let lastAction = 'update';
|
||||
const hasDetailPanel = state.panels.some((p) => p.name === panel_names.DETAIL_PANEL);
|
||||
const isPlayerPanel =
|
||||
action.payload.name === panel_names.PLAYER_PANEL ||
|
||||
action.payload.name === panel_names.PLAYER_PANEL_NEW;
|
||||
const existingPanel = state.panels.find((p) => p.name === action.payload.name);
|
||||
let nextPanelInfo = action.payload.panelInfo || {};
|
||||
|
||||
// lockModalFalse 플래그 처리: DetailPanel이 스택에 있거나 lock이 이미 true면 modal=true 업데이트를 차단
|
||||
if (isPlayerPanel && existingPanel) {
|
||||
const lockFlag =
|
||||
existingPanel.panelInfo?.lockModalFalse === true || nextPanelInfo.lockModalFalse === true;
|
||||
|
||||
// unlock 명시 시 그대로 진행
|
||||
if (nextPanelInfo.lockModalFalse === false) {
|
||||
// do nothing
|
||||
} else if (lockFlag && nextPanelInfo.modal === true) {
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
lockModalFalse: true,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
} else if (lockFlag && nextPanelInfo.modal === undefined && hasDetailPanel) {
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal:
|
||||
existingPanel.panelInfo?.modal === true ? false : existingPanel.panelInfo?.modal,
|
||||
modalContainerId:
|
||||
existingPanel.panelInfo?.modal === true
|
||||
? undefined
|
||||
: existingPanel.panelInfo?.modalContainerId,
|
||||
lockModalFalse: true,
|
||||
modalStyle: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalStyle,
|
||||
modalScale: existingPanel.panelInfo?.modal === true ? undefined : nextPanelInfo.modalScale,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
} else if (hasDetailPanel && nextPanelInfo.modal === true) {
|
||||
// DetailPanel 존재 시 modal=true 업데이트 차단
|
||||
nextPanelInfo = {
|
||||
...nextPanelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
modalStyle: undefined,
|
||||
modalScale: undefined,
|
||||
shouldShrinkTo1px: false,
|
||||
isHidden: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
// 배열의 끝에서부터 시작하여 조건에 맞는 마지막 인덱스 찾기
|
||||
for (let i = state.panels.length - 1; i >= 0; i--) {
|
||||
if (state.panels[i].name === action.payload.name) {
|
||||
@@ -143,7 +197,7 @@ export const panelsReducer = (state = initialState, action) => {
|
||||
index === lastIndex
|
||||
? {
|
||||
...panel,
|
||||
panelInfo: { ...panel.panelInfo, ...action.payload.panelInfo },
|
||||
panelInfo: { ...panel.panelInfo, ...nextPanelInfo },
|
||||
}
|
||||
: panel
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -303,6 +303,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const sourcePanel = panelInfo?.sourcePanel;
|
||||
const sourceMenu = panelInfo?.sourceMenu;
|
||||
|
||||
console.log('[DP-TRACE] Detail unmount start', {
|
||||
sourcePanel,
|
||||
sourceMenu,
|
||||
panelsSnapshot: panels.map((p) => p.name),
|
||||
});
|
||||
|
||||
console.log('[Detail-BG] 306-line sourcePanel:', sourcePanel, 'sourceMenu:', sourceMenu);
|
||||
|
||||
// DetailPanel이 unmount되는 시점
|
||||
console.log('[DetailPanel] unmount:', {
|
||||
sourcePanel,
|
||||
@@ -323,6 +331,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
|
||||
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
|
||||
lastFocusedTargetId: panelInfo?.lastFocusedTargetId, // ✅ 포커스 복원 타겟 전달
|
||||
lockModalFalse: false, // Detail 종료 시 lock 해제
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -1025,34 +1034,56 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// ProductAllSection에 비디오가 있는지 확인
|
||||
const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)();
|
||||
|
||||
console.log('[BgVideo] DetailPanel - Video Control Check:', {
|
||||
console.log('[Detail-BG] 🎬 DetailPanel - Video Control Check (mount/update):', {
|
||||
hasPlayerPanel,
|
||||
isModal,
|
||||
playerPanelModalStatus: isModal,
|
||||
hasProductVideo,
|
||||
sourceMenu: panelInfo?.sourceMenu,
|
||||
productDataUrl: productData?.prdtMediaUrl,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
|
||||
if (hasPlayerPanel && hasProductVideo) {
|
||||
console.log('[BgVideo] DetailPanel - Pausing video');
|
||||
console.log('[Detail-BG] ⏸️ DetailPanel - Pausing PlayerPanel video (match: playerPanel + productVideo)', {
|
||||
isModalVideo: isModal,
|
||||
action: isModal ? 'pauseModalVideo' : 'pauseFullscreenVideo',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (isModal) {
|
||||
dispatch(pauseModalVideo());
|
||||
} else {
|
||||
dispatch(pauseFullscreenVideo());
|
||||
}
|
||||
} else {
|
||||
console.log('[BgVideo] DetailPanel - Skipping pause');
|
||||
}
|
||||
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping pause (no playerPanel or no productVideo)', {
|
||||
hasPlayerPanel,
|
||||
hasProductVideo,
|
||||
reason: !hasPlayerPanel ? 'no playerPanel' : 'no productVideo',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
|
||||
if (hasPlayerPanel && hasProductVideo) {
|
||||
console.log('[BgVideo] DetailPanel - Resuming video');
|
||||
console.log('[Detail-BG] ▶️ DetailPanel - Resuming PlayerPanel video (unmount cleanup)', {
|
||||
isModalVideo: isModal,
|
||||
action: isModal ? 'resumeModalVideo' : 'resumeFullscreenVideo',
|
||||
sourceMenu: panelInfo?.sourceMenu,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (isModal) {
|
||||
dispatch(resumeModalVideo());
|
||||
} else {
|
||||
dispatch(resumeFullscreenVideo());
|
||||
}
|
||||
} else {
|
||||
console.log('[Detail-BG] ⏭️ DetailPanel - Skipping resume on unmount', {
|
||||
hasPlayerPanel,
|
||||
hasProductVideo,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -1080,12 +1111,33 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const hasPlayerPanel = panels.some(
|
||||
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal === true
|
||||
);
|
||||
const launchedFromPlayer = panelInfo?.fromPlayer || panelInfo?.sourcePanel === panel_names.PLAYER_PANEL;
|
||||
|
||||
if (hasPlayerPanel) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview');
|
||||
if (hasPlayerPanel && !launchedFromPlayer) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - stopping video preview (non-player source)');
|
||||
dispatch(finishVideoPreview());
|
||||
} else if (hasPlayerPanel && launchedFromPlayer) {
|
||||
console.log('[DetailPanel] PlayerPanel modal=true detected - launched from Player, skip finishVideoPreview');
|
||||
|
||||
// Detail 동안 modal=true로 바뀌지 않도록 lockModalFalse 설정
|
||||
const playerPanelEntry = panels.find(
|
||||
(p) => p.name === panel_names.PLAYER_PANEL || p.name === panel_names.PLAYER_PANEL_NEW
|
||||
);
|
||||
if (playerPanelEntry?.panelInfo?.modal === true) {
|
||||
dispatch(
|
||||
updatePanel({
|
||||
name: playerPanelEntry.name,
|
||||
panelInfo: {
|
||||
...playerPanelEntry.panelInfo,
|
||||
modal: false,
|
||||
modalContainerId: undefined,
|
||||
lockModalFalse: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [panels, dispatch]);
|
||||
}, [panels, dispatch, panelInfo?.fromPlayer, panelInfo?.sourcePanel]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
|
||||
@@ -112,6 +112,8 @@ import {
|
||||
} from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
|
||||
import ProductVideo
|
||||
from '../ProductContentSection/ProductVideo/ProductVideo.v3';
|
||||
import SeeMoreProducts
|
||||
from '../ProductContentSection/SeeMoreProducts/SeeMoreProducts';
|
||||
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
|
||||
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
|
||||
import YouMayAlsoLike
|
||||
@@ -267,6 +269,10 @@ export default function ProductAllSection({
|
||||
const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
|
||||
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
|
||||
const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible);
|
||||
// 🆕 [251210] patnrId=21 카테고리 그룹 데이터
|
||||
const brandShopByShowCategoryGroups = useSelector(
|
||||
(state) => state.brand.brandShopByShowCategoryGroups
|
||||
);
|
||||
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
|
||||
const [productVideoVersion, setProductVideoVersion] = useState(1);
|
||||
// 비디오 재생 여부 flag (재생 전에는 minimize/restore 로직 비활성화)
|
||||
@@ -274,7 +280,6 @@ export default function ProductAllSection({
|
||||
|
||||
// const [currentHeight, setCurrentHeight] = useState(0);
|
||||
//하단부분까지 갔을때 체크용
|
||||
const [documentHeight, setDocumentHeight] = useState(0);
|
||||
const [isBottom, setIsBottom] = useState(false);
|
||||
|
||||
//qr코드 노출용
|
||||
@@ -283,6 +288,8 @@ export default function ProductAllSection({
|
||||
|
||||
// sendLogGNB용 entryMenu
|
||||
const entryMenuRef = useRef(null);
|
||||
const lastProductDetailLogKeyRef = useRef(null);
|
||||
const lastGnbLogKeyRef = useRef(null);
|
||||
|
||||
// 출처 정보 통합 (향후 확장성 대비)
|
||||
// YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지
|
||||
@@ -298,6 +305,7 @@ export default function ProductAllSection({
|
||||
|
||||
// 모든 timeout/timer를 추적하기 위한 ref
|
||||
const timersRef = useRef([]);
|
||||
const contentHeightRef = useRef(0);
|
||||
|
||||
// handleScrollToImages의 timeout을 추적하기 위한 ref
|
||||
const scrollToImagesTimeoutRef = useRef(null);
|
||||
@@ -322,15 +330,20 @@ export default function ProductAllSection({
|
||||
if (!container) return;
|
||||
if (typeof container.scrollTo === 'function') {
|
||||
scrollTop({ y: 0, animate: true });
|
||||
const timeOut = setTimeout(()=>{
|
||||
Spotlight.focus("product-detail-container-0");
|
||||
setTimeout(()=>{
|
||||
if(hasVideo){
|
||||
Spotlight.focus("product-video-player");
|
||||
} else {
|
||||
Spotlight.focus("product-detail-container-0");
|
||||
}
|
||||
},100);
|
||||
}
|
||||
}, [
|
||||
scrollTop
|
||||
scrollTop,
|
||||
hasVideo
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCouponData = useCallback(() => {
|
||||
dispatch(
|
||||
getProductCouponSearch({
|
||||
patnrId: selectedPatnrId,
|
||||
@@ -338,7 +351,14 @@ export default function ProductAllSection({
|
||||
mbrNo: userNumber,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
}, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// 필수 값이 모두 있을 때만 호출
|
||||
if (selectedPatnrId && selectedPrdtId) {
|
||||
fetchCouponData();
|
||||
}
|
||||
}, [fetchCouponData]);
|
||||
|
||||
const getCouponCode = () => {
|
||||
const snoArray = [];
|
||||
@@ -378,7 +398,7 @@ export default function ProductAllSection({
|
||||
setCouponTypes(idx);
|
||||
dispatch(setShowPopup(Config.ACTIVE_POPUP.couponPopup));
|
||||
},
|
||||
[dispatch, popupVisible, promotions, userNumber]
|
||||
[dispatch, userNumber, partnerCoupon]
|
||||
);
|
||||
|
||||
const handleCouponTotDownload = useCallback(() => {
|
||||
@@ -507,7 +527,7 @@ export default function ProductAllSection({
|
||||
)}
|
||||
aria-label="Download Button"
|
||||
>
|
||||
{downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno)
|
||||
{downloadCouponArr.length > 0 && downloadCouponArr.includes(cpnSno) && duplDwldYn === 'N'
|
||||
? $L('DOWNLOAD COMPLETED')
|
||||
: $L('DOWNLOAD')}
|
||||
</div>
|
||||
@@ -550,6 +570,21 @@ export default function ProductAllSection({
|
||||
[productType, themeProductInfo, themeProducts, selectedIndex, productInfo]
|
||||
);
|
||||
|
||||
// 🆕 [251211] patnrId=21인 경우 QR 데이터 확인
|
||||
useEffect(() => {
|
||||
if (productData?.patnrId === 21 || productData?.patnrId === "21") {
|
||||
console.log('[QR-Data] patnrId=21 QR 데이터 확인:', {
|
||||
patnrId: productData?.patnrId,
|
||||
prdtId: productData?.prdtId,
|
||||
qrImgUrl: productData?.qrImgUrl,
|
||||
qrCodeUrl: productData?.qrCodeUrl,
|
||||
hasQrImgUrl: !!productData?.qrImgUrl,
|
||||
hasQrCodeUrl: !!productData?.qrCodeUrl,
|
||||
allData: productData,
|
||||
});
|
||||
}
|
||||
}, [productData]);
|
||||
|
||||
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
|
||||
const isBillingProductVisible = useMemo(() => {
|
||||
// API Mode: 기존 로직 100% 유지 (절대 수정 안 함)
|
||||
@@ -721,6 +756,7 @@ export default function ProductAllSection({
|
||||
|
||||
// sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴
|
||||
useEffect(() => {
|
||||
if (!productData?.prdtId) return;
|
||||
if (!entryMenuRef.current) entryMenuRef.current = nowMenu;
|
||||
|
||||
// BUY NOW 버튼 활성화 상태에 따른 메뉴 결정 (Source SingleProduct vs UnableProduct 패턴)
|
||||
@@ -742,55 +778,88 @@ export default function ProductAllSection({
|
||||
? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}`
|
||||
: baseMenu;
|
||||
|
||||
const logKey = `${productData?.patnrId || ''}-${productData?.prdtId || ''}`;
|
||||
if (lastGnbLogKeyRef.current === logKey) {
|
||||
return;
|
||||
}
|
||||
lastGnbLogKeyRef.current = logKey;
|
||||
|
||||
dispatch(sendLogGNB(menu));
|
||||
}, [
|
||||
isBillingProductVisible,
|
||||
isGroupProductVisible,
|
||||
isTravelProductVisible,
|
||||
fromPanel?.fromYouMayLike,
|
||||
productData?.patnrId,
|
||||
productData?.prdtId,
|
||||
nowMenu,
|
||||
]);
|
||||
|
||||
// sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화)
|
||||
if (fromPanel?.fromYouMayLike === true) {
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
...panelInfo,
|
||||
fromPanel: {
|
||||
fromYouMayLike: false // 플래그 초기화
|
||||
useEffect(() => {
|
||||
if (fromPanel?.fromYouMayLike === true) {
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
...panelInfo,
|
||||
fromPanel: {
|
||||
fromYouMayLike: false // 플래그 초기화
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [fromPanel?.fromYouMayLike, isBillingProductVisible, isUnavailableProductVisible, isGroupProductVisible, isTravelProductVisible]); // BUY NOW 상태 변경 시 재실행
|
||||
}));
|
||||
}
|
||||
}, [fromPanel?.fromYouMayLike, dispatch, panelInfo]);
|
||||
|
||||
// sendLogProductDetail 로깅 - Source의 productData 변경 감지와 동일한 패턴
|
||||
useEffect(() => {
|
||||
if (productData && Object.keys(productData).length > 0) {
|
||||
const params = {
|
||||
befPrice: productData?.priceInfo?.split("|")[0],
|
||||
curationId: productData?.curationId ?? "",
|
||||
curationNm: productData?.curationNm ?? "",
|
||||
entryMenu: entryMenuRef.current,
|
||||
expsOrd: "1",
|
||||
inDt: formatGMTString(new Date()),
|
||||
lastPrice: productData?.priceInfo?.split("|")[1],
|
||||
lgCatCd: productData?.catCd ?? "",
|
||||
lgCatNm: productData?.catNm ?? "",
|
||||
linkTpCd: panelInfo?.linkTpCd ?? "",
|
||||
logTpNo: isTravelProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.TRAVEL_DETAIL
|
||||
: isGroupProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.GROUP_DETAIL
|
||||
: isBillingProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.BILLING_PRODUCT_DETAIL
|
||||
: Config.LOG_TP_NO.PRODUCT.PRODUCT_DETAIL,
|
||||
patncNm: productData?.patncNm ?? "",
|
||||
patnrId: productData?.patnrId ?? "",
|
||||
prdtId: productData?.prdtId ?? "",
|
||||
prdtNm: productData?.prdtNm ?? "",
|
||||
revwGrd: productData?.revwGrd ?? "",
|
||||
rewdAplyFlag: productData.priceInfo?.split("|")[2],
|
||||
tsvFlag: productData?.todaySpclFlag ?? "",
|
||||
};
|
||||
|
||||
return () => dispatch(sendLogProductDetail(params));
|
||||
if (!productData || Object.keys(productData).length === 0) {
|
||||
return;
|
||||
}
|
||||
}, [productData, entryMenuRef.current, panelInfo?.linkTpCd, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]); // productData 변경 시 재실행
|
||||
|
||||
const logTpNo = isTravelProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.TRAVEL_DETAIL
|
||||
: isGroupProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.GROUP_DETAIL
|
||||
: isBillingProductVisible
|
||||
? Config.LOG_TP_NO.PRODUCT.BILLING_PRODUCT_DETAIL
|
||||
: Config.LOG_TP_NO.PRODUCT.PRODUCT_DETAIL;
|
||||
|
||||
const logKey = `${productData?.patnrId || ''}-${productData?.prdtId || ''}-${panelInfo?.linkTpCd || ''}-${logTpNo}`;
|
||||
if (lastProductDetailLogKeyRef.current === logKey) {
|
||||
return;
|
||||
}
|
||||
lastProductDetailLogKeyRef.current = logKey;
|
||||
|
||||
const params = {
|
||||
befPrice: productData?.priceInfo?.split("|")[0],
|
||||
curationId: productData?.curationId ?? "",
|
||||
curationNm: productData?.curationNm ?? "",
|
||||
entryMenu: entryMenuRef.current,
|
||||
expsOrd: "1",
|
||||
inDt: formatGMTString(new Date()),
|
||||
lastPrice: productData?.priceInfo?.split("|")[1],
|
||||
lgCatCd: productData?.catCd ?? "",
|
||||
lgCatNm: productData?.catNm ?? "",
|
||||
linkTpCd: panelInfo?.linkTpCd ?? "",
|
||||
logTpNo,
|
||||
patncNm: productData?.patncNm ?? "",
|
||||
patnrId: productData?.patnrId ?? "",
|
||||
prdtId: productData?.prdtId ?? "",
|
||||
prdtNm: productData?.prdtNm ?? "",
|
||||
revwGrd: productData?.revwGrd ?? "",
|
||||
rewdAplyFlag: productData.priceInfo?.split("|")[2],
|
||||
tsvFlag: productData?.todaySpclFlag ?? "",
|
||||
};
|
||||
|
||||
dispatch(sendLogProductDetail(params));
|
||||
}, [
|
||||
productData,
|
||||
panelInfo?.linkTpCd,
|
||||
isBillingProductVisible,
|
||||
isGroupProductVisible,
|
||||
isTravelProductVisible,
|
||||
dispatch,
|
||||
]); // productData 변경 시 재실행
|
||||
|
||||
// [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로
|
||||
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
|
||||
@@ -951,6 +1020,9 @@ export default function ProductAllSection({
|
||||
|
||||
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
|
||||
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = useState(false);
|
||||
// 🆕 [251210] patnrId=21 SEE MORE PRODUCTS 버튼 표시 여부
|
||||
const [hasSeeMoreProducts, setHasSeeMoreProducts] = useState(false);
|
||||
const [seeMoreProductsData, setSeeMoreProductsData] = useState([]);
|
||||
|
||||
const reviewTotalCount = stats.totalReviews;
|
||||
|
||||
@@ -1060,6 +1132,7 @@ export default function ProductAllSection({
|
||||
const descriptionRef = useRef(null);
|
||||
const reviewRef = useRef(null);
|
||||
const youMayAlsoLikelRef = useRef(null);
|
||||
const seeMoreProductsRef = useRef(null);
|
||||
const prevMediaPanelModalStateRef = useRef(null); // MediaPanel의 이전 modal 상태 추적
|
||||
|
||||
// 동영상과 이미지를 통합한 렌더링 아이템 리스트 생성 (Indicator.jsx 로직 기반)
|
||||
@@ -1106,35 +1179,32 @@ export default function ProductAllSection({
|
||||
return hasVideo && productVideoVersion === 1;
|
||||
}, [hasVideo, productVideoVersion]);
|
||||
|
||||
const handleShopByMobileOpen = useCallback(
|
||||
pipe(() => {
|
||||
// sendLogShopByMobile - Source와 동일한 로깅 추가
|
||||
if (productData && Object.keys(productData).length > 0) {
|
||||
const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData;
|
||||
const regularPrice = priceInfo?.split("|")[0];
|
||||
const discountPrice = priceInfo?.split("|")[1];
|
||||
const discountRate = priceInfo?.split("|")[4];
|
||||
const handleShopByMobileOpen = useCallback(() => {
|
||||
// sendLogShopByMobile - Source와 동일한 로깅 추가
|
||||
if (productData && Object.keys(productData).length > 0) {
|
||||
const { priceInfo, patncNm, patnrId, prdtId, prdtNm, brndNm, catNm } = productData;
|
||||
const regularPrice = priceInfo?.split("|")[0];
|
||||
const discountPrice = priceInfo?.split("|")[1];
|
||||
const discountRate = priceInfo?.split("|")[4];
|
||||
|
||||
const logParams = {
|
||||
prdtId,
|
||||
patnrId,
|
||||
prdtNm,
|
||||
patncNm,
|
||||
brndNm,
|
||||
catNm,
|
||||
regularPrice,
|
||||
discountPrice,
|
||||
discountRate,
|
||||
shopByMobileTime: new Date().toISOString(),
|
||||
};
|
||||
const logParams = {
|
||||
prdtId,
|
||||
patnrId,
|
||||
prdtNm,
|
||||
patncNm,
|
||||
brndNm,
|
||||
catNm,
|
||||
regularPrice,
|
||||
discountPrice,
|
||||
discountRate,
|
||||
shopByMobileTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
dispatch(sendLogShopByMobile(logParams));
|
||||
}
|
||||
dispatch(sendLogShopByMobile(logParams));
|
||||
}
|
||||
|
||||
setMobileSendPopupOpen(true); // 팝업 열기
|
||||
}, setMobileSendPopupOpen),
|
||||
[]
|
||||
);
|
||||
setMobileSendPopupOpen(true); // 팝업 열기
|
||||
}, [productData, dispatch]);
|
||||
|
||||
const shopByMobileId = useMemo(
|
||||
() => SpotlightIds?.DETAIL_SHOPBYMOBILE || 'detail_shop_by_mobile',
|
||||
@@ -1219,6 +1289,116 @@ export default function ProductAllSection({
|
||||
Spotlight.focus("detail_youMayAlsoLike_area")
|
||||
},100);
|
||||
}, [scrollToSection, dispatch]);
|
||||
|
||||
const handleSeeMoreProductsClick = useCallback(() => {
|
||||
console.log('[SEE MORE PRODUCTS] Button clicked - just scroll to section');
|
||||
// 버튼 클릭 시 스크롤만 처리 (데이터는 useEffect에서 처리)
|
||||
scrollToSection('scroll-marker-see-more-products');
|
||||
setTimeout(() => {
|
||||
Spotlight.focus("detail_seeMoreProducts_area");
|
||||
}, 100);
|
||||
}, [scrollToSection]);
|
||||
|
||||
// 🆕 [251210] patnrId=21인 경우 그룹 상품 데이터 미리 처리
|
||||
useEffect(() => {
|
||||
if (panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") {
|
||||
console.log('[SEE MORE PRODUCTS] patnrId=21 detected - processing group data on panel mount');
|
||||
console.log('[SEE MORE PRODUCTS] panelInfo:', panelInfo);
|
||||
console.log('[SEE MORE PRODUCTS] brandShopByShowCategoryGroups:', brandShopByShowCategoryGroups);
|
||||
|
||||
const patnrIdString = String(panelInfo.patnrId);
|
||||
const currentPrdtId = panelInfo.prdtId;
|
||||
const categoryGroups = brandShopByShowCategoryGroups[patnrIdString];
|
||||
|
||||
console.log('[SEE MORE PRODUCTS] patnrIdString:', patnrIdString);
|
||||
console.log('[SEE MORE PRODUCTS] currentPrdtId:', currentPrdtId);
|
||||
console.log('[SEE MORE PRODUCTS] categoryGroups:', categoryGroups);
|
||||
|
||||
let shouldShowButton = false; // 버튼 표시 여부
|
||||
|
||||
if (categoryGroups) {
|
||||
let foundGroup = null;
|
||||
let foundConts = null;
|
||||
|
||||
// 모든 contsId에서 현재 상품이 속한 그룹 찾기
|
||||
for (const contsId in categoryGroups) {
|
||||
const contsInfo = categoryGroups[contsId];
|
||||
console.log(`[SEE MORE PRODUCTS] Checking contsId: ${contsId}, contsNm: ${contsInfo.contsNm}`);
|
||||
|
||||
if (contsInfo.brandShopByShowClctInfos) {
|
||||
for (const group of contsInfo.brandShopByShowClctInfos) {
|
||||
console.log(`[SEE MORE PRODUCTS] Checking group: ${group.clctNm} (${group.clctId})`);
|
||||
|
||||
if (group.brandProductInfos) {
|
||||
const foundProduct = group.brandProductInfos.find(p => p.prdtId === currentPrdtId);
|
||||
if (foundProduct) {
|
||||
console.log('[SEE MORE PRODUCTS] 🎯 Found current product:', foundProduct);
|
||||
foundGroup = group;
|
||||
foundConts = contsInfo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundGroup) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundGroup && foundConts) {
|
||||
console.log('[SEE MORE PRODUCTS] ✅ Found group info:');
|
||||
console.log(' - Category:', foundConts.contsNm);
|
||||
console.log(' - Group:', foundGroup.clctNm);
|
||||
console.log(' - Group ID:', foundGroup.clctId);
|
||||
|
||||
// 현재 상품을 제외한 다른 상품들 확인
|
||||
const otherProducts = foundGroup.brandProductInfos.filter(p => p.prdtId !== currentPrdtId);
|
||||
console.log(' - Other products count:', otherProducts.length);
|
||||
|
||||
if (otherProducts.length > 0) {
|
||||
// 다른 상품이 있을 때만 버튼 표시
|
||||
shouldShowButton = true;
|
||||
|
||||
console.log('[SEE MORE PRODUCTS] 📦 Showing button - group has other products:');
|
||||
otherProducts.forEach((product, index) => {
|
||||
console.log(` ${index + 1}. ${product.prdtNm} (${product.prdtId})`);
|
||||
console.log(` - Price: ${product.priceInfo}`);
|
||||
console.log(` - Brand: ${product.brndNm || 'N/A'}`);
|
||||
});
|
||||
|
||||
// 🆕 SeeMoreProducts 컴포넌트를 위한 데이터 변환
|
||||
const formattedProducts = otherProducts.map(product => ({
|
||||
prdtId: product.prdtId,
|
||||
prdtNm: product.prdtNm,
|
||||
priceInfo: product.priceInfo,
|
||||
patncNm: foundConts.patncNm,
|
||||
patnrId: foundConts.patnrId,
|
||||
brndNm: product.brndNm,
|
||||
imgUrl: product.prdtImgUrl,
|
||||
lgCatCd: product.lgCatCd,
|
||||
offerInfo: product.offerInfo,
|
||||
groupNm: foundGroup.clctNm,
|
||||
}));
|
||||
|
||||
// YouMayAlsoLike 데이터 형식으로 맞추기
|
||||
setSeeMoreProductsData(formattedProducts);
|
||||
} else {
|
||||
console.log('[SEE MORE PRODUCTS] ❌ No other products in group - hiding button');
|
||||
setSeeMoreProductsData([]);
|
||||
}
|
||||
} else {
|
||||
console.log('[SEE MORE PRODUCTS] ❌ No group found for current product - hiding button');
|
||||
}
|
||||
} else {
|
||||
console.log('[SEE MORE PRODUCTS] ❌ No category groups found for patnrId 21 - hiding button');
|
||||
}
|
||||
|
||||
// 버튼 표시 여부 상태 설정
|
||||
setHasSeeMoreProducts(shouldShowButton);
|
||||
} else {
|
||||
// patnrId=21이 아닌 경우 버튼 숨김
|
||||
setHasSeeMoreProducts(false);
|
||||
}
|
||||
}, [panelInfo, brandShopByShowCategoryGroups]);
|
||||
// 헤더 Back 아이콘에서 아래로 내려올 때 첫 번째 버튼을 바라보도록 설정
|
||||
useEffect(() => {
|
||||
const firstId = stackOrder[0];
|
||||
@@ -1235,6 +1415,13 @@ export default function ProductAllSection({
|
||||
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
|
||||
const scrollExpandTimerRef = useRef(null); // 스크롤 확장 타이머
|
||||
const mediaMinimizedRef = useRef(false);
|
||||
const getTotalContentHeight = useCallback(() => {
|
||||
const measuredHeight =
|
||||
contentHeightRef.current ||
|
||||
scrollContainerRef.current?.scrollHeight ||
|
||||
0;
|
||||
return measuredHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
|
||||
}, []);
|
||||
|
||||
const handleArrowClickAlternative = useCallback(() => {
|
||||
dispatch(minimizeModalMedia());
|
||||
@@ -1247,14 +1434,14 @@ export default function ProductAllSection({
|
||||
animate: true,
|
||||
});
|
||||
|
||||
// documentHeight를 활용하여 반복 계산 제거
|
||||
const totalHeight = documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
|
||||
// 캐시된 콘텐츠 높이를 활용하여 반복 계산 최소화
|
||||
const totalHeight = getTotalContentHeight();
|
||||
const isAtBottom = scrollPositionRef.current + 1100 >= totalHeight;
|
||||
|
||||
if (isAtBottom) {
|
||||
setIsBottom(isAtBottom);
|
||||
}
|
||||
}, [documentHeight, scrollTop, dispatch]);
|
||||
}, [scrollTop, dispatch, getTotalContentHeight]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e) => {
|
||||
@@ -1265,12 +1452,12 @@ export default function ProductAllSection({
|
||||
const prevScrollTop = prevScrollTopRef.current;
|
||||
|
||||
scrollPositionRef.current = currentScrollTop;
|
||||
contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
|
||||
|
||||
// 기존 bottom 체크 로직 유지
|
||||
if (documentHeight) {
|
||||
const isAtBottom =
|
||||
scrollPositionRef.current + 944 >=
|
||||
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
|
||||
const totalHeight = getTotalContentHeight();
|
||||
if (totalHeight) {
|
||||
const isAtBottom = scrollPositionRef.current + 944 >= totalHeight;
|
||||
if (isAtBottom !== isBottom) {
|
||||
setIsBottom(isAtBottom);
|
||||
}
|
||||
@@ -1323,7 +1510,7 @@ export default function ProductAllSection({
|
||||
}
|
||||
// v2: onScrollStop에서 처리 (기존 로직 유지)
|
||||
},
|
||||
[documentHeight, isBottom, productVideoVersion, isVideoPlaying, dispatch]
|
||||
[isBottom, productVideoVersion, isVideoPlaying, dispatch, getTotalContentHeight]
|
||||
);
|
||||
|
||||
// 스크롤 멈추었을 때만 호출 (성능 최적화)
|
||||
@@ -1331,10 +1518,10 @@ export default function ProductAllSection({
|
||||
(e) => {
|
||||
const currentScrollTop = e.scrollTop;
|
||||
scrollPositionRef.current = currentScrollTop;
|
||||
if (documentHeight) {
|
||||
const isAtBottom =
|
||||
currentScrollTop + 944 >=
|
||||
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
|
||||
contentHeightRef.current = e?.scrollHeight || contentHeightRef.current || 0;
|
||||
const totalHeight = getTotalContentHeight();
|
||||
if (totalHeight) {
|
||||
const isAtBottom = currentScrollTop + 944 >= totalHeight;
|
||||
if (isAtBottom !== isBottom) {
|
||||
setIsBottom(isAtBottom);
|
||||
}
|
||||
@@ -1349,7 +1536,7 @@ export default function ProductAllSection({
|
||||
return shouldMinimize;
|
||||
});
|
||||
},
|
||||
[documentHeight, isBottom]
|
||||
[getTotalContentHeight]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1374,14 +1561,6 @@ export default function ProductAllSection({
|
||||
setActiveButton(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentHeight(
|
||||
(productDetailRef.current?.scrollHeight || 0) +
|
||||
(descriptionRef.current?.scrollHeight || 0) +
|
||||
(reviewRef.current?.scrollHeight || 0)
|
||||
);
|
||||
}, [productDetailRef.current, descriptionRef.current, reviewRef.current]);
|
||||
|
||||
// 스크롤 위치에 따른 MediaPanel 제어 (비디오 재생 중에는 자동 제어 안함 - unmount 시에만 정리)
|
||||
// useEffect(() => {
|
||||
// console.log('📍 [ProductAllSection] useEffect 실행 - shouldMinimizeMedia:', shouldMinimizeMedia);
|
||||
@@ -1422,6 +1601,8 @@ export default function ProductAllSection({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sponserImage = useMemo(() => productData?.spnsrImgUrl, [productData]);
|
||||
|
||||
// WebOS TV focus-within 대체 로직
|
||||
// useEffect(() => {
|
||||
// const detailAreaElement = document.querySelector('.detailArea');
|
||||
@@ -1700,7 +1881,7 @@ export default function ProductAllSection({
|
||||
</TButton>
|
||||
</>
|
||||
)}
|
||||
{hasYouMayAlsoLike && (
|
||||
{(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
|
||||
<TButton
|
||||
className={classNames(
|
||||
css.youMayLikeButton,
|
||||
@@ -1711,6 +1892,19 @@ export default function ProductAllSection({
|
||||
{$L('YOU MAY ALSO LIKE')}
|
||||
</TButton>
|
||||
)}
|
||||
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 버튼 */}
|
||||
{(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
|
||||
<TButton
|
||||
className={classNames(
|
||||
css.seeMoreProductButton,
|
||||
activeButton === 'seemoreproducts' ? css.active : ''
|
||||
)}
|
||||
onClick={handleSeeMoreProductsClick}
|
||||
spotlightId="see-more-products-button"
|
||||
>
|
||||
{$L('SEE MORE PRODUCTS')}
|
||||
</TButton>
|
||||
)}
|
||||
{/* YouMayLike 버튼 렌더링 상태 로그 */}
|
||||
{/* {(() => {
|
||||
console.log('[YouMayLike] 버튼 렌더링 체크:', {
|
||||
@@ -1784,7 +1978,7 @@ export default function ProductAllSection({
|
||||
id="product-details-section"
|
||||
ref={productDetailRef}
|
||||
onFocus={() => handleButtonFocus('product')}
|
||||
onBlur={handleButtonBlur}
|
||||
onBlur={handleButtonBlur}
|
||||
>
|
||||
{/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */}
|
||||
{hasVideo && renderItems[0].type === 'video' && productVideoVersion !== 3 && (
|
||||
@@ -1873,7 +2067,7 @@ export default function ProductAllSection({
|
||||
</div>
|
||||
<div ref={youMayAlsoLikelRef}>
|
||||
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div>
|
||||
{hasYouMayAlsoLike && (
|
||||
{(panelInfo?.patnrId !== 21 || panelInfo?.patnrId !== "21") && hasYouMayAlsoLike && (
|
||||
<div id="you-may-also-like-section">
|
||||
{/* {(() => {
|
||||
console.log('[YouMayLike] YouMayAlsoLike 컴포넌트 렌더링:', {
|
||||
@@ -1894,6 +2088,22 @@ export default function ProductAllSection({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 🆕 [251210] patnrId=21인 경우 SEE MORE PRODUCTS 섹션 */}
|
||||
{(panelInfo?.patnrId === 21 || panelInfo?.patnrId === "21") && hasSeeMoreProducts && (
|
||||
<div ref={seeMoreProductsRef}>
|
||||
<div id="scroll-marker-see-more-products" className={css.scrollMarker}></div>
|
||||
<div id="see-more-products-section" data-spotlight-id="see-more-products-area">
|
||||
<SeeMoreProducts
|
||||
groupProducts={seeMoreProductsData}
|
||||
sponserImage={sponserImage}
|
||||
panelInfo={panelInfo}
|
||||
onFocus={() => handleButtonFocus('seemoreproducts')}
|
||||
onBlur={handleButtonBlur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={css.topButtonBox}>
|
||||
<TButton
|
||||
|
||||
@@ -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,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]);
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
.thumbnailWrapper .productImage {
|
||||
transform: scale(1.015); // 가로세로 10px 정도 확대 효과
|
||||
transform: scale(1.015); // 가로세로 10px 정도 확대 효과
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -214,4 +227,4 @@ export default function ShopByMobilePriceDisplay({
|
||||
]);
|
||||
|
||||
return <div>{renderPriceItem()}</div>;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btmLayer2 {
|
||||
.btmLayer2 {
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function THeaderCustom({
|
||||
if(onBackButtonFocus){
|
||||
onBackButtonFocus();
|
||||
}
|
||||
},[onBackButtonFocus])
|
||||
},[onBackButtonFocus])
|
||||
|
||||
return (
|
||||
<Container className={classNames(css.tHeaderCustom, className)} {...rest}>
|
||||
@@ -113,7 +113,7 @@ export default function THeaderCustom({
|
||||
marqueeDisabled={marqueeDisabled}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{prdtId && <span className={css.prdtId}>ID : {prdtId}</span>}
|
||||
{(prdtId && patnrId !== "21") && <span className={css.prdtId}>ID : {prdtId}</span>}
|
||||
{convertedTitle && (
|
||||
<span dangerouslySetInnerHTML={{ __html: convertedTitle }} />
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export default function HomeBanner({
|
||||
const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
|
||||
|
||||
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
|
||||
const focusHistory = useFocusHistory({
|
||||
enableLogging: true,
|
||||
@@ -163,7 +164,10 @@ export default function HomeBanner({
|
||||
videoData = targetBannerData.bannerDetailInfos?.[0];
|
||||
}
|
||||
|
||||
if (videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
|
||||
// DetailPanel이 떠 있는 동안에는 배너 자동 재생을 스킵 (PlayerPanel 모달 재설정 방지)
|
||||
const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL);
|
||||
|
||||
if (!hasDetailPanel && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
|
||||
console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
|
||||
|
||||
dispatch(
|
||||
@@ -175,12 +179,13 @@ export default function HomeBanner({
|
||||
shptmBanrTpNm: videoData.shptmBanrTpNm,
|
||||
lgCatCd: videoData.lgCatCd,
|
||||
chanId: videoData.brdcChnlId,
|
||||
// 기본: 배너는 modal=true로 재생
|
||||
modal: true,
|
||||
modalContainerId: defaultFocus,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [bannerDataList, defaultFocus, dispatch]);
|
||||
}, [bannerDataList, defaultFocus, dispatch, panels]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(index, isHorizontal) => {
|
||||
|
||||
@@ -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) => {
|
||||
@@ -489,6 +519,14 @@ export default function RandomUnit({
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case 'DSP00510':
|
||||
linkInfo = {
|
||||
name: panel_names.JUST_FOR_YOU_TEST_PANEL,
|
||||
panelInfo: {
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
linkInfo = {
|
||||
@@ -643,6 +681,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(() => {
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { getContainerId } from '@enact/spotlight/src/container';
|
||||
|
||||
import btnPlay from '../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||
import emptyHorImage from '../../../../assets/images/img-home-banner-empty-hor.png';
|
||||
import emptyVerImage from '../../../../assets/images/img-home-banner-empty-ver.png';
|
||||
import defaultImageItem from '../../../../assets/images/img-thumb-empty-product@3x.png';
|
||||
import defaultLogoImg
|
||||
from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||
import emptyHorImage
|
||||
from '../../../../assets/images/img-home-banner-empty-hor.png';
|
||||
import emptyVerImage
|
||||
from '../../../../assets/images/img-home-banner-empty-ver.png';
|
||||
import defaultImageItem
|
||||
from '../../../../assets/images/img-thumb-empty-product@3x.png';
|
||||
import liveShow from '../../../../assets/images/tag-liveshow.png';
|
||||
import { setBannerIndex, updateHomeInfo } from '../../../actions/homeActions';
|
||||
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions';
|
||||
import { pushPanel, SOURCE_MENUS } from '../../../actions/panelActions';
|
||||
import { startVideoPlayer, finishVideoPreview } from '../../../actions/playActions';
|
||||
import {
|
||||
setBannerIndex,
|
||||
updateHomeInfo,
|
||||
} from '../../../actions/homeActions';
|
||||
import {
|
||||
sendLogTopContents,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../../actions/logActions';
|
||||
import {
|
||||
pushPanel,
|
||||
SOURCE_MENUS,
|
||||
} from '../../../actions/panelActions';
|
||||
import {
|
||||
finishVideoPreview,
|
||||
startVideoPlayer,
|
||||
} from '../../../actions/playActions';
|
||||
import CustomImage from '../../../components/CustomImage/CustomImage';
|
||||
import usePriceInfo from '../../../hooks/usePriceInfo';
|
||||
import {
|
||||
@@ -27,7 +53,10 @@ import {
|
||||
LOG_TP_NO,
|
||||
panel_names,
|
||||
} from '../../../utils/Config';
|
||||
import { $L, formatGMTString } from '../../../utils/helperMethods';
|
||||
import {
|
||||
$L,
|
||||
formatGMTString,
|
||||
} from '../../../utils/helperMethods';
|
||||
import { TEMPLATE_CODE_CONF } from '../HomePanel';
|
||||
import css from './RollingUnit.module.less';
|
||||
|
||||
@@ -81,6 +110,7 @@ export default function RollingUnit({
|
||||
const introTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
|
||||
const homeCategory = useSelector((state) => state.home.menuData?.data?.homeCategory);
|
||||
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
|
||||
const foryouInfos = useSelector((state) => state.foryou.recommendInfo.recommendProduct);
|
||||
|
||||
const { userNumber } = useSelector((state) => state.common.appStatus.loginUserData);
|
||||
|
||||
@@ -119,7 +149,7 @@ export default function RollingUnit({
|
||||
|
||||
// filteredRollingDataRef 업데이트
|
||||
useEffect(() => {
|
||||
filteredRollingDataRef.current = filteredRollingData;
|
||||
filteredRollingDataRef.current = filteredRollingData;
|
||||
}, [filteredRollingData]);
|
||||
|
||||
const topContentsLogInfo = useMemo(() => {
|
||||
@@ -333,6 +363,29 @@ export default function RollingUnit({
|
||||
const { originalPrice, discountedPrice, discountRate, offerInfo } =
|
||||
usePriceInfo(filteredRollingData.length > 0 ? filteredRollingData[startIndex].priceInfo : {}) || {};
|
||||
|
||||
// Just For You 데이터에서 첫 번째 상품 추출
|
||||
const justForYouProduct = useMemo(() => {
|
||||
if (foryouInfos && foryouInfos.length > 0) {
|
||||
const justForYouShelf = foryouInfos.find(
|
||||
(shelf) => shelf.recommendTpCd === 'JUSTFORYOU'
|
||||
);
|
||||
if (justForYouShelf && justForYouShelf.productInfos && justForYouShelf.productInfos.length > 0) {
|
||||
return justForYouShelf.productInfos[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [foryouInfos]);
|
||||
|
||||
// Just For You 상품의 가격 정보
|
||||
const justForYouPriceInfo = usePriceInfo(
|
||||
justForYouProduct && justForYouProduct.priceInfo ? justForYouProduct.priceInfo : ''
|
||||
) || {
|
||||
originalPrice: '',
|
||||
discountedPrice: '',
|
||||
discountRate: null,
|
||||
offerInfo: '',
|
||||
};
|
||||
|
||||
const handlePushPanel = useCallback(
|
||||
(name, panelInfo) => {
|
||||
const isDetailPanel = name === panel_names.DETAIL_PANEL;
|
||||
@@ -582,7 +635,8 @@ export default function RollingUnit({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === 'Image Banner' ? (
|
||||
{/* 일반 Image Banner (Just For You, Today's Deals 제외) */}
|
||||
{filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === 'Image Banner' && filteredRollingData[startIndex].shptmLnkTpNm !== 'Just For You' && filteredRollingData[startIndex].shptmLnkTpNm !== "Today's Deals" ? (
|
||||
<SpottableComponent
|
||||
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
|
||||
onClick={imageBannerClick}
|
||||
@@ -700,7 +754,7 @@ export default function RollingUnit({
|
||||
)}
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmBanrTpNm === "Today's Deals" ? (
|
||||
) : filteredRollingData && filteredRollingData.length > 0 && (filteredRollingData[startIndex].shptmBanrTpNm === "Today's Deals" || filteredRollingData[startIndex].shptmLnkTpNm === "Today's Deals") ? (
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
@@ -754,6 +808,75 @@ export default function RollingUnit({
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : filteredRollingData && filteredRollingData.length > 0 && filteredRollingData[startIndex].shptmLnkTpNm === "Just For You" ? (
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
css.justforyou,
|
||||
countryCode === 'RU' ? css.ru : '',
|
||||
countryCode === 'DE' ? css.de : '',
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onClick={imageBannerClick}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
spotlightId={spotlightId}
|
||||
spotlightDisabled={contentsFocus}
|
||||
aria-label={
|
||||
justForYouProduct?.prdtNm
|
||||
? justForYouProduct.prdtNm
|
||||
: filteredRollingData[startIndex].tmnlImgNm
|
||||
}
|
||||
>
|
||||
<div className={css.productInfo}>
|
||||
<div className={css.justforyouTitle}>{$L("Just For You")}</div>
|
||||
<div
|
||||
className={css.textBox}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${justForYouProduct?.prdtNm || filteredRollingData[startIndex].prdtNm}`,
|
||||
}}
|
||||
/>
|
||||
<div className={css.accBox}>
|
||||
{justForYouProduct ? (
|
||||
<>
|
||||
{parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') === 0
|
||||
? justForYouProduct.offerInfo
|
||||
: justForYouPriceInfo.discountRate
|
||||
? justForYouPriceInfo.discountedPrice
|
||||
: justForYouPriceInfo.originalPrice}
|
||||
{justForYouPriceInfo.discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{parseFloat(originalPrice?.replace('$', '') || '0') === 0
|
||||
? filteredRollingData[startIndex]?.offerInfo
|
||||
: discountRate
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
{discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isHorizontal && justForYouProduct && parseFloat(justForYouPriceInfo.originalPrice?.replace('$', '') || '0') !== 0 && (
|
||||
<span className={css.saleAccBox}>{justForYouPriceInfo.originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.itemImgBox}>
|
||||
<CustomImage
|
||||
alt=""
|
||||
delay={0}
|
||||
animationSpeed="fast"
|
||||
src={justForYouProduct?.imgUrl || filteredRollingData[startIndex]?.tmnlImgPath}
|
||||
fallbackSrc={defaultImageItem}
|
||||
ariaLabel={justForYouProduct?.prdtNm || filteredRollingData[startIndex]?.tmnlImgNm}
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : null}
|
||||
|
||||
{filteredRollingData.length !== 1 ? (
|
||||
|
||||
@@ -237,6 +237,165 @@
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
&.justforyou {
|
||||
background-image: url(../../../../assets/images/img-home-banner-jfy-ver@3x.png);
|
||||
background-size: 486px 858px;
|
||||
background-position: left top;
|
||||
border-radius: 10px;
|
||||
padding: 75px 51px 0;
|
||||
&.ru {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 58px;
|
||||
line-height: 60px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.de {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 74px !important;
|
||||
line-height: 63px !important;
|
||||
letter-spacing: -1px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.productInfo {
|
||||
margin-bottom: 31px;
|
||||
.justforyouTitle {
|
||||
.size(@w:100%,@h:132px);
|
||||
font-family: Arial;
|
||||
font-weight: bold;
|
||||
font-size: 80px;
|
||||
word-break: break-word;
|
||||
font-stretch: normal;
|
||||
color: #151515;
|
||||
text-align: center;
|
||||
line-height: 76px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
.textBox {
|
||||
.size(@w: 100%, @h: 80px);
|
||||
margin-top: 71px;
|
||||
.elip(@clamp:2);
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
color: @COLOR_GRAY06;
|
||||
line-height: 1.27;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.accBox {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 42px;
|
||||
color: @PRIMARY_COLOR_RED;
|
||||
line-height: 1.14;
|
||||
display: inline-block;
|
||||
.elip(@clamp:1);
|
||||
> strong {
|
||||
width: 260px;
|
||||
font-size: 30px;
|
||||
line-height: 1.27;
|
||||
display: block;
|
||||
.elip(@clamp:2);
|
||||
}
|
||||
.saleAccBox {
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
color: @COLOR_GRAY04;
|
||||
vertical-align: middle;
|
||||
text-decoration: line-through;
|
||||
margin-left: 9px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.itemImgBox {
|
||||
> img {
|
||||
.size(@w: 356px, @h: 356px);
|
||||
border-radius: 12px;
|
||||
border:6px solid #DCB9A1;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
&.isHorizontal {
|
||||
background-image: url(../../../../assets/images/img-home-banner-jfy-hor@3x.png);
|
||||
background-size: 744px 420px;
|
||||
background-position: center center;
|
||||
display: flex;
|
||||
padding: 0 30px 0 0;
|
||||
border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
-o-border-radius: 10px;
|
||||
> div {
|
||||
flex: none;
|
||||
}
|
||||
&.ru {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 58px;
|
||||
line-height: 60px;
|
||||
font-family: @arialFontBold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.de {
|
||||
.productInfo {
|
||||
.justforyouTitle {
|
||||
font-size: 59px !important;
|
||||
line-height: 63px !important;
|
||||
letter-spacing: -2px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.productInfo {
|
||||
margin-bottom: 0;
|
||||
.justforyouTitle {
|
||||
.size(@w:305px,@h:114px);
|
||||
margin-top: 53px;
|
||||
margin-left: 49px;
|
||||
font-size: 66px;
|
||||
word-break: break-word;
|
||||
color: #151515;
|
||||
text-align: left;
|
||||
line-height: 57px;
|
||||
font-family: @arialBlack;
|
||||
}
|
||||
.textBox {
|
||||
.size(@w: 294px, @h: 80px);
|
||||
margin: 67px 0 5px 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.accBox {
|
||||
.size(@w: 320px, @h: 50px);
|
||||
margin-left: 50px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
.elip(@clamp:1);
|
||||
}
|
||||
.saleAccBox {
|
||||
color: #767676;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: 5px 0 0 55px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
.itemImgBox {
|
||||
.position(@position: absolute, @top: 47px, @left: 389px);
|
||||
.size(@w: 326px, @h: 326px);
|
||||
> img {
|
||||
.size(@w: inherit, @h: inherit);
|
||||
border-radius: 12px;
|
||||
border:6px solid #DCB9A1;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { types } from '../../actions/actionTypes';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { applyMiddleware } from 'redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
@@ -11,53 +19,6 @@ import {
|
||||
setContainerLastFocusedElement,
|
||||
} from '@enact/spotlight/src/container';
|
||||
|
||||
import {
|
||||
changeAppStatus,
|
||||
setDeepLink,
|
||||
setExitApp,
|
||||
setHidePopup,
|
||||
setShowPopup,
|
||||
setOptionalTermsPopupShown,
|
||||
updateOptionalTermsAgreement,
|
||||
} from '../../actions/commonActions';
|
||||
import { getWelcomeEventInfo } from '../../actions/eventActions';
|
||||
import {
|
||||
checkEnterThroughGNB,
|
||||
getHomeLayout,
|
||||
getHomeMainContents,
|
||||
updateHomeInfo,
|
||||
// <<<<<<< HEAD
|
||||
} from '../../actions/homeActions';
|
||||
import { setMyPageTermsAgree } from '../../actions/myPageActions';
|
||||
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
|
||||
import { getSubCategory, getTop20Show } from '../../actions/mainActions';
|
||||
import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
|
||||
import { updatePanel } from '../../actions/panelActions';
|
||||
import {
|
||||
showModalVideo,
|
||||
finishVideoPreview,
|
||||
hideModalVideo,
|
||||
startVideoPlayerNew,
|
||||
} from '../../actions/playActions';
|
||||
import { getBestSeller } from '../../actions/productActions';
|
||||
import TBody from '../../components/TBody/TBody';
|
||||
import TButton, { TYPES } from '../../components/TButton/TButton';
|
||||
import OptionalConfirm from '../../components/Optional/OptionalConfirm';
|
||||
import TNewPopUp from '../../components/TPopUp/TNewPopUp';
|
||||
import TPanel from '../../components/TPanel/TPanel';
|
||||
import TPopUp from '../../components/TPopUp/TPopUp';
|
||||
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import useDebugKey from '../../hooks/useDebugKey';
|
||||
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay';
|
||||
import ImagePreloader from '../../utils/ImagePreloader';
|
||||
import { createDebugHelpers } from '../../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// DetailPanelBackground 이미지 imports for preloading
|
||||
import hsn from '../../../assets/images/bg/hsn_new.png';
|
||||
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png';
|
||||
@@ -66,17 +27,54 @@ import ontv4u from '../../../assets/images/bg/ontv4u_new.png';
|
||||
import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
|
||||
import qvc from '../../../assets/images/bg/qvc_new.png';
|
||||
import shoplc from '../../../assets/images/bg/shoplc_new.png';
|
||||
|
||||
// 파트너사별 배경 이미지 맵
|
||||
const BACKGROUND_IMAGES = {
|
||||
1: qvc, // QVC
|
||||
2: hsn, // HSN
|
||||
4: ontv4u, // ONTV
|
||||
9: lgelectronics, // LG ELECTRONICS
|
||||
11: shoplc, // SHOPLC
|
||||
16: koreaKiosk, // KOREA KIOSK
|
||||
19: Pinkfong, // PINKFONG
|
||||
};
|
||||
import nbcu from '../../../assets/images/bg/nbcu_new.png';
|
||||
import { types } from '../../actions/actionTypes';
|
||||
import {
|
||||
changeAppStatus,
|
||||
setDeepLink,
|
||||
setExitApp,
|
||||
setHidePopup,
|
||||
setOptionalTermsPopupShown,
|
||||
setShowPopup,
|
||||
updateOptionalTermsAgreement,
|
||||
} from '../../actions/commonActions';
|
||||
import { getWelcomeEventInfo } from '../../actions/eventActions';
|
||||
import {
|
||||
checkEnterThroughGNB,
|
||||
getHomeLayout,
|
||||
getHomeMainContents,
|
||||
updateHomeInfo,
|
||||
} from '../../actions/homeActions';
|
||||
import {
|
||||
sendLogGNB,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../actions/logActions';
|
||||
import {
|
||||
getSubCategory,
|
||||
getTop20Show,
|
||||
} from '../../actions/mainActions';
|
||||
import { setMyPageTermsAgree } from '../../actions/myPageActions';
|
||||
import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
|
||||
import { updatePanel } from '../../actions/panelActions';
|
||||
import {
|
||||
finishVideoPreview,
|
||||
hideModalVideo,
|
||||
showModalVideo,
|
||||
startVideoPlayerNew,
|
||||
} from '../../actions/playActions';
|
||||
import { getBestSeller } from '../../actions/productActions';
|
||||
import OptionalConfirm from '../../components/Optional/OptionalConfirm';
|
||||
import TBody from '../../components/TBody/TBody';
|
||||
import TButton, { TYPES } from '../../components/TButton/TButton';
|
||||
import TPanel from '../../components/TPanel/TPanel';
|
||||
import TNewPopUp from '../../components/TPopUp/TNewPopUp';
|
||||
import TPopUp from '../../components/TPopUp/TPopUp';
|
||||
import TVerticalPagenator
|
||||
from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||
import useDebugKey from '../../hooks/useDebugKey';
|
||||
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay';
|
||||
// [COMMENTED OUT] useVideoMove 관련 코드 주석 처리 - 향후 사용 검토 필요
|
||||
// import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove';
|
||||
// =======
|
||||
@@ -100,7 +98,9 @@ import {
|
||||
LOG_MESSAGE_ID,
|
||||
panel_names,
|
||||
} from '../../utils/Config';
|
||||
import { createDebugHelpers } from '../../utils/debug';
|
||||
import { $L } from '../../utils/helperMethods';
|
||||
import ImagePreloader from '../../utils/ImagePreloader';
|
||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||
import BestSeller from '../HomePanel/BestSeller/BestSeller';
|
||||
import HomeBanner from '../HomePanel/HomeBanner/HomeBanner';
|
||||
@@ -111,6 +111,22 @@ import SubCategory from '../HomePanel/SubCategory/SubCategory';
|
||||
import EventPopUpBanner from './EventPopUpBanner/EventPopUpBanner';
|
||||
import PickedForYou from './PickedForYou/PickedForYou';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// 파트너사별 배경 이미지 맵
|
||||
const BACKGROUND_IMAGES = {
|
||||
1: qvc, // QVC
|
||||
2: hsn, // HSN
|
||||
4: ontv4u, // ONTV
|
||||
9: lgelectronics, // LG ELECTRONICS
|
||||
11: shoplc, // SHOPLC
|
||||
16: koreaKiosk, // KOREA KIOSK
|
||||
19: Pinkfong, // PINKFONG
|
||||
21: nbcu, // NBCU
|
||||
};
|
||||
|
||||
export const TEMPLATE_CODE_CONF = {
|
||||
TOP: 'DSP00101',
|
||||
CATEGORY_ITEM: 'DSP00102',
|
||||
@@ -399,6 +415,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
|
||||
optionalTerms: 'Y',
|
||||
},
|
||||
});
|
||||
setTimeout(()=>{
|
||||
Spotlight.focus('home_tbody');
|
||||
},100)
|
||||
}, [handleOptionalAgree, dispatch, currentTermsFlag]);
|
||||
|
||||
const handleOptionalDeclineClick = useCallback(() => {
|
||||
@@ -407,6 +426,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
|
||||
}
|
||||
dispatch(updateOptionalTermsAgreement(false));
|
||||
setIsOptionalConfirmVisible(false);
|
||||
setTimeout(()=>{
|
||||
Spotlight.focus('home_tbody');
|
||||
},100)
|
||||
}, [dispatch]);
|
||||
|
||||
const handleTermsPopupClosed = useCallback(() => {
|
||||
|
||||
@@ -53,7 +53,7 @@ import { types } from "../../actions/actionTypes";
|
||||
import { focusById } from "../../utils/spotlight-utils";
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
{ enterTo: "#selectAllCheckbox" },
|
||||
"div",
|
||||
);
|
||||
|
||||
@@ -282,7 +282,7 @@ function IntroPanelWithOptional({
|
||||
useEffect(() => {
|
||||
const focusTimer = setTimeout(() => {
|
||||
focusById("selectAllCheckbox");
|
||||
}, 500);
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
clearTimeout(focusTimer);
|
||||
@@ -844,27 +844,29 @@ function IntroPanelWithOptional({
|
||||
|
||||
//20250903 pjh
|
||||
//selectAll에서 포커스 올렸을때 처리 변경.
|
||||
const onSelectAllSpotlightUp = () => {
|
||||
const focusTimer = setTimeout(() => {
|
||||
Spotlight.focus("optionalCheckbox");
|
||||
}, 100);
|
||||
return () => {
|
||||
clearTimeout(focusTimer);
|
||||
};
|
||||
const onSelectAllSpotlightUp = (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
setTimeout(() => {
|
||||
focusById("optionalCheckbox");
|
||||
}, 10);
|
||||
};
|
||||
|
||||
//Optional terms 체크박스가 체크가 되어있을때 아래로 내리면(3개전부 체크기준) agree로 가고, 아닐경우 select all로 포커스이동해야함.
|
||||
const onOptionalTermSpotlightDown = () => {
|
||||
const focusTimer = setTimeout(() => {
|
||||
const onOptionalTermSpotlightDown = (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (termsChecked && privacyChecked && optionalChecked) {
|
||||
Spotlight.focus("agreeButton");
|
||||
focusById("agreeButton");
|
||||
} else {
|
||||
Spotlight.focus("selectAllCheckbox");
|
||||
focusById("selectAllCheckbox");
|
||||
}
|
||||
}, 100);
|
||||
return () => {
|
||||
clearTimeout(focusTimer);
|
||||
};
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
@@ -905,7 +907,7 @@ function IntroPanelWithOptional({
|
||||
handleCancel={onCancel}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<Container {...rest} className={css.introLayout}>
|
||||
<Container className={css.introLayout} defaultElement="#selectAllCheckbox" {...rest}>
|
||||
{/* 첫 번째 영역: 헤더 섹션 */}
|
||||
<div className={css.headerSection}>
|
||||
<div className={css.titleContainer}>
|
||||
|
||||
@@ -206,7 +206,9 @@ export default function MainView({ className, initService }) {
|
||||
let renderingPanels = [];
|
||||
|
||||
const topPanel = panels[panels.length - 1];
|
||||
|
||||
const hasFeaturedBrandsPanel = panels.some(
|
||||
(panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL
|
||||
);
|
||||
// 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들
|
||||
if (DEBUG_MODE) {
|
||||
console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`);
|
||||
@@ -248,7 +250,7 @@ export default function MainView({ className, initService }) {
|
||||
'[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel'
|
||||
);
|
||||
}
|
||||
renderingPanels = panels.slice(-3);
|
||||
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-4) : panels.slice(-3);
|
||||
} else if (
|
||||
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
|
||||
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
|
||||
@@ -256,12 +258,17 @@ export default function MainView({ className, initService }) {
|
||||
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL ||
|
||||
panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL
|
||||
) {
|
||||
renderingPanels = panels.slice(-2);
|
||||
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-3) : panels.slice(-2);
|
||||
} else {
|
||||
renderingPanels = panels.slice(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// DetailPanel 위치 확인 (있으면 항상 onTop 처리)
|
||||
const detailPanelIndex = renderingPanels.findIndex(
|
||||
(panel) => panel.name === Config.panel_names.DETAIL_PANEL
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isHomeOnTop ||
|
||||
@@ -287,17 +294,12 @@ export default function MainView({ className, initService }) {
|
||||
console.log(`[MainView] Standalone panel ${panel.name} is always onTop`);
|
||||
}
|
||||
}
|
||||
// 3-layer 케이스: 중간 패널(DetailPanel)이 onTop
|
||||
else if (renderingPanels.length === 3) {
|
||||
if (index === 1) {
|
||||
// DetailPanel (중간)
|
||||
isPanelOnTop = true;
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[MainView] 3-layer: DetailPanel is onTop');
|
||||
}
|
||||
// DetailPanel이 포함되어 있으면 항상 onTop
|
||||
else if (detailPanelIndex >= 0) {
|
||||
isPanelOnTop = index === detailPanelIndex;
|
||||
if (DEBUG_MODE && isPanelOnTop) {
|
||||
console.log('[MainView] DetailPanel set to onTop');
|
||||
}
|
||||
// PlayerPanel (index 0): isOnTop = false (백그라운드)
|
||||
// MediaPanel (index 2): isOnTop = false (modal overlay)
|
||||
}
|
||||
// 2-layer 케이스: modal이면 첫 번째가 onTop
|
||||
else if (
|
||||
|
||||
@@ -106,7 +106,10 @@ export default memo(function PlayerItemCard({
|
||||
}
|
||||
}, [onFocus]);
|
||||
|
||||
const progressStyle = useMemo(() => ({ width: `${percentGauge}%` }), [percentGauge]);
|
||||
const progressStyle = useMemo(
|
||||
() => ({ width: `${percentGauge}%` }),
|
||||
[percentGauge]
|
||||
);
|
||||
const ariaLabel = 'Selected, ' + patnerName + ' ' + productName();
|
||||
|
||||
const css = version === 2 ? css2 : css1;
|
||||
@@ -121,13 +124,17 @@ export default memo(function PlayerItemCard({
|
||||
onBlur={_onBlur}
|
||||
onClick={_onClick}
|
||||
onFocus={_onFocus}
|
||||
spotlightId={productId ? 'spotlightId-' + removeDotAndColon(productId) : spotlightId}
|
||||
spotlightId={
|
||||
productId ? 'spotlightId-' + removeDotAndColon(productId) : spotlightId
|
||||
}
|
||||
aria-label={ariaLabel}
|
||||
{...rest}
|
||||
>
|
||||
<div className={css.imageWrap}>
|
||||
<CustomImage alt={imageAlt} delay={0} src={imageSource} />
|
||||
{soldoutFlag && soldoutFlag === 'Y' && <div>{$L(STRING_CONF.SOLD_OUT)}</div>}
|
||||
{soldoutFlag && soldoutFlag === 'Y' && (
|
||||
<div>{$L(STRING_CONF.SOLD_OUT)}</div>
|
||||
)}
|
||||
{currentVideoVisible && (
|
||||
<div className={css.nowPlay}>
|
||||
<h3>{$L('Now Playing')}</h3>
|
||||
@@ -136,7 +143,14 @@ export default memo(function PlayerItemCard({
|
||||
</div>
|
||||
<div className={css.descWrap}>
|
||||
<div className={css.patnerWrap}>
|
||||
<CustomImage className={css.logo} src={logo} fallbackSrc={defaultLogoImg} />
|
||||
<CustomImage
|
||||
className={classNames(
|
||||
css.logo,
|
||||
(patnerName === 'QVC' || patnerName === 'qvc') && css.qvcLogo
|
||||
)}
|
||||
src={logo}
|
||||
fallbackSrc={defaultLogoImg}
|
||||
/>
|
||||
<h3 className={css.brandName}>{patnerName}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../../style/CommonStyle.module.less";
|
||||
@import "../../../style/utils.module.less";
|
||||
@import '../../../style/CommonStyle.module.less';
|
||||
@import '../../../style/utils.module.less';
|
||||
|
||||
/* liveHorizontal */
|
||||
.liveHorizontal {
|
||||
@@ -86,10 +86,13 @@
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
object-fit: contain;
|
||||
&.qvcLogo {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.brandName {
|
||||
color: #EAEAEA;
|
||||
color: #eaeaea;
|
||||
font-size: 24px;
|
||||
font-family: @baseFont;
|
||||
font-weight: 700;
|
||||
@@ -100,7 +103,7 @@
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
color: #EAEAEA;
|
||||
color: #eaeaea;
|
||||
font-size: 24px;
|
||||
font-family: @baseFont;
|
||||
font-weight: 400;
|
||||
@@ -124,16 +127,16 @@
|
||||
|
||||
// track 역할
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
.size(@w: 100%, @h: 5px);
|
||||
.position(@position: absolute, @top: 0, @left: 0);
|
||||
background: #D4D4D4;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
|
||||
.gauge {
|
||||
height: 5px;
|
||||
.position(@position: absolute, @top: 0, @left: 0);
|
||||
background: #7D848C;
|
||||
background: #7d848c;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
&.focused {
|
||||
@@ -158,7 +161,7 @@
|
||||
border-color: @PRIMARY_COLOR_RED;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -259,7 +262,7 @@
|
||||
}
|
||||
|
||||
.brandName {
|
||||
color: #EAEAEA;
|
||||
color: #eaeaea;
|
||||
font-size: 24px;
|
||||
font-family: @baseFont;
|
||||
font-weight: 700;
|
||||
@@ -270,7 +273,7 @@
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
color: #EAEAEA;
|
||||
color: #eaeaea;
|
||||
font-size: 24px;
|
||||
font-family: @baseFont;
|
||||
font-weight: 400;
|
||||
@@ -289,7 +292,7 @@
|
||||
border-color: @PRIMARY_COLOR_RED;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -1161,6 +1161,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
}
|
||||
|
||||
if (!panelInfo.modal) {
|
||||
console.log('[PlayerPanel] popPanel - closeButtonHandler');
|
||||
dispatch(PanelActions.popPanel());
|
||||
dispatch(changeAppStatus({ cursorVisible: false }));
|
||||
|
||||
@@ -1191,13 +1192,27 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
//todo if(modal)
|
||||
return () => {
|
||||
// 패널이 2개 존재할때만 popPanel 진행
|
||||
if (panelInfo.modal && !isOnTop) {
|
||||
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
|
||||
console.log('[PP-TRACE] cleanup start', {
|
||||
modal: panelInfo.modal,
|
||||
isOnTop,
|
||||
topPanel: panels[panels.length - 1]?.name,
|
||||
stack: panels.map((p) => p.name),
|
||||
});
|
||||
const topPanelName = panels[panels.length - 1]?.name;
|
||||
if (
|
||||
panelInfo.modal &&
|
||||
!isOnTop &&
|
||||
topPanelName === panel_names.PLAYER_PANEL &&
|
||||
panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지)
|
||||
) {
|
||||
console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)');
|
||||
dispatch(PanelActions.popPanel());
|
||||
} else {
|
||||
Spotlight.focus('tbody');
|
||||
}
|
||||
};
|
||||
}, [panelInfo?.modal, isOnTop]);
|
||||
}, [panelInfo?.modal, isOnTop, panels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') {
|
||||
@@ -1810,9 +1825,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
panelInfo?.modalContainerId
|
||||
) {
|
||||
// case: Featured Brands
|
||||
if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
|
||||
dispatch(PanelActions.popPanel());
|
||||
}
|
||||
// if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
|
||||
// dispatch(PanelActions.popPanel());
|
||||
// }
|
||||
console.log('[PlayerPanel] Condition 4: Handling video error in fullscreen mode');
|
||||
}
|
||||
}, [
|
||||
broadcast?.type,
|
||||
@@ -2384,6 +2400,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
);
|
||||
Spotlight.pause();
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.log('[PlayerPanel] popPanel - VIDEO_END_ACTION_DELAY');
|
||||
Spotlight.resume();
|
||||
dispatch(PanelActions.popPanel());
|
||||
}, VIDEO_END_ACTION_DELAY);
|
||||
@@ -2955,7 +2972,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
disabled={panelInfo.modal}
|
||||
onEnded={onEnded}
|
||||
noAutoPlay={cannotPlay}
|
||||
autoCloseTimeout={6000}
|
||||
autoCloseTimeout={10000}
|
||||
onBackButton={onClickBack}
|
||||
spotlightDisabled={panelInfo.modal}
|
||||
isYoutube={isYoutube}
|
||||
|
||||
@@ -4,16 +4,22 @@ import { useDispatch } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
|
||||
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
||||
// <<<<<<< HEAD
|
||||
import { updatePanel } from '../../../../actions/panelActions';
|
||||
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config';
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MENU,
|
||||
LOG_MESSAGE_ID,
|
||||
panel_names,
|
||||
} from '../../../../utils/Config';
|
||||
import { $L } from '../../../../utils/helperMethods';
|
||||
import PlayerItemCard, { TYPES } from '../../PlayerItemCard/PlayerItemCard';
|
||||
import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents';
|
||||
import css from './LiveChannelContents.module.less';
|
||||
import cssV2 from './LiveChannelContents.v2.module.less';
|
||||
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
||||
|
||||
// =======
|
||||
// import { updatePanel } from "../../../../actions/panelActions";
|
||||
// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
|
||||
@@ -40,7 +46,7 @@ export default function LiveChannelContents({
|
||||
handleItemFocus,
|
||||
tabTitle,
|
||||
panelInfo,
|
||||
// <<<<<<< HEAD
|
||||
// <<<<<<< HEAD
|
||||
direction = 'vertical',
|
||||
version = 1,
|
||||
isFilteredByPatnr19,
|
||||
@@ -49,13 +55,13 @@ export default function LiveChannelContents({
|
||||
const isClickBlocked = useRef(false);
|
||||
const blockTimeoutRef = useRef(null);
|
||||
|
||||
// =======
|
||||
// isFilteredByPatnr19,
|
||||
// }) {
|
||||
// const dispatch = useDispatch();
|
||||
// const isClickBlocked = useRef(false);
|
||||
// =======
|
||||
// isFilteredByPatnr19,
|
||||
// }) {
|
||||
// const dispatch = useDispatch();
|
||||
// const isClickBlocked = useRef(false);
|
||||
const scrollToRef = useRef(null);
|
||||
// >>>>>>> gitlab/develop
|
||||
// >>>>>>> gitlab/develop
|
||||
const handleFocus = useCallback(
|
||||
() => () => {
|
||||
if (handleItemFocus) {
|
||||
@@ -181,15 +187,24 @@ export default function LiveChannelContents({
|
||||
startDt={strtDt}
|
||||
endDt={endDt}
|
||||
currentTime={currentTime}
|
||||
// <<<<<<< HEAD
|
||||
currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
|
||||
// <<<<<<< HEAD
|
||||
version={version}
|
||||
// =======
|
||||
// currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
|
||||
// >>>>>>> gitlab/develop
|
||||
// =======
|
||||
// currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
|
||||
// >>>>>>> gitlab/develop
|
||||
/>
|
||||
);
|
||||
},
|
||||
[liveInfos, currentTime, currentVideoShowId, isClickBlocked, dispatch, handleFocus, version]
|
||||
[
|
||||
liveInfos,
|
||||
currentTime,
|
||||
currentVideoShowId,
|
||||
isClickBlocked,
|
||||
dispatch,
|
||||
handleFocus,
|
||||
version,
|
||||
]
|
||||
);
|
||||
|
||||
const containerClass = version === 2 ? cssV2.container : css.container;
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function ShopNowContents({
|
||||
|
||||
// ===== navigateToDetail 방식 (handleYouMayLikeItemClick 참고) =====
|
||||
console.log(
|
||||
"[ShopNowContents] ShopNow DetailPanel 진입 - sourceMenu:",
|
||||
"[DetailPanel-BG][ShopNowContents] ShopNow DetailPanel 진입 - sourceMenu:",
|
||||
SOURCE_MENUS.PLAYER_SHOP_NOW
|
||||
);
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { compose } from 'ramda/src/compose';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import {
|
||||
Marquee,
|
||||
MarqueeController,
|
||||
} from '@enact/ui/Marquee';
|
||||
import { Marquee, MarqueeController } from '@enact/ui/Marquee';
|
||||
|
||||
import icon_arrow_dwon
|
||||
from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import { SpotlightIds } from '../../../../utils/SpotlightIds';
|
||||
import css from './LiveChannelNext.module.less';
|
||||
@@ -55,9 +52,19 @@ export default function LiveChannelNext({
|
||||
onSpotlightRight={handleSpotlightRight}
|
||||
>
|
||||
<div className={css.logoWrapper}>
|
||||
<div className={css.logoBackground} style={{ background: backgroundColor }}>
|
||||
<div
|
||||
className={css.logoBackground}
|
||||
style={{ background: backgroundColor }}
|
||||
>
|
||||
{channelLogo ? (
|
||||
<CustomImage src={channelLogo} alt={channelName} className={css.logoImage} />
|
||||
<CustomImage
|
||||
src={channelLogo}
|
||||
alt={channelName}
|
||||
className={classNames(
|
||||
css.logoImage,
|
||||
channelName === 'QVC' && css.qvcLogoImg
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className={css.logoPlaceholder} />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../../../style/CommonStyle.module.less";
|
||||
@import "../../../../style/utils.module.less";
|
||||
@import '../../../../style/CommonStyle.module.less';
|
||||
@import '../../../../style/utils.module.less';
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
@@ -51,13 +51,16 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, #284998 0%, #06B0EE 100%);
|
||||
}
|
||||
|
||||
.logoImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
&.qvcLogoImg {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.logoPlaceholder {
|
||||
@@ -69,7 +72,7 @@
|
||||
.channelName {
|
||||
color: #fcfcfc;
|
||||
font-size: 25px;
|
||||
font-family: "LG Smart UI";
|
||||
font-family: 'LG Smart UI';
|
||||
font-weight: 700;
|
||||
line-height: 35px;
|
||||
white-space: nowrap;
|
||||
@@ -81,7 +84,7 @@
|
||||
.programName {
|
||||
color: rgba(234, 234, 234, 0.7);
|
||||
font-size: 25px;
|
||||
font-family: "LG Smart UI";
|
||||
font-family: 'LG Smart UI';
|
||||
font-weight: 600;
|
||||
line-height: 35px;
|
||||
white-space: nowrap;
|
||||
@@ -91,7 +94,6 @@
|
||||
max-width: 180px; // 최대 너비 제한 완화
|
||||
}
|
||||
|
||||
|
||||
.arrowIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -7,27 +7,28 @@ import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
// import icon_arrow_right from '../../../../../assets/images/icons';
|
||||
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import { LOG_MENU } from '../../../../utils/Config';
|
||||
import { createDebugHelpers } from '../../../../utils/debug';
|
||||
import { $L } from '../../../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../../../utils/SpotlightIds';
|
||||
import FeaturedShowContents from '../TabContents/FeaturedShowContents';
|
||||
import LiveChannelContents from '../TabContents/LiveChannelContents';
|
||||
import ShopNowContents from '../TabContents/ShopNowContents';
|
||||
import LiveChannelNext from './LiveChannelNext';
|
||||
import ShopNowButton from './ShopNowButton';
|
||||
import css from './TabContainer.v2.module.less';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// import icon_arrow_right from '../../../../../assets/images/icons';
|
||||
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
|
||||
import icon_shop_now from '../../../../../assets/images/player/icon_tabcontainer_shopnow.png';
|
||||
import { LOG_MENU } from '../../../../utils/Config';
|
||||
import { $L } from '../../../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../../../utils/SpotlightIds';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import LiveChannelContents from '../TabContents/LiveChannelContents';
|
||||
import FeaturedShowContents from '../TabContents/FeaturedShowContents';
|
||||
import ShopNowContents from '../TabContents/ShopNowContents';
|
||||
import ShopNowButton from './ShopNowButton';
|
||||
import LiveChannelNext from './LiveChannelNext';
|
||||
import css from './TabContainer.v2.module.less';
|
||||
|
||||
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: 'last-focused' },
|
||||
'div'
|
||||
);
|
||||
|
||||
const SpottableDiv = Spottable('div');
|
||||
|
||||
@@ -50,18 +51,22 @@ export default function TabContainerV2({
|
||||
onTabClose, // 탭 닫기 콜백 함수
|
||||
tabVisible,
|
||||
}) {
|
||||
const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos);
|
||||
const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos);
|
||||
|
||||
// 다음 재생 가능한 쇼 찾기
|
||||
const findNextPlayableShow = useCallback((currentPlayList, currentIndex) => {
|
||||
if (!currentPlayList || currentPlayList.length === 0) return null;
|
||||
|
||||
let nextIndex = currentIndex === currentPlayList.length - 1 ? 0 : currentIndex + 1;
|
||||
let nextIndex =
|
||||
currentIndex === currentPlayList.length - 1 ? 0 : currentIndex + 1;
|
||||
let initialIndex = nextIndex;
|
||||
let attempts = 0;
|
||||
|
||||
// 유효한 showId를 가진 다음 쇼 찾기
|
||||
while (!currentPlayList[nextIndex]?.showId && attempts < currentPlayList.length) {
|
||||
while (
|
||||
!currentPlayList[nextIndex]?.showId &&
|
||||
attempts < currentPlayList.length
|
||||
) {
|
||||
nextIndex = nextIndex === currentPlayList.length - 1 ? 0 : nextIndex + 1;
|
||||
attempts++;
|
||||
if (nextIndex === initialIndex) break;
|
||||
@@ -87,13 +92,18 @@ export default function TabContainerV2({
|
||||
data: youmaylikeInfos,
|
||||
shopNowInfo_length: shopNowInfo?.length,
|
||||
shouldShowYouMayAlso:
|
||||
shopNowInfo && shopNowInfo.length < 3 && youmaylikeInfos && youmaylikeInfos.length > 0,
|
||||
shopNowInfo &&
|
||||
shopNowInfo.length < 3 &&
|
||||
youmaylikeInfos &&
|
||||
youmaylikeInfos.length > 0,
|
||||
});
|
||||
}, [youmaylikeInfos, shopNowInfo]);
|
||||
|
||||
const tabList = [
|
||||
$L('SHOP NOW'),
|
||||
panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'),
|
||||
panelInfo?.shptmBanrTpNm === 'LIVE'
|
||||
? $L('LIVE CHANNEL')
|
||||
: $L('FEATURED SHOWS'),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,7 +115,9 @@ export default function TabContainerV2({
|
||||
|
||||
if (tabIndex === 1) {
|
||||
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE';
|
||||
nowMenu = isLive ? LOG_MENU.FULL_LIVE_CHANNELS : LOG_MENU.FULL_FEATURED_SHOWS;
|
||||
nowMenu = isLive
|
||||
? LOG_MENU.FULL_LIVE_CHANNELS
|
||||
: LOG_MENU.FULL_FEATURED_SHOWS;
|
||||
}
|
||||
|
||||
if (nowMenu) {
|
||||
@@ -160,7 +172,9 @@ export default function TabContainerV2({
|
||||
|
||||
// 하나의 함수에서 모든 tabIndex 변화 처리
|
||||
const handleTabIndexChange = useCallback((newTabIndex, oldTabIndex) => {
|
||||
console.log(`[TabIndexChange] Tab changed from ${oldTabIndex} to ${newTabIndex}`);
|
||||
console.log(
|
||||
`[TabIndexChange] Tab changed from ${oldTabIndex} to ${newTabIndex}`
|
||||
);
|
||||
|
||||
if (newTabIndex === 0) {
|
||||
// tabIndex = 0 (ShopNow)
|
||||
@@ -220,8 +234,18 @@ export default function TabContainerV2({
|
||||
Spotlight.focus('shop-now-item-0');
|
||||
}}
|
||||
>
|
||||
<div className={css.shopNowIconWrapper}>
|
||||
<img src={icon_shop_now} alt="shop now icon" className={css.shopNowIcon} />
|
||||
<div
|
||||
className={classNames(
|
||||
css.shopNowIconWrapper,
|
||||
playListInfo[selectedIndex]?.patncNm === 'QVC' &&
|
||||
css.shopNowQvcIconWrapper
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={playListInfo[selectedIndex]?.patncLogoPath}
|
||||
alt="shop now icon"
|
||||
className={css.shopNowIcon}
|
||||
/>
|
||||
</div>
|
||||
<div className={css.shopNowHeaderText}>SHOP NOW</div>
|
||||
<div className={css.arrowIcon}>
|
||||
@@ -250,7 +274,9 @@ export default function TabContainerV2({
|
||||
youmaylikeInfos &&
|
||||
youmaylikeInfos.length > 0 && (
|
||||
<div className={css.youMayAlsoLikeHeader}>
|
||||
<div className={css.youMayAlsoLikeText}>You may also like</div>
|
||||
<div className={css.youMayAlsoLikeText}>
|
||||
You may also like
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -273,7 +299,11 @@ export default function TabContainerV2({
|
||||
<SpottableDiv
|
||||
className={css.liveChannelButton}
|
||||
onClick={onLiveChannelButtonClick}
|
||||
spotlightId={panelInfo?.shptmBanrTpNm === 'LIVE' ? 'below-tab-live-channel-button' : 'below-tab-featured-show-button'}
|
||||
spotlightId={
|
||||
panelInfo?.shptmBanrTpNm === 'LIVE'
|
||||
? 'below-tab-live-channel-button'
|
||||
: 'below-tab-featured-show-button'
|
||||
}
|
||||
onSpotlightUp={handleSpotlightUpToBackButton}
|
||||
onSpotlightDown={(e) => {
|
||||
// 첫 번째 PlayerItem으로 포커스 이동
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../../../style/CommonStyle.module.less";
|
||||
@import "../../../../style/utils.module.less";
|
||||
@import '../../../../style/CommonStyle.module.less';
|
||||
@import '../../../../style/utils.module.less';
|
||||
|
||||
.tabContainer {
|
||||
.position(@position: fixed, @bottom: 0, @left: 0);
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
.position(@position: absolute, @top: 0, @left: 0);
|
||||
.size(@w: 100%, @h: 100%);
|
||||
background: linear-gradient(
|
||||
@@ -113,10 +113,17 @@
|
||||
background: white;
|
||||
border-radius: 100px;
|
||||
.flex(@display: flex, @justifyCenter: center, @alignCenter: center);
|
||||
&.shopNowQvcIconWrapper {
|
||||
border-radius: 0;
|
||||
.shopNowIcon {
|
||||
.size(@w: 40px, @h: 40px);
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shopNowIcon {
|
||||
.size(@w: 20.67px, @h: 20.67px);
|
||||
.size(@w: 40px, @h: 40px);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -265,7 +272,7 @@
|
||||
|
||||
.youMayAlsoLikeText {
|
||||
margin-right: 15px;
|
||||
color: #EAEAEA;
|
||||
color: #eaeaea;
|
||||
font-size: 24px;
|
||||
font-family: @baseFont;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #4a4c50 !important; // 기본 배경색과 동일
|
||||
background: #4F172C !important; // 기본 배경색과 동일
|
||||
border-radius: 100px;
|
||||
border: 1px solid #585858 !important;
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in New Issue
Block a user