[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_USER_DECISION: "SET_OPTIONAL_TERMS_USER_DECISION",
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",
GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA",
GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN",
@@ -209,12 +212,9 @@ export const types = {
*
* SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다.
* CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다.
* PAUSE_PLAYER_CONTROL: 현재 제어권을 가진 비디오를 '일시정지' 상태로 변경합니다.
*/
SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL",
CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL",
PAUSE_PLAYER_CONTROL: "PAUSE_PLAYER_CONTROL",
RESUME_PLAYER_CONTROL: "RESUME_PLAYER_CONTROL",
// reset action
RESET_REDUX_STATE: "RESET_REDUX_STATE",
@@ -242,4 +242,7 @@ export const types = {
// device
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(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 { types } from "./actionTypes";
import { changeAppStatus, getTermsAgreeYn } from "./commonActions";
import { collectBannerPositions } from "../utils/domUtils";
// 약관 정보 조회 IF-LGSP-005
export const getHomeTerms = (props) => (dispatch, getState) => {
@@ -521,3 +522,57 @@ export const setBannerIndex = (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:
return state;
}

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import {
setOptionalTermsUserDecision,
setOptionalTermsPopupShown,
resetOptionalTermsSession,
updateOptionalTermsAgreement,
// setTermsAgreeYn,
} from "../../actions/commonActions";
import { registerDevice } from "../../actions/deviceActions";
@@ -38,11 +39,11 @@ import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare";
import TPanel from "../../components/TPanel/TPanel";
import TPopUp from "../../components/TPopUp/TPopUp";
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 useSafeFocusState from "../../hooks/useSafeFocusState";
// import useSafeFocusState from "../../hooks/useSafeFocusState"; // 2단계: 제거
import useTermsStateMachine from "../../hooks/useTermsStateMachine";
import useRightPanelContent from "../../hooks/useRightPanelContent";
// import useRightPanelContent from "../../hooks/useRightPanelContent"; // 1단계: 제거
import * as Config from "../../utils/Config";
import { panel_names } from "../../utils/Config";
import { $L } from "../../utils/helperMethods";
@@ -108,9 +109,11 @@ function IntroPanelWithOptional({
const processingTimeoutRef = useRef(null);
// [추가] 재시도 인터벌 참조
const retryIntervalRef = useRef(null);
// 필수약관 팝업 타이머 참조
const popupTimeoutRef = useRef(null);
// const [isRequiredFocused, setIsRequiredFocused] = useState(false);
const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState();
// const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState(); // 2단계: 제거
const { state: termsState, updateStateAsync } = useTermsStateMachine();
const {
termsChecked,
@@ -152,33 +155,83 @@ function IntroPanelWithOptional({
// WebOS 버전별 UI 표시 모드 결정
// 이미지 표시: 4.0, 5.0, 23, 24
// 텍스트 표시: 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(() => {
if (!webOSVersion) return false;
const versionNum = Number.parseFloat(String(webOSVersion));
// 텍스트 표시 버전들 (숫자로 비교)
const textVersions = [4.5, 6.0, 22];
// 이미지 표시 버전들
const imageVersions = [4.0, 5.0, 23, 24];
// 텍스트 버전인지 확인
const shouldShowText = textVersions.includes(versionNum);
// if (!webOSVersion) return false;
// webOSVersion 이 없거나 빈 문자열인 경우 이미지 보기 처리
if (!webOSVersion || webOSVersion === "") {
return false;
}
const versionNum = Number.parseFloat(String(webOSVersion)); // 기존 로직 유지
const versionStr = String(webOSVersion);
// 버전 문자열을 배열로 변환해서 비교
const parseVersionArray = (version) => {
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") {
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(" - 텍스트 버전들:", textVersions);
console.log(" - 이미지 버전들:", imageVersions);
}
return shouldShowText;
}, [webOSVersion]);
// 상태 관리
const [currentTerms, setCurrentTerms] = useState(null);
// 필수약관 체크 해제시 팝업표시용
const [requiredAgreePopup, setRequiredAgreePopup] = useState(false);
const [prevTermsChecked, setPrevTermsChecked] = useState(false);
const [prevPrivacyChecked, setPrevPrivacyChecked] = useState(false);
useEffect(() => {
dispatch({ type: types.REGISTER_DEVICE_RESET });
@@ -221,6 +274,10 @@ function IntroPanelWithOptional({
return () => {
clearTimeout(focusTimer);
// 팝업 타이머도 정리
if (popupTimeoutRef.current) {
clearTimeout(popupTimeoutRef.current);
}
};
}, []);
@@ -237,6 +294,23 @@ function IntroPanelWithOptional({
}
}, [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(() => {
console.log("[IntroPanel] deviceRegistered", deviceRegistered);
if (deviceRegistered) {
@@ -361,20 +435,19 @@ function IntroPanelWithOptional({
if (newRegDeviceData && newRegDeviceData.retCode === 0) {
dispatch(setDeviceRegistered(true));
// 선택약관 상태를 Redux에 업데이트 (TV 환경 최적화)
// 선택약관 상태를 통합 액션으로 업데이트 (TV 환경 최적화)
if (optionalChecked) {
// 선택약관에 동의한 경우
console.log("[IntroPanel] 선택약관 동의됨 - Redux 상태 업데이트");
dispatch(setOptionalTermsUserDecision('agreed'));
dispatch(setOptionalTermsPopupShown(true));
console.log("[IntroPanel] 선택약관 동의됨 - 통합 Redux 상태 업데이트");
dispatch(updateOptionalTermsAgreement(true));
} else {
// 선택약관에 동의하지 않은 경우 - HomeBanner에서 팝업이 나올 수 있도록 상태를 초기화
console.log("[IntroPanel] 선택약관 미동의 - HomeBanner 팝업 허용을 위해 상태 초기화");
// fetchCurrentUserHomeTerms 완료 후 Redux 상태를 리셋하여 HomeBanner에서 팝업 표시 조건을 정확히 평가할 수 있도록 함
dispatch(updateOptionalTermsAgreement(false));
setTimeout(() => {
console.log("[IntroPanel] 약관 데이터 갱신 후 상태 리셋 실행");
console.log("[IntroPanel] 선택약관 세션 리셋 실행");
dispatch(resetOptionalTermsSession());
}, 1000); // 약관 데이터 갱신 완료를 기다림
}, 1000);
}
dispatch(
@@ -568,16 +641,16 @@ function IntroPanelWithOptional({
}
}, [dispatch, activePopup]);
const handleFocus = useCallback(
(item) => {
setFocusAsync(item);
},
[setFocusAsync],
);
// const handleFocus = useCallback(
// (item) => {
// setFocusAsync(item);
// },
// [setFocusAsync],
// ); // 2단계: 제거
const handleBlur = useCallback(() => {
clearFocusAsync(0);
}, [clearFocusAsync]);
// const handleBlur = useCallback(() => {
// clearFocusAsync(0);
// }, [clearFocusAsync]); // 2단계: 제거
// 체크박스 핸들러들
const handleTermsToggle = useCallback(
@@ -669,49 +742,64 @@ function IntroPanelWithOptional({
[handleOptionalTermsClick],
);
const handleFocusTermsCheckbox = useCallback(
() => handleFocus("termsCheckbox"),
[handleFocus],
);
const handleFocusTermsButton = useCallback(
() => handleFocus("termsButton"),
[handleFocus],
);
const handleFocusPrivacyCheckbox = useCallback(
() => handleFocus("privacyCheckbox"),
[handleFocus],
);
const handleFocusPrivacyButton = useCallback(
() => handleFocus("privacyButton"),
[handleFocus],
);
const handleFocusOptionalCheckbox = useCallback(
() => handleFocus("optionalCheckbox"),
[handleFocus],
);
const handleFocusOptionalButton = useCallback(
() => handleFocus("optionalButton"),
[handleFocus],
);
const handleFocusSelectAllCheckbox = useCallback(
() => handleFocus("selectAllCheckbox"),
[handleFocus],
);
const handleFocusAgreeButton = useCallback(
() => handleFocus("agreeButton"),
[handleFocus],
);
const handleFocusDisagreeButton = useCallback(
() => handleFocus("disagreeButton"),
[handleFocus],
);
// const handleFocusTermsCheckbox = useCallback(
// () => handleFocus("termsCheckbox"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusTermsButton = useCallback(
// () => handleFocus("termsButton"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusPrivacyCheckbox = useCallback(
// () => handleFocus("privacyCheckbox"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusPrivacyButton = useCallback(
// () => handleFocus("privacyButton"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusOptionalCheckbox = useCallback(
// () => handleFocus("optionalCheckbox"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusOptionalButton = useCallback(
// () => handleFocus("optionalButton"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusSelectAllCheckbox = useCallback(
// () => handleFocus("selectAllCheckbox"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusAgreeButton = useCallback(
// () => handleFocus("agreeButton"),
// [handleFocus],
// ); // 2단계: 제거
// const handleFocusDisagreeButton = useCallback(
// () => handleFocus("disagreeButton"),
// [handleFocus],
// ); // 2단계: 제거
const rightPanelContent = useRightPanelContent(
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView,
);
// const rightPanelContent = useRightPanelContent(
// focusedItem,
// termsChecked,
// privacyChecked,
// 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(() => {
Spotlight.focus();
@@ -776,24 +864,20 @@ function IntroPanelWithOptional({
<div className={css.termsLeftPanel}>
{/* Terms & Conditions */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={termsChecked}
onToggle={handleTermsToggle}
onFocus={handleFocusTermsCheckbox}
onBlur={handleBlur}
spotlightId="termsCheckbox"
ariaLabel={$L("Terms & Conditions checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleTermsClickMST00402}
onFocus={handleFocusTermsButton}
onBlur={handleBlur}
spotlightId="termsButton"
type={TYPES.terms}
ariaLabel={$L("View Terms & Conditions")}
>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={termsChecked}
onToggle={handleTermsToggle}
spotlightId="termsCheckbox"
ariaLabel={$L("Terms & Conditions checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleTermsClickMST00402}
spotlightId="termsButton"
type={TYPES.terms}
ariaLabel={$L("View Terms & Conditions")}
>
<span className={`${css.termsText} ${css.required}`}>
{$L("Terms & Conditions")}
</span>
@@ -802,24 +886,20 @@ function IntroPanelWithOptional({
{/* Privacy Policy */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={privacyChecked}
onToggle={handlePrivacyToggle}
onFocus={handleFocusPrivacyCheckbox}
onBlur={handleBlur}
spotlightId="privacyCheckbox"
ariaLabel={$L("Privacy Policy checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleTermsClickMST00401}
onFocus={handleFocusPrivacyButton}
onBlur={handleBlur}
spotlightId="privacyButton"
type={TYPES.terms}
ariaLabel={$L("View Privacy Policy")}
>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={privacyChecked}
onToggle={handlePrivacyToggle}
spotlightId="privacyCheckbox"
ariaLabel={$L("Privacy Policy checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleTermsClickMST00401}
spotlightId="privacyButton"
type={TYPES.terms}
ariaLabel={$L("View Privacy Policy")}
>
<span className={`${css.termsText} ${css.required}`}>
{$L("Privacy Policy")}
</span>
@@ -828,25 +908,21 @@ function IntroPanelWithOptional({
{/* Optional Terms */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={optionalChecked}
onToggle={handleOptionalToggle}
onFocus={handleFocusOptionalCheckbox}
onBlur={handleBlur}
spotlightId="optionalCheckbox"
ariaLabel={$L("Optional Terms checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleOptionalTermsClickMST00405}
onFocus={handleFocusOptionalButton}
onBlur={handleBlur}
spotlightId="optionalButton"
type={TYPES.terms}
ariaLabel={$L("View Optional Terms")}
style={{ marginBottom: 0 }} // 제일 아래 버튼의 경우 margin-bottom 0
>
<TCheckBoxSquare
className={css.customeCheckbox}
selected={optionalChecked}
onToggle={handleOptionalToggle}
spotlightId="optionalCheckbox"
ariaLabel={$L("Optional Terms checkbox")}
/>
<TButton
className={css.termsButton}
onClick={handleOptionalTermsClickMST00405}
spotlightId="optionalButton"
type={TYPES.terms}
ariaLabel={$L("View Optional Terms")}
style={{ marginBottom: 0 }} // 제일 아래 버튼의 경우 margin-bottom 0
>
<span className={css.termsText}>{$L("Optional Terms")}</span>
</TButton>
</div>
@@ -860,8 +936,6 @@ function IntroPanelWithOptional({
className={css.selectAllCheckbox}
selected={selectAllChecked}
onToggle={handleSelectAllToggle}
onFocus={handleFocusSelectAllCheckbox}
onBlur={handleBlur}
spotlightId="selectAllCheckbox"
ariaLabel={$L("Select All checkbox")}
/>
@@ -873,8 +947,6 @@ function IntroPanelWithOptional({
<TButton
className={css.agreeButton}
onClick={handleAgree}
onFocus={handleFocusAgreeButton}
onBlur={handleBlur}
spotlightId="agreeButton"
type={TYPES.agree}
ariaLabel={$L("Agree to terms")}
@@ -886,8 +958,6 @@ function IntroPanelWithOptional({
<TButton
className={css.disagreeButton}
onClick={handleDisagree}
onFocus={handleFocusDisagreeButton}
onBlur={handleBlur}
spotlightId="disagreeButton"
type={TYPES.agree}
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>
);
}

View File

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