[20250721] 선택약관관련 수정사항 반영

This commit is contained in:
djaco
2025-07-21 16:26:31 +09:00
parent d0642f2f6b
commit e8f94c680f
8 changed files with 446 additions and 310 deletions

View File

@@ -31,6 +31,9 @@ export const types = {
SET_OPTIONAL_TERMS_POPUP_SHOWN: "SET_OPTIONAL_TERMS_POPUP_SHOWN", SET_OPTIONAL_TERMS_POPUP_SHOWN: "SET_OPTIONAL_TERMS_POPUP_SHOWN",
SET_OPTIONAL_TERMS_USER_DECISION: "SET_OPTIONAL_TERMS_USER_DECISION", SET_OPTIONAL_TERMS_USER_DECISION: "SET_OPTIONAL_TERMS_USER_DECISION",
RESET_OPTIONAL_TERMS_SESSION: "RESET_OPTIONAL_TERMS_SESSION", RESET_OPTIONAL_TERMS_SESSION: "RESET_OPTIONAL_TERMS_SESSION",
// 선택약관 직접 상태 업데이트 (API 호출 없이)
UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: "UPDATE_OPTIONAL_TERMS_AGREE_DIRECT",
UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: "UPDATE_TERMS_AGREEMENT_STATUS_DIRECT",
SET_EXIT_APP: "SET_EXIT_APP", SET_EXIT_APP: "SET_EXIT_APP",
GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA", GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA",
GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN", GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN",
@@ -209,12 +212,9 @@ export const types = {
* *
* SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다. * SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다.
* CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다. * CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다.
* PAUSE_PLAYER_CONTROL: 현재 제어권을 가진 비디오를 '일시정지' 상태로 변경합니다.
*/ */
SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL", SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL",
CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL", CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL",
PAUSE_PLAYER_CONTROL: "PAUSE_PLAYER_CONTROL",
RESUME_PLAYER_CONTROL: "RESUME_PLAYER_CONTROL",
// reset action // reset action
RESET_REDUX_STATE: "RESET_REDUX_STATE", RESET_REDUX_STATE: "RESET_REDUX_STATE",
@@ -242,4 +242,7 @@ export const types = {
// device // device
REQ_REG_DEVICE_INFO: "REQ_REG_DEVICE_INFO", REQ_REG_DEVICE_INFO: "REQ_REG_DEVICE_INFO",
// 🔽 [추가] 영구재생 비디오 정보 저장
SET_PERSISTENT_VIDEO_INFO: "SET_PERSISTENT_VIDEO_INFO",
}; };

View File

@@ -769,3 +769,24 @@ export const handleOptionalTermsDecline = () => (dispatch) => {
dispatch(setOptionalTermsUserDecision('declined')); dispatch(setOptionalTermsUserDecision('declined'));
dispatch(setOptionalTermsPopupShown(true)); dispatch(setOptionalTermsPopupShown(true));
}; };
// 선택약관 상태 통합 업데이트 (TV 환경 최적화 - API 호출 없이 즉시 반영)
export const updateOptionalTermsAgreement = (agreed = true) => (dispatch) => {
console.log(`[CommonActions] 선택약관 통합 상태 업데이트: ${agreed}`);
// 1. optionalTermsPopupFlow 업데이트 (TV 환경용)
dispatch(setOptionalTermsUserDecision(agreed ? 'agreed' : 'declined'));
dispatch(setOptionalTermsPopupShown(true));
// 2. 기본 optionalTermsAgree 상태 직접 업데이트 (API 호출 없이)
dispatch({
type: types.UPDATE_OPTIONAL_TERMS_AGREE_DIRECT,
payload: agreed
});
// 3. termsAgreementStatus도 동기화
dispatch({
type: types.UPDATE_TERMS_AGREEMENT_STATUS_DIRECT,
payload: { MST00405: agreed }
});
};

View File

@@ -2,6 +2,7 @@ import { URLS } from "../api/apiConfig";
import { TAxios,TAxiosPromise } from "../api/TAxios"; import { TAxios,TAxiosPromise } from "../api/TAxios";
import { types } from "./actionTypes"; import { types } from "./actionTypes";
import { changeAppStatus, getTermsAgreeYn } from "./commonActions"; import { changeAppStatus, getTermsAgreeYn } from "./commonActions";
import { collectBannerPositions } from "../utils/domUtils";
// 약관 정보 조회 IF-LGSP-005 // 약관 정보 조회 IF-LGSP-005
export const getHomeTerms = (props) => (dispatch, getState) => { export const getHomeTerms = (props) => (dispatch, getState) => {
@@ -521,3 +522,57 @@ export const setBannerIndex = (bannerId, index) => {
payload: { bannerId, index }, payload: { bannerId, index },
}; };
}; };
// 🔽 [추가] 새로운 배너 위치 및 영구재생 관련 액션들
/**
* 모든 배너의 위치 정보를 설정합니다.
* @param {Object} positions - 배너별 위치 정보 맵
*/
export const setBannerPositions = (positions) => ({
type: types.SET_BANNER_POSITIONS,
payload: positions,
});
/**
* 특정 배너의 위치 정보를 업데이트합니다.
* @param {string} bannerId - 배너 ID
* @param {Object} position - 위치 정보
*/
export const updateBannerPosition = (bannerId, position) => ({
type: types.UPDATE_BANNER_POSITION,
payload: { bannerId, position },
});
/**
* 영구재생 비디오 정보를 설정합니다.
* @param {Object} videoInfo - 비디오 정보
*/
export const setPersistentVideoInfo = (videoInfo) => ({
type: types.SET_PERSISTENT_VIDEO_INFO,
payload: videoInfo,
});
/**
* 영구재생 비디오 정보를 클리어합니다.
*/
export const clearPersistentVideoInfo = () => ({
type: types.CLEAR_PERSISTENT_VIDEO_INFO,
});
/**
* 모든 배너의 현재 DOM 위치를 수집하여 Redux 스토어에 저장합니다.
* @param {Array<string>} bannerIds - 수집할 배너 ID 배열
*/
export const collectAndSaveBannerPositions = (bannerIds) => async (dispatch) => {
try {
const positions = await collectBannerPositions(bannerIds);
dispatch(setBannerPositions(positions));
if (process.env.NODE_ENV === "development") {
console.log("[homeActions] 배너 위치 수집 완료:", positions);
}
} catch (error) {
console.error("[homeActions] 배너 위치 수집 실패:", error);
}
};

