16 Commits

Author SHA1 Message Date
cdf0d3de04 [251201] feat: views - NBCUContent.jsx - 기능 개선
🕐 커밋 시간: 2025. 12. 01. 17:04:51

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.jsx
2025-12-01 17:04:51 +09:00
9ff6064bc9 [251127] feat: Featured Brands - NBCU Series Card Images
🕐 커밋 시간: 2025. 11. 27. 10:53:45

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

📁 추가된 파일:
  + com.twin.app.shoptime/assets/images/featuredBrands/series-card-1.png
  + com.twin.app.shoptime/assets/images/featuredBrands/series-card-2.png
  + com.twin.app.shoptime/assets/images/featuredBrands/series-card-3.png

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.jsx
2025-11-27 10:53:46 +09:00
cfee554bf6 [251127] feat: Featured Brands - NBCU Series
🕐 커밋 시간: 2025. 11. 27. 10:42:58

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUSectionTitle/NBCUSectionTitle.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUSectionTitle/NBCUSectionTitle.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUSeries/NBCUSeries.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUSeries/NBCUSeries.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/.gitignore
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.module.less

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
  • 모듈 구조 개선
2025-11-27 10:42:59 +09:00
a5fbb21d43 [251126] feat: Featured Brands - NBCU Page Rendering using BestSeller
🕐 커밋 시간: 2025. 11. 26. 20:56:09

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

📁 추가된 파일:
  + com.twin.app.shoptime/assets/images/featuredBrands/image-bg.png

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/Banner/Banner.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.module.less
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUList/NBCUList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenu.jsx

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
  • 모듈 구조 개선
2025-11-26 20:56:10 +09:00
57cc6dbf20 [251126] feat: Featured Brands - NBCU QuickItem Icon
🕐 커밋 시간: 2025. 11. 26. 20:26:07

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenuItemNBCU/QuickMenuItemNBCU.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenuItemNBCU/QuickMenuItemNBCU.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenu.jsx
2025-11-26 20:26:09 +09:00
74d2b827b0 [251126] feat: Featured Brands - NBCU Basic Page
🕐 커밋 시간: 2025. 11. 26. 20:11:39

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUContent.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUList/NBCUList.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/NBCUContent/NBCUList/NBCUList.module.less

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-11-26 20:11:40 +09:00
9439630bad [251126] feat: Featured Brands - NBCU - 1
🕐 커밋 시간: 2025. 11. 26. 19:43:03

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

📁 추가된 파일:
  + com.twin.app.shoptime/assets/images/featuredBrands/image-nbcu.png
  + com.twin.app.shoptime/assets/images/featuredBrands/nbcu.svg
  + com.twin.app.shoptime/src/components/TabLayout/iconComponents/NbcuIcon.jsx

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

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
2025-11-26 19:43:04 +09:00
junghoon86.park
0a2ef0e68b [영상 스타일 수정]
- 노출 이상부분 수정과 버튼 위치 및 프로그레스바 위치변경.
2025-11-26 17:17:12 +09:00
96cbd1f67e [251126] fix: Remove Lint warinings - 1
🕐 커밋 시간: 2025. 11. 26. 14:59:11

📊 변경 통계:
  • 총 파일: 12개
  • 추가: +47줄
  • 삭제: -50줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/App/deepLinkHandler.js
  ~ com.twin.app.shoptime/src/actions/appDataActions.js
  ~ com.twin.app.shoptime/src/actions/billingActions.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/actions/cancelActions.js
  ~ com.twin.app.shoptime/src/actions/cardActions.js
  ~ com.twin.app.shoptime/src/actions/checkoutActions.js
  ~ com.twin.app.shoptime/src/actions/commonActions.js
  ~ com.twin.app.shoptime/src/actions/convertActions.js
  ~ com.twin.app.shoptime/src/actions/couponActions.js
  ~ com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선

Performance: 코드 최적화로 성능 개선 기대
2025-11-26 14:59:12 +09:00
e8464b98b6 Merge remote-tracking branch 'gitlab/develop_si' into develop_si 2025-11-26 14:17:14 +09:00
4904c6fb58 [251126] fix: Log Migration - SearchPanel.new.v2.jsx
🕐 커밋 시간: 2025. 11. 26. 14:16:12

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/actions/commonActions.js
  ~ com.twin.app.shoptime/src/api/TAxios.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 소규모 기능 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-11-26 14:16:13 +09:00
junghoon86.park
1c9db184fa [상품상세] 녹화된 영상 관련 문구 노출
- productallsection에서 disclaimer 내려주고
 - productVideo 에서 노출하는 방식으로 노출
 - 단 재생시에는 자막관련노출이 겹쳐져 재생이 종료이후 노출됨.
2025-11-26 14:14:26 +09:00
3add749c07 [251126] fix: Log Migration - DetailPanel Done
🕐 커밋 시간: 2025. 11. 26. 13:47:36

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-11-26 13:47:36 +09:00
3c3662f791 [251126] fix: Log Migration - DetailPanel sendLogDetail
🕐 커밋 시간: 2025. 11. 26. 13:17:14

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

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

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-11-26 13:17:14 +09:00
42eda7e0bb [251126] fix: Log Migration - DetailPanel sendLogGNB
🕐 커밋 시간: 2025. 11. 26. 12:45:16

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

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

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-11-26 12:45:17 +09:00
d795182d4c [251126] fix: Log Migration - PlayerPanel.jsx
🕐 커밋 시간: 2025. 11. 26. 10:08:34

📊 변경 통계:
  • 총 파일: 2개

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

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
2025-11-26 10:08:35 +09:00
47 changed files with 1248 additions and 982 deletions

View File

@@ -22,3 +22,5 @@ nul
OPTIMAL.md
.docs
GEMINI.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

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

View File

@@ -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,8 +788,6 @@ 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) {
//계정정보 변경시 또는 초기 로딩시
if (!httpHeader) {

View File

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

View File

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

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

@@ -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: "";

View File

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

View File

@@ -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}`,
}));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -169,6 +169,9 @@ export default function YouMayAlsoLike({
prdtId,
launchedFromPlayer: launchedFromPlayer,
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
fromPanel: {
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
}, // 출처 정보 통합 객체
},
})
);

View File

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

View File

@@ -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}
/>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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%;
}
}
}
}

View File

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

View File

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

View File

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

View File

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