fix: 20250626 선택약관 UI수정, 선택약관팝업 조건 수정

This commit is contained in:
djaco
2025-06-26 10:29:11 +09:00
parent 4fcb0729af
commit f6ac3f5884
17 changed files with 805 additions and 1022 deletions

View File

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

View File

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

View File

@@ -120,69 +120,68 @@ 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 && currentOwnerId !== ownerId) {
// 만약 현재 재생중인 비디오가 영구 재생 비디오라면, 종료하는 대신 '일시정지'
if (isPersistentOwner) {
dispatch(pausePlayerControl(currentOwnerId));
if (currentOwnerId) {
// 현재 제어중인 컴포넌트가 영구재생 배너이면 '일시정지'
if (currentOwnerId === 'banner0_persistent') {
dispatch(pausePlayerControl());
} else {
// 그 외의 경우는 기존처럼 완전히 종료
dispatch(releasePlayControl(currentOwnerId, true)); // fromPreemption = true
// 다른 미리보기라면 완전히 숨김
dispatch(releasePlayControl(currentOwnerId, true));
}
}
// 새로운 제어권을 설정하고 비디오를 재생
dispatch({
type: types.SET_PLAYER_CONTROL,
payload: { ownerId },
});
dispatch(startVideoPlayer({ ...videoInfo, modal: true }));
// 1. 매니저 상태 업데이트
dispatch({ type: types.SET_PLAYER_CONTROL, payload: { ownerId } });
// 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';
// 제어권을 가진 컴포넌트가 자신일 경우에만 해제
// 단, 선점 로직에 의해 호출된 경우는 소유권 확인 없이 즉시 실행
if (fromPreemption || playerControl.ownerId === ownerId) {
dispatch({
type: types.CLEAR_PLAYER_CONTROL,
});
dispatch(finishVideoPreview());
// 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());
}
};

View File

@@ -392,7 +392,6 @@ export default function TNewPopUp({
// 자동으로 Yes/No 버튼 텍스트 설정
const finalButton2Text = useMemo(() => {
if (kind === "optionalAgreement" && !button2Text) {
return "No";

View File

@@ -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 오류 대비용용
}
}
}

View File

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

View File

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

View File

@@ -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] ?? {};
// DSP00201 레이아웃의 두 번째 배너는 새로운 RandomUnitNew를 사용
if (selectTemplate === "DSP00201" && index === 1) {
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
<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
</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 (
<>

View File

@@ -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={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
onFocus={handleFocus}
onClick={handleClick}
className={css.itemBox} // 스타일은 재사용
spotlightId={spotlightId}
aria-label={
randomData?.shptmBanrTpNm === "LIVE"
? "LIVE " + randomData?.showNm
: randomData?.showNm
}
alt={"LIVE"}
onClick={handleGoToFullScreen}
onFocus={onFocus}
aria-label={videoData?.showNm}
>
{randomData?.shptmBanrTpNm === "LIVE" && (
<p className={css.liveIcon}>
<div className={css.itemBox}>
<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}
src={videoData?.tmnlImgPath}
fallbackSrc={isHorizontal ? emptyHorImage : undefined}
ariaLabel={videoData?.tmnImgNm}
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>
{/* 필요하다면 플레이 아이콘이나 다른 UI 요소 추가 */}
</SpottableComponent>
</Container>
);
};
export default PersistentVideoUnit;
}

View File

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

View File

@@ -1,641 +1,118 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
// src/views/HomePanel/HomeBanner/RandomUnit.new.jsx (새 파일)
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import classNames from "classnames";
import { useDispatch, useSelector } from "react-redux";
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 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,
requestPlayControl,
releasePlayControl,
goToFullScreen,
} 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 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 Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
/**
* 이 컴포넌트는 새로운 '플레이 제어 매니저' 아키텍처를 사용하여
* '포커스를 받을 때만' 미리보기 비디오를 재생하는 '배너 1'의 역할을 합니다.
*/
export default function RandomUnitNew({
bannerData,
spotlightId,
isHorizontal,
handleShelfFocus,
handleItemFocus,
randomNumber,
onFocus,
onBlur,
}) {
const dispatch = useDispatch();
const randomData = bannerData.bannerDetailInfos[randomNumber];
const bannerDetailInfos = bannerData.bannerDetailInfos;
const shptmTmplCd = useSelector(
(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
);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
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([]);
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,
/**
* 이 컴포넌트에 포커스가 들어왔을 때 호출됩니다.
* '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));
}
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",
})
);
if (handleItemFocus) {
handleItemFocus();
}
}, [randomDataRef, nowMenu]);
}, [dispatch, randomData, spotlightId, handleItemFocus]);
useEffect(() => {
if (bannerDetailInfos && randomNumber) {
const indices = bannerDetailInfos
.map((info, index) => (info.shptmBanrTpNm === "LIVE" ? index : null))
.filter((index) => index !== null && index !== randomNumber);
/**
* 이 컴포넌트에서 포커스가 나갔을 때 호출됩니다.
* 'banner1_preview'가 가지고 있던 비디오 재생 제어권을 '해제'합니다.
*/
const handleBlur = useCallback(() => {
dispatch(releasePlayControl("banner1_preview"));
}, [dispatch]);
setLiveIndicies(indices);
}
}, [bannerDetailInfos, randomNumber]);
/**
* 사용자가 이 배너를 클릭했을 때 호출됩니다.
* 'goToFullScreen' 액션을 호출하여 공유 PlayerPanel을 전체화면으로 전환합니다.
*/
const handleGoToFullScreen = useCallback(() => {
dispatch(goToFullScreen());
}, [dispatch]);
const videoErrorClick = useCallback(() => {
return dispatch(
pushPanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
})
);
}, [randomData, dispatch]);
const shelfFocus = useCallback(() => {
// 상위 컴포넌트로 포커스 이벤트를 전달하는 콜백
const onContainerFocus = 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]);
// 비디오 재생이 가능한 타입인지 확인 (LIVE 또는 VOD)
const isVideoContent =
randomData?.shptmBanrTpNm === "LIVE" || randomData?.shptmBanrTpNm === "VOD";
return (
<>
<Container
className={classNames(
css.rollingWrap,
isHorizontal && css.isHorizontalWrap
)}
onFocus={shelfFocus}
>
{randomData?.shptmBanrTpNm == "Image Banner" ? (
<div onFocus={onContainerFocus}>
<SpottableComponent
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
onClick={imageBannerClick}
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
spotlightId={spotlightId}
aria-label={
randomData.prdtNm ? randomData.prdtNm : randomData.tmnlImgNm
}
onClick={isVideoContent ? handleGoToFullScreen : undefined} // 비디오 컨텐츠일 때만 클릭 이벤트 연결
onFocus={isVideoContent ? handleFocus : handleItemFocus} // 비디오 컨텐츠일 때만 포커스 이벤트 연결
onBlur={isVideoContent ? handleBlur : undefined} // 비디오 컨텐츠일 때만 블러 이벤트 연결
aria-label={randomData?.showNm || randomData?.prdtNm}
>
<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
)}
className={classNames(css.itemBox, isHorizontal && css.isHorizontal)}
>
{randomData.tmnlImgPath ? (
<CustomImage
delay={0}
src={randomData.tmnlImgPath}
ariaLabel={randomData.tmnImgNm}
src={randomData?.tmnlImgPath}
fallbackSrc={isHorizontal ? emptyHorImage : undefined}
animationSpeed="fast"
/>
) : (
<CustomImage
delay={0}
src={
randomData.vtctpYn === "Y" ? emptyVerImage : emptyHorImage
}
animationSpeed="fast"
ariaLabel={randomData.tmnImgNm}
/>
)}
</div>
)}
{videoError === false && (
{isVideoContent && randomData?.tmnlImgPath && (
<div className={css.btnPlay}>
{randomData.tmnlImgPath == null ? "" : <img src={btnPlay} />}
<img src={btnPlay} alt="Play" />
</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}
/>
)}
{randomData?.shptmBanrTpNm === "LIVE" && (
<p className={css.liveIcon}>
<CustomImage delay={0} src={liveShow} animationSpeed="fast" />
</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>
</>
</div>
);
}

View File

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

View File

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

View File

@@ -99,6 +99,8 @@ export default function IntroPanel({
if (introTermsData) {
const trmsIds = introTermsData.map((term) => term.trmsId);
dispatch(
registerDevice({
agreeTerms: trmsIds,

View File

@@ -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, {
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: termsError.message,
button1Text: $L("OK")
}));
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}

View File

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

View File

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