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
|
||||
.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 {
|
||||
changeAppStatus,
|
||||
changeLocalSettings,
|
||||
// cancelFocusElement,
|
||||
// focusElement,
|
||||
// setExitApp,
|
||||
@@ -45,7 +44,7 @@ import { pushPanel } from '../actions/panelActions';
|
||||
import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
||||
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
||||
import ToastContainer from '../components/TToast/ToastContainer';
|
||||
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
|
||||
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { lunaTest } from '../lunaSend/lunaTest';
|
||||
import { store } from '../store/store';
|
||||
@@ -280,7 +279,7 @@ const originFocus = Spotlight.focus;
|
||||
const originMove = Spotlight.move;
|
||||
const originSilentlyFocus = Spotlight.silentlyFocus;
|
||||
let lastLoggedSpotlightId = null;
|
||||
let lastLoggedBlurSpotlightId = null;
|
||||
let lastLoggedBlurSpotlightId = null; // eslint-disable-line no-unused-vars
|
||||
let focusLoggingSuppressed = 0;
|
||||
|
||||
const resolveSpotlightIdFromNode = (node) => {
|
||||
@@ -407,28 +406,7 @@ Spotlight.silentlyFocus = function (...args) {
|
||||
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]
|
||||
// DOM 이벤트 리스너로 대체
|
||||
@@ -448,7 +426,7 @@ const resolveSpotlightIdFromEvent = (event) => {
|
||||
// });
|
||||
// }
|
||||
|
||||
function AppBase(props) {
|
||||
function AppBase(_props /* eslint-disable-line no-unused-vars */) {
|
||||
const dispatch = useDispatch();
|
||||
const httpHeader = useSelector((state) => state.common.httpHeader);
|
||||
const httpHeaderRef = useRef(httpHeader);
|
||||
@@ -650,7 +628,7 @@ function AppBase(props) {
|
||||
clearLaunchParams();
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleRelaunchEvent = useCallback(() => {
|
||||
@@ -704,7 +682,7 @@ function AppBase(props) {
|
||||
if (typeof window === 'object' && window.PalmSystem) {
|
||||
window.PalmSystem.activate();
|
||||
}
|
||||
}, [initService, introTermsAgreeRef, dispatch]);
|
||||
}, [initService, introTermsAgreeRef]);
|
||||
|
||||
const visibilityChanged = useCallback(() => {
|
||||
// console.log('document is hidden', document.hidden);
|
||||
@@ -748,7 +726,7 @@ function AppBase(props) {
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownEvent = (event) => {
|
||||
const keyDownEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||
dispatch(changeAppStatus({ cursorVisible: false }));
|
||||
Spotlight.setPointerMode(false);
|
||||
};
|
||||
@@ -757,7 +735,7 @@ function AppBase(props) {
|
||||
let lastMoveTime = 0;
|
||||
const THROTTLE_MS = 100;
|
||||
|
||||
const mouseMoveEvent = (event) => {
|
||||
const mouseMoveEvent = (_event /* eslint-disable-line no-unused-vars */) => {
|
||||
const now = Date.now();
|
||||
if (now - lastMoveTime < THROTTLE_MS) {
|
||||
// throttle 기간 내에는 hideCursor만 재시작
|
||||
@@ -810,9 +788,7 @@ function AppBase(props) {
|
||||
let userDataChanged = false;
|
||||
if (JSON.stringify(loginUserDataRef.current) !== JSON.stringify(loginUserData)) {
|
||||
userDataChanged = true;
|
||||
}
|
||||
if (!httpHeader || !deviceId) {
|
||||
} else if (userDataChanged || httpHeaderRef.current === null) {
|
||||
} else if (userDataChanged || httpHeaderRef.current === null) {
|
||||
//계정정보 변경시 또는 초기 로딩시
|
||||
if (!httpHeader) {
|
||||
dispatch(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { updateHomeInfo } from "../actions/homeActions";
|
||||
import { pushPanel } from "../actions/panelActions";
|
||||
import {
|
||||
@@ -11,7 +11,7 @@ import { SpotlightIds } from "../utils/SpotlightIds";
|
||||
import { sendLogTotalRecommend } from "../actions/logActions";
|
||||
|
||||
//V2_진입경로코드_진입경로명_MT_노출순번
|
||||
export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
|
||||
console.log("[handleDeepLink] ~ contentTarget: ", contentTarget);
|
||||
let linkTpCd; // 진입경로코드
|
||||
let linkTpNm; // 진입경로명
|
||||
@@ -21,7 +21,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
let curationId; // 큐레이션아이디
|
||||
let showId; // 방송아이디
|
||||
let chanId; // 채널아이디
|
||||
let expsOrd; // 노출순번
|
||||
let grNumber; // 그룹번호
|
||||
let evntId; // 이벤트아이디
|
||||
let lgCatCd; // LG카테고리Code
|
||||
@@ -65,7 +64,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
// V3_진입경로코드_진입경로명_PD_파트너아이디_상품아이디_노출순번_큐레이션아이디
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
prdtId = tokens[5]; // 상품아이디
|
||||
expsOrd = tokens[6]; // 노출순번
|
||||
curationId = tokens[7]; // 큐레이션아이디
|
||||
panelName = panel_names.DETAIL_PANEL;
|
||||
deeplinkPanel = "Product Detaoil";
|
||||
@@ -81,7 +79,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
// V3_진입경로코드_진입경로명_LS_파트너아이디_채널아이디_노출순번_큐레이션아이디
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
chanId = tokens[5]; // 채널아이디
|
||||
expsOrd = tokens[6]; // 노출순번
|
||||
curationId = tokens[7]; // 큐레이션아이디
|
||||
panelName = panel_names.PLAYER_PANEL;
|
||||
deeplinkPanel = "Live Show";
|
||||
@@ -98,7 +95,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
// V3_진입경로코드_진입경로명_VS_파트너아이디_방송아이디_노출순번_큐레이션아이디
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
showId = tokens[5]; // 방송아이디
|
||||
expsOrd = tokens[6]; // 노출순번
|
||||
curationId = tokens[7]; // 큐레이션아이디
|
||||
panelName = panel_names.PLAYER_PANEL;
|
||||
deeplinkPanel = "VOD Show";
|
||||
@@ -119,7 +115,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
curationId = tokens[5]; // 큐레이션아이디\
|
||||
prdtId = tokens[6]; // 상품아이디
|
||||
expsOrd = tokens[7]; // 노출순번
|
||||
grNumber = tokens[8]; // 그룹번호
|
||||
panelName = panel_names.DETAIL_PANEL;
|
||||
deeplinkPanel = "Theme Detail";
|
||||
@@ -140,7 +135,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
curationId = tokens[5]; // 큐레이션아이디
|
||||
expsOrd = tokens[6]; // 노출순번
|
||||
panelName = panel_names.DETAIL_PANEL;
|
||||
deeplinkPanel = "Hotel Detail";
|
||||
panelInfo = {
|
||||
@@ -157,7 +151,6 @@ export const handleDeepLink = (contentTarget) => (dispatch, getState) => {
|
||||
|
||||
patnrId = tokens[4]; // 파트너아이디
|
||||
curationId = tokens[5]; // 큐레이션아이디
|
||||
expsOrd = tokens[6]; // 노출순번
|
||||
panelName = panel_names.HOT_PICKS_PANEL;
|
||||
deeplinkPanel = "Hot Picks";
|
||||
panelInfo = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
export const addMainIndex = (index) => ({
|
||||
type: types.ADD_MAIN_INDEX,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// IF-LGSP-328 : 회원 Billing Address 조회
|
||||
export const getMyInfoBillingSearch = (props) => (dispatch, getState) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// Featured Brands 정보 조회 IF-LGSP-304
|
||||
export const getBrandList = () => (dispatch, getState) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// 회원 주문 취소/반품/교환 사유 조회 (IF-LGSP-347)
|
||||
export const getMyinfoOrderCancelColumnsSearch = (params, callback) => (dispatch, getState) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// 회원의 등록 카드 정보 조회 IF-LGSP-332
|
||||
export const getMyInfoCardSearch = (props) => (dispatch, getState) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// 회원 체크아웃 정보 조회 IF-LGSP-345
|
||||
export const getMyInfoCheckoutInfo = (props, callback) => (dispatch, getState) => {
|
||||
|
||||
@@ -7,9 +7,7 @@ import Spotlight from '@enact/spotlight';
|
||||
import appinfo from '../../webos-meta/appinfo.json';
|
||||
import appinfo35 from '../../webos-meta/appinfo35.json';
|
||||
import appinfo79 from '../../webos-meta/appinfo79.json';
|
||||
import { handleBypassLink } from '../App/bypassLinkHandler';
|
||||
import * as lunaSend from '../lunaSend';
|
||||
import { initialLocalSettings } from '../reducers/localSettingsReducer';
|
||||
import * as Config from '../utils/Config';
|
||||
import * as HelperMethods from '../utils/helperMethods';
|
||||
import { types } from './actionTypes';
|
||||
@@ -17,7 +15,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
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 appinfo35 from "../../webos-meta/appinfo35.json";
|
||||
@@ -94,7 +92,7 @@ export const toggleOptionalTermsConfirm = (selected) => ({
|
||||
payload: selected,
|
||||
});
|
||||
|
||||
export const setExitApp = () => (dispatch, getState) => {
|
||||
export const setExitApp = () => (dispatch) => {
|
||||
dispatch({ type: types.SET_EXIT_APP });
|
||||
|
||||
dlog('Exiting App...');
|
||||
@@ -116,7 +114,7 @@ export const loadingComplete = (status) => ({
|
||||
payload: status,
|
||||
});
|
||||
|
||||
export const alertToast = (payload) => (dispatch, getState) => {
|
||||
export const alertToast = (payload) => (dispatch) => {
|
||||
if (typeof window === 'object' && !window.PalmSystem) {
|
||||
dispatch(changeAppStatus({ toast: true, toastText: payload }));
|
||||
} else {
|
||||
@@ -124,13 +122,13 @@ export const alertToast = (payload) => (dispatch, getState) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSettings = () => (dispatch, getState) => {
|
||||
export const getSystemSettings = () => (dispatch) => {
|
||||
dlog('getSystemSettings ');
|
||||
lunaSend.getSystemSettings(
|
||||
{ category: 'caption', keys: ['captionEnable'] },
|
||||
{
|
||||
onSuccess: (res) => {},
|
||||
onFailure: (err) => {},
|
||||
onSuccess: () => {},
|
||||
onFailure: () => {},
|
||||
onComplete: (res) => {
|
||||
dlog('getSystemSettings onComplete', res);
|
||||
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 ');
|
||||
const { serverType, ricCodeSetting, languageSetting } = getState().localSettings;
|
||||
lunaSend.getHttpHeaderForServiceRequest({
|
||||
@@ -267,10 +265,9 @@ export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getStat
|
||||
const mbrNo = res['X-User-Number'];
|
||||
|
||||
lunaSend.getLoginUserData(parameters, {
|
||||
onSuccess: (res) => {
|
||||
const userId = res.id ?? '';
|
||||
const userNumber = res.lastSignInUserNo;
|
||||
const profileNick = res.profileNick || userId.split('@')[0];
|
||||
onSuccess: (loginRes) => {
|
||||
const userId = loginRes.id ?? '';
|
||||
const profileNick = loginRes.profileNick || userId.split('@')[0];
|
||||
dispatch(
|
||||
getLoginUserData({
|
||||
userId,
|
||||
@@ -288,7 +285,7 @@ export const getHttpHeaderForServiceRequest = (onComplete) => (dispatch, getStat
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeviceId = (onComplete) => (dispatch, getState) => {
|
||||
export const getDeviceId = (onComplete) => (dispatch) => {
|
||||
lunaSend.getDeviceId(
|
||||
{ idType: ['LGUDID'] },
|
||||
{
|
||||
@@ -466,7 +463,7 @@ export const setFocus = (spotlightId) => ({
|
||||
payload: spotlightId,
|
||||
});
|
||||
|
||||
export const focusElement = (spotlightId) => (dispatch, getState) => {
|
||||
export const focusElement = (spotlightId) => (dispatch) => {
|
||||
dispatch(setFocus(spotlightId));
|
||||
|
||||
if (typeof window === 'object') {
|
||||
@@ -488,7 +485,7 @@ export const cancelFocusElement = () => () => {
|
||||
let broadcastTimer = null;
|
||||
export const sendBroadCast =
|
||||
({ type, moreInfo }) =>
|
||||
(dispatch, getState) => {
|
||||
(dispatch) => {
|
||||
clearTimeout(broadcastTimer);
|
||||
dispatch(changeBroadcastEvent({ type, moreInfo }));
|
||||
broadcastTimer = setTimeout(() => {
|
||||
@@ -545,7 +542,7 @@ export const addReservation = (data) => (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteReservationCallback = (scheduleIdList) => (dispatch) => {
|
||||
export const deleteReservationCallback = (scheduleIdList) => () => {
|
||||
lunaSend.deleteReservationCallback(scheduleIdList, {
|
||||
onSuccess: (res) => {
|
||||
// dispatch(alertToast("success" + JSON.stringify(res)));
|
||||
@@ -636,8 +633,8 @@ export const showError =
|
||||
export const deleteOldDb8Datas = () => (dispatch) => {
|
||||
for (let i = 1; i < 10; i++) {
|
||||
lunaSend.deleteOldDb8(i, {
|
||||
onSuccess: (res) => {},
|
||||
onFailure: (err) => {},
|
||||
onSuccess: () => {},
|
||||
onFailure: () => {},
|
||||
});
|
||||
}
|
||||
dispatch(changeLocalSettings({ oldDb8Deleted: true }));
|
||||
@@ -683,7 +680,7 @@ let updateNetworkStateJob = new Job((dispatch, connected) => {
|
||||
dispatch(changeAppStatus({ isInternetConnected: connected }));
|
||||
});
|
||||
|
||||
export const getConnectionStatus = () => (dispatch, getState) => {
|
||||
export const getConnectionStatus = () => (dispatch) => {
|
||||
lunaSend.getConnectionStatus({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend getConnectionStatus', res);
|
||||
@@ -712,7 +709,7 @@ export const getConnectionStatus = () => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
// macAddress
|
||||
export const getConnectionInfo = () => (dispatch, getState) => {
|
||||
export const getConnectionInfo = () => (dispatch) => {
|
||||
lunaSend.getConnectionInfo({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend getConnectionStatus', res);
|
||||
@@ -734,7 +731,7 @@ export const getConnectionInfo = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const disableNotification = () => (dispatch, getState) => {
|
||||
export const disableNotification = () => {
|
||||
lunaSend.disableNotification({
|
||||
onSuccess: (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({
|
||||
onSuccess: (res) => {
|
||||
dlog('lunasend enable notification success', res);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const convertPdfToImage =
|
||||
const timeoutError = new Error(
|
||||
`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) {
|
||||
@@ -39,7 +39,7 @@ export const convertPdfToImage =
|
||||
attemptConversion();
|
||||
} else {
|
||||
// 최종 실패
|
||||
derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts:`, pdfUrl);
|
||||
void derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts:`, pdfUrl);
|
||||
dispatch({
|
||||
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
||||
payload: { pdfUrl, error: timeoutError },
|
||||
@@ -64,17 +64,14 @@ export const convertPdfToImage =
|
||||
|
||||
if (retCode !== undefined && retCode !== 0 && retCode !== '0') {
|
||||
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 에러도 재시도
|
||||
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();
|
||||
} else {
|
||||
derror(
|
||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (API error):`,
|
||||
pdfUrl
|
||||
);
|
||||
void derror(`❌ [EnergyLabel] Final failure after ${attempts} attempts (API error):`, pdfUrl);
|
||||
dispatch({
|
||||
type: types.CONVERT_PDF_TO_IMAGE_FAILURE,
|
||||
payload: { pdfUrl, error },
|
||||
@@ -111,7 +108,7 @@ export const convertPdfToImage =
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
dlog(`✅ [EnergyLabel] Conversion successful on attempt ${attempts}:`, pdfUrl);
|
||||
void dlog(`✅ [EnergyLabel] Conversion successful on attempt ${attempts}:`, pdfUrl);
|
||||
dispatch({
|
||||
type: types.CONVERT_PDF_TO_IMAGE_SUCCESS,
|
||||
payload: { pdfUrl, imageUrl },
|
||||
@@ -119,16 +116,16 @@ export const convertPdfToImage =
|
||||
|
||||
callback && callback(null, imageUrl);
|
||||
} 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) {
|
||||
dlog(
|
||||
void dlog(
|
||||
`🔄 [EnergyLabel] Retrying due to image creation error... (${attempts}/${maxRetries + 1})`
|
||||
);
|
||||
attemptConversion();
|
||||
} else {
|
||||
derror(
|
||||
void derror(
|
||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (image error):`,
|
||||
pdfUrl
|
||||
);
|
||||
@@ -147,14 +144,14 @@ export const convertPdfToImage =
|
||||
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) {
|
||||
dlog(`🔄 [EnergyLabel] Retrying due to network error... (${attempts}/${maxRetries + 1})`);
|
||||
void dlog(`🔄 [EnergyLabel] Retrying due to network error... (${attempts}/${maxRetries + 1})`);
|
||||
attemptConversion();
|
||||
} else {
|
||||
derror(
|
||||
void derror(
|
||||
`❌ [EnergyLabel] Final failure after ${attempts} attempts (network error):`,
|
||||
pdfUrl
|
||||
);
|
||||
@@ -188,7 +185,7 @@ export const convertPdfToImage =
|
||||
* @param {Array<string>} pdfUrls - 변환할 PDF URL 배열
|
||||
* @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) {
|
||||
callback && callback(null, []);
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createDebugHelpers } from '../utils/debug';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const { dlog, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
|
||||
// IF-LGSP-339 : 회원 다운로드 쿠폰 정보 조회
|
||||
export const getProductCouponInfo = (props) => (dispatch, getState) => {
|
||||
|
||||
@@ -348,8 +348,14 @@ export const TAxiosAdvancedPromise = (
|
||||
clearTimeout(timeoutId);
|
||||
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})`);
|
||||
setTimeout(() => {
|
||||
attemptRequest();
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
margin-left: 130px;
|
||||
margin-right: 130px;
|
||||
flex: 1 0 auto;
|
||||
width: 1540px;
|
||||
height: 6px;
|
||||
|
||||
&.videoVertical {
|
||||
@@ -31,10 +30,11 @@
|
||||
}
|
||||
|
||||
.mediaSlider {
|
||||
margin: 0 @slider-padding-h;
|
||||
margin: 0 0 0 @slider-padding-h;
|
||||
padding: @slider-padding-v 0;
|
||||
height: @sand-mediaplayer-slider-height;
|
||||
right: 154px;
|
||||
width: 1466px;
|
||||
// Add a tap area that extends to the edges of the screen, to make the slider more accessible
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -7,22 +7,28 @@
|
||||
position: absolute;
|
||||
font-family: @baseFont;
|
||||
width: 100%;
|
||||
top: 22px;
|
||||
right: 30px;
|
||||
right: 90px;
|
||||
bottom: -5px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 30px;
|
||||
text-align: right;
|
||||
|
||||
letter-spacing: -1px;
|
||||
.separator {
|
||||
position: absolute;
|
||||
right: 110px;
|
||||
right: 105px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.currentTime {
|
||||
position: absolute;
|
||||
right: 140px;
|
||||
right: 130px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.totalTime {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
right:0px;
|
||||
}
|
||||
|
||||
> * {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -172,8 +172,23 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
||||
break;
|
||||
//브랜드
|
||||
case 10300:
|
||||
result =
|
||||
data?.shortFeaturedBrands?.map((item) => ({
|
||||
result = [
|
||||
// NBCU 브랜드 (하드코딩)
|
||||
{
|
||||
icons: FeaturedBrandIcon,
|
||||
id: 'nbcu-brand',
|
||||
path: 'assets/images/featuredBrands/nbcu.svg',
|
||||
patncNm: 'NBCU',
|
||||
spotlightId: 'spotlight_featuredbrand_nbcu',
|
||||
target: [
|
||||
{
|
||||
name: panel_names.FEATURED_BRANDS_PANEL,
|
||||
panelInfo: { from: 'gnb', patnrId: 'NBCU' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// API에서 가져온 기존 브랜드들
|
||||
...(data?.shortFeaturedBrands?.map((item) => ({
|
||||
icons: FeaturedBrandIcon,
|
||||
id: item.patnrId,
|
||||
path: item.patncLogoPath,
|
||||
@@ -185,7 +200,8 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
||||
panelInfo: { from: 'gnb', patnrId: item.patnrId },
|
||||
},
|
||||
],
|
||||
})) || [];
|
||||
})) || []),
|
||||
];
|
||||
break;
|
||||
//
|
||||
case 10600:
|
||||
@@ -304,6 +320,7 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
|
||||
title: item.title,
|
||||
path: item.path,
|
||||
patncNm: item.patncNm,
|
||||
icons: item.icons,
|
||||
target: item.target,
|
||||
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 css from './VideoPlayer.module.less';
|
||||
import { updateVideoPlayState } from '../../actions/playActions';
|
||||
import createMemoryMonitor from '../../utils/memoryMonitor';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
const DEBUG_MODE = false;
|
||||
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
||||
const memoryMonitor = createMemoryMonitor();
|
||||
|
||||
const isEnter = is('enter');
|
||||
const isLeft = is('left');
|
||||
@@ -828,7 +826,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
memoryMonitor.logMemory('[VideoPlayer] componentDidMount');
|
||||
on('mousemove', this.activityDetected);
|
||||
if (platform.touch) {
|
||||
on('touchmove', this.activityDetected);
|
||||
@@ -1024,7 +1021,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
|
||||
// console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src });
|
||||
off('mousemove', this.activityDetected);
|
||||
if (platform.touch) {
|
||||
@@ -1132,7 +1128,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
}
|
||||
// 레퍼런스도 해제해 GC 대상이 되도록 함
|
||||
this.video = null;
|
||||
memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - cleanup done');
|
||||
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
|
||||
if (this.floatingLayerController) {
|
||||
this.floatingLayerController.unregister();
|
||||
@@ -1569,14 +1564,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
handleEvent = (ev) => {
|
||||
const el = this.video;
|
||||
|
||||
// 재생 종료 또는 오류 시 메모리 모니터링 타이머 정리
|
||||
if (ev.type === 'ended' || ev.type === 'error') {
|
||||
if (this.memoryMonitoringInterval) {
|
||||
clearInterval(this.memoryMonitoringInterval);
|
||||
this.memoryMonitoringInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedState = {
|
||||
// Standard media properties
|
||||
currentTime: 0,
|
||||
@@ -1811,10 +1798,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
play = () => {
|
||||
memoryMonitor.logMemory('[VideoPlayer] play() called', {
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
});
|
||||
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
@@ -1859,10 +1842,6 @@ const VideoPlayerBase = class extends React.Component {
|
||||
* @public
|
||||
*/
|
||||
pause = () => {
|
||||
memoryMonitor.logMemory('[VideoPlayer] pause() called', {
|
||||
currentTime: this.state.currentTime.toFixed(2),
|
||||
duration: this.state.duration.toFixed(2),
|
||||
});
|
||||
dlog('🔴 [VideoPlayer] pause() called', {
|
||||
currentTime: this.state.currentTime,
|
||||
duration: this.state.duration,
|
||||
|
||||
@@ -692,10 +692,10 @@
|
||||
// display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
margin-left: 60px;
|
||||
margin-right: 59px;
|
||||
height: 70px;
|
||||
bottom: -20px;
|
||||
width:1800px;
|
||||
margin-left:60px;
|
||||
bottom:92px;
|
||||
> *:first-child {
|
||||
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,
|
||||
getProductCouponTotDownload,
|
||||
} from '../../../actions/couponActions.js';
|
||||
import {
|
||||
sendLogDetail,
|
||||
sendLogGNB,
|
||||
sendLogProductDetail,
|
||||
sendLogShopByMobile,
|
||||
sendLogTotalRecommend,
|
||||
} from '../../../actions/logActions';
|
||||
// import { pushPanel } from '../../../actions/panelActions';
|
||||
import {
|
||||
minimizeModalMedia,
|
||||
restoreModalMedia,
|
||||
} from '../../../actions/mediaActions';
|
||||
import { updatePanel } from '../../../actions/panelActions';
|
||||
import { pauseFullscreenVideo } from '../../../actions/playActions';
|
||||
import { resetShowAllReviews } from '../../../actions/productActions';
|
||||
import {
|
||||
@@ -59,7 +67,12 @@ import TVirtualGridList
|
||||
import useReviews from '../../../hooks/useReviews/useReviews';
|
||||
import useScrollTo from '../../../hooks/useScrollTo';
|
||||
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 {
|
||||
andThen,
|
||||
@@ -76,7 +89,10 @@ import {
|
||||
tap,
|
||||
when,
|
||||
} from '../../../utils/fp';
|
||||
import { $L } from '../../../utils/helperMethods';
|
||||
import {
|
||||
$L,
|
||||
formatGMTString,
|
||||
} from '../../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||
import ShowUserReviews from '../../UserReview/ShowUserReviews';
|
||||
// import CustomScrollbar from '../components/CustomScrollbar/CustomScrollbar';
|
||||
@@ -242,6 +258,7 @@ export default function ProductAllSection({
|
||||
// Redux 상태
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
const groupInfos = useSelector((state) => state.product.groupInfo);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
|
||||
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
|
||||
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
|
||||
@@ -263,6 +280,18 @@ export default function ProductAllSection({
|
||||
const [isShowQRCode, setIsShowQRCode] = useState(true);
|
||||
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);
|
||||
|
||||
@@ -652,6 +681,116 @@ export default function ProductAllSection({
|
||||
dispatch(resetShowAllReviews());
|
||||
}, []); // 빈 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에서 이미 포커스 이동을 처리하므로
|
||||
// ProductAllSection의 자동 포커스는 포커스 탈취를 일으킬 수 있음
|
||||
// useEffect(() => {
|
||||
@@ -674,6 +813,35 @@ export default function ProductAllSection({
|
||||
// console.log('[BuyNow] Buy Now button clicked');
|
||||
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:', {
|
||||
// prdtId: productData?.prdtId,
|
||||
// patnrId: productData?.patnrId,
|
||||
@@ -705,7 +873,7 @@ export default function ProductAllSection({
|
||||
setOpenToast(true);
|
||||
}
|
||||
},
|
||||
[dispatch, productData, openToast]
|
||||
[dispatch, productData, openToast, nowMenu]
|
||||
);
|
||||
|
||||
//닫히도록
|
||||
@@ -926,7 +1094,32 @@ export default function ProductAllSection({
|
||||
}, [hasVideo, productVideoVersion]);
|
||||
|
||||
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(() => {
|
||||
dispatch(minimizeModalMedia());
|
||||
scrollToSection('scroll-marker-product-details');
|
||||
}, [scrollToSection, dispatch]);
|
||||
|
||||
// Source의 handleIndicatorOptions와 동일한 로깅 기능 추가
|
||||
handleIndicatorOptions();
|
||||
}, [scrollToSection, dispatch, handleIndicatorOptions]);
|
||||
|
||||
const handleYouMayAlsoLikeClick = useCallback(() => {
|
||||
dispatch(minimizeModalMedia());
|
||||
@@ -1592,6 +1788,7 @@ export default function ProductAllSection({
|
||||
onScrollToImages={handleScrollToImagesV1}
|
||||
onFocus={() => {}}
|
||||
data-spotlight-id="product-video-player-container"
|
||||
disclaimer={productData.disclaimer}
|
||||
/>
|
||||
) : (
|
||||
<ProductVideoV2
|
||||
|
||||
@@ -215,3 +215,32 @@
|
||||
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 { useDispatch, useSelector } from 'react-redux';
|
||||
import React, {
|
||||
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 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 {
|
||||
startMediaPlayer,
|
||||
finishMediaPreview,
|
||||
switchMediaToFullscreen,
|
||||
minimizeModalMedia,
|
||||
restoreModalMedia,
|
||||
startMediaPlayer,
|
||||
switchMediaToFullscreen,
|
||||
} from '../../../../actions/mediaActions';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import { panel_names } from '../../../../utils/Config';
|
||||
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||
import css from './ProductVideo.module.less';
|
||||
|
||||
const SpottableComponent = Spottable('div');
|
||||
@@ -25,6 +39,7 @@ export default function ProductVideo({
|
||||
autoPlay = false, // 자동 재생 여부
|
||||
continuousPlay = false, // 반복 재생 여부
|
||||
onFocus = null, // 외부에서 전달된 포커스 핸들러
|
||||
disclaimer,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -315,6 +330,12 @@ export default function ProductVideo({
|
||||
<img src={playImg} alt="재생" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.notice}>
|
||||
<Marquee className={css.marquee} marqueeOn="render">
|
||||
<img src={ic_warning} alt={disclaimer} />
|
||||
<span>{disclaimer}</span>
|
||||
</Marquee>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ export default function YouMayAlsoLike({
|
||||
prdtId,
|
||||
launchedFromPlayer: launchedFromPlayer,
|
||||
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
|
||||
fromPanel: {
|
||||
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
|
||||
}, // 출처 정보 통합 객체
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { memo } from "react";
|
||||
|
||||
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 css from "./Banner.module.less";
|
||||
|
||||
@@ -15,16 +16,20 @@ export default memo(function Banner({
|
||||
const { patncLogoPath, patncNm } = selectedBrandInfo;
|
||||
const { topImgAlt, topImgPath } = brandTopImgInfo;
|
||||
|
||||
// NBCU 로고 이미지 처리
|
||||
const logoSrc = panelPatnrId === 'NBCU' ? NBCULogoImage : patncLogoPath;
|
||||
const logoName = panelPatnrId === 'NBCU' ? 'Peacock' : patncNm;
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<figure>
|
||||
<CustomImage
|
||||
src={patncLogoPath}
|
||||
alt={patncNm}
|
||||
src={logoSrc}
|
||||
alt={logoName}
|
||||
fallbackSrc={IcPartnersDefault}
|
||||
ariaLabel={patncNm}
|
||||
ariaLabel={logoName}
|
||||
/>
|
||||
<figcaption>{patncNm}</figcaption>
|
||||
<figcaption>{logoName}</figcaption>
|
||||
</figure>
|
||||
<CustomImage src={topImgPath} alt={topImgAlt} ariaLabel={topImgAlt} />
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@ import css from "./FeaturedBrandsPanel.module.less";
|
||||
import FeaturedCategory from "./FeaturedCategory/FeaturedCategory";
|
||||
import FeaturedCreators from "./FeaturedCreators/FeaturedCreators";
|
||||
import LiveChannels from "./LiveChannels/LiveChannels";
|
||||
import NBCUContent from "./NBCUContent/NBCUContent";
|
||||
import QuickMenu from "./QuickMenu/QuickMenu";
|
||||
import RecommendedShows from "./RecommendedShows/RecommendedShows";
|
||||
import Series from "./Series/Series";
|
||||
@@ -68,6 +69,7 @@ import TodaysDeals from "./TodaysDeals/TodaysDeals";
|
||||
import UpComing from "./UpComing/UpComing";
|
||||
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container";
|
||||
import { sortedIndexOf } from "lodash";
|
||||
import NBCUBgImage from "../../../assets/images/featuredBrands/image-bg.png";
|
||||
|
||||
const STRING_CONF = {
|
||||
CANCEL: "CANCEL",
|
||||
@@ -81,6 +83,7 @@ const STRING_CONF = {
|
||||
};
|
||||
|
||||
const TEMPLATE_CODE_CONF = {
|
||||
NBCU: "NBU00100",
|
||||
LIVE_CHANNELS: "BRD00101",
|
||||
UP_COMING: "BRD00102",
|
||||
TODAYS_DEALS: "BRD00103",
|
||||
@@ -304,8 +307,42 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
);
|
||||
|
||||
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(
|
||||
@@ -415,6 +452,21 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
<>
|
||||
{sortedBrandLayoutInfo.map((el, idx) => {
|
||||
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: {
|
||||
return (
|
||||
<React.Fragment key={el.shptmBrndOptTpCd}>
|
||||
@@ -709,7 +761,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
|
||||
// effect: layout information fetching due to partner id change
|
||||
useEffect(() => {
|
||||
if (!fromDetail) {
|
||||
if (!fromDetail && panelInfo?.patnrId) {
|
||||
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
|
||||
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
|
||||
setIsInitialFocusOccurred(false);
|
||||
@@ -719,14 +771,21 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
|
||||
// effect: set selectedPatnrId and selectedPatncNm
|
||||
useEffect(() => {
|
||||
if (brandInfo) {
|
||||
if (brandInfo || panelInfo?.patnrId) {
|
||||
const patnrId = panelInfo?.patnrId;
|
||||
const patncNm = brandInfo.find((b) => b?.patnrId === patnrId).patncNm;
|
||||
|
||||
setSelectedPatncNm(patncNm);
|
||||
|
||||
// NBCU 특별 처리
|
||||
if (patnrId === 'NBCU') {
|
||||
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]);
|
||||
|
||||
// effect: data fetching based on brandLayoutInfo and selectedPatnrId
|
||||
@@ -958,10 +1017,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{brandInfo && brandTopImgInfo && (
|
||||
{((brandInfo && processedBrandTopImgInfo) || panelInfo?.patnrId === 'NBCU') && processedBrandTopImgInfo && (
|
||||
<Banner
|
||||
brandInfo={brandInfo}
|
||||
brandTopImgInfo={brandTopImgInfo}
|
||||
brandTopImgInfo={processedBrandTopImgInfo}
|
||||
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 useScrollTo from "../../../hooks/useScrollTo";
|
||||
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";
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
@@ -76,7 +77,32 @@ const QuickMenu = ({
|
||||
noScrollByWheel
|
||||
>
|
||||
<ul ref={ulRef}>
|
||||
{panelPatnrId === 'NBCU' ? (
|
||||
<>
|
||||
<QuickMenuItemNBCU
|
||||
itemIndex={0}
|
||||
handleItemFocus={_handleItemFocus}
|
||||
key="nbcu-item"
|
||||
resetStates={resetStates}
|
||||
scrollLeft={scrollLeft}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
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}
|
||||
@@ -85,9 +111,10 @@ const QuickMenu = ({
|
||||
resetStates={resetStates}
|
||||
scrollLeft={scrollLeft}
|
||||
selectedPatnrId={selectedPatnrId}
|
||||
label={itemIndex * 1 + 1 + " of " + brandInfo.length}
|
||||
label={(itemIndex + 1) + " of " + brandInfo.length}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</TScroller>
|
||||
</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 TabContainer from './PlayerTabContents/TabContainer';
|
||||
import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2';
|
||||
import createMemoryMonitor from '../../utils/memoryMonitor';
|
||||
// import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer';
|
||||
// import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer';
|
||||
// import ShopNowButton from './PlayerTabContents/v2/ShopNowButton';
|
||||
@@ -180,7 +179,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
const focusReturnRef = useRef(null);
|
||||
const modalPrevRef = useRef(panelInfo?.modal);
|
||||
const prevIsTopPanelDetailFromPlayerRef = useRef(false);
|
||||
const memoryMonitor = useRef(null);
|
||||
const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', '');
|
||||
const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo');
|
||||
const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0);
|
||||
@@ -294,8 +292,15 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
if (liveShowInfos && liveShowInfos.length > 0) {
|
||||
const panelInfoChanId = panelInfo?.chanId;
|
||||
const isLive = panelInfo?.shptmBanrTpNm === 'LIVE';
|
||||
const isModal = panelInfo?.modal;
|
||||
|
||||
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 //
|
||||
.find(({ chanId }) => panelInfoChanId === chanId);
|
||||
|
||||
@@ -306,7 +311,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm]);
|
||||
}, [liveShowInfos, panelInfo?.chanId, panelInfo?.shptmBanrTpNm, panelInfo?.modal]);
|
||||
|
||||
const currentVODShowInfo = useMemo(() => {
|
||||
if (showDetailInfo && showDetailInfo.length > 0) {
|
||||
@@ -330,7 +335,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
prevNowMenuRef.current = nowMenuRef.current;
|
||||
|
||||
return () => dispatch(sendLogGNB(prevNowMenuRef.current));
|
||||
} else if (panelInfo?.modal) {
|
||||
} else if (panelInfo?.modal && panelInfo?.shptmBanrTpNm !== 'LIVE') {
|
||||
dispatch(sendLogGNB(entryMenu));
|
||||
}
|
||||
}, [panelInfo?.modal, panelInfo?.shptmBanrTpNm]);
|
||||
@@ -372,32 +377,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
panelInfoRef.current = 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 감지 추가
|
||||
useEffect(() => {
|
||||
dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', {
|
||||
@@ -739,6 +718,13 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
panelInfo?.modal &&
|
||||
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());
|
||||
|
||||
watchIntervalLive.current = setInterval(() => {
|
||||
@@ -757,6 +743,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
isModalLiveLogReady: false,
|
||||
}));
|
||||
clearInterval(watchIntervalLive.current);
|
||||
dlog('[PlayerPanel] 🚀 Dispatching LIVE Modal Log:', {
|
||||
logParams: liveLogParamsRef.current,
|
||||
watchStrtDt,
|
||||
});
|
||||
dispatch(
|
||||
sendLogLive({ ...liveLogParamsRef.current, watchStrtDt }, () =>
|
||||
dispatch(changeLocalSettings({ watchRecord: {} }))
|
||||
@@ -1761,24 +1751,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
|
||||
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': {
|
||||
dispatch(
|
||||
sendBroadCast({
|
||||
@@ -1792,23 +1764,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
const mediaId = videoPlayer.current?.video?.media?.mediaId;
|
||||
setMediaId(mediaId);
|
||||
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(
|
||||
'[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:',
|
||||
panelInfoRef.current?.shptmBanrTpNm
|
||||
@@ -2061,17 +2016,6 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
setVideoLoaded(false);
|
||||
}, [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(() => {
|
||||
if (currentPlayingUrl) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 5;
|
||||
@@ -14,7 +14,7 @@
|
||||
max-width: 455px;
|
||||
height: 92px;
|
||||
padding: 10px 10px 10px 10px;
|
||||
margin-bottom: 50px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(234, 234, 234, 0.3);
|
||||
border-radius: 100px;
|
||||
|
||||
@@ -1987,35 +1987,25 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
/**
|
||||
* LOG 용도,
|
||||
* 검색 시 로그를 보내는 용도의 이펙트
|
||||
* 우선 주석처리 (계속보내는부분에 대한 처리 필요)
|
||||
*/
|
||||
// useEffect(() => {
|
||||
// const result = Object.values(searchDatas).reduce((acc, curr) => {
|
||||
// return acc + curr.length;
|
||||
// }, 0);
|
||||
useEffect(() => {
|
||||
const result = Object.values(searchDatas).reduce((acc, curr) => {
|
||||
return acc + curr.length;
|
||||
}, 0);
|
||||
|
||||
// if (searchQuery) {
|
||||
// dispatch(
|
||||
// sendLogTotalRecommend({
|
||||
// query: searchQuery,
|
||||
// searchType: searchPerformed ? 'query' : 'keyword',
|
||||
// result: result,
|
||||
// contextName: LOG_CONTEXT_NAME.SEARCH,
|
||||
// messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
|
||||
// })
|
||||
// );
|
||||
|
||||
// // 검색 완료 후 결과에 따른 Toast 표시
|
||||
// // if (searchPerformed && searchQuery.trim()) {
|
||||
// // if (result > 0) {
|
||||
// // dispatch(showSearchSuccessToast(searchQuery, result));
|
||||
// // } else {
|
||||
// // dispatch(showSearchErrorToast(searchQuery));
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [searchDatas, searchPerformed, searchQuery]);
|
||||
if (searchQuery) {
|
||||
dispatch(
|
||||
sendLogTotalRecommend({
|
||||
query: searchQuery,
|
||||
searchType: searchPerformed ? 'query' : 'keyword',
|
||||
result: result,
|
||||
contextName: LOG_CONTEXT_NAME.SEARCH,
|
||||
messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
|
||||
})
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchDatas, searchPerformed, searchQuery]);
|
||||
|
||||
/**
|
||||
* clean up 용도
|
||||
|
||||
@@ -20,7 +20,7 @@ import css from './UserReviewPanel.module.less';
|
||||
|
||||
// 디버그 헬퍼 설정
|
||||
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 설정
|
||||
const VERSION_LABEL = REVIEW_VERSION === 1 ? '[v1 - 기존 API]' : '[v2 - 신 API]';
|
||||
@@ -41,8 +41,8 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
userReviewPanelTotalPages,
|
||||
goToNextUserReviewPage,
|
||||
goToPrevUserReviewPage,
|
||||
applyRatingFilter,
|
||||
applySentimentFilter,
|
||||
applyRatingFilter, // eslint-disable-line no-unused-vars
|
||||
applySentimentFilter, // eslint-disable-line no-unused-vars
|
||||
clearAllFilters,
|
||||
currentFilter,
|
||||
filterCounts,
|
||||
@@ -50,10 +50,10 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
_debug,
|
||||
// 🎯 API 기반 필터링 데이터
|
||||
filters,
|
||||
filteredReviewListData,
|
||||
filteredReviewListData, // eslint-disable-line no-unused-vars
|
||||
currentReviewFilter,
|
||||
// 전체 리뷰 데이터 (팝업용)
|
||||
allReviews,
|
||||
allReviews, // eslint-disable-line no-unused-vars
|
||||
filteredReviews, // ✅ 필터링된 전체 리뷰 (팝업에서 사용)
|
||||
getReviewsWithImages,
|
||||
extractImagesFromReviews,
|
||||
@@ -226,7 +226,15 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
return sentimentMap;
|
||||
}, [filters]);
|
||||
|
||||
// API 기반 별점 필터 핸들러
|
||||
const getApiKeywordClickHandler = useCallback(
|
||||
(keywordValue) => () => handleApiKeywordsFilter(keywordValue),
|
||||
[handleApiKeywordsFilter]
|
||||
);
|
||||
|
||||
const getApiSentimentClickHandler = useCallback(
|
||||
(sentimentValue) => () => handleApiSentimentFilter(sentimentValue),
|
||||
[handleApiSentimentFilter]
|
||||
);
|
||||
const handleApiRatingFilter = useCallback(
|
||||
(rating) => {
|
||||
if (!prdtId || !patnrId) {
|
||||
@@ -621,7 +629,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
<FilterItemButton
|
||||
key={keyword.filterTpVal}
|
||||
text={`${keyword.filterNm} (${keyword.filterNmCnt})`}
|
||||
onClick={() => handleApiKeywordsFilter(keyword.filterTpVal)}
|
||||
onClick={getApiKeywordClickHandler(keyword.filterTpVal)}
|
||||
spotlightId={`filter-keyword-${index}`}
|
||||
ariaLabel={`Filter by ${keyword.filterNm} keyword`}
|
||||
dataSpotlightUp={
|
||||
@@ -693,7 +701,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
<FilterItemButton
|
||||
key={sentiment}
|
||||
text={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} (${count})`}
|
||||
onClick={() => handleApiSentimentFilter(sentiment)}
|
||||
onClick={getApiSentimentClickHandler(sentiment)}
|
||||
spotlightId={`filter-sentiment-${sentiment}`}
|
||||
ariaLabel={`Filter by ${sentiment} sentiment`}
|
||||
dataSpotlightUp={
|
||||
|
||||
Reference in New Issue
Block a user