Compare commits
16 Commits
develop_si
...
cdf0d3de04
| Author | SHA1 | Date | |
|---|---|---|---|
| cdf0d3de04 | |||
| 9ff6064bc9 | |||
| cfee554bf6 | |||
| a5fbb21d43 | |||
| 57cc6dbf20 | |||
| 74d2b827b0 | |||
| 9439630bad | |||
|
|
0a2ef0e68b | ||
| 96cbd1f67e | |||
| e8464b98b6 | |||
| 4904c6fb58 | |||
|
|
1c9db184fa | ||
| 3add749c07 | |||
| 3c3662f791 | |||
| 42eda7e0bb | |||
| d795182d4c |
2
com.twin.app.shoptime/.gitignore
vendored
2
com.twin.app.shoptime/.gitignore
vendored
@@ -22,3 +22,5 @@ nul
|
|||||||
OPTIMAL.md
|
OPTIMAL.md
|
||||||
.docs
|
.docs
|
||||||
|
|
||||||
|
GEMINI.md
|
||||||
|
|
||||||
|
|||||||
BIN
com.twin.app.shoptime/assets/images/featuredBrands/image-bg.png
Normal file
BIN
com.twin.app.shoptime/assets/images/featuredBrands/image-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
15
com.twin.app.shoptime/assets/images/featuredBrands/nbcu.svg
Normal file
15
com.twin.app.shoptime/assets/images/featuredBrands/nbcu.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 346 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -18,7 +18,6 @@ import { ThemeDecorator } from '@enact/sandstone/ThemeDecorator';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
changeAppStatus,
|
changeAppStatus,
|
||||||
changeLocalSettings,
|
|
||||||
// cancelFocusElement,
|
// cancelFocusElement,
|
||||||
// focusElement,
|
// focusElement,
|
||||||
// setExitApp,
|
// setExitApp,
|
||||||
@@ -45,7 +44,7 @@ import { pushPanel } from '../actions/panelActions';
|
|||||||
import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
||||||
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
||||||
import ToastContainer from '../components/TToast/ToastContainer';
|
import ToastContainer from '../components/TToast/ToastContainer';
|
||||||
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
|
|
||||||
import usePrevious from '../hooks/usePrevious';
|
import usePrevious from '../hooks/usePrevious';
|
||||||
import { lunaTest } from '../lunaSend/lunaTest';
|
import { lunaTest } from '../lunaSend/lunaTest';
|
||||||
import { store } from '../store/store';
|
import { store } from '../store/store';
|
||||||
@@ -280,7 +279,7 @@ const originFocus = Spotlight.focus;
|
|||||||
const originMove = Spotlight.move;
|
const originMove = Spotlight.move;
|
||||||
const originSilentlyFocus = Spotlight.silentlyFocus;
|
const originSilentlyFocus = Spotlight.silentlyFocus;
|
||||||
let lastLoggedSpotlightId = null;
|
let lastLoggedSpotlightId = null;
|
||||||
let lastLoggedBlurSpotlightId = null;
|
let lastLoggedBlurSpotlightId = null; // eslint-disable-line no-unused-vars
|
||||||
let focusLoggingSuppressed = 0;
|
let focusLoggingSuppressed = 0;
|
||||||
|
|
||||||
const resolveSpotlightIdFromNode = (node) => {
|
const resolveSpotlightIdFromNode = (node) => {
|
||||||
@@ -407,28 +406,7 @@ Spotlight.silentlyFocus = function (...args) {
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveSpotlightIdFromEvent = (event) => {
|
|
||||||
if (!event) return undefined;
|
|
||||||
const { detail, target } = event;
|
|
||||||
|
|
||||||
if (detail) {
|
|
||||||
if (detail.spotlightId) {
|
|
||||||
return detail.spotlightId;
|
|
||||||
}
|
|
||||||
if (detail.id) {
|
|
||||||
return detail.id;
|
|
||||||
}
|
|
||||||
if (detail.target && detail.target.dataset && detail.target.dataset.spotlightId) {
|
|
||||||
return detail.target.dataset.spotlightId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target && target.dataset && target.dataset.spotlightId) {
|
|
||||||
return target.dataset.spotlightId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spotlight Focus 추적 로그 [251115]
|
// Spotlight Focus 추적 로그 [251115]
|
||||||
// DOM 이벤트 리스너로 대체
|
// DOM 이벤트 리스너로 대체
|
||||||
@@ -448,7 +426,7 @@ const resolveSpotlightIdFromEvent = (event) => {
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function AppBase(props) {
|
function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const httpHeader = useSelector((state) => state.common.httpHeader);
|
const httpHeader = useSelector((state) => state.common.httpHeader);
|
||||||
const httpHeaderRef = useRef(httpHeader);
|
const httpHeaderRef = useRef(httpHeader);
|
||||||
@@ -650,7 +628,7 @@ function AppBase(props) {
|
|||||||
clearLaunchParams();
|
clearLaunchParams();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRelaunchEvent = useCallback(() => {
|
const handleRelaunchEvent = useCallback(() => {
|
||||||
@@ -704,7 +682,7 @@ function AppBase(props) {
|
|||||||
if (typeof window === 'object' && window.PalmSystem) {
|
if (typeof window === 'object' && window.PalmSystem) {
|
||||||
window.PalmSystem.activate();
|
window.PalmSystem.activate();
|
||||||
}
|
}
|
||||||
}, [initService, introTermsAgreeRef, dispatch]);
|
}, [initService, introTermsAgreeRef]);
|
||||||
|
|
||||||
const visibilityChanged = useCallback(() => {
|
const visibilityChanged = useCallback(() => {
|
||||||
// console.log('document is hidden', document.hidden);
|
// console.log('document is hidden', document.hidden);
|
||||||
@@ -748,7 +726,7 @@ function AppBase(props) {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyDownEvent = (event) => {
|
const keyDownEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||||
dispatch(changeAppStatus({ cursorVisible: false }));
|
dispatch(changeAppStatus({ cursorVisible: false }));
|
||||||
Spotlight.setPointerMode(false);
|
Spotlight.setPointerMode(false);
|
||||||
};
|
};
|
||||||
@@ -757,7 +735,7 @@ function AppBase(props) {
|
|||||||
let lastMoveTime = 0;
|
let lastMoveTime = 0;
|
||||||
const THROTTLE_MS = 100;
|
const THROTTLE_MS = 100;
|
||||||
|
|
||||||
const mouseMoveEvent = (event) => {
|
const mouseMoveEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastMoveTime < THROTTLE_MS) {
|
if (now - lastMoveTime < THROTTLE_MS) {
|
||||||
// throttle 기간 내에는 hideCursor만 재시작
|
// throttle 기간 내에는 hideCursor만 재시작
|
||||||
@@ -810,9 +788,7 @@ function AppBase(props) {
|
|||||||
let userDataChanged = false;
|
let userDataChanged = false;
|
||||||
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
|
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
|
||||||
userDataChanged = true;
|
userDataChanged = true;
|
||||||
}
|
} else if (userDataChanged || httpHeaderRef.current === null) {
|
||||||
if (!httpHeader || !deviceId) {
|
|
||||||
} else if (userDataChanged || httpHeaderRef.current === null) {
|
|
||||||
//계정정보 변경시 또는 초기 로딩시
|
//계정정보 변경시 또는 초기 로딩시
|
||||||
if (!httpHeader) {
|
if (!httpHeader) {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { updateHomeInfo } from "../actions/homeActions";
|
import { updateHomeInfo } from "../actions/homeActions";
|
||||||
import { pushPanel } from "../actions/panelActions";
|
import { pushPanel } from "../actions/panelActions";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,7 @@ import { SpotlightIds } from "../utils/SpotlightIds";
|
|||||||
import { sendLogTotalRecommend } from "../actions/logActions";
|
import { sendLogTotalRecommend } from "../actions/logActions";
|
||||||
|
|
||||||
//V2_진입경로코드_진입경로명_MT_노출순번
|
//V2_진입경로코드_진입경로명_MT_노출순번
|
||||||
export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
|
||||||
console.log("[handleDeepLink] ~ contentTarget: ", contentTarget);
|
console.log("[handleDeepLink] ~ contentTarget: ", contentTarget);
|
||||||
let linkTpCd; // 진입경로코드
|
let linkTpCd; // 진입경로코드
|
||||||
let linkTpNm; // 진입경로명
|
let linkTpNm; // 진입경로명
|
||||||
@@ -21,7 +21,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
let curationId; // 큐레이션아이디
|
let curationId; // 큐레이션아이디
|
||||||
let showId; // 방송아이디
|
let showId; // 방송아이디
|
||||||
let chanId; // 채널아이디
|
let chanId; // 채널아이디
|
||||||
let expsOrd; // 노출순번
|
|
||||||
let grNumber; // 그룹번호
|
let grNumber; // 그룹번호
|
||||||
let evntId; // 이벤트아이디
|
let evntId; // 이벤트아이디
|
||||||
let lgCatCd; // LG카테고리Code
|
let lgCatCd; // LG카테고리Code
|
||||||
@@ -65,7 +64,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
// V3_진입경로코드_진입경로명_PD_파트너아이디_상품아이디_노출순번_큐레이션아이디
|
// V3_진입경로코드_진입경로명_PD_파트너아이디_상품아이디_노출순번_큐레이션아이디
|
||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
prdtId = tokens[5]; // 상품아이디
|
prdtId = tokens[5]; // 상품아이디
|
||||||
expsOrd = tokens[6]; // 노출순번
|
|
||||||
curationId = tokens[7]; // 큐레이션아이디
|
curationId = tokens[7]; // 큐레이션아이디
|
||||||
panelName = panel_names.DETAIL_PANEL;
|
panelName = panel_names.DETAIL_PANEL;
|
||||||
deeplinkPanel = "Product Detaoil";
|
deeplinkPanel = "Product Detaoil";
|
||||||
@@ -81,7 +79,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
// V3_진입경로코드_진입경로명_LS_파트너아이디_채널아이디_노출순번_큐레이션아이디
|
// V3_진입경로코드_진입경로명_LS_파트너아이디_채널아이디_노출순번_큐레이션아이디
|
||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
chanId = tokens[5]; // 채널아이디
|
chanId = tokens[5]; // 채널아이디
|
||||||
expsOrd = tokens[6]; // 노출순번
|
|
||||||
curationId = tokens[7]; // 큐레이션아이디
|
curationId = tokens[7]; // 큐레이션아이디
|
||||||
panelName = panel_names.PLAYER_PANEL;
|
panelName = panel_names.PLAYER_PANEL;
|
||||||
deeplinkPanel = "Live Show";
|
deeplinkPanel = "Live Show";
|
||||||
@@ -98,7 +95,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
// V3_진입경로코드_진입경로명_VS_파트너아이디_방송아이디_노출순번_큐레이션아이디
|
// V3_진입경로코드_진입경로명_VS_파트너아이디_방송아이디_노출순번_큐레이션아이디
|
||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
showId = tokens[5]; // 방송아이디
|
showId = tokens[5]; // 방송아이디
|
||||||
expsOrd = tokens[6]; // 노출순번
|
|
||||||
curationId = tokens[7]; // 큐레이션아이디
|
curationId = tokens[7]; // 큐레이션아이디
|
||||||
panelName = panel_names.PLAYER_PANEL;
|
panelName = panel_names.PLAYER_PANEL;
|
||||||
deeplinkPanel = "VOD Show";
|
deeplinkPanel = "VOD Show";
|
||||||
@@ -119,7 +115,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
curationId = tokens[5]; // 큐레이션아이디\
|
curationId = tokens[5]; // 큐레이션아이디\
|
||||||
prdtId = tokens[6]; // 상품아이디
|
prdtId = tokens[6]; // 상품아이디
|
||||||
expsOrd = tokens[7]; // 노출순번
|
|
||||||
grNumber = tokens[8]; // 그룹번호
|
grNumber = tokens[8]; // 그룹번호
|
||||||
panelName = panel_names.DETAIL_PANEL;
|
panelName = panel_names.DETAIL_PANEL;
|
||||||
deeplinkPanel = "Theme Detail";
|
deeplinkPanel = "Theme Detail";
|
||||||
@@ -140,7 +135,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
|
|
||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
curationId = tokens[5]; // 큐레이션아이디
|
curationId = tokens[5]; // 큐레이션아이디
|
||||||
expsOrd = tokens[6]; // 노출순번
|
|
||||||
panelName = panel_names.DETAIL_PANEL;
|
panelName = panel_names.DETAIL_PANEL;
|
||||||
deeplinkPanel = "Hotel Detail";
|
deeplinkPanel = "Hotel Detail";
|
||||||
panelInfo = {
|
panelInfo = {
|
||||||
@@ -157,7 +151,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
|||||||
|
|
||||||
patnrId = tokens[4]; // 파트너아이디
|
patnrId = tokens[4]; // 파트너아이디
|
||||||
curationId = tokens[5]; // 큐레이션아이디
|
curationId = tokens[5]; // 큐레이션아이디
|
||||||
expsOrd = tokens[6]; // 노출순번
|
|
||||||
panelName = panel_names.HOT_PICKS_PANEL;
|
panelName = panel_names.HOT_PICKS_PANEL;
|
||||||
deeplinkPanel = "Hot Picks";
|
deeplinkPanel = "Hot Picks";
|
||||||
panelInfo = {
|
panelInfo = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
export const addMainIndex = (index) => ({
|
export const addMainIndex = (index) => ({
|
||||||
type: types.ADD_MAIN_INDEX,
|
type: types.ADD_MAIN_INDEX,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// IF-LGSP-328 : 회원 Billing Address 조회
|
// IF-LGSP-328 : 회원 Billing Address 조회
|
||||||
export const getMyInfoBillingSearch = (props) => (dispatch, getState) => {
|
export const getMyInfoBillingSearch = (props) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// Featured Brands 정보 조회 IF-LGSP-304
|
// Featured Brands 정보 조회 IF-LGSP-304
|
||||||
export const getBrandList = () => (dispatch, getState) => {
|
export const getBrandList = () => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// 회원 주문 취소/반품/교환 사유 조회 (IF-LGSP-347)
|
// 회원 주문 취소/반품/교환 사유 조회 (IF-LGSP-347)
|
||||||
export const getMyinfoOrderCancelColumnsSearch = (params, callback) => (dispatch, getState) => {
|
export const getMyinfoOrderCancelColumnsSearch = (params, callback) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// 회원의 등록 카드 정보 조회 IF-LGSP-332
|
// 회원의 등록 카드 정보 조회 IF-LGSP-332
|
||||||
export const getMyInfoCardSearch = (props) => (dispatch, getState) => {
|
export const getMyInfoCardSearch = (props) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// 회원 체크아웃 정보 조회 IF-LGSP-345
|
// 회원 체크아웃 정보 조회 IF-LGSP-345
|
||||||
export const getMyInfoCheckoutInfo = (props, callback) => (dispatch, getState) => {
|
export const getMyInfoCheckoutInfo = (props, callback) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import Spotlight from '@enact/spotlight';
|
|||||||
import appinfo from '../../webos-meta/appinfo.json';
|
import appinfo from '../../webos-meta/appinfo.json';
|
||||||
import appinfo35 from '../../webos-meta/appinfo35.json';
|
import appinfo35 from '../../webos-meta/appinfo35.json';
|
||||||
import appinfo79 from '../../webos-meta/appinfo79.json';
|
import appinfo79 from '../../webos-meta/appinfo79.json';
|
||||||
import { handleBypassLink } from '../App/bypassLinkHandler';
|
|
||||||
import * as lunaSend from '../lunaSend';
|
import * as lunaSend from '../lunaSend';
|
||||||
import { initialLocalSettings } from '../reducers/localSettingsReducer';
|
|
||||||
import * as Config from '../utils/Config';
|
import * as Config from '../utils/Config';
|
||||||
import * as HelperMethods from '../utils/helperMethods';
|
import * as HelperMethods from '../utils/helperMethods';
|
||||||
import { types } from './actionTypes';
|
import { types } from './actionTypes';
|
||||||
@@ -17,7 +15,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
// =======
|
// =======
|
||||||
// import appinfo from "../../webos-meta/appinfo.json";
|
// import appinfo from "../../webos-meta/appinfo.json";
|
||||||
// import appinfo35 from "../../webos-meta/appinfo35.json";
|
// import appinfo35 from "../../webos-meta/appinfo35.json";
|
||||||
@@ -94,7 +92,7 @@ export const toggleOptionalTermsConfirm = (selected) => ({
|
|||||||
payload: selected,
|
payload: selected,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setExitApp = () => (dispatch, getState) => {
|
export const setExitApp = () => (dispatch) => {
|
||||||
dispatch({ type: types.SET_EXIT_APP });
|
dispatch({ type: types.SET_EXIT_APP });
|
||||||
|
|
||||||
dlog('Exiting App...');
|
dlog('Exiting App...');
|
||||||
@@ -116,7 +114,7 @@ export const loadingComplete = (status) => ({
|
|||||||
payload: status,
|
payload: status,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const alertToast = (payload) => (dispatch, getState) => {
|
export const alertToast = (payload) => (dispatch) => {
|
||||||
if (typeof window === 'object' && !window.PalmSystem) {
|
if (typeof window === 'object' && !window.PalmSystem) {
|
||||||
dispatch(changeAppStatus({ toast: true, toastText: payload }));
|
dispatch(changeAppStatus({ toast: true, toastText: payload }));
|
||||||
} else {
|
} else {
|
||||||
@@ -124,13 +122,13 @@ export const alertToast = (payload) => (dispatch, getState) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSystemSettings = () => (dispatch, getState) => {
|
export const getSystemSettings = () => (dispatch) => {
|
||||||
dlog('getSystemSettings ');
|
dlog('getSystemSettings ');
|
||||||
lunaSend.getSystemSettings(
|
lunaSend.getSystemSettings(
|
||||||
{ category: 'caption', keys: ['captionEnable'] },
|
{ category: 'caption', keys: ['captionEnable'] },
|
||||||
{
|
{
|
||||||
onSuccess: (res) => {},
|
onSuccess: () => {},
|
||||||
onFailure: (err) => {},
|
onFailure: () => {},
|
||||||
onComplete: (res) => {
|
onComplete: (res) => {
|
||||||
dlog('getSystemSettings onComplete', res);
|
dlog('getSystemSettings onComplete', res);
|
||||||
if (res && res.settings) {
|
if (res && res.settings) {
|
||||||
@@ -148,7 +146,7 @@ export const getSystemSettings = () => (dispatch, getState) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getState) => {
|
export const getHttpHeaderForServiceRequest = () => (dispatch, getState) => {
|
||||||
dlog('getHttpHeaderForServiceRequest ');
|
dlog('getHttpHeaderForServiceRequest ');
|
||||||
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
|
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
|
||||||
lunaSend.getHttpHeaderForServiceRequest({
|
lunaSend.getHttpHeaderForServiceRequest({
|
||||||
@@ -267,10 +265,9 @@ export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getStat
|
|||||||
const mbrNo = res['X-User-Number'];
|
const mbrNo = res['X-User-Number'];
|
||||||
|
|
||||||
lunaSend.getLoginUserData(parameters, {
|
lunaSend.getLoginUserData(parameters, {
|
||||||
onSuccess: (res) => {
|
onSuccess: (loginRes) => {
|
||||||
const userId = res.id ?? '';
|
const userId = loginRes.id ?? '';
|
||||||
const userNumber = res.lastSignInUserNo;
|
const profileNick = loginRes.profileNick || userId.split('@')[0];
|
||||||
const profileNick = res.profileNick || userId.split('@')[0];
|
|
||||||
dispatch(
|
dispatch(
|
||||||
getLoginUserData({
|
getLoginUserData({
|
||||||
userId,
|
userId,
|
||||||
@@ -288,7 +285,7 @@ export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getStat
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDeviceId = (onComplete) => (dispatch, getState) => {
|
export const getDeviceId = (onComplete) => (dispatch) => {
|
||||||
lunaSend.getDeviceId(
|
lunaSend.getDeviceId(
|
||||||
{ idType: ['LGUDID'] },
|
{ idType: ['LGUDID'] },
|
||||||
{
|
{
|
||||||
@@ -466,7 +463,7 @@ export const setFocus = (spotlightId) => ({
|
|||||||
payload: spotlightId,
|
payload: spotlightId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const focusElement = (spotlightId) => (dispatch, getState) => {
|
export const focusElement = (spotlightId) => (dispatch) => {
|
||||||
dispatch(setFocus(spotlightId));
|
dispatch(setFocus(spotlightId));
|
||||||
|
|
||||||
if (typeof window === 'object') {
|
if (typeof window === 'object') {
|
||||||
@@ -488,7 +485,7 @@ export const cancelFocusElement = () => () => {
|
|||||||
let broadcastTimer = null;
|
let broadcastTimer = null;
|
||||||
export const sendBroadCast =
|
export const sendBroadCast =
|
||||||
({ type, moreInfo }) =>
|
({ type, moreInfo }) =>
|
||||||
(dispatch, getState) => {
|
(dispatch) => {
|
||||||
clearTimeout(broadcastTimer);
|
clearTimeout(broadcastTimer);
|
||||||
dispatch(changeBroadcastEvent({ type, moreInfo }));
|
dispatch(changeBroadcastEvent({ type, moreInfo }));
|
||||||
broadcastTimer = setTimeout(() => {
|
broadcastTimer = setTimeout(() => {
|
||||||
@@ -545,7 +542,7 @@ export const addReservation = (data) => (dispatch) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteReservationCallback = (scheduleIdList) => (dispatch) => {
|
export const deleteReservationCallback = (scheduleIdList) => () => {
|
||||||
lunaSend.deleteReservationCallback(scheduleIdList, {
|
lunaSend.deleteReservationCallback(scheduleIdList, {
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
// dispatch(alertToast("success" + JSON.stringify(res)));
|
// dispatch(alertToast("success" + JSON.stringify(res)));
|
||||||
@@ -636,8 +633,8 @@ export const showError =
|
|||||||
export const deleteOldDb8Datas = () => (dispatch) => {
|
export const deleteOldDb8Datas = () => (dispatch) => {
|
||||||
for (let i = 1; i < 10; i++) {
|
for (let i = 1; i < 10; i++) {
|
||||||
lunaSend.deleteOldDb8(i, {
|
lunaSend.deleteOldDb8(i, {
|
||||||
onSuccess: (res) => {},
|
onSuccess: () => {},
|
||||||
onFailure: (err) => {},
|
onFailure: () => {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dispatch(changeLocalSettings({ oldDb8Deleted: true }));
|
dispatch(changeLocalSettings({ oldDb8Deleted: true }));
|
||||||
@@ -683,7 +680,7 @@ let updateNetworkStateJob = new Job((dispatch, connected) => {
|
|||||||
dispatch(changeAppStatus({ isInternetConnected: connected }));
|
dispatch(changeAppStatus({ isInternetConnected: connected }));
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getConnectionStatus = () => (dispatch, getState) => {
|
export const getConnectionStatus = () => (dispatch) => {
|
||||||
lunaSend.getConnectionStatus({
|
lunaSend.getConnectionStatus({
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
dlog('lunasend getConnectionStatus', res);
|
dlog('lunasend getConnectionStatus', res);
|
||||||
@@ -712,7 +709,7 @@ export const getConnectionStatus = () => (dispatch, getState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// macAddress
|
// macAddress
|
||||||
export const getConnectionInfo = () => (dispatch, getState) => {
|
export const getConnectionInfo = () => (dispatch) => {
|
||||||
lunaSend.getConnectionInfo({
|
lunaSend.getConnectionInfo({
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
dlog('lunasend getConnectionStatus', res);
|
dlog('lunasend getConnectionStatus', res);
|
||||||
@@ -734,7 +731,7 @@ export const getConnectionInfo = () => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const disableNotification = () => (dispatch, getState) => {
|
export const disableNotification = () => {
|
||||||
lunaSend.disableNotification({
|
lunaSend.disableNotification({
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
dlog('lunasend disable notification success', res);
|
dlog('lunasend disable notification success', res);
|
||||||
@@ -748,7 +745,7 @@ export const disableNotification = () => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enableNotification = () => (dispatch, getState) => {
|
export const enableNotification = () => {
|
||||||
lunaSend.enableNotification({
|
lunaSend.enableNotification({
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
dlog('lunasend enable notification success', res);
|
dlog('lunasend enable notification success', res);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const convertPdfToImage =
|
|||||||
const timeoutError = new Error(
|
const timeoutError = new Error(
|
||||||
`Conversion timeout after ${timeout}ms (attempt ${attempts})`
|
`Conversion timeout after ${timeout}ms (attempt ${attempts})`
|
||||||
);
|
);
|
||||||
dwarn(`⏱️ [EnergyLabel] Timeout on attempt ${attempts}:`, timeoutError.message);
|
void dwarn(`⏱️ [EnergyLabel] Timeout on attempt ${attempts}:`, timeoutError.message);
|
||||||
|
|
||||||
// 재시도 가능한 경우
|
// 재시도 가능한 경우
|
||||||
if (attempts < maxRetries + 1) {
|
if (attempts < maxRetries + 1) {
|
||||||
@@ -39,7 +39,7 @@ export const convertPdfToImage =
|
|||||||
attemptConversion();
|
attemptConversion();
|
||||||
} else {
|
} else {
|
||||||
// 최종 실패
|
// 최종 실패
|
||||||
derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts:`, pdfUrl);
|
void derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts:`, pdfUrl);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
||||||
payload: { pdfUrl, error: timeoutError },
|
payload: { pdfUrl, error: timeoutError },
|
||||||
@@ -64,17 +64,14 @@ export const convertPdfToImage =
|
|||||||
|
|
||||||
if (retCode !== undefined && retCode !== 0 && retCode !== '0') {
|
if (retCode !== undefined && retCode !== 0 && retCode !== '0') {
|
||||||
const error = new Error(`API Error: retCode=${retCode}`);
|
const error = new Error(`API Error: retCode=${retCode}`);
|
||||||
dwarn(`⚠️ [EnergyLabel] API returned error on attempt ${attempts}:`, retCode);
|
void dwarn(`⚠️ [EnergyLabel] API returned error on attempt ${attempts}:`, retCode);
|
||||||
|
|
||||||
// retCode 에러도 재시도
|
// retCode 에러도 재시도
|
||||||
if (attempts < maxRetries + 1) {
|
if (attempts < maxRetries + 1) {
|
||||||
dlog(`🔄 [EnergyLabel] Retrying due to API error... (${attempts}/${maxRetries + 1})`);
|
void dlog(`🔄 [EnergyLabel] Retrying due to API error... (${attempts}/${maxRetries + 1})`);
|
||||||
attemptConversion();
|
attemptConversion();
|
||||||
} else {
|
} else {
|
||||||
derror(
|
void derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts (API error):`, pdfUrl);
|
||||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (API error):`,
|
|
||||||
pdfUrl
|
|
||||||
);
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
||||||
payload: { pdfUrl, error },
|
payload: { pdfUrl, error },
|
||||||
@@ -111,7 +108,7 @@ export const convertPdfToImage =
|
|||||||
imageUrl = URL.createObjectURL(blob);
|
imageUrl = URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
dlog(`✅ [EnergyLabel] Conversion successful on attempt ${attempts}:`, pdfUrl);
|
void dlog(`✅ [EnergyLabel] Conversion successful on attempt ${attempts}:`, pdfUrl);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: types.CONVERT_PDF_TO_IMAGE_SUCCESS,
|
type: types.CONVERT_PDF_TO_IMAGE_SUCCESS,
|
||||||
payload: { pdfUrl, imageUrl },
|
payload: { pdfUrl, imageUrl },
|
||||||
@@ -119,16 +116,16 @@ export const convertPdfToImage =
|
|||||||
|
|
||||||
callback && callback(null, imageUrl);
|
callback && callback(null, imageUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
derror(`❌ [EnergyLabel] Image creation failed on attempt ${attempts}:`, error);
|
void derror(`❌ [EnergyLabel] Image creation failed on attempt ${attempts}:`, error);
|
||||||
|
|
||||||
// 이미지 생성 실패도 재시도
|
// 이미지 생성 실패도 재시도
|
||||||
if (attempts < maxRetries + 1) {
|
if (attempts < maxRetries + 1) {
|
||||||
dlog(
|
void dlog(
|
||||||
`🔄 [EnergyLabel] Retrying due to image creation error... (${attempts}/${maxRetries + 1})`
|
`🔄 [EnergyLabel] Retrying due to image creation error... (${attempts}/${maxRetries + 1})`
|
||||||
);
|
);
|
||||||
attemptConversion();
|
attemptConversion();
|
||||||
} else {
|
} else {
|
||||||
derror(
|
void derror(
|
||||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (image error):`,
|
`❌ [EnergyLabel] Final failure after ${attempts} attempts (image error):`,
|
||||||
pdfUrl
|
pdfUrl
|
||||||
);
|
);
|
||||||
@@ -147,14 +144,14 @@ export const convertPdfToImage =
|
|||||||
timeoutId = null;
|
timeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dwarn(`⚠️ [EnergyLabel] Network error on attempt ${attempts}:`, error.message);
|
void dwarn(`⚠️ [EnergyLabel] Network error on attempt ${attempts}:`, error.message);
|
||||||
|
|
||||||
// 네트워크 에러도 재시도
|
// 네트워크 에러도 재시도
|
||||||
if (attempts < maxRetries + 1) {
|
if (attempts < maxRetries + 1) {
|
||||||
dlog(`🔄 [EnergyLabel] Retrying due to network error... (${attempts}/${maxRetries + 1})`);
|
void dlog(`🔄 [EnergyLabel] Retrying due to network error... (${attempts}/${maxRetries + 1})`);
|
||||||
attemptConversion();
|
attemptConversion();
|
||||||
} else {
|
} else {
|
||||||
derror(
|
void derror(
|
||||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (network error):`,
|
`❌ [EnergyLabel] Final failure after ${attempts} attempts (network error):`,
|
||||||
pdfUrl
|
pdfUrl
|
||||||
);
|
);
|
||||||
@@ -188,7 +185,7 @@ export const convertPdfToImage =
|
|||||||
* @param {Array<string>} pdfUrls - 변환할 PDF URL 배열
|
* @param {Array<string>} pdfUrls - 변환할 PDF URL 배열
|
||||||
* @param {function} callback - 완료 후 실행할 콜백 (errors, results)
|
* @param {function} callback - 완료 후 실행할 콜백 (errors, results)
|
||||||
*/
|
*/
|
||||||
export const convertMultiplePdfs = (pdfUrls, callback) => async (dispatch, getState) => {
|
export const convertMultiplePdfs = (pdfUrls, callback) => async (dispatch) => {
|
||||||
if (!pdfUrls || pdfUrls.length === 0) {
|
if (!pdfUrls || pdfUrls.length === 0) {
|
||||||
callback && callback(null, []);
|
callback && callback(null, []);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// IF-LGSP-339 : 회원 다운로드 쿠폰 정보 조회
|
// IF-LGSP-339 : 회원 다운로드 쿠폰 정보 조회
|
||||||
export const getProductCouponInfo = (props) => (dispatch, getState) => {
|
export const getProductCouponInfo = (props) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -348,8 +348,14 @@ export const TAxiosAdvancedPromise = (
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
|
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
|
||||||
|
|
||||||
|
// Check if the error is due to token expiration
|
||||||
|
// TAxios already handles token refresh and queueing for these codes (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;
|
||||||
|
|
||||||
// 재시도 로직
|
// 재시도 로직
|
||||||
if (attempts < maxAttempts) {
|
if (attempts < maxAttempts && !isTokenError) {
|
||||||
console.log(`Retrying in ${retryDelay}ms... (${attempts}/${maxAttempts})`);
|
console.log(`Retrying in ${retryDelay}ms... (${attempts}/${maxAttempts})`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
attemptRequest();
|
attemptRequest();
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
@slider-padding-h: @sand-mediaplayer-slider-knob-size;
|
@slider-padding-h: @sand-mediaplayer-slider-knob-size;
|
||||||
margin-left: 130px;
|
margin-left: 130px;
|
||||||
margin-right: 130px;
|
margin-right: 130px;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
width: 1540px;
|
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|
||||||
&.videoVertical {
|
&.videoVertical {
|
||||||
@@ -31,10 +30,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mediaSlider {
|
.mediaSlider {
|
||||||
margin: 0 @slider-padding-h;
|
margin: 0 0 0 @slider-padding-h;
|
||||||
padding: @slider-padding-v 0;
|
padding: @slider-padding-v 0;
|
||||||
height: @sand-mediaplayer-slider-height;
|
height: @sand-mediaplayer-slider-height;
|
||||||
right: 154px;
|
right: 154px;
|
||||||
|
width: 1466px;
|
||||||
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
|
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@@ -6,23 +6,29 @@
|
|||||||
.times {
|
.times {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: @baseFont;
|
font-family: @baseFont;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 22px;
|
right: 90px;
|
||||||
right: 30px;
|
bottom: -5px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
letter-spacing: -1px;
|
||||||
.separator {
|
.separator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 110px;
|
right: 105px;
|
||||||
|
bottom: -5px;
|
||||||
}
|
}
|
||||||
.currentTime {
|
.currentTime {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 140px;
|
right: 130px;
|
||||||
|
bottom: -5px;
|
||||||
|
}
|
||||||
|
.totalTime {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right:0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,8 +172,23 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
|||||||
break;
|
break;
|
||||||
//브랜드
|
//브랜드
|
||||||
case 10300:
|
case 10300:
|
||||||
result =
|
result = [
|
||||||
data?.shortFeaturedBrands?.map((item) => ({
|
// 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) => ({
|
||||||
icons: FeaturedBrandIcon,
|
icons: FeaturedBrandIcon,
|
||||||
id: item.patnrId,
|
id: item.patnrId,
|
||||||
path: item.patncLogoPath,
|
path: item.patncLogoPath,
|
||||||
@@ -185,7 +200,8 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
|||||||
panelInfo: { from: 'gnb', patnrId: item.patnrId },
|
panelInfo: { from: 'gnb', patnrId: item.patnrId },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})) || [];
|
})) || []),
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
//
|
//
|
||||||
case 10600:
|
case 10600:
|
||||||
@@ -304,6 +320,7 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
|||||||
title: item.title,
|
title: item.title,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
patncNm: item.patncNm,
|
patncNm: item.patncNm,
|
||||||
|
icons: item.icons,
|
||||||
target: item.target,
|
target: item.target,
|
||||||
spotlightId: `secondDepth-${item.id}`,
|
spotlightId: `secondDepth-${item.id}`,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { scaleW } from "../../../utils/helperMethods";
|
||||||
|
import useConvertThemeColor from "./useConvertThemeColor";
|
||||||
|
|
||||||
|
const NbcuIcon = ({ iconType = "normal" }) => {
|
||||||
|
const themeColor = useConvertThemeColor({ iconType });
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={scaleW(48)}
|
||||||
|
height={scaleW(48)}
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle cx="24" cy="24" r="22" fill={themeColor} opacity="0.1" stroke={themeColor} strokeWidth="0.5" />
|
||||||
|
<text
|
||||||
|
x="24"
|
||||||
|
y="32"
|
||||||
|
textAnchor="middle"
|
||||||
|
fill={themeColor}
|
||||||
|
fontSize="18"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
>
|
||||||
|
NBC
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x="24"
|
||||||
|
y="40"
|
||||||
|
textAnchor="middle"
|
||||||
|
fill={themeColor}
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NbcuIcon;
|
||||||
@@ -66,12 +66,10 @@ import TReactPlayer from './TReactPlayer';
|
|||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
import css from './VideoPlayer.module.less';
|
import css from './VideoPlayer.module.less';
|
||||||
import { updateVideoPlayState } from '../../actions/playActions';
|
import { updateVideoPlayState } from '../../actions/playActions';
|
||||||
import createMemoryMonitor from '../../utils/memoryMonitor';
|
|
||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||||
const memoryMonitor = createMemoryMonitor();
|
|
||||||
|
|
||||||
const isEnter = is('enter');
|
const isEnter = is('enter');
|
||||||
const isLeft = is('left');
|
const isLeft = is('left');
|
||||||
@@ -828,7 +826,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
memoryMonitor.logMemory('[VideoPlayer] componentDidMount');
|
|
||||||
on('mousemove', this.activityDetected);
|
on('mousemove', this.activityDetected);
|
||||||
if (platform.touch) {
|
if (platform.touch) {
|
||||||
on('touchmove', this.activityDetected);
|
on('touchmove', this.activityDetected);
|
||||||
@@ -1024,7 +1021,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
|
|
||||||
// console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
|
// console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
|
||||||
off('mousemove', this.activityDetected);
|
off('mousemove', this.activityDetected);
|
||||||
if (platform.touch) {
|
if (platform.touch) {
|
||||||
@@ -1132,7 +1128,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
}
|
}
|
||||||
// 레퍼런스도 해제해 GC 대상이 되도록 함
|
// 레퍼런스도 해제해 GC 대상이 되도록 함
|
||||||
this.video = null;
|
this.video = null;
|
||||||
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - cleanup done');
|
|
||||||
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
|
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
|
||||||
if (this.floatingLayerController) {
|
if (this.floatingLayerController) {
|
||||||
this.floatingLayerController.unregister();
|
this.floatingLayerController.unregister();
|
||||||
@@ -1569,14 +1564,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
handleEvent = (ev) => {
|
handleEvent = (ev) => {
|
||||||
const el = this.video;
|
const el = this.video;
|
||||||
|
|
||||||
// 재생 종료 또는 오류 시 메모리 모니터링 타이머 정리
|
|
||||||
if (ev.type === 'ended' || ev.type === 'error') {
|
|
||||||
if (this.memoryMonitoringInterval) {
|
|
||||||
clearInterval(this.memoryMonitoringInterval);
|
|
||||||
this.memoryMonitoringInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedState = {
|
const updatedState = {
|
||||||
// Standard media properties
|
// Standard media properties
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
@@ -1811,10 +1798,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
play = () => {
|
play = () => {
|
||||||
memoryMonitor.logMemory('[VideoPlayer] play() called', {
|
|
||||||
currentTime: this.state.currentTime,
|
|
||||||
duration: this.state.duration,
|
|
||||||
});
|
|
||||||
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
|
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
|
||||||
currentTime: this.state.currentTime,
|
currentTime: this.state.currentTime,
|
||||||
duration: this.state.duration,
|
duration: this.state.duration,
|
||||||
@@ -1859,10 +1842,6 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
pause = () => {
|
pause = () => {
|
||||||
memoryMonitor.logMemory('[VideoPlayer] pause() called', {
|
|
||||||
currentTime: this.state.currentTime.toFixed(2),
|
|
||||||
duration: this.state.duration.toFixed(2),
|
|
||||||
});
|
|
||||||
dlog('🔴 [VideoPlayer] pause() called', {
|
dlog('🔴 [VideoPlayer] pause() called', {
|
||||||
currentTime: this.state.currentTime,
|
currentTime: this.state.currentTime,
|
||||||
duration: this.state.duration,
|
duration: this.state.duration,
|
||||||
|
|||||||
@@ -692,10 +692,10 @@
|
|||||||
// display: flex;
|
// display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 60px;
|
|
||||||
margin-right: 59px;
|
|
||||||
height: 70px;
|
height: 70px;
|
||||||
bottom: -20px;
|
width:1800px;
|
||||||
|
margin-left:60px;
|
||||||
|
bottom:92px;
|
||||||
> *:first-child {
|
> *:first-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,705 +0,0 @@
|
|||||||
/**
|
|
||||||
* 메모리 모니터링 유틸리티
|
|
||||||
* [Memory] 태그를 붙인 로그로 메모리 사용량을 추적합니다
|
|
||||||
*/
|
|
||||||
|
|
||||||
let memoryMonitorInstance = null;
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
export const createMemoryMonitor = (enableInitLog = true) => {
|
|
||||||
// 싱글톤 패턴: 이미 생성된 인스턴스가 있으면 재사용
|
|
||||||
if (memoryMonitorInstance) {
|
|
||||||
return memoryMonitorInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableInitLog && !initialized) {
|
|
||||||
initialized = true;
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Memory Monitor Initialized] ${timestamp}`);
|
|
||||||
if (typeof performance !== 'undefined' && performance.memory) {
|
|
||||||
console.log(`[Memory] API Support: YES - performance.memory available`);
|
|
||||||
} else {
|
|
||||||
console.log(`[Memory] API Support: NO - performance.memory NOT available (webOS TV 또는 제한된 브라우저)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const getMemoryInfo = () => {
|
|
||||||
if (typeof performance !== 'undefined' && performance.memory) {
|
|
||||||
return {
|
|
||||||
usedJSHeapSize: (performance.memory.usedJSHeapSize / 1048576).toFixed(2),
|
|
||||||
totalJSHeapSize: (performance.memory.totalJSHeapSize / 1048576).toFixed(2),
|
|
||||||
jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 미디어 리소스 메모리 정보 수집
|
|
||||||
const getMediaMemoryInfo = () => {
|
|
||||||
try {
|
|
||||||
const mediaElements = document.querySelectorAll('video, audio');
|
|
||||||
let totalVideoBuffer = 0;
|
|
||||||
let totalAudioBuffer = 0;
|
|
||||||
let videoCount = 0;
|
|
||||||
let audioCount = 0;
|
|
||||||
const mediaInfo = [];
|
|
||||||
|
|
||||||
// NodeList를 배열로 변환하여 forEach 사용
|
|
||||||
Array.from(mediaElements).forEach((media, index) => {
|
|
||||||
try {
|
|
||||||
const buffered = media.buffered;
|
|
||||||
let totalDuration = 0;
|
|
||||||
|
|
||||||
if (buffered && buffered.length) {
|
|
||||||
for (let i = 0; i < buffered.length; i++) {
|
|
||||||
try {
|
|
||||||
totalDuration += buffered.end(i) - buffered.start(i);
|
|
||||||
} catch (e) {
|
|
||||||
// buffered 접근 중 오류 발생 시 무시
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.tagName === 'VIDEO') {
|
|
||||||
videoCount++;
|
|
||||||
} else if (media.tagName === 'AUDIO') {
|
|
||||||
audioCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비디오 메타데이터 수집
|
|
||||||
let videoBitrate = 0;
|
|
||||||
let codecInfo = 'unknown';
|
|
||||||
|
|
||||||
if (media.tagName === 'VIDEO' && media.videoWidth && media.videoHeight) {
|
|
||||||
// 해상도 기반 비트레이트 추정 (HLS 스트리밍 기준)
|
|
||||||
const resolution = media.videoWidth * media.videoHeight;
|
|
||||||
if (resolution >= 3840 * 2160) { // 4K
|
|
||||||
videoBitrate = 15000000; // 15Mbps
|
|
||||||
codecInfo = '4K/HLS';
|
|
||||||
} else if (resolution >= 1920 * 1080) { // FHD
|
|
||||||
videoBitrate = 8000000; // 8Mbps
|
|
||||||
codecInfo = 'FHD/HLS';
|
|
||||||
} else if (resolution >= 1280 * 720) { // HD
|
|
||||||
videoBitrate = 4000000; // 4Mbps
|
|
||||||
codecInfo = 'HD/HLS';
|
|
||||||
} else { // SD
|
|
||||||
videoBitrate = 2000000; // 2Mbps
|
|
||||||
codecInfo = 'SD/HLS';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HLS 스트리밍 정보 확인
|
|
||||||
let hlsInfo = null;
|
|
||||||
if (media.src && media.src.includes('.m3u8')) {
|
|
||||||
hlsInfo = {
|
|
||||||
isHLS: true,
|
|
||||||
playlistUrl: media.src.substring(0, 100) + '...',
|
|
||||||
estimatedSegments: Math.ceil((media.duration || 0) / 10), // 10초 세그먼트 기준
|
|
||||||
};
|
|
||||||
} else if (media.src) {
|
|
||||||
hlsInfo = {
|
|
||||||
isHLS: false,
|
|
||||||
contentType: 'progressive',
|
|
||||||
format: media.src.includes('.mp4') ? 'MP4' : 'Unknown',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaData = {
|
|
||||||
index,
|
|
||||||
type: media.tagName ? media.tagName.toLowerCase() : 'unknown',
|
|
||||||
src: media.src ? (media.src.length > 50 ? media.src.substring(0, 50) + '...' : media.src) : 'N/A',
|
|
||||||
duration: media.duration || 0,
|
|
||||||
bufferedDuration: totalDuration,
|
|
||||||
currentTime: media.currentTime || 0,
|
|
||||||
readyState: media.readyState || 0,
|
|
||||||
networkState: media.networkState || 0,
|
|
||||||
videoWidth: media.videoWidth || 0,
|
|
||||||
videoHeight: media.videoHeight || 0,
|
|
||||||
// 비디오 전용 정보
|
|
||||||
bitrate: videoBitrate,
|
|
||||||
codecInfo: codecInfo,
|
|
||||||
// HLS/스트리밍 정보
|
|
||||||
hlsInfo: hlsInfo,
|
|
||||||
// 버퍼 효율성
|
|
||||||
bufferEfficiency: media.duration > 0 ? (totalDuration / media.duration * 100).toFixed(1) + '%' : '0%',
|
|
||||||
// 재생 상태
|
|
||||||
paused: media.paused,
|
|
||||||
ended: media.ended,
|
|
||||||
muted: media.muted,
|
|
||||||
volume: media.volume || 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaInfo.push(mediaData);
|
|
||||||
|
|
||||||
// 실제 버퍼 메모리 계산
|
|
||||||
if (media.tagName === 'VIDEO' && media.videoWidth && media.videoHeight) {
|
|
||||||
// 비디오: 실제 비트레이트 기반 계산
|
|
||||||
totalVideoBuffer += totalDuration * (videoBitrate / 8); // bytes
|
|
||||||
} else if (media.tagName === 'AUDIO') {
|
|
||||||
// 오디오: 고품질 320kbps로 추정
|
|
||||||
totalAudioBuffer += totalDuration * 320000 / 8; // bytes
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 개별 미디어 요소 처리 중 오류 발생 시 무시
|
|
||||||
console.warn('[Memory Monitor] Error processing media element:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
mediaCount: mediaElements.length,
|
|
||||||
videoElements: videoCount,
|
|
||||||
audioElements: audioCount,
|
|
||||||
totalVideoBufferMB: (totalVideoBuffer / 1048576).toFixed(2),
|
|
||||||
totalAudioBufferMB: (totalAudioBuffer / 1048576).toFixed(2),
|
|
||||||
estimatedMediaMemoryMB: ((totalVideoBuffer + totalAudioBuffer) / 1048576).toFixed(2),
|
|
||||||
mediaElements: mediaInfo
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Memory Monitor] Error getting media memory info:', e);
|
|
||||||
return {
|
|
||||||
mediaCount: 0,
|
|
||||||
videoElements: 0,
|
|
||||||
audioElements: 0,
|
|
||||||
totalVideoBufferMB: '0.00',
|
|
||||||
totalAudioBufferMB: '0.00',
|
|
||||||
estimatedMediaMemoryMB: '0.00',
|
|
||||||
mediaElements: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 이미지 리소스 메모리 정보 수집
|
|
||||||
const getImageMemoryInfo = () => {
|
|
||||||
try {
|
|
||||||
const images = document.querySelectorAll('img');
|
|
||||||
let totalImageMemory = 0;
|
|
||||||
const imageInfo = [];
|
|
||||||
|
|
||||||
// NodeList를 배열로 변환하여 forEach 사용
|
|
||||||
Array.from(images).forEach((img, index) => {
|
|
||||||
try {
|
|
||||||
if (img.naturalWidth && img.naturalHeight) {
|
|
||||||
// 이미지 메모리 크기 추정 (너비 * 높이 * 4바이트 RGBA)
|
|
||||||
const estimatedMemory = img.naturalWidth * img.naturalHeight * 4;
|
|
||||||
totalImageMemory += estimatedMemory;
|
|
||||||
|
|
||||||
imageInfo.push({
|
|
||||||
index,
|
|
||||||
src: img.src ? (img.src.length > 50 ? img.src.substring(0, 50) + '...' : img.src) : 'N/A',
|
|
||||||
naturalWidth: img.naturalWidth,
|
|
||||||
naturalHeight: img.naturalHeight,
|
|
||||||
displayWidth: img.offsetWidth || 0,
|
|
||||||
displayHeight: img.offsetHeight || 0,
|
|
||||||
estimatedMemoryMB: (estimatedMemory / 1048576).toFixed(2),
|
|
||||||
complete: img.complete || false,
|
|
||||||
loading: img.loading || 'auto'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 개별 이미지 요소 처리 중 오류 발생 시 무시
|
|
||||||
console.warn('[Memory Monitor] Error processing image element:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageCount: images.length,
|
|
||||||
totalImageMemoryMB: (totalImageMemory / 1048576).toFixed(2),
|
|
||||||
images: imageInfo
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Memory Monitor] Error getting image memory info:', e);
|
|
||||||
return {
|
|
||||||
imageCount: 0,
|
|
||||||
totalImageMemoryMB: '0.00',
|
|
||||||
images: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Canvas/WebGL 리소스 메모리 정보 수집
|
|
||||||
const getCanvasMemoryInfo = () => {
|
|
||||||
try {
|
|
||||||
const canvases = document.querySelectorAll('canvas');
|
|
||||||
let totalCanvasMemory = 0;
|
|
||||||
const canvasInfo = [];
|
|
||||||
|
|
||||||
// NodeList를 배열로 변환하여 forEach 사용
|
|
||||||
Array.from(canvases).forEach((canvas, index) => {
|
|
||||||
try {
|
|
||||||
const context = canvas.getContext('2d') || canvas.getContext('webgl') || canvas.getContext('webgl2');
|
|
||||||
if (context) {
|
|
||||||
const memory = canvas.width * canvas.height * 4; // 4바이트 per 픽셀
|
|
||||||
totalCanvasMemory += memory;
|
|
||||||
|
|
||||||
canvasInfo.push({
|
|
||||||
index,
|
|
||||||
width: canvas.width || 0,
|
|
||||||
height: canvas.height || 0,
|
|
||||||
contextType: context.constructor.name || 'unknown',
|
|
||||||
estimatedMemoryMB: (memory / 1048576).toFixed(2)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 개별 캔버스 요소 처리 중 오류 발생 시 무시
|
|
||||||
console.warn('[Memory Monitor] Error processing canvas element:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvasCount: canvases.length,
|
|
||||||
totalCanvasMemoryMB: (totalCanvasMemory / 1048576).toFixed(2),
|
|
||||||
canvases: canvasInfo
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Memory Monitor] Error getting canvas memory info:', e);
|
|
||||||
return {
|
|
||||||
canvasCount: 0,
|
|
||||||
totalCanvasMemoryMB: '0.00',
|
|
||||||
canvases: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 통합 미디어 메모리 정보
|
|
||||||
const getCompleteMediaMemoryInfo = () => {
|
|
||||||
const mediaMemory = getMediaMemoryInfo();
|
|
||||||
const imageMemory = getImageMemoryInfo();
|
|
||||||
const canvasMemory = getCanvasMemoryInfo();
|
|
||||||
|
|
||||||
const totalEstimatedMB = (
|
|
||||||
parseFloat(mediaMemory.estimatedMediaMemoryMB) +
|
|
||||||
parseFloat(imageMemory.totalImageMemoryMB) +
|
|
||||||
parseFloat(canvasMemory.totalCanvasMemoryMB)
|
|
||||||
).toFixed(2);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalEstimatedMediaMemoryMB: totalEstimatedMB,
|
|
||||||
media: mediaMemory,
|
|
||||||
images: imageMemory,
|
|
||||||
canvas: canvasMemory,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDetailedMemoryInfo = () => {
|
|
||||||
const info = getMemoryInfo();
|
|
||||||
if (!info) return null;
|
|
||||||
|
|
||||||
// 추가 메모리 정보
|
|
||||||
const detailed = {
|
|
||||||
...info,
|
|
||||||
// usedJSHeapSize의 percentage (상세)
|
|
||||||
heapUsagePercent: ((parseFloat(info.usedJSHeapSize) / parseFloat(info.jsHeapSizeLimit)) * 100).toFixed(1),
|
|
||||||
// DOM 노드 수
|
|
||||||
domNodeCount: document.querySelectorAll('*').length,
|
|
||||||
// 리스너 수 (대략값)
|
|
||||||
eventListenerEstimate: Object.keys(window).filter(key => key.startsWith('on')).length,
|
|
||||||
// 미디어 리소스 정보 추가
|
|
||||||
mediaMemory: getCompleteMediaMemoryInfo(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return detailed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatMemoryLog = (usedMB, totalMB, limitMB) => {
|
|
||||||
const percentage = ((usedMB / limitMB) * 100).toFixed(1);
|
|
||||||
return `[Memory] Used: ${usedMB}MB / Total: ${totalMB}MB / Limit: ${limitMB}MB (${percentage}%)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* 현재 메모리 상태를 로깅
|
|
||||||
* @param {string} context - 컨텍스트 설명
|
|
||||||
* @param {object} additionalInfo - 추가 정보
|
|
||||||
*/
|
|
||||||
logMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
console.log(`${logMsg} | ${context} ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메모리 사용량 변화를 추적
|
|
||||||
* @param {string} context - 컨텍스트 설명
|
|
||||||
* @param {number} previousMB - 이전 메모리 사용량 (MB)
|
|
||||||
* @returns {number} 현재 메모리 사용량 (MB)
|
|
||||||
*/
|
|
||||||
trackMemoryDelta: (context = '', previousMB = 0) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const currentMB = parseFloat(mem.usedJSHeapSize);
|
|
||||||
const delta = (currentMB - previousMB).toFixed(2);
|
|
||||||
const deltaSign = delta > 0 ? '+' : '';
|
|
||||||
console.log(
|
|
||||||
`[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB`
|
|
||||||
);
|
|
||||||
return currentMB;
|
|
||||||
}
|
|
||||||
return previousMB;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 정기적으로 메모리를 모니터링
|
|
||||||
* @param {number} intervalMs - 모니터링 간격 (기본값: 10000ms)
|
|
||||||
* @param {string} label - 모니터링 라벨
|
|
||||||
* @returns {function} cleanup 함수
|
|
||||||
*/
|
|
||||||
startPeriodicMonitoring: (intervalMs = 10000, label = 'Periodic') => {
|
|
||||||
let lastMemory = 0;
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) lastMemory = parseFloat(mem.usedJSHeapSize);
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
lastMemory = this.trackMemoryDelta(`${label}:`, lastMemory);
|
|
||||||
}, intervalMs);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 버퍼 관련 메모리 정보 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} bufferInfo - 버퍼 정보 { bufferedSegments, totalDuration, etc }
|
|
||||||
*/
|
|
||||||
logBufferMemory: (context = '', bufferInfo = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const bufferStr = JSON.stringify(bufferInfo);
|
|
||||||
console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HLS 상태에 따른 메모리 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} hlsState - HLS 상태 정보
|
|
||||||
*/
|
|
||||||
logHlsMemory: (context = '', hlsState = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const hlsStr = JSON.stringify(hlsState);
|
|
||||||
console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 상세 메모리 정보 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} additionalInfo - 추가 정보
|
|
||||||
*/
|
|
||||||
logDetailedMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const detailed = getDetailedMemoryInfo();
|
|
||||||
if (detailed) {
|
|
||||||
const logMsg = formatMemoryLog(detailed.usedJSHeapSize, detailed.totalJSHeapSize, detailed.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
const detailStr = JSON.stringify({
|
|
||||||
heapUsagePercent: detailed.heapUsagePercent + '%',
|
|
||||||
domNodeCount: detailed.domNodeCount,
|
|
||||||
eventListenerEstimate: detailed.eventListenerEstimate,
|
|
||||||
});
|
|
||||||
const mediaMemory = detailed.mediaMemory;
|
|
||||||
const mediaStr = JSON.stringify({
|
|
||||||
totalMediaMemory: mediaMemory.totalEstimatedMediaMemoryMB + 'MB',
|
|
||||||
videoElements: mediaMemory.media.videoElements,
|
|
||||||
audioElements: mediaMemory.media.audioElements,
|
|
||||||
imageCount: mediaMemory.images.imageCount,
|
|
||||||
imageMemory: mediaMemory.images.totalImageMemoryMB + 'MB',
|
|
||||||
canvasCount: mediaMemory.canvas.canvasCount,
|
|
||||||
canvasMemory: mediaMemory.canvas.totalCanvasMemoryMB + 'MB'
|
|
||||||
});
|
|
||||||
|
|
||||||
const jsTotal = parseFloat(detailed.usedJSHeapSize);
|
|
||||||
const mediaTotal = parseFloat(mediaMemory.totalEstimatedMediaMemoryMB);
|
|
||||||
const estimatedTotal = (jsTotal + mediaTotal).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`${logMsg} | ${context} | Details: ${detailStr} | Media: ${mediaStr} | Est.Total: ${estimatedTotal}MB ${info}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 미디어 리소스 메모리 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} additionalInfo - 추가 정보
|
|
||||||
*/
|
|
||||||
logMediaMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getCompleteMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const jsTotal = parseFloat(jsMem.usedJSHeapSize);
|
|
||||||
const mediaTotal = parseFloat(mediaMem.totalEstimatedMediaMemoryMB);
|
|
||||||
const estimatedTotal = (jsTotal + mediaTotal).toFixed(2);
|
|
||||||
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | Media: ${context}`);
|
|
||||||
console.log(`[Media Breakdown] Images: ${mediaMem.images.totalImageMemoryMB}MB (${mediaMem.images.imageCount}개), Video: ${mediaMem.media.estimatedMediaMemoryMB}MB (${mediaMem.media.mediaCount}개), Canvas: ${mediaMem.canvas.totalCanvasMemoryMB}MB (${mediaMem.canvas.canvasCount}개)`);
|
|
||||||
console.log(`[Total Estimated] JS(${jsTotal}MB) + Media(${mediaTotal}MB) = ${estimatedTotal}MB ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Media Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 비디오 전용 상세 메모리 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} additionalInfo - 추가 정보
|
|
||||||
*/
|
|
||||||
logVideoMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | Video Memory: ${context}`);
|
|
||||||
console.log(`[Video Summary] ${mediaMem.videoElements}개 비디오, ${mediaMem.totalVideoBufferMB}MB 버퍼 메모리 사용`);
|
|
||||||
|
|
||||||
// 개별 비디오 정보 상세 출력
|
|
||||||
mediaMem.mediaElements.forEach((video, idx) => {
|
|
||||||
if (video.type === 'video') {
|
|
||||||
console.log(`[Video ${video.index}] ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Buffered: ${video.bufferedDuration.toFixed(1)}s/${video.duration.toFixed(1)}s (${video.bufferEfficiency}) | ${video.hlsInfo?.isHLS ? 'HLS' : 'Progressive'} | ${video.paused ? 'Paused' : 'Playing'} | Src: ${video.src}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[Video Estimation] JS Heap: ${jsMem.usedJSHeapSize}MB + Video Buffer: ${mediaMem.totalVideoBufferMB}MB = ${(parseFloat(jsMem.usedJSHeapSize) + parseFloat(mediaMem.totalVideoBufferMB)).toFixed(2)}MB ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Video Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HLS 스트리밍 메모리 전용 로깅
|
|
||||||
* @param {string} context - 컨텍스트
|
|
||||||
* @param {object} additionalInfo - 추가 정보
|
|
||||||
*/
|
|
||||||
logHLSMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const hlsVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && video.hlsInfo.isHLS);
|
|
||||||
const progressiveVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && !video.hlsInfo.isHLS);
|
|
||||||
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | HLS Streaming: ${context}`);
|
|
||||||
console.log(`[Streaming Analysis] HLS: ${hlsVideos.length}개, Progressive: ${progressiveVideos.length}개 | Total Video Memory: ${mediaMem.totalVideoBufferMB}MB`);
|
|
||||||
|
|
||||||
// HLS 비디오 상세 정보
|
|
||||||
if (hlsVideos.length > 0) {
|
|
||||||
console.log(`[HLS Videos]`);
|
|
||||||
hlsVideos.forEach(video => {
|
|
||||||
console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Segments: ~${video.hlsInfo.estimatedSegments}개 | Buffer: ${video.bufferedDuration.toFixed(1)}s | Efficiency: ${video.bufferEfficiency}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progressive 비디오 상세 정보
|
|
||||||
if (progressiveVideos.length > 0) {
|
|
||||||
console.log(`[Progressive Videos]`);
|
|
||||||
progressiveVideos.forEach(video => {
|
|
||||||
console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Format: ${video.hlsInfo.format} | Buffer: ${video.bufferedDuration.toFixed(1)}s`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamingMemoryMB = hlsVideos.reduce((sum, video) => {
|
|
||||||
return sum + parseFloat(video.bufferedDuration) * (video.bitrate / 8 / 1048576);
|
|
||||||
}, 0).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`[Streaming Memory] HLS Buffer: ${streamingMemoryMB}MB | Progressive Buffer: ${(parseFloat(mediaMem.totalVideoBufferMB) - parseFloat(streamingMemoryMB)).toFixed(2)}MB ${info}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메모리 정보만 반환 (로깅 없음)
|
|
||||||
* @returns {object} 메모리 정보 객체
|
|
||||||
*/
|
|
||||||
getMemory: () => getMemoryInfo(),
|
|
||||||
getDetailedMemory: () => getDetailedMemoryInfo(),
|
|
||||||
getMediaMemory: () => getCompleteMediaMemoryInfo(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 싱글톤 인스턴스 저장
|
|
||||||
memoryMonitorInstance = {
|
|
||||||
logMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
console.log(`${logMsg} | ${context} ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trackMemoryDelta: (context = '', previousMB = 0) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const currentMB = parseFloat(mem.usedJSHeapSize);
|
|
||||||
const delta = (currentMB - previousMB).toFixed(2);
|
|
||||||
const deltaSign = delta > 0 ? '+' : '';
|
|
||||||
console.log(
|
|
||||||
`[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB`
|
|
||||||
);
|
|
||||||
return currentMB;
|
|
||||||
}
|
|
||||||
return previousMB;
|
|
||||||
},
|
|
||||||
startPeriodicMonitoring: (intervalMs = 30000, label = 'Periodic') => {
|
|
||||||
let lastMemory = 0;
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) lastMemory = parseFloat(mem.usedJSHeapSize);
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
this.trackMemoryDelta(`${label}:`, lastMemory);
|
|
||||||
}, intervalMs);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
},
|
|
||||||
logBufferMemory: (context = '', bufferInfo = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const bufferStr = JSON.stringify(bufferInfo);
|
|
||||||
console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logHlsMemory: (context = '', hlsState = {}) => {
|
|
||||||
const mem = getMemoryInfo();
|
|
||||||
if (mem) {
|
|
||||||
const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit);
|
|
||||||
const hlsStr = JSON.stringify(hlsState);
|
|
||||||
console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logDetailedMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const detailed = getDetailedMemoryInfo();
|
|
||||||
if (detailed) {
|
|
||||||
const logMsg = formatMemoryLog(detailed.usedJSHeapSize, detailed.totalJSHeapSize, detailed.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
const detailStr = JSON.stringify({
|
|
||||||
heapUsagePercent: detailed.heapUsagePercent + '%',
|
|
||||||
domNodeCount: detailed.domNodeCount,
|
|
||||||
eventListenerEstimate: detailed.eventListenerEstimate,
|
|
||||||
});
|
|
||||||
const mediaMemory = detailed.mediaMemory;
|
|
||||||
const mediaStr = JSON.stringify({
|
|
||||||
totalMediaMemory: mediaMemory.totalEstimatedMediaMemoryMB + 'MB',
|
|
||||||
videoElements: mediaMemory.media.videoElements,
|
|
||||||
audioElements: mediaMemory.media.audioElements,
|
|
||||||
imageCount: mediaMemory.images.imageCount,
|
|
||||||
imageMemory: mediaMemory.images.totalImageMemoryMB + 'MB',
|
|
||||||
canvasCount: mediaMemory.canvas.canvasCount,
|
|
||||||
canvasMemory: mediaMemory.canvas.totalCanvasMemoryMB + 'MB'
|
|
||||||
});
|
|
||||||
|
|
||||||
const jsTotal = parseFloat(detailed.usedJSHeapSize);
|
|
||||||
const mediaTotal = parseFloat(mediaMemory.totalEstimatedMediaMemoryMB);
|
|
||||||
const estimatedTotal = (jsTotal + mediaTotal).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`${logMsg} | ${context} | Details: ${detailStr} | Media: ${mediaStr} | Est.Total: ${estimatedTotal}MB ${info}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
logMediaMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getCompleteMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const jsTotal = parseFloat(jsMem.usedJSHeapSize);
|
|
||||||
const mediaTotal = parseFloat(mediaMem.totalEstimatedMediaMemoryMB);
|
|
||||||
const estimatedTotal = (jsTotal + mediaTotal).toFixed(2);
|
|
||||||
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | Media: ${context}`);
|
|
||||||
console.log(`[Media Breakdown] Images: ${mediaMem.images.totalImageMemoryMB}MB (${mediaMem.images.imageCount}개), Video: ${mediaMem.media.estimatedMediaMemoryMB}MB (${mediaMem.media.mediaCount}개), Canvas: ${mediaMem.canvas.totalCanvasMemoryMB}MB (${mediaMem.canvas.canvasCount}개)`);
|
|
||||||
console.log(`[Total Estimated] JS(${jsTotal}MB) + Media(${mediaTotal}MB) = ${estimatedTotal}MB ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Media Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getMemory: () => getMemoryInfo(),
|
|
||||||
getDetailedMemory: () => getDetailedMemoryInfo(),
|
|
||||||
getMediaMemory: () => getCompleteMediaMemoryInfo(),
|
|
||||||
logVideoMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | Video Memory: ${context}`);
|
|
||||||
console.log(`[Video Summary] ${mediaMem.videoElements}개 비디오, ${mediaMem.totalVideoBufferMB}MB 버퍼 메모리 사용`);
|
|
||||||
|
|
||||||
// 개별 비디오 정보 상세 출력
|
|
||||||
mediaMem.mediaElements.forEach((video, idx) => {
|
|
||||||
if (video.type === 'video') {
|
|
||||||
console.log(`[Video ${video.index}] ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Buffered: ${video.bufferedDuration.toFixed(1)}s/${video.duration.toFixed(1)}s (${video.bufferEfficiency}) | ${video.hlsInfo?.isHLS ? 'HLS' : 'Progressive'} | ${video.paused ? 'Paused' : 'Playing'} | Src: ${video.src}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[Video Estimation] JS Heap: ${jsMem.usedJSHeapSize}MB + Video Buffer: ${mediaMem.totalVideoBufferMB}MB = ${(parseFloat(jsMem.usedJSHeapSize) + parseFloat(mediaMem.totalVideoBufferMB)).toFixed(2)}MB ${info}`);
|
|
||||||
} else {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[Video Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logHLSMemory: (context = '', additionalInfo = {}) => {
|
|
||||||
const jsMem = getMemoryInfo();
|
|
||||||
const mediaMem = getMediaMemoryInfo();
|
|
||||||
|
|
||||||
if (jsMem && mediaMem) {
|
|
||||||
const hlsVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && video.hlsInfo.isHLS);
|
|
||||||
const progressiveVideos = mediaMem.mediaElements.filter(video => video.hlsInfo && !video.hlsInfo.isHLS);
|
|
||||||
|
|
||||||
const logMsg = formatMemoryLog(jsMem.usedJSHeapSize, jsMem.totalJSHeapSize, jsMem.jsHeapSizeLimit);
|
|
||||||
const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : '';
|
|
||||||
|
|
||||||
console.log(`${logMsg} | HLS Streaming: ${context}`);
|
|
||||||
console.log(`[Streaming Analysis] HLS: ${hlsVideos.length}개, Progressive: ${progressiveVideos.length}개 | Total Video Memory: ${mediaMem.totalVideoBufferMB}MB`);
|
|
||||||
|
|
||||||
// HLS 비디오 상세 정보
|
|
||||||
if (hlsVideos.length > 0) {
|
|
||||||
console.log(`[HLS Videos]`);
|
|
||||||
hlsVideos.forEach(video => {
|
|
||||||
console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Segments: ~${video.hlsInfo.estimatedSegments}개 | Buffer: ${video.bufferedDuration.toFixed(1)}s | Efficiency: ${video.bufferEfficiency}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progressive 비디오 상세 정보
|
|
||||||
if (progressiveVideos.length > 0) {
|
|
||||||
console.log(`[Progressive Videos]`);
|
|
||||||
progressiveVideos.forEach(video => {
|
|
||||||
console.log(` ${video.codecInfo} ${video.videoWidth}x${video.videoHeight} | Format: ${video.hlsInfo.format} | Buffer: ${video.bufferedDuration.toFixed(1)}s`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamingMemoryMB = hlsVideos.reduce((sum, video) => {
|
|
||||||
return sum + parseFloat(video.bufferedDuration) * (video.bitrate / 8 / 1048576);
|
|
||||||
}, 0).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`[Streaming Memory] HLS Buffer: ${streamingMemoryMB}MB | Progressive Buffer: ${(parseFloat(mediaMem.totalVideoBufferMB) - parseFloat(streamingMemoryMB)).toFixed(2)}MB ${info}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return memoryMonitorInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createMemoryMonitor;
|
|
||||||
@@ -38,11 +38,19 @@ import {
|
|||||||
getProductCouponSearch,
|
getProductCouponSearch,
|
||||||
getProductCouponTotDownload,
|
getProductCouponTotDownload,
|
||||||
} from '../../../actions/couponActions.js';
|
} from '../../../actions/couponActions.js';
|
||||||
|
import {
|
||||||
|
sendLogDetail,
|
||||||
|
sendLogGNB,
|
||||||
|
sendLogProductDetail,
|
||||||
|
sendLogShopByMobile,
|
||||||
|
sendLogTotalRecommend,
|
||||||
|
} from '../../../actions/logActions';
|
||||||
// import { pushPanel } from '../../../actions/panelActions';
|
// import { pushPanel } from '../../../actions/panelActions';
|
||||||
import {
|
import {
|
||||||
minimizeModalMedia,
|
minimizeModalMedia,
|
||||||
restoreModalMedia,
|
restoreModalMedia,
|
||||||
} from '../../../actions/mediaActions';
|
} from '../../../actions/mediaActions';
|
||||||
|
import { updatePanel } from '../../../actions/panelActions';
|
||||||
import { pauseFullscreenVideo } from '../../../actions/playActions';
|
import { pauseFullscreenVideo } from '../../../actions/playActions';
|
||||||
import { resetShowAllReviews } from '../../../actions/productActions';
|
import { resetShowAllReviews } from '../../../actions/productActions';
|
||||||
import {
|
import {
|
||||||
@@ -59,7 +67,12 @@ import TVirtualGridList
|
|||||||
import useReviews from '../../../hooks/useReviews/useReviews';
|
import useReviews from '../../../hooks/useReviews/useReviews';
|
||||||
import useScrollTo from '../../../hooks/useScrollTo';
|
import useScrollTo from '../../../hooks/useScrollTo';
|
||||||
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
|
import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig';
|
||||||
import { panel_names } from '../../../utils/Config';
|
import {
|
||||||
|
LOG_CONTEXT_NAME,
|
||||||
|
LOG_MESSAGE_ID,
|
||||||
|
LOG_TP_NO,
|
||||||
|
panel_names,
|
||||||
|
} from '../../../utils/Config';
|
||||||
import * as Config from '../../../utils/Config.js';
|
import * as Config from '../../../utils/Config.js';
|
||||||
import {
|
import {
|
||||||
andThen,
|
andThen,
|
||||||
@@ -76,7 +89,10 @@ import {
|
|||||||
tap,
|
tap,
|
||||||
when,
|
when,
|
||||||
} from '../../../utils/fp';
|
} from '../../../utils/fp';
|
||||||
import { $L } from '../../../utils/helperMethods';
|
import {
|
||||||
|
$L,
|
||||||
|
formatGMTString,
|
||||||
|
} from '../../../utils/helperMethods';
|
||||||
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||||
import ShowUserReviews from '../../UserReview/ShowUserReviews';
|
import ShowUserReviews from '../../UserReview/ShowUserReviews';
|
||||||
// import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar';
|
// import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar';
|
||||||
@@ -242,6 +258,7 @@ export default function ProductAllSection({
|
|||||||
// Redux 상태
|
// Redux 상태
|
||||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||||
const groupInfos = useSelector((state) => state.product.groupInfo);
|
const groupInfos = useSelector((state) => state.product.groupInfo);
|
||||||
|
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||||
|
|
||||||
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
|
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
|
||||||
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
|
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
|
||||||
@@ -263,6 +280,18 @@ export default function ProductAllSection({
|
|||||||
const [isShowQRCode, setIsShowQRCode] = useState(true);
|
const [isShowQRCode, setIsShowQRCode] = useState(true);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
|
// sendLogGNB용 entryMenu
|
||||||
|
const entryMenuRef = useRef(null);
|
||||||
|
|
||||||
|
// 출처 정보 통합 (향후 확장성 대비)
|
||||||
|
// YouMayLike 상품이 아닐 경우 fromPanel을 초기화하여 오기 방지
|
||||||
|
const fromPanel = useMemo(() => ({
|
||||||
|
fromYouMayLike: panelInfo?.fromPanel?.fromYouMayLike || false,
|
||||||
|
// 향후 다른 출처 플래그들 추가 가능
|
||||||
|
// fromRecommendation: panelInfo?.fromPanel?.fromRecommendation || false,
|
||||||
|
// fromSearch: panelInfo?.fromPanel?.fromSearch || false,
|
||||||
|
}), [panelInfo?.fromPanel?.fromYouMayLike]);
|
||||||
|
|
||||||
//구매 하단 토스트 노출 확인을 위한 용도
|
//구매 하단 토스트 노출 확인을 위한 용도
|
||||||
const [openToast, setOpenToast] = useState(false);
|
const [openToast, setOpenToast] = useState(false);
|
||||||
|
|
||||||
@@ -652,6 +681,116 @@ export default function ProductAllSection({
|
|||||||
dispatch(resetShowAllReviews());
|
dispatch(resetShowAllReviews());
|
||||||
}, []); // 빈 dependency array = 마운트 시에만 실행
|
}, []); // 빈 dependency array = 마운트 시에만 실행
|
||||||
|
|
||||||
|
// 제품 상세 버튼 클릭 핸들러 - Source의 handleIndicatorOptions와 동일한 기능
|
||||||
|
const handleIndicatorOptions = useCallback(() => {
|
||||||
|
if (productData && Object.keys(productData).length > 0) {
|
||||||
|
// sendLogDetail - 제품 상세 버튼 클릭 로깅 (Source와 동일)
|
||||||
|
const detailLogParams = {
|
||||||
|
curationId: productData?.curationId ?? "",
|
||||||
|
curationNm: productData?.curationNm ?? "",
|
||||||
|
inDt: "",
|
||||||
|
linkTpCd: panelInfo?.linkTpCd ?? "",
|
||||||
|
logTpNo: LOG_TP_NO.DETAIL.DETAIL_BUTTON_CLICK,
|
||||||
|
patncNm: productData?.patncNm ?? "",
|
||||||
|
patnrId: productData?.patnrId ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(sendLogDetail(detailLogParams));
|
||||||
|
|
||||||
|
// sendLogTotalRecommend - 추천 버튼 클릭 로깅 (Source와 동일)
|
||||||
|
let menuType;
|
||||||
|
if (isTravelProductVisible) {
|
||||||
|
menuType = Config.LOG_MENU.DETAIL_PAGE_TRAVEL_THEME_DETAIL;
|
||||||
|
} else if (isGroupProductVisible) {
|
||||||
|
menuType = Config.LOG_MENU.DETAIL_PAGE_GROUP_DETAIL;
|
||||||
|
} else if (isBillingProductVisible) {
|
||||||
|
menuType = Config.LOG_MENU.DETAIL_PAGE_BILLING_PRODUCT_DETAIL;
|
||||||
|
} else {
|
||||||
|
menuType = Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(sendLogTotalRecommend({
|
||||||
|
menu: menuType,
|
||||||
|
buttonTitle: "DESCRIPTION",
|
||||||
|
contextName: LOG_CONTEXT_NAME.DETAILPAGE,
|
||||||
|
messageId: LOG_MESSAGE_ID.BUTTONCLICK,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [productData, panelInfo, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]);
|
||||||
|
|
||||||
|
// sendLogGNB 로깅 - Source의 DetailPanel 컴포넌트들과 동일한 패턴
|
||||||
|
useEffect(() => {
|
||||||
|
if (!entryMenuRef.current) entryMenuRef.current = nowMenu;
|
||||||
|
|
||||||
|
// BUY NOW 버튼 활성화 상태에 따른 메뉴 결정 (Source SingleProduct vs UnableProduct 패턴)
|
||||||
|
let baseMenu;
|
||||||
|
if (isTravelProductVisible) {
|
||||||
|
baseMenu = Config.LOG_MENU.DETAIL_PAGE_TRAVEL_THEME_DETAIL;
|
||||||
|
} else if (isGroupProductVisible) {
|
||||||
|
baseMenu = Config.LOG_MENU.DETAIL_PAGE_GROUP_DETAIL;
|
||||||
|
} else if (isBillingProductVisible) {
|
||||||
|
// BUY NOW 버튼 활성화 = SingleProduct
|
||||||
|
baseMenu = Config.LOG_MENU.DETAIL_PAGE_BILLING_PRODUCT_DETAIL;
|
||||||
|
} else {
|
||||||
|
// BUY NOW 버튼 비활성화 = UnableProduct
|
||||||
|
baseMenu = Config.LOG_MENU.DETAIL_PAGE_PRODUCT_DETAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// YouMayLike에서 상품 선택 시 메뉴 변경 (Source의 isYouMayLikeOpened와 동일 패턴)
|
||||||
|
const menu = (fromPanel?.fromYouMayLike !== undefined && fromPanel?.fromYouMayLike === true)
|
||||||
|
? `${baseMenu}/${Config.LOG_MENU.DETAIL_PAGE_YOU_MAY_LIKE}`
|
||||||
|
: baseMenu;
|
||||||
|
|
||||||
|
dispatch(sendLogGNB(menu));
|
||||||
|
|
||||||
|
// sendLogGNB 전송 후 플래그 초기화 (1회 사용 후 비활성화)
|
||||||
|
if (fromPanel?.fromYouMayLike === true) {
|
||||||
|
dispatch(updatePanel({
|
||||||
|
name: panel_names.DETAIL_PANEL,
|
||||||
|
panelInfo: {
|
||||||
|
...panelInfo,
|
||||||
|
fromPanel: {
|
||||||
|
fromYouMayLike: false // 플래그 초기화
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [fromPanel?.fromYouMayLike, isBillingProductVisible, isUnavailableProductVisible, isGroupProductVisible, isTravelProductVisible]); // BUY NOW 상태 변경 시 재실행
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}, [productData, entryMenuRef.current, panelInfo?.linkTpCd, isBillingProductVisible, isGroupProductVisible, isTravelProductVisible]); // productData 변경 시 재실행
|
||||||
|
|
||||||
// [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로
|
// [251115] 주석 처리: MediaPanel에서 이미 포커스 이동을 처리하므로
|
||||||
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
|
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -674,6 +813,35 @@ export default function ProductAllSection({
|
|||||||
// console.log('[BuyNow] Buy Now button clicked');
|
// console.log('[BuyNow] Buy Now button clicked');
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 🚀 SingleOption.jsx의 sendLogTotalRecommend 로직 추가
|
||||||
|
if (productData && Object.keys(productData).length > 0) {
|
||||||
|
const { priceInfo, patncNm, prdtId, prdtNm, brndNm, catNm, showId, showNm } = productData;
|
||||||
|
const regularPrice = priceInfo?.split("|")[0];
|
||||||
|
const discountPrice = priceInfo?.split("|")[1];
|
||||||
|
const discountRate = priceInfo?.split("|")[4];
|
||||||
|
|
||||||
|
// Option 정보는 현재 선택된 옵션이 없으므로 기본값 사용
|
||||||
|
const prodOptCval = ""; // 실제로는 선택된 옵션 값이 들어가야 함
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
sendLogTotalRecommend({
|
||||||
|
nowMenu: nowMenu,
|
||||||
|
productId: prdtId,
|
||||||
|
productTitle: prdtNm,
|
||||||
|
partner: patncNm,
|
||||||
|
price: discountRate ? discountPrice : regularPrice,
|
||||||
|
discount: discountRate,
|
||||||
|
brand: brndNm,
|
||||||
|
productOption: prodOptCval,
|
||||||
|
category: catNm,
|
||||||
|
contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE,
|
||||||
|
messageId: Config.LOG_MESSAGE_ID.BUY_NOW,
|
||||||
|
showId: showId ?? "",
|
||||||
|
showNm: showNm ?? "",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[ProductAllSection] 🛒 BUY NOW clicked - productData:', {
|
// console.log('[ProductAllSection] 🛒 BUY NOW clicked - productData:', {
|
||||||
// prdtId: productData?.prdtId,
|
// prdtId: productData?.prdtId,
|
||||||
// patnrId: productData?.patnrId,
|
// patnrId: productData?.patnrId,
|
||||||
@@ -705,7 +873,7 @@ export default function ProductAllSection({
|
|||||||
setOpenToast(true);
|
setOpenToast(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, productData, openToast]
|
[dispatch, productData, openToast, nowMenu]
|
||||||
);
|
);
|
||||||
|
|
||||||
//닫히도록
|
//닫히도록
|
||||||
@@ -926,7 +1094,32 @@ export default function ProductAllSection({
|
|||||||
}, [hasVideo, productVideoVersion]);
|
}, [hasVideo, productVideoVersion]);
|
||||||
|
|
||||||
const handleShopByMobileOpen = useCallback(
|
const handleShopByMobileOpen = useCallback(
|
||||||
pipe(() => true, setMobileSendPopupOpen),
|
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 logParams = {
|
||||||
|
prdtId,
|
||||||
|
patnrId,
|
||||||
|
prdtNm,
|
||||||
|
patncNm,
|
||||||
|
brndNm,
|
||||||
|
catNm,
|
||||||
|
regularPrice,
|
||||||
|
discountPrice,
|
||||||
|
discountRate,
|
||||||
|
shopByMobileTime: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(sendLogShopByMobile(logParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileSendPopupOpen(true); // 팝업 열기
|
||||||
|
}, setMobileSendPopupOpen),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -998,7 +1191,10 @@ export default function ProductAllSection({
|
|||||||
const handleProductDetailsClick = useCallback(() => {
|
const handleProductDetailsClick = useCallback(() => {
|
||||||
dispatch(minimizeModalMedia());
|
dispatch(minimizeModalMedia());
|
||||||
scrollToSection('scroll-marker-product-details');
|
scrollToSection('scroll-marker-product-details');
|
||||||
}, [scrollToSection, dispatch]);
|
|
||||||
|
// Source의 handleIndicatorOptions와 동일한 로깅 기능 추가
|
||||||
|
handleIndicatorOptions();
|
||||||
|
}, [scrollToSection, dispatch, handleIndicatorOptions]);
|
||||||
|
|
||||||
const handleYouMayAlsoLikeClick = useCallback(() => {
|
const handleYouMayAlsoLikeClick = useCallback(() => {
|
||||||
dispatch(minimizeModalMedia());
|
dispatch(minimizeModalMedia());
|
||||||
@@ -1592,6 +1788,7 @@ export default function ProductAllSection({
|
|||||||
onScrollToImages={handleScrollToImagesV1}
|
onScrollToImages={handleScrollToImagesV1}
|
||||||
onFocus={() => {}}
|
onFocus={() => {}}
|
||||||
data-spotlight-id="product-video-player-container"
|
data-spotlight-id="product-video-player-container"
|
||||||
|
disclaimer={productData.disclaimer}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProductVideoV2
|
<ProductVideoV2
|
||||||
|
|||||||
@@ -215,3 +215,32 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
width: 100%;
|
||||||
|
height: 54px;
|
||||||
|
background: #000000;
|
||||||
|
.flex(@justifyCenter:flex-start);
|
||||||
|
padding: 6px 18px 18px 18px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
|
||||||
|
.marquee {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 10px 12px 0 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-align: left;
|
||||||
|
.font(@fontFamily:@baseFont, @fontSize:20px);
|
||||||
|
color: @COLOR_GRAY04;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
import React, {
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useDispatch,
|
||||||
|
useSelector,
|
||||||
|
} from 'react-redux';
|
||||||
|
|
||||||
|
import Marquee from '@enact/sandstone/Marquee';
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
|
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||||
|
import ic_warning from '../../../../../assets/images/icons/ic-warning@3x.png';
|
||||||
import {
|
import {
|
||||||
startMediaPlayer,
|
|
||||||
finishMediaPreview,
|
finishMediaPreview,
|
||||||
switchMediaToFullscreen,
|
|
||||||
minimizeModalMedia,
|
minimizeModalMedia,
|
||||||
restoreModalMedia,
|
restoreModalMedia,
|
||||||
|
startMediaPlayer,
|
||||||
|
switchMediaToFullscreen,
|
||||||
} from '../../../../actions/mediaActions';
|
} from '../../../../actions/mediaActions';
|
||||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||||
import { panel_names } from '../../../../utils/Config';
|
import { panel_names } from '../../../../utils/Config';
|
||||||
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
|
|
||||||
import css from './ProductVideo.module.less';
|
import css from './ProductVideo.module.less';
|
||||||
|
|
||||||
const SpottableComponent = Spottable('div');
|
const SpottableComponent = Spottable('div');
|
||||||
@@ -25,6 +39,7 @@ export default function ProductVideo({
|
|||||||
autoPlay = false, // 자동 재생 여부
|
autoPlay = false, // 자동 재생 여부
|
||||||
continuousPlay = false, // 반복 재생 여부
|
continuousPlay = false, // 반복 재생 여부
|
||||||
onFocus = null, // 외부에서 전달된 포커스 핸들러
|
onFocus = null, // 외부에서 전달된 포커스 핸들러
|
||||||
|
disclaimer,
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -315,6 +330,12 @@ export default function ProductVideo({
|
|||||||
<img src={playImg} alt="재생" />
|
<img src={playImg} alt="재생" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={css.notice}>
|
||||||
|
<Marquee className={css.marquee} marqueeOn="render">
|
||||||
|
<img src={ic_warning} alt={disclaimer} />
|
||||||
|
<span>{disclaimer}</span>
|
||||||
|
</Marquee>
|
||||||
|
</div>
|
||||||
</SpottableComponent>
|
</SpottableComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ export default function YouMayAlsoLike({
|
|||||||
prdtId,
|
prdtId,
|
||||||
launchedFromPlayer: launchedFromPlayer,
|
launchedFromPlayer: launchedFromPlayer,
|
||||||
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
|
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
|
||||||
|
fromPanel: {
|
||||||
|
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
|
||||||
|
}, // 출처 정보 통합 객체
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
|
|
||||||
import IcPartnersDefault from "../../../../assets/images/ic-tab-partners-default@3x.png";
|
import IcPartnersDefault from "../../../../assets/images/ic-tab-partners-default@3x.png";
|
||||||
|
import NBCULogoImage from "../../../../assets/images/featuredBrands/image-nbcu.png";
|
||||||
import CustomImage from "../../../components/CustomImage/CustomImage";
|
import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||||
import css from "./Banner.module.less";
|
import css from "./Banner.module.less";
|
||||||
|
|
||||||
@@ -15,16 +16,20 @@ export default memo(function Banner({
|
|||||||
const { patncLogoPath, patncNm } = selectedBrandInfo;
|
const { patncLogoPath, patncNm } = selectedBrandInfo;
|
||||||
const { topImgAlt, topImgPath } = brandTopImgInfo;
|
const { topImgAlt, topImgPath } = brandTopImgInfo;
|
||||||
|
|
||||||
|
// NBCU 로고 이미지 처리
|
||||||
|
const logoSrc = panelPatnrId === 'NBCU' ? NBCULogoImage : patncLogoPath;
|
||||||
|
const logoName = panelPatnrId === 'NBCU' ? 'Peacock' : patncNm;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css.container}>
|
<div className={css.container}>
|
||||||
<figure>
|
<figure>
|
||||||
<CustomImage
|
<CustomImage
|
||||||
src={patncLogoPath}
|
src={logoSrc}
|
||||||
alt={patncNm}
|
alt={logoName}
|
||||||
fallbackSrc={IcPartnersDefault}
|
fallbackSrc={IcPartnersDefault}
|
||||||
ariaLabel={patncNm}
|
ariaLabel={logoName}
|
||||||
/>
|
/>
|
||||||
<figcaption>{patncNm}</figcaption>
|
<figcaption>{logoName}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<CustomImage src={topImgPath} alt={topImgAlt} ariaLabel={topImgAlt} />
|
<CustomImage src={topImgPath} alt={topImgAlt} ariaLabel={topImgAlt} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import css from "./FeaturedBrandsPanel.module.less";
|
|||||||
import FeaturedCategory from "./FeaturedCategory/FeaturedCategory";
|
import FeaturedCategory from "./FeaturedCategory/FeaturedCategory";
|
||||||
import FeaturedCreators from "./FeaturedCreators/FeaturedCreators";
|
import FeaturedCreators from "./FeaturedCreators/FeaturedCreators";
|
||||||
import LiveChannels from "./LiveChannels/LiveChannels";
|
import LiveChannels from "./LiveChannels/LiveChannels";
|
||||||
|
import NBCUContent from "./NBCUContent/NBCUContent";
|
||||||
import QuickMenu from "./QuickMenu/QuickMenu";
|
import QuickMenu from "./QuickMenu/QuickMenu";
|
||||||
import RecommendedShows from "./RecommendedShows/RecommendedShows";
|
import RecommendedShows from "./RecommendedShows/RecommendedShows";
|
||||||
import Series from "./Series/Series";
|
import Series from "./Series/Series";
|
||||||
@@ -68,6 +69,7 @@ import TodaysDeals from "./TodaysDeals/TodaysDeals";
|
|||||||
import UpComing from "./UpComing/UpComing";
|
import UpComing from "./UpComing/UpComing";
|
||||||
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container";
|
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container";
|
||||||
import { sortedIndexOf } from "lodash";
|
import { sortedIndexOf } from "lodash";
|
||||||
|
import NBCUBgImage from "../../../assets/images/featuredBrands/image-bg.png";
|
||||||
|
|
||||||
const STRING_CONF = {
|
const STRING_CONF = {
|
||||||
CANCEL: "CANCEL",
|
CANCEL: "CANCEL",
|
||||||
@@ -81,6 +83,7 @@ const STRING_CONF = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TEMPLATE_CODE_CONF = {
|
const TEMPLATE_CODE_CONF = {
|
||||||
|
NBCU: "NBU00100",
|
||||||
LIVE_CHANNELS: "BRD00101",
|
LIVE_CHANNELS: "BRD00101",
|
||||||
UP_COMING: "BRD00102",
|
UP_COMING: "BRD00102",
|
||||||
TODAYS_DEALS: "BRD00103",
|
TODAYS_DEALS: "BRD00103",
|
||||||
@@ -304,8 +307,42 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sortedBrandLayoutInfo = useMemo(
|
const sortedBrandLayoutInfo = useMemo(
|
||||||
() => brandLayoutInfo?.sort((a, b) => a.expsOrd - b.expsOrd) ?? [],
|
() => {
|
||||||
[brandLayoutInfo]
|
if (!panelInfo?.patnrId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// NBCU 특별 처리
|
||||||
|
if (panelInfo?.patnrId === 'NBCU') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
shptmBrndOptTpCd: TEMPLATE_CODE_CONF.NBCU,
|
||||||
|
shptmBrndOptTpNm: 'NBCU',
|
||||||
|
expsOrd: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return brandLayoutInfo?.sort((a, b) => a.expsOrd - b.expsOrd) ?? [];
|
||||||
|
},
|
||||||
|
[brandLayoutInfo, panelInfo?.patnrId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedBrandTopImgInfo = useMemo(
|
||||||
|
() => {
|
||||||
|
// NBCU 특별 처리
|
||||||
|
if (panelInfo?.patnrId === 'NBCU') {
|
||||||
|
return {
|
||||||
|
topImgPath: NBCUBgImage,
|
||||||
|
topImgAlt: 'NBCU Background Image',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 다른 브랜드: brandTopImgInfo가 유효한 객체여야 함
|
||||||
|
if (brandTopImgInfo && brandTopImgInfo.topImgPath) {
|
||||||
|
return brandTopImgInfo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[brandTopImgInfo, panelInfo?.patnrId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const doSendLogGNB = useCallback(
|
const doSendLogGNB = useCallback(
|
||||||
@@ -415,6 +452,21 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
|||||||
<>
|
<>
|
||||||
{sortedBrandLayoutInfo.map((el, idx) => {
|
{sortedBrandLayoutInfo.map((el, idx) => {
|
||||||
switch (el.shptmBrndOptTpCd) {
|
switch (el.shptmBrndOptTpCd) {
|
||||||
|
case TEMPLATE_CODE_CONF.NBCU: {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={el.shptmBrndOptTpCd}>
|
||||||
|
<NBCUContent
|
||||||
|
handleItemFocus={handleItemFocus}
|
||||||
|
spotlightId={TEMPLATE_CODE_CONF.NBCU}
|
||||||
|
shelfOrder={el.expsOrd}
|
||||||
|
shelfTitle={el.shptmBrndOptTpNm}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
order={idx + 1}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
|
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={el.shptmBrndOptTpCd}>
|
<React.Fragment key={el.shptmBrndOptTpCd}>
|
||||||
@@ -709,7 +761,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
|||||||
|
|
||||||
// effect: layout information fetching due to partner id change
|
// effect: layout information fetching due to partner id change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fromDetail) {
|
if (!fromDetail && panelInfo?.patnrId) {
|
||||||
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
|
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
|
||||||
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
|
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
|
||||||
setIsInitialFocusOccurred(false);
|
setIsInitialFocusOccurred(false);
|
||||||
@@ -719,13 +771,20 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
|||||||
|
|
||||||
// effect: set selectedPatnrId and selectedPatncNm
|
// effect: set selectedPatnrId and selectedPatncNm
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (brandInfo) {
|
if (brandInfo || panelInfo?.patnrId) {
|
||||||
const patnrId = panelInfo?.patnrId;
|
const patnrId = panelInfo?.patnrId;
|
||||||
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId).patncNm;
|
|
||||||
|
|
||||||
setSelectedPatncNm(patncNm);
|
// NBCU 특별 처리
|
||||||
|
if (patnrId === 'NBCU') {
|
||||||
if (!fromDetail) setSelectedPatnrId(patnrId);
|
setSelectedPatncNm('NBCU');
|
||||||
|
if (!fromDetail) setSelectedPatnrId('NBCU');
|
||||||
|
} else if (brandInfo) {
|
||||||
|
const brandItem = brandInfo.find((b) => b?.patnrId === patnrId);
|
||||||
|
if (brandItem) {
|
||||||
|
setSelectedPatncNm(brandItem.patncNm);
|
||||||
|
if (!fromDetail) setSelectedPatnrId(patnrId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [brandInfo, panelInfo?.patnrId]);
|
}, [brandInfo, panelInfo?.patnrId]);
|
||||||
|
|
||||||
@@ -958,10 +1017,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{brandInfo && brandTopImgInfo && (
|
{((brandInfo && processedBrandTopImgInfo) || panelInfo?.patnrId === 'NBCU') && processedBrandTopImgInfo && (
|
||||||
<Banner
|
<Banner
|
||||||
brandInfo={brandInfo}
|
brandInfo={brandInfo}
|
||||||
brandTopImgInfo={brandTopImgInfo}
|
brandTopImgInfo={processedBrandTopImgInfo}
|
||||||
panelPatnrId={panelInfo?.patnrId}
|
panelPatnrId={panelInfo?.patnrId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import React, { memo, useCallback, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import Spotlight from "@enact/spotlight";
|
||||||
|
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||||
|
import Spottable from "@enact/spotlight/Spottable";
|
||||||
|
|
||||||
|
import SectionTitle from "../../../components/SectionTitle/SectionTitle";
|
||||||
|
import NBCUSectionTitle from "./NBCUSectionTitle/NBCUSectionTitle";
|
||||||
|
import { $L } from "../../../utils/helperMethods";
|
||||||
|
import css from "./NBCUContent.module.less";
|
||||||
|
import NBCUList from "./NBCUList/NBCUList";
|
||||||
|
import NBCUSeries from "./NBCUSeries/NBCUSeries";
|
||||||
|
import seriesCard1 from "../../../../assets/images/featuredBrands/series-card-1.png";
|
||||||
|
import seriesCard2 from "../../../../assets/images/featuredBrands/series-card-2.png";
|
||||||
|
import seriesCard3 from "../../../../assets/images/featuredBrands/series-card-3.png";
|
||||||
|
|
||||||
|
const STRING_CONF = {
|
||||||
|
NBCU: "NBCU",
|
||||||
|
PICKED_FOR_YOU: "PICKED FOR YOU",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock data for Series
|
||||||
|
const MOCK_BRAND_SERIES_GROUP_INFO = [
|
||||||
|
{
|
||||||
|
seriesId: "series-1",
|
||||||
|
seriesNm: "Drama Collection",
|
||||||
|
seriesImgUrl: seriesCard1,
|
||||||
|
patnrId: "nbcu-partner-1",
|
||||||
|
brandSeriesProductInfo: Array.from({ length: 6 }).map((_, i) => ({
|
||||||
|
productId: `drama-${i}`,
|
||||||
|
productNm: `Drama Show ${i + 1}`,
|
||||||
|
imageUrl: "assets/images/img-thumb-empty-product@3x.png",
|
||||||
|
priceInfo: "$15.00|$10.00|N|$5.00|33%|PROMO|2025-12-31",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seriesId: "series-2",
|
||||||
|
seriesNm: "Comedy Series",
|
||||||
|
seriesImgUrl: seriesCard2,
|
||||||
|
patnrId: "nbcu-partner-1",
|
||||||
|
brandSeriesProductInfo: Array.from({ length: 6 }).map((_, i) => ({
|
||||||
|
productId: `comedy-${i}`,
|
||||||
|
productNm: `Comedy Show ${i + 1}`,
|
||||||
|
imageUrl: "assets/images/img-thumb-empty-product@3x.png",
|
||||||
|
priceInfo: "$12.00|$8.00|N|$4.00|33%|PROMO|2025-12-31",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seriesId: "series-3",
|
||||||
|
seriesNm: "Sci-Fi Originals",
|
||||||
|
seriesImgUrl: seriesCard3,
|
||||||
|
patnrId: "nbcu-partner-1",
|
||||||
|
brandSeriesProductInfo: Array.from({ length: 6 }).map((_, i) => ({
|
||||||
|
productId: `scifi-${i}`,
|
||||||
|
productNm: `Sci-Fi Show ${i + 1}`,
|
||||||
|
imageUrl: "assets/images/img-thumb-empty-product@3x.png",
|
||||||
|
priceInfo: "$18.00|$12.00|N|$6.00|33%|PROMO|2025-12-31",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_BRAND_SERIES_INFO = [
|
||||||
|
{ seriesId: "series-1", seriesNm: "LOVE ISLAND" },
|
||||||
|
{ seriesId: "series-2", seriesNm: "TOP CHEF" },
|
||||||
|
{ seriesId: "series-3", seriesNm: "BELOW DECK" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SpottableDiv = Spottable('div');
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{ leaveFor: { right: "" }, enterTo: "last-focused" },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const NBCUContent = ({
|
||||||
|
handleItemFocus,
|
||||||
|
spotlightId,
|
||||||
|
shelfOrder,
|
||||||
|
selectedPatnrId,
|
||||||
|
shelfTitle,
|
||||||
|
order,
|
||||||
|
}) => {
|
||||||
|
const [firstChk, setFirstChk] = useState(0);
|
||||||
|
const [selectedSeriesId, setSelectedSeriesId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[NBCUContent] Rendered. order:', order);
|
||||||
|
}, [order]);
|
||||||
|
|
||||||
|
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 = "NBCU, 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("NBCU, Heading1,", "");
|
||||||
|
c.setAttribute("aria-label", newcAriaLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [handleItemFocus, firstChk, spotlightId, shelfOrder]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
className={css.container}
|
||||||
|
data-shelf-order={order}
|
||||||
|
data-wheel-point
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
>
|
||||||
|
<SectionTitle
|
||||||
|
title={$L(STRING_CONF.NBCU)}
|
||||||
|
data-title="nbcu"
|
||||||
|
label="NBCU Heading 1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<NBCUList
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
shelfOrder={shelfOrder}
|
||||||
|
shelfTitle={shelfTitle}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Keyword Bubble Section (Dummy) */}
|
||||||
|
{/* <div className={css.keywordContainer}>
|
||||||
|
|
||||||
|
{['Action', 'Comedy', 'Drama', 'Sci-Fi', 'Thriller', 'Romance', 'Documentary'].map((keyword, index) => (
|
||||||
|
<SpottableDiv
|
||||||
|
key={index}
|
||||||
|
className={css.keywordBubble}
|
||||||
|
onClick={() => console.log(`Clicked keyword: ${keyword}`)}
|
||||||
|
>
|
||||||
|
{keyword}
|
||||||
|
</SpottableDiv>
|
||||||
|
))}
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Picked For You Section Title */}
|
||||||
|
<NBCUSectionTitle
|
||||||
|
title={$L(STRING_CONF.PICKED_FOR_YOU)}
|
||||||
|
data-title="picked-for-you"
|
||||||
|
label="Picked For You Heading"
|
||||||
|
isBlack={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Series Component with Mock Data */}
|
||||||
|
<NBCUSeries
|
||||||
|
brandSeriesGroupInfo={MOCK_BRAND_SERIES_GROUP_INFO}
|
||||||
|
brandSeriesInfo={MOCK_BRAND_SERIES_INFO}
|
||||||
|
fromGNB={false}
|
||||||
|
fromQuickMenu={false}
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
order={order}
|
||||||
|
shelfOrder={shelfOrder}
|
||||||
|
shelfTitle={shelfTitle}
|
||||||
|
spotlightId={`${spotlightId}-series`}
|
||||||
|
selectedPatncNm="NBCU"
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
selectedSeriesId={selectedSeriesId}
|
||||||
|
setSelectedSeriesId={setSelectedSeriesId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(NBCUContent);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@import "../../../style/CommonStyle.module.less";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 50px 0; // Adjust padding as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywordContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 60px; // Match side padding of other contents
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywordBubble {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:focus, &:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: #ffffff;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import Spotlight from '@enact/spotlight';
|
||||||
|
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
|
|
||||||
|
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
|
||||||
|
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
||||||
|
import css from './NBCUList.module.less';
|
||||||
|
|
||||||
|
// 더미 데이터 생성
|
||||||
|
// priceInfo format: originalPrice|discountedPrice|rewardFlag|discountAmount|discountRate|promotionCode|promotionDate
|
||||||
|
const DUMMY_DATA = Array.from({ length: 10 }).map((_, index) => ({
|
||||||
|
id: `nbcu-item-${index}`,
|
||||||
|
title: `NBCU Content ${index + 1}`,
|
||||||
|
imgUrl: 'assets/images/img-thumb-empty-product@3x.png',
|
||||||
|
priceInfo: '$20.00|$10.00|N|$10.00|50%|PROMO|2025-12-31',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{ leaveFor: { right: '' }, enterTo: 'last-focused' },
|
||||||
|
'div'
|
||||||
|
);
|
||||||
|
|
||||||
|
const NBCUList = ({ handleItemFocus, spotlightId, shelfTitle, shelfOrder }) => {
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ index, ...rest }) => {
|
||||||
|
const item = DUMMY_DATA[index];
|
||||||
|
const labelText = `${index + 1} of ${DUMMY_DATA.length}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TItemCardNew
|
||||||
|
{...rest}
|
||||||
|
key={item.id}
|
||||||
|
imageSource={item.imgUrl}
|
||||||
|
productName={item.title}
|
||||||
|
priceInfo={item.priceInfo}
|
||||||
|
spotlightId={`nbcu-spotlightId-${index}`}
|
||||||
|
shelfId={spotlightId}
|
||||||
|
shelfLocation={shelfOrder}
|
||||||
|
shelfTitle={shelfTitle}
|
||||||
|
label={labelText}
|
||||||
|
onFocus={handleItemFocus}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('Clicked NBCU item:', item.title);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[handleItemFocus, spotlightId, shelfOrder, shelfTitle]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className={css.container} spotlightId="nbcu-list-id">
|
||||||
|
<TVirtualGridList
|
||||||
|
dataSize={DUMMY_DATA.length}
|
||||||
|
direction="horizontal"
|
||||||
|
itemHeight={438}
|
||||||
|
itemWidth={324}
|
||||||
|
spacing={18}
|
||||||
|
renderItem={renderItem}
|
||||||
|
className={css.tVirtualGridList}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NBCUList);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@import "../../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
.size(@w: 100%, @h: 438px);
|
||||||
|
padding-right: 18px;
|
||||||
|
|
||||||
|
// tVirtualGridListContainer
|
||||||
|
> div:nth-child(1) {
|
||||||
|
.size(@w: 100%, @h: inherit);
|
||||||
|
|
||||||
|
&.tVirtualGridList {
|
||||||
|
padding-left: 60px;
|
||||||
|
|
||||||
|
> div:nth-child(3) {
|
||||||
|
right: -18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import css from "./NBCUSectionTitle.module.less";
|
||||||
|
|
||||||
|
export default memo(function NBCUSectionTitle({
|
||||||
|
className,
|
||||||
|
itemCount,
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
isBlack = false,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={classNames(css.sectionTitle, isBlack && css.blackTitle, className)}
|
||||||
|
aria-label={label ? label : title}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{itemCount && <span>({itemCount})</span>}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@import "../../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
position: relative;
|
||||||
|
.flex(@justifyCenter: flex-start);
|
||||||
|
min-height: 50px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 42px;
|
||||||
|
color: #000000 !important;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
content: "";
|
||||||
|
.size(@w: 6px, @h: 36px);
|
||||||
|
margin-right: 12px;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackTitle {
|
||||||
|
color: #000000;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { memo, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import Spotlight from "@enact/spotlight";
|
||||||
|
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||||
|
|
||||||
|
import NBCUSectionTitle from "../NBCUSectionTitle/NBCUSectionTitle";
|
||||||
|
import { $L } from "../../../../utils/helperMethods";
|
||||||
|
import css from "./NBCUSeries.module.less";
|
||||||
|
import SeriesContents from "../../Series/SeriesContents/SeriesContents";
|
||||||
|
import SeriesNav from "../../Series/SeriesNav/SeriesNav";
|
||||||
|
|
||||||
|
const STRING_CONF = {
|
||||||
|
SERIES: "SERIES",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{ leaveFor: { right: "" }, enterTo: "last-focused" },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const NBCUSeries = ({
|
||||||
|
brandSeriesGroupInfo,
|
||||||
|
brandSeriesInfo,
|
||||||
|
fromGNB,
|
||||||
|
fromQuickMenu,
|
||||||
|
handleItemFocus,
|
||||||
|
order,
|
||||||
|
shelfOrder,
|
||||||
|
shelfTitle,
|
||||||
|
spotlightId,
|
||||||
|
selectedPatncNm,
|
||||||
|
selectedPatnrId,
|
||||||
|
selectedSeriesId,
|
||||||
|
setSelectedSeriesId,
|
||||||
|
}) => {
|
||||||
|
const [filteredBrandSeriesGroupInfo, setFilteredSeriesGroupInfo] = useState();
|
||||||
|
const [firstChk, setFirstChk] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSeriesId) {
|
||||||
|
return setFilteredSeriesGroupInfo(brandSeriesGroupInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredSeriesGroupInfo(
|
||||||
|
brandSeriesGroupInfo.filter(
|
||||||
|
({ seriesId }) => seriesId === selectedSeriesId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [brandSeriesGroupInfo, selectedSeriesId]);
|
||||||
|
|
||||||
|
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 = "series, 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("series, Heading1,", "");
|
||||||
|
c.setAttribute("aria-label", newcAriaLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [handleItemFocus, firstChk]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
className={css.container}
|
||||||
|
data-shelf-order={order}
|
||||||
|
data-wheel-point
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
>
|
||||||
|
{/* <NBCUSectionTitle title={$L(STRING_CONF.SERIES)} data-title="series" isBlack={true} /> */}
|
||||||
|
<SeriesNav
|
||||||
|
brandSeriesInfo={brandSeriesInfo}
|
||||||
|
fromGNB={fromGNB}
|
||||||
|
fromQuickMenu={fromQuickMenu}
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
selectedPatncNm={selectedPatncNm}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
selectedSeriesId={selectedSeriesId}
|
||||||
|
setSelectedSeriesId={setSelectedSeriesId}
|
||||||
|
/>
|
||||||
|
{filteredBrandSeriesGroupInfo &&
|
||||||
|
filteredBrandSeriesGroupInfo.map(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
brandSeriesProductInfo,
|
||||||
|
patnrId,
|
||||||
|
seriesId,
|
||||||
|
seriesImgUrl,
|
||||||
|
seriesNm,
|
||||||
|
},
|
||||||
|
contentsIndex
|
||||||
|
) => (
|
||||||
|
<SeriesContents
|
||||||
|
brandSeriesProductInfo={brandSeriesProductInfo}
|
||||||
|
filteredBrandLength={filteredBrandSeriesGroupInfo.length}
|
||||||
|
contentsIndex={contentsIndex}
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
isCarousel={!selectedSeriesId}
|
||||||
|
key={`${spotlightId}-${contentsIndex}`}
|
||||||
|
patnrId={patnrId}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
selectedSeriesId={selectedSeriesId}
|
||||||
|
seriesId={seriesId}
|
||||||
|
seriesImgUrl={seriesImgUrl}
|
||||||
|
seriesNm={seriesNm}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
shelfOrder={shelfOrder}
|
||||||
|
shelfTitle={shelfTitle}
|
||||||
|
selectedPatncNm={selectedPatncNm}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(NBCUSeries);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@import "../../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDeco
|
|||||||
import TScroller from "../../../components/TScroller/TScroller";
|
import TScroller from "../../../components/TScroller/TScroller";
|
||||||
import useScrollTo from "../../../hooks/useScrollTo";
|
import useScrollTo from "../../../hooks/useScrollTo";
|
||||||
import { scaleW } from "../../../utils/helperMethods";
|
import { scaleW } from "../../../utils/helperMethods";
|
||||||
import QuickMenuItem from "../QuickMenu/QuickMenuItem/QuickMenuItem";
|
import QuickMenuItem from "./QuickMenuItem/QuickMenuItem";
|
||||||
|
import QuickMenuItemNBCU from "./QuickMenuItemNBCU/QuickMenuItemNBCU";
|
||||||
import css from "./QuickMenu.module.less";
|
import css from "./QuickMenu.module.less";
|
||||||
|
|
||||||
const Container = SpotlightContainerDecorator(
|
const Container = SpotlightContainerDecorator(
|
||||||
@@ -76,18 +77,44 @@ const QuickMenu = ({
|
|||||||
noScrollByWheel
|
noScrollByWheel
|
||||||
>
|
>
|
||||||
<ul ref={ulRef}>
|
<ul ref={ulRef}>
|
||||||
{brandInfo.map((brandInfoItem, itemIndex) => (
|
{panelPatnrId === 'NBCU' ? (
|
||||||
<QuickMenuItem
|
<>
|
||||||
brandInfoItem={brandInfoItem}
|
<QuickMenuItemNBCU
|
||||||
itemIndex={itemIndex}
|
itemIndex={0}
|
||||||
handleItemFocus={_handleItemFocus}
|
handleItemFocus={_handleItemFocus}
|
||||||
key={"brand-info" + itemIndex}
|
key="nbcu-item"
|
||||||
resetStates={resetStates}
|
resetStates={resetStates}
|
||||||
scrollLeft={scrollLeft}
|
scrollLeft={scrollLeft}
|
||||||
selectedPatnrId={selectedPatnrId}
|
selectedPatnrId={selectedPatnrId}
|
||||||
label={itemIndex * 1 + 1 + " of " + brandInfo.length}
|
label={"1 of " + (brandInfo.length + 1)}
|
||||||
/>
|
/>
|
||||||
))}
|
{brandInfo.map((brandInfoItem, itemIndex) => (
|
||||||
|
<QuickMenuItem
|
||||||
|
brandInfoItem={brandInfoItem}
|
||||||
|
itemIndex={itemIndex + 1}
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
key={"brand-info" + itemIndex}
|
||||||
|
resetStates={resetStates}
|
||||||
|
scrollLeft={scrollLeft}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
label={(itemIndex + 2) + " of " + (brandInfo.length + 1)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
brandInfo.map((brandInfoItem, itemIndex) => (
|
||||||
|
<QuickMenuItem
|
||||||
|
brandInfoItem={brandInfoItem}
|
||||||
|
itemIndex={itemIndex}
|
||||||
|
handleItemFocus={_handleItemFocus}
|
||||||
|
key={"brand-info" + itemIndex}
|
||||||
|
resetStates={resetStates}
|
||||||
|
scrollLeft={scrollLeft}
|
||||||
|
selectedPatnrId={selectedPatnrId}
|
||||||
|
label={(itemIndex + 1) + " of " + brandInfo.length}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</TScroller>
|
</TScroller>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import React, { memo, useCallback } from "react";
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import Spottable from "@enact/spotlight/Spottable";
|
||||||
|
|
||||||
|
import IcPartnersDefault from "../../../../../assets/images/ic-tab-partners-default@3x.png";
|
||||||
|
import { resetPanels, updatePanel } from "../../../../actions/panelActions";
|
||||||
|
import CustomImage from "../../../../components/CustomImage/CustomImage";
|
||||||
|
import useScrollReset from "../../../../hooks/useScrollReset";
|
||||||
|
import { panel_names } from "../../../../utils/Config";
|
||||||
|
import css from "./QuickMenuItemNBCU.module.less";
|
||||||
|
|
||||||
|
const SpottableComponent = Spottable("li");
|
||||||
|
|
||||||
|
const QuickMenuItemNBCU = ({
|
||||||
|
itemIndex,
|
||||||
|
handleItemFocus,
|
||||||
|
resetStates,
|
||||||
|
scrollLeft,
|
||||||
|
selectedPatnrId,
|
||||||
|
label,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { handleScrollReset, handleStopScrolling } = useScrollReset(
|
||||||
|
scrollLeft,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
|
||||||
|
|
||||||
|
const patnrId = "NBCU";
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
if (itemIndex !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStopScrolling();
|
||||||
|
}, [handleStopScrolling, itemIndex]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = "menu";
|
||||||
|
const name = panel_names.FEATURED_BRANDS_PANEL;
|
||||||
|
|
||||||
|
dispatch(resetPanels([{ name }]));
|
||||||
|
dispatch(updatePanel({ name, panelInfo: { from, patnrId } }));
|
||||||
|
resetStates();
|
||||||
|
}, [dispatch, patnrId, resetStates, selectedPatnrId]);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
if (handleItemFocus) handleItemFocus();
|
||||||
|
|
||||||
|
if (itemIndex !== 0) return;
|
||||||
|
|
||||||
|
handleScrollReset();
|
||||||
|
}, [handleScrollReset, handleItemFocus, itemIndex]);
|
||||||
|
|
||||||
|
const selected =
|
||||||
|
(selectedPatnrId ?? panelInfo?.patnrId) === patnrId ? "Selected, " : "";
|
||||||
|
const ariaLabel = selected + "Channel NBCU, Tap " + label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpottableComponent
|
||||||
|
className={classNames(
|
||||||
|
css.brand,
|
||||||
|
(selectedPatnrId ?? panelInfo?.patnrId) === patnrId && css.selected
|
||||||
|
)}
|
||||||
|
data-menu-index={itemIndex}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onClick={handleClick}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
spotlightId={"spotlightId-NBCU"}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<CustomImage
|
||||||
|
src="assets/images/featuredBrands/image-nbcu.png"
|
||||||
|
alt="NBCU"
|
||||||
|
fallbackSrc={IcPartnersDefault}
|
||||||
|
ariaLabel="NBCU"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SpottableComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(QuickMenuItemNBCU);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@import "../../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
position: relative;
|
||||||
|
.flex();
|
||||||
|
.size(@w: 144px, @h: 144px);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
position: relative;
|
||||||
|
// NBCU image
|
||||||
|
> img {
|
||||||
|
.size(@w: 120px, @h: 120px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
// NBCU image
|
||||||
|
.size(@w: 144px, @h: 144px);
|
||||||
|
> div {
|
||||||
|
&:after {
|
||||||
|
.focused(@boxShadow: 0px, @borderRadius: 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> img {
|
||||||
|
.size(@w: 144px, @h: 144px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
// NBCU image
|
||||||
|
&:after {
|
||||||
|
.size(@w:100%, @h:6px);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -18px;
|
||||||
|
background: @PRIMARY_COLOR_RED;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
&:after {
|
||||||
|
.focused(@boxShadow: 0px, @borderRadius: 50%);
|
||||||
|
border-color: @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
> img {
|
||||||
|
.size(@w: 120px, @h: 120px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,6 @@ import css from './PlayerPanel.module.less';
|
|||||||
import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton';
|
import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton';
|
||||||
import TabContainer from './PlayerTabContents/TabContainer';
|
import TabContainer from './PlayerTabContents/TabContainer';
|
||||||
import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2';
|
import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2';
|
||||||
import createMemoryMonitor from '../../utils/memoryMonitor';
|
|
||||||
// import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer';
|
// import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer';
|
||||||
// import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer';
|
// import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer';
|
||||||
// import ShopNowButton from './PlayerTabContents/v2/ShopNowButton';
|
// import ShopNowButton from './PlayerTabContents/v2/ShopNowButton';
|
||||||
@@ -180,7 +179,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
const focusReturnRef = useRef(null);
|
const focusReturnRef = useRef(null);
|
||||||
const modalPrevRef = useRef(panelInfo?.modal);
|
const modalPrevRef = useRef(panelInfo?.modal);
|
||||||
const prevIsTopPanelDetailFromPlayerRef = useRef(false);
|
const prevIsTopPanelDetailFromPlayerRef = useRef(false);
|
||||||
const memoryMonitor = useRef(null);
|
|
||||||
const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', '');
|
const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', '');
|
||||||
const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo');
|
const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo');
|
||||||
const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0);
|
const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0);
|
||||||
@@ -294,8 +292,15 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
if (liveShowInfos && liveShowInfos.length > 0) {
|
if (liveShowInfos && liveShowInfos.length > 0) {
|
||||||
const panelInfoChanId = panelInfo?.chanId;
|
const panelInfoChanId = panelInfo?.chanId;
|
||||||
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE';
|
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE';
|
||||||
|
const isModal = panelInfo?.modal;
|
||||||
|
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
|
// live full 화면에서 modal 전환시 로그 전송 추가
|
||||||
|
if (isModal) {
|
||||||
|
dispatch(sendLogGNB(Config.LOG_MENU.FULL));
|
||||||
|
prevNowMenuRef.current = nowMenuRef.current;
|
||||||
|
return () => dispatch(sendLogGNB(prevNowMenuRef.current));
|
||||||
|
}
|
||||||
const liveShowInfo = liveShowInfos //
|
const liveShowInfo = liveShowInfos //
|
||||||
.find(({ chanId }) => panelInfoChanId === chanId);
|
.find(({ chanId }) => panelInfoChanId === chanId);
|
||||||
|
|
||||||
@@ -306,7 +311,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm]);
|
}, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm, panelInfo?.modal]);
|
||||||
|
|
||||||
const currentVODShowInfo = useMemo(() => {
|
const currentVODShowInfo = useMemo(() => {
|
||||||
if (showDetailInfo && showDetailInfo.length > 0) {
|
if (showDetailInfo && showDetailInfo.length > 0) {
|
||||||
@@ -330,7 +335,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
prevNowMenuRef.current = nowMenuRef.current;
|
prevNowMenuRef.current = nowMenuRef.current;
|
||||||
|
|
||||||
return () => dispatch(sendLogGNB(prevNowMenuRef.current));
|
return () => dispatch(sendLogGNB(prevNowMenuRef.current));
|
||||||
} else if (panelInfo?.modal) {
|
} else if (panelInfo?.modal && panelInfo?.shptmBanrTpNm !== 'LIVE') {
|
||||||
dispatch(sendLogGNB(entryMenu));
|
dispatch(sendLogGNB(entryMenu));
|
||||||
}
|
}
|
||||||
}, [panelInfo?.modal, panelInfo?.shptmBanrTpNm]);
|
}, [panelInfo?.modal, panelInfo?.shptmBanrTpNm]);
|
||||||
@@ -372,32 +377,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
panelInfoRef.current = panelInfo;
|
panelInfoRef.current = panelInfo;
|
||||||
}, [panelInfo]);
|
}, [panelInfo]);
|
||||||
|
|
||||||
// memoryMonitor 초기화 (마운트 시 한 번만)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!memoryMonitor.current) {
|
|
||||||
memoryMonitor.current = createMemoryMonitor(false); // 초기 로그 비활성화
|
|
||||||
console.log('[PlayerPanel] Memory monitor initialized');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 재생 중 15초마다 메모리 모니터링 (상세 정보 포함)
|
|
||||||
useEffect(() => {
|
|
||||||
const memoryLogInterval = setInterval(() => {
|
|
||||||
const mediaState = videoPlayer.current?.getMediaState();
|
|
||||||
|
|
||||||
// 재생 중인지 확인 (duration > 0이고 paused가 아님)
|
|
||||||
if (mediaState?.duration > 0 && !mediaState?.paused && mediaState?.currentTime > 0) {
|
|
||||||
memoryMonitor.current.logDetailedMemory('[Video Playing]', {
|
|
||||||
currentTime: (mediaState?.currentTime ?? 0).toFixed(2),
|
|
||||||
duration: (mediaState?.duration ?? 0).toFixed(2),
|
|
||||||
buffered: (mediaState?.proportionLoaded ?? 0).toFixed(2),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 15000); // 15초마다 체크
|
|
||||||
|
|
||||||
return () => clearInterval(memoryLogInterval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가
|
// PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', {
|
dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', {
|
||||||
@@ -739,6 +718,13 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
panelInfo?.modal &&
|
panelInfo?.modal &&
|
||||||
liveLogParamsRef.current?.showId === panelInfo?.showId
|
liveLogParamsRef.current?.showId === panelInfo?.showId
|
||||||
) {
|
) {
|
||||||
|
dlog('[PlayerPanel] 📡 LIVE Modal Log Ready and Conditions Met:', {
|
||||||
|
isModalLiveLogReady: logStatus.isModalLiveLogReady,
|
||||||
|
isOnTop,
|
||||||
|
isModal: panelInfo?.modal,
|
||||||
|
showIdMatch: liveLogParamsRef.current?.showId === panelInfo?.showId,
|
||||||
|
logParams: liveLogParamsRef.current,
|
||||||
|
});
|
||||||
let watchStrtDt = formatGMTString(new Date());
|
let watchStrtDt = formatGMTString(new Date());
|
||||||
|
|
||||||
watchIntervalLive.current = setInterval(() => {
|
watchIntervalLive.current = setInterval(() => {
|
||||||
@@ -757,6 +743,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
isModalLiveLogReady: false,
|
isModalLiveLogReady: false,
|
||||||
}));
|
}));
|
||||||
clearInterval(watchIntervalLive.current);
|
clearInterval(watchIntervalLive.current);
|
||||||
|
dlog('[PlayerPanel] 🚀 Dispatching LIVE Modal Log:', {
|
||||||
|
logParams: liveLogParamsRef.current,
|
||||||
|
watchStrtDt,
|
||||||
|
});
|
||||||
dispatch(
|
dispatch(
|
||||||
sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () =>
|
sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () =>
|
||||||
dispatch(changeLocalSettings({ watchRecord: {} }))
|
dispatch(changeLocalSettings({ watchRecord: {} }))
|
||||||
@@ -1761,24 +1751,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
|
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'onBuffer': {
|
|
||||||
// 버퍼링 시작 시 메모리 상태 로깅
|
|
||||||
memoryMonitor.current.logMemory('[Video Buffer Start]', {
|
|
||||||
currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2),
|
|
||||||
duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2),
|
|
||||||
proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'onBufferEnd': {
|
|
||||||
// 버퍼링 종료 시 메모리 상태 로깅
|
|
||||||
memoryMonitor.current.logMemory('[Video Buffer End]', {
|
|
||||||
currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2),
|
|
||||||
duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2),
|
|
||||||
proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'error': {
|
case 'error': {
|
||||||
dispatch(
|
dispatch(
|
||||||
sendBroadCast({
|
sendBroadCast({
|
||||||
@@ -1792,23 +1764,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
const mediaId = videoPlayer.current?.video?.media?.mediaId;
|
const mediaId = videoPlayer.current?.video?.media?.mediaId;
|
||||||
setMediaId(mediaId);
|
setMediaId(mediaId);
|
||||||
setVideoLoaded(true);
|
setVideoLoaded(true);
|
||||||
// HLS 인스턴스 정보 로깅
|
|
||||||
try {
|
|
||||||
const hlsInstance = videoPlayer.current?.video?.getInternalPlayer?.('hls');
|
|
||||||
if (hlsInstance) {
|
|
||||||
memoryMonitor.current.logHlsMemory('[Video Loaded] HLS Instance', {
|
|
||||||
hlsVersion: hlsInstance.version,
|
|
||||||
config: {
|
|
||||||
maxBufferLength: hlsInstance.config?.maxBufferLength,
|
|
||||||
maxMaxBufferLength: hlsInstance.config?.maxMaxBufferLength,
|
|
||||||
backBufferLength: hlsInstance.config?.backBufferLength,
|
|
||||||
maxBufferSize: hlsInstance.config?.maxBufferSize,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// HLS 정보 수집 실패는 무시
|
|
||||||
}
|
|
||||||
dlog(
|
dlog(
|
||||||
'[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:',
|
'[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:',
|
||||||
panelInfoRef.current?.shptmBanrTpNm
|
panelInfoRef.current?.shptmBanrTpNm
|
||||||
@@ -2061,17 +2016,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
setVideoLoaded(false);
|
setVideoLoaded(false);
|
||||||
}, [currentPlayingUrl]);
|
}, [currentPlayingUrl]);
|
||||||
|
|
||||||
// 메모리 모니터링: 비디오 URL 변경 시
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPlayingUrl) {
|
|
||||||
memoryMonitor.current.logMemory(`[Video Change] New URL loaded`, {
|
|
||||||
url: currentPlayingUrl.substring(0, 50),
|
|
||||||
isHLS: currentPlayingUrl.includes('.m3u8'),
|
|
||||||
isDASH: currentPlayingUrl.includes('.mpd'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentPlayingUrl]);
|
|
||||||
|
|
||||||
// 비디오가 새로 선택될 때 타이머 초기화
|
// 비디오가 새로 선택될 때 타이머 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPlayingUrl) {
|
if (currentPlayingUrl) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 40px;
|
bottom: 30px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
max-width: 455px;
|
max-width: 455px;
|
||||||
height: 92px;
|
height: 92px;
|
||||||
padding: 10px 10px 10px 10px;
|
padding: 10px 10px 10px 10px;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 10px;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid rgba(234, 234, 234, 0.3);
|
border: 1px solid rgba(234, 234, 234, 0.3);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
|
|||||||
@@ -1987,35 +1987,25 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
/**
|
/**
|
||||||
* LOG 용도,
|
* LOG 용도,
|
||||||
* 검색 시 로그를 보내는 용도의 이펙트
|
* 검색 시 로그를 보내는 용도의 이펙트
|
||||||
* 우선 주석처리 (계속보내는부분에 대한 처리 필요)
|
|
||||||
*/
|
*/
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// const result = Object.values(searchDatas).reduce((acc, curr) => {
|
const result = Object.values(searchDatas).reduce((acc, curr) => {
|
||||||
// return acc + curr.length;
|
return acc + curr.length;
|
||||||
// }, 0);
|
}, 0);
|
||||||
|
|
||||||
// if (searchQuery) {
|
if (searchQuery) {
|
||||||
// dispatch(
|
dispatch(
|
||||||
// sendLogTotalRecommend({
|
sendLogTotalRecommend({
|
||||||
// query: searchQuery,
|
query: searchQuery,
|
||||||
// searchType: searchPerformed ? 'query' : 'keyword',
|
searchType: searchPerformed ? 'query' : 'keyword',
|
||||||
// result: result,
|
result: result,
|
||||||
// contextName: LOG_CONTEXT_NAME.SEARCH,
|
contextName: LOG_CONTEXT_NAME.SEARCH,
|
||||||
// messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
|
messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
|
||||||
// })
|
})
|
||||||
// );
|
);
|
||||||
|
}
|
||||||
// // 검색 완료 후 결과에 따른 Toast 표시
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
// // if (searchPerformed && searchQuery.trim()) {
|
}, [searchDatas, searchPerformed, searchQuery]);
|
||||||
// // if (result > 0) {
|
|
||||||
// // dispatch(showSearchSuccessToast(searchQuery, result));
|
|
||||||
// // } else {
|
|
||||||
// // dispatch(showSearchErrorToast(searchQuery));
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
// }, [searchDatas, searchPerformed, searchQuery]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* clean up 용도
|
* clean up 용도
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import css from './UserReviewPanel.module.less';
|
|||||||
|
|
||||||
// 디버그 헬퍼 설정
|
// 디버그 헬퍼 설정
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
const { dlog, dwarn, derror /* eslint-disable-line no-unused-vars */ } = createDebugHelpers(DEBUG_MODE);
|
||||||
|
|
||||||
// 버전에 따른 UI 설정
|
// 버전에 따른 UI 설정
|
||||||
const VERSION_LABEL = REVIEW_VERSION === 1 ? '[v1 - 기존 API]' : '[v2 - 신 API]';
|
const VERSION_LABEL = REVIEW_VERSION === 1 ? '[v1 - 기존 API]' : '[v2 - 신 API]';
|
||||||
@@ -41,8 +41,8 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
|||||||
userReviewPanelTotalPages,
|
userReviewPanelTotalPages,
|
||||||
goToNextUserReviewPage,
|
goToNextUserReviewPage,
|
||||||
goToPrevUserReviewPage,
|
goToPrevUserReviewPage,
|
||||||
applyRatingFilter,
|
applyRatingFilter, // eslint-disable-line no-unused-vars
|
||||||
applySentimentFilter,
|
applySentimentFilter, // eslint-disable-line no-unused-vars
|
||||||
clearAllFilters,
|
clearAllFilters,
|
||||||
currentFilter,
|
currentFilter,
|
||||||
filterCounts,
|
filterCounts,
|
||||||
@@ -50,10 +50,10 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
|||||||
_debug,
|
_debug,
|
||||||
// 🎯 API 기반 필터링 데이터
|
// 🎯 API 기반 필터링 데이터
|
||||||
filters,
|
filters,
|
||||||
filteredReviewListData,
|
filteredReviewListData, // eslint-disable-line no-unused-vars
|
||||||
currentReviewFilter,
|
currentReviewFilter,
|
||||||
// 전체 리뷰 데이터 (팝업용)
|
// 전체 리뷰 데이터 (팝업용)
|
||||||
allReviews,
|
allReviews, // eslint-disable-line no-unused-vars
|
||||||
filteredReviews, // ✅ 필터링된 전체 리뷰 (팝업에서 사용)
|
filteredReviews, // ✅ 필터링된 전체 리뷰 (팝업에서 사용)
|
||||||
getReviewsWithImages,
|
getReviewsWithImages,
|
||||||
extractImagesFromReviews,
|
extractImagesFromReviews,
|
||||||
@@ -226,7 +226,15 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
|||||||
return sentimentMap;
|
return sentimentMap;
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
// API 기반 별점 필터 핸들러
|
const getApiKeywordClickHandler = useCallback(
|
||||||
|
(keywordValue) => () => handleApiKeywordsFilter(keywordValue),
|
||||||
|
[handleApiKeywordsFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getApiSentimentClickHandler = useCallback(
|
||||||
|
(sentimentValue) => () => handleApiSentimentFilter(sentimentValue),
|
||||||
|
[handleApiSentimentFilter]
|
||||||
|
);
|
||||||
const handleApiRatingFilter = useCallback(
|
const handleApiRatingFilter = useCallback(
|
||||||
(rating) => {
|
(rating) => {
|
||||||
if (!prdtId || !patnrId) {
|
if (!prdtId || !patnrId) {
|
||||||
@@ -621,7 +629,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
|||||||
<FilterItemButton
|
<FilterItemButton
|
||||||
key={keyword.filterTpVal}
|
key={keyword.filterTpVal}
|
||||||
text={`${keyword.filterNm} (${keyword.filterNmCnt})`}
|
text={`${keyword.filterNm} (${keyword.filterNmCnt})`}
|
||||||
onClick={() => handleApiKeywordsFilter(keyword.filterTpVal)}
|
onClick={getApiKeywordClickHandler(keyword.filterTpVal)}
|
||||||
spotlightId={`filter-keyword-${index}`}
|
spotlightId={`filter-keyword-${index}`}
|
||||||
ariaLabel={`Filter by ${keyword.filterNm} keyword`}
|
ariaLabel={`Filter by ${keyword.filterNm} keyword`}
|
||||||
dataSpotlightUp={
|
dataSpotlightUp={
|
||||||
@@ -693,7 +701,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
|||||||
<FilterItemButton
|
<FilterItemButton
|
||||||
key={sentiment}
|
key={sentiment}
|
||||||
text={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} (${count})`}
|
text={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} (${count})`}
|
||||||
onClick={() => handleApiSentimentFilter(sentiment)}
|
onClick={getApiSentimentClickHandler(sentiment)}
|
||||||
spotlightId={`filter-sentiment-${sentiment}`}
|
spotlightId={`filter-sentiment-${sentiment}`}
|
||||||
ariaLabel={`Filter by ${sentiment} sentiment`}
|
ariaLabel={`Filter by ${sentiment} sentiment`}
|
||||||
dataSpotlightUp={
|
dataSpotlightUp={
|
||||||
|
|||||||
Reference in New Issue
Block a user