View File

@@ -414,6 +414,24 @@ export const commonReducer = (state = initialState, action) => {
}; };
} }
// 선택약관 직접 상태 업데이트 케이스들 (API 호출 없이 즉시 반영)
case types.UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: {
return {
...state,
optionalTermsAgree: action.payload, // 직접 업데이트 (API 호출 없이)
};
}
case types.UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: {
return {
...state,
termsAgreementStatus: {
...state.termsAgreementStatus,
...action.payload, // { MST00405: true/false }
},
};
}
default: default:
return state; return state;
} }

View File

@@ -23,10 +23,11 @@ const initialState = {
curationTitle: "", curationTitle: "",
playerControl: { playerControl: {
ownerId: null, ownerId: null,
isPaused: false, videoInfo: null,
}, },
termsIdMap: {}, // added new property to initialState termsIdMap: {}, // added new property to initialState
optionalTermsAvailable: false, // 선택약관 존재 여부 optionalTermsAvailable: false, // 선택약관 존재 여부
persistentVideoInfo: null, // 영구재생 비디오 정보
}; };
export const homeReducer = (state = initialState, action) => { export const homeReducer = (state = initialState, action) => {
@@ -207,14 +208,13 @@ export const homeReducer = (state = initialState, action) => {
} }
} }
// 🔽 [추가] 플레이 제어 매니저 Reducer 로직 // 🔽 [수정] 새로운 플레이 제어 로직
case types.SET_PLAYER_CONTROL: { case types.SET_PLAYER_CONTROL: {
return { return {
...state, ...state,
playerControl: { playerControl: {
...state.playerControl, ownerId: action.payload.ownerId,
ownerId: action.payload.ownerId, // 제어권 소유주 ID 설정 videoInfo: action.payload.videoInfo,
isPaused: false, // 새로운 제어권이 부여되면 '일시정지' 상태는 해제
}, },
}; };
} }
@@ -223,34 +223,19 @@ export const homeReducer = (state = initialState, action) => {
return { return {
...state, ...state,
playerControl: { playerControl: {
...state.playerControl, ownerId: null,
ownerId: null, // 제어권 소유주 없음 videoInfo: null,
// isPaused는 유지할 수도, 초기화할 수도 있음. 여기선 초기화.
isPaused: false,
}, },
}; };
} }
case types.PAUSE_PLAYER_CONTROL: { case types.SET_PERSISTENT_VIDEO_INFO: {
return { return {
...state, ...state,
playerControl: { persistentVideoInfo: action.payload,
...state.playerControl,
isPaused: true, // '일시정지' 상태로 설정
},
};
}
// 🔼 [추가]
case types.RESUME_PLAYER_CONTROL: {
return {
...state,
playerControl: {
...state.playerControl,
isPaused: false,
},
}; };
} }
// 🔼 [수정]
default: default:
return state; return state;

View File

