From 7e67e05aac0b15280553cb7f5d1bde035ebbc1a7 Mon Sep 17 00:00:00 2001 From: djaco Date: Thu, 19 Jun 2025 08:59:10 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=84=A0=ED=83=9D=EC=95=BD=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95=20250619?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- com.twin.app.shoptime/src/App/App.js | 96 ++++--- .../src/actions/actionTypes.js | 7 + .../src/actions/commonActions.js | 136 +++++++--- .../src/actions/deviceActions.js | 2 + .../src/actions/homeActions.js | 48 ++++ .../components/Optional/OptionalConfirm.jsx | 65 +++++ .../Optional/OptionalConfirm.module.less | 12 + .../Optional/OptionalTermsConfirm.jsx | 9 +- .../Optional/OptionalTermsConfirm.module.less | 7 +- .../Optional/OptionalTermsConfirmBottom.jsx | 185 ++++++++++++++ .../OptionalTermsConfirmBottom.module.less | 13 + .../TCheckBox/TCheckBoxSquare.module.less | 9 +- .../src/components/TPopUp/TNewPopUp.jsx | 223 ++++++++++++++-- .../components/TPopUp/TNewPopUp.module.less | 186 +++++++++++++- .../src/components/TPopUp/TPopUp.jsx | 182 +++++++++++-- .../src/components/TPopUp/TPopUp.module.less | 73 +++++- .../src/reducers/commonReducer.js | 46 +++- com.twin.app.shoptime/src/utils/Config.js | 4 +- .../src/utils/focus-monitor.js | 205 +++++++++++++++ .../src/utils/spotlight-utils.js | 240 ++++++++++++++++++ .../FeaturedBrandsPanel.jsx | 1 + .../views/HomePanel/HomeBanner/HomeBanner.jsx | 183 ++++++++++++- .../src/views/IntroPanel/IntroPanel.new.jsx | 180 +++++++++---- .../IntroPanel/IntroPanel.new.module.less | 147 ++++++++--- .../src/views/MainView/MainView.jsx | 19 +- .../TermsOfService/TermsOfService.jsx | 14 +- .../TermsOfService/TermsOfService.module.less | 2 +- 27 files changed, 2065 insertions(+), 229 deletions(-) create mode 100644 com.twin.app.shoptime/src/components/Optional/OptionalConfirm.jsx create mode 100644 com.twin.app.shoptime/src/components/Optional/OptionalConfirm.module.less create mode 100644 com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirmBottom.jsx create mode 100644 com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirmBottom.module.less create mode 100644 com.twin.app.shoptime/src/utils/focus-monitor.js create mode 100644 com.twin.app.shoptime/src/utils/spotlight-utils.js diff --git a/com.twin.app.shoptime/src/App/App.js b/com.twin.app.shoptime/src/App/App.js index 2211ad05..0399bdaa 100644 --- a/com.twin.app.shoptime/src/App/App.js +++ b/com.twin.app.shoptime/src/App/App.js @@ -44,6 +44,7 @@ import css from "./App.module.less"; import { handleBypassLink } from "./bypassLinkHandler"; import { handleDeepLink } from "./deepLinkHandler"; import { sendLogTotalRecommend } from "../actions/logActions"; +import { startFocusMonitoring, stopFocusMonitoring } from '../utils/focus-monitor'; let foreGroundChangeTimer = null; @@ -121,22 +122,21 @@ function AppBase(props) { (state) => state.common.appStatus.cursorVisible ); const introTermsAgree = useSelector((state) => state.common.introTermsAgree); - const optionalTermsAgree = useSelector( - (state) => state.common.optionalTermsAgree - ); + // const optionalTermsAgree = useSelector((state) => state.common.optionalTermsAgree); + const termsLoading = useSelector((state) => state.common.termsLoading); // termsFlag 전체 상태 확인 - const termsFlag = useSelector((state) => state.common.termsFlag); + // const termsFlag = useSelector((state) => state.common.termsFlag); const termsData = useSelector((state) => state.home.termsData); - const shouldShowOptionalTermsPopup = useMemo(() => { - const terms = termsData?.data?.terms; - if (!terms) { - return false; - } - const optionalTerm = terms.find((term) => term.trmsTpCd === "MST00405"); - return optionalTerm ? optionalTerm.trmsPopFlag === "Y" : false; - }, [termsData]); - + // const shouldShowOptionalTermsPopup = useMemo(() => { + // const terms = termsData?.data?.terms; + // if (!terms) { + // return false; + // } + // const optionalTerm = terms.find(term => term.trmsTpCd === "MST00405"); + // return optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false; + // }, [termsData]); + useEffect(() => { if (termsData?.data?.terms) { dispatch(getTermsAgreeYn()); @@ -179,6 +179,23 @@ function AppBase(props) { }, 5000) ); + // 컴포넌트에서 모니터링 시작 - 한시적 모니터링 + // useEffect(() => { + // startFocusMonitoring(); + // return () => stopFocusMonitoring(); + // }, []); + + // 임시 작업용 코드 + // useEffect(() => { + // const timer = setTimeout(() => { + // dispatch( + // pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} }) + // ); + // }, 1500); + + // return () => clearTimeout(timer); + // }, [dispatch]); + // called by [receive httpHeader, launch, relaunch] const initService = useCallback( (haveyInit = true) => { @@ -370,29 +387,38 @@ function AppBase(props) { } }, [webOSVersion, deviceId]); + + // 테스트용 팝업 표시 // useEffect(() => { // setTimeout(() => { // console.log("App.js optionalTermsTest 팝업 표시"); - // dispatch(setShowPopup({ activePopup: "optionalTermsTest" })); + // dispatch(setShowPopup({ activePopup: "optionalTermsConfirmBottom" })); // }, 3000); // }, [dispatch]); // 약관 동의 및 선택 약관 팝업 처리 useEffect(() => { - if (introTermsAgree === undefined) { + if (introTermsAgree === undefined || termsLoading) { // 약관 동의 여부 확인 전에는 아무것도 하지 않음 return; } - + if (introTermsAgree) { // 필수 약관에 동의한 경우 - if (shouldShowOptionalTermsPopup) { + // if (shouldShowOptionalTermsPopup) { // 선택 약관 팝업을 띄워야 하는 경우 - dispatch(setShowPopup({ activePopup: "optionalTermsTest" })); - } else { + // 3초 후에 팝업을 띄우도록 설정 + // console.log("App.js optionalTermsTest 팝업 표시"); + // const timer = setTimeout(() => { + // dispatch(setShowPopup({ activePopup: "optionalTermsConfirm" })); + // }, 3000); // 3000 milliseconds = 3 seconds + + // 컴포넌트 언마운트 시 타이머 클리어 + // return () => clearTimeout(timer); + // } else { // 선택 약관 팝업이 필요 없는 경우, 바로 서비스 초기화 initService(true); - } + // } } else { // 필수 약관에 동의하지 않은 경우 dispatch( @@ -400,7 +426,7 @@ function AppBase(props) { ); dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); } - }, [introTermsAgree, shouldShowOptionalTermsPopup, dispatch, initService]); + }, [introTermsAgree, dispatch, initService]); useEffect(() => { const launchParmas = getLaunchParams(); @@ -432,21 +458,19 @@ function AppBase(props) { return ( - <> - {webOSVersion === "" ? null : Number(webOSVersion) < 4 ? ( - - ) : ( - - )} - + {webOSVersion === "" ? null : Number(webOSVersion) < 4 ? ( + + ) : ( + + )} ); } diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 1080e6a1..b91169c1 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -1,3 +1,5 @@ +// src/actions/actionTypes.js + export const types = { // panel actions PUSH_PANEL: "PUSH_PANEL", @@ -209,4 +211,9 @@ export const types = { // new actions CANCEL_FOCUS_ELEMENT: "CANCEL_FOCUS_ELEMENT", + + // 약관동의 여부 확인 상태 + GET_TERMS_AGREE_YN_START: "GET_TERMS_AGREE_YN_START", + GET_TERMS_AGREE_YN_SUCCESS: "GET_TERMS_AGREE_YN_SUCCESS", + GET_TERMS_AGREE_YN_FAILURE: "GET_TERMS_AGREE_YN_FAILURE", }; diff --git a/com.twin.app.shoptime/src/actions/commonActions.js b/com.twin.app.shoptime/src/actions/commonActions.js index cb6ea7b9..61653cf2 100644 --- a/com.twin.app.shoptime/src/actions/commonActions.js +++ b/com.twin.app.shoptime/src/actions/commonActions.js @@ -1,3 +1,5 @@ +// src/actions/commonActions.js + import { Job } from "@enact/core/util"; import Spotlight from "@enact/spotlight"; @@ -286,50 +288,110 @@ export const getDeviceId = (onComplete) => (dispatch, getState) => { }; export const getTermsAgreeYn = () => (dispatch, getState) => { - const { terms } = getState().home.termsData.data; + dispatch({ type: types.GET_TERMS_AGREE_YN_START }); + + try { + const { terms } = getState().home.termsData.data; - // console.log("getTermsAgreeYn", terms); - console.log("getTermsAgreeYn", terms.map(term => ({ - trmsId: term.trmsId, - trmsTpCd: term.trmsTpCd, - trmsAgrFlag: term.trmsAgrFlag - }))); + console.log("getTermsAgreeYn", terms.map(term => ({ + trmsId: term.trmsId, + trmsTpCd: term.trmsTpCd, + trmsAgrFlag: term.trmsAgrFlag, + trmsPopFlag: term.trmsPopFlag, + }))); - const termsAgreeFlag = terms.reduce((acc, term) => { - switch (term.trmsTpCd) { - case "MST00401": - acc.privacyTerms = term.trmsAgrFlag; - break; - - case "MST00402": - acc.serviceTerms = term.trmsAgrFlag; - break; - - case "MST00403": - acc.purchaseTerms = term.trmsAgrFlag; - break; - - case "MST00404": - acc.paymentTerms = term.trmsAgrFlag; - break; - - case "MST00405": - acc.optionalTerms = term.trmsAgrFlag; - break; - - default: - break; + // MST00405 선택약관 정보만 따로 출력 + const optionalTerm = terms.find(term => term.trmsTpCd === 'MST00405'); + if (optionalTerm) { + console.log("getTermsAgreeYn MST00405 선택약관:", { + trmsId: optionalTerm.trmsId, + trmsTpCd: optionalTerm.trmsTpCd, + trmsAgrFlag: optionalTerm.trmsAgrFlag, + trmsPopFlag: optionalTerm.trmsPopFlag + }); + } else { + console.log("getTermsAgreeYn MST00405 선택약관을 찾을 수 없습니다."); } - return acc; - }, {}); + const termsAgreeFlag = terms.reduce((acc, term) => { + switch (term.trmsTpCd) { + case "MST00401": + acc.privacyTerms = term.trmsAgrFlag; + break; + case "MST00402": + acc.serviceTerms = term.trmsAgrFlag; + break; + case "MST00403": + acc.purchaseTerms = term.trmsAgrFlag; + break; + case "MST00404": + acc.paymentTerms = term.trmsAgrFlag; + break; + case "MST00405": + acc.optionalTerms = term.trmsAgrFlag; + break; + default: + break; + } + return acc; + }, {}); - dispatch({ - type: types.GET_TERMS_AGREE_YN, - payload: termsAgreeFlag, - }); + dispatch({ + type: types.GET_TERMS_AGREE_YN_SUCCESS, + payload: termsAgreeFlag, + }); + } catch (error) { + console.error("getTermsAgreeYn error:", error); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + } }; +// export const getTermsAgreeYn = () => (dispatch, getState) => { +// const { terms } = getState().home.termsData.data; + +// // console.log("getTermsAgreeYn", terms); +// console.log("getTermsAgreeYn", terms.map(term => ({ +// trmsId: term.trmsId, +// trmsTpCd: term.trmsTpCd, +// trmsAgrFlag: term.trmsAgrFlag, +// trmsPopFlag: term.trmsPopFlag, +// }))); + +// const termsAgreeFlag = terms.reduce((acc, term) => { +// switch (term.trmsTpCd) { +// case "MST00401": +// acc.privacyTerms = term.trmsAgrFlag; +// break; + +// case "MST00402": +// acc.serviceTerms = term.trmsAgrFlag; +// break; + +// case "MST00403": +// acc.purchaseTerms = term.trmsAgrFlag; +// break; + +// case "MST00404": +// acc.paymentTerms = term.trmsAgrFlag; +// break; + +// case "MST00405": +// acc.optionalTerms = term.trmsAgrFlag; +// break; + +// default: +// break; +// } + +// return acc; +// }, {}); + +// dispatch({ +// type: types.GET_TERMS_AGREE_YN, +// payload: termsAgreeFlag, +// }); +// }; + export const launchMembershipApp = () => (dispatch, getState) => { const state = getState(); const panels = state.panels.panels; diff --git a/com.twin.app.shoptime/src/actions/deviceActions.js b/com.twin.app.shoptime/src/actions/deviceActions.js index 17478af2..f48fa5f9 100644 --- a/com.twin.app.shoptime/src/actions/deviceActions.js +++ b/com.twin.app.shoptime/src/actions/deviceActions.js @@ -3,6 +3,7 @@ import { runDelayedAction, setTokenRefreshing, TAxios } from "../api/TAxios"; import * as lunaSend from "../lunaSend"; import { types } from "./actionTypes"; import { changeLocalSettings } from "./commonActions"; +import { fetchCurrentUserHomeTerms } from "./homeActions"; // IF-LGSP-000 인증코드 요청 export const getAuthenticationCode = () => (dispatch, getState) => { @@ -48,6 +49,7 @@ export const registerDevice = (params) => (dispatch, getState) => { retCode: response.data.retCode, }); dispatch(getAuthenticationCode()); + dispatch(fetchCurrentUserHomeTerms()); }; const onFail = (error) => { diff --git a/com.twin.app.shoptime/src/actions/homeActions.js b/com.twin.app.shoptime/src/actions/homeActions.js index 31d7f17d..8d61cb03 100644 --- a/com.twin.app.shoptime/src/actions/homeActions.js +++ b/com.twin.app.shoptime/src/actions/homeActions.js @@ -37,6 +37,54 @@ export const getHomeTerms = (props) => (dispatch, getState) => { ); }; +// 현재 로그인 사용자 기준으로 약관 정보 조회 (인자 없이 호출 가능) +export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => { + const loginUserData = getState().common.appStatus.loginUserData; + + if (!loginUserData || !loginUserData.userNumber) { + console.error("fetchCurrentUserHomeTerms: userNumber (mbrNo) is not available. User might not be logged in."); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + return; + } + + const mbrNo = loginUserData.userNumber; + const trmsTpCdList = "MST00401, MST00402, MST00405"; // 기본 약관 코드 리스트 + + const onSuccess = (response) => { + console.log("fetchCurrentUserHomeTerms onSuccess ", response.data); + + if (response.data.retCode === 0) { + dispatch({ + type: types.GET_HOME_TERMS, // 기존 GET_HOME_TERMS 타입을 재사용 + payload: response.data, + }); + // getHomeTerms와 동일하게 getTermsAgreeYn 후속 처리 + setTimeout(() => { + dispatch(getTermsAgreeYn()); + }, 0); + } else { + // retCode가 0이 아닌 경우 실패로 처리 + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + } + }; + + const onFail = (error) => { + console.error("fetchCurrentUserHomeTerms onFail ", error); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + }; + + TAxios( + dispatch, + getState, + "get", + URLS.GET_HOME_TERMS, // 동일한 API 엔드포인트 사용 + { trmsTpCdList, mbrNo }, + {}, + onSuccess, + onFail + ); + }; + // 메뉴 목록 조회 IF-LGSP-044 export const getHomeMenu = () => (dispatch, getState) => { const onSuccess = (response) => { diff --git a/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.jsx b/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.jsx new file mode 100644 index 00000000..8e6d6cc8 --- /dev/null +++ b/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import TNewPopUp from '../TPopUp/TNewPopUp'; // TNewPopUp 컴포넌트의 정확한 경로를 확인해주세요. +import css from './OptionalConfirm.module.less'; + +/** + * 선택 약관 동의 팝업 컴포넌트입니다. + * 이 컴포넌트는 약관 동의 UI를 표시하며, 내부에서 다른 팝업을 관리하지 않습니다. + * 버튼 클릭 이벤트는 props로 전달받아 상위 컴포넌트에서 처리합니다. + */ +const OptionalConfirm = ({ + open, + spotlightId, + className, + onOptionalTermsClick, // 약관 자세히 보기 버튼 클릭 핸들러 + onOptionalAgreeClick, // 동의 버튼 클릭 핸들러 + onOptionalDeclineClick, // 거절 또는 다음에 하기 버튼 클릭 핸들러 + customPosition, + position, +}) => { + return ( + + ); +}; + +OptionalConfirm.propTypes = { + /** 팝업의 표시 여부 */ + open: PropTypes.bool.isRequired, + /** Spotlight ID */ + spotlightId: PropTypes.string.isRequired, + /** 추가적인 CSS 클래스 */ + className: PropTypes.string, + /** 약관 자세히 보기 버튼 클릭 시 호출될 함수 */ + onOptionalTermsClick: PropTypes.func, + /** 동의 버튼 클릭 시 호출될 함수 */ + onOptionalAgreeClick: PropTypes.func, + /** 거절 또는 다음에 하기 버튼 클릭 시 호출될 함수 */ + onOptionalDeclineClick: PropTypes.func, + /** 사용자 정의 위치 사용 여부 */ + customPosition: PropTypes.bool, + /** 사용자 정의 위치 값 (customPosition이 true일 때 사용) */ + position: PropTypes.object, +}; + +OptionalConfirm.defaultProps = { + className: '', + onOptionalTermsClick: () => console.log('OptionalConfirm: onOptionalTermsClick not provided'), + onOptionalAgreeClick: () => console.log('OptionalConfirm: onOptionalAgreeClick not provided'), + onOptionalDeclineClick: () => console.log('OptionalConfirm: onOptionalDeclineClick not provided'), + customPosition: false, +// position: {}, +}; + +export default OptionalConfirm; diff --git a/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.module.less b/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.module.less new file mode 100644 index 00000000..b8212139 --- /dev/null +++ b/com.twin.app.shoptime/src/components/Optional/OptionalConfirm.module.less @@ -0,0 +1,12 @@ +.optionalConfirmPopup { + width: 1920px; + height: auto; + background-color: white !important; + border-radius: 12px; + box-shadow: 0 20px 70px rgba(2, 3, 3, 0.7) !important; + position: absolute !important; + // top: 882px !important; + left: 0 !important; + } + + \ No newline at end of file diff --git a/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirm.jsx b/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirm.jsx index f105c1dc..724b061a 100644 --- a/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirm.jsx +++ b/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirm.jsx @@ -17,10 +17,7 @@ const OptionalTermsConfirm = ({ open }) => { const [isChecked, setIsChecked] = useState(false); const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false); const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false); - - useEffect(() => { - console.log("OptionalTermsTest - in Component Rendered"); - }, []); + const optionalTermsData = useSelector((state) => state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") @@ -132,10 +129,10 @@ const OptionalTermsConfirm = ({ open }) => { { + const dispatch = useDispatch(); + + const [isChecked, setIsChecked] = useState(false); + const [isTermsPopupVisible, setIsTermsPopupVisible] = useState(false); + const [isWarningPopupVisible, setIsWarningPopupVisible] = useState(false); + + + const optionalTermsData = useSelector((state) => + state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") + ); + + + // 포커스 복원을 위한 useEffect 추가 + useEffect(() => { + if (open && !isTermsPopupVisible && !isWarningPopupVisible) { + const timer = setTimeout(() => { + Spotlight.focus("optional-terms-confirm-popup"); + }, 150); // 약간 더 긴 지연 + + return () => clearTimeout(timer); + } + }, [open, isTermsPopupVisible, isWarningPopupVisible]); + + const handleMainPopupClose = useCallback(() => { + dispatch(setHidePopup()); + }, [dispatch]); + + const handleCheckboxToggle = useCallback(({ selected }) => { + setIsChecked(selected); + }, []); + + const handleViewTermsClick = useCallback(() => { + setIsTermsPopupVisible(true); + }, []); + + const handleCloseTermsPopup = useCallback((e) => { + if (e) { + e.stopPropagation(); + } + setIsTermsPopupVisible(false); + Spotlight.focus("optional-terms-confirm-popup"); + }, []); + + const handleTermsPopupClosed = useCallback(() => { + setTimeout(() => { + Spotlight.focus("optional-terms-confirm-popup"); + }, 50); + }, []); + + const handleAgreeTest = useCallback(() => { + console.log("handleAgreeTest"); + Spotlight.pause(); + setIsTermsPopupVisible(false); + // 상태 업데이트 후 DOM이 완전히 렌더링될 때까지 기다린 후 포커스 + setTimeout(() => { + Spotlight.resume(); + Spotlight.focus("optional-terms-confirm-popup"); + }, 500); // 50ms에서 100ms로 증가 + + }, []); + + const handleAgree = useCallback(() => { + if (isChecked) { + // 약관 동의할 항목들 (string array) + const termsList = ["TID0000222", "TID0000223", "TID0000232"]; + + // 동의하지 않을 항목들 (빈 배열) + const notTermsList = []; + + console.log('OptionalTermsConfirm -약관 동의 API 호출 파라미터:', { termsList, notTermsList }); + + const callback = (response) => { + if (response.retCode === "000" || response.retCode === 0) { + console.log('약관 동의 성공:', response); + } else { + console.error('약관 동의 실패:', response); + } + }; + + console.log('OptionalTermsConfirm - 약관 동의 API 호출 payload:', { termsList, notTermsList }); + dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback)); + dispatch(setHidePopup()); + } else { + setIsWarningPopupVisible(true); + } + }, [isChecked, dispatch]); + + const handleCloseWarningPopup = useCallback(() => { + setIsWarningPopupVisible(false); + Spotlight.focus("optional-terms-confirm-popup"); + }, []); + + const handleDontAskAgain = () => { + console.log("Don't Ask Again 처리 필요"); + dispatch(setHidePopup()); + }; + + if (isTermsPopupVisible) { + return ( + + {optionalTermsData && ( +
+
{$L("Optional Terms")}
+ +
+ +
+ )} + + ); + } + + if (isWarningPopupVisible) { + return ( + + ); + } + + return ( + + ); +}; + +export default OptionalTermsConfirm; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirmBottom.module.less b/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirmBottom.module.less new file mode 100644 index 00000000..b6a53e4f --- /dev/null +++ b/com.twin.app.shoptime/src/components/Optional/OptionalTermsConfirmBottom.module.less @@ -0,0 +1,13 @@ +// src/components/Optional/OptionalTermsConfirm.module.less + +.optionalConfirmPopup { + width: auto; + height: auto; + background-color: white !important; + border-radius: 12px; + box-shadow: 0 20px 70px rgba(2, 3, 3, 0.7) !important; + position: absolute !important; + // top: 882px !important; + left: 0 !important; +} + diff --git a/com.twin.app.shoptime/src/components/TCheckBox/TCheckBoxSquare.module.less b/com.twin.app.shoptime/src/components/TCheckBox/TCheckBoxSquare.module.less index 7382fb60..48e4b5c3 100644 --- a/com.twin.app.shoptime/src/components/TCheckBox/TCheckBoxSquare.module.less +++ b/com.twin.app.shoptime/src/components/TCheckBox/TCheckBoxSquare.module.less @@ -3,6 +3,8 @@ @SQUARE_BORDER_DEFAULT: #CCCCCC; @SQUARE_BORDER_ACTIVE: #C70850; @SQUARE_BG_SELECTED: #7A808D; +// @SQUARE_BG_SELECTED: #C70850; +; .tCheckBoxSquare { min-width: 45px !important; @@ -21,6 +23,7 @@ &:focus, &.focus { border-color: @SQUARE_BORDER_ACTIVE !important; + border-width: 4px !important; // 🔥 포커스 시 굵은 테두리 } &::before { @@ -39,6 +42,7 @@ &.selected { border-color: @SQUARE_BG_SELECTED !important; + border-width: 4px !important; background-color: @SQUARE_BG_SELECTED !important; &::before { transform: translate(-50%, -70%) rotate(-45deg) scale(1); @@ -46,9 +50,10 @@ } &.selectedFocus { - border-color: @SQUARE_BG_SELECTED !important; + border-color: @SQUARE_BORDER_ACTIVE !important; + border-width: 4px !important; background-color: @SQUARE_BG_SELECTED !important; - box-shadow: 0 0 0 4px fade(@SQUARE_BG_SELECTED, 20%) !important; + box-shadow: 0 0 0 4px fade(@SQUARE_BORDER_ACTIVE, 20%) !important; &::before { transform: translate(-50%, -70%) rotate(-45deg) scale(1); } diff --git a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx index 401de234..0174c50f 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx +++ b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo,useState } from "react"; import classNames from "classnames"; import { useDispatch, useSelector } from "react-redux"; @@ -31,6 +31,14 @@ const ButtonContainerNegative = SpotlightContainerDecorator( { defaultElement: `[data-spotlight-id="${"tPopupBtn2"}"]` }, "div" ); +const IntroTermsButtonContainer = SpotlightContainerDecorator( + { defaultElement: '[data-spotlight-id="introTermsAgreeBtn"]' }, + "div" +); +const OptionalConfirmButtonSection = SpotlightContainerDecorator( + { defaultElement: '[data-spotlight-id="optionalConfirmAgreeBtn"]' }, + "div" +); const SpottableComponent = Spottable("li"); const SpottableDiv = Spottable("div"); @@ -58,6 +66,7 @@ const KINDS = [ "trackPackagePopup", "optionalAgreement", "normal", + "optionalConfirm", ]; // 각 kind별 클래스명 매핑 @@ -207,6 +216,14 @@ const CLASS_MAPPINGS = { text: "optionalAgreementText", buttonContainer: "optionalAgreementButtonContainer", contentContainer: "optionalAgreementContentContainer" + }, + optionalConfirm: { + info: "optionalConfirmInfo", + textLayer: "optionalConfirmTextLayer", + title: "optionalConfirmTitle", + text: "optionalConfirmText", + buttonContainer: "optionalConfirmButtonContainer", + contentContainer: "optionalConfirmContentContainer" // 👈 추가 } }; @@ -250,6 +267,12 @@ export default function TNewPopUp({ selectedIndex, spotlightId, onSpotlightRight, + customPosition = false, + position = {}, + onOptionalTermsClick, // Optional Terms 버튼용 + onOptionalAgreeClick, // Agree 버튼용 + onOptionalDeclineClick, // Not Now 버튼용 + onIntroTermsAgreeClick, // introTerms Agree 버튼용 ...rest }) { const dispatch = useDispatch(); @@ -268,6 +291,7 @@ export default function TNewPopUp({ }; } const timerId = setTimeout(() => { + console.log("focusTarget = ", SpotlightIds.TPOPUP); Spotlight.focus(SpotlightIds.TPOPUP); }, 0); @@ -283,19 +307,74 @@ export default function TNewPopUp({ } }, [kind]); + useEffect(() => { + if (open) { + let focusTarget = SpotlightIds.TPOPUP; + if (kind === 'introTermsPopup') { + focusTarget = 'introTermsAgreeBtn'; + } else if (kind === 'optionalConfirm') { + focusTarget = 'optionalConfirmAgreeBtn'; + } else if (spotlightId) { + focusTarget = spotlightId; + } + + const timerId = setTimeout(() => { + console.log("focusTarget is ", focusTarget); + Spotlight.focus(focusTarget); + }, 0); + + return () => { + clearTimeout(timerId); + }; + } + }, [kind, open, spotlightId]); + + // useEffect(() => { + // if (open) { + // const focusTarget = spotlightId || SpotlightIds.TPOPUP; + // const timerId = setTimeout(() => { + // Spotlight.focus(focusTarget); + // }, 0); + + // return () => { + // clearTimeout(timerId); + // }; + // } + // }, [open, spotlightId]); + + // 커스텀 스타일 생성 + const customStyle = useMemo(() => { + if (!customPosition) return {}; + + return { + position: position.position || 'fixed', + top: position.top, + bottom: position.bottom, + left: position.left, + right: position.right, + transform: position.transform, + zIndex: position.zIndex || 99999, + ...position.style // 추가 스타일 + }; + }, [customPosition, position]); + const ButtonContainerComp = useMemo(() => { - return kind === "exitPopup" ? ButtonContainerNegative : ButtonContainer; + if (kind === "exitPopup") { + return ButtonContainerNegative; + } + if (kind === "introTermsPopup") { + return IntroTermsButtonContainer; + } + if (kind === "optionalConfirm") { + return OptionalConfirmButtonSection; + } + return ButtonContainer; }, [kind]); // optionalAgreement // 자동으로 Yes/No 버튼 텍스트 설정 - const finalButton1Text = useMemo(() => { - if (kind === "optionalAgreement" && !button1Text) { - return "Yes"; - } - return button1Text; - }, [kind, button1Text]); + const finalButton2Text = useMemo(() => { if (kind === "optionalAgreement" && !button2Text) { @@ -309,6 +388,18 @@ export default function TNewPopUp({ return hasButton || kind === "optionalAgreement"; }, [hasButton, kind]); + + const finalButton1Text = useMemo(() => { + if (kind === "optionalAgreement" && !button1Text) { + return "Yes"; + } + if (kind === "introTermsPopup" && !button1Text) { + return "Close"; // 기존 버튼을 Close로 변경 + } + return button1Text; + }, [kind, button1Text]); + + //------------------------------------------------------- const _onClick = useCallback( @@ -354,29 +445,67 @@ export default function TNewPopUp({ [optionClick] ); - const _onSpotlightRight = useCallback( - (e) => { - if (onSpotlightRight) onSpotlightRight(e); - }, - [onSpotlightRight] - ); + // 핸들러 함수들 추가 + const _onOptionalTermsClick = useCallback(() => { + if (onOptionalTermsClick) { + onOptionalTermsClick(); + } + }, [onOptionalTermsClick]); - if (!open) { - return null; + const _onOptionalAgreeClick = useCallback(() => { + if (onOptionalAgreeClick) { + onOptionalAgreeClick(); + } + }, [onOptionalAgreeClick]); + + const _onOptionalDeclineClick = useCallback(() => { + if (onOptionalDeclineClick) { + onOptionalDeclineClick(); + } + }, [onOptionalDeclineClick]); + + const _onIntroTermsAgreeClick = useCallback(() => { + if (onIntroTermsAgreeClick) { + onIntroTermsAgreeClick(); + } + }, [onIntroTermsAgreeClick]); + + const _onSpotlightRight = useCallback( + (e) => { + if (onSpotlightRight) onSpotlightRight(e); + }, + [onSpotlightRight] + ); + const alertStyle = useMemo(() => { + if (kind === 'optionalConfirm') { + return { + bottom: 'unset', + transform: 'none' + }; + } + return {}; + }, [kind]); + + if (!open) { + return null; } + + return ( {hasOnClose && ( )} + + {/* 다른 종류들의 children */} - {kind !== "optionalAgreement" && children} + {kind !== "optionalAgreement" && kind !== "optionalConfirm" && children} {hasIndicator && ( <> @@ -502,6 +633,17 @@ export default function TNewPopUp({ )} {shouldShowButtons && ( + {/* introTermsPopup일 때 Agree 버튼 먼저 표시 */} + {kind === "introTermsPopup" && ( + + Agree + + )} {finalButton1Text && ( )} + )} + + {kind === "optionalConfirm" && ( +
+
+ Get recommendations, special offers, and ads tailored just for you. +
+ +
+ +
Optional Terms
+
+
+
+ + Agree + + + Not Now + + +
+
+
)}
diff --git a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less index 78d9bdbe..d346ad87 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less +++ b/com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less @@ -1,6 +1,42 @@ +// src/components/TPopUp/TNewPopUp.module.less + @import "../../style/CommonStyle.module.less"; @import "../../style/utils.module.less"; +// 👇 TPopUp.module.less에서 가져온 floatLayer 스타일을 optionalConfirm용으로 수정 +// [id="floatLayer"] { +// // optionalConfirm일 때만 다른 위치 적용 +// > div:not([id]) > div > div:nth-child(2) { +// &:has(.src_components_TPopUp_TNewPopUp_optionalConfirm) { +// bottom: unset !important; +// transform: none !important; +// overflow: unset; + +// > div { +// overflow: unset; +// } +// } + +// // 다른 팝업들은 기존 TPopUp 방식 유지 +// &:not(:has(.src_components_TPopUp_TNewPopUp_optionalConfirm)) { +// bottom: 50%; +// transform: translateY(50%); +// overflow: unset; + +// > div { +// overflow: unset; +// } +// } +// } +// } + +// html body [id="floatLayer"] > div:not([id]) > div > div:nth-child(2) { +// &:has(.src_components_TPopUp_TNewPopUp_optionalConfirm) { +// bottom: unset !important; +// transform: none !important; +// } +// } + .tNewPopUp { //enact popup reset margin: 0 auto !important; @@ -73,6 +109,7 @@ margin-top: 30px; display: flex; justify-content: center; + gap: 12px; > div { min-width: 300px; height: 78px; @@ -809,6 +846,7 @@ .optionalAgreementInfo { .size(@w: 1064px, @h: 240px); + top: 80% !important; padding: 60px 57px 40px; // 상단 60px, 좌우 57px, 하단 40px 패딩 적용 box-sizing: border-box; // 패딩이 너비/높이에 포함되도록 설정 background-color: @BG_COLOR_01; @@ -859,4 +897,150 @@ } } } -} \ No newline at end of file + + &.optionalConfirm { + .default-style(); + + bottom: unset !important; + transform: none !important; + top: 20% !important; + + // 기존 위치 스타일들... + + .optionalConfirmInfo { + width: 100vw; + height: 198px; + background-color: #E6EBF0; + border-radius: 4px; + box-shadow: 0px 20px 12px rgba(0, 0, 0, 0.30); + display: flex; + flex-direction: column; + box-sizing: border-box; + gap: 15px; + + .optionalConfirmContentContainer { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 25px 140px 25px 140px; + box-sizing: border-box; + justify-content: center; + gap: 20px; + + .optionalConfirmTextSection { + // flex: 1; // 나머지 높이를 모두 차지 + height: 30px; + display: flex; + flex-direction: column; + min-height: 0; + // border : 1px solid red; + } + + .optionalConfirmButtonSection { + height: 60px; + // margin-top: 15px; // gap 대신 margin으로 간격 처리 + display: flex; + flex-direction: row; + justify-content: space-between; + flex-shrink: 0; // 줄어들지 않도록 고정 + // border : 1px solid blue; + + .optionalConfirmLeftButtonSection { + width: 320px; + height: 60px; // 부모 높이(60px) 모두 사용 + display: flex; + align-items: center; // 수직 중앙 정렬 + display: flex; + justify-content: space-between; + + .optionalTermsButton { + width: 100% !important; + height: 100% !important; + min-width: unset !important; + max-width: unset !important; + padding: 0 20px !important; + margin: 0 !important; + background: white !important; + border: 1px solid #CFCFCF !important; + border-radius: 4px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; // 👈 flex-start → space-between 변경 + flex-shrink: 0; + box-sizing: border-box !important; + + // 포커스 스타일 + &:focus { + outline: 2px solid #C70850 !important; + outline-offset: 1px !important; + } + + .optionalTermsTitle { + height: 100%; + color: #1A1A1A; + font-size: 22px; + font-family: 'LG Smart UI'; + font-weight: 600; + line-height: 22px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + } + + // 👈 '>' 아이콘 스타일 추가 + .optionalTermsIcon { + width: 24px; + height: 24px; + border-radius: 50%; + border: 2px solid #1A1A1A; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: 8px; + + &::after { + content: '>'; + font-size: 14px; + font-weight: bold; + color: #1A1A1A; + } + } + } + } + + .optionalConfirmRightButtonSection { + // width: 332px; + height: 100%; // 부모 높이(60px) 모두 사용 + display: flex; + align-items: center; // 수직 중앙 정렬 + justify-content: space-between; + gap: 12px; + + .optionalConfirmButton { + width: 160px; + height: 60px; + flex-shrink: 0; // 크기 고정 + } + } + } + } + } + } +} + +// optionalConfirm일 때만 기존 위치 스타일 무력화 + +// :global([id="floatLayer"]) :global(> div:not([id])) :global(> div) :global(> div:nth-child(2)) { +// .tNewPopUp.optionalConfirm & { +// bottom: unset !important; +// transform: none !important; +// top: unset !important; +// position: fixed !important; +// bottom: 0 !important; +// left: 50% !important; +// transform: translateX(-50%) !important; +// } +// } diff --git a/com.twin.app.shoptime/src/components/TPopUp/TPopUp.jsx b/com.twin.app.shoptime/src/components/TPopUp/TPopUp.jsx index ad821312..8e649902 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TPopUp.jsx +++ b/com.twin.app.shoptime/src/components/TPopUp/TPopUp.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo } from "react"; import classNames from "classnames"; import { useDispatch, useSelector } from "react-redux"; @@ -14,7 +14,9 @@ import { SpotlightIds } from "../../utils/SpotlightIds"; import CustomImage from "../CustomImage/CustomImage"; import TButton from "../TButton/TButton"; import css from "../TPopUp/TPopUp.module.less"; -import { $L } from "../../utils/helperMethods"; +import { $L, scaleH, scaleW } from "../../utils/helperMethods"; +import TButtonTab from "../TButtonTab/TButtonTab"; +import TButtonScroller from "../TButtonScroller/TButtonScroller"; const Container = SpotlightContainerDecorator( { enterTo: "default-element" }, @@ -31,6 +33,10 @@ const ButtonContainerNegative = SpotlightContainerDecorator( { defaultElement: `[data-spotlight-id="${"tPopupBtn2"}"]` }, "div" ); +const IntroTermsButtonContainer = SpotlightContainerDecorator( + { defaultElement: '[data-spotlight-id="tPopupBtn1"]' }, + "div" +); const SpottableComponent = Spottable("li"); const SpottableDiv = Spottable("div"); @@ -57,9 +63,16 @@ const KINDS = [ "cancelConfirmPopup", "trackPackagePopup", "optionalAgreement", + "wideLeftPopup", "normal", ]; +export const CONTENT_TYPES = { + TERMS: 'terms', + DEFAULT: 'default' +}; + + export default function TPopUp({ kind, children, @@ -90,32 +103,61 @@ export default function TPopUp({ selectedIndex, spotlightId, onSpotlightRight, + contentType = CONTENT_TYPES.DEFAULT, + termsData, + onTermsAgree, ...rest }) { const dispatch = useDispatch(); const popupVisible = useSelector((state) => state.common.popup.popupVisible); - const httpHeader = useSelector((state) => state.common.httpHeader); + const httpHeader = useSelector((state) => state.common.httpHeader); + // useEffect(() => { + // if (popupVisible) { + // if (spotlightId) { + // const timerId = setTimeout(() => { + // Spotlight.focus(spotlightId); + // }, 0); + + // return () => { + // clearTimeout(timerId); + // }; + // } + // const timerId = setTimeout(() => { + // Spotlight.focus(SpotlightIds.TPOPUP); + // }, 0); + + // return () => { + // clearTimeout(timerId); + // }; + // } + // }, [spotlightId, popupVisible]); + + useEffect(() => { if (popupVisible) { + let focusTarget; if (spotlightId) { - const timerId = setTimeout(() => { - Spotlight.focus(spotlightId); - }, 0); - - return () => { - clearTimeout(timerId); - }; + // console.log("focusTarget-1", spotlightId); + focusTarget = spotlightId; + } else if (kind === "introTermsPopup" || contentType === CONTENT_TYPES.TERMS) { + // console.log("focusTarget-2", "tPopupBtn1"); + focusTarget = "tPopupBtn1"; + } else { + // console.log("focusTarget-3 and kind and contentType", SpotlightIds.TPOPUP, kind, contentType); + focusTarget = SpotlightIds.TPOPUP; } - const timerId = setTimeout(() => { - Spotlight.focus(SpotlightIds.TPOPUP); - }, 0); + + const timerId = setTimeout(() => { + console.log("focusTarget", focusTarget); + Spotlight.focus(focusTarget); + }, 200); return () => { clearTimeout(timerId); }; } - }, [spotlightId, popupVisible]); + }, [popupVisible, spotlightId, kind, contentType]); useEffect(() => { if (KINDS.indexOf(kind) < 0) { @@ -123,9 +165,23 @@ export default function TPopUp({ } }, [kind]); - const ButtonContainerComp = useMemo(() => { - return kind === "exitPopup" ? ButtonContainerNegative : ButtonContainer; + useEffect(() => { + if (kind === "introTermsPopup") { + console.log("introTermsPopup"); + // Spotlight.focus("introTermsAgreeBtn"); + } }, [kind]); + + const ButtonContainerComp = useMemo(() => { + if (kind === "exitPopup") { + return ButtonContainerNegative; + } + if (kind === "introTermsPopup") { + return IntroTermsButtonContainer; + } + return ButtonContainer; + }, [kind]); + // optionalAgreement // 자동으로 Yes/No 버튼 텍스트 설정 @@ -146,19 +202,35 @@ export default function TPopUp({ // optionalAgreement일 경우 항상 버튼 표시 const shouldShowButtons = useMemo(() => { - return hasButton || kind === "optionalAgreement"; + return hasButton || kind === "optionalAgreement" || kind === "introTermsPopup"; }, [hasButton, kind]); //------------------------------------------------------- + // const _onClick = useCallback( + // (e) => { + // if (onClick) { + // onClick(e); + // } else if (kind === "exitPopup") _onExit(); + // else _onClose(); + // }, + // [onClick, kind, _onExit, _onClose] + // ); + + // onClick 핸들러 수정 - 약관 동의 처리 추가 const _onClick = useCallback( (e) => { - if (onClick) { + if (contentType === CONTENT_TYPES.TERMS && onTermsAgree) { + onTermsAgree(e); + } else if (onClick) { onClick(e); - } else if (kind === "exitPopup") _onExit(); - else _onClose(); + } else if (kind === "exitPopup") { + _onExit(); + } else { + _onClose(); + } }, - [onClick, kind, _onExit, _onClose] + [contentType, onTermsAgree, onClick, kind, _onExit, _onClose] ); const _optionClick = useCallback( @@ -231,6 +303,72 @@ export default function TPopUp({ return true; } }, [httpHeader]); + + // 약관 제목 매핑 함수 + const getTermsTitle = useCallback((trmsTpCd) => { + switch(trmsTpCd) { + case "MST00401": + return $L("Privacy Policy"); + case "MST00402": + return $L("Terms & Conditions"); + case "MST00405": + return $L("Optional Terms"); + default: + return ""; + } + }, []); + + // 약관 상세보기 팝업시 렌더링 함수 + const renderTermsContent = useCallback(() => { + if (!termsData) return null; + + return ( +
+ + +
+ +
+ ); + }, [termsData, getTermsTitle]); + + + // 약관 동의 핸들러 + const handleTermsAgree = useCallback((e) => { + if (onTermsAgree) { + onTermsAgree(e); + } + if (onClick) { + onClick(e); + } + }, [onTermsAgree, onClick]); + + // 컨텐츠 타입별 렌더링 함수 + const renderContent = useMemo(() => { + switch(contentType) { + case CONTENT_TYPES.TERMS: + return renderTermsContent(); + default: + return children; + } + }, [contentType, children, renderTermsContent]); + + return ( )} - {children} + {renderContent} {hasIndicator && ( <> {currentPage !== 0 && ( diff --git a/com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less b/com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less index 83162965..187cb5ca 100644 --- a/com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less +++ b/com.twin.app.shoptime/src/components/TPopUp/TPopUp.module.less @@ -69,7 +69,7 @@ // kind .introTermsPopup { - .default-style(); + .default-style(); .info { .size(@w: 1100px , @h: 564px); @@ -82,6 +82,11 @@ > div { min-width: 300px; height: 78px; + + // 첫 번째 버튼(Agree)의 오른쪽에만 마진 추가 + &:first-child { + margin-right: 12px; + } } } } @@ -741,3 +746,69 @@ // position: fixed !important; // margin: 0 !important; // } + +// wideLeftPopup 전용 스타일 - 다른 팝업과 완전히 분리 +.tPopUp.wideLeftPopup { + // 팝업 전체 위치 설정 + // position: absolute !important; + // left: 0px !important; + // top: 50% !important; + // width: 100% !important; + // // transform: translateY(-50%) !important; + // margin: 0 !important; + + .info { + // 실제 팝업 컨텐츠 크기 설정 + .size(@w: 1920px, @h: 110px); + // .position(absolute, 20%, auto, auto, 0); + + background-color: @BG_COLOR_01; + color: @COLOR_GRAY03; + display: flex; + flex-direction: column; + font-weight: normal; + box-sizing: border-box; + border-radius: 4px; + padding: 60px 57px 40px; + + .textLayer { + width: 100%; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + + .title { + text-align: left; + font-size: 36px; + font-weight: bold; + line-height: normal; + color: @COLOR_BLACK; + margin-bottom: 30px; + } + + .text { + color: @COLOR_GRAY03; + font-size: 30px; + line-height: 38px; + text-align: center; + min-height: 100px; + display: flex; + align-items: center; + justify-content: center; + } + } + + .buttonContainer { + margin-top: 30px; + display: flex; + justify-content: center; + gap: 12px; + + > div { + min-width: 300px; + height: 80px; + } + } + } +} diff --git a/com.twin.app.shoptime/src/reducers/commonReducer.js b/com.twin.app.shoptime/src/reducers/commonReducer.js index bd766da5..d441f68f 100644 --- a/com.twin.app.shoptime/src/reducers/commonReducer.js +++ b/com.twin.app.shoptime/src/reducers/commonReducer.js @@ -1,3 +1,5 @@ +// src/reducers/commonReducer.js + import { types } from "../actions/actionTypes"; import { LOG_MENU } from "../utils/Config"; @@ -9,7 +11,7 @@ const initialState = { serverHOST: "", //"US.nextlgsdp.com", mbr_no: "", //X-User-Number : "US2401051532595" deviceId: "", //d87cedca-84e7-c05e-613d-39739bb7941f - cursorVisible: false, + cursorVisible: false, loginUserData: {}, toast: false, toastText: null, @@ -30,6 +32,7 @@ const initialState = { optionalTermsConfirmSelected: false, }, termsFlag: null, + termsLoading: false, // 25.06.16 추가 introTermsAgree: undefined, // Y, N checkoutTermsAgree: undefined, useLog: true, @@ -200,16 +203,26 @@ export const commonReducer = (state = initialState, action) => { case types.SET_EXIT_APP: return state; - case types.GET_TERMS_AGREE_YN: { - const { privacyTerms, serviceTerms, purchaseTerms, paymentTerms,optionalTerms } = + // 25.06.16 추가 + + case types.GET_TERMS_AGREE_YN_START: { + return { + ...state, + termsLoading: true, + }; + } + + case types.GET_TERMS_AGREE_YN_SUCCESS: { + const { privacyTerms, serviceTerms, purchaseTerms, paymentTerms, optionalTerms } = action.payload; const introTermsAgree = privacyTerms === "Y" && serviceTerms === "Y"; const checkoutTermsAgree = purchaseTerms === "Y" && paymentTerms === "Y"; - const optionalTermsAgree = optionalTerms == "Y" ; + const optionalTermsAgree = optionalTerms == "Y"; return { ...state, + termsLoading: false, termsFlag: { ...action.payload, }, @@ -218,6 +231,31 @@ export const commonReducer = (state = initialState, action) => { optionalTermsAgree, }; } + + case types.GET_TERMS_AGREE_YN_FAILURE: { + 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 { diff --git a/com.twin.app.shoptime/src/utils/Config.js b/com.twin.app.shoptime/src/utils/Config.js index f2b8f4c4..2857e36a 100644 --- a/com.twin.app.shoptime/src/utils/Config.js +++ b/com.twin.app.shoptime/src/utils/Config.js @@ -84,9 +84,11 @@ export const ACTIVE_POPUP = { endOfServicePopup: "endOfServicePopup", checkoutErrorPopup: "checkoutErrorPopup", optionalTermsConfirmPopup: "optionalTermsConfirmPopup", - optionalTermsTest: "optionalTermsTest", + optionalTermsConfirm: "optionalTermsConfirm", + optionalTermsConfirmBottom: "optionalTermsConfirmBottom", introTermsPopup: "introTermsPopup", toast: "toast", + optionalConfirm: "optionalConfirm", }; export const DEBUG_VIDEO_SUBTITLE_TEST = false; export const AUTO_SCROLL_DELAY = 600; diff --git a/com.twin.app.shoptime/src/utils/focus-monitor.js b/com.twin.app.shoptime/src/utils/focus-monitor.js new file mode 100644 index 00000000..8e6614c5 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/focus-monitor.js @@ -0,0 +1,205 @@ +// focus-monitor.js +import Spotlight from '@enact/spotlight'; + +// 포커스 모니터링 클래스 +class FocusMonitor { + constructor() { + this.isListening = false; + this.focusHistory = []; + } + + // 포커스 모니터링 시작 + startMonitoring() { + if (this.isListening) return; + + console.log("[focus] 포커스 모니터링 시작"); + this.isListening = true; + + // DOM 이벤트로 포커스 모니터링 + // capture: true로 설정하여 모든 포커스 이벤트 캐치 + document.addEventListener('focusin', this.handleDOMFocus, true); + document.addEventListener('focusout', this.handleDOMBlur, true); + + // 키보드 이벤트로 spotlight 네비게이션 감지 + document.addEventListener('keydown', this.handleKeydown, true); + } + + // 포커스 모니터링 중지 + stopMonitoring() { + if (!this.isListening) return; + + console.log("[focus] 포커스 모니터링 중지"); + this.isListening = false; + + // 이벤트 리스너 제거 + document.removeEventListener('focusin', this.handleDOMFocus, true); + document.removeEventListener('focusout', this.handleDOMBlur, true); + document.removeEventListener('keydown', this.handleKeydown, true); + } + + // 키보드 이벤트 핸들러 (spotlight 네비게이션 감지) + handleKeydown = (event) => { + // spotlight 네비게이션 키들 + const spotlightKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter']; + + if (spotlightKeys.includes(event.key)) { + console.log(`[focus] 🎮 SPOTLIGHT KEY: ${event.key}`); + + // 키 이벤트 후 잠시 대기하여 포커스 변화 감지 + setTimeout(() => { + const current = this.getCurrentSpotlightFocus(); + if (current) { + console.log(`[focus] 🎯 SPOTLIGHT NAVIGATION RESULT:`, current); + } + }, 10); + } + } + + // DOM 포커스 이벤트 핸들러 + handleDOMFocus = (event) => { + const target = event.target; + const focusInfo = this.getElementInfo(target); + + // spotlight 요소인지 확인 + const isSpottable = this.isSpottableElement(target); + const logPrefix = isSpottable ? "🎯 SPOTLIGHT FOCUS" : "🔍 DOM FOCUS"; + + console.log(`[focus] ${logPrefix}:`, focusInfo); + + this.focusHistory.push({ + type: isSpottable ? 'spotlight-focus' : 'dom-focus', + timestamp: Date.now(), + target: focusInfo, + event: event + }); + } + + // DOM 블러 이벤트 핸들러 + handleDOMBlur = (event) => { + const target = event.target; + const blurInfo = this.getElementInfo(target); + + // spotlight 요소인지 확인 + const isSpottable = this.isSpottableElement(target); + const logPrefix = isSpottable ? "💨 SPOTLIGHT BLUR" : "🌫️ DOM BLUR"; + + console.log(`[focus] ${logPrefix}:`, blurInfo); + + this.focusHistory.push({ + type: isSpottable ? 'spotlight-blur' : 'dom-blur', + timestamp: Date.now(), + target: blurInfo, + event: event + }); + } + + // 요소가 spotlight 요소인지 확인 + isSpottableElement(element) { + if (!element) return false; + + // spotlight 관련 속성들 확인 + return element.hasAttribute('data-spotlight-id') || + element.hasAttribute('data-spotlight-container') || + element.classList.contains('spottable') || + element.getAttribute('tabindex') === '0' || + // Enact 컴포넌트들은 보통 특정 클래스명을 가짐 + element.className.includes('Button_') || + element.className.includes('Input_') || + element.className.includes('Item_'); + } + + // 현재 spotlight 포커스 가져오기 + getCurrentSpotlightFocus() { + try { + const current = Spotlight.getCurrent(); + return this.getElementInfo(current); + } catch (e) { + // getCurrent가 없는 경우 document.activeElement 사용 + return this.getElementInfo(document.activeElement); + } + } + + // 요소 정보 추출 + getElementInfo(element) { + if (!element) return { element: null, info: 'No element' }; + + const info = { + tagName: element.tagName, + id: element.id || '', + className: element.className || '', + spotlightId: element.getAttribute('data-spotlight-id') || '', + spotlightContainer: element.getAttribute('data-spotlight-container') || '', + textContent: element.textContent ? element.textContent.substring(0, 50) : '', + tabIndex: element.tabIndex, + isSpottable: this.isSpottableElement(element), + element: element + }; + + return info; + } + + // 현재 포커스된 요소 정보 가져오기 + getCurrentFocus() { + const activeElement = document.activeElement; + const spotlightCurrent = this.getCurrentSpotlightFocus(); + + console.log("[focus] 현재 포커스 상태:"); + console.log("[focus] - DOM activeElement:", this.getElementInfo(activeElement)); + console.log("[focus] - Spotlight current:", spotlightCurrent); + + return { + dom: this.getElementInfo(activeElement), + spotlight: spotlightCurrent + }; + } + + // 포커스 히스토리 출력 + printHistory(limit = 10) { + console.log(`[focus] 📋 포커스 히스토리 (최근 ${limit}개):`); + this.focusHistory + .slice(-limit) + .forEach((entry, index) => { + const time = new Date(entry.timestamp).toLocaleTimeString(); + console.log(`[focus] ${index + 1}. [${time}] ${entry.type}:`, entry.target); + }); + } + + // 히스토리 초기화 + clearHistory() { + this.focusHistory = []; + console.log("[focus] 포커스 히스토리가 초기화되었습니다."); + } + + // spotlight 상태 확인 + getSpotlightStatus() { + try { + const current = Spotlight.getCurrent(); + const paused = Spotlight.isPaused ? Spotlight.isPaused() : false; + + console.log("[focus] 🔍 Spotlight 상태:"); + console.log("[focus] - Current:", this.getElementInfo(current)); + console.log("[focus] - Paused:", paused); + + return { + current: this.getElementInfo(current), + paused: paused + }; + } catch (e) { + console.log("[focus] ⚠️ Spotlight 상태를 가져올 수 없습니다:", e.message); + return null; + } + } +} + +// 싱글톤 인스턴스 생성 +const focusMonitor = new FocusMonitor(); + +// 편의 함수들 export +export const startFocusMonitoring = () => focusMonitor.startMonitoring(); +export const stopFocusMonitoring = () => focusMonitor.stopMonitoring(); +export const getCurrentFocus = () => focusMonitor.getCurrentFocus(); +export const printFocusHistory = (limit) => focusMonitor.printHistory(limit); +export const clearFocusHistory = () => focusMonitor.clearHistory(); +export const getSpotlightStatus = () => focusMonitor.getSpotlightStatus(); + +export default focusMonitor; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/spotlight-utils.js b/com.twin.app.shoptime/src/utils/spotlight-utils.js new file mode 100644 index 00000000..437b35d6 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/spotlight-utils.js @@ -0,0 +1,240 @@ +// spotlight-utils.js +import {getTargetByContainer} from '@enact/spotlight/src/target'; +import {getTargetBySelector} from '@enact/spotlight/src/target'; +import {getContainerConfig} from '@enact/spotlight/src/container'; +import {isContainer, getContainerId} from '@enact/spotlight/src/container'; +import {getContainersForNode} from '@enact/spotlight/src/container'; +import {isNavigable} from '@enact/spotlight/src/container'; +import {setLastContainer} from '@enact/spotlight/src/container'; +import Spotlight from '@enact/spotlight'; + +// lodash 없이 last 함수 직접 구현 +const last = (array) => { + return array && array.length > 0 ? array[array.length - 1] : undefined; +}; + +// focusElement 함수는 spotlight 내부 함수이므로, 직접 구현하거나 spotlight의 focus를 사용 +const focusElement = (target, containerIds) => { + console.log("focusElement called with:", target, containerIds); + if (target && typeof target.focus === 'function') { + try { + target.focus(); + return true; + } catch (e) { + console.error("Focus failed:", e); + return false; + } + } + return false; +}; + +/** + * Predicts which element would be focused by Spotlight.focus() without actually + * changing the focus. + * + * @param {String|Node} [elem] The spotlight ID or selector for either a spottable + * component or a spotlight container, or spottable node. If not supplied, the default + * container's target will be used. + * @returns {Node|null} The DOM element that would be focused, or null if no + * navigable target is found. + */ +export const getPredictedFocus = (elem) => { + let target = elem; + + if (!elem) { + target = getTargetByContainer(); + } else if (typeof elem === 'string') { + if (getContainerConfig(elem)) { + // String is a container ID + target = getTargetByContainer(elem); + } else if (/^[\w\d-]+$/.test(elem)) { + // Support component IDs consisting of alphanumeric, dash, or underscore + target = getTargetBySelector(`[data-spotlight-id=${elem}]`); + } else { + // Treat as a CSS selector + target = getTargetBySelector(elem); + } + } else if (isContainer(elem)) { + // elem is a container element + target = getTargetByContainer(getContainerId(elem)); + } + + // Check navigability without attempting to focus + const nextContainerIds = getContainersForNode(target); + const nextContainerId = last(nextContainerIds); + + if (isNavigable(target, nextContainerId, true)) { + return target; + } + + return null; + }; + +// 메인 focus 함수 +export const focus = (elem) => { + console.log("focus test", elem); + var target = elem; + var wasContainerId = false; + + if (!elem) { + // elem이 없으면 기본 컨테이너의 타겟을 가져옴 + target = getTargetByContainer(); + } else if (typeof elem === 'string') { + if (getContainerConfig(elem)) { + // 문자열이 컨테이너 ID인 경우 + target = getTargetByContainer(elem); + wasContainerId = true; + } else if (/^[\w\d-]+$/.test(elem)) { + // 알파벳, 숫자, 대시, 언더스코어로 구성된 컴포넌트 ID 지원 + target = getTargetBySelector("[data-spotlight-id=".concat(elem, "]")); + } else { + // CSS 셀렉터로 처리 + target = getTargetBySelector(elem); + } + } else if (isContainer(elem)) { + // elem이 컨테이너 요소인 경우 + target = getTargetByContainer(getContainerId(elem)); + } + + // 타겟 노드의 컨테이너들을 가져옴 + var nextContainerIds = getContainersForNode(target); + var nextContainerId = last(nextContainerIds); // 마지막 컨테이너 ID + + if (isNavigable(target, nextContainerId, true)) { + // 네비게이션 가능한 경우 포커스 설정 + var focused = focusElement(target, nextContainerIds); + + if (!focused && wasContainerId) { + // 포커스 실패했지만 컨테이너 ID였던 경우 마지막 컨테이너로 설정 + setLastContainer(elem); + } + + return focused; + } else if (wasContainerId) { + // 제공된 컨테이너 내에서 spottable 타겟을 찾지 못한 경우 + // 내용이 변경될 때 자체적으로 포커스할 수 있도록 활성 컨테이너로 설정 + setLastContainer(elem); + } + + return false; +}; + +// spotlight-utils.js에 추가할 함수 + +/** + * spotlightId로 직접 포커스를 설정하는 함수 + * + * @param {String} spotlightId - data-spotlight-id 속성값 + * @param {Boolean} force - 강제 포커스 여부 (기본값: false) + * @returns {Boolean} 포커스 성공 여부 + */ +export const focusById = (spotlightId, force = false) => { + // spotlightId 유효성 검사 + if (!spotlightId || typeof spotlightId !== 'string') { + console.error('[focusById] spotlightId는 반드시 문자열이어야 합니다.'); + return false; + } + + try { + // data-spotlight-id 속성을 가진 요소 직접 검색 + const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`); + + if (!targetElement) { + console.warn(`[focusById] spotlightId "${spotlightId}"를 가진 요소를 찾을 수 없습니다.`); + return false; + } + + // 요소가 현재 보이고 활성화되어 있는지 확인 + if (!isElementVisible(targetElement)) { + console.warn(`[focusById] 요소 "${spotlightId}"가 보이지 않거나 비활성화되어 있습니다.`); + if (!force) return false; + } + + // Spotlight의 isSpottable로 포커스 가능 여부 확인 + if (typeof Spotlight !== 'undefined' && Spotlight.isSpottable) { + if (!Spotlight.isSpottable(targetElement) && !force) { + console.warn(`[focusById] 요소 "${spotlightId}"가 현재 spottable하지 않습니다.`); + return false; + } + } + + // 직접 DOM 포커스 시도 + if (force) { + // 강제 모드: DOM focus() 직접 호출 + console.log(`[focusById] 강제 포커스 모드: "${spotlightId}"`); + targetElement.focus(); + return true; + } else { + // 일반 모드: Spotlight 시스템 사용 + console.log(`[focusById] Spotlight 포커스: "${spotlightId}"`); + + // Spotlight.focus() 사용 (선택자 형태로 전달) + const focusResult = focus(`[data-spotlight-id="${spotlightId}"]`); + + if (!focusResult) { + // Spotlight 포커스 실패 시 직접 포커스 시도 + console.log(`[focusById] Spotlight 포커스 실패, 직접 포커스 시도: "${spotlightId}"`); + targetElement.focus(); + return document.activeElement === targetElement; + } + + return focusResult; + } + + } catch (error) { + console.error(`[focusById] 포커스 설정 중 오류 발생: "${spotlightId}"`, error); + return false; + } + }; + + /** + * 요소가 보이고 포커스 가능한 상태인지 확인하는 헬퍼 함수 + * + * @param {Element} element - 확인할 DOM 요소 + * @returns {Boolean} 요소의 가시성 및 활성화 상태 + */ + const isElementVisible = (element) => { + if (!element) return false; + + // 요소가 DOM에 연결되어 있는지 확인 + if (!element.isConnected) return false; + + // disabled 속성 확인 + if (element.disabled) return false; + + // display: none 또는 visibility: hidden 확인 + const style = window.getComputedStyle(element); + if (style.display === 'none' || style.visibility === 'hidden') return false; + + // opacity가 0인지 확인 + if (parseFloat(style.opacity) === 0) return false; + + // 요소의 크기가 0인지 확인 + const rect = element.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return false; + + return true; + }; + + /** + * 현재 포커스된 요소의 spotlightId를 반환하는 헬퍼 함수 + * + * @returns {String|null} 현재 포커스된 요소의 spotlightId 또는 null + */ + export const getCurrentSpotlightId = () => { + const current = document.activeElement; + if (current && current.hasAttribute('data-spotlight-id')) { + return current.getAttribute('data-spotlight-id'); + } + return null; + }; + + /** + * 특정 spotlightId를 가진 요소가 현재 포커스되어 있는지 확인하는 함수 + * + * @param {String} spotlightId - 확인할 spotlightId + * @returns {Boolean} 해당 요소가 현재 포커스되어 있는지 여부 + */ + export const isCurrentlyFocused = (spotlightId) => { + return getCurrentSpotlightId() === spotlightId; + }; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx b/com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx index 498f9382..fccd11c2 100644 --- a/com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx +++ b/com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx @@ -403,6 +403,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => { (containerId) => setFocusedContainerId(containerId), [] ); + const renderPageItem = useCallback(() => { return ( <> diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx index 8cac19df..f538f52a 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx @@ -1,16 +1,25 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; +// src/views/HomePanel/HomeBanner/HomeBanner.jsx + +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import Spotlight from "@enact/spotlight"; import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator"; import Spottable from "@enact/spotlight/Spottable"; - -import { setDefaultFocus } from "../../../actions/homeActions"; +import { $L, scaleH, scaleW } from '../../../utils/helperMethods'; +import { setDefaultFocus, setShowPopup } from "../../../actions/homeActions"; +import { changeAppStatus } from "../../../actions/commonActions"; +import { setMyPageTermsAgree } from '../../../actions/myPageActions'; +import { pushPanel } from "../../../actions/panelActions"; import CustomImage from "../../../components/CustomImage/CustomImage"; import css from "./HomeBanner.module.less"; import Random from "./RandomUnit"; import Rolling from "./RollingUnit"; +import TNewPopUp from "../../../components/TPopUp/TNewPopUp"; +import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller"; +import OptionalConfirm from "../../../components/Optional/OptionalConfirm"; +import * as Config from "../../../utils/Config"; const SpottableComponent = Spottable("div"); const Container = SpotlightContainerDecorator( @@ -49,6 +58,84 @@ export default function HomeBanner({ } }, [handleItemFocus]); + const termsData = useSelector((state) => state.home.termsData); + const optionalTermsData = useSelector((state) => + state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405") + ); + const termsLoading = useSelector((state) => state.common.termsLoading); + // 선택약관 동의여부 + const introTermsAgree = useSelector((state) => state.common.introTermsAgree); + + //------------------------------------------------------------------------------ + // 팝업표시 상태 + const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false); + const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false); + + // 선택약관 팝업 표시 여부 + const shouldShowOptionalTermsPopup = useMemo(() => { + const terms = termsData?.data?.terms; + if (!terms) { + return false; + } + const optionalTerm = terms.find(term => term.trmsTpCd === "MST00405"); + return optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false; + }, [termsData]); + + const handleOptionalAgree = useCallback(() => { + console.log('handleAgree Click'); + + // 약관 동의할 항목들 (string array) + const termsList = ["TID0000222", "TID0000223", "TID0000232"]; + // 동의하지 않을 항목들 (빈 배열) + const notTermsList = []; + console.log('OptionalTermsConfirm -약관 동의 API 호출 파라미터:', { termsList, notTermsList }); + const callback = (response) => { + if (response.retCode === "000" || response.retCode === 0) { + console.log('약관 동의 성공:', response); + } else { + console.error('약관 동의 실패:', response); + } + }; + + console.log('OptionalTermsConfirm - 약관 동의 API 호출 payload:', { termsList, notTermsList }); + dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback)); + + }, []); + + const handleOptionalTermsClick = useCallback(() => { + console.log('약관 자세히 보기 클릭'); + setIsOptionalConfirmVisible(false); + setIsOptionalTermsVisible(true); + // 약관 상세 팝업을 띄우는 로직 추가 + }, []); + + const handleOptionalAgreeClick = useCallback(() => { + handleOptionalAgree(); + setIsOptionalConfirmVisible(false); + }, []); + + const handleOptionalDeclineClick = useCallback(() => { + console.log('거절/다음에 하기 버튼 클릭'); + setIsOptionalConfirmVisible(false); + // 거절 처리 로직 추가 + }, []); + + // 선택약관 팝업 Close + const handleTermsPopupClosed = useCallback(() => { + setIsOptionalTermsVisible(false); + setIsOptionalConfirmVisible(true); + Spotlight.focus("optional-confirm-popup"); + }, []); + + // 선택약관 팝업 Agree + const handleTermsPopupAgree = useCallback(() => { + console.log("handleTermsPopupAgree"); + handleOptionalAgree(); + setIsOptionalTermsVisible(false); + }, []); + + + //------------------------------------------------------------------------------ const _handleShelfFocus = useCallback(() => { if (handleShelfFocus) { handleShelfFocus(); @@ -97,6 +184,36 @@ export default function HomeBanner({ } }, [defaultFocus, dispatch, popupVisible]); + // 테스트용 팝업 표시 + // useEffect(() => { + // setTimeout(() => { + // console.log("App.js optionalTermsTest 팝업 표시"); + // setIsOptionalConfirmVisible(true); + // // setIsOptionalTermsVisible(true); + // }, 3000); + // }, []); + + // 약관 동의 및 선택 약관 팝업 처리 + useEffect(() => { + if (termsLoading) { + // 약관 데이터 로딩 중에는 아무것도 하지 않음 + return; + } + // 선택 약관 팝업을 띄워야 하는 경우 + if (shouldShowOptionalTermsPopup) { + // 3초 후에 팝업을 띄우도록 설정 + console.log("shouldShowOptionalTermsPopup", shouldShowOptionalTermsPopup); + console.log("App.js optionalTermsConfirm 팝업 표시"); + const timer = setTimeout(() => { + setIsOptionalConfirmVisible(true); + // dispatch(setShowPopup({ activePopup: "optionalTermsConfirm" })); + }, 3000); // 3000 milliseconds = 3 seconds + + // 컴포넌트 언마운트 시 타이머 클리어 + return () => clearTimeout(timer); + } + }, [shouldShowOptionalTermsPopup, termsLoading]); + const renderItem = useCallback( (index, isHorizontal) => { const data = bannerDataList?.[index] ?? {}; @@ -187,12 +304,58 @@ export default function HomeBanner({ }, [selectTemplate, renderItem]); return ( - -
{renderLayout()}
-
+ <> + +
{renderLayout()}
+
+ {/* 선택약관 동의 팝업 */} + + {/* 선택약관 자세히 보기 팝업 */} + + {optionalTermsData && ( +
+
{$L("Optional Terms")}
+ +
+ +
+ )} + + ); } diff --git a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx index fc21b9ad..8db3180c 100644 --- a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx +++ b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx @@ -1,6 +1,6 @@ // src: views/IntroPanel/IntroPanel.new.jsx -import React, { useCallback, useEffect, useState,useMemo } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -16,7 +16,7 @@ import { } from "../../actions/commonActions"; import { registerDevice } from "../../actions/deviceActions"; import { getWelcomeEventInfo } from "../../actions/eventActions"; -import { getHomeTerms } from "../../actions/homeActions"; +import { fetchCurrentUserHomeTerms } from "../../actions/homeActions"; import { sendLogGNB, sendLogTerms, @@ -28,13 +28,17 @@ import TButtonScroller from "../../components/TButtonScroller/TButtonScroller"; import TButtonTab from "../../components/TButtonTab/TButtonTab"; import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare"; import TPanel from "../../components/TPanel/TPanel"; -import TPopUp from "../../components/TPopUp/TPopUp"; +import TPopUp, { CONTENT_TYPES } from "../../components/TPopUp/TPopUp"; +// import TNewPopUp from "../../components/TPopUp/TNewPopUp"; import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo"; import useDebugKey from "../../hooks/useDebugKey"; import * as Config from "../../utils/Config"; import { panel_names } from "../../utils/Config"; import { $L, scaleH, scaleW } from "../../utils/helperMethods"; import css from "./IntroPanel.new.module.less"; +import { types } from "../../actions/actionTypes"; +import { focusById } from "../../utils/spotlight-utils"; + const Container = SpotlightContainerDecorator( { enterTo: "last-focused" }, @@ -62,6 +66,10 @@ export default function IntroPanel({ (state) => state.device.regDeviceInfoData ); + // registerDevice API 호출 중 여부 + const [isProcessing, setIsProcessing] = useState(false); + const [showExitMessagePopup, setShowExitMessagePopup] = useState(false); + // const introTermsData = termsData?.data?.terms.filter( // (item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402" // ); @@ -116,9 +124,20 @@ export default function IntroPanel({ const [privacyChecked, setPrivacyChecked] = useState(true); // Privacy Policy 기본 체크 const [optionalChecked, setOptionalChecked] = useState(false); // Optional Terms 기본 체크 안됨 const [selectAllChecked, setSelectAllChecked] = useState(false); + useEffect(() => { dispatch(sendLogGNB(Config.LOG_MENU.TERMS_CONDITIONS)); }, []); + + // 컴포넌트 마운트 시 현재 Redux 상태 로깅 + useEffect(() => { + console.log('🔍 IntroPanel 마운트 시 Redux 상태:'); + console.log(' - regDeviceData:', regDeviceData); + console.log(' - regDeviceInfoData:', regDeviceInfoData); + console.log(' - eventInfos:', eventInfos); + console.log(' - termsData:', termsData); + }, []); + // 디버깅용 WebOS 버전 로그 useEffect(() => { console.log('🔍 IntroPanel WebOS 버전 정보:'); @@ -126,11 +145,69 @@ export default function IntroPanel({ console.log(' - shouldShowBenefitsView:', shouldShowBenefitsView); }, [webOSVersion, shouldShowBenefitsView]); + useEffect(() => { + if (showExitMessagePopup) { + const timer = setTimeout(() => { + dispatch(setExitApp()); + dispatch( + sendLogTotalRecommend({ + contextName: Config.LOG_CONTEXT_NAME.SHOPTIME, + messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE, + visible: false, + }) + ); + }, 3000); + return () => clearTimeout(timer); + } + }, [showExitMessagePopup, dispatch]); + // Select All 상태 업데이트 useEffect(() => { setSelectAllChecked(termsChecked && privacyChecked && optionalChecked); }, [termsChecked, privacyChecked, optionalChecked]); + // 컴포넌트 마운트 후 1.5초 뒤에 agreeButton으로 강제 포커스 + useEffect(() => { + // 1.5초(1500ms) 후에 실행될 타이머 설정 + const focusTimer = setTimeout(() => { + console.log('[Focus] 1.5초 후 agreeButton으로 강제 포커스 시도'); + + // focusById 함수를 사용하여 강제 포커스 (force: true) + const focusSuccess = focusById('agreeButton', true); + + if (focusSuccess) { + console.log('[Focus] agreeButton 포커스 성공'); + } else { + console.warn('[Focus] agreeButton 포커스 실패'); + } + }, 1500); + + // 컴포넌트 언마운트 시 타이머 정리 + return () => { + clearTimeout(focusTimer); + console.log('[Focus] agreeButton 포커스 타이머 정리됨'); + }; + }, []); // 빈 dependency 배열로 컴포넌트 마운트 시에만 실행 + + // 약관 팝업 동의여부에 따른 이벤트 핸들러 + const handleTermsAgree = useCallback(() => { + if (!currentTerms) { + return; + } + const termType = currentTerms.trmsTpCd; + if (termType === "MST00402") { + setTermsChecked(true); + } else if (termType === "MST00401") { + setPrivacyChecked(true); + } else if (termType === "MST00405") { + // Optional Terms + setOptionalChecked(true); + } + // 팝업 닫기 + dispatch(setHidePopup()); + }, [currentTerms, dispatch]); + + const handleTermsClick = useCallback( (trmsTpCdList) => { if (introTermsData) { @@ -174,7 +251,12 @@ export default function IntroPanel({ const onClose = useCallback(() => { dispatch(setHidePopup()); - }, [dispatch]); const handleAgree = useCallback(() => { + }, [dispatch]); + + const handleAgree = useCallback(() => { + + if(isProcessing) return; + // 필수 약관이 체크되어 있는지 확인 if (!termsChecked || !privacyChecked) { // 필수 약관이 체크되지 않았을 때 알림 @@ -187,6 +269,10 @@ export default function IntroPanel({ return; } + setIsProcessing(true); + // 약관 동의 처리 시작 시 로딩 상태로 설정 + dispatch({ type: types.GET_TERMS_AGREE_YN_START }); + // 약관 ID 정확하게 매핑 const agreeTerms = []; @@ -205,15 +291,33 @@ export default function IntroPanel({ // registerDevice 호출 - 필수 + 선택 약관 모두 포함 dispatch(registerDevice({ agreeTerms: agreeTerms - })); + })); + // dispatch(fetchCurrentUserHomeTerms()); // 중복호출 방지 + setIsProcessing(false); }, [termsChecked, privacyChecked, optionalChecked, dispatch]); + // 실패 감지를 위한 useEffect 추가 + useEffect(() => { + // isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만) + if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) { + console.error('registerDevice 실패:', regDeviceData); + dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, { + title: $L("Error"), + text: $L("Device registration failed. Please try again."), + button1Text: $L("OK") + })); + setIsProcessing(false); + } + }, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가 + const handleDisagree = useCallback(() => { dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup)); dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.DO_NOT_AGREE })); }, [dispatch]); - const onExit = useCallback(() => { + const onExit = useCallback(() => { + dispatch(setHidePopup()); + setShowExitMessagePopup(true); dispatch(setExitApp()); dispatch( sendLogTotalRecommend({ @@ -252,7 +356,7 @@ export default function IntroPanel({ useEffect(() => { - Spotlight.focus("termsCheckbox"); + Spotlight.focus("selectAllCheckbox"); }, []); useEffect(() => { @@ -283,7 +387,7 @@ export default function IntroPanel({ ) { displayWelcomeEventPanel = true; } - + setIsProcessing(false); dispatch(popPanel(panel_names.INTRO_PANEL)); dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE })); @@ -338,7 +442,7 @@ export default function IntroPanel({ {/* Terms & Conditions */}
- {/* TERMS */} + {/* 약관 보기 팝업 */} {activePopup === Config.ACTIVE_POPUP.termsPopup && ( - - {currentTerms && ( -
- - -
- -
- )} - + button1Text={$L("Agree")} + button2Text={$L("Close")} + spotlightId="tPopupBtn1" + /> )} {/* DO NOT AGREE */} {activePopup === Config.ACTIVE_POPUP.exitPopup && ( )} + {/* Final Exit Message Popup */} + {showExitMessagePopup && ( + + )} ); -} \ No newline at end of file +} + diff --git a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less index 368ee9d1..7310a582 100644 --- a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less +++ b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.module.less @@ -146,24 +146,29 @@ transition: color 0.3s ease; } - 포커스 상태 - &.focused { - outline: 2px #C91D53 solid !important; + // ✅ 포커스 상태 (화살표 키 네비게이션용) + &.focused, + &:focus, + &:focus-visible { + outline: 4px #C91D53 solid !important; outline-offset: 2px !important; background-color: rgba(201, 29, 83, 0.1) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(201, 29, 83, 0.3) !important; .termsText { - color: #C70850; + // color: #C70850 !important; + font-weight: bold !important; } } - // 호버 효과 + // ✅ 호버 효과 (마우스용) &:hover { - background-color: rgba(255, 255, 255, 0.1); - transform: translate3d(0, -2px, 0); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + outline: 2px #C91D53 solid; + outline-offset: 2px; + background-color: rgba(201, 29, 83, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(201, 29, 83, 0.3); .termsText { color: #C70850; @@ -238,14 +243,14 @@ .agreeButton { width: 450px; height: 100px; - background-color: #C70850 !important; - border: 2px solid #C70850 !important; + // background-color: #C70850 !important; + // border: 2px solid #C70850 !important; color: white !important; border-radius: 12px; font-size: 35px; font-family: 'LG Smart UI'; font-weight: 700; - line-height: 35px; + line-height: 2rem; cursor: pointer; transition: all 0.3s ease; @@ -277,14 +282,14 @@ .disagreeButton { width: 450px; height: 100px; - background-color: #999999 !important; - border: 2px solid #999999 !important; + // background-color: #999999 !important; + // border: 2px solid #999999 !important; color: white !important; border-radius: 12px; font-size: 35px; font-family: 'LG Smart UI'; font-weight: 700; - line-height: 35px; + line-height: 2rem; cursor: pointer; transition: all 0.3s ease; @@ -337,29 +342,117 @@ } } -// ✅ 로컬 체크박스 스타일로 변경 +// ✅ 로컬 체크박스 스타일 (전체 교체) +// .customCheckbox { +// width: 45px; +// height: 45px; +// position: relative; + +// // 기본 상자 스타일 +// &:before { +// content: ''; +// width: 42px; +// height: 42px; +// background-color: @COLOR_WHITE; +// border: 2px solid @COLOR_GRAY02; +// border-radius: 4px; +// display: block; +// transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +// } + +// // --- 상태별 스타일 정의 --- + +// // [상태 2] 선택됨: 선택되면 배경을 진한 붉은색으로 만듭니다. +// &.selected:before { +// background-color: #C91D53; +// border-color: #C91D53; +// } + +// // [상태 3] 포커스 받았지만, 선택은 안됨: 이때만 배경을 연한 붉은색으로 만듭니다. +// &.focused:not(.selected):before { +// background-color: rgba(201, 29, 83, 0.1); +// } + +// // [상태 1] 포커스 받음: 어떤 상태든 포커스를 받으면 굵고 붉은 테두리와 그림자 효과를 줍니다. +// // 이 규칙을 다른 상태들보다 아래에 두어 우선순위를 높입니다. +// &.focused:before { +// border: 4px solid #C91D53; +// box-shadow: 0 0 10px rgba(199, 8, 80, 0.3); +// } + +// // [상태 4] 선택됨 (체크마크): 선택되었을 때만 체크마크를 표시합니다. +// &.selected:after { +// content: '✓'; +// color: @COLOR_WHITE; +// font-size: 24px; +// font-weight: bold; +// position: absolute; +// top: 50%; +// left: 50%; +// transform: translate(-50%, -50%); +// } + +// // [상태 5] 비활성화됨 +// &.disabled:before { +// background-color: @COLOR_GRAY01; +// border-color: @COLOR_GRAY02; +// opacity: 0.5; +// } + +// // [상태 6] 비활성화 상태에서 포커스 받음 +// &.disabled.focused:before { +// background-color: #C91D53; +// border-color: #C91D53; // 비활성화 포커스는 배경색만 변경하므로, 테두리는 포커스 기본 스타일을 따름 +// opacity: 1; +// } +// } + +// ✅ 로컬 체크박스 스타일 (전체 교체) .customCheckbox { width: 45px; height: 45px; position: relative; + // 기본 상자 스타일 &:before { content: ''; width: 42px; height: 42px; - background: @COLOR_WHITE; + background-color: @COLOR_WHITE; border: 2px solid @COLOR_GRAY02; border-radius: 4px; display: block; + transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } - - // 선택된 상태 + + // ✅ 선택된 상태: 배경색과 테두리 모두 붉은색 + 굵은 테두리 &.selected:before { - background: #C91D53; - border-color: #C91D53; + background-color: #C91D53 !important; + border: 4px solid #C91D53 !important; // 굵은 테두리로 변경 + box-shadow: 0 0 8px rgba(201, 29, 83, 0.3); // 약간의 그림자 효과 } - // 선택된 상태의 체크 마크 + // 포커스 받았지만 선택 안됨 + &.focused:not(.selected):before { + background-color: rgba(201, 29, 83, 0.1); + border: 4px solid #C91D53 !important; + box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important; + } + + // 비활성화됨 + &.disabled:before { + background-color: @COLOR_GRAY01; + border-color: @COLOR_GRAY02; + opacity: 0.5; + } + + // 포커스 받음 (선택된 상태가 아닌 경우에만 적용) + &.focused:not(.selected):before { + border: 4px solid #C91D53 !important; + box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important; + } + + // 체크마크 &.selected:after { content: '✓'; color: @COLOR_WHITE; @@ -370,16 +463,4 @@ left: 50%; transform: translate(-50%, -50%); } - - &.focused:before { - border-color: #C91D53; - box-shadow: 0 0 10px rgba(199, 8, 80, 0.3); - } - - // 비활성화 상태 - &.disabled:before { - background: @COLOR_GRAY01; - border-color: @COLOR_GRAY02; - opacity: 0.5; - } } \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/MainView/MainView.jsx b/com.twin.app.shoptime/src/views/MainView/MainView.jsx index 4acfe04f..1254881a 100644 --- a/com.twin.app.shoptime/src/views/MainView/MainView.jsx +++ b/com.twin.app.shoptime/src/views/MainView/MainView.jsx @@ -50,6 +50,7 @@ import SystemNotification from "../../components/SystemNotification/SystemNotifi import TabLayout from "../../components/TabLayout/TabLayout"; import TButton from "../../components/TButton/TButton"; import TPopUp from "../../components/TPopUp/TPopUp"; +import TNewPopUp from "../../components/TPopUp/TNewPopUp"; import usePrevious from "../../hooks/usePrevious"; import * as Config from "../../utils/Config"; import { panel_names } from "../../utils/Config"; @@ -80,8 +81,8 @@ import ThemeCurationPanel from "../ThemeCurationPanel/ThemeCurationPanel"; import TrendingNowPanel from "../TrendingNowPanel/TrendingNowPanel"; import VideoTestPanel from "../VideoTestPanel/VideoTestPanel"; import WelcomeEventPanel from "../WelcomeEventPanel/WelcomeEventPanel"; -import TermsOfOptional from "../MyPagePanel/MyPageSub/TermsOfService/TermsOfOptionalSimple"; // 선택약관 반영 인트로 import OptionalTermsConfirm from "../../components/Optional/OptionalTermsConfirm"; +import OptionalTermsConfirmBottom from "../../components/Optional/OptionalTermsConfirmBottom"; import css from "./MainView.module.less"; const preloadImages = [ @@ -800,9 +801,23 @@ export default function MainView({ className, initService }) { /> )} {/* OptionalTermsConfirmPopup */} - {activePopup === Config.ACTIVE_POPUP.optionalTermsTest && ( + {activePopup === Config.ACTIVE_POPUP.optionalTermsConfirm && ( )} + {activePopup === Config.ACTIVE_POPUP.optionalTermsConfirmBottom && ( + + )} + {/* {activePopup === Config.ACTIVE_POPUP.optionalConfirm && ( + + )} */}
); } diff --git a/com.twin.app.shoptime/src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfService.jsx b/com.twin.app.shoptime/src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfService.jsx index 85625c59..628aa6e8 100644 --- a/com.twin.app.shoptime/src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfService.jsx +++ b/com.twin.app.shoptime/src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfService.jsx @@ -23,7 +23,7 @@ import { setMyTermsWithdraw, setMyPageTermsAgree, } from "../../../../actions/myPageActions"; -import { getHomeTerms } from "../../../../actions/homeActions"; +import { fetchCurrentUserHomeTerms } from "../../../../actions/homeActions"; import TBody from "../../../../components/TBody/TBody"; import TButton, { TYPES } from "../../../../components/TButton/TButton"; import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller"; @@ -64,8 +64,8 @@ export default function TermsOfService({ title, cbScrollTo }) { const focusJob = useRef(null); - useEffect(() => { - dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] })); + useEffect(() => { + dispatch(fetchCurrentUserHomeTerms()); }, [dispatch]); useEffect(() => { @@ -186,7 +186,7 @@ export default function TermsOfService({ title, cbScrollTo }) { } // 약관 철회 - const onExit = useCallback(() => { + const onExit = useCallback(() => { dispatch(setHidePopup()); dispatch( setMyTermsWithdraw( @@ -231,7 +231,7 @@ export default function TermsOfService({ title, cbScrollTo }) { if (response.retCode === "000" || response.retCode === 0) { console.log("Optional terms agreement successful."); // 약관 동의의 후 약관 정보 조회 - dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] })); + dispatch(fetchCurrentUserHomeTerms()); setAgreePopup(true); } else { console.error("Optional terms agreement failed:", response); @@ -267,7 +267,7 @@ export default function TermsOfService({ title, cbScrollTo }) { if (response.retCode === "000" || response.retCode === 0) { console.log("Optional terms withdrawal successful."); // 약관 철회 후 약관 정보 조회 - dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] })); + dispatch(fetchCurrentUserHomeTerms()); setIsOptionalChecked(false); } else { console.error("Optional terms withdrawal failed:", response); @@ -463,7 +463,7 @@ export default function TermsOfService({ title, cbScrollTo }) { hasText text={$L("Are you sure you want to disagree with the optional terms?")} /> - + {/* 필수약관 체크 확인 팝업 */}