fix: 선택약관관련 수정 250619

This commit is contained in:
djaco
2025-06-19 08:59:10 +09:00
parent eb4f951822
commit 7e67e05aac
27 changed files with 2065 additions and 229 deletions

View File

@@ -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,21 +122,20 @@ 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) {
@@ -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,7 +458,6 @@ function AppBase(props) {
return (
<ErrorBoundary>
<>
{webOSVersion === "" ? null : Number(webOSVersion) < 4 ? (
<NotSupportedVersion />
) : (
@@ -446,7 +471,6 @@ function AppBase(props) {
}
/>
)}
</>
</ErrorBoundary>
);
}

View File

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

View File

@@ -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) => {
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
trmsAgrFlag: term.trmsAgrFlag,
trmsPopFlag: term.trmsPopFlag,
})));
// 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 선택약관을 찾을 수 없습니다.");
}
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,
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;

View File

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

View File

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

View File

@@ -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 (
<TNewPopUp
kind="optionalConfirm" // kind에 따라 TNewPopUp 내부 UI/UX가 결정됩니다.
open={open}
spotlightId={spotlightId}
spotlightRestrict="self-only" // 필요에 따라 props로 설정 가능하게 있습니다.
className={`${css.optionalConfirmPopup} ${className || ''}`.trim()}
onOptionalTermsClick={onOptionalTermsClick}
onOptionalAgreeClick={onOptionalAgreeClick}
onOptionalDeclineClick={onOptionalDeclineClick}
customPosition={customPosition}
position={position}
/>
);
};
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;

View File

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

View File

@@ -18,9 +18,6 @@ const OptionalTermsConfirm = ({ open }) => {
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 }) => {
<TPopUp
kind="introTermsPopup"
open={open}
onClose={handleMainPopupClose}
// onClose={handleMainPopupClose}
spotlightId="optional-terms-test-popup"
className={css.testPopup}
type="none"
// type="fullscreen"
// style={{
// position: 'absolute',
// top: '100px',

View File

@@ -1,17 +1,16 @@
// src/components/Optional/OptionalTermsConfirm.module.less
.testPopup {
width: 958px;
// width: 1920px;
height: 300px;
background-color: white !important;
border-radius: 12px;
box-shadow: 0 20px 70px rgba(2, 3, 3, 0.7) !important;
position: absolute !important;
top: 45% !important;
left: 450px !important;
left: 470px !important;
}
.contentContainer {
// width: 958px;
// height: 310px;

View File

@@ -0,0 +1,185 @@
// src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TNewPopUp from '../TPopUp/TNewPopUp';
import TButton from '../TButton/TButton';
import TCheckBoxSquare from '../TCheckBox/TCheckBoxSquare';
import TButtonScroller from '../TButtonScroller/TButtonScroller';
import { $L, scaleH, scaleW } from '../../utils/helperMethods';
import { setHidePopup } from '../../actions/commonActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions';
import css from './OptionalTermsConfirmBottom.module.less';
import cssPopup from '../TPopUp/TNewPopUp.module.less';
import Spotlight from "@enact/spotlight";
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 (
<TNewPopUp
kind="introTermsPopup"
open
onClose={handleTermsPopupClosed}
onClick={handleCloseTermsPopup}
onIntroTermsAgreeClick={handleAgreeTest}
hasButton
button1Text={$L("OK")}
spotlightId="terms-viewer-popup"
>
{optionalTermsData && (
<div className={css.termsViewerContent}>
<div className={css.termsViewerTitle}>{$L("Optional Terms")}</div>
<TButtonScroller
boxHeight={scaleH(300)}
width={scaleW(980)}
className={css.termsDescription}
>
<div
className={css.termsDesc}
dangerouslySetInnerHTML={{
__html: optionalTermsData.trmsCntt,
}}
/>
</TButtonScroller>
</div>
)}
</TNewPopUp>
);
}
if (isWarningPopupVisible) {
return (
<TNewPopUp
kind="textPopup"
open
onClose={handleCloseWarningPopup}
hasButton
button1Text={$L("OK")}
hasText
title={$L("Agreement Required")}
text={$L("Please agree to the Optional Terms.")}
spotlightId="warning-popup"
/>
);
}
return (
<TNewPopUp
kind="optionalConfirm"
open={open}
spotlightId="optional-terms-confirm-popup"
spotlightRestrict="self-only"
className={css.optionalConfirmPopup}
onOptionalTermsClick={handleViewTermsClick}
onOptionalAgreeClick={handleAgree}
onOptionalDeclineClick={handleDontAskAgain}
customPosition={true}
position={{
position: 'absolute',
top: '342px', // 가운데를 기준으로 한 좌표 (1080/2) - 198
left: '0px',
bottom: 'unset',
transform: 'none',
}}
/>
);
};
export default OptionalTermsConfirm;

