fix: 20250626 선택약관 UI수정, 선택약관팝업 조건 수정
This commit is contained in:
@@ -195,6 +195,7 @@ export const types = {
|
||||
GET_SUBTITLE: "GET_SUBTITLE",
|
||||
CLEAR_PLAYER_INFO: "CLEAR_PLAYER_INFO",
|
||||
|
||||
// 🔽 [추가] 플레이 제어 매니저 액션 타입
|
||||
/**
|
||||
* 홈 화면 배너의 비디오 재생 제어를 위한 액션 타입.
|
||||
* 여러 컴포넌트가 동시에 비디오를 재생하려고 할 때 충돌을 방지하고,
|
||||
@@ -203,7 +204,6 @@ export const types = {
|
||||
* SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다.
|
||||
* CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다.
|
||||
* PAUSE_PLAYER_CONTROL: 현재 제어권을 가진 비디오를 '일시정지' 상태로 변경합니다.
|
||||
* RESUME_PLAYER_CONTROL: '일시정지' 상태의 비디오를 다시 재생합니다.
|
||||
*/
|
||||
SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL",
|
||||
CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL",
|
||||
|
||||
@@ -221,9 +221,24 @@ export const setMyPageTermsAgree =
|
||||
const onSuccess = (response) => {
|
||||
console.log("setMyPageTermsAgree onSuccess ", response.data);
|
||||
|
||||
// 약관 ID를 약관 코드로 변환하기 위해 state에서 termsIdMap 조회
|
||||
const termsIdMap = getState().home.termsIdMap || {};
|
||||
const idToCodeMap = Object.entries(termsIdMap).reduce((acc, [code, id]) => {
|
||||
acc[id] = code;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 동의한 약관 ID 목록을 약관 코드로 변환
|
||||
const agreedTermCodes = termsList
|
||||
.map(id => idToCodeMap[id])
|
||||
.filter(Boolean);
|
||||
|
||||
dispatch({
|
||||
type: types.SET_MYPAGE_TERMS_AGREE_SUCCESS,
|
||||
payload: response.data,
|
||||
payload: {
|
||||
...response.data,
|
||||
agreedTermCodes: agreedTermCodes, // 변환된 약관 코드 리스트를 payload에 추가
|
||||
},
|
||||
retCode: response.data.retCode,
|
||||
});
|
||||
|
||||
|
||||
@@ -120,71 +120,70 @@ export const CLEAR_PLAYER_INFO = () => ({
|
||||
type: types.CLEAR_PLAYER_INFO,
|
||||
});
|
||||
|
||||
/* 🔽 [추가] 새로운 '플레이 제어 매니저' 액션들 */
|
||||
|
||||
/**
|
||||
* 비디오 재생 제어권을 요청하는 액션.
|
||||
* 컴포넌트가 비디오를 재생하고 싶을 때 이 액션을 호출합니다.
|
||||
* 'playerControl' 상태를 확인하여 현재 다른 컴포넌트가 비디오를 제어하고 있지 않은 경우에만
|
||||
* 비디오 플레이어를 활성화(PlayerPanel을 modal로 push)합니다.
|
||||
*
|
||||
* @param {string} ownerId - 비디오 제어권을 요청하는 컴포넌트의 고유 ID.
|
||||
* @param {object} videoInfo - 'startVideoPlayer'에 필요한 비디오 정보.
|
||||
* 비디오 재생 제어권을 요청합니다.
|
||||
* 컴포넌트는 이 액션을 통해 중앙 매니저에게 재생을 '요청'합니다.
|
||||
* @param {string} ownerId - 제어권을 요청하는 컴포넌트의 고유 ID (예: 'banner0_persistent')
|
||||
* @param {object} videoInfo - 재생할 비디오 정보 (url, id 등)
|
||||
*/
|
||||
export const requestPlayControl =
|
||||
(ownerId, videoInfo) => (dispatch, getState) => {
|
||||
export const requestPlayControl = (ownerId, videoInfo) => (dispatch, getState) => {
|
||||
const { playerControl } = getState().home;
|
||||
const currentOwnerId = playerControl.ownerId;
|
||||
const isPersistentOwner = currentOwnerId === 'banner0_persistent';
|
||||
|
||||
// 이미 같은 컴포넌트가 제어권을 가지고 있다면 아무것도 하지 않음
|
||||
if (currentOwnerId === ownerId) {
|
||||
return;
|
||||
if (currentOwnerId === ownerId) return; // 이미 제어권 소유
|
||||
|
||||
if (currentOwnerId) {
|
||||
// 현재 제어중인 컴포넌트가 영구재생 배너이면 '일시정지'
|
||||
if (currentOwnerId === 'banner0_persistent') {
|
||||
dispatch(pausePlayerControl());
|
||||
} else {
|
||||
// 다른 미리보기라면 완전히 숨김
|
||||
dispatch(releasePlayControl(currentOwnerId, true));
|
||||
}
|
||||
}
|
||||
|
||||
// 다른 컴포넌트가 제어권을 가지고 있을 때의 처리 (선점 로직)
|
||||
if (currentOwnerId && currentOwnerId !== ownerId) {
|
||||
// 만약 현재 재생중인 비디오가 영구 재생 비디오라면, 종료하는 대신 '일시정지'
|
||||
if (isPersistentOwner) {
|
||||
dispatch(pausePlayerControl(currentOwnerId));
|
||||
} else {
|
||||
// 그 외의 경우는 기존처럼 완전히 종료
|
||||
dispatch(releasePlayControl(currentOwnerId, true)); // fromPreemption = true
|
||||
}
|
||||
}
|
||||
// 1. 매니저 상태 업데이트
|
||||
dispatch({ type: types.SET_PLAYER_CONTROL, payload: { ownerId } });
|
||||
|
||||
// 새로운 제어권을 설정하고 비디오를 재생
|
||||
dispatch({
|
||||
type: types.SET_PLAYER_CONTROL,
|
||||
payload: { ownerId },
|
||||
});
|
||||
dispatch(startVideoPlayer({ ...videoInfo, modal: true }));
|
||||
};
|
||||
// 2. 공유 PlayerPanel의 상태 업데이트
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
panelInfo: {
|
||||
isHidden: false,
|
||||
modal: true,
|
||||
...videoInfo
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 비디오 재생 제어권을 해제하는 액션.
|
||||
* 컴포넌트가 비디오 재생을 중단할 때(예: 포커스 잃음, 언마운트) 호출합니다.
|
||||
* 현재 제어권을 가진 컴포넌트가 자신일 경우에만 'playerControl' 상태를 초기화하고
|
||||
* 비디오 플레이어를 종료(PlayerPanel을 pop)합니다.
|
||||
*
|
||||
* @param {string} ownerId - 비디오 제어권을 해제하려는 컴포넌트의 고유 ID.
|
||||
* @param {boolean} fromPreemption - 다른 요청에 의해 강제로 해제되었는지 여부.
|
||||
* 비디오 재생 제어권을 해제하고, 필요시 영구재생 비디오를 복원합니다.
|
||||
* @param {string} ownerId - 제어권을 해제하는 컴포넌트의 고유 ID
|
||||
* @param {boolean} fromPreemption - 다른 요청에 의해 강제로 중단되었는지 여부
|
||||
*/
|
||||
export const releasePlayControl = (ownerId, fromPreemption = false) => (dispatch, getState) => {
|
||||
const { playerControl } = getState().home;
|
||||
const isPersistentOwner = playerControl.ownerId === 'banner0_persistent';
|
||||
const { playerControl } = getState().home;
|
||||
|
||||
// 제어권을 가진 컴포넌트가 자신일 경우에만 해제
|
||||
// 단, 선점 로직에 의해 호출된 경우는 소유권 확인 없이 즉시 실행
|
||||
if (fromPreemption || playerControl.ownerId === ownerId) {
|
||||
dispatch({
|
||||
type: types.CLEAR_PLAYER_CONTROL,
|
||||
});
|
||||
dispatch(finishVideoPreview());
|
||||
if (fromPreemption || playerControl.ownerId === ownerId) {
|
||||
// 1. 공유 PlayerPanel을 다시 숨김
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
panelInfo: {
|
||||
isHidden: true,
|
||||
}
|
||||
}));
|
||||
|
||||
// 제어권 해제 후, 만약 이전에 일시정지된 영구 비디오가 있었다면 다시 재생
|
||||
if (isPersistentOwner && playerControl.isPaused) {
|
||||
dispatch(resumePlayerControl('banner0_persistent'));
|
||||
// 2. 매니저 상태 업데이트 (현재 소유주 없음)
|
||||
dispatch({ type: types.CLEAR_PLAYER_CONTROL });
|
||||
|
||||
// 3. 만약 '일시정지'된 영구재생 비디오가 있었다면, 제어권을 되돌려주고 다시 재생
|
||||
if (playerControl.isPaused && playerControl.ownerId === 'banner0_persistent') {
|
||||
const persistentVideoInfo = { /* 영구 비디오 정보를 가져오는 로직 (필요시) */ };
|
||||
dispatch(requestPlayControl('banner0_persistent', persistentVideoInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -193,16 +192,16 @@ export const releasePlayControl = (ownerId, fromPreemption = false) => (dispatch
|
||||
*
|
||||
* @param {string} ownerId - 비디오 제어권을 가진 컴포넌트의 고유 ID.
|
||||
*/
|
||||
export const pausePlayerControl = (ownerId) => (dispatch, getState) => {
|
||||
const { playerControl } = getState().home;
|
||||
// export const pausePlayerControl = (ownerId) => (dispatch, getState) => {
|
||||
// const { playerControl } = getState().home;
|
||||
|
||||
// 제어권을 가진 컴포넌트가 자신일 경우에만 일시정지
|
||||
if (playerControl.ownerId === ownerId) {
|
||||
dispatch({
|
||||
type: types.PAUSE_PLAYER_CONTROL,
|
||||
});
|
||||
}
|
||||
};
|
||||
// // 제어권을 가진 컴포넌트가 자신일 경우에만 일시정지
|
||||
// if (playerControl.ownerId === ownerId) {
|
||||
// dispatch({
|
||||
// type: types.PAUSE_PLAYER_CONTROL,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* '일시정지' 상태의 비디오를 다시 재생하는 액션.
|
||||
@@ -219,3 +218,56 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 공유 PlayerPanel을 전체화면 모드로 전환합니다.
|
||||
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
|
||||
*/
|
||||
export const goToFullScreen = () => (dispatch, getState) => {
|
||||
// 공유 PlayerPanel의 'modal' 상태를 false로 변경하여 전체화면으로 전환
|
||||
dispatch(updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
panelInfo: {
|
||||
modal: false,
|
||||
isHidden: false, // 혹시 숨겨져 있었다면 보이도록
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 영구재생 비디오를 일시정지 상태로 만듭니다. (내부 사용)
|
||||
*/
|
||||
export const pausePlayerControl = () => ({
|
||||
type: types.PAUSE_PLAYER_CONTROL
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 전체화면 플레이어에서 미리보기 상태로 복귀할 때 호출됩니다.
|
||||
* 중앙 'playerControl' 상태를 확인하여 올바른 위치와 비디오로 복원합니다.
|
||||
*/
|
||||
export const returnToPreview = () => (dispatch, getState) => {
|
||||
const { playerControl } = getState().home;
|
||||
|
||||
let targetOwnerId;
|
||||
let targetVideoInfo;
|
||||
|
||||
// 만약 '일시정지'된 영구재생 비디오가 있다면, 무조건 그 비디오로 복귀하는 것이 최우선
|
||||
if (playerControl.isPaused) {
|
||||
targetOwnerId = 'banner0_persistent';
|
||||
// targetVideoInfo = ... (0번 배너의 비디오 정보를 가져오는 로직)
|
||||
} else {
|
||||
// 그렇지 않다면, 전체화면으로 가기 직전의 소유주(ownerId)에게로 복귀
|
||||
targetOwnerId = playerControl.ownerId;
|
||||
// targetVideoInfo = ... (해당 ownerId의 비디오 정보를 가져오는 로직)
|
||||
}
|
||||
|
||||
// 매니저에게 해당 타겟으로 재생을 다시 요청
|
||||
if (targetOwnerId) {
|
||||
dispatch(requestPlayControl(targetOwnerId, targetVideoInfo));
|
||||
} else {
|
||||
// 돌아갈 곳이 없으면 그냥 플레이어를 닫음
|
||||
dispatch(finishVideoPreview());
|
||||
}
|
||||
};
|
||||
@@ -392,7 +392,6 @@ export default function TNewPopUp({
|
||||
// 자동으로 Yes/No 버튼 텍스트 설정
|
||||
|
||||
|
||||
|
||||
const finalButton2Text = useMemo(() => {
|
||||
if (kind === "optionalAgreement" && !button2Text) {
|
||||
return "No";
|
||||
|
||||
@@ -916,7 +916,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
gap: 15px;
|
||||
// gap: 15px;
|
||||
|
||||
.optionalConfirmContentContainer {
|
||||
width: 100%;
|
||||
@@ -926,7 +926,7 @@
|
||||
padding: 25px 140px 25px 140px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
// gap: 20px;
|
||||
|
||||
.optionalConfirmTextSection {
|
||||
// flex: 1; // 나머지 높이를 모두 차지
|
||||
@@ -935,6 +935,7 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
// border : 1px solid red;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.optionalConfirmButtonSection {
|
||||
@@ -1017,12 +1018,13 @@
|
||||
display: flex;
|
||||
align-items: center; // 수직 중앙 정렬
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
// gap: 12px;
|
||||
|
||||
.optionalConfirmButton {
|
||||
width: 160px;
|
||||
height: 60px;
|
||||
flex-shrink: 0; // 크기 고정
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1083,17 +1085,18 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center; // 버튼 수직 정렬을 위해 추가
|
||||
gap: 15px; // 버튼 사이 간격
|
||||
// gap: 15px; // 버튼 사이 간격
|
||||
}
|
||||
|
||||
.figmaTermsAgreeButton {
|
||||
// 이제 TButton의 type="popup" 스타일을 사용하므로,
|
||||
// 여기서는 추가적인 스타일이 필요 없습니다.
|
||||
// margin-right는 gap으로 대체되었습니다.
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.figmaTermsCloseButton {
|
||||
// TButton의 type="popup" 스타일을 사용합니다.
|
||||
margin-left: 0px; // lint 오류 대비용용
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@ const initialState = {
|
||||
secondLayerInfo: {},
|
||||
macAddress: { wifi: "", wired: "", p2p: "" },
|
||||
connectionFailed: false,
|
||||
|
||||
termsAgreementStatus: {
|
||||
MST00401: false, // 개인정보처리방침 (필수)
|
||||
MST00402: false, // 이용약관 (필수)
|
||||
MST00405: false, // 선택약관 (선택)
|
||||
}
|
||||
};
|
||||
|
||||
export const commonReducer = (state = initialState, action) => {
|
||||
@@ -239,30 +245,43 @@ export const commonReducer = (state = initialState, action) => {
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_MYPAGE_TERMS_AGREE_SUCCESS:
|
||||
case types.GET_HOME_TERMS: {
|
||||
const newTermsStatus = { ...state.termsAgreementStatus };
|
||||
if (action.payload?.data?.terms) {
|
||||
action.payload.data.terms.forEach(term => {
|
||||
if (Object.prototype.hasOwnProperty.call(newTermsStatus, term.trmsTpCd)) {
|
||||
newTermsStatus[term.trmsTpCd] = term.trmsAgrFlag === 'Y';
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
termsAgreementStatus: newTermsStatus,
|
||||
termsLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_MYPAGE_TERMS_AGREE_SUCCESS: {
|
||||
const newTermsStatus = { ...state.termsAgreementStatus };
|
||||
// action payload에 담겨온 동의한 약관 코드 리스트를 기반으로 상태 업데이트
|
||||
if (action.payload?.agreedTermCodes) {
|
||||
action.payload.agreedTermCodes.forEach(termCode => {
|
||||
if (Object.prototype.hasOwnProperty.call(newTermsStatus, termCode)) {
|
||||
newTermsStatus[termCode] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
termsLoading: false,
|
||||
termsAgreementStatus: newTermsStatus
|
||||
};
|
||||
}
|
||||
case types.SET_MYPAGE_TERMS_AGREE_FAIL:
|
||||
return {
|
||||
...state,
|
||||
termsLoading: false,
|
||||
};
|
||||
// case types.GET_TERMS_AGREE_YN: {
|
||||
// const { privacyTerms, serviceTerms, purchaseTerms, paymentTerms,optionalTerms } =
|
||||
// action.payload;
|
||||
|
||||
// const introTermsAgree = privacyTerms === "Y" && serviceTerms === "Y";
|
||||
// const checkoutTermsAgree = purchaseTerms === "Y" && paymentTerms === "Y";
|
||||
// const optionalTermsAgree = optionalTerms == "Y" ;
|
||||
|
||||
// return {
|
||||
// ...state,
|
||||
// termsFlag: {
|
||||
// ...action.payload,
|
||||
// },
|
||||
// introTermsAgree,
|
||||
// checkoutTermsAgree,
|
||||
// optionalTermsAgree,
|
||||
// };
|
||||
// }
|
||||
case types.REGISTER_DEVICE: {
|
||||
if (action.payload && action.payload.dvcIndex) {
|
||||
return {
|
||||
@@ -273,6 +292,11 @@ export const commonReducer = (state = initialState, action) => {
|
||||
serviceTerms: "Y",
|
||||
},
|
||||
introTermsAgree: true,
|
||||
termsAgreementStatus: {
|
||||
...state.termsAgreementStatus,
|
||||
MST00401: true,
|
||||
MST00402: true,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return state;
|
||||
|
||||
@@ -200,13 +200,14 @@ export const homeReducer = (state = initialState, action) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 [추가] 플레이 제어 매니저 Reducer 로직
|
||||
case types.SET_PLAYER_CONTROL: {
|
||||
return {
|
||||
...state,
|
||||
playerControl: {
|
||||
...state.playerControl,
|
||||
ownerId: action.payload.ownerId,
|
||||
isPaused: false,
|
||||
ownerId: action.payload.ownerId, // 제어권 소유주 ID 설정
|
||||
isPaused: false, // 새로운 제어권이 부여되면 '일시정지' 상태는 해제
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -216,7 +217,8 @@ export const homeReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
playerControl: {
|
||||
...state.playerControl,
|
||||
ownerId: null,
|
||||
ownerId: null, // 제어권 소유주 없음
|
||||
// isPaused는 유지할 수도, 초기화할 수도 있음. 여기선 초기화.
|
||||
isPaused: false,
|
||||
},
|
||||
};
|
||||
@@ -227,10 +229,11 @@ export const homeReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
playerControl: {
|
||||
...state.playerControl,
|
||||
isPaused: true,
|
||||
isPaused: true, // '일시정지' 상태로 설정
|
||||
},
|
||||
};
|
||||
}
|
||||
// 🔼 [추가]
|
||||
|
||||
case types.RESUME_PLAYER_CONTROL: {
|
||||
return {
|
||||
|
||||
@@ -6,16 +6,18 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import { $L, scaleH, scaleW } from "../../../utils/helperMethods";
|
||||
import { $L } from "../../../utils/helperMethods";
|
||||
import {
|
||||
setDefaultFocus,
|
||||
setShowPopup,
|
||||
// setShowPopup,
|
||||
fetchCurrentUserHomeTerms,
|
||||
} from "../../../actions/homeActions";
|
||||
import { changeAppStatus } from "../../../actions/commonActions";
|
||||
import { setMyPageTermsAgree } from "../../../actions/myPageActions";
|
||||
import { pushPanel } from "../../../actions/panelActions";
|
||||
import { pushPanel, popPanel } from "../../../actions/panelActions";
|
||||
import { panel_names } from "../../../utils/Config";
|
||||
import {
|
||||
startVideoPlayer,
|
||||
requestPlayControl,
|
||||
releasePlayControl,
|
||||
} from "../../../actions/playActions";
|
||||
@@ -23,12 +25,15 @@ import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||
import css from "./HomeBanner.module.less";
|
||||
import Random from "./RandomUnit";
|
||||
import Rolling from "./RollingUnit";
|
||||
import RandomUnitNew from "./RandomUnit.new";
|
||||
import TNewPopUp from "../../../components/TPopUp/TNewPopUp";
|
||||
// import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller";
|
||||
import OptionalConfirm from "../../../components/Optional/OptionalConfirm";
|
||||
// import * as Config from "../../../utils/Config";
|
||||
|
||||
// 새로운 비디오 유닛 컴포넌트 임포트
|
||||
import PersistentVideoUnit from "./PersistentVideoUnit";
|
||||
import RandomUnitNew from "./RandomUnit.new";
|
||||
import SimpleVideoContainer from "./SimpleVideoContainer";
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const Container = SpotlightContainerDecorator(
|
||||
@@ -67,6 +72,50 @@ export default function HomeBanner({
|
||||
}
|
||||
}, [handleItemFocus]);
|
||||
|
||||
// 🔽 [추가] 0번째 배너에 대한 영구 비디오 재생을 처리하는 useEffect
|
||||
// useEffect(() => {
|
||||
// if (
|
||||
// bannerDataList &&
|
||||
// bannerDataList.length > 0 &&
|
||||
// selectTemplate === "DSP00201"
|
||||
// ) {
|
||||
// const banner0Data = bannerDataList[0]?.bannerDetailInfos?.[0];
|
||||
|
||||
// if (banner0Data && banner0Data.showUrl) {
|
||||
// // DOM 요소가 존재하는지 확인 후 실행
|
||||
// console.log("[HomeBanner] banner0Data", banner0Data);
|
||||
// const checkAndPlay = () => {
|
||||
// const targetElement = document.querySelector(
|
||||
// '[data-spotlight-id="banner0"]',
|
||||
// );
|
||||
// console.log("[HomeBanner] targetElement", targetElement);
|
||||
// if (targetElement) {
|
||||
// console.log("[HomeBanner] targetElement 존재");
|
||||
// dispatch(
|
||||
// startVideoPlayer({
|
||||
// showUrl: banner0Data.showUrl,
|
||||
// patnrId: banner0Data.patnrId,
|
||||
// showId: banner0Data.showId,
|
||||
// shptmBanrTpNm: "MEDIA",
|
||||
// modal: true,
|
||||
// modalContainerId: "banner0",
|
||||
// spotlightDisable: true,
|
||||
// }),
|
||||
// );
|
||||
// console.log("[HomeBanner] startVideoPlayer 호출");
|
||||
// } else {
|
||||
// // 요소가 없으면 잠시 후 재시도
|
||||
// console.log("[HomeBanner] targetElement 없음");
|
||||
// setTimeout(checkAndPlay, 100);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 다음 tick에서 실행하여 렌더링 완료 보장
|
||||
// setTimeout(checkAndPlay, 0);
|
||||
// }
|
||||
// }
|
||||
// }, [dispatch, bannerDataList, selectTemplate]);
|
||||
|
||||
const handleSecondBannerFocus = useCallback(() => {
|
||||
const secondBannerData = bannerDataList?.[1];
|
||||
if (secondBannerData) {
|
||||
@@ -113,20 +162,35 @@ export default function HomeBanner({
|
||||
useState(false);
|
||||
const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false);
|
||||
|
||||
// 선택약관 팝업 표시 여부
|
||||
const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false);
|
||||
|
||||
// 선택약관 팝업 표시 여부 ===================================================
|
||||
const shouldShowOptionalTermsPopup = useMemo(() => {
|
||||
if (termsLoading || isGnbOpened) {
|
||||
console.log('[HomeBanner] Step 1: termsLoading, isGnbOpened, optionalTermsAgreed 상태 확인', { termsLoading, isGnbOpened, optionalTermsAgreed });
|
||||
if (termsLoading || isGnbOpened || optionalTermsAgreed) {
|
||||
console.log('[HomeBanner] Early return: 조건 불만족 (termsLoading || isGnbOpened || optionalTermsAgreed)');
|
||||
return false;
|
||||
}
|
||||
|
||||
const terms = termsData?.data?.terms;
|
||||
console.log('[HomeBanner] Step 2: termsData 확인', terms);
|
||||
if (!terms) {
|
||||
console.log('[HomeBanner] Early return: terms가 존재하지 않음');
|
||||
return false;
|
||||
}
|
||||
|
||||
const optionalTerm = terms.find((term) => term.trmsTpCd === "MST00405");
|
||||
return optionalTerm
|
||||
console.log('[HomeBanner] Step 3: optionalTerm 검색 결과', optionalTerm);
|
||||
|
||||
const result = optionalTerm
|
||||
? optionalTerm.trmsPopFlag === "Y" && optionalTerm.trmsAgrFlag === "N"
|
||||
: false;
|
||||
}, [termsData, termsLoading, isGnbOpened]);
|
||||
|
||||
console.log('[HomeBanner] Step 4: 최종 결과', result);
|
||||
return result;
|
||||
}, [termsData, termsLoading, isGnbOpened, optionalTermsAgreed]);
|
||||
|
||||
// 선택약관 팝업 표시 여부 ===================================================
|
||||
|
||||
const handleOptionalAgree = useCallback(() => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -141,9 +205,7 @@ export default function HomeBanner({
|
||||
}
|
||||
|
||||
const requiredTermTypes = ["MST00401", "MST00402", "MST00405"];
|
||||
const missingTerms = requiredTermTypes.filter(
|
||||
(type) => !termsIdMap[type],
|
||||
);
|
||||
const missingTerms = requiredTermTypes.filter((type) => !termsIdMap[type]);
|
||||
|
||||
if (missingTerms.length > 0) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -179,6 +241,9 @@ export default function HomeBanner({
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] 약관 동의 성공:", response);
|
||||
}
|
||||
// 약관 동의 성공 상태 설정
|
||||
setOptionalTermsAgreed(true);
|
||||
// 약관 데이터 갱신
|
||||
dispatch(fetchCurrentUserHomeTerms());
|
||||
} else {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -423,25 +488,19 @@ export default function HomeBanner({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[_handleItemFocus, _handleShelfFocus, bannerDataList]
|
||||
[_handleItemFocus, _handleShelfFocus, bannerDataList],
|
||||
);
|
||||
|
||||
const renderItemPersistentVideo = useCallback(
|
||||
// 1번째 배너(포커스 재생) 및 기타 배너를 위한 렌더링 함수
|
||||
const renderItemNew = useCallback(
|
||||
(index, isHorizontal) => {
|
||||
const data = bannerDataList?.[index] ?? {};
|
||||
return (
|
||||
<div className={!isHorizontal ? css.imgBox : undefined}>
|
||||
{data.shptmDspyTpNm === "Rolling" ? (
|
||||
<Rolling
|
||||
bannerData={data}
|
||||
isHorizontal={isHorizontal}
|
||||
key={"banner" + index}
|
||||
spotlightId={"banner" + index}
|
||||
handleShelfFocus={_handleShelfFocus}
|
||||
handleItemFocus={_handleItemFocus}
|
||||
/>
|
||||
) : data.shptmDspyTpNm === "Random" ? (
|
||||
<PersistentVideoUnit
|
||||
|
||||
// DSP00201 레이아웃의 두 번째 배너는 새로운 RandomUnitNew를 사용
|
||||
if (selectTemplate === "DSP00201" && index === 1) {
|
||||
return (
|
||||
<div className={!isHorizontal ? css.imgBox : undefined}>
|
||||
<RandomUnitNew
|
||||
bannerData={data}
|
||||
isHorizontal={isHorizontal}
|
||||
key={"banner" + index}
|
||||
@@ -450,27 +509,69 @@ export default function HomeBanner({
|
||||
handleItemFocus={_handleItemFocus}
|
||||
randomNumber={data.randomIndex}
|
||||
/>
|
||||
) : (
|
||||
<SpottableComponent spotlightId={"banner" + index}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={
|
||||
isHorizontal
|
||||
? homeTopDisplayInfo.wdthtpImgPath1
|
||||
: homeTopDisplayInfo.vtctpImgPath1
|
||||
}
|
||||
aria-label={
|
||||
isHorizontal
|
||||
? homeTopDisplayInfo.wdthtpImgNm1
|
||||
: homeTopDisplayInfo.vtctpImgNm1
|
||||
}
|
||||
/>
|
||||
</SpottableComponent>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 다른 타입의 유닛 렌더링 (예: RollingUnit)
|
||||
if (data.shptmDspyTpNm === "Rolling") {
|
||||
return (
|
||||
<div className={!isHorizontal ? css.imgBox : undefined}>
|
||||
<RollingUnit
|
||||
bannerData={data}
|
||||
isHorizontal={isHorizontal}
|
||||
key={"banner" + index}
|
||||
spotlightId={"banner" + index}
|
||||
handleShelfFocus={_handleShelfFocus}
|
||||
handleItemFocus={_handleItemFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 기본 이미지만 있는 배너 등 다른 케이스 처리
|
||||
return (
|
||||
<div
|
||||
className={!isHorizontal ? css.imgBox : undefined}
|
||||
key={"banner" + index}
|
||||
>
|
||||
<SpottableComponent spotlightId={"banner" + index}>
|
||||
{/* ... 정적 이미지 렌더링 로직 ... */}
|
||||
</SpottableComponent>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[_handleItemFocus, _handleShelfFocus, bannerDataList, homeTopDisplayInfo],
|
||||
[_handleItemFocus, _handleShelfFocus, bannerDataList, selectTemplate],
|
||||
);
|
||||
|
||||
// 0번째 배너(영구 재생)를 위한 렌더링 함수
|
||||
const renderItemPersistentVideo = useCallback(
|
||||
(index, isHorizontal) => {
|
||||
return (
|
||||
<div className={!isHorizontal ? css.imgBox : undefined}>
|
||||
<SimpleVideoContainer
|
||||
spotlightId={"banner" + index} // "banner0"
|
||||
isHorizontal={isHorizontal}
|
||||
handleShelfFocus={_handleShelfFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[_handleShelfFocus],
|
||||
);
|
||||
|
||||
const renderSimpleVideoContainer = useCallback(
|
||||
(index, isHorizontal) => {
|
||||
return (
|
||||
<div className={!isHorizontal ? css.imgBox : undefined}>
|
||||
<SimpleVideoContainer
|
||||
spotlightId={"banner" + index}
|
||||
isHorizontal={isHorizontal}
|
||||
handleShelfFocus={_handleShelfFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[_handleShelfFocus],
|
||||
);
|
||||
|
||||
const renderLayout = useCallback(() => {
|
||||
@@ -479,7 +580,6 @@ export default function HomeBanner({
|
||||
return (
|
||||
<>
|
||||
<ContainerBasic className={css.smallBox}>
|
||||
{/* {renderItemPersistentVideo(0, true)} */}
|
||||
{renderItem(0, true)}
|
||||
{renderItem(1, true)}
|
||||
</ContainerBasic>
|
||||
@@ -514,7 +614,7 @@ export default function HomeBanner({
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [selectTemplate, renderItem, renderItemPersistentVideo]);
|
||||
}, [selectTemplate, renderItem, renderSimpleVideoContainer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,155 +1,87 @@
|
||||
// src/views/HomePanel/HomeBanner/PersistentVideoUnit.jsx (새 파일)
|
||||
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import classNames from "classnames";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
|
||||
import { requestPlayControl, releasePlayControl, startVideoPlayer } from "../../../actions/playActions";
|
||||
// 새로운 '플레이 제어 매니저' 액션을 임포트합니다.
|
||||
import {
|
||||
requestPlayControl,
|
||||
goToFullScreen,
|
||||
} from "../../../actions/playActions";
|
||||
|
||||
import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||
import liveShow from "../../../../assets/images/tag-liveshow.png";
|
||||
import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png";
|
||||
import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png";
|
||||
import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png";
|
||||
import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png";
|
||||
import css from "./RandomUnit.module.less";
|
||||
import css from "./RandomUnit.module.less"; // 스타일은 기존 RandomUnit과 공유 가능
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
);
|
||||
|
||||
const PersistentVideoUnit = (props) => {
|
||||
const { bannerData, spotlightId, isHorizontal, randomNumber, handleItemFocus, handleShelfFocus } = props;
|
||||
export default function PersistentVideoUnit({
|
||||
bannerData,
|
||||
spotlightId,
|
||||
isHorizontal,
|
||||
handleShelfFocus,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const randomData = bannerData?.bannerDetailInfos?.[randomNumber];
|
||||
|
||||
const requestVideo = useCallback(() => {
|
||||
if (randomData) {
|
||||
const videoInfo = {
|
||||
showUrl: randomData.showUrl,
|
||||
patnrId: randomData.patnrId,
|
||||
showId: randomData.showId,
|
||||
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
chanId: randomData.brdcChnlId,
|
||||
modal: true,
|
||||
modalContainerId: spotlightId,
|
||||
modalClassName: css.videoModal,
|
||||
isVerticalModal: !isHorizontal,
|
||||
};
|
||||
dispatch(requestPlayControl(spotlightId, videoInfo));
|
||||
}
|
||||
}, [dispatch, randomData, spotlightId, isHorizontal]);
|
||||
// bannerData에서 첫 번째 비디오 정보를 추출합니다.
|
||||
// 이 컴포넌트는 항상 고정된 비디오를 재생하므로 randomIndex가 필요 없습니다.
|
||||
const videoData = bannerData.bannerDetailInfos[0];
|
||||
|
||||
/**
|
||||
* 컴포넌트가 처음 렌더링될 때(마운트 시) 딱 한 번만 실행됩니다.
|
||||
* 'banner0_persistent'라는 고유 ID로 비디오 재생 제어권을 요청하여
|
||||
* 포커스와 상관없이 비디오가 자동으로 재생되도록 합니다.
|
||||
*/
|
||||
useEffect(() => {
|
||||
requestVideo();
|
||||
|
||||
return () => {
|
||||
dispatch(releasePlayControl(spotlightId));
|
||||
};
|
||||
}, [dispatch, requestVideo, spotlightId]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
requestVideo();
|
||||
if (handleItemFocus) {
|
||||
handleItemFocus();
|
||||
}
|
||||
}, [requestVideo, handleItemFocus]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (randomData) {
|
||||
if (videoData && videoData.showUrl) {
|
||||
const videoInfo = {
|
||||
showUrl: randomData.showUrl,
|
||||
patnrId: randomData.patnrId,
|
||||
showId: randomData.showId,
|
||||
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
chanId: randomData.brdcChnlId,
|
||||
modal: false,
|
||||
showUrl: videoData.showUrl,
|
||||
patnrId: videoData.patnrId,
|
||||
showId: videoData.showId,
|
||||
shptmBanrTpNm: videoData.shptmBanrTpNm,
|
||||
lgCatCd: videoData.lgCatCd,
|
||||
chanId: videoData.brdcChnlId,
|
||||
modalContainerId: spotlightId, // PlayerPanel이 위치할 컨테이너 ID
|
||||
};
|
||||
dispatch(startVideoPlayer(videoInfo));
|
||||
dispatch(requestPlayControl("banner0_persistent", videoInfo));
|
||||
}
|
||||
}, [dispatch, randomData]);
|
||||
}, [dispatch, videoData, spotlightId]);
|
||||
|
||||
/**
|
||||
* 사용자가 이 배너를 클릭했을 때 호출됩니다.
|
||||
* 새로운 'goToFullScreen' 액션을 호출하여, 공유 PlayerPanel을
|
||||
* 부드럽게 전체화면으로 전환합니다.
|
||||
*/
|
||||
const handleGoToFullScreen = useCallback(() => {
|
||||
dispatch(goToFullScreen());
|
||||
}, [dispatch]);
|
||||
|
||||
// 상위 HomeBanner로 포커스 이벤트를 전달하는 콜백
|
||||
const onFocus = useCallback(() => {
|
||||
if (handleShelfFocus) {
|
||||
handleShelfFocus();
|
||||
}
|
||||
}, [handleShelfFocus]);
|
||||
|
||||
return (
|
||||
<Container
|
||||
className={classNames(
|
||||
css.rollingWrap,
|
||||
isHorizontal && css.isHorizontalWrap
|
||||
)}
|
||||
onFocus={handleShelfFocus}
|
||||
<SpottableComponent
|
||||
className={css.itemBox} // 스타일은 재사용
|
||||
spotlightId={spotlightId}
|
||||
onClick={handleGoToFullScreen}
|
||||
onFocus={onFocus}
|
||||
aria-label={videoData?.showNm}
|
||||
>
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleClick}
|
||||
spotlightId={spotlightId}
|
||||
aria-label={
|
||||
randomData?.shptmBanrTpNm === "LIVE"
|
||||
? "LIVE " + randomData?.showNm
|
||||
: randomData?.showNm
|
||||
}
|
||||
alt={"LIVE"}
|
||||
>
|
||||
{randomData?.shptmBanrTpNm === "LIVE" && (
|
||||
<p className={css.liveIcon}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={liveShow}
|
||||
animationSpeed="fast"
|
||||
ariaLabel="LIVE icon"
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
>
|
||||
{randomData?.tmnlImgPath ? (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.tmnlImgPath}
|
||||
ariaLabel={randomData.tmnlImgNm}
|
||||
animationSpeed="fast"
|
||||
/>
|
||||
) : (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={
|
||||
randomData?.vtctpYn === "Y" ? emptyVerImage : emptyHorImage
|
||||
}
|
||||
animationSpeed="fast"
|
||||
ariaLabel={randomData?.tmnlImgNm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.btnPlay}>
|
||||
{randomData?.tmnlImgPath == null ? "" : <img src={btnPlay} alt="play" />}
|
||||
</div>
|
||||
|
||||
<p className={css.brandIcon}>
|
||||
{randomData?.showId && (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.showId ? randomData.patncLogoPath : null}
|
||||
fallbackSrc={defaultLogoImg}
|
||||
animationSpeed="fast"
|
||||
ariaLabel={randomData.brdcChnlId}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</SpottableComponent>
|
||||
</Container>
|
||||
<div className={css.itemBox}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={videoData?.tmnlImgPath}
|
||||
fallbackSrc={isHorizontal ? emptyHorImage : undefined}
|
||||
ariaLabel={videoData?.tmnImgNm}
|
||||
animationSpeed="fast"
|
||||
/>
|
||||
</div>
|
||||
{/* 필요하다면 플레이 아이콘이나 다른 UI 요소 추가 */}
|
||||
</SpottableComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersistentVideoUnit;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ const SpottableComponent = Spottable("div");
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
"div",
|
||||
);
|
||||
|
||||
export default function RandomUnit({
|
||||
@@ -64,13 +64,13 @@ export default function RandomUnit({
|
||||
const bannerDetailInfos = bannerData.bannerDetailInfos;
|
||||
|
||||
const shptmTmplCd = useSelector(
|
||||
(state) => state.home?.bannerData?.shptmTmplCd
|
||||
(state) => state.home?.bannerData?.shptmTmplCd,
|
||||
);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
|
||||
|
||||
const homeCategory = useSelector(
|
||||
(state) => state.home.menuData?.data?.homeCategory
|
||||
(state) => state.home.menuData?.data?.homeCategory,
|
||||
);
|
||||
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function RandomUnit({
|
||||
brand: data.brndNm, // <- 'brnad' 확인
|
||||
location: data.dspyOrdr,
|
||||
bannerType: data.vtctpYn === "Y" ? "Vertical" : "Horizontal",
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [randomDataRef, nowMenu]);
|
||||
@@ -189,7 +189,7 @@ export default function RandomUnit({
|
||||
pushPanel({
|
||||
name: panel_names.FEATURED_BRANDS_PANEL,
|
||||
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
|
||||
})
|
||||
}),
|
||||
);
|
||||
}, [randomData, dispatch]);
|
||||
|
||||
@@ -213,7 +213,9 @@ export default function RandomUnit({
|
||||
setIsFocused(false);
|
||||
clearTimeout(timerRef.current);
|
||||
|
||||
console.log("[RandomUnit] onBlur");
|
||||
dispatch(finishVideoPreview());
|
||||
console.log("[RandomUnit] finishVideoPreview");
|
||||
}, [isFocused]);
|
||||
|
||||
// DSP00501 : Featured Brands
|
||||
@@ -230,7 +232,7 @@ export default function RandomUnit({
|
||||
if (randomData && randomData.shptmLnkTpCd === "DSP00505") {
|
||||
if (homeCategory && homeCategory.length > 0) {
|
||||
const foundCategory = homeCategory.find(
|
||||
(data) => data.lgCatCd === randomData.lgCatCd
|
||||
(data) => data.lgCatCd === randomData.lgCatCd,
|
||||
);
|
||||
if (foundCategory) {
|
||||
return {
|
||||
@@ -358,7 +360,7 @@ export default function RandomUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
categoryData,
|
||||
@@ -381,7 +383,7 @@ export default function RandomUnit({
|
||||
patnrId: randomData.patnrId,
|
||||
prdtId: randomData.prdtId,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
sendBannerLog();
|
||||
@@ -391,7 +393,7 @@ export default function RandomUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
dispatch,
|
||||
@@ -415,7 +417,7 @@ export default function RandomUnit({
|
||||
focusedContainerId: TEMPLATE_CODE_CONF.TOP,
|
||||
currentSpot: currentSpot?.getAttribute("data-spotlight-id"),
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -430,7 +432,7 @@ export default function RandomUnit({
|
||||
modal: false,
|
||||
modalContainerId: spotlightId,
|
||||
modalClassName: css.videoModal,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
sendBannerLog();
|
||||
@@ -440,7 +442,7 @@ export default function RandomUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
onBlur();
|
||||
@@ -503,9 +505,9 @@ export default function RandomUnit({
|
||||
modalContainerId: spotlightId,
|
||||
modalClassName: css.videoModal,
|
||||
isVerticalModal: !isHorizontal,
|
||||
})
|
||||
}),
|
||||
),
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
}
|
||||
if (!isFocused) {
|
||||
@@ -540,7 +542,7 @@ export default function RandomUnit({
|
||||
<Container
|
||||
className={classNames(
|
||||
css.rollingWrap,
|
||||
isHorizontal && css.isHorizontalWrap
|
||||
isHorizontal && css.isHorizontalWrap,
|
||||
)}
|
||||
onFocus={shelfFocus}
|
||||
>
|
||||
@@ -548,7 +550,7 @@ export default function RandomUnit({
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
onClick={imageBannerClick}
|
||||
spotlightId={spotlightId}
|
||||
@@ -570,7 +572,7 @@ export default function RandomUnit({
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
onClick={videoError === true ? videoErrorClick : videoClick}
|
||||
onFocus={onFocus}
|
||||
@@ -616,7 +618,7 @@ export default function RandomUnit({
|
||||
<div
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
>
|
||||
{randomData.tmnlImgPath ? (
|
||||
@@ -668,7 +670,7 @@ export default function RandomUnit({
|
||||
css.todaysDeals,
|
||||
countryCode === "RU" ? css.ru : "",
|
||||
countryCode === "DE" ? css.de : "",
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
onClick={todayDealClick}
|
||||
spotlightId={spotlightId}
|
||||
@@ -688,8 +690,8 @@ export default function RandomUnit({
|
||||
{parseFloat(originalPrice?.replace("$", "")) === 0
|
||||
? randomData?.offerInfo
|
||||
: discountRate
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
{discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
|
||||
@@ -1,641 +1,118 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
// src/views/HomePanel/HomeBanner/RandomUnit.new.jsx (새 파일)
|
||||
|
||||
import classNames from "classnames";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React, { useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import classNames from "classnames";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
|
||||
import Spotlight from "@enact/spotlight";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import { getContainerId } from "@enact/spotlight/src/container";
|
||||
// 새로운 '플레이 제어 매니저' 액션들을 임포트합니다.
|
||||
import {
|
||||
requestPlayControl,
|
||||
releasePlayControl,
|
||||
goToFullScreen,
|
||||
} from "../../../actions/playActions";
|
||||
|
||||
import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png";
|
||||
import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png";
|
||||
import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png";
|
||||
import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png";
|
||||
import defaultImageItem from "../../../../assets/images/img-thumb-empty-product@3x.png";
|
||||
import liveShow from "../../../../assets/images/tag-liveshow.png";
|
||||
import { changeAppStatus } from "../../../actions/commonActions";
|
||||
import { updateHomeInfo } from "../../../actions/homeActions";
|
||||
import {
|
||||
sendLogTopContents,
|
||||
sendLogTotalRecommend,
|
||||
} from "../../../actions/logActions";
|
||||
import { pushPanel } from "../../../actions/panelActions";
|
||||
import {
|
||||
finishVideoPreview,
|
||||
startVideoPlayer,
|
||||
} from "../../../actions/playActions";
|
||||
import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||
import usePriceInfo from "../../../hooks/usePriceInfo";
|
||||
import {
|
||||
LOG_CONTEXT_NAME,
|
||||
LOG_MENU,
|
||||
LOG_MESSAGE_ID,
|
||||
LOG_TP_NO,
|
||||
panel_names,
|
||||
} from "../../../utils/Config";
|
||||
import { $L, formatGMTString } from "../../../utils/helperMethods";
|
||||
import { TEMPLATE_CODE_CONF } from "../HomePanel";
|
||||
import css from "./RandomUnit.new.module.less";
|
||||
import CustomImage from "../../../components/CustomImage/CustomImage";
|
||||
import liveShow from "../../../../assets/images/tag-liveshow.png";
|
||||
import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png";
|
||||
import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png";
|
||||
import css from "./RandomUnit.module.less"; // 스타일은 기존 RandomUnit과 공유
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const SpottableComponent = Spottable("div");
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
);
|
||||
/**
|
||||
* 이 컴포넌트는 새로운 '플레이 제어 매니저' 아키텍처를 사용하여
|
||||
* '포커스를 받을 때만' 미리보기 비디오를 재생하는 '배너 1'의 역할을 합니다.
|
||||
*/
|
||||
export default function RandomUnitNew({
|
||||
bannerData,
|
||||
spotlightId,
|
||||
isHorizontal,
|
||||
handleShelfFocus,
|
||||
handleItemFocus,
|
||||
randomNumber,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const randomData = bannerData.bannerDetailInfos[randomNumber];
|
||||
|
||||
export default function RandomUnitNew({
|
||||
bannerData,
|
||||
spotlightId,
|
||||
isHorizontal,
|
||||
handleShelfFocus,
|
||||
randomNumber,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
/**
|
||||
* 이 컴포넌트에 포커스가 들어왔을 때 호출됩니다.
|
||||
* 'banner1_preview'라는 고유 ID로 비디오 재생 제어권을 '요청'합니다.
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
if (randomData && randomData.showUrl) {
|
||||
const videoInfo = {
|
||||
showUrl: randomData.showUrl,
|
||||
patnrId: randomData.patnrId,
|
||||
showId: randomData.showId,
|
||||
shptmBanrTpNm: randomData.shptmBanrTpNm,
|
||||
modalContainerId: spotlightId,
|
||||
};
|
||||
dispatch(requestPlayControl("banner1_preview", videoInfo));
|
||||
}
|
||||
if (handleItemFocus) {
|
||||
handleItemFocus();
|
||||
}
|
||||
}, [dispatch, randomData, spotlightId, handleItemFocus]);
|
||||
|
||||
const bannerDetailInfos = bannerData.bannerDetailInfos;
|
||||
/**
|
||||
* 이 컴포넌트에서 포커스가 나갔을 때 호출됩니다.
|
||||
* 'banner1_preview'가 가지고 있던 비디오 재생 제어권을 '해제'합니다.
|
||||
*/
|
||||
const handleBlur = useCallback(() => {
|
||||
dispatch(releasePlayControl("banner1_preview"));
|
||||
}, [dispatch]);
|
||||
|
||||
const shptmTmplCd = useSelector(
|
||||
(state) => state.home?.bannerData?.shptmTmplCd
|
||||
);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
|
||||
/**
|
||||
* 사용자가 이 배너를 클릭했을 때 호출됩니다.
|
||||
* 'goToFullScreen' 액션을 호출하여 공유 PlayerPanel을 전체화면으로 전환합니다.
|
||||
*/
|
||||
const handleGoToFullScreen = useCallback(() => {
|
||||
dispatch(goToFullScreen());
|
||||
}, [dispatch]);
|
||||
|
||||
const homeCategory = useSelector(
|
||||
(state) => state.home.menuData?.data?.homeCategory
|
||||
);
|
||||
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
|
||||
// 상위 컴포넌트로 포커스 이벤트를 전달하는 콜백
|
||||
const onContainerFocus = useCallback(() => {
|
||||
if (handleShelfFocus) {
|
||||
handleShelfFocus();
|
||||
}
|
||||
}, [handleShelfFocus]);
|
||||
|
||||
const broadcast = useSelector((state) => state.common.broadcast);
|
||||
const { curationId, curationTitle } = useSelector((state) => state.home);
|
||||
const [randomData, setRandomData] = useState("");
|
||||
const [priceInfos, setpriceInfos] = useState("");
|
||||
const [videoError, setVideoError] = useState(false);
|
||||
const [liveIndicies, setLiveIndicies] = useState([]);
|
||||
// 비디오 재생이 가능한 타입인지 확인 (LIVE 또는 VOD)
|
||||
const isVideoContent =
|
||||
randomData?.shptmBanrTpNm === "LIVE" || randomData?.shptmBanrTpNm === "VOD";
|
||||
|
||||
const bannerDataRef = useRef(bannerData);
|
||||
const randomDataRef = useRef(bannerDetailInfos[randomNumber]);
|
||||
|
||||
const topContentsLogInfo = useMemo(() => {
|
||||
if (randomDataRef.current) {
|
||||
const currentRandomData = randomDataRef.current;
|
||||
|
||||
let contId, contNm;
|
||||
|
||||
switch (currentRandomData?.shptmBanrTpCd) {
|
||||
// case: "LIVE" or "VOD"
|
||||
case "DSP00301":
|
||||
case "DSP00302":
|
||||
contId = currentRandomData?.showId;
|
||||
contNm = currentRandomData?.showNm;
|
||||
break;
|
||||
|
||||
// case: "Image Banner"
|
||||
case "DSP00303":
|
||||
contId = currentRandomData?.shptmLnkTpCd;
|
||||
contNm = currentRandomData?.shptmLnkTpNm;
|
||||
break;
|
||||
|
||||
// case: "Today's Deals"
|
||||
default:
|
||||
contId = currentRandomData?.prdtId;
|
||||
contNm = currentRandomData?.prdtNm;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
currentRandomData?.shptmLnkTpCd === "DSP00503" || // "Hot Picks"
|
||||
currentRandomData?.shptmLnkTpCd === "DSP00509" // "Theme"
|
||||
) {
|
||||
contNm = contNm + " | " + currentRandomData?.lnkCurationId;
|
||||
}
|
||||
|
||||
return {
|
||||
banrNo: `${currentRandomData?.banrDpOrd}`,
|
||||
banrTpNm: currentRandomData?.vtctpYn
|
||||
? currentRandomData.vtctpYn === "Y"
|
||||
? "Vertical"
|
||||
: "Horizontal"
|
||||
: "",
|
||||
contId,
|
||||
contNm,
|
||||
contTpNm: currentRandomData?.shptmBanrTpNm ?? "",
|
||||
dspyTpNm: bannerDataRef.current?.shptmDspyTpNm ?? "",
|
||||
expsOrd: bannerDataRef.current?.banrLctnNo ?? "",
|
||||
linkTpCd: "",
|
||||
patncNm: currentRandomData?.patncNm ?? "",
|
||||
patnrId: currentRandomData?.patnrId ?? "",
|
||||
tmplCd: shptmTmplCd,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [shptmTmplCd]);
|
||||
|
||||
const sendBannerLog = useCallback(() => {
|
||||
const data = randomDataRef.current;
|
||||
|
||||
if (data && nowMenu === LOG_MENU.HOME_TOP) {
|
||||
dispatch(
|
||||
sendLogTotalRecommend({
|
||||
contextName: LOG_CONTEXT_NAME.HOME,
|
||||
messageId: LOG_MESSAGE_ID.BANNER,
|
||||
curationId,
|
||||
curationTitle,
|
||||
contentType: data.shptmBanrTpNm,
|
||||
contentId: data.showId,
|
||||
contentTitle: data.showNm,
|
||||
productId: data.prdtId,
|
||||
productTitle: data.prdtNm,
|
||||
displayType: "rolling",
|
||||
partner: data.patncNm,
|
||||
brand: data.brndNm, // <- 'brnad' 확인
|
||||
location: data.dspyOrdr,
|
||||
bannerType: data.vtctpYn === "Y" ? "Vertical" : "Horizontal",
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [randomDataRef, nowMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bannerDetailInfos && randomNumber) {
|
||||
const indices = bannerDetailInfos
|
||||
.map((info, index) => (info.shptmBanrTpNm === "LIVE" ? index : null))
|
||||
.filter((index) => index !== null && index !== randomNumber);
|
||||
|
||||
setLiveIndicies(indices);
|
||||
}
|
||||
}, [bannerDetailInfos, randomNumber]);
|
||||
|
||||
const videoErrorClick = useCallback(() => {
|
||||
return dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.FEATURED_BRANDS_PANEL,
|
||||
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
|
||||
})
|
||||
);
|
||||
}, [randomData, dispatch]);
|
||||
|
||||
const shelfFocus = useCallback(() => {
|
||||
if (handleShelfFocus) {
|
||||
handleShelfFocus();
|
||||
}
|
||||
}, [handleShelfFocus]);
|
||||
|
||||
const categoryData = useMemo(() => {
|
||||
if (randomData && randomData.shptmLnkTpCd === "DSP00505") {
|
||||
if (homeCategory && homeCategory.length > 0) {
|
||||
const foundCategory = homeCategory.find(
|
||||
(data) => data.lgCatCd === randomData.lgCatCd
|
||||
);
|
||||
if (foundCategory) {
|
||||
return {
|
||||
lgCatNm: foundCategory.lgCatNm,
|
||||
COUNT: foundCategory.COUNT,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [homeCategory, randomData.shptmLnkTpCd]);
|
||||
|
||||
const imageBannerClick = useCallback(() => {
|
||||
let linkInfo = {};
|
||||
const linkType = randomData.shptmLnkTpCd;
|
||||
|
||||
switch (linkType) {
|
||||
case "DSP00501":
|
||||
linkInfo = {
|
||||
name: panel_names.FEATURED_BRANDS_PANEL,
|
||||
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00502":
|
||||
linkInfo = {
|
||||
name: panel_names.TRENDING_NOW_PANEL,
|
||||
panelInfo: {},
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00503":
|
||||
linkInfo = {
|
||||
name: panel_names.HOT_PICKS_PANEL,
|
||||
panelInfo: {
|
||||
patnrId: randomData.patnrId,
|
||||
curationId: randomData.lnkCurationId,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00504":
|
||||
linkInfo = {
|
||||
name: panel_names.ON_SALE_PANEL,
|
||||
panelInfo: {
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00505":
|
||||
if (Object.keys(categoryData).length > 0) {
|
||||
linkInfo = {
|
||||
name: panel_names.CATEGORY_PANEL,
|
||||
panelInfo: {
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
lgCatNm: categoryData.lgCatNm,
|
||||
COUNT: categoryData.COUNT,
|
||||
currentSpot: null,
|
||||
dropDownTab: 0,
|
||||
tab: 0,
|
||||
focusedContainerId: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "DSP00506":
|
||||
linkInfo = {
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId: randomData.patnrId,
|
||||
prdtId: randomData.prdtId,
|
||||
curationId: randomData.lnkCurationId,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00507":
|
||||
linkInfo = {
|
||||
patnrId: randomData.patnrId,
|
||||
showId: randomData.showId,
|
||||
shptmBanrTpNm: "VOD",
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
modal: false,
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00508":
|
||||
linkInfo = {
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId: randomData.patnrId,
|
||||
curationId: randomData.lnkCurationId,
|
||||
prdtId: randomData.prdtId,
|
||||
type: "theme",
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case "DSP00509":
|
||||
linkInfo = {
|
||||
name: panel_names.THEME_CURATION_PANEL,
|
||||
panelInfo: {
|
||||
curationId: randomData.lnkCurationId,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
linkInfo = {
|
||||
name: panel_names.HOME_PANEL,
|
||||
panelInfo: {},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
let action = linkType === "DSP00507" ? startVideoPlayer : pushPanel;
|
||||
|
||||
dispatch(action(linkInfo));
|
||||
sendBannerLog();
|
||||
dispatch(
|
||||
sendLogTopContents({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
categoryData,
|
||||
dispatch,
|
||||
randomData?.lgCatCd,
|
||||
randomData?.lnkCurationId,
|
||||
randomData?.patnrId,
|
||||
randomData?.prdtId,
|
||||
randomData?.showId,
|
||||
randomData?.shptmLnkTpCd,
|
||||
topContentsLogInfo,
|
||||
]);
|
||||
|
||||
const todayDealClick = useCallback(() => {
|
||||
dispatch(
|
||||
pushPanel({
|
||||
name: panel_names.DETAIL_PANEL,
|
||||
panelInfo: {
|
||||
patnrId: randomData.patnrId,
|
||||
prdtId: randomData.prdtId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
sendBannerLog();
|
||||
|
||||
dispatch(
|
||||
sendLogTopContents({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
dispatch,
|
||||
randomData?.patnrId,
|
||||
randomData?.prdtId,
|
||||
randomDataRef,
|
||||
topContentsLogInfo,
|
||||
]);
|
||||
|
||||
const videoClick = useCallback(() => {
|
||||
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
|
||||
const currentSpot = Spotlight.getCurrent();
|
||||
|
||||
if (lastFocusedTargetId) {
|
||||
dispatch(
|
||||
updateHomeInfo({
|
||||
name: panel_names.HOME_PANEL,
|
||||
panelInfo: {
|
||||
lastFocusedTargetId,
|
||||
focusedContainerId: TEMPLATE_CODE_CONF.TOP,
|
||||
currentSpot: currentSpot?.getAttribute("data-spotlight-id"),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
startVideoPlayer({
|
||||
showUrl: randomData.showUrl,
|
||||
patnrId: randomData.patnrId,
|
||||
showId: randomData.showId,
|
||||
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
|
||||
lgCatCd: randomData.lgCatCd,
|
||||
chanId: randomData.brdcChnlId,
|
||||
modal: false,
|
||||
modalContainerId: spotlightId,
|
||||
modalClassName: css.videoModal,
|
||||
})
|
||||
);
|
||||
|
||||
sendBannerLog();
|
||||
|
||||
dispatch(
|
||||
sendLogTopContents({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
);
|
||||
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
}, [randomData, spotlightId, topContentsLogInfo, nowMenu, randomDataRef, onBlur]);
|
||||
|
||||
const { originalPrice, discountedPrice, discountRate, offerInfo } =
|
||||
usePriceInfo(priceInfos) || {};
|
||||
|
||||
useEffect(() => {
|
||||
let _nowMenu = nowMenu;
|
||||
let _entryMenu = entryMenu;
|
||||
|
||||
if (nowMenu === LOG_MENU.HOME_TOP) {
|
||||
const params = {
|
||||
...topContentsLogInfo,
|
||||
entryMenu: _entryMenu,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.VIEW,
|
||||
nowMenu: _nowMenu,
|
||||
};
|
||||
|
||||
return () => dispatch(sendLogTopContents(params));
|
||||
}
|
||||
}, [dispatch, entryMenu, nowMenu, topContentsLogInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
sendBannerLog();
|
||||
}, [randomDataRef, nowMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bannerData) {
|
||||
setRandomData(bannerDetailInfos[randomNumber]);
|
||||
}
|
||||
}, [bannerData, dispatch, randomNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
if (randomData && randomData.priceInfo !== null) {
|
||||
return setpriceInfos(randomData.priceInfo);
|
||||
}
|
||||
}, [randomData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (broadcast?.type === "videoError") {
|
||||
setVideoError(true);
|
||||
if (liveIndicies.length > 0) {
|
||||
const nextIndex = liveIndicies[0];
|
||||
|
||||
setLiveIndicies((prev) => prev.slice(1));
|
||||
setRandomData(bannerDetailInfos[nextIndex]);
|
||||
|
||||
setTimeout(() => {
|
||||
setVideoError(false);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}, [broadcast, liveIndicies, bannerDetailInfos]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
className={classNames(
|
||||
css.rollingWrap,
|
||||
isHorizontal && css.isHorizontalWrap
|
||||
)}
|
||||
onFocus={shelfFocus}
|
||||
return (
|
||||
<div onFocus={onContainerFocus}>
|
||||
<SpottableComponent
|
||||
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
|
||||
spotlightId={spotlightId}
|
||||
onClick={isVideoContent ? handleGoToFullScreen : undefined} // 비디오 컨텐츠일 때만 클릭 이벤트 연결
|
||||
onFocus={isVideoContent ? handleFocus : handleItemFocus} // 비디오 컨텐츠일 때만 포커스 이벤트 연결
|
||||
onBlur={isVideoContent ? handleBlur : undefined} // 비디오 컨텐츠일 때만 블러 이벤트 연결
|
||||
aria-label={randomData?.showNm || randomData?.prdtNm}
|
||||
>
|
||||
<div
|
||||
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
|
||||
>
|
||||
{randomData?.shptmBanrTpNm == "Image Banner" ? (
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onClick={imageBannerClick}
|
||||
spotlightId={spotlightId}
|
||||
aria-label={
|
||||
randomData.prdtNm ? randomData.prdtNm : randomData.tmnlImgNm
|
||||
}
|
||||
>
|
||||
<div className={css.imgBanner}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.tmnlImgPath}
|
||||
animationSpeed="fast"
|
||||
ariaLabel={randomData.tmnImgNm}
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : randomData?.shptmBanrTpNm == "LIVE" ||
|
||||
randomData?.shptmBanrTpNm == "VOD" ? (
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onClick={videoError === true ? videoErrorClick : videoClick}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
spotlightId={spotlightId}
|
||||
aria-label={
|
||||
randomData.shptmBanrTpNm == "LIVE"
|
||||
? "LIVE " + randomData.showNm
|
||||
: randomData.showNm
|
||||
}
|
||||
alt={"LIVE"}
|
||||
>
|
||||
{randomData.shptmBanrTpNm == "LIVE" && videoError === false && (
|
||||
<p className={css.liveIcon}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={liveShow}
|
||||
animationSpeed="fast"
|
||||
ariaLabel="LIVE icon"
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{videoError === true && (
|
||||
<div className={css.errorContents}>
|
||||
<div>
|
||||
{randomData.patncLogoPath && (
|
||||
<img
|
||||
className={css.errorlogo}
|
||||
src={randomData.patncLogoPath}
|
||||
/>
|
||||
)}
|
||||
<p className={css.errorText}>
|
||||
{$L("Click the screen to see more products!")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videoError === false && (
|
||||
<div
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
>
|
||||
{randomData.tmnlImgPath ? (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.tmnlImgPath}
|
||||
ariaLabel={randomData.tmnImgNm}
|
||||
animationSpeed="fast"
|
||||
/>
|
||||
) : (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={
|
||||
randomData.vtctpYn === "Y" ? emptyVerImage : emptyHorImage
|
||||
}
|
||||
animationSpeed="fast"
|
||||
ariaLabel={randomData.tmnImgNm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videoError === false && (
|
||||
<div className={css.btnPlay}>
|
||||
{randomData.tmnlImgPath == null ? "" : <img src={btnPlay} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videoError === false && (
|
||||
<p className={css.brandIcon}>
|
||||
{randomData.showId && (
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.showId ? randomData.patncLogoPath : null}
|
||||
fallbackSrc={defaultLogoImg}
|
||||
animationSpeed="fast"
|
||||
ariaLabel={randomData.brdcChnlId}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</SpottableComponent>
|
||||
) : randomData?.shptmBanrTpNm == "Today's Deals" ? (
|
||||
<SpottableComponent
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
css.todaysDeals,
|
||||
countryCode === "RU" ? css.ru : "",
|
||||
countryCode === "DE" ? css.de : "",
|
||||
isHorizontal && css.isHorizontal
|
||||
)}
|
||||
onClick={todayDealClick}
|
||||
spotlightId={spotlightId}
|
||||
aria-label={
|
||||
randomData.prdtNm ? randomData.prdtNm : randomData.tmnlImgNm
|
||||
}
|
||||
>
|
||||
<div className={css.productInfo}>
|
||||
<div className={css.todaysDealTitle}>{$L("TODAY's DEALS")}</div>
|
||||
<div
|
||||
className={css.textBox}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${randomData.prdtNm}`,
|
||||
}}
|
||||
/>
|
||||
<div className={css.accBox}>
|
||||
{parseFloat(originalPrice?.replace("$", "")) === 0
|
||||
? randomData?.offerInfo
|
||||
: discountRate
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
{discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
{isHorizontal &&
|
||||
parseFloat(originalPrice?.replace("$", "")) !== 0 && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.itemImgBox}>
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData.tmnlImgPath}
|
||||
animationSpeed="fast"
|
||||
fallbackSrc={defaultImageItem}
|
||||
ariaLabel={randomData.tmnlImgNm}
|
||||
/>
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
) : null}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<CustomImage
|
||||
delay={0}
|
||||
src={randomData?.tmnlImgPath}
|
||||
fallbackSrc={isHorizontal ? emptyHorImage : undefined}
|
||||
animationSpeed="fast"
|
||||
/>
|
||||
{isVideoContent && randomData?.tmnlImgPath && (
|
||||
<div className={css.btnPlay}>
|
||||
<img src={btnPlay} alt="Play" />
|
||||
</div>
|
||||
)}
|
||||
{randomData?.shptmBanrTpNm === "LIVE" && (
|
||||
<p className={css.liveIcon}>
|
||||
<CustomImage delay={0} src={liveShow} animationSpeed="fast" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ const SpottableComponent = Spottable("div");
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused", preserveId: true },
|
||||
"div"
|
||||
"div",
|
||||
);
|
||||
|
||||
const LINK_TYPES = {
|
||||
@@ -87,13 +87,13 @@ export default function RollingUnit({
|
||||
const { curationId, curationTitle } = useSelector((state) => state.home);
|
||||
const curtNm = useSelector((state) => state.home?.bannerData?.curtNm);
|
||||
const shptmTmplCd = useSelector(
|
||||
(state) => state.home?.bannerData?.shptmTmplCd
|
||||
(state) => state.home?.bannerData?.shptmTmplCd,
|
||||
);
|
||||
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
|
||||
|
||||
const homeCategory = useSelector(
|
||||
(state) => state.home.menuData?.data?.homeCategory
|
||||
(state) => state.home.menuData?.data?.homeCategory,
|
||||
);
|
||||
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function RollingUnit({
|
||||
const savedIndex = useSelector((state) => state.home.bannerIndices[bannerId]);
|
||||
|
||||
const [startIndex, setStartIndex] = useState(
|
||||
savedIndex !== undefined ? savedIndex : 0
|
||||
savedIndex !== undefined ? savedIndex : 0,
|
||||
);
|
||||
const lastIndexRef = useRef(rollingDataLength - 1);
|
||||
const doRollingRef = useRef(false);
|
||||
@@ -193,7 +193,7 @@ export default function RollingUnit({
|
||||
rollingDataRef.current[startIndex].vtctpYn === "Y"
|
||||
? "Vertical"
|
||||
: "Horizontal",
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [nowMenu, rollingDataRef]);
|
||||
@@ -210,7 +210,7 @@ export default function RollingUnit({
|
||||
|
||||
if (deltaTime >= 10000 && doRollingRef.current) {
|
||||
setStartIndex((prevIndex) =>
|
||||
prevIndex === lastIndexRef.current ? 0 : prevIndex + 1
|
||||
prevIndex === lastIndexRef.current ? 0 : prevIndex + 1,
|
||||
);
|
||||
previousTimeRef.current = time;
|
||||
}
|
||||
@@ -304,7 +304,7 @@ export default function RollingUnit({
|
||||
) {
|
||||
if (homeCategory && homeCategory.length > 0) {
|
||||
const foundCategory = homeCategory.find(
|
||||
(data) => data.lgCatCd === rollingData[startIndex].lgCatCd
|
||||
(data) => data.lgCatCd === rollingData[startIndex].lgCatCd,
|
||||
);
|
||||
if (foundCategory) {
|
||||
return {
|
||||
@@ -324,14 +324,14 @@ export default function RollingUnit({
|
||||
(name, panelInfo) => {
|
||||
dispatch(pushPanel({ name, panelInfo }));
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleStartVideoPlayer = useCallback(
|
||||
(playerInfo) => {
|
||||
dispatch(startVideoPlayer(playerInfo));
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const imageBannerClick = useCallback(() => {
|
||||
@@ -350,7 +350,7 @@ export default function RollingUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -366,14 +366,14 @@ export default function RollingUnit({
|
||||
case LINK_TYPES.TRENDING_NOW:
|
||||
handlePushPanel(
|
||||
panel_names.TRENDING_NOW_PANEL,
|
||||
createPanelInfo(currentData)
|
||||
createPanelInfo(currentData),
|
||||
);
|
||||
break;
|
||||
|
||||
case LINK_TYPES.HOT_PICKS:
|
||||
handlePushPanel(
|
||||
panel_names.HOT_PICKS_PANEL,
|
||||
createPanelInfo(currentData)
|
||||
createPanelInfo(currentData),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -387,7 +387,7 @@ export default function RollingUnit({
|
||||
if (Object.keys(categoryData).length > 0) {
|
||||
handlePushPanel(
|
||||
panel_names.CATEGORY_PANEL,
|
||||
createPanelInfo(currentData, categoryData)
|
||||
createPanelInfo(currentData, categoryData),
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -418,7 +418,7 @@ export default function RollingUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
rollingData,
|
||||
@@ -443,7 +443,7 @@ export default function RollingUnit({
|
||||
focusedContainerId: TEMPLATE_CODE_CONF.TOP,
|
||||
currentSpot: currentSpot?.getAttribute("data-spotlight-id"),
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -471,7 +471,7 @@ export default function RollingUnit({
|
||||
...topContentsLogInfo,
|
||||
inDt: formatGMTString(new Date()) ?? "",
|
||||
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
rollingData,
|
||||
@@ -544,7 +544,7 @@ export default function RollingUnit({
|
||||
<Container
|
||||
className={classNames(
|
||||
css.rollingWrap,
|
||||
isHorizontal && css.isHorizontalWrap
|
||||
isHorizontal && css.isHorizontalWrap,
|
||||
)}
|
||||
spotlightId={`container-${spotlightId}`}
|
||||
onFocus={shelfFocus}
|
||||
@@ -603,7 +603,7 @@ export default function RollingUnit({
|
||||
<div
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
>
|
||||
{rollingData[startIndex].tmnlImgPath == null ? (
|
||||
@@ -662,7 +662,7 @@ export default function RollingUnit({
|
||||
<div
|
||||
className={classNames(
|
||||
css.itemBox,
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
>
|
||||
{rollingData[startIndex].tmnlImgPath == null ? (
|
||||
@@ -715,7 +715,7 @@ export default function RollingUnit({
|
||||
css.todaysDeals,
|
||||
countryCode === "RU" ? css.ru : "",
|
||||
countryCode === "DE" ? css.de : "",
|
||||
isHorizontal && css.isHorizontal
|
||||
isHorizontal && css.isHorizontal,
|
||||
)}
|
||||
onClick={imageBannerClick}
|
||||
onFocus={onFocus}
|
||||
@@ -740,8 +740,8 @@ export default function RollingUnit({
|
||||
{parseFloat(originalPrice?.replace("$", "")) === 0
|
||||
? rollingData[startIndex].offerInfo
|
||||
: discountRate
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
? discountedPrice
|
||||
: originalPrice}
|
||||
{discountRate && !isHorizontal && (
|
||||
<span className={css.saleAccBox}>{originalPrice}</span>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// src/views/HomePanel/HomeBanner/SimpleVideoContainer.jsx
|
||||
|
||||
import React from "react";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import css from "./RandomUnit.module.less";
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
|
||||
export default function SimpleVideoContainer({
|
||||
spotlightId,
|
||||
isHorizontal,
|
||||
handleShelfFocus,
|
||||
}) {
|
||||
return (
|
||||
<SpottableComponent
|
||||
className={`${css.itemBox} ${isHorizontal ? css.isHorizontal : ""}`}
|
||||
spotlightId={spotlightId}
|
||||
onFocus={handleShelfFocus}
|
||||
style={{
|
||||
position: "relative",
|
||||
// 더 정확한 크기 설정 (RandomUnit과 동일하게)
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
{/* 완전 투명한 컨테이너 - 비디오 오버레이 타겟 */}
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
opacity: 0,
|
||||
minHeight: "inherit", // 부모의 최소 높이 상속
|
||||
}}
|
||||
/>
|
||||
</SpottableComponent>
|
||||
);
|
||||
}
|
||||
@@ -99,6 +99,8 @@ export default function IntroPanel({
|
||||
if (introTermsData) {
|
||||
const trmsIds = introTermsData.map((term) => term.trmsId);
|
||||
|
||||
|
||||
|
||||
dispatch(
|
||||
registerDevice({
|
||||
agreeTerms: trmsIds,
|
||||
|
||||
@@ -77,8 +77,12 @@ export default function IntroPanel({
|
||||
// registerDevice API 호출 중 여부
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [showExitMessagePopup, setShowExitMessagePopup] = useState(false);
|
||||
// [추가] 재시도 관련 상태
|
||||
const [pendingAgree, setPendingAgree] = useState(false);
|
||||
// race condition 방지를 위한 안전장치
|
||||
const processingTimeoutRef = useRef(null);
|
||||
// [추가] 재시도 인터벌 참조
|
||||
const retryIntervalRef = useRef(null);
|
||||
// const [isRequiredFocused, setIsRequiredFocused] = useState(false);
|
||||
|
||||
const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState();
|
||||
@@ -146,21 +150,15 @@ export default function IntroPanel({
|
||||
dispatch(sendLogGNB(Config.LOG_MENU.TERMS_CONDITIONS));
|
||||
}, [dispatch]);
|
||||
|
||||
// 컴포넌트 마운트 시 현재 Redux 상태 로깅
|
||||
// useEffect(() => {
|
||||
// console.log('🔍 IntroPanel 마운트 시 Redux 상태:');
|
||||
// console.log(' - regDeviceData:', regDeviceData);
|
||||
// console.log(' - regDeviceInfoData:', regDeviceInfoData);
|
||||
// console.log(' - eventInfos:', eventInfos);
|
||||
// console.log(' - termsData:', termsData);
|
||||
// }, []);
|
||||
|
||||
// 디버깅용 WebOS 버전 로그
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("🔍 IntroPanel WebOS 버전 정보:");
|
||||
console.log(" - webOSVersion:", webOSVersion);
|
||||
console.log(" - shouldShowBenefitsView:", shouldShowBenefitsView);
|
||||
console.log("[IntroPanel] WebOS 버전 정보:");
|
||||
console.log("[IntroPanel] webOSVersion:", webOSVersion);
|
||||
console.log(
|
||||
"[IntroPanel] shouldShowBenefitsView:",
|
||||
shouldShowBenefitsView,
|
||||
);
|
||||
}
|
||||
}, [webOSVersion, shouldShowBenefitsView]);
|
||||
|
||||
@@ -194,11 +192,13 @@ export default function IntroPanel({
|
||||
// [추가] useTermsStateMachine의 에러 상태를 감지하여 팝업으로 표시
|
||||
useEffect(() => {
|
||||
if (termsError) {
|
||||
dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
|
||||
title: $L("Error"),
|
||||
text: termsError.message,
|
||||
button1Text: $L("OK")
|
||||
}));
|
||||
dispatch(
|
||||
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
|
||||
title: $L("Error"),
|
||||
text: termsError.message,
|
||||
button1Text: $L("OK"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [termsError, dispatch]);
|
||||
|
||||
@@ -264,31 +264,41 @@ export default function IntroPanel({
|
||||
dispatch(setHidePopup());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleAgree = useCallback(() => {
|
||||
console.log("[IntroPanel] handleAgree isProcessing=", isProcessing);
|
||||
if (isProcessing){
|
||||
return;
|
||||
// 실패 감지를 위한 useEffect 추가
|
||||
useEffect(() => {
|
||||
// isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만)
|
||||
if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(
|
||||
`[IntroPanel] registerDevice 실패: isProcessing=${isProcessing}, retCode=${regDeviceData.retCode}`,
|
||||
regDeviceData,
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
|
||||
title: $L("Error"),
|
||||
text: $L("Device registration failed. Please try again."),
|
||||
button1Text: $L("OK"),
|
||||
}),
|
||||
);
|
||||
setIsProcessing(false);
|
||||
clearTimeout(processingTimeoutRef.current); // 타임아웃 정리
|
||||
}
|
||||
}, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가
|
||||
|
||||
// 필수 약관이 체크되어 있는지 확인
|
||||
// if (!termsChecked || !privacyChecked) {
|
||||
// // 필수 약관이 체크되지 않았을 때 알림
|
||||
// // window.alert($L("Please agree to Terms & Conditions and Privacy Policy."));
|
||||
// dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
|
||||
// title: $L("Required Terms"),
|
||||
// text: $L("Please agree to Terms & Conditions and Privacy Policy."),
|
||||
// button1Text: $L("OK")
|
||||
// }));
|
||||
// return;
|
||||
// }
|
||||
// [추가] 실제 처리 로직 분리
|
||||
const executeAgree = useCallback(() => {
|
||||
console.log("[IntroPanel] executeAgree 실행 시작");
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
// 안전장치: 30초 후 자동으로 isProcessing 해제
|
||||
// 기존 타임아웃을 5초로 단축
|
||||
processingTimeoutRef.current = setTimeout(() => {
|
||||
console.warn("[IntroPanel] handleAgree 타임아웃 - isProcessing 강제 해제");
|
||||
console.warn(
|
||||
"[IntroPanel] executeAgree 타임아웃 - isProcessing 강제 해제",
|
||||
);
|
||||
setIsProcessing(false);
|
||||
}, 30000);
|
||||
}, 5000);
|
||||
|
||||
// 약관 동의 처리 시작 시 로딩 상태로 설정
|
||||
dispatch({ type: types.GET_TERMS_AGREE_YN_START });
|
||||
@@ -310,6 +320,7 @@ export default function IntroPanel({
|
||||
console.log("[IntroPanel] 현재 termsIdMap:", termsIdMap);
|
||||
console.log("[IntroPanel] 최종 전송될 agreeTerms:", agreeTerms);
|
||||
}
|
||||
console.log("[IntroPanel] agreeTerms!!", agreeTerms);
|
||||
|
||||
dispatch(
|
||||
registerDevice(
|
||||
@@ -381,32 +392,75 @@ export default function IntroPanel({
|
||||
privacyChecked,
|
||||
optionalChecked,
|
||||
dispatch,
|
||||
isProcessing,
|
||||
webOSVersion,
|
||||
termsIdMap,
|
||||
]);
|
||||
|
||||
// 실패 감지를 위한 useEffect 추가
|
||||
useEffect(() => {
|
||||
// isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만)
|
||||
if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(
|
||||
`[IntroPanel] registerDevice 실패: isProcessing=${isProcessing}, retCode=${regDeviceData.retCode}`,
|
||||
regDeviceData,
|
||||
);
|
||||
// [추가] 재시도 메커니즘 시작
|
||||
const startRetryMechanism = useCallback(() => {
|
||||
// 기존 재시도가 있다면 정리
|
||||
if (retryIntervalRef.current) {
|
||||
clearInterval(retryIntervalRef.current);
|
||||
}
|
||||
|
||||
console.log("[IntroPanel] 재시도 메커니즘 시작");
|
||||
|
||||
// 500ms마다 isProcessing 상태 체크
|
||||
retryIntervalRef.current = setInterval(() => {
|
||||
if (!isProcessing && pendingAgree) {
|
||||
console.log("[IntroPanel] 처리 가능 상태 감지 - 실행");
|
||||
clearInterval(retryIntervalRef.current);
|
||||
retryIntervalRef.current = null;
|
||||
setPendingAgree(false);
|
||||
executeAgree();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// 10초 후 자동 포기
|
||||
setTimeout(() => {
|
||||
if (retryIntervalRef.current) {
|
||||
console.warn("[IntroPanel] 재시도 타임아웃");
|
||||
clearInterval(retryIntervalRef.current);
|
||||
retryIntervalRef.current = null;
|
||||
setPendingAgree(false);
|
||||
}
|
||||
}, 10000);
|
||||
}, [isProcessing, pendingAgree, executeAgree]);
|
||||
|
||||
const handleAgree = useCallback(() => {
|
||||
console.log("[IntroPanel] handleAgree 호출, isProcessing=", isProcessing);
|
||||
|
||||
// 필수 약관 체크
|
||||
if (!termsChecked || !privacyChecked) {
|
||||
dispatch(
|
||||
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
|
||||
title: $L("Error"),
|
||||
text: $L("Device registration failed. Please try again."),
|
||||
title: $L("Required Terms"),
|
||||
text: $L("Please agree to Terms & Conditions and Privacy Policy."),
|
||||
button1Text: $L("OK"),
|
||||
}),
|
||||
);
|
||||
setIsProcessing(false);
|
||||
clearTimeout(processingTimeoutRef.current); // 타임아웃 정리
|
||||
return;
|
||||
}
|
||||
}, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가
|
||||
|
||||
// 이미 처리 중이면 대기 모드로 전환
|
||||
if (isProcessing) {
|
||||
console.log("[IntroPanel] 이미 처리 중 - 대기 모드 시작");
|
||||
setPendingAgree(true);
|
||||
startRetryMechanism();
|
||||
return;
|
||||
}
|
||||
|
||||
// 실제 처리 실행
|
||||
executeAgree();
|
||||
}, [
|
||||
termsChecked,
|
||||
privacyChecked,
|
||||
dispatch,
|
||||
isProcessing,
|
||||
pendingAgree,
|
||||
executeAgree,
|
||||
startRetryMechanism,
|
||||
]);
|
||||
|
||||
// 컴포넌트 언마운트 시 타임아웃 정리
|
||||
useEffect(() => {
|
||||
@@ -414,6 +468,10 @@ export default function IntroPanel({
|
||||
if (processingTimeoutRef.current) {
|
||||
clearTimeout(processingTimeoutRef.current);
|
||||
}
|
||||
// [추가] 재시도 인터벌 정리
|
||||
if (retryIntervalRef.current) {
|
||||
clearInterval(retryIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -445,7 +503,7 @@ export default function IntroPanel({
|
||||
(item) => {
|
||||
setFocusAsync(item);
|
||||
},
|
||||
[setFocusAsync]
|
||||
[setFocusAsync],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
@@ -457,7 +515,11 @@ export default function IntroPanel({
|
||||
async ({ selected }) => {
|
||||
try {
|
||||
const newState = await updateStateAsync({ termsChecked: selected });
|
||||
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
|
||||
if (
|
||||
newState.termsChecked &&
|
||||
newState.privacyChecked &&
|
||||
newState.optionalChecked
|
||||
) {
|
||||
setTimeout(() => Spotlight.focus("agreeButton"), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -466,14 +528,18 @@ export default function IntroPanel({
|
||||
}
|
||||
}
|
||||
},
|
||||
[updateStateAsync]
|
||||
[updateStateAsync],
|
||||
);
|
||||
|
||||
const handlePrivacyToggle = useCallback(
|
||||
async ({ selected }) => {
|
||||
try {
|
||||
const newState = await updateStateAsync({ privacyChecked: selected });
|
||||
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
|
||||
if (
|
||||
newState.termsChecked &&
|
||||
newState.privacyChecked &&
|
||||
newState.optionalChecked
|
||||
) {
|
||||
setTimeout(() => Spotlight.focus("agreeButton"), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -482,14 +548,18 @@ export default function IntroPanel({
|
||||
}
|
||||
}
|
||||
},
|
||||
[updateStateAsync]
|
||||
[updateStateAsync],
|
||||
);
|
||||
|
||||
const handleOptionalToggle = useCallback(
|
||||
async ({ selected }) => {
|
||||
try {
|
||||
const newState = await updateStateAsync({ optionalChecked: selected });
|
||||
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
|
||||
if (
|
||||
newState.termsChecked &&
|
||||
newState.privacyChecked &&
|
||||
newState.optionalChecked
|
||||
) {
|
||||
setTimeout(() => Spotlight.focus("agreeButton"), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -498,7 +568,7 @@ export default function IntroPanel({
|
||||
}
|
||||
}
|
||||
},
|
||||
[updateStateAsync]
|
||||
[updateStateAsync],
|
||||
);
|
||||
|
||||
const handleSelectAllToggle = useCallback(
|
||||
@@ -514,7 +584,7 @@ export default function IntroPanel({
|
||||
}
|
||||
}
|
||||
},
|
||||
[updateStateAsync]
|
||||
[updateStateAsync],
|
||||
);
|
||||
|
||||
const handleTermsClickMST00402 = useCallback(
|
||||
@@ -571,7 +641,7 @@ export default function IntroPanel({
|
||||
focusedItem,
|
||||
termsChecked,
|
||||
privacyChecked,
|
||||
shouldShowBenefitsView
|
||||
shouldShowBenefitsView,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -697,7 +767,7 @@ export default function IntroPanel({
|
||||
ariaLabel={$L("Optional Terms checkbox")}
|
||||
/>
|
||||
<TButton
|
||||
className={css.termsButton}
|
||||
className={css.termsButtonOptional}
|
||||
onClick={handleOptionalTermsClickMST00405}
|
||||
onFocus={handleFocusOptionalButton}
|
||||
onBlur={handleBlur}
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
// gap: 20px;
|
||||
display: inline-flex;
|
||||
|
||||
.termsItem {
|
||||
@@ -147,6 +147,7 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.termsText {
|
||||
color: black;
|
||||
@@ -176,6 +177,52 @@
|
||||
font-weight: bold !important;
|
||||
}
|
||||
}
|
||||
|
||||
.termsButtonOptional {
|
||||
width: 530px;
|
||||
height: 120px;
|
||||
padding: 0 50px;
|
||||
background: @COLOR_WHITE;
|
||||
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cfcfcf;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
// margin-bottom: 20px;
|
||||
|
||||
.termsText {
|
||||
color: black;
|
||||
font-size: 35px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 700;
|
||||
line-height: 35px;
|
||||
word-wrap: break-word;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&.required {
|
||||
color: @COLOR_GREEN;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 포커스 및 호버 상태 (통합)
|
||||
&.focused,
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
outline: 4px #c91d53 solid !important;
|
||||
outline-offset: 2px !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 12px rgba(201, 29, 83, 0.3) !important;
|
||||
|
||||
.termsText {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
@@ -101,7 +100,27 @@
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.agreeButton,
|
||||
.agreeButton {
|
||||
width: 240px;
|
||||
height: 80px;
|
||||
background: #999999;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 5px 5px #003 0 6px 7px #0000001a;
|
||||
.flex();
|
||||
margin-right: 12px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
&:not([disabled]) {
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
box-shadow: 0px 18px 28.2px 1.8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disagreeButton {
|
||||
width: 240px;
|
||||
height: 80px;
|
||||
|
||||
Reference in New Issue
Block a user