@@ -11,6 +11,7 @@ import {
setDefaultFocus, setDefaultFocus,
// setShowPopup, // setShowPopup,
fetchCurrentUserHomeTerms, fetchCurrentUserHomeTerms,
collectAndSaveBannerPositions,
} from "../../../actions/homeActions"; } from "../../../actions/homeActions";
import { import {
changeAppStatus, changeAppStatus,
@@ -18,12 +19,12 @@ import {
setOptionalTermsUserDecision, setOptionalTermsUserDecision,
handleOptionalTermsAgree as handleOptionalTermsAgreeAction, handleOptionalTermsAgree as handleOptionalTermsAgreeAction,
handleOptionalTermsDecline, handleOptionalTermsDecline,
updateOptionalTermsAgreement,
} from "../../../actions/commonActions"; } from "../../../actions/commonActions";
import { setMyPageTermsAgree } from "../../../actions/myPageActions"; import { setMyPageTermsAgree } from "../../../actions/myPageActions";
import { pushPanel, popPanel } from "../../../actions/panelActions"; import { pushPanel, popPanel } from "../../../actions/panelActions";
import { panel_names } from "../../../utils/Config"; import { panel_names } from "../../../utils/Config";
import { import {
startVideoPlayer,
requestPlayControl, requestPlayControl,
releasePlayControl, releasePlayControl,
} from "../../../actions/playActions"; } from "../../../actions/playActions";
@@ -78,50 +79,34 @@ export default function HomeBanner({
} }
}, [handleItemFocus]); }, [handleItemFocus]);
// 🔽 [추가] 0번째 배너에 대한 영구 비디오 재생을 처리하는 useEffect // 🔽 [수정] 중앙 제어 시스템을 사용하는 새로운 영구재생 로직
// useEffect(() => { useEffect(() => {
// if ( // DSP00201 템플릿이고 배너 데이터가 있을 때만 실행
// bannerDataList && if (
// bannerDataList.length > 0 && bannerDataList &&
// selectTemplate === "DSP00201" bannerDataList.length > 0 &&
// ) { selectTemplate === "DSP00201"
// const banner0Data = bannerDataList[0]?.bannerDetailInfos?.[0]; ) {
const banner0Data = bannerDataList[0]?.bannerDetailInfos?.[0];
// if (banner0Data && banner0Data.showUrl) { if (banner0Data && banner0Data.showUrl) {
// // DOM 요소가 존재하는지 확인 후 실행 const videoInfo = {
// console.log("[HomeBanner] banner0Data", banner0Data); showUrl: banner0Data.showUrl,
// const checkAndPlay = () => { patnrId: banner0Data.patnrId,
// const targetElement = document.querySelector( showId: banner0Data.showId,
// '[data-spotlight-id="banner0"]', shptmBanrTpNm: "MEDIA",
// ); modal: true,
// console.log("[HomeBanner] targetElement", targetElement); modalContainerId: "banner0",
// if (targetElement) { spotlightDisable: true, // 영구재생 비디오는 포커스를 받지 않음
// console.log("[HomeBanner] targetElement 존재"); };
// dispatch( // 중앙 제어 시스템에 영구재생 시작을 요청
// startVideoPlayer({ dispatch(requestPlayControl("banner0_persistent", videoInfo));
// showUrl: banner0Data.showUrl, }
// patnrId: banner0Data.patnrId, }
// showId: banner0Data.showId, // eslint-disable-next-line react-hooks/exhaustive-deps
// shptmBanrTpNm: "MEDIA", }, [dispatch, selectTemplate, JSON.stringify(bannerDataList)]); // bannerDataList의 내용이 바뀔 때만 실행
// 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]);
// 🔽 [수정] 새로운 1번 배너 포커스/블러 핸들러
const handleSecondBannerFocus = useCallback(() => { const handleSecondBannerFocus = useCallback(() => {
const secondBannerData = bannerDataList?.[1]; const secondBannerData = bannerDataList?.[1];
if (secondBannerData) { if (secondBannerData) {
@@ -137,16 +122,20 @@ export default function HomeBanner({
modal: true, modal: true,
modalContainerId: "banner1", modalContainerId: "banner1",
modalClassName: css.videoModal, modalClassName: css.videoModal,
isVerticalModal: true, // Assuming second banner is horizontal, so modal is vertical isVerticalModal: true,
}; };
// 중앙 제어 시스템에 '미리보기' 재생을 요청
dispatch(requestPlayControl("banner1_preview", videoInfo)); dispatch(requestPlayControl("banner1_preview", videoInfo));
} }
if (handleItemFocus) { if (handleItemFocus) {
handleItemFocus(); handleItemFocus();
} }
}, [dispatch, bannerDataList, handleItemFocus]); }, [dispatch, bannerDataList, handleItemFocus]);
const handleSecondBannerBlur = useCallback(() => { const handleSecondBannerBlur = useCallback(() => {
// 중앙 제어 시스템에 '미리보기' 제어권 해제를 요청
dispatch(releasePlayControl("banner1_preview")); dispatch(releasePlayControl("banner1_preview"));
}, [dispatch]); }, [dispatch]);
@@ -271,13 +260,10 @@ export default function HomeBanner({
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.log("[HomeBanner] 약관 동의 성공:", response); console.log("[HomeBanner] 약관 동의 성공:", response);
} }
// 새로운 Redux 액션을 사용하여 상태 업데이트 // ✅ IntroPanel과 동일한 방식으로 Redux 상태 직접 업데이트 (API 호출 없이)
dispatch(setOptionalTermsUserDecision('agreed')); dispatch(updateOptionalTermsAgreement(true));
dispatch(setOptionalTermsPopupShown(true));
// 로컬 상태도 업데이트 (기존 로직 유지) // 로컬 상태도 업데이트 (기존 로직 유지)
setOptionalTermsAgreed(true); setOptionalTermsAgreed(true);
// 약관 데이터 갱신
dispatch(fetchCurrentUserHomeTerms());
} else { } else {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.error("[HomeBanner] 약관 동의 실패:", response); console.error("[HomeBanner] 약관 동의 실패:", response);
@@ -309,9 +295,8 @@ export default function HomeBanner({
const handleOptionalDeclineClick = useCallback(() => { const handleOptionalDeclineClick = useCallback(() => {
console.log("[HomeBanner] 거절/다음에 하기 버튼 클릭"); console.log("[HomeBanner] 거절/다음에 하기 버튼 클릭");
// 새로운 Redux 액션을 사용하여 거절 상태 업데이트 // 거절 상태 업데이트
dispatch(setOptionalTermsUserDecision('declined')); dispatch(updateOptionalTermsAgreement(false));
dispatch(setOptionalTermsPopupShown(true));
setIsOptionalConfirmVisible(false); setIsOptionalConfirmVisible(false);
}, [dispatch]); }, [dispatch]);
@@ -410,80 +395,29 @@ export default function HomeBanner({
} }
}, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]); }, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]);
// const renderItem = useCallback(
// (index, isHorizontal) => {
// const data = bannerDataList?.[index] ?? {};
// if (index === 1) {
// return (
// <div className={!isHorizontal ? css.imgBox : undefined}>
// <RandomUnitNew
// bannerData={data}
// isHorizontal={isHorizontal}
// key={"banner" + index}
// spotlightId={"banner" + index}
// handleShelfFocus={_handleShelfFocus}
// onFocus={handleSecondBannerFocus}
// onBlur={handleSecondBannerBlur}
// randomNumber={data.randomIndex}
// />
// </div>
// );
// }
// 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" ? (
// <Random
// bannerData={data}
// isHorizontal={isHorizontal}
// key={"banner" + index}
// spotlightId={"banner" + index}
// handleShelfFocus={_handleShelfFocus}
// 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>
// );
// },
// [
// bannerDataList,
// _handleItemFocus,
// _handleShelfFocus,
// handleSecondBannerFocus,
// handleSecondBannerBlur,
// ],
// );
const renderItem = useCallback( const renderItem = useCallback(
(index, isHorizontal) => { (index, isHorizontal) => {
const data = bannerDataList?.[index] ?? {}; const data = bannerDataList?.[index] ?? {};
// 1번 배너에 새로운 포커스/블러 이벤트 핸들러 연결
if (index === 1) {
return (
<div className={!isHorizontal ? css.imgBox : undefined}>
<Random
bannerData={data}
isHorizontal={isHorizontal}
key={"banner" + index}
spotlightId={"banner" + index}
handleShelfFocus={_handleShelfFocus}
handleItemFocus={_handleItemFocus}
randomNumber={data.randomIndex}
onFocus={handleSecondBannerFocus}
onBlur={handleSecondBannerBlur}
/>
</div>
);
}
return ( return (
<div className={!isHorizontal ? css.imgBox : undefined}> <div className={!isHorizontal ? css.imgBox : undefined}>
{data.shptmDspyTpNm === "Rolling" ? ( {data.shptmDspyTpNm === "Rolling" ? (
@@ -525,7 +459,7 @@ export default function HomeBanner({
</div> </div>
); );
}, },
[_handleItemFocus, _handleShelfFocus, bannerDataList], [_handleItemFocus, _handleShelfFocus, bannerDataList, handleSecondBannerFocus, handleSecondBannerBlur],
); );
// 1번째 배너(포커스 재생) 및 기타 배너를 위한 렌더링 함수 // 1번째 배너(포커스 재생) 및 기타 배너를 위한 렌더링 함수
@@ -554,7 +488,7 @@ export default function HomeBanner({
if (data.shptmDspyTpNm === "Rolling") { if (data.shptmDspyTpNm === "Rolling") {
return ( return (
<div className={!isHorizontal ? css.imgBox : undefined}> <div className={!isHorizontal ? css.imgBox : undefined}>
<RollingUnit <Rolling
bannerData={data} bannerData={data}
isHorizontal={isHorizontal} isHorizontal={isHorizontal}
key={"banner" + index} key={"banner" + index}

View File

@@ -20,6 +20,7 @@ import {
setOptionalTermsUserDecision, setOptionalTermsUserDecision,
setOptionalTermsPopupShown, setOptionalTermsPopupShown,
resetOptionalTermsSession, resetOptionalTermsSession,
updateOptionalTermsAgreement,
// setTermsAgreeYn, // setTermsAgreeYn,
} from "../../actions/commonActions"; } from "../../actions/commonActions";
import { registerDevice } from "../../actions/deviceActions"; import { registerDevice } from "../../actions/deviceActions";
@@ -38,11 +39,11 @@ import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare";
import TPanel from "../../components/TPanel/TPanel"; import TPanel from "../../components/TPanel/TPanel";
import TPopUp from "../../components/TPopUp/TPopUp"; import TPopUp from "../../components/TPopUp/TPopUp";
import TNewPopUp from "../../components/TPopUp/TNewPopUp"; import TNewPopUp from "../../components/TPopUp/TNewPopUp";
// import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; // 1단계: 활성화
import useDebugKey from "../../hooks/useDebugKey"; import useDebugKey from "../../hooks/useDebugKey";
import useSafeFocusState from "../../hooks/useSafeFocusState"; // import useSafeFocusState from "../../hooks/useSafeFocusState"; // 2단계: 제거
import useTermsStateMachine from "../../hooks/useTermsStateMachine"; import useTermsStateMachine from "../../hooks/useTermsStateMachine";
import useRightPanelContent from "../../hooks/useRightPanelContent"; // import useRightPanelContent from "../../hooks/useRightPanelContent"; // 1단계: 제거
import * as Config from "../../utils/Config"; import * as Config from "../../utils/Config";
import { panel_names } from "../../utils/Config"; import { panel_names } from "../../utils/Config";
import { $L } from "../../utils/helperMethods"; import { $L } from "../../utils/helperMethods";
@@ -108,9 +109,11 @@ function IntroPanelWithOptional({
const processingTimeoutRef = useRef(null); const processingTimeoutRef = useRef(null);
// [추가] 재시도 인터벌 참조 // [추가] 재시도 인터벌 참조
const retryIntervalRef = useRef(null); const retryIntervalRef = useRef(null);
// 필수약관 팝업 타이머 참조
const popupTimeoutRef = useRef(null);
// const [isRequiredFocused, setIsRequiredFocused] = useState(false); // const [isRequiredFocused, setIsRequiredFocused] = useState(false);
const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState(); // const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState(); // 2단계: 제거
const { state: termsState, updateStateAsync } = useTermsStateMachine(); const { state: termsState, updateStateAsync } = useTermsStateMachine();
const { const {
termsChecked, termsChecked,
@@ -152,33 +155,83 @@ function IntroPanelWithOptional({
// WebOS 버전별 UI 표시 모드 결정 // WebOS 버전별 UI 표시 모드 결정
// 이미지 표시: 4.0, 5.0, 23, 24 // 이미지 표시: 4.0, 5.0, 23, 24
// 텍스트 표시: 4.5, 6.0, 22 // 텍스트 표시: 4.5, 6.0, 22
// 25.07.17 추가
// 4.0 : 4.4.9 까지
// 4.5 : 4.5.0 ~
// 5.0 : 5.0.0~
// 6.0 : 6.0.0~
// 22 : 7.0.0~
// 23 : 8.0.0~
// 24 : 9.0.0~
// 25 : 10.0.0~
const shouldShowBenefitsView = useMemo(() => { const shouldShowBenefitsView = useMemo(() => {
if (!webOSVersion) return false; // if (!webOSVersion) return false;
// webOSVersion 이 없거나 빈 문자열인 경우 이미지 보기 처리
const versionNum = Number.parseFloat(String(webOSVersion)); if (!webOSVersion || webOSVersion === "") {
return false;
// 텍스트 표시 버전들 (숫자로 비교) }
const textVersions = [4.5, 6.0, 22];
const versionNum = Number.parseFloat(String(webOSVersion)); // 기존 로직 유지
// 이미지 표시 버전들 const versionStr = String(webOSVersion);
const imageVersions = [4.0, 5.0, 23, 24];
// 버전 문자열을 배열로 변환해서 비교
// 텍스트 버전인지 확인 const parseVersionArray = (version) => {
const shouldShowText = textVersions.includes(versionNum); return version.split('.').map(Number);
};
const currentVersionArray = parseVersionArray(versionStr);
// 버전 구간별 약관 버전 매핑
let termsVersion = null;
if (currentVersionArray[0] >= 10) {
termsVersion = 25;
} else if (currentVersionArray[0] >= 9) {
termsVersion = 24;
} else if (currentVersionArray[0] >= 8) {
termsVersion = 23;
} else if (currentVersionArray[0] >= 7) {
termsVersion = 22;
} else if (currentVersionArray[0] >= 6) {
termsVersion = 6.0;
} else if (currentVersionArray[0] >= 5) {
termsVersion = 5.0;
} else if (currentVersionArray[0] === 4) {
// 4.x 버전은 세밀한 구분 필요
const minor = currentVersionArray[1] || 0;
const patch = currentVersionArray[2] || 0;
if (minor >= 5) {
termsVersion = 4.5; // 4.5.0 이상
} else {
termsVersion = 4.0; // 4.0.0 ~ 4.4.9
}
}
const textTermsVersions = [4.5, 6.0, 22];
// const imageTermsVersions = [4.0, 5.0, 23, 24];
const shouldShowText = textTermsVersions.includes(termsVersion);
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.log("🔍 WebOS 버전별 UI 모드:"); console.log("🔍 WebOS 버전별 UI 모드:");
console.log(" - webOSVersion:", versionNum); console.log(" - webOSVersion:", webOSVersion);
console.log(" - versionNum:", versionNum);
console.log(" - 파싱된 버전 배열:", currentVersionArray);
console.log(" - 매핑된 약관 버전:", termsVersion);
console.log(" - shouldShowText (텍스트 모드):", shouldShowText); console.log(" - shouldShowText (텍스트 모드):", shouldShowText);
console.log(" - 텍스트 버전들:", textVersions);
console.log(" - 이미지 버전들:", imageVersions);
} }
return shouldShowText; return shouldShowText;
}, [webOSVersion]); }, [webOSVersion]);
// 상태 관리 // 상태 관리
const [currentTerms, setCurrentTerms] = useState(null); const [currentTerms, setCurrentTerms] = useState(null);
// 필수약관 체크 해제시 팝업표시용
const [requiredAgreePopup, setRequiredAgreePopup] = useState(false);
const [prevTermsChecked, setPrevTermsChecked] = useState(false);
const [prevPrivacyChecked, setPrevPrivacyChecked] = useState(false);
useEffect(() => { useEffect(() => {
dispatch({ type: types.REGISTER_DEVICE_RESET }); dispatch({ type: types.REGISTER_DEVICE_RESET });
@@ -221,6 +274,10 @@ function IntroPanelWithOptional({
return () => { return () => {
clearTimeout(focusTimer); clearTimeout(focusTimer);
// 팝업 타이머도 정리
if (popupTimeoutRef.current) {
clearTimeout(popupTimeoutRef.current);
}
}; };
}, []); }, []);
@@ -237,6 +294,23 @@ function IntroPanelWithOptional({
} }
}, [termsError, dispatch]); }, [termsError, dispatch]);
// 필수약관 체크 해제 감지 및 팝업 표시
useEffect(() => {
// 이전 상태가 true이고 현재 상태가 false인 경우 (체크 해제)
if ((prevTermsChecked && !termsChecked) || (prevPrivacyChecked && !privacyChecked)) {
setRequiredAgreePopup(true);
// 3초 후 자동 닫기
popupTimeoutRef.current = setTimeout(() => {
setRequiredAgreePopup(false);
}, 3000);
}
// 현재 상태를 이전 상태로 업데이트
setPrevTermsChecked(termsChecked);
setPrevPrivacyChecked(privacyChecked);
}, [termsChecked, privacyChecked, prevTermsChecked, prevPrivacyChecked]);
useEffect(() => { useEffect(() => {
console.log("[IntroPanel] deviceRegistered", deviceRegistered); console.log("[IntroPanel] deviceRegistered", deviceRegistered);
if (deviceRegistered) { if (deviceRegistered) {
@@ -361,20 +435,19 @@ function IntroPanelWithOptional({
if (newRegDeviceData && newRegDeviceData.retCode === 0) { if (newRegDeviceData && newRegDeviceData.retCode === 0) {
dispatch(setDeviceRegistered(true)); dispatch(setDeviceRegistered(true));
// 선택약관 상태를 Redux에 업데이트 (TV 환경 최적화) // 선택약관 상태를 통합 액션으로 업데이트 (TV 환경 최적화)
if (optionalChecked) { if (optionalChecked) {
// 선택약관에 동의한 경우 // 선택약관에 동의한 경우
console.log("[IntroPanel] 선택약관 동의됨 - Redux 상태 업데이트"); console.log("[IntroPanel] 선택약관 동의됨 - 통합 Redux 상태 업데이트");
dispatch(setOptionalTermsUserDecision('agreed')); dispatch(updateOptionalTermsAgreement(true));
dispatch(setOptionalTermsPopupShown(true));
} else { } else {
// 선택약관에 동의하지 않은 경우 - HomeBanner에서 팝업이 나올 수 있도록 상태를 초기화 // 선택약관에 동의하지 않은 경우 - HomeBanner에서 팝업이 나올 수 있도록 상태를 초기화
console.log("[IntroPanel] 선택약관 미동의 - HomeBanner 팝업 허용을 위해 상태 초기화"); console.log("[IntroPanel] 선택약관 미동의 - HomeBanner 팝업 허용을 위해 상태 초기화");
// fetchCurrentUserHomeTerms 완료 후 Redux 상태를 리셋하여 HomeBanner에서 팝업 표시 조건을 정확히 평가할 수 있도록 함 dispatch(updateOptionalTermsAgreement(false));
setTimeout(() => { setTimeout(() => {
console.log("[IntroPanel] 약관 데이터 갱신 후 상태 리셋 실행"); console.log("[IntroPanel] 선택약관 세션 리셋 실행");
dispatch(resetOptionalTermsSession()); dispatch(resetOptionalTermsSession());
}, 1000); // 약관 데이터 갱신 완료를 기다림 }, 1000);
} }
dispatch( dispatch(
@@ -568,16 +641,16 @@ function IntroPanelWithOptional({
} }
}, [dispatch, activePopup]); }, [dispatch, activePopup]);
const handleFocus = useCallback( // const handleFocus = useCallback(
(item) => { // (item) => {
setFocusAsync(item); // setFocusAsync(item);
}, // },
[setFocusAsync], // [setFocusAsync],
); // ); // 2단계: 제거
const handleBlur = useCallback(() => { // const handleBlur = useCallback(() => {
clearFocusAsync(0); // clearFocusAsync(0);
}, [clearFocusAsync]); // }, [clearFocusAsync]); // 2단계: 제거
// 체크박스 핸들러들 // 체크박스 핸들러들
const handleTermsToggle = useCallback( const handleTermsToggle = useCallback(
@@ -669,49 +742,64 @@ function IntroPanelWithOptional({
[handleOptionalTermsClick], [handleOptionalTermsClick],
); );
const handleFocusTermsCheckbox = useCallback( // const handleFocusTermsCheckbox = useCallback(
() => handleFocus("termsCheckbox"), // () => handleFocus("termsCheckbox"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusTermsButton = useCallback( // const handleFocusTermsButton = useCallback(
() => handleFocus("termsButton"), // () => handleFocus("termsButton"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusPrivacyCheckbox = useCallback( // const handleFocusPrivacyCheckbox = useCallback(
() => handleFocus("privacyCheckbox"), // () => handleFocus("privacyCheckbox"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusPrivacyButton = useCallback( // const handleFocusPrivacyButton = useCallback(
() => handleFocus("privacyButton"), // () => handleFocus("privacyButton"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusOptionalCheckbox = useCallback( // const handleFocusOptionalCheckbox = useCallback(
() => handleFocus("optionalCheckbox"), // () => handleFocus("optionalCheckbox"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusOptionalButton = useCallback( // const handleFocusOptionalButton = useCallback(
() => handleFocus("optionalButton"), // () => handleFocus("optionalButton"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusSelectAllCheckbox = useCallback( // const handleFocusSelectAllCheckbox = useCallback(
() => handleFocus("selectAllCheckbox"), // () => handleFocus("selectAllCheckbox"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusAgreeButton = useCallback( // const handleFocusAgreeButton = useCallback(
() => handleFocus("agreeButton"), // () => handleFocus("agreeButton"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const handleFocusDisagreeButton = useCallback( // const handleFocusDisagreeButton = useCallback(
() => handleFocus("disagreeButton"), // () => handleFocus("disagreeButton"),
[handleFocus], // [handleFocus],
); // ); // 2단계: 제거
const rightPanelContent = useRightPanelContent( // const rightPanelContent = useRightPanelContent(
focusedItem, // focusedItem,
termsChecked, // termsChecked,
privacyChecked, // privacyChecked,
shouldShowBenefitsView, // shouldShowBenefitsView,
); // ); // 1단계: 제거
// 1단계: WebOS 버전 기반 고정 콘텐츠로 교체
const rightPanelContent = useMemo(() => {
return shouldShowBenefitsView ? (
<div className={css.optionalDescription}>
{$L('By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features')}
</div>
) : (
<OptionalTermsInfo
displayMode="image"
imageTitle={$L("Agree and Enjoy Special Benefits")}
spotlightId="optional-terms-info"
/>
);
}, [shouldShowBenefitsView]);
useEffect(() => { useEffect(() => {
Spotlight.focus(); Spotlight.focus();
@@ -776,24 +864,20 @@ function IntroPanelWithOptional({
<div className={css.termsLeftPanel}> <div className={css.termsLeftPanel}>
{/* Terms & Conditions */} {/* Terms & Conditions */}
<div className={css.termsItem}> <div className={css.termsItem}>
<TCheckBoxSquare <TCheckBoxSquare
className={css.customeCheckbox} className={css.customeCheckbox}
selected={termsChecked} selected={termsChecked}
onToggle={handleTermsToggle} onToggle={handleTermsToggle}
onFocus={handleFocusTermsCheckbox} spotlightId="termsCheckbox"
onBlur={handleBlur} ariaLabel={$L("Terms & Conditions checkbox")}
spotlightId="termsCheckbox" />
ariaLabel={$L("Terms & Conditions checkbox")} <TButton
/> className={css.termsButton}
<TButton onClick={handleTermsClickMST00402}
className={css.termsButton} spotlightId="termsButton"
onClick={handleTermsClickMST00402} type={TYPES.terms}
onFocus={handleFocusTermsButton} ariaLabel={$L("View Terms & Conditions")}
onBlur={handleBlur} >
spotlightId="termsButton"
type={TYPES.terms}
ariaLabel={$L("View Terms & Conditions")}
>
<span className={`${css.termsText} ${css.required}`}> <span className={`${css.termsText} ${css.required}`}>
{$L("Terms & Conditions")} {$L("Terms & Conditions")}
</span> </span>
@@ -802,24 +886,20 @@ function IntroPanelWithOptional({
{/* Privacy Policy */} {/* Privacy Policy */}
<div className={css.termsItem}> <div className={css.termsItem}>
<TCheckBoxSquare <TCheckBoxSquare
className={css.customeCheckbox} className={css.customeCheckbox}
selected={privacyChecked} selected={privacyChecked}
onToggle={handlePrivacyToggle} onToggle={handlePrivacyToggle}
onFocus={handleFocusPrivacyCheckbox} spotlightId="privacyCheckbox"
onBlur={handleBlur} ariaLabel={$L("Privacy Policy checkbox")}
spotlightId="privacyCheckbox" />
ariaLabel={$L("Privacy Policy checkbox")} <TButton
/> className={css.termsButton}
<TButton onClick={handleTermsClickMST00401}
className={css.termsButton} spotlightId="privacyButton"
onClick={handleTermsClickMST00401} type={TYPES.terms}
onFocus={handleFocusPrivacyButton} ariaLabel={$L("View Privacy Policy")}
onBlur={handleBlur} >
spotlightId="privacyButton"
type={TYPES.terms}
ariaLabel={$L("View Privacy Policy")}
>
<span className={`${css.termsText} ${css.required}`}> <span className={`${css.termsText} ${css.required}`}>
{$L("Privacy Policy")} {$L("Privacy Policy")}
</span> </span>
@@ -828,25 +908,21 @@ function IntroPanelWithOptional({
{/* Optional Terms */} {/* Optional Terms */}
<div className={css.termsItem}> <div className={css.termsItem}>
<TCheckBoxSquare <TCheckBoxSquare
className={css.customeCheckbox} className={css.customeCheckbox}
selected={optionalChecked} selected={optionalChecked}
onToggle={handleOptionalToggle} onToggle={handleOptionalToggle}
onFocus={handleFocusOptionalCheckbox} spotlightId="optionalCheckbox"
onBlur={handleBlur} ariaLabel={$L("Optional Terms checkbox")}
spotlightId="optionalCheckbox" />
ariaLabel={$L("Optional Terms checkbox")} <TButton
/> className={css.termsButton}
<TButton onClick={handleOptionalTermsClickMST00405}
className={css.termsButton} spotlightId="optionalButton"
onClick={handleOptionalTermsClickMST00405} type={TYPES.terms}
onFocus={handleFocusOptionalButton} ariaLabel={$L("View Optional Terms")}
onBlur={handleBlur} style={{ marginBottom: 0 }} // 제일 아래 버튼의 경우 margin-bottom 0
spotlightId="optionalButton" >
type={TYPES.terms}
ariaLabel={$L("View Optional Terms")}
style={{ marginBottom: 0 }} // 제일 아래 버튼의 경우 margin-bottom 0
>
<span className={css.termsText}>{$L("Optional Terms")}</span> <span className={css.termsText}>{$L("Optional Terms")}</span>
</TButton> </TButton>
</div> </div>
@@ -860,8 +936,6 @@ function IntroPanelWithOptional({
className={css.selectAllCheckbox} className={css.selectAllCheckbox}
selected={selectAllChecked} selected={selectAllChecked}
onToggle={handleSelectAllToggle} onToggle={handleSelectAllToggle}
onFocus={handleFocusSelectAllCheckbox}
onBlur={handleBlur}
spotlightId="selectAllCheckbox" spotlightId="selectAllCheckbox"
ariaLabel={$L("Select All checkbox")} ariaLabel={$L("Select All checkbox")}
/> />
@@ -873,8 +947,6 @@ function IntroPanelWithOptional({
<TButton <TButton
className={css.agreeButton} className={css.agreeButton}
onClick={handleAgree} onClick={handleAgree}
onFocus={handleFocusAgreeButton}
onBlur={handleBlur}
spotlightId="agreeButton" spotlightId="agreeButton"
type={TYPES.agree} type={TYPES.agree}
ariaLabel={$L("Agree to terms")} ariaLabel={$L("Agree to terms")}
@@ -886,8 +958,6 @@ function IntroPanelWithOptional({
<TButton <TButton
className={css.disagreeButton} className={css.disagreeButton}
onClick={handleDisagree} onClick={handleDisagree}
onFocus={handleFocusDisagreeButton}
onBlur={handleBlur}
spotlightId="disagreeButton" spotlightId="disagreeButton"
type={TYPES.agree} type={TYPES.agree}
ariaLabel={$L("Do not agree to terms")} ariaLabel={$L("Do not agree to terms")}
@@ -978,6 +1048,18 @@ function IntroPanelWithOptional({
)} )}
/> />
)} )}
<TPopUp
kind="textPopup"
open={requiredAgreePopup}
onClose={() => setRequiredAgreePopup(false)}
hasText
title={$L("")}
text={$L(
"Please check the box to accept the Terms & Conditions and Privacy Policy."
)}
/>
</Region> </Region>
); );
} }

View File

@@ -19,12 +19,12 @@ import {
setHidePopup, setHidePopup,
setShowPopup, setShowPopup,
getTermsAgreeYn, getTermsAgreeYn,
updateOptionalTermsAgreement,
} from "../../../../actions/commonActions"; } from "../../../../actions/commonActions";
import { import {
setMyTermsWithdraw, setMyTermsWithdraw,
setMyPageTermsAgree, setMyPageTermsAgree,
} from "../../../../actions/myPageActions"; } from "../../../../actions/myPageActions";
import { fetchCurrentUserHomeTerms } from "../../../../actions/homeActions";
import TBody from "../../../../components/TBody/TBody"; import TBody from "../../../../components/TBody/TBody";
import TButton, { TYPES } from "../../../../components/TButton/TButton"; import TButton, { TYPES } from "../../../../components/TButton/TButton";
import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller"; import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller";
@@ -75,9 +75,7 @@ export default function TermsOfService({ title, cbScrollTo }) {
}; };
}, []); }, []);
useEffect(() => { // ✅ fetchCurrentUserHomeTerms 호출 제거 - TV 환경에서 서버 동기화 지연 방지
dispatch(fetchCurrentUserHomeTerms());
}, [dispatch]);
useEffect(() => { useEffect(() => {
setLocalOptionalTermsAgree(optionalTermsAgree); setLocalOptionalTermsAgree(optionalTermsAgree);
@@ -156,28 +154,67 @@ export default function TermsOfService({ title, cbScrollTo }) {
[trmsTpCd] [trmsTpCd]
); );
// 1단계: 포커스 대상 결정 함수 생성
const getOptionalTermsFocusTarget = useCallback(() => {
console.log("[TermsOfService] 포커스 대상 결정:", {
optionalTermsAgree,
isOptionalChecked
});
if (optionalTermsAgree) {
console.log("[TermsOfService] → optional-disagree-button (이미 동의함)");
return "optional-disagree-button"; // 이미 동의한 경우
}
//미동의 상태에서 체크박스 선택 여부에 따라 결정
if (isOptionalChecked) {
console.log("[TermsOfService] → optional-agree-button (체크박스 선택됨)");
return "optional-agree-button"; // 체크박스 선택됨 → Agree 버튼으로
} else {
console.log("[TermsOfService] → optional-agree-checkbox (체크박스 미선택)");
return "optional-agree-checkbox"; // 체크박스 미선택 → 체크박스로
}
}, [optionalTermsAgree, isOptionalChecked]);
useEffect(() => { useEffect(() => {
// 이전 Job 정리
if (focusJob.current) { if (focusJob.current) {
focusJob.current.stop(); focusJob.current.stop();
} }
// 이전 타이머 정리
// if (timeoutRef.current) {
// clearTimeout(timeoutRef.current);
// timeoutRef.current = null;
// }
if ( if (
termsList.length > 0 && termsList.length > 0 &&
termsList[selectedTab]?.trmsTpCd === "MST00405" termsList[selectedTab]?.trmsTpCd === "MST00405"
) { ) {
focusJob.current = new Job(() => { focusJob.current = new Job(() => {
const focusTarget = optionalTermsAgree // 초기 포커스: 체크박스로 이동
? "optional-disagree-button" const initialFocusTarget = getOptionalTermsFocusTarget();
: "optional-agree-checkbox"; Spotlight.focus(initialFocusTarget);
Spotlight.focus(focusTarget);
// 0.5초 후 Agree 버튼으로 포커스 이동 (미동의 상태이고 체크박스 미선택인 경우)
if (!optionalTermsAgree && !isOptionalChecked) {
setTimeout(() => {
console.log("[TermsOfService] 1.5초 후 Agree 버튼으로 포커스 이동");
Spotlight.focus("optional-agree-button");
}, 300);
}
}); });
focusJob.current.startAfter(100); focusJob.current.startAfter(100);
} }
return () => { return () => {
if (focusJob.current) { if (focusJob.current) {
focusJob.current.stop(); focusJob.current.stop();
} };
// if (timeoutRef.current) {
// clearTimeout(timeoutRef.current);
// timeoutRef.current = null;
// }
}; };
}, [selectedTab, termsList, optionalTermsAgree]); }, [selectedTab, termsList, optionalTermsAgree, isOptionalChecked, getOptionalTermsFocusTarget]); // 3단계: 의존성 배열 업데이트
useEffect(() => { useEffect(() => {
if (!spotlightDisabled) { if (!spotlightDisabled) {
@@ -238,9 +275,8 @@ export default function TermsOfService({ title, cbScrollTo }) {
({ selected }) => { ({ selected }) => {
if (optionalTermsAgree) return; if (optionalTermsAgree) return;
setIsOptionalChecked(selected); setIsOptionalChecked(selected);
if (selected) { // 포커스 이동은 useEffect의 getOptionalTermsFocusTarget에서 통합 처리
Spotlight.focus("optional-agree-button"); console.log("[TermsOfService] 체크박스 토글:", { selected });
}
}, },
[optionalTermsAgree] [optionalTermsAgree]
); );
@@ -265,8 +301,10 @@ export default function TermsOfService({ title, cbScrollTo }) {
if (response.retCode === "000" || response.retCode === 0) { if (response.retCode === "000" || response.retCode === 0) {
setLocalOptionalTermsAgree(true); setLocalOptionalTermsAgree(true);
console.log("Optional terms agreement successful."); console.log("Optional terms agreement successful.");
// 약관 동의의 후 약관 정보 조회
dispatch(fetchCurrentUserHomeTerms()); // ✅ IntroPanel과 동일한 방식으로 Redux 상태 직접 업데이트 (API 호출 없이)
dispatch(updateOptionalTermsAgreement(true));
setAgreePopup(true); setAgreePopup(true);
if (agreePopupTimeoutRef.current) { if (agreePopupTimeoutRef.current) {
@@ -318,8 +356,8 @@ export default function TermsOfService({ title, cbScrollTo }) {
// console.log("setMyTermsWithdraw callback response:", response); // console.log("setMyTermsWithdraw callback response:", response);
if (response.retCode === "000" || response.retCode === 0) { if (response.retCode === "000" || response.retCode === 0) {
console.log("Optional terms withdrawal successful."); console.log("Optional terms withdrawal successful.");
// 약관 철회 후 약관 정보 조회 // ✅ fetchCurrentUserHomeTerms 호출 제거하고 Redux 상태 직접 업데이트
dispatch(fetchCurrentUserHomeTerms()); dispatch(updateOptionalTermsAgreement(false));
setLocalOptionalTermsAgree(false); setLocalOptionalTermsAgree(false);
setIsOptionalChecked(false); setIsOptionalChecked(false);
} else { } else {