View File

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

View File

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

View File

@@ -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,17 +445,53 @@ export default function TNewPopUp({
[optionClick]
);
// 핸들러 함수들 추가
const _onOptionalTermsClick = useCallback(() => {
if (onOptionalTermsClick) {
onOptionalTermsClick();
}
}, [onOptionalTermsClick]);
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 (
<Alert
{...rest}
@@ -372,11 +499,13 @@ export default function TNewPopUp({
open={open}
className={classNames(css.tNewPopUp, css[kind], className)}
onClose={onClose}
style={alertStyle}
>
<Container
className={getClassName(kind, "info")}
spotlightId={SpotlightIds.TPOPUP}
spotlightDisabled={rest.spotlightDisabled}
style={customPosition ? customStyle : undefined} // 👈 커스텀 스타일 적용
>
{hasOnClose && (
<TButton
@@ -477,8 +606,10 @@ export default function TNewPopUp({
</div>
)}
{/* 다른 종류들의 children */}
{kind !== "optionalAgreement" && children}
{kind !== "optionalAgreement" && kind !== "optionalConfirm" && children}
{hasIndicator && (
<>
@@ -502,6 +633,17 @@ export default function TNewPopUp({
)}
{shouldShowButtons && (
<ButtonContainerComp className={getClassName(kind, "buttonContainer")}>
{/* introTermsPopup일 때 Agree 버튼 먼저 표시 */}
{kind === "introTermsPopup" && (
<TButton
spotlightId="introTermsAgreeBtn"
onClick={_onIntroTermsAgreeClick}
role="button"
ariaLabel="Agree"
>
Agree
</TButton>
)}
{finalButton1Text && (
<TButton
spotlightId="tPopupBtn1"
@@ -525,6 +667,51 @@ export default function TNewPopUp({
)}
</ButtonContainerComp>
)}
{kind === "optionalConfirm" && (
<div className={getClassName(kind, "optionalConfirmContentContainer")}>
<div className={getClassName(kind, "optionalConfirmTextSection")}>
Get recommendations, special offers, and ads tailored just for you.
</div>
<OptionalConfirmButtonSection className={getClassName(kind, "optionalConfirmButtonSection")}>
<div className={getClassName(kind, "optionalConfirmLeftButtonSection")}>
<TButton
className={css.optionalTermsButton}
onClick={_onOptionalTermsClick}
spotlightId="optionalConfirmTermsBtn"
type="terms"
ariaLabel="Optional Terms"
size="small"
>
<div className={css.optionalTermsTitle}>Optional Terms</div>
</TButton>
</div>
<div className={getClassName(kind, "optionalConfirmRightButtonSection")}>
<TButton
className={getClassName(kind, "optionalConfirmButton")}
onClick={_onOptionalAgreeClick}
role="button"
ariaLabel="Agree"
spotlightId="optionalConfirmAgreeBtn"
size="small"
>
Agree
</TButton>
<TButton
className={getClassName(kind, "optionalConfirmButton")}
onClick={_onOptionalDeclineClick}
role="button"
ariaLabel="Not Now"
spotlightId="optionalConfirmDeclineBtn"
size="small"
>
Not Now
</TButton>
</div>
</OptionalConfirmButtonSection>
</div>
)}
</Container>
</Alert>
);

View File

@@ -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 @@
}
}
}
&.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;
// }
// }

View File

@@ -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);
// 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) {
// 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(spotlightId);
}, 0);
console.log("focusTarget", focusTarget);
Spotlight.focus(focusTarget);
}, 200);
return () => {
clearTimeout(timerId);
};
}
const timerId = setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 0);
return () => {
clearTimeout(timerId);
};
}
}, [spotlightId, popupVisible]);
}, [popupVisible, spotlightId, kind, contentType]);
useEffect(() => {
if (KINDS.indexOf(kind) < 0) {
@@ -123,10 +165,24 @@ 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 (
<div className={css.introTermsConts}>
<TButtonTab
className={css.tab}
selectedIndex={0}
contents={[getTermsTitle(termsData.trmsTpCd)]}
spotlightDisabled={true}
role="button"
/>
<TButtonScroller
boxHeight={scaleH(300)}
width={scaleW(980)}
className={css.termsDescription}
>
<div
className={css.termsDesc}
dangerouslySetInnerHTML={{
__html: termsData.trmsCntt,
}}
/>
</TButtonScroller>
</div>
);
}, [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 (
<Alert
open={open}
@@ -331,7 +469,7 @@ export default function TPopUp({
})}
</OptionContainer>
)}
{children}
{renderContent}
{hasIndicator && (
<>
{currentPage !== 0 && (

View File

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

View File

@@ -1,3 +1,5 @@
// src/reducers/commonReducer.js
import { types } from "../actions/actionTypes";
import { LOG_MENU } from "../utils/Config";
@@ -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 {

View File

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

View File

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

View File

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

View File

@@ -403,6 +403,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
(containerId) => setFocusedContainerId(containerId),
[]
);
const renderPageItem = useCallback(() => {
return (
<>

View File

@@ -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,6 +304,7 @@ export default function HomeBanner({
}, [selectTemplate, renderItem]);
return (
<>
<Container
className={css.container}
spotlightId={spotlightId}
@@ -194,5 +312,50 @@ export default function HomeBanner({
>
<div className={css.homeTemplateBox}>{renderLayout()}</div>
</Container>
{/* 선택약관 동의 팝업 */}
<OptionalConfirm
open={isOptionalConfirmVisible}
spotlightId="optional-confirm-popup"
onOptionalTermsClick={handleOptionalTermsClick}
onOptionalAgreeClick={handleOptionalAgreeClick}
onOptionalDeclineClick={handleOptionalDeclineClick}
customPosition={true}
position={{
position: 'absolute',
top: '342px', // 가운데를 기준으로 한 좌표 (1080/2) - 198
left: '0px',
bottom: 'unset',
transform: 'none',
}}
/>
{/* 선택약관 자세히 보기 팝업 */}
<TNewPopUp
kind="introTermsPopup"
open={isOptionalTermsVisible}
// onClose={handleTermsPopupClosed}
onClick={handleTermsPopupClosed}
onIntroTermsAgreeClick={handleTermsPopupAgree}
hasButton
button1Text={$L("Close")}
>
{optionalTermsData && (
<div className={css.termsViewerContent}>
<div className={css.termsViewerTitle}>{$L("Optional Terms")}</div>
<TButtonScroller
boxHeight={scaleH(300)}
width={scaleW(980)}
className={css.termsDescription}
>
<div
className={css.termsDesc}
dangerouslySetInnerHTML={{
__html: optionalTermsData.trmsCntt,
}}
/>
</TButtonScroller>
</div>
)}
</TNewPopUp>
</>
);
}

View File

@@ -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 = [];
@@ -206,14 +292,32 @@ export default function IntroPanel({
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(() => {
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 */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
className={css.customeCheckbox}
selected={termsChecked}
onToggle={handleTermsToggle}
spotlightId="termsCheckbox"
@@ -360,7 +464,7 @@ export default function IntroPanel({
{/* Privacy Policy */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
className={css.customeCheckbox}
selected={privacyChecked}
onToggle={handlePrivacyToggle}
spotlightId="privacyCheckbox"
@@ -382,7 +486,7 @@ export default function IntroPanel({
{/* Optional Terms */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
className={css.customeCheckbox}
selected={optionalChecked}
onToggle={handleOptionalToggle}
spotlightId="optionalCheckbox"
@@ -456,43 +560,20 @@ export default function IntroPanel({
</Container>
</TPanel>
{/* TERMS */}
{/* 약관 보기 팝업 */}
{activePopup === Config.ACTIVE_POPUP.termsPopup && (
<TPopUp
kind="introTermsPopup"
contentType={CONTENT_TYPES.TERMS} // 추가
open={popupVisible}
onClose={onClose}
onTermsAgree={handleTermsAgree} // onClick에서 변경
termsData={currentTerms} // 추가
hasButton
button1Text={$L("OK")}
>
{currentTerms && (
<div className={css.introTermsConts}>
<TButtonTab
className={css.tab}
selectedIndex={0}
contents={
currentTerms.trmsTpCd === "MST00401"
? [$L("Privacy Policy")]
: [$L("Terms & Conditions")]
}
spotlightDisabled={true}
role="button"
button1Text={$L("Agree")}
button2Text={$L("Close")}
spotlightId="tPopupBtn1"
/>
<TButtonScroller
boxHeight={scaleH(300)}
width={scaleW(980)}
className={css.termsDescription}
>
<div
className={css.termsDesc}
dangerouslySetInnerHTML={{
__html: currentTerms && currentTerms.trmsCntt,
}}
/>
</TButtonScroller>
</div>
)}
</TPopUp>
)} {/* DO NOT AGREE */}
{activePopup === Config.ACTIVE_POPUP.exitPopup && (
<TPopUp
@@ -520,6 +601,17 @@ export default function IntroPanel({
text={$L("Please agree to Terms & Conditions and Privacy Policy.")}
/>
)}
{/* Final Exit Message Popup */}
{showExitMessagePopup && (
<TPopUp
kind="textPopup"
open={showExitMessagePopup}
hasText
title={$L("Exit Shop Time")}
text={$L("Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds.")}
/>
)}
</Region>
);
}

View File

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

View File

@@ -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 && (
<OptionalTermsConfirm open={popupVisible} />
)}
{activePopup === Config.ACTIVE_POPUP.optionalTermsConfirmBottom && (
<OptionalTermsConfirmBottom open={popupVisible} />
)}
{/* {activePopup === Config.ACTIVE_POPUP.optionalConfirm && (
<TNewPopUp
kind="optionalConfirm"
open={true}
text="Get recommendations, special offers, and ads tailored just for you."
button1Text="Agree"
button2Text="Not Now"
onClose={handleClose}
onClick={handleAgree}
/>
)} */}
</div>
);
}

View File

@@ -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";
@@ -65,7 +65,7 @@ export default function TermsOfService({ title, cbScrollTo }) {
const focusJob = useRef(null);
useEffect(() => {
dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] }));
dispatch(fetchCurrentUserHomeTerms());
}, [dispatch]);
useEffect(() => {
@@ -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?")}
/>
{/* 필수약관 체크 확인 팝업 */}
<TPopUp
kind="textPopup"
open={showCheckboxAlert}

View File

@@ -81,7 +81,7 @@
.agreeButton {
width: 240px;
height: 80px;
background: #c70850;
background: #777d8a;
box-shadow: 0px 0px 50px rgba(0, 0, 0, 0.5);
border-radius: 12px;
color: white;