merge: OptionalTerms 250613

This commit is contained in:
djaco
2025-06-13 15:41:51 +09:00
parent 79f0648b7d
commit 2e19c8e131
41 changed files with 6973 additions and 89 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@@ -21,6 +21,8 @@ import {
setDeepLink, setDeepLink,
setGNBMenu, setGNBMenu,
setSecondLayerInfo, setSecondLayerInfo,
setShowPopup,
getTermsAgreeYn,
} from "../actions/commonActions"; } from "../actions/commonActions";
import { getShoptimeTerms } from "../actions/empActions"; import { getShoptimeTerms } from "../actions/empActions";
import { getHomeMenu, getHomeTerms } from "../actions/homeActions"; import { getHomeMenu, getHomeTerms } from "../actions/homeActions";
@@ -119,6 +121,25 @@ function AppBase(props) {
(state) => state.common.appStatus.cursorVisible (state) => state.common.appStatus.cursorVisible
); );
const introTermsAgree = useSelector((state) => state.common.introTermsAgree); const introTermsAgree = useSelector((state) => state.common.introTermsAgree);
const optionalTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
// 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]);
useEffect(() => {
if (termsData?.data?.terms) {
dispatch(getTermsAgreeYn());
}
}, [termsData, dispatch]);
const introTermsAgreeRef = usePrevious(introTermsAgree); const introTermsAgreeRef = usePrevious(introTermsAgree);
const logEnable = useSelector((state) => state.localSettings.logEnable); const logEnable = useSelector((state) => state.localSettings.logEnable);
@@ -326,7 +347,7 @@ function AppBase(props) {
dispatch( dispatch(
getHomeTerms({ getHomeTerms({
mbrNo: loginUserData.userNumber, mbrNo: loginUserData.userNumber,
trmsTpCdList: "MST00401, MST00402", trmsTpCdList: "MST00401, MST00402, MST00405", // 선택약관 추가 25.06
}) })
); );
@@ -347,19 +368,38 @@ function AppBase(props) {
} }
}, [webOSVersion, deviceId]); }, [webOSVersion, deviceId]);
// useEffect(() => {
// setTimeout(() => {
// console.log("App.js optionalTermsTest 팝업 표시");
// dispatch(setShowPopup({ activePopup: "optionalTermsTest" }));
// }, 3000);
// }, [dispatch]);
// 약관 동의 및 선택 약관 팝업 처리
useEffect(() => { useEffect(() => {
console.log("App.js introTermsAgree", introTermsAgree); if (introTermsAgree === undefined) {
if (introTermsAgree !== undefined) { // 약관 동의 여부 확인 전에는 아무것도 하지 않음
if (introTermsAgree) { return;
initService(true);
} else {
dispatch(
pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} })
);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
} }
}, [introTermsAgree, dispatch]);
if (introTermsAgree) {
// 필수 약관에 동의한 경우
if (shouldShowOptionalTermsPopup) {
// 선택 약관 팝업을 띄워야 하는 경우
dispatch(setShowPopup({ activePopup: "optionalTermsTest" }));
} else {
// 선택 약관 팝업이 필요 없는 경우, 바로 서비스 초기화
initService(true);
}
} else {
// 필수 약관에 동의하지 않은 경우
dispatch(
pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} })
);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
}, [introTermsAgree, shouldShowOptionalTermsPopup, dispatch, initService]);
useEffect(() => { useEffect(() => {
const launchParmas = getLaunchParams(); const launchParmas = getLaunchParams();

View File

@@ -17,14 +17,17 @@ export const types = {
CHANGE_APP_STATUS: "CHANGE_APP_STATUS", CHANGE_APP_STATUS: "CHANGE_APP_STATUS",
SEND_BROADCAST: "SEND_BROADCAST", SEND_BROADCAST: "SEND_BROADCAST",
CHANGE_LOCAL_SETTINGS: "CHANGE_LOCAL_SETTINGS", CHANGE_LOCAL_SETTINGS: "CHANGE_LOCAL_SETTINGS",
GNB_OPENED: "GNB_OPENED", GNB_OPENED: "GNB_OPENED", SET_SHOW_POPUP: "SET_SHOW_POPUP",
SET_SHOW_POPUP: "SET_SHOW_POPUP",
SET_SHOW_SECONDARY_POPUP: "SET_SHOW_SECONDARY_POPUP", SET_SHOW_SECONDARY_POPUP: "SET_SHOW_SECONDARY_POPUP",
SET_HIDE_POPUP: "SET_HIDE_POPUP", SET_HIDE_POPUP: "SET_HIDE_POPUP",
SET_HIDE_SECONDARY_POPUP: "SET_HIDE_SECONDARY_POPUP", SET_HIDE_SECONDARY_POPUP: "SET_HIDE_SECONDARY_POPUP",
SHOW_OPTIONAL_TERMS_CONFIRM_POPUP: "SHOW_OPTIONAL_TERMS_CONFIRM_POPUP",
HIDE_OPTIONAL_TERMS_CONFIRM_POPUP: "HIDE_OPTIONAL_TERMS_CONFIRM_POPUP",
TOGGLE_OPTIONAL_TERMS_CONFIRM: "TOGGLE_OPTIONAL_TERMS_CONFIRM",
SET_EXIT_APP: "SET_EXIT_APP", SET_EXIT_APP: "SET_EXIT_APP",
GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA", GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA",
GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN", GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN",
LAUNCH_MEMBERSHIP_APP: "LAUNCH_MEMBERSHIP_APP",
SET_FOCUS: "SET_FOCUS", SET_FOCUS: "SET_FOCUS",
SET_GNB_MENU: "SET_GNB_MENU", SET_GNB_MENU: "SET_GNB_MENU",
SET_SYSTEM_NOTICE: "SET_SYSTEM_NOTICE", SET_SYSTEM_NOTICE: "SET_SYSTEM_NOTICE",
@@ -120,6 +123,9 @@ export const types = {
GET_MY_RECENTLY_VIEWED_INFO: "GET_MY_RECENTLY_VIEWED_INFO", GET_MY_RECENTLY_VIEWED_INFO: "GET_MY_RECENTLY_VIEWED_INFO",
CLEAR_RECENTLY_VIEWED_INFO: "CLEAR_RECENTLY_VIEWED_INFO", CLEAR_RECENTLY_VIEWED_INFO: "CLEAR_RECENTLY_VIEWED_INFO",
CLEAR_FAVORITES: "CLEAR_FAVORITES", CLEAR_FAVORITES: "CLEAR_FAVORITES",
SET_MYPAGE_TERMS_AGREE: "SET_MYPAGE_TERMS_AGREE",
SET_MYPAGE_TERMS_AGREE_SUCCESS: "SET_MYPAGE_TERMS_AGREE_SUCCESS",
SET_MYPAGE_TERMS_AGREE_FAIL: "SET_MYPAGE_TERMS_AGREE_FAIL",
// onSale actions // onSale actions
GET_HOME_ON_SALE_INFO: "GET_HOME_ON_SALE_INFO", GET_HOME_ON_SALE_INFO: "GET_HOME_ON_SALE_INFO",
@@ -200,4 +206,7 @@ export const types = {
// pinCode actions // pinCode actions
GET_MY_INFO_CARD_PINCODE_CHECK: "GET_MY_INFO_CARD_PINCODE_CHECK", GET_MY_INFO_CARD_PINCODE_CHECK: "GET_MY_INFO_CARD_PINCODE_CHECK",
// new actions
CANCEL_FOCUS_ELEMENT: "CANCEL_FOCUS_ELEMENT",
}; };

View File

@@ -29,10 +29,13 @@ export const gnbOpened = (status) => ({
payload: status, payload: status,
}); });
export const setShowPopup = (popupType, payload = {}) => ({ export const setShowPopup = (config) => {
type: types.SET_SHOW_POPUP, const payload = typeof config === 'string' ? { activePopup: config } : config;
payload: { activePopup: popupType, ...payload }, return {
}); type: types.SET_SHOW_POPUP,
payload,
};
};
export const setShowSecondaryPopup = (popupType, payload = {}) => ({ export const setShowSecondaryPopup = (popupType, payload = {}) => ({
type: types.SET_SHOW_SECONDARY_POPUP, type: types.SET_SHOW_SECONDARY_POPUP,
@@ -47,6 +50,19 @@ export const setHideSecondaryPopup = () => ({
type: types.SET_HIDE_SECONDARY_POPUP, type: types.SET_HIDE_SECONDARY_POPUP,
}); });
export const showOptionalTermsConfirmPopup = () => ({
type: types.SHOW_OPTIONAL_TERMS_CONFIRM_POPUP,
});
export const hideOptionalTermsConfirmPopup = () => ({
type: types.HIDE_OPTIONAL_TERMS_CONFIRM_POPUP,
});
export const toggleOptionalTermsConfirm = (selected) => ({
type: types.TOGGLE_OPTIONAL_TERMS_CONFIRM,
payload: selected,
});
export const setExitApp = () => (dispatch, getState) => { export const setExitApp = () => (dispatch, getState) => {
dispatch({ type: types.SET_EXIT_APP }); dispatch({ type: types.SET_EXIT_APP });
@@ -272,6 +288,13 @@ export const getDeviceId = (onComplete) => (dispatch, getState) => {
export const getTermsAgreeYn = () => (dispatch, getState) => { export const getTermsAgreeYn = () => (dispatch, getState) => {
const { terms } = getState().home.termsData.data; 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
})));
const termsAgreeFlag = terms.reduce((acc, term) => { const termsAgreeFlag = terms.reduce((acc, term) => {
switch (term.trmsTpCd) { switch (term.trmsTpCd) {
case "MST00401": case "MST00401":
@@ -290,6 +313,10 @@ export const getTermsAgreeYn = () => (dispatch, getState) => {
acc.paymentTerms = term.trmsAgrFlag; acc.paymentTerms = term.trmsAgrFlag;
break; break;
case "MST00405":
acc.optionalTerms = term.trmsAgrFlag;
break;
default: default:
break; break;
} }

View File

@@ -211,6 +211,43 @@ export const setMyTermsWithdraw =
); );
}; };
// MyPage 약관 동의 (IF-LGSP-031)
export const setMyPageTermsAgree =
(params, callback) => (dispatch, getState) => {
const { termsList, notTermsList } = params;
const onSuccess = (response) => {
console.log("setMyPageTermsAgree onSuccess ", response.data);
dispatch({
type: types.SET_MYPAGE_TERMS_AGREE_SUCCESS,
payload: response.data,
retCode: response.data.retCode,
});
if (callback) callback(response.data);
};
const onFail = (error) => {
console.error("setMyPageTermsAgree onFail ", error);
dispatch({
type: types.SET_MYPAGE_TERMS_AGREE_FAIL,
payload: error,
});
};
TAxios(
dispatch,
getState,
"post",
URLS.SET_MYPAGE_TERMS_AGREE,
{},
{ termsList, notTermsList },
onSuccess,
onFail
);
};
// MyPage Upcoming Alert 정보 변경 조회 (IF-LGSP-050) // MyPage Upcoming Alert 정보 변경 조회 (IF-LGSP-050)
export const getMyUpcomingChangeInfo = () => (dispatch, getState) => { export const getMyUpcomingChangeInfo = () => (dispatch, getState) => {
const onSuccess = (response) => { const onSuccess = (response) => {

View File

@@ -128,7 +128,7 @@ export const URLS = {
// emp controller // emp controller
GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge", GET_SHOPTIME_TERMS: "/lgsp/v1/emp/shoptime/terms.lge",
SET_MYPAGE_TERMS_AGREE: "/lgsp/v1/mypage/terms/agree.lge",
// pinCode controller // pinCode controller
GET_MY_INFO_CARD_PINCODE_CHECK: "/lgsp/v1/myinfo/card/pincode/check.lge", GET_MY_INFO_CARD_PINCODE_CHECK: "/lgsp/v1/myinfo/card/pincode/check.lge",

View File

@@ -0,0 +1,106 @@
// src/components/Optional/OptionalTermsConfirm.jsx
import React, { useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import classNames from "classnames";
import TPopUp from "../TPopUp/TPopUp";
import TCheckBoxSquare from "../TCheckBox/TCheckBoxSquare";
import TButton from "../TButton/TButton";
import { $L } from "../../utils/helperMethods";
import css from "./OptionalTermsConfirm.module.less";
export default function OptionalTermsConfirm({
className,
open = false,
onAgree,
onNotNow,
onToggleOptionalTerms,
optionalTermsSelected = false,
spotlightId = "optional-terms-confirm",
onClose,
...rest
}) {
useEffect(() => {
console.log("OptionalTermsConfirm", optionalTermsSelected);
}, [optionalTermsSelected]);
const handleAgree = useCallback(() => {
if (onAgree) {
onAgree();
}
}, [onAgree]);
const handleNotNow = useCallback(() => {
if (onNotNow) {
onNotNow();
}
}, [onNotNow]);
const handleTermsToggle = useCallback(({ selected }) => {
if (onToggleOptionalTerms) {
onToggleOptionalTerms(selected);
}
}, [onToggleOptionalTerms]);
return (
<TPopUp
kind="optionalTermsConfirmPopup"
open={open}
onClose={onClose}
className={classNames(css.optionalTermsPopup, className)}
spotlightId={spotlightId}
hasButton={false}
hasOnClose={false}
type="normal"
{...rest}
>
<div className={css.popupContent}>
<div className={css.description}>
{$L("Get recommendations, special offers, and ads tailored just for you.")}
</div>
<div className={css.rightSection}>
<div className={css.termsSection}>
<div className={css.termsButton}>
<TCheckBoxSquare
className={css.checkbox}
selected={optionalTermsSelected}
onToggle={handleTermsToggle}
spotlightId="optional-terms-checkbox"
ariaLabel={$L("Optional Terms")}
/>
<div className={css.termsText}>
{$L("Optional Terms")}
</div>
</div>
</div>
<div className={css.buttonSection}>
<TButton
className={css.agreeButton}
onClick={handleAgree}
spotlightId="agree-button"
type="agree"
disabled={!optionalTermsSelected}
ariaLabel={$L("Agree")}
>
{$L("Agree")}
</TButton>
<TButton
className={css.notNowButton}
onClick={handleNotNow}
spotlightId="not-now-button"
type="normal"
color="gray"
ariaLabel={$L("Not Now")}
>
{$L("Not Now")}
</TButton>
</div>
</div>
</div>
</TPopUp>
);
}

View File

@@ -0,0 +1,236 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
// Optional Terms Confirm 팝업 전용 전역 스타일 - 화면 하단 위치 조정
:global {
// 팝업이 화면 하단에 나타나도록 조정
.src_components_Optional_OptionalTermsConfirm_optionalTermsPopup {
position: fixed !important;
left: 120px !important; // GNB 너비만큼 오른쪽으로 이동
bottom: 0 !important;
top: auto !important;
right: auto !important;
width: calc(100vw - 120px) !important; // GNB 너비를 제외한 나머지 영역
height: auto !important;
transform: none !important;
margin: 0 !important;
border-radius: 0 !important;
background: #E6EBF0;
box-shadow: 0px 20px 12px rgba(0, 0, 0, 0.30);
}
// 팝업 컨테이너들의 위치 조정
.enact_ui_Transition_Transition_transition.enact_sandstone_Popup_Popup_popupTransitionContainer {
align-items: flex-end !important;
justify-content: flex-start !important;
padding-left: 120px !important; // GNB 너비만큼 패딩
}
}
.optionalTermsPopup {
background: #E6EBF0;
box-shadow: 0px 20px 12px rgba(0, 0, 0, 0.30);
// TPopUp의 기본 스타일을 하단 팝업으로 오버라이드
.popupContent {
width: 100%;
height: 140px;
padding: 25px 140px;
.flex(@alignCenter: center, @justifyCenter: flex-start);
gap: 20px;
.description {
.flex(@alignCenter: center, @justifyCenter: flex-end);
color: black;
font-size: 26px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 400;
word-wrap: break-word;
line-height: 1.3;
flex: 0 0 auto;
}
.rightSection {
flex: 1 1 0;
.flex(@alignCenter: center, @justifyCenter: space-between);
.termsSection {
.flex(@direction: column, @alignCenter: center, @justifyCenter: flex-start);
gap: 10px;
.termsButton {
.flex(@alignCenter: center, @justifyCenter: space-between);
width: 320px;
height: 60px;
padding: 0 30px;
background: white;
border-radius: 6px;
border: 1px solid #CFCFCF;
.checkbox {
width: 27px;
height: 27px;
border-radius: 50%;
border: 1.82px solid black;
}
.termsText {
color: #1A1A1A;
font-size: 22px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
flex: 1;
margin-left: 10px;
}
}
}
.buttonSection {
.flex(@alignCenter: center, @justifyCenter: center);
gap: 12px;
.agreeButton {
width: 160px;
height: 50px;
max-width: 450px;
min-width: 150px;
background: #C70850;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.50);
border-radius: 12px;
> div {
text-align: center;
color: white;
font-size: 20px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
word-wrap: break-word;
}
&:disabled {
background: #777D8A;
box-shadow: none;
> div {
color: #E6E6E6;
}
}
}
.notNowButton {
width: 160px;
height: 50px;
max-width: 450px;
min-width: 150px;
background: #777D8A;
border-radius: 12px;
> div {
text-align: center;
color: #E6E6E6;
font-size: 20px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
word-wrap: break-word;
}
&:hover {
background: #8A8F9C;
}
&:focus {
background: #8A8F9C;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.30);
}
}
}
}
}
}
// 반응형 대응
@media (max-width: 1366px) {
.optionalTermsPopup {
.popupContent {
padding: 20px 100px;
.description {
font-size: 24px;
}
.rightSection {
.termsSection {
.termsButton {
width: 280px;
height: 55px;
.termsText {
font-size: 20px;
}
}
}
.buttonSection {
.agreeButton,
.notNowButton {
width: 140px;
height: 45px;
> div {
font-size: 18px;
}
}
}
}
}
}
}
@media (max-width: 1024px) {
.optionalTermsPopup {
.popupContent {
height: 120px;
padding: 15px 60px;
gap: 15px;
.description {
font-size: 22px;
}
.rightSection {
.termsSection {
.termsButton {
width: 250px;
height: 50px;
padding: 0 20px;
.checkbox {
width: 24px;
height: 24px;
}
.termsText {
font-size: 18px;
line-height: 30px;
}
}
}
.buttonSection {
gap: 8px;
.agreeButton,
.notNowButton {
width: 120px;
height: 40px;
> div {
font-size: 16px;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,204 @@
// src/components/Optional/OptionalTermsConfirmTest.jsx
import React, { useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TPopUp from '../TPopUp/TPopUp';
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 './OptionalTermsConfirmTest.module.less';
const OptionalTermsConfirmTest = ({ open }) => {
const dispatch = useDispatch();
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")
);
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);
}, []);
const handleAgree = useCallback(() => {
if (isChecked) {
// 약관 동의할 항목들 (string array)
const termsList = ["TID0000222", "TID0000223", "TID0000232"];
// 동의하지 않을 항목들 (빈 배열)
const notTermsList = [];
console.log('약관 동의 API 호출 파라미터:', { termsList, notTermsList });
const callback = (response) => {
if (response.retCode === "000" || response.retCode === 0) {
console.log('약관 동의 성공:', response);
} else {
console.error('약관 동의 실패:', response);
}
};
dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback));
dispatch(setHidePopup());
} else {
setIsWarningPopupVisible(true);
}
}, [isChecked, dispatch]);
const handleCloseWarningPopup = useCallback(() => {
setIsWarningPopupVisible(false);
}, []);
const handleDontAskAgain = () => {
console.log("Don't Ask Again 처리 필요");
dispatch(setHidePopup());
};
if (isTermsPopupVisible) {
return (
<TPopUp
kind="introTermsPopup"
open
onClose={handleCloseTermsPopup}
onClick={handleCloseTermsPopup}
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>
)}
</TPopUp>
);
}
if (isWarningPopupVisible) {
return (
<TPopUp
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 (
<TPopUp
kind="introTermsPopup"
open={open}
onClose={handleMainPopupClose}
spotlightId="optional-terms-test-popup"
className={css.testPopup}
type="none"
style={{
position: 'absolute',
top: '100px',
left: '270px',
zIndex: 9999
}}
>
<div className={css.contentContainer}>
<div className={css.mainContent}>
<div className={css.checkboxSection}>
<div className={css.checkboxArea}>
<TCheckBoxSquare
selected={isChecked}
onToggle={handleCheckboxToggle}
spotlightId="optional-checkbox"
className={css.checkbox}
/>
<TButton
className={css.termBox}
onClick={handleViewTermsClick}
spotlightId="optional-terms-button"
type="terms"
ariaLabel={$L("View Optional Terms")}
>
<div className={css.termTitle}>Optional Terms</div>
</TButton>
</div>
</div>
<div className={css.descriptionSection}>
<div className={css.description}>
Get recommendations, special offers, and ads tailored just for you.
</div>
</div>
<div className={css.buttonSection}>
<div className={css.buttonGroup}>
<TButton
onClick={handleAgree}
spotlightId="agree-button"
className={css.agreeButton}
>
{$L('Agree')}
</TButton>
<TButton
onClick={handleMainPopupClose}
spotlightId="not-now-button"
className={css.notNowButton}
>
{$L('Not Now')}
</TButton>
<TButton
onClick={handleDontAskAgain}
spotlightId="dont-ask-button"
className={css.dontAskButton}
>
{$L("Don't Ask Again")}
</TButton>
</div>
</div>
</div>
</div>
</TPopUp>
);
};
export default OptionalTermsConfirmTest;

View File

@@ -0,0 +1,191 @@
// src/components/Optional/OptionalTermsConfirmTest.module.less
.testPopup {
width: 958px;
height: 310px;
background-color: white !important;
border-radius: 12px;
box-shadow: 0 20px 70px rgba(2, 3, 3, 0.7) !important;
position: absolute !important;
top: 100px !important;
left: 300px !important;
}
.contentContainer {
// width: 958px;
// height: 310px;
padding: 60px 57px 40px 57px;
background: #E6EBF0;
box-shadow: 0px 20px 12px rgba(0, 0, 0, 0.30);
border-radius: 4px;
display: inline-flex;
justify-content: flex-start;
align-items: flex-start;
box-sizing: border-box;
.mainContent {
flex: 1 1 0;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 30px;
.checkboxSection {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 10px;
.checkboxArea {
min-height: 120px;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 20px;
.checkbox {
min-width: 45px;
min-height: 45px;
flex-shrink: 0;
}
// TButton으로 변경되어 포커스 가능
.termBox {
min-width: 530px;
min-height: 120px;
padding-left: 50px !important;
padding-right: 50px !important;
background: white !important;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20);
border-radius: 6px;
outline: 1px #CFCFCF solid;
outline-offset: -1px;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
border: none !important;
// 포커스 스타일 추가
&:focus {
outline: 3px solid #C70850 !important;
outline-offset: 2px !important;
}
.termTitle {
color: #1A1A1A;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
}
.expandIcon {
min-width: 37px;
min-height: 37px;
position: relative;
border-radius: 100px;
outline: 2.5px black solid;
outline-offset: -2.5px;
flex-shrink: 0;
.arrow {
width: 14.66px;
height: 6.99px;
left: 16.01px;
top: 25.83px;
position: absolute;
transform: rotate(-90deg);
transform-origin: top left;
outline: 2.5px black solid;
outline-offset: -1.25px;
}
}
}
}
}
.descriptionSection {
align-self: stretch;
padding-top: 20px;
border-top: 1px #C5C6C9 solid;
display: inline-flex;
justify-content: center;
align-items: center;
.description {
display: flex;
justify-content: center;
flex-direction: column;
color: black;
font-size: 26px;
font-family: 'LG Smart UI';
font-weight: 400;
}
}
.buttonSection {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
.buttonGroup {
align-self: stretch;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 12px;
.agreeButton, .notNowButton, .dontAskButton {
min-width: 300px;
min-height: 80px;
max-width: 450px;
flex: 1;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
> div {
text-align: center;
font-size: 30px;
font-family: 'LG Smart UI';
font-weight: 700;
}
}
.agreeButton {
background: #C70850 !important;
box-shadow: 0px 0px 50px rgba(0, 0, 0, 0.50);
> div {
color: white;
}
}
.notNowButton {
background: #777D8A !important;
> div {
color: #E6E6E6;
}
}
.dontAskButton {
background: rgba(122, 128, 141, 0.30) !important;
> div {
opacity: 0.30;
color: #E6E6E6;
}
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
// src/components/TCheckBox/TCheckBoxSquare.jsx
import React, { useCallback, useState } from "react";
import classNames from "classnames";
import Spottable from "@enact/spotlight/Spottable";
import css from "./TCheckBoxSquare.module.less";
const SpottableComponent = Spottable("div");
export default function TCheckBoxSquare({
className,
spotlightDisabled,
spotlightId,
onFocus,
onToggle,
selected = false, // 기본값을 false로 변경
disabled = false, // disabled prop 추가
ariaLabel,
...rest
}) {
const [focus, setFocus] = useState(false);
const _onClick = useCallback(() => {
// disabled 상태일 때는 클릭 처리하지 않음
if (disabled || !onToggle) {
console.log('[TCheckBoxSquare] 클릭 무시됨 - disabled:', disabled, 'onToggle 존재:', !!onToggle);
return;
}
console.log('[TCheckBoxSquare] 클릭 처리됨 - 현재 selected:', selected, '-> 새로운 값:', !selected);
onToggle({ selected: !selected });
}, [onToggle, selected, disabled]);
const _onFocus = useCallback(() => {
if (disabled) return; // disabled 상태일 때는 포커스 처리하지 않음
setFocus(true);
if (onFocus) {
onFocus();
}
}, [onFocus, disabled]);
const _onBlur = useCallback(() => {
setFocus(false);
}, []);
return (
<SpottableComponent
{...rest}
className={classNames(
className,
css.tCheckBoxSquare,
focus && css.focus,
selected && css.selected,
focus && selected && css.selectedFocus,
disabled && css.disabled // disabled 클래스 추가
)}
spotlightDisabled={spotlightDisabled || disabled} // spotlightDisabled와 disabled 모두 고려
onClick={_onClick}
onFocus={_onFocus}
onBlur={_onBlur}
spotlightId={spotlightId}
aria-label={ariaLabel}
aria-disabled={disabled} // 접근성을 위한 aria-disabled 추가
></SpottableComponent>
);
}

View File

@@ -0,0 +1,67 @@
// src/components/TCheckBox/TCheckBoxSquare.module.less
@SQUARE_BORDER_DEFAULT: #CCCCCC;
@SQUARE_BORDER_ACTIVE: #C70850;
@SQUARE_BG_SELECTED: #7A808D;
.tCheckBoxSquare {
min-width: 45px !important;
min-height: 45px !important;
width: 45px !important;
height: 45px !important;
border: 3px solid @SQUARE_BORDER_DEFAULT !important;
border-radius: 8px !important;
background-color: #fff !important;
position: relative;
box-sizing: border-box;
cursor: pointer;
transition: background 0.15s, border 0.15s !important;
&:hover,
&:focus,
&.focus {
border-color: @SQUARE_BORDER_ACTIVE !important;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18px !important;
height: 10px !important;
border-left: 4px solid #fff !important;
border-bottom: 4px solid #fff !important;
transform: translate(-50%, -70%) rotate(-45deg) scale(0);
border-radius: 0 !important;
transition: transform 0.2s ease-in-out !important;
}
&.selected {
border-color: @SQUARE_BG_SELECTED !important;
background-color: @SQUARE_BG_SELECTED !important;
&::before {
transform: translate(-50%, -70%) rotate(-45deg) scale(1);
}
}
&.selectedFocus {
border-color: @SQUARE_BG_SELECTED !important;
background-color: @SQUARE_BG_SELECTED !important;
box-shadow: 0 0 0 4px fade(@SQUARE_BG_SELECTED, 20%) !important;
&::before {
transform: translate(-50%, -70%) rotate(-45deg) scale(1);
}
}
&.disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
pointer-events: none !important;
&:hover,
&:focus {
border-color: @SQUARE_BORDER_DEFAULT !important;
}
}
}

View File

@@ -56,6 +56,8 @@ const KINDS = [
"orderCancelPopup", "orderCancelPopup",
"cancelConfirmPopup", "cancelConfirmPopup",
"trackPackagePopup", "trackPackagePopup",
"optionalAgreement",
"normal",
]; ];
export default function TPopUp({ export default function TPopUp({
@@ -125,6 +127,30 @@ export default function TPopUp({
return kind === "exitPopup" ? ButtonContainerNegative : ButtonContainer; return kind === "exitPopup" ? ButtonContainerNegative : ButtonContainer;
}, [kind]); }, [kind]);
// optionalAgreement
// 자동으로 Yes/No 버튼 텍스트 설정
const finalButton1Text = useMemo(() => {
if (kind === "optionalAgreement" && !button1Text) {
return "Yes";
}
return button1Text;
}, [kind, button1Text]);
const finalButton2Text = useMemo(() => {
if (kind === "optionalAgreement" && !button2Text) {
return "No";
}
return button2Text;
}, [kind, button2Text]);
// optionalAgreement일 경우 항상 버튼 표시
const shouldShowButtons = useMemo(() => {
return hasButton || kind === "optionalAgreement";
}, [hasButton, kind]);
//-------------------------------------------------------
const _onClick = useCallback( const _onClick = useCallback(
(e) => { (e) => {
if (onClick) { if (onClick) {
@@ -205,7 +231,6 @@ export default function TPopUp({
return true; return true;
} }
}, [httpHeader]); }, [httpHeader]);
return ( return (
<Alert <Alert
open={open} open={open}
@@ -217,6 +242,7 @@ export default function TPopUp({
title && css.title, title && css.title,
text && css.text, text && css.text,
hasButton && css.hasButton, hasButton && css.hasButton,
kind === "optionalTermsConfirmPopup" && css.bottomPopup,
className ? className : null className ? className : null
)} )}
aria-hidden={ariaHidden} aria-hidden={ariaHidden}
@@ -326,27 +352,27 @@ export default function TPopUp({
)} )}
</> </>
)} )}
{hasButton && ( {shouldShowButtons && (
<ButtonContainerComp className={css.buttonContainer}> <ButtonContainerComp className={css.buttonContainer}>
{button1Text && ( {finalButton1Text && (
<TButton <TButton
spotlightId="tPopupBtn1" spotlightId="tPopupBtn1"
onClick={_onClick} onClick={_onClick}
role="button" role="button"
ariaLabel={button1Text} ariaLabel={finalButton1Text}
onSpotlightRight={_onSpotlightRight} onSpotlightRight={_onSpotlightRight}
> >
{button1Text} {finalButton1Text}
</TButton> </TButton>
)} )}
{button2Text && ( {finalButton2Text && (
<TButton <TButton
spotlightId="tPopupBtn2" spotlightId="tPopupBtn2"
onClick={onClose} onClick={onClose}
role="button" role="button"
ariaLabel={button2Text} ariaLabel={finalButton2Text}
> >
{button2Text} {finalButton2Text}
</TButton> </TButton>
)} )}
</ButtonContainerComp> </ButtonContainerComp>

View File

@@ -673,3 +673,71 @@
} }
} }
} }
/* 👇 새로운 팝업 스타일 */
.optionalAgreement {
.default-style();
// 팝업 위치를 정중앙보다 아래로 명시적으로 지정
position: absolute !important;
top: 80% !important; // 50%가 정중앙, 값을 키우면 아래로 이동
left: 50% !important;
transform: translate(-50%, -50%) !important;
margin: 0 !important;
.info {
.size(@w: 1064px, @h: 240px);
padding: 60px 57px 40px; // 상단 60px, 좌우 57px, 하단 40px 패딩 적용
box-sizing: border-box; // 패딩이 너비/높이에 포함되도록 설정
// 컨텐츠 영역
.contentContainer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
// 텍스트 영역 스타일
.textLayer {
flex: 1; // 남은 공간을 모두 차지하도록 설정
min-height: 0; // flex item이 부모를 넘어 수축할 수 있도록 허용
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden; // 내용이 넘칠 경우 숨김
.text {
font-size: 30px;
line-height: 38px;
text-align: center;
color: @COLOR_GRAY03;
}
}
// 버튼 컨테이너 스타일
.buttonContainer {
margin-top: 3%; // gap 대신 margin 사용
display: flex;
justify-content: center;
gap: 12px; // 버튼 간 간격
flex-shrink: 0; // 컨테이너가 줄어들지 않도록 설정
> div {
min-width: 300px;
height: 80px; // 높이 80px로 수정
}
}
}
}
/* 👇 '자물쇠' 추가: kind="optionalTermsConfirmPopup"일 때 적용될 위치 스타일 */
// .bottomPopup {
// top: auto !important;
// bottom: 60px !important; /* 화면 하단에서 60px 위 */
// left: 50% !important;
// transform: translateX(-50%) !important;
// position: fixed !important;
// margin: 0 !important;
// }

View File

@@ -18,8 +18,7 @@ const initialState = {
}, },
broadcast: {}, broadcast: {},
httpHeader: null, httpHeader: null,
isGnbOpened: false, isGnbOpened: false, popup: {
popup: {
popupVisible: false, popupVisible: false,
activePopup: null, activePopup: null,
secondaryPopup: null, secondaryPopup: null,
@@ -28,9 +27,10 @@ const initialState = {
secondaryData: {}, secondaryData: {},
text: null, text: null,
data: {}, data: {},
optionalTermsConfirmSelected: false,
}, },
termsFlag: null, termsFlag: null,
introTermsAgree: undefined, introTermsAgree: undefined, // Y, N
checkoutTermsAgree: undefined, checkoutTermsAgree: undefined,
useLog: true, useLog: true,
useMockup: true, useMockup: true,
@@ -70,6 +70,7 @@ const initialState = {
secondLayerInfo: {}, secondLayerInfo: {},
macAddress: { wifi: "", wired: "", p2p: "" }, macAddress: { wifi: "", wired: "", p2p: "" },
connectionFailed: false,
}; };
export const commonReducer = (state = initialState, action) => { export const commonReducer = (state = initialState, action) => {
@@ -132,11 +133,9 @@ export const commonReducer = (state = initialState, action) => {
return { return {
...state, ...state,
popup: { popup: {
...state.popup,
popupVisible: true, popupVisible: true,
activePopup: action.payload.activePopup, ...action.payload,
secondaryPopup: action.payload.secondaryPopup,
text: action.payload.text,
data: action.payload.data,
}, },
}; };
case types.SET_SHOW_SECONDARY_POPUP: case types.SET_SHOW_SECONDARY_POPUP:
@@ -162,8 +161,7 @@ export const commonReducer = (state = initialState, action) => {
secondaryPopupVisible: false, secondaryPopupVisible: false,
secondaryPopup: null, secondaryPopup: null,
}, },
}; }; case types.SET_HIDE_SECONDARY_POPUP:
case types.SET_HIDE_SECONDARY_POPUP:
return { return {
...state, ...state,
popup: { popup: {
@@ -172,15 +170,43 @@ export const commonReducer = (state = initialState, action) => {
secondaryPopup: null, secondaryPopup: null,
}, },
}; };
case types.SHOW_OPTIONAL_TERMS_CONFIRM_POPUP:
return {
...state,
popup: {
...state.popup,
popupVisible: true,
activePopup: "optionalTermsConfirmPopup",
},
};
case types.HIDE_OPTIONAL_TERMS_CONFIRM_POPUP:
return {
...state,
popup: {
...state.popup,
popupVisible: false,
activePopup: null,
optionalTermsConfirmSelected: false,
},
};
case types.TOGGLE_OPTIONAL_TERMS_CONFIRM:
return {
...state,
popup: {
...state.popup,
optionalTermsConfirmSelected: action.payload,
},
};
case types.SET_EXIT_APP: case types.SET_EXIT_APP:
return state; return state;
case types.GET_TERMS_AGREE_YN: { case types.GET_TERMS_AGREE_YN: {
const { privacyTerms, serviceTerms, purchaseTerms, paymentTerms } = const { privacyTerms, serviceTerms, purchaseTerms, paymentTerms,optionalTerms } =
action.payload; action.payload;
const introTermsAgree = privacyTerms === "Y" && serviceTerms === "Y"; const introTermsAgree = privacyTerms === "Y" && serviceTerms === "Y";
const checkoutTermsAgree = purchaseTerms === "Y" && paymentTerms === "Y"; const checkoutTermsAgree = purchaseTerms === "Y" && paymentTerms === "Y";
const optionalTermsAgree = optionalTerms == "Y" ;
return { return {
...state, ...state,
@@ -189,6 +215,7 @@ export const commonReducer = (state = initialState, action) => {
}, },
introTermsAgree, introTermsAgree,
checkoutTermsAgree, checkoutTermsAgree,
optionalTermsAgree,
}; };
} }
case types.REGISTER_DEVICE: { case types.REGISTER_DEVICE: {

View File

@@ -18,6 +18,7 @@ const initialState = {
recentlyViewedData: {}, recentlyViewedData: {},
recentlyViewedSuccess: null, recentlyViewedSuccess: null,
favoriteRetCode: null, favoriteRetCode: null,
setMyPageTermsAgreeResult: { data: null, retCode: null, error: null },
}; };
export const myPageReducer = (state = initialState, action) => { export const myPageReducer = (state = initialState, action) => {
@@ -139,6 +140,26 @@ export const myPageReducer = (state = initialState, action) => {
}, },
}; };
case types.SET_MYPAGE_TERMS_AGREE_SUCCESS:
return {
...state,
setMyPageTermsAgreeResult: {
data: action.payload,
retCode: action.retCode,
error: null,
},
};
case types.SET_MYPAGE_TERMS_AGREE_FAIL:
return {
...state,
setMyPageTermsAgreeResult: {
data: null,
retCode: null,
error: action.payload,
},
};
default: default:
return state; return state;
} }

View File

@@ -29,6 +29,7 @@ export const panel_names = {
THEME_CURATION_PANEL: "themeCurationPanel", THEME_CURATION_PANEL: "themeCurationPanel",
IMAGE_PANEL: "imagepanel", IMAGE_PANEL: "imagepanel",
SERVICE_UNAVAILABLE: "servicepanel", SERVICE_UNAVAILABLE: "servicepanel",
OPTIONAL_TERMS_PANEL: "optionaltermspanel", // 선택약관 Intro Panel
// error // error
ERROR_PANEL: "errorpanel", ERROR_PANEL: "errorpanel",
@@ -76,13 +77,16 @@ export const ACTIVE_POPUP = {
orderDetailPopup: "orderDetailPopup", orderDetailPopup: "orderDetailPopup",
orderDetailCancel: "orderDetailCancel", orderDetailCancel: "orderDetailCancel",
errorPopup: "errorPopup", errorPopup: "errorPopup",
returnExchangePopup: "returnExchangePopup", returnExchangePopup: "returnExchangePopup", trackPackagePopup: "trackPackagePopup",
trackPackagePopup: "trackPackagePopup",
unSupportedCountryPopup: "unSupportedCountryPopup", unSupportedCountryPopup: "unSupportedCountryPopup",
changeCountyPopup: "changeCounty", changeCountyPopup: "changeCounty",
networkErrorPopup: "networkErrorPopup", networkErrorPopup: "networkErrorPopup",
endOfServicePopup: "endOfServicePopup", endOfServicePopup: "endOfServicePopup",
checkoutErrorPopup: "checkoutErrorPopup", checkoutErrorPopup: "checkoutErrorPopup",
optionalTermsConfirmPopup: "optionalTermsConfirmPopup",
optionalTermsTest: "optionalTermsTest",
introTermsPopup: "introTermsPopup",
toast: "toast",
}; };
export const DEBUG_VIDEO_SUBTITLE_TEST = false; export const DEBUG_VIDEO_SUBTITLE_TEST = false;
export const AUTO_SCROLL_DELAY = 600; export const AUTO_SCROLL_DELAY = 600;

View File

@@ -108,10 +108,6 @@ const hasTemplateCodeWithValue = (array, value) =>
array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false; array?.some((obj) => obj?.shptmBrndOptTpCd === value) ?? false;
const shouldRenderComponent = (data) => { const shouldRenderComponent = (data) => {
if (data === null) {
return;
}
return ( return (
(Array.isArray(data) && data.length > 0) || (Array.isArray(data) && data.length > 0) ||
(typeof data === "object" && Object.keys(data).length > 0) (typeof data === "object" && Object.keys(data).length > 0)

View File

@@ -0,0 +1,525 @@
// src: views/IntroPanel/IntroPanel.new.jsx
import React, { useCallback, useEffect, useState,useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import Region from "@enact/sandstone/Region";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import {
setExitApp,
setHidePopup,
setShowPopup,
setTermsAgreeYn,
} from "../../actions/commonActions";
import { registerDevice } from "../../actions/deviceActions";
import { getWelcomeEventInfo } from "../../actions/eventActions";
import { getHomeTerms } from "../../actions/homeActions";
import {
sendLogGNB,
sendLogTerms,
sendLogTotalRecommend,
} from "../../actions/logActions";
import { popPanel, pushPanel } from "../../actions/panelActions";
import TButton, { TYPES } from "../../components/TButton/TButton";
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 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";
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
export default function IntroPanel({
children,
isTabActivated,
handleCancel,
spotlightId,
...rest
}) {
delete rest.isTabActivated;
delete rest.panelInfo;
useDebugKey({});
const dispatch = useDispatch(); const termsData = useSelector((state) => state.home.termsData);
const { popupVisible, activePopup, ...popupState } = useSelector(
(state) => state.common.popup
);
const eventInfos = useSelector((state) => state.event.eventData);
const regDeviceData = useSelector((state) => state.device.regDeviceData);
const regDeviceInfoData = useSelector(
(state) => state.device.regDeviceInfoData
);
// const introTermsData = termsData?.data?.terms.filter(
// (item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402"
// );
// const optionalTermsData = termsData?.data?.terms.filter(
// (item) => item.trmsTpCd === "MST00405"
// );
const introTermsData = useMemo(() => {
return termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402"
) || [];
}, [termsData]);
const optionalTermsData = useMemo(() => {
return termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00405"
) || [];
}, [termsData]);
const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion
);
// WebOS 버전별 UI 표시 모드 결정
// 이미지 표시: 4.0, 5.0, 23, 24
// 텍스트 표시: 4.5, 6.0, 22
const shouldShowBenefitsView = useMemo(() => {
if (!webOSVersion) return false;
const version = String(webOSVersion);
// 텍스트 표시 버전들
const textVersions = ['4.5', '6.0', '22'];
// 이미지 표시 버전들
const imageVersions = ['4.0', '5.0', '23', '24'];
// 텍스트 버전인지 확인
const shouldShowText = textVersions.includes(version);
console.log('🔍 WebOS 버전별 UI 모드:');
console.log(' - webOSVersion:', version);
console.log(' - shouldShowText (텍스트 모드):', shouldShowText);
console.log(' - 텍스트 버전들:', textVersions);
console.log(' - 이미지 버전들:', imageVersions);
return shouldShowText;
}, [webOSVersion]);
// 상태 관리
const [currentTerms, setCurrentTerms] = useState(null);
const [termsChecked, setTermsChecked] = useState(true); // Terms & Conditions 기본 체크
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));
}, []);
// 디버깅용 WebOS 버전 로그
useEffect(() => {
console.log('🔍 IntroPanel WebOS 버전 정보:');
console.log(' - webOSVersion:', webOSVersion);
console.log(' - shouldShowBenefitsView:', shouldShowBenefitsView);
}, [webOSVersion, shouldShowBenefitsView]);
// Select All 상태 업데이트
useEffect(() => {
setSelectAllChecked(termsChecked && privacyChecked && optionalChecked);
}, [termsChecked, privacyChecked, optionalChecked]);
const handleTermsClick = useCallback(
(trmsTpCdList) => {
if (introTermsData) {
const selectedTerms = introTermsData.find(
(term) => term.trmsTpCd === trmsTpCdList
);
setCurrentTerms(selectedTerms);
dispatch(setShowPopup(Config.ACTIVE_POPUP.termsPopup));
const logTpNo =
trmsTpCdList === "MST00402"
? Config.LOG_TP_NO.TERMS.TEARMS_CONDITIONS
: Config.LOG_TP_NO.TERMS.PRIVACY_POLICY;
dispatch(sendLogTerms({ logTpNo }));
}
},
[introTermsData, dispatch]
);
const handleOptionalTermsClick = useCallback(
(trmsTpCdList) => {
if (optionalTermsData) {
const selectedTerms = optionalTermsData.find(
(term) => term.trmsTpCd === trmsTpCdList
);
setCurrentTerms(selectedTerms);
dispatch(setShowPopup(Config.ACTIVE_POPUP.termsPopup));
// const logTpNo =
// trmsTpCdList === "MST00402"
// ? Config.LOG_TP_NO.TERMS.TEARMS_CONDITIONS
// : Config.LOG_TP_NO.TERMS.PRIVACY_POLICY;
// dispatch(sendLogTerms({ logTpNo }));
}
},
[optionalTermsData, dispatch]
);
const onClose = useCallback(() => {
dispatch(setHidePopup());
}, [dispatch]); const handleAgree = useCallback(() => {
// 필수 약관이 체크되어 있는지 확인
if (!termsChecked || !privacyChecked) {
// 필수 약관이 체크되지 않았을 때 알림
// window.alert($L("Please agree to Terms & Conditions and Privacy Policy."));
dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Required Terms"),
text: $L("Please agree to Terms & Conditions and Privacy Policy."),
button1Text: $L("OK")
}));
return;
}
// 약관 ID 정확하게 매핑
const agreeTerms = [];
if (termsChecked) {
agreeTerms.push("TID0000222"); // MST00402 -> TID0000222 (이용약관)
}
if (privacyChecked) {
agreeTerms.push("TID0000223"); // MST00401 -> TID0000223 (개인정보처리방침)
}
if (optionalChecked) {
agreeTerms.push("TID0000232"); // MST00405 -> TID0000232 (선택약관)
}
console.log('최종 전송될 agreeTerms:', agreeTerms);
// registerDevice 호출 - 필수 + 선택 약관 모두 포함
dispatch(registerDevice({
agreeTerms: agreeTerms
}));
}, [termsChecked, privacyChecked, optionalChecked, dispatch]);
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(setExitApp());
dispatch(
sendLogTotalRecommend({
contextName: Config.LOG_CONTEXT_NAME.SHOPTIME,
messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE,
visible: false,
})
);
}, [dispatch]);
const onCancel = useCallback(() => {
if (activePopup === null) {
dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup));
}
}, [dispatch, activePopup]);
// 체크박스 핸들러들
const handleTermsToggle = useCallback(({ selected }) => {
setTermsChecked(selected);
}, []);
const handlePrivacyToggle = useCallback(({ selected }) => {
setPrivacyChecked(selected);
}, []);
const handleOptionalToggle = useCallback(({ selected }) => {
setOptionalChecked(selected);
}, []);
const handleSelectAllToggle = useCallback(({ selected }) => {
setSelectAllChecked(selected);
setTermsChecked(selected);
setPrivacyChecked(selected);
setOptionalChecked(selected);
}, []);
useEffect(() => {
Spotlight.focus("termsCheckbox");
}, []);
useEffect(() => {
Spotlight.focus();
}, [popupVisible]);
useEffect(() => {
if (regDeviceData && regDeviceData.retCode === 0) {
dispatch(getWelcomeEventInfo());
}
}, [dispatch, regDeviceData]);
useEffect(() => {
if (regDeviceInfoData && regDeviceInfoData.retCode === 0) {
dispatch(setHidePopup());
dispatch(popPanel(panel_names.INTRO_PANEL));
}
}, [dispatch, regDeviceInfoData]);
useEffect(() => {
if (eventInfos && Object.keys(eventInfos).length > 0 && webOSVersion) {
let displayWelcomeEventPanel = false;
if (
eventInfos?.welcomeEventFlag === "Y" ||
(eventInfos?.welcomeBillCpnEventFlag === "Y" &&
Number(webOSVersion) >= 6)
) {
displayWelcomeEventPanel = true;
}
dispatch(popPanel(panel_names.INTRO_PANEL));
dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }));
if (displayWelcomeEventPanel) {
dispatch(
pushPanel({
name: panel_names.WELCOME_EVENT_PANEL,
panelInfo: { eventInfos: eventInfos },
})
);
}
}
}, [eventInfos, webOSVersion]);
const description = $L(
"Check out more LIVE SHOWS now and enjoy shopping at Shop Time's special price on your TV by agreeing to the LG TV Shopping Terms and Conditions."
);
const title = "welcome to shoptime!";
delete rest.isOnTop;
return (
<Region title={title + description}>
<TPanel
className={css.panel}
isTabActivated={false}
handleCancel={onCancel}
spotlightId={spotlightId}
>
<Container {...rest} className={css.introLayout}>
{/* 첫 번째 영역: 헤더 섹션 */}
<div className={css.headerSection}>
<div className={css.titleContainer}>
<div className={css.welcomeText}>
{$L("Welcome to")}
</div>
<div className={css.brandContainer}>
<span className={css.shopText}>{$L("Sh")}</span>
<span className={css.oText}>{$L("o")}</span>
<span className={css.timeText}>{$L("pTime !")}</span>
</div>
</div>
<div className={css.descriptionContainer}>
<div className={css.descriptionText}>
{description}
</div>
</div>
</div>
{/* 두 번째 영역: 약관 섹션 */}
<div className={css.termsSection}>
<div className={css.termsLeftPanel}>
{/* Terms & Conditions */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
selected={termsChecked}
onToggle={handleTermsToggle}
spotlightId="termsCheckbox"
ariaLabel={$L("Terms & Conditions checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleTermsClick("MST00402")}
spotlightId="termsButton"
type={TYPES.terms}
ariaLabel={$L("View Terms & Conditions")}
>
<span className={css.termsText}>
{$L("Terms & Conditions")}
</span>
</TButton>
</div>
{/* Privacy Policy */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
selected={privacyChecked}
onToggle={handlePrivacyToggle}
spotlightId="privacyCheckbox"
ariaLabel={$L("Privacy Policy checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleTermsClick("MST00401")}
spotlightId="privacyButton"
type={TYPES.terms}
ariaLabel={$L("View Privacy Policy")}
>
<span className={css.termsText}>
{$L("Privacy Policy")}
</span>
</TButton>
</div>
{/* Optional Terms */}
<div className={css.termsItem}>
<TCheckBoxSquare
className={css.checkbox}
selected={optionalChecked}
onToggle={handleOptionalToggle}
spotlightId="optionalCheckbox"
ariaLabel={$L("Optional Terms checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleOptionalTermsClick("MST00405")}
spotlightId="optionalButton"
type={TYPES.terms}
ariaLabel={$L("View Optional Terms")}
>
<span className={css.termsText}>
{$L("Optional Terms")}
</span>
</TButton>
</div>
</div>
<div className={css.termsRightPanel}>
{shouldShowBenefitsView ? (
// WebOS 텍스트 표시 버전 (4.5, 6.0, 22): 기존 텍스트 설명
<div className={css.optionalDescription}>
{$L('By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features')}
</div>
) : (
// WebOS 이미지 표시 버전 (4.0, 5.0, 23, 24): OptionalTermsInfo 컴포넌트로 이미지 표시
<OptionalTermsInfo
displayMode="image"
imageTitle={$L('Agree and Enjoy Special Benefits')}
spotlightId="optional-terms-info"
/>
)}
</div>
</div>
{/* 세 번째 영역: Select All */}
<div className={css.selectAllSection}>
<TCheckBoxSquare
className={css.selectAllCheckbox}
selected={selectAllChecked}
onToggle={handleSelectAllToggle}
spotlightId="selectAllCheckbox"
ariaLabel={$L("Select All checkbox")}
/>
<span className={css.selectAllText}>
{$L("Select All")}
</span>
</div>
{/* 네 번째 영역: 버튼 섹션 */}
<div className={css.buttonSection}>
<TButton
className={css.agreeButton}
onClick={handleAgree}
spotlightId="agreeButton"
type={TYPES.agree}
ariaLabel={$L("Agree to terms")}
>
{$L("Agree")}
</TButton>
<TButton
className={css.disagreeButton}
onClick={handleDisagree}
spotlightId="disagreeButton"
type={TYPES.agree}
ariaLabel={$L("Do not agree to terms")}
>
{$L("Do Not Agree")}
</TButton>
</div>
</Container>
</TPanel>
{/* TERMS */}
{activePopup === Config.ACTIVE_POPUP.termsPopup && (
<TPopUp
kind="introTermsPopup"
open={popupVisible}
onClose={onClose}
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"
/>
<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
kind="exitPopup"
open={popupVisible}
onExit={onExit}
onClose={onClose}
hasButton
button1Text={$L("Exit")}
button2Text={$L("Cancel")}
hasText
title={$L("Exit Shop Time")}
text={$L("Are you sure you want to exit Shop Time?")}
/>
)} {/* ALERT POPUP (필수 약관 알림) */}
{activePopup === Config.ACTIVE_POPUP.alertPopup && (
<TPopUp
kind="textPopup"
open={popupVisible}
onClose={onClose}
hasButton
button1Text={popupState.button1Text || $L("OK")}
hasText
title={$L("Required Terms")}
text={$L("Please agree to Terms & Conditions and Privacy Policy.")}
/>
)}
</Region>
);
}

View File

@@ -0,0 +1,386 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
.panel {
> section {
color: @COLOR_GRAY06;
}
}
.introLayout {
width: 100%;
height: 100%;
padding: 45.5px 43px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 40px;
display: inline-flex;
background-color: @BG_COLOR_03;
// 첫 번째 영역: 헤더 섹션 (타이틀 + 설명)
.headerSection {
width: 1834px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 40px;
display: flex;
.titleContainer {
align-self: stretch;
justify-content: center;
align-items: center;
gap: 14px;
display: inline-flex;
.welcomeText {
color: #807F81;
font-size: 62px;
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 62px;
word-wrap: break-word;
}
.brandContainer {
.shopText {
color: #57585A;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
}
.oText {
color: #C91D53;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
}
.timeText {
color: #57585A;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
}
}
}
.descriptionContainer {
align-self: stretch;
justify-content: center;
align-items: center;
gap: 10px;
display: inline-flex;
.descriptionText {
width: 1012.49px;
text-align: center;
color: #807F81;
font-size: 36px;
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 43px;
word-wrap: break-word;
}
}
}
// 두 번째 영역: 약관 선택 섹션
.termsSection {
align-self: stretch;
justify-content: center;
align-items: flex-start;
display: inline-flex;
.termsLeftPanel {
flex: 1 1 0;
padding-left: 60px;
padding-right: 60px;
flex-direction: column;
justify-content: center;
align-items: flex-end;
gap: 20px;
display: inline-flex;
.termsItem {
justify-content: flex-start;
align-items: center;
gap: 20px;
display: inline-flex; .checkbox {
/* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */
width: 45px;
height: 45px;
}
.termsButton {
width: 530px;
height: 120px;
padding: 0 50px;
background: @COLOR_WHITE;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20);
border-radius: 6px;
border: 1px solid #CFCFCF;
justify-content: space-between;
align-items: center;
display: flex;
cursor: pointer;
transition: all 0.3s ease;
will-change: transform;
.termsText {
// color: #207847;
color: black;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
transition: color 0.3s ease;
}
// 포커스 상태
&:global(.spottable):global(.focus) {
outline: 2px #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;
}
}
// 호버 효과
&: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);
.termsText {
color: #C70850;
font-weight: bold;
}
}
}
}
} .termsRightPanel {
flex: 1 1 0;
align-self: stretch;
padding-left: 60px;
padding-right: 60px;
border-left: 1px #C5C6C9 solid;
justify-content: flex-start;
align-items: center;
gap: 10px;
display: flex;
.optionalDescription {
width: 595px;
color: #807F81;
font-size: 26px;
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 43px;
word-wrap: break-word;
}
// OptionalTermsInfo 컴포넌트 스타일
.termsInfo {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
}
// 세 번째 영역: Select All
.selectAllSection {
justify-content: center;
align-items: center;
gap: 15px;
display: inline-flex; .selectAllCheckbox {
/* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */
width: 45px;
height: 45px;
}
.selectAllText {
color: @COLOR_BLACK;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
}
}
// 네 번째 영역: 버튼 섹션
.buttonSection {
width: 940px;
height: 100px;
position: relative;
display: flex;
gap: 40px;
justify-content: center;
.agreeButton {
width: 450px;
height: 100px;
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;
cursor: pointer;
transition: all 0.3s ease;
// 포커스 상태
&:global(.spottable):global(.focus) {
background-color: #a40640 !important;
border-color: #a40640 !important;
color: white !important;
transform: scale(1.05);
}
// 호버 효과
&:hover {
background-color: #a40640 !important;
border-color: #a40640 !important;
color: white !important;
transform: scale(1.05);
}
// 비활성화 상태
&:disabled {
background-color: @COLOR_GRAY03 !important;
border-color: @COLOR_GRAY03 !important;
cursor: not-allowed;
transform: none;
}
}
.disagreeButton {
width: 450px;
height: 100px;
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;
cursor: pointer;
transition: all 0.3s ease;
// 포커스 상태
&:global(.spottable):global(.focus) {
background-color: #7a7a7a !important;
border-color: #7a7a7a !important;
color: white !important;
transform: scale(1.05);
}
// 호버 효과
&:hover {
background-color: #7a7a7a !important;
border-color: #7a7a7a !important;
color: white !important;
transform: scale(1.05);
}
}
}
}
/* intro terms popup */
.introTermsConts {
background-color: #f8f8f8;
> div:nth-child(2) {
background-color: @COLOR_WHITE;
border: 1px solid @COLOR_GRAY02;
width: 980px;
height: 300px;
}
.termsDesc {
padding: 30px;
min-height: 300px;
color: @COLOR_GRAY03;
font-size: 24px;
line-height: 1.27;
letter-spacing: normal;
text-align: left;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
.tab {
width: 980px;
}
}
// 체크박스 커스텀 스타일
:global(.tCheckBoxSquare) {
width: 45px;
height: 45px;
// 기본 상태
&:before {
content: '';
width: 42px;
height: 42px;
background: @COLOR_WHITE;
border: 2px solid @COLOR_GRAY02;
border-radius: 4px;
display: block;
}
// 선택된 상태
&.selected:before {
background: #C91D53;
border-color: #C91D53;
}
// 선택된 상태의 체크 마크
&.selected:after {
content: '✓';
color: @COLOR_WHITE;
font-size: 24px;
font-weight: bold;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// 포커스 상태
&.focus: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

@@ -70,7 +70,7 @@ import FeaturedBrandsPanel from "../FeaturedBrandsPanel/FeaturedBrandsPanel";
import HomePanel from "../HomePanel/HomePanel"; import HomePanel from "../HomePanel/HomePanel";
import HotPicksPanel from "../HotPicksPanel/HotPicksPanel"; import HotPicksPanel from "../HotPicksPanel/HotPicksPanel";
import ImagePanel from "../ImagePanel/ImagePanel"; import ImagePanel from "../ImagePanel/ImagePanel";
import IntroPanel from "../IntroPanel/IntroPanel"; import IntroPanel from "../IntroPanel/IntroPanel.new";
import LoadingPanel from "../LoadingPanel/LoadingPanel"; import LoadingPanel from "../LoadingPanel/LoadingPanel";
import MyPagePanel from "../MyPagePanel/MyPagePanel"; import MyPagePanel from "../MyPagePanel/MyPagePanel";
import OnSalePanel from "../OnSalePanel/OnSalePanel"; import OnSalePanel from "../OnSalePanel/OnSalePanel";
@@ -80,6 +80,8 @@ import ThemeCurationPanel from "../ThemeCurationPanel/ThemeCurationPanel";
import TrendingNowPanel from "../TrendingNowPanel/TrendingNowPanel"; import TrendingNowPanel from "../TrendingNowPanel/TrendingNowPanel";
import VideoTestPanel from "../VideoTestPanel/VideoTestPanel"; import VideoTestPanel from "../VideoTestPanel/VideoTestPanel";
import WelcomeEventPanel from "../WelcomeEventPanel/WelcomeEventPanel"; import WelcomeEventPanel from "../WelcomeEventPanel/WelcomeEventPanel";
import TermsOfOptional from "../MyPagePanel/MyPageSub/TermsOfService/TermsOfOptionalSimple"; // 선택약관 반영 인트로
import OptionalTermsConfirmTest from "../../components/Optional/OptionalTermsConfirmTest";
import css from "./MainView.module.less"; import css from "./MainView.module.less";
const preloadImages = [ const preloadImages = [
@@ -110,6 +112,7 @@ const panelMap = {
[Config.panel_names.THEME_CURATION_PANEL]: ThemeCurationPanel, [Config.panel_names.THEME_CURATION_PANEL]: ThemeCurationPanel,
[Config.panel_names.IMAGE_PANEL]: ImagePanel, [Config.panel_names.IMAGE_PANEL]: ImagePanel,
[Config.panel_names.CONFIRM_PANEL]: ConfirmPanel, [Config.panel_names.CONFIRM_PANEL]: ConfirmPanel,
// [Config.panel_names.OPTIONAL_TERMS_PANEL]: TermsOfOptional,
}; };
const logTpNoLiveSet = new Set([ const logTpNoLiveSet = new Set([
@@ -135,8 +138,7 @@ export default function MainView({ className, initService }) {
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const lastPanelAction = useSelector((state) => state.panels.lastPanelAction); const lastPanelAction = useSelector((state) => state.panels.lastPanelAction);
const loadingComplete = useSelector((state) => state.common?.loadingComplete); const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const menuData = useSelector((state) => state.home.menuData?.data); const menuData = useSelector((state) => state.home.menuData?.data); const {
const {
popupVisible, popupVisible,
activePopup, activePopup,
data: errorCode, data: errorCode,
@@ -169,6 +171,14 @@ export default function MainView({ className, initService }) {
const [showEndOfServicePopup, setShowEndOfServicePopup] = useState(false); const [showEndOfServicePopup, setShowEndOfServicePopup] = useState(false);
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
// OptionalTermsConfirm 팝업 상태 디버깅
useEffect(() => {
console.log('🔍 MainView 팝업 상태 변경:', {
popupVisible,
activePopup,
});
}, [popupVisible, activePopup]);
const isHomeOnTop = useMemo(() => { const isHomeOnTop = useMemo(() => {
return ( return (
!mainIndex && !mainIndex &&
@@ -627,8 +637,8 @@ export default function MainView({ className, initService }) {
dispatch(setShowPopup(Config.ACTIVE_POPUP.endOfServicePopup)); dispatch(setShowPopup(Config.ACTIVE_POPUP.endOfServicePopup));
} }
}, [webOSVersion]); }, [webOSVersion]);
const handleErrorPopupClose = useCallback(() => { const handleErrorPopupClose = useCallback(() => {
console.log('handleErrorPopupClose 호출됨! activePopup:', activePopup, 'popupData:', popupData);
if (popupData?.shouldPopPanel) { if (popupData?.shouldPopPanel) {
dispatch(popPanel()); dispatch(popPanel());
} }
@@ -641,7 +651,7 @@ export default function MainView({ className, initService }) {
Spotlight.focus(SpotlightIds.DETAIL_BUYNOW); Spotlight.focus(SpotlightIds.DETAIL_BUYNOW);
}); });
} }
}, [dispatch, popupData]); }, [dispatch, popupData, activePopup, topPanel?.name]);
// useEffect(() => { // useEffect(() => {
// if (!isInternetConnected) { // if (!isInternetConnected) {
@@ -722,8 +732,7 @@ export default function MainView({ className, initService }) {
popupData.retDetailCode, popupData.retDetailCode,
popupData.returnBindStrings popupData.returnBindStrings
)} )}
</p> </p> <TButton className={css.popupBtn} onClick={handleErrorPopupClose}>
<TButton className={css.popupBtn} onClick={handleErrorPopupClose}>
{$L("OK")} {$L("OK")}
</TButton> </TButton>
</div> </div>
@@ -770,10 +779,44 @@ export default function MainView({ className, initService }) {
title={$L("Exit Shop Time")} title={$L("Exit Shop Time")}
/> />
) : null} ) : null}
{/* {activePopup === "optionalTermsTest" && (
<OptionalTermsConfirmTest open={popupVisible} />
)} */}
{/* OptionalTermsConfirmPopup */}
{activePopup === Config.ACTIVE_POPUP.optionalTermsConfirmPopup && (
<OptionalTermsConfirm
kind="optionalTermsConfirmPopup"
open={popupVisible}
onClose={handleErrorPopupClose}
hasText
hasButton
button1Text={$L("OK")}
button2Text={$L("Cancel")}
/>
)}
<SystemNotification /> <SystemNotification />
{loadingComplete && {loadingComplete &&
activePopup === Config.ACTIVE_POPUP.endOfServicePopup && activePopup === Config.ACTIVE_POPUP.endOfServicePopup &&
!skipEndOfServicePopup && <EndOfServicePopUp />} !skipEndOfServicePopup && <EndOfServicePopUp />}
{/* Pop up */}
{activePopup === Config.ACTIVE_POPUP.toast && (
<TPopUp
kind="toast"
open={popupVisible}
onClose={handleErrorPopupClose}
hasText
hasButton
button1Text={toastText}
button2Text={$L("OK")}
/>
)}
{activePopup === Config.ACTIVE_POPUP.optionalTermsTest && (
<OptionalTermsConfirmTest open={popupVisible} />
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,95 @@
// src/views/MyPagePanel/MyPageSub/TermsOfService/CustomTermButton.jsx
import React, { useCallback, useState } from "react";
import classNames from "classnames";
import Spotlight from "@enact/spotlight";
import Spottable from "@enact/spotlight/Spottable";
import css from "./CustomTermButton.module.less";
// 스포터블 컴포넌트 생성
const SpottableDiv = Spottable("div");
/**
* 약관 버튼 전용 커스텀 버튼 컴포넌트
* TButton 컴포넌트를 기반으로 하되, 약관 버튼에 최적화된 레이아웃을 제공합니다.
*/
function CustomTermButton({
children,
icon,
className,
spotlightId,
onClick,
onFocus,
onBlur,
onMouseEnter,
onMouseLeave,
spotlightDisabled = false,
disabled = false,
selected = false,
hovered = false,
...rest
}) {
const [isFocused, setIsFocused] = useState(false);
// 클릭 핸들러
const handleClick = useCallback(
(e) => {
if (disabled) {
e.stopPropagation();
return;
}
if (onClick) {
onClick(e);
}
},
[onClick, disabled]
);
// 포커스 핸들러
const handleFocus = useCallback(() => {
setIsFocused(true);
if (onFocus) {
onFocus();
}
}, [onFocus]);
// 블러 핸들러
const handleBlur = useCallback(() => {
setIsFocused(false);
if (onBlur) {
onBlur();
}
}, [onBlur]);
return (
<SpottableDiv
{...rest}
className={classNames(
css.customTermButton,
isFocused && css.focused,
hovered && css.hovered,
selected && css.selected,
disabled && css.disabled,
className
)}
spotlightId={spotlightId}
onFocus={handleFocus}
onBlur={handleBlur}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
role="button"
spotlightDisabled={spotlightDisabled}
>
<div className={css.content}>
<div className={css.textContent}>{children}</div>
{icon && <div className={css.iconContent}>{icon}</div>}
</div>
</SpottableDiv>
);
}
export default CustomTermButton;

View File

@@ -0,0 +1,96 @@
// src/vies/MyPagePanel/MyPageSub/TermsOfService/CustomTermButton.module.less
/* 스타일 직접 정의 - 외부 의존성 제거 */
.customTermButton {
// 기본 버튼 스타일
width: 630px; // 디자인 스펙에 맞게 630px로 변경
height: 120px;
background-color: white;
border-radius: 6px;
outline: 1px #CFCFCF solid;
outline-offset: -1px;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20);
cursor: pointer;
transition: all 0.3s ease;
will-change: transform;
// 중요: 내부 콘텐츠 레이아웃 설정
.content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 0px; // 디자인 스펙에 맞게 50px로 변경
}
// 텍스트 영역 스타일
.textContent {
flex: 1;
text-align: left;
color: #1a1a1a;
font-size: 35px; // 디자인 스펙에 맞게 35px로 변경
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px; // 디자인 스펙에 맞게 35px로 변경
white-space: nowrap; // 텍스트가 한 줄에 표시되도록 설정
overflow: hidden; // 넘치는 텍스트 숨김
text-overflow: ellipsis; // 넘치는 텍스트에 ... 표시
transition: color 0.3s ease;
margin-right: 10px; // 오른쪽 마진 더 줄임 (15px → 10px)
}
// 아이콘 영역 스타일
.iconContent {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
// 호버 상태
&.hovered {
background-color: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
.textContent {
color: #207847;
}
&[data-type='OPTIONAL_TERMS'] .textContent {
color: #c70850;
}
}
// 포커스 상태
&.focused, &:focus {
outline: 2px #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;
.textContent {
color: #C91D53;
}
}
// 선택된 상태
&.selected {
background-color: rgba(201, 29, 83, 0.05);
.textContent {
color: #C91D53;
}
}
// 비활성화 상태
&.disabled {
opacity: 0.5;
cursor: default;
pointer-events: none;
}
}

View File

@@ -0,0 +1,136 @@
// OptionalTermsInfo.clean.module.less
// IntroPanel용 깔끔한 스타일 (global 스타일 제거)
.termsInfo {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
}
// 텍스트 모드
.textMode {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.textDescription {
color: #1A1A1A;
font-size: 18px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 400;
line-height: 1.4;
text-align: center;
word-wrap: break-word;
margin: 0;
}
// 이미지 모드
.imageMode {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
}
.imageTitle {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.imageTitleText {
color: #C70850;
font-size: 20px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
text-align: center;
word-wrap: break-word;
}
.imageContainer {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.benefitImage {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.benefitImage:hover {
transform: scale(1.05);
}
// 이미지 개수별 스타일
.imageCount1 {
.benefitImage {
width: 120px;
height: 120px;
}
}
.imageCount2 {
.benefitImage {
width: 100px;
height: 100px;
}
}
.imageCount3 {
.benefitImage {
width: 80px;
height: 80px;
}
}
// 반응형 디자인
@media (max-width: 1200px) {
.termsInfo {
padding: 15px;
}
.textDescription {
font-size: 16px;
}
.imageTitleText {
font-size: 18px;
}
.benefitImage {
width: 60px;
height: 60px;
}
.imageCount1 .benefitImage {
width: 100px;
height: 100px;
}
.imageCount2 .benefitImage {
width: 80px;
height: 80px;
}
.imageCount3 .benefitImage {
width: 60px;
height: 60px;
}
}

View File

@@ -0,0 +1,117 @@
// src/views/MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo.jsx
import React, { memo } from 'react';
import kind from '@enact/core/kind';
import PropTypes from 'prop-types';
import $L from '@enact/i18n/$L';
import css from './OptionalTermsInfo.module.less';
import benefitImage1 from '/assets/images/benefits/image-benefits-1.png';
import benefitImage2 from '/assets/images/benefits/image-benefits-2.png';
import benefitImage3 from '/assets/images/benefits/image-benefits-3.png';
/**
* OptionalTermsInfo 컴포넌트
* WebOS 버전에 따라 텍스트 또는 이미지로 선택적 약관 정보를 표시합니다.
*/
const OptionalTermsInfo = kind({
name: 'OptionalTermsInfo',
propTypes: {
/**
* 추가 CSS 클래스명
*/
className: PropTypes.string,
/**
* 표시 모드 - 텍스트 또는 이미지
* @type {('text'|'image')}
*/
displayMode: PropTypes.oneOf(['text', 'image']),
/**
* 이미지 모드일 때 사용할 이미지 소스들 (배열)
*/
imageSources: PropTypes.arrayOf(PropTypes.string),
/**
* 이미지 모드일 때 표시할 제목
*/
imageTitle: PropTypes.string,
/**
* 텍스트 모드일 때 표시할 설명 텍스트
*/
textDescription: PropTypes.string,
/**
* Spotlight ID
*/
spotlightId: PropTypes.string
},
defaultProps: {
displayMode: 'text',
imageSources: [
benefitImage1,
benefitImage2,
benefitImage3
],
imageTitle: $L('Agree and Enjoy Special Benefits'),
textDescription: $L('By checking Optional terms, you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features')
},
styles: {
css,
className: 'termsInfo'
},
computed: {
className: ({ className, styler }) => styler.append(className)
},
render: ({
displayMode,
imageSources,
imageTitle,
textDescription,
spotlightId,
...rest
}) => {
const imageCount = imageSources ? imageSources.length : 0;
return (
<div
{...rest}
data-spotlight-id={spotlightId}
>
{displayMode === 'text' ? (
// 텍스트 모드
<div className={css.textMode}>
<p className={css.textDescription}>
{textDescription}
</p>
</div>
) : (
// 이미지 모드
<div className={css.imageMode}>
<div className={`${css.imageContainer} ${css[`imageCount${imageCount}`]}`}>
{imageSources && imageSources.map((src, index) => (
<img
key={index}
className={css.benefitImage}
src={src}
alt={$L(`Benefit image ${index + 1}`)}
/>
))}
</div>
</div>
)}
</div>
);
}
});
export default memo(OptionalTermsInfo);
export { OptionalTermsInfo };

View File

@@ -0,0 +1,115 @@
// src/views/MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo.module.less
.termsInfo {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
min-height: 360px;
}
// 텍스트 모드
.textMode {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.textDescription {
color: #1A1A1A;
font-size: 18px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 400;
line-height: 1.4;
text-align: center;
word-wrap: break-word;
margin: 0;
}
// 이미지 모드
.imageMode {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100%;
}
.imageTitle {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
}
.imageTitleText {
color: #C70850;
font-size: 20px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
text-align: center;
word-wrap: break-word;
}
.imageContainer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.benefitImage {
height: 95%;
width: 30%;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.benefitImage:hover {
transform: scale(1.08);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.imageCount1 .benefitImage,
.imageCount2 .benefitImage,
.imageCount3 .benefitImage {
height: 95%;
width: 30%;
}
// 반응형 디자인
@media (max-width: 1200px) {
.termsInfo {
padding: 15px;
}
.textDescription {
font-size: 16px;
}
.imageTitleText {
font-size: 18px;
}
.benefitImage,
.imageCount1 .benefitImage,
.imageCount2 .benefitImage,
.imageCount3 .benefitImage {
width: 30%;
height: 95%;
}
}

View File

@@ -0,0 +1,362 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.panel {
> section {
color: @COLOR_GRAY06;
}
}
.termsOptionalLayout {
.flex(@direction: column);
background-color: @BG_COLOR_03;
text-align: center;
width: 100%;
height: 100vh;
.title {
font-size: 60px;
padding: 44px 201px;
white-space: pre-wrap;
line-height: normal;
}
.txtPoint {
font-weight: bold;
font-size: 74px;
color: #57585a;
}
.pointColor {
color: #c91d53;
}
.description {
width: 1200px;
white-space: pre-wrap;
line-height: normal;
font-size: 36px;
padding: 0 10px;
padding-top: 14px;
}
.termsItemsLayer {
display: flex;
margin: 113px 0 120px 0;
.termsButton {
> div {
padding-right: 10px;
}
}
}
.bottomBtnLayer {
margin-top: 50px;
.flex();
}
}
/* TermsOfOptional 전용 스타일들 - IntroPanel 패턴 따라서 추가 */
.welcomeSection {
width: 100%;
max-width: 1834px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4vh;
margin-bottom: 50px;
}
.welcomeTitle {
align-self: stretch;
display: flex;
justify-content: center;
align-items: center;
gap: 14px;
}
.welcomeText {
color: #807f81;
font-size: clamp(48px, 3.23vw, 62px);
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 1;
word-break: break-word;
}
.brandTitle {
font-size: clamp(54px, 3.65vw, 70px);
font-family: 'LG Smart_Korean';
font-weight: 700;
line-height: 1.07;
word-break: break-word;
display: inline;
}
.brandShop { color: #57585a; }
.brandO { color: #c91d53; }
.brandPTime { color: #57585a; }
.welcomeDescription {
width: 100%;
max-width: 1012.49px;
text-align: center;
color: #807f81;
font-size: clamp(24px, 1.875vw, 36px);
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 1.19;
word-break: break-word;
margin-top: 1vh;
}
.termsSection {
width: 100%;
display: flex;
justify-content: center;
align-items: stretch;
padding: 0;
margin: 50px 0;
}
.mandatoryTerms {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 20px;
padding-right: 60px;
}
.termItem {
height: 120px;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 20px;
margin: 0;
}
.termCheckbox,
.selectAllCheckbox {
width: 45px !important;
height: 45px !important;
min-width: 45px !important;
min-height: 45px !important;
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
&.tCheckBox {
border-radius: 0 !important;
border: 3px solid #C70850 !important;
background: #fff !important;
transition: background 0.15s, border 0.15s;
}
&.tCheckBox.selected {
border: 3px solid #7A808D !important;
background: #7A808D !important;
}
&.tCheckBox::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18px !important;
height: 10px !important;
border-left: 4px solid #fff !important;
border-bottom: 4px solid #fff !important;
transform: translate(-50%, -70%) rotate(-45deg) scale(0);
border-radius: 0 !important;
transition: transform 0.2s ease-in-out;
}
&.tCheckBox.selected::before {
transform: translate(-50%, -70%) rotate(-45deg) scale(1);
}
}
.termBox {
width: 530px;
height: 120px;
padding: 0 50px;
background: white;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20);
border-radius: 6px;
outline: 1px #CFCFCF solid;
outline-offset: -1px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
will-change: transform;
&:focus {
outline: 2px #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;
}
&.hovered {
background-color: rgba(255, 255, 255, 0.1);
transform: translate3d(0, -2px, 0);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
.termName {
color: #1a1a1a;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
transition: color 0.3s ease;
flex: 1;
text-align: left;
margin-right: 20px;
&.titleHovered {
color: #c70850;
font-weight: bold;
}
}
.termNameMandatory {
color: #207847 !important;
}
.expandIconContainer {
width: 37px;
height: 37px;
position: relative;
border-radius: 100px;
outline: 2.5px black solid;
outline-offset: -2.5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
margin-left: auto;
&:hover {
transform: scale(1.05);
}
}
.selectAllSection {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 30px;
}
.selectAllText {
color: #1a1a1a;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
word-break: break-word;
}
.agreeButton {
width: 450px;
height: 100px;
padding-left: 50px;
padding-right: 50px;
background-color: #C70850 !important;
border: 2px solid #C70850 !important;
color: white !important;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
transition: all 0.3s ease;
&:hover,
&:focus,
&.spott-focus {
background-color: #a40640 !important;
border-color: #a40640 !important;
color: white !important;
transform: scale(1.05);
}
}
.disagreeButton {
width: 450px;
height: 100px;
padding-left: 50px;
padding-right: 50px;
background-color: #999999 !important;
border: 2px solid #999999 !important;
color: white !important;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
transition: all 0.3s ease;
&:hover,
&:focus,
&.spott-focus {
background-color: #7a7a7a !important;
border-color: #7a7a7a !important;
color: white !important;
transform: scale(1.05);
}
}
.divider {
width: 1px;
height: 100%;
background-color: #c5c6c9;
margin: 0 16px;
display: inline-block;
}
/* IntroPanel과 동일한 약관 팝업 스타일 */
.introTermsConts {
background-color: #f8f8f8;
> div:nth-child(2) {
background-color: @COLOR_WHITE;
.border-solid(1px,@COLOR_GRAY02);
width: 980px;
height: 300px;
}
.termsDesc {
padding: 30px;
min-height: 300px;
color: @COLOR_GRAY03;
font-size: 24px;
line-height: 1.27;
letter-spacing: normal;
text-align: left;
.flex( @direction:column, @justifyCenter:flex-start,@alignCenter:flex-start);
}
}
.tab {
width: 980px;
}

View File

@@ -0,0 +1,406 @@
// src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfOptional.jsx
/**
* ShopTime 애플리케이션의 선택적 약관 동의 컴포넌트 (IntroPanel 기반 단순화 버전)
*
* @component TermsOfOptional
* @description
* webOS TV 환경에서 선택적 약관 정보를 표시하고 사용자의 동의 여부를 관리합니다.
* IntroPanel과 동일한 구조로 모달 문제 없이 안전하게 동작합니다.
*/
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import Spotlight from "@enact/spotlight";
import {
setHidePopup,
setShowPopup,
setExitApp
} from "../../../../actions/commonActions";
import { registerDevice } from "../../../../actions/deviceActions";
import { sendLogTerms } from "../../../../actions/logActions";
import { popPanel } from "../../../../actions/panelActions";
import TBody from "../../../../components/TBody/TBody";
import TPanel from "../../../../components/TPanel/TPanel";
import TButton from "../../../../components/TButton/TButton";
import TCheckBoxSquare from "../../../../components/TCheckBox/TCheckBoxSquare";
import TPopUp from "../../../../components/TPopUp/TPopUp";
import * as Config from "../../../../utils/Config";
import { $L } from "../../../../utils/helperMethods";
import css from "./TermsOfOptionalSimple.module.less";
// 약관 타입 정의
const TERMS_TYPE = {
TERMS_CONDITIONS: 'TERMS_CONDITIONS',
PRIVACY_POLICY: 'PRIVACY_POLICY',
OPTIONAL_TERMS: 'OPTIONAL_TERMS'
};
// 약관 ID 매핑
const TERMS_ID_MAP = {
[TERMS_TYPE.TERMS_CONDITIONS]: "TID0000222",
[TERMS_TYPE.PRIVACY_POLICY]: "TID0000223",
[TERMS_TYPE.OPTIONAL_TERMS]: "TID0000232"
};
// 약관 표시명
const TERMS_DISPLAY_NAMES = {
[TERMS_TYPE.TERMS_CONDITIONS]: "이용약관 동의 (필수)",
[TERMS_TYPE.PRIVACY_POLICY]: "개인정보처리방침 동의 (필수)",
[TERMS_TYPE.OPTIONAL_TERMS]: "선택약관 동의"
};
// 필수 약관 여부 확인
const isMandatoryTerms = (type) => {
return type === TERMS_TYPE.TERMS_CONDITIONS || type === TERMS_TYPE.PRIVACY_POLICY;
};
// 초기 체크박스 상태
const getInitialCheckboxStates = () => ({
[TERMS_TYPE.TERMS_CONDITIONS]: false,
[TERMS_TYPE.PRIVACY_POLICY]: false,
[TERMS_TYPE.OPTIONAL_TERMS]: false
});
export default function TermsOfOptional({
title,
isModalMode,
closeModalCallback,
panelInfo,
spotlightId = "termsOfOptionalPanel",
initialAgreeState,
}) {
const dispatch = useDispatch();
// Redux state
const commonPopupState = useSelector((state) => state.common?.popup || {});
const { popupVisible = false, activePopup = null } = commonPopupState;
const deviceState = useSelector((state) => state.device);
// 컴포넌트 상태
const [checkboxStates, setCheckboxStates] = useState(() => getInitialCheckboxStates());
const [isAllChecked, setIsAllChecked] = useState(false);
const [spotlightDisabled, setSpotlightDisabled] = useState(false);
// 체크박스 개별 토글 핸들러
const handleCheckboxToggle = useCallback((type) => ({ selected }) => {
console.log(`[CHECKBOX-CLICK] ${type} 체크박스 클릭됨:`, selected);
setCheckboxStates(prev => {
const newStates = { ...prev, [type]: selected };
const allChecked = Object.values(newStates).every(state => state);
setIsAllChecked(allChecked);
console.log(`[CHECKBOX-CLICK] 새로운 전체 상태:`, newStates);
return newStates;
});
}, []);
// 전체 선택 토글 핸들러
const handleSelectAllToggle = useCallback(({ selected }) => {
console.log(`[SELECT-ALL-CLICK] 전체 선택 클릭됨:`, selected);
setIsAllChecked(selected);
setCheckboxStates(prev => {
const newStates = Object.keys(prev).reduce((acc, key) => ({
...acc,
[key]: selected
}), {});
console.log(`[SELECT-ALL-CLICK] 새로운 전체 상태:`, newStates);
return newStates;
});
}, []);
// 약관 내용 보기 핸들러 (간단한 알림 팝업으로 대체)
const handleTermsView = useCallback((type) => {
const termName = TERMS_DISPLAY_NAMES[type];
const termId = TERMS_ID_MAP[type];
dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: termName,
text: `${termName}\n\n약관 ID: ${termId}\n\n실제 약관 내용은 서버에서 로드됩니다.\n현재는 단순화된 버전입니다.`,
button1Text: $L("확인")
}));
}, [dispatch]);
// 팝업 닫기 핸들러
const handleClosePopup = useCallback(() => {
console.log('[TermsOfOptional] handleClosePopup triggered');
dispatch(setHidePopup());
}, [dispatch]);
// 앱 종료 핸들러
const onExit = useCallback(() => {
console.log('[TermsOfOptional] onExit triggered');
dispatch(setHidePopup());
dispatch(setExitApp());
}, [dispatch]);
// 팝업 취소 핸들러
const onClose = useCallback(() => {
console.log('[TermsOfOptional] onClose triggered');
dispatch(setHidePopup());
}, [dispatch]);
// 뒤로가기 핸들러
const handleBack = useCallback(() => {
if (isModalMode && closeModalCallback) {
console.log('[TermsOfOptional] Modal mode back pressed');
closeModalCallback();
} else if (!isModalMode && panelInfo && panelInfo.name) {
console.log('[TermsOfOptional] Panel mode back pressed');
dispatch(popPanel(panelInfo.name));
} else {
console.log('[TermsOfOptional] Back pressed - showing exit popup');
dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup));
}
}, [isModalMode, closeModalCallback, dispatch, panelInfo]);
// 동의 버튼 핸들러
const handleAgree = useCallback(() => {
// 필수 약관 검증
const hasRequiredTerms = checkboxStates[TERMS_TYPE.TERMS_CONDITIONS] &&
checkboxStates[TERMS_TYPE.PRIVACY_POLICY];
if (!hasRequiredTerms) {
dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Notice"),
text: $L("Please agree to all required terms."),
button1Text: $L("OK")
}));
return;
}
// 동의한 약관 ID 배열 구성
const agreeTerms = [];
if (checkboxStates[TERMS_TYPE.TERMS_CONDITIONS]) {
agreeTerms.push(TERMS_ID_MAP[TERMS_TYPE.TERMS_CONDITIONS]);
}
if (checkboxStates[TERMS_TYPE.PRIVACY_POLICY]) {
agreeTerms.push(TERMS_ID_MAP[TERMS_TYPE.PRIVACY_POLICY]);
}
if (checkboxStates[TERMS_TYPE.OPTIONAL_TERMS]) {
agreeTerms.push(TERMS_ID_MAP[TERMS_TYPE.OPTIONAL_TERMS]);
}
console.log('최종 전송될 agreeTerms:', agreeTerms);
// registerDevice 호출
dispatch(registerDevice({ agreeTerms }));
}, [checkboxStates, dispatch]);
// registerDevice 성공 감지
useEffect(() => {
console.log('📊 deviceState 변화 감지:', deviceState);
if (deviceState?.regDeviceData &&
(deviceState.regDeviceData.retCode === 0 || deviceState.regDeviceData.retCode === "000")) {
console.log('✅ registerDevice 성공 감지');
setTimeout(() => {
if (isModalMode && closeModalCallback) {
console.log('📱 Modal mode - closeModalCallback 호출');
closeModalCallback(true);
} else {
console.log('📋 Panel mode - popPanel 호출');
dispatch(popPanel());
}
}, 1000);
}
}, [deviceState, isModalMode, closeModalCallback, dispatch]);
// 비동의 버튼 핸들러
const handleDoNotAgree = useCallback(() => {
console.log('[TermsOfOptional] Do Not Agree pressed');
dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.DO_NOT_AGREE }));
dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup, {
title: $L(""),
text: (
<div>
{$L("Are you sure you want to opt out?")}
<br />
<p style={{ color: "black", fontWeight: "bold" }}>
{$L("The service will not be available anymore after opting out.")}
</p>
</div>
),
button1Text: $L("Yes"),
button2Text: $L("No"),
onButton1Press: onExit,
onButton2Press: onClose
}));
}, [dispatch, onExit, onClose]);
// 컴포넌트 마운트 시 포커스 설정
useEffect(() => {
setSpotlightDisabled(false);
const timer = setTimeout(() => {
Spotlight.set('checkbox-all');
}, 200);
return () => clearTimeout(timer);
}, []);
// Exit Popup 포커스 관리
useEffect(() => {
if (activePopup === Config.ACTIVE_POPUP.exitPopup && popupVisible) {
const timer = setTimeout(() => {
const focusAttempts = [
() => Spotlight.set('tPopupBtn2'),
() => Spotlight.set('tPopupBtn1'),
];
for (const attempt of focusAttempts) {
if (attempt()) {
console.log('포커스 설정 성공');
break;
}
}
}, 300);
return () => clearTimeout(timer);
}
}, [activePopup, popupVisible]);
return (
<TPanel
title={title || $L("Terms and Conditions")}
handleCancel={handleBack}
className={css.panel}
spotlightId={spotlightId}
noCloseButton={isModalMode}
>
<TBody className={css.introLayout}>
{/* Welcome Section */}
<div className={css.title}>
<span className={css.welcomeText}>{$L("Welcome to")}</span>
<span className={css.txtPoint}>
<span className={css.brandShop}>Sh</span>
<span className={css.pointColor}>o</span>
<span className={css.brandPTime}>p Time !</span>
</span>
</div>
<div className={css.description}>
{$L("Check out more LIVE SHOWS now and enjoy shopping at Shop Time's special price on your TV by agreeing to the LG TV Shopping Terms and Conditions.")}
</div>
{/* Terms Section */}
<div className={css.termsItemsLayer}>
{Object.entries(TERMS_TYPE).map(([key, type]) => (
<div key={type} className={css.termItem}>
<TCheckBoxSquare
id={`checkbox-${type.toLowerCase()}`}
data-spotlight-id={`checkbox-${type.toLowerCase()}`}
selected={checkboxStates[type]}
onToggle={handleCheckboxToggle(type)}
className={css.termCheckbox}
disabled={spotlightDisabled}
spotlightDisabled={spotlightDisabled}
tabIndex={2}
aria-label={TERMS_DISPLAY_NAMES[type]}
role="checkbox"
aria-checked={checkboxStates[type]}
/>
<div className={css.termsButton}>
<TButton
data-spotlight-id={`termbox-${type.toLowerCase()}`}
onClick={() => handleTermsView(type)}
className={css.termBox}
spotlightDisabled={spotlightDisabled}
>
<span className={`${css.termName} ${isMandatoryTerms(type) ? css.termNameMandatory : ''}`}>
{TERMS_DISPLAY_NAMES[type]}
</span>
<div className={css.expandIconContainer}>
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="32" height="32" rx="16" stroke="black" strokeWidth="2.5"/>
<path d="M16.0059 25.8301L22.74 19.1108C22.8205 19.0307 22.8844 18.9355 22.928 18.8306C22.9716 18.7258 22.994 18.6133 22.994 18.4998C22.994 18.3862 22.9716 18.2738 22.928 18.1689C22.8844 18.0641 22.8205 17.9689 22.74 17.8888L16.0059 11.1701" stroke="black" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</div>
</TButton>
</div>
</div>
))}
</div>
{/* Select All Section */}
<div className={css.selectAllSection}>
<TCheckBoxSquare
id="checkbox-all"
data-spotlight-id="checkbox-all"
selected={isAllChecked}
onToggle={handleSelectAllToggle}
className={css.selectAllCheckbox}
disabled={spotlightDisabled}
spotlightDisabled={spotlightDisabled}
tabIndex={1}
/>
<span className={css.selectAllText}>{$L("Select All")}</span>
</div>
{/* Bottom Button Layer */}
<div className={css.bottomBtnLayer}>
<TButton
onClick={handleAgree}
className={css.agreeButton}
spotlightId={spotlightId + "_agreeButton"}
>
{$L("Agree")}
</TButton>
<TButton
onClick={handleDoNotAgree}
className={css.disagreeButton}
spotlightId={spotlightId + "_disagreeButton"}
>
{$L("Do Not Agree")}
</TButton>
</div>
{/* Alert Popup */}
{activePopup === Config.ACTIVE_POPUP.alertPopup && popupVisible && (
<TPopUp
kind="alertPopup"
open={popupVisible}
onClose={handleClosePopup}
onButton1Press={handleClosePopup}
hasButton
button1Text={commonPopupState.button1Text || $L("OK")}
hasText
title={commonPopupState.title}
text={commonPopupState.text}
/>
)}
{/* Exit Popup */}
{activePopup === Config.ACTIVE_POPUP.exitPopup && popupVisible && (
<TPopUp
kind="exitPopup"
open={popupVisible}
onExit={onExit}
onClose={onClose}
hasButton
button1Text={$L("Yes")}
button2Text={$L("No")}
hasText
title={$L("")}
text={
<div>
{$L("Are you sure you want to opt out?")}
<br />
<p style={{ color: "black", fontWeight: "bold" }}>
{$L("The service will not be available anymore after opting out.")}
</p>
</div>
}
/>
)}
</TBody>
</TPanel>
);
}

View File

@@ -0,0 +1,348 @@
// src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfOptional.module.less
// IntroPanel 기반으로 단순화된 스타일 - 모달 관련 문제 없음
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.panel {
> section {
color: @COLOR_GRAY06;
}
}
.introLayout {
.flex(@direction: column);
background-color: @BG_COLOR_03;
text-align: center;
width: 100%;
height: 100vh;
.title {
font-size: 60px;
padding: 44px 201px;
white-space: pre-wrap;
line-height: normal;
}
.welcomeText {
color: #807f81;
font-size: clamp(48px, 3.23vw, 62px);
font-family: 'LG Smart UI';
font-weight: 400;
line-height: 1;
word-break: break-word;
margin-right: 14px;
}
.txtPoint {
font-weight: bold;
font-size: 74px;
color: #57585a;
}
.brandShop {
color: #57585a;
}
.pointColor {
color: #c91d53;
}
.brandPTime {
color: #57585a;
}
.description {
width: 1200px;
white-space: pre-wrap;
line-height: normal;
font-size: 36px;
padding: 0 10px;
padding-top: 14px;
color: #807f81;
margin: 0 auto;
}
.termsItemsLayer {
display: flex;
flex-direction: column;
margin: 113px 0 60px 0;
gap: 20px;
align-items: center;
.termItem {
display: flex;
align-items: center;
gap: 20px;
width: 100%;
max-width: 800px;
justify-content: flex-start;
}
.termsButton {
flex: 1;
> div {
padding-right: 10px;
}
}
}
.selectAllSection {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin: 30px 0;
}
.bottomBtnLayer {
margin-top: 50px;
.flex();
gap: 40px;
}
}
// 체크박스 스타일
.termCheckbox,
.selectAllCheckbox {
width: 45px !important;
height: 45px !important;
min-width: 45px !important;
min-height: 45px !important;
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
&.tCheckBox {
border-radius: 0 !important;
border: 3px solid #C70850 !important;
background: #fff !important;
transition: background 0.15s, border 0.15s;
}
&.tCheckBox.selected {
border: 3px solid #7A808D !important;
background: #7A808D !important;
}
&.tCheckBox::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18px !important;
height: 10px !important;
border-left: 4px solid #fff !important;
border-bottom: 4px solid #fff !important;
transform: translate(-50%, -70%) rotate(-45deg) scale(0);
border-radius: 0 !important;
transition: transform 0.2s ease-in-out;
}
&.tCheckBox.selected::before {
transform: translate(-50%, -70%) rotate(-45deg) scale(1);
}
}
// 약관 박스 스타일
.termBox {
width: 100%;
min-width: 500px;
height: 80px;
padding: 0 30px;
background: white;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.15);
border-radius: 6px;
outline: 1px #CFCFCF solid;
outline-offset: -1px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
&:focus {
outline: 2px #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;
}
&: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);
}
}
.termName {
color: #1a1a1a;
font-size: 30px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 30px;
word-wrap: break-word;
transition: color 0.3s ease;
flex: 1;
text-align: left;
margin-right: 20px;
&.titleHovered {
color: #c70850;
font-weight: bold;
}
}
.termNameMandatory {
color: #207847 !important;
}
.expandIconContainer {
width: 37px;
height: 37px;
position: relative;
border-radius: 100px;
outline: 2.5px black solid;
outline-offset: -2.5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
&:hover {
transform: scale(1.05);
}
svg {
width: 100%;
height: 100%;
}
}
.selectAllText {
color: #1a1a1a;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 35px;
word-break: break-word;
}
// 버튼 스타일
.agreeButton {
width: 450px;
height: 100px;
padding-left: 50px;
padding-right: 50px;
background-color: #C70850 !important;
border: 2px solid #C70850 !important;
color: white !important;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
transition: all 0.3s ease;
&:hover,
&:focus,
&.spott-focus {
background-color: #a40640 !important;
border-color: #a40640 !important;
color: white !important;
transform: scale(1.05);
}
&:active {
background-color: #8f0533 !important;
border-color: #8f0533 !important;
color: white !important;
}
}
.disagreeButton {
width: 450px;
height: 100px;
padding-left: 50px;
padding-right: 50px;
background-color: #999999 !important;
border: 2px solid #999999 !important;
color: white !important;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 35px;
font-family: 'LG Smart UI';
font-weight: 700;
transition: all 0.3s ease;
&:hover,
&:focus,
&.spott-focus {
background-color: #7a7a7a !important;
border-color: #7a7a7a !important;
color: white !important;
transform: scale(1.05);
}
&:active {
background-color: #666666 !important;
border-color: #666666 !important;
color: white !important;
}
}
// Spotlight 포커스 스타일
[data-spotlight-id]:focus {
outline: 2px solid #C91D53;
outline-offset: 2px;
box-shadow: 0 0 8px rgba(201, 29, 83, 0.5);
}
.termBox[data-spotlight-id]:focus {
background-color: rgba(201, 29, 83, 0.1);
transform: translateY(-2px);
transition: all 0.2s ease;
}
.termCheckbox[data-spotlight-id]:focus {
transform: scale(1.05);
transition: transform 0.2s ease;
}
// IntroPanel 스타일 호환성을 위한 추가 클래스들
.introTermsConts {
background-color: #f8f8f8;
> div:nth-child(2) {
background-color: @COLOR_WHITE;
.border-solid(1px,@COLOR_GRAY02);
width: 980px;
height: 300px;
}
.termsDesc {
padding: 30px;
min-height: 300px;
color: @COLOR_GRAY03;
font-size: 24px;
line-height: 1.27;
letter-spacing: normal;
text-align: left;
.flex( @direction:column, @justifyCenter:flex-start,@alignCenter:flex-start);
}
}
.tab {
width: 980px;
}

View File

@@ -9,6 +9,7 @@ import React, {
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Spotlight from "@enact/spotlight"; import Spotlight from "@enact/spotlight";
import { Job } from "@enact/core/util";
import { import {
changeLocalSettings, changeLocalSettings,
@@ -16,8 +17,13 @@ import {
setExitApp, setExitApp,
setHidePopup, setHidePopup,
setShowPopup, setShowPopup,
getTermsAgreeYn,
} from "../../../../actions/commonActions"; } from "../../../../actions/commonActions";
import { setMyTermsWithdraw } from "../../../../actions/myPageActions"; import {
setMyTermsWithdraw,
setMyPageTermsAgree,
} from "../../../../actions/myPageActions";
import { getHomeTerms } from "../../../../actions/homeActions";
import TBody from "../../../../components/TBody/TBody"; import TBody from "../../../../components/TBody/TBody";
import TButton, { TYPES } from "../../../../components/TButton/TButton"; import TButton, { TYPES } from "../../../../components/TButton/TButton";
import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller"; import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller";
@@ -28,6 +34,7 @@ import { initialLocalSettings } from "../../../../reducers/localSettingsReducer"
import * as Config from "../../../../utils/Config"; import * as Config from "../../../../utils/Config";
import { $L, scaleH, scaleW } from "../../../../utils/helperMethods"; import { $L, scaleH, scaleW } from "../../../../utils/helperMethods";
import css from "./TermsOfService.module.less"; import css from "./TermsOfService.module.less";
import TCheckBoxSquare from "../../../../components/TCheckBox/TCheckBoxSquare";
export default function TermsOfService({ title, cbScrollTo }) { export default function TermsOfService({ title, cbScrollTo }) {
const [selectedTab, setSelectedTab] = useState(0); const [selectedTab, setSelectedTab] = useState(0);
@@ -36,8 +43,15 @@ export default function TermsOfService({ title, cbScrollTo }) {
const [trmsCdList, setTrmsCdList] = useState([]); const [trmsCdList, setTrmsCdList] = useState([]);
const [closePopUp, setClosePopUp] = useState(false); const [closePopUp, setClosePopUp] = useState(false);
const [resetScroll, setResetScroll] = useState(false); const [resetScroll, setResetScroll] = useState(false);
const [agreePopup, setAgreePopup] = useState(false);
const [isOptionalChecked, setIsOptionalChecked] = useState(false);
const [optionalDisagreePopupOpen, setOptionalDisagreePopupOpen] =
useState(false);
const [showCheckboxAlert, setShowCheckboxAlert] = useState(false);
const { popupVisible } = useSelector((state) => state.common?.popup); const { popupVisible } = useSelector((state) => state.common?.popup);
const { optionalTermsAgree } = useSelector((state) => state.common);
const termsData = useSelector((state) => state.home.termsData); const termsData = useSelector((state) => state.home.termsData);
const empTermsData = useSelector((state) => state.emp.empTermsData); const empTermsData = useSelector((state) => state.emp.empTermsData);
const webOSVersion = useSelector( const webOSVersion = useSelector(
@@ -48,6 +62,8 @@ export default function TermsOfService({ title, cbScrollTo }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const focusJob = useRef(null);
useEffect(() => { useEffect(() => {
const newTabList = []; const newTabList = [];
const tempList = []; const tempList = [];
@@ -90,6 +106,12 @@ export default function TermsOfService({ title, cbScrollTo }) {
setSpotlightDisabled(false); setSpotlightDisabled(false);
}, [termsData, empTermsData, webOSVersion]); }, [termsData, empTermsData, webOSVersion]);
useEffect(() => {
if (termsData && termsData.data && termsData.data.terms) {
dispatch(getTermsAgreeYn());
}
}, [termsData, dispatch]);
const handleItemClick = useCallback( const handleItemClick = useCallback(
({ index }) => { ({ index }) => {
setSelectedTab(index); setSelectedTab(index);
@@ -99,6 +121,29 @@ export default function TermsOfService({ title, cbScrollTo }) {
[trmsTpCd] [trmsTpCd]
); );
useEffect(() => {
if (focusJob.current) {
focusJob.current.stop();
}
if (
termsList.length > 0 &&
termsList[selectedTab]?.trmsTpCd === "MST00405"
) {
focusJob.current = new Job(() => {
const focusTarget = optionalTermsAgree
? "optional-disagree-button"
: "optional-agree-checkbox";
Spotlight.focus(focusTarget);
});
focusJob.current.startAfter(100);
}
return () => {
if (focusJob.current) {
focusJob.current.stop();
}
};
}, [selectedTab, termsList, optionalTermsAgree]);
useEffect(() => { useEffect(() => {
if (!spotlightDisabled) { if (!spotlightDisabled) {
Spotlight.focus("tab-0"); Spotlight.focus("tab-0");
@@ -154,6 +199,86 @@ export default function TermsOfService({ title, cbScrollTo }) {
dispatch(setHidePopup()); dispatch(setHidePopup());
}, [dispatch]); }, [dispatch]);
const handleOptionalCheckboxToggle = useCallback(
({ selected }) => {
if (optionalTermsAgree) return;
setIsOptionalChecked(selected);
},
[optionalTermsAgree]
);
const handleOptionalAgree = useCallback(() => {
console.log(
"handleOptionalAgree called with isChecked:",
isOptionalChecked
);
if (isOptionalChecked) {
const firstThreeTermIds = termsList
.slice(0, 3)
.map((term) => term.termsId)
.filter(Boolean);
if (firstThreeTermIds.length > 0) {
const payload = { termsList: firstThreeTermIds, notTermsList: [] };
console.log("Dispatching setMyPageTermsAgree with payload:", payload);
dispatch(
setMyPageTermsAgree(payload, (response) => {
console.log("setMyPageTermsAgree callback response:", response);
if (response.retCode === "000" || response.retCode === 0) {
console.log("Optional terms agreement successful.");
// 약관 동의의 후 약관 정보 조회
dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] }));
setAgreePopup(true);
} else {
console.error("Optional terms agreement failed:", response);
}
})
);
} else {
console.log("No terms found to agree to.");
}
} else {
console.log("Checkbox not checked, not proceeding with agreement.");
setShowCheckboxAlert(true);
}
}, [dispatch, termsList, isOptionalChecked]);
const handleOptionalDisagree = useCallback(() => {
setOptionalDisagreePopupOpen(true);
}, []);
const confirmOptionalDisagree = useCallback(() => {
const optionalTerm = termsList.find(
(term) => term.trmsTpCd === "MST00405"
);
if (optionalTerm && optionalTerm.termsId) {
const payload = {
mandatoryIncludeYn: "N",
termsList: [optionalTerm.termsId],
};
console.log("Dispatching setMyTermsWithdraw with payload:", payload);
// 선택약관 철회
dispatch(setMyTermsWithdraw(payload, (response) => {
console.log("setMyTermsWithdraw callback response:", response);
if (response.retCode === "000" || response.retCode === 0) {
console.log("Optional terms withdrawal successful.");
// 약관 철회 후 약관 정보 조회
dispatch(getHomeTerms({ trmsTpCdList: ["MST00401", "MST00402", "MST00405"] }));
setIsOptionalChecked(false);
} else {
console.error("Optional terms withdrawal failed:", response);
}
}));
} else {
console.log("No optional term found to disagree.");
}
setOptionalDisagreePopupOpen(false);
}, [termsList, dispatch]);
const onCancelDisagree = useCallback(() => {
// setDisagreeConfirmOpen(false);
}, []);
const termsAriaLabel = useMemo(() => { const termsAriaLabel = useMemo(() => {
if (!termsList || !termsList[selectedTab]) return ""; if (!termsList || !termsList[selectedTab]) return "";
@@ -179,7 +304,11 @@ export default function TermsOfService({ title, cbScrollTo }) {
selectedIndex={selectedTab && selectedTab} selectedIndex={selectedTab && selectedTab}
/> />
<TButtonScroller <TButtonScroller
boxHeight={scaleH(714)} boxHeight={
termsList[selectedTab]?.trmsTpCd === "MST00405"
? scaleH(469)
: scaleH(714)
}
width={scaleW(1680)} width={scaleW(1680)}
resetScrollPosition={resetScroll} resetScrollPosition={resetScroll}
> >
@@ -193,43 +322,87 @@ export default function TermsOfService({ title, cbScrollTo }) {
</div> </div>
</TButtonScroller> </TButtonScroller>
<div className={css.buttonBox}> {termsList[selectedTab]?.trmsTpCd === "MST00405" ? (
{empTermsData && ( <div className={css.optionalContainer}>
<div className={css.termText}> <div className={css.checkboxContainer}>
{termsList[selectedTab]?.termsId === "20211118044_US" <TCheckBoxSquare
? $L( selected={isOptionalChecked}
"You can change your agreement to the terms and conditions in your LG Account." onToggle={handleOptionalCheckboxToggle}
) spotlightId="optional-agree-checkbox"
: termsList[selectedTab]?.termsId === "20230908001_US" spotlightDisabled={spotlightDisabled || optionalTermsAgree}
? $L( disabled={optionalTermsAgree}
"If you do not wish to agree to these terms, please proceed to the following link." />
) <div
: null} className={`${css.checkboxLabel} ${
optionalTermsAgree ? css.disabledLabel : ""
}`}
>
{$L(
"I agree to receive Personalized Recommendations and Advertisements"
)}
</div>
</div> </div>
)} <div className={css.buttonContainer}>
<TButton <TButton
spotlightDisabled={spotlightDisabled} onClick={handleOptionalAgree}
onClick={ className={css.agreeButton}
termsList[selectedTab]?.trmsTpCd === "MST00401" || spotlightId="optional-agree-button"
spotlightDisabled={
spotlightDisabled || optionalTermsAgree
}
disabled={optionalTermsAgree}
>
{$L("Agree")}
</TButton>
<TButton
onClick={handleOptionalDisagree}
className={css.disagreeButton}
spotlightDisabled={spotlightDisabled}
spotlightId="optional-disagree-button"
>
{$L("Do Not Agree")}
</TButton>
</div>
</div>
) : (
<div className={css.buttonBox}>
{empTermsData && (
<div className={css.termText}>
{termsList[selectedTab]?.termsId === "20211118044_US"
? $L(
"You can change your agreement to the terms and conditions in your LG Account."
)
: termsList[selectedTab]?.termsId === "20230908001_US"
? $L(
"If you do not wish to agree to these terms, please proceed to the following link."
)
: null}
</div>
)}
<TButton
spotlightDisabled={spotlightDisabled}
onClick={
termsList[selectedTab]?.trmsTpCd === "MST00401" ||
termsList[selectedTab]?.trmsTpCd === "MST00402"
? handleDisagree
: handleLGAccount
}
type={TYPES.mypage}
className={css.locateBox}
ariaLabel={
termsList[selectedTab]?.trmsTpCd === "MST00401" ||
termsList[selectedTab]?.trmsTpCd === "MST00402"
? "Do Not Agree"
: "LG Account"
}
>
{termsList[selectedTab]?.trmsTpCd === "MST00401" ||
termsList[selectedTab]?.trmsTpCd === "MST00402" termsList[selectedTab]?.trmsTpCd === "MST00402"
? handleDisagree ? $L("Do Not Agree")
: handleLGAccount : $L("LG Account")}
} </TButton>
type={TYPES.mypage} </div>
className={css.locateBox} )}
ariaLabel={
termsList[selectedTab]?.trmsTpCd === "MST00401" ||
termsList[selectedTab]?.trmsTpCd === "MST00402"
? "Do Not Agree"
: "LG Account"
}
>
{termsList[selectedTab]?.trmsTpCd === "MST00401" ||
termsList[selectedTab]?.trmsTpCd === "MST00402"
? $L("Do Not Agree")
: $L("LG Account")}
</TButton>
</div>
</div> </div>
{/* TODO 추후 폰트 변경 */} {/* TODO 추후 폰트 변경 */}
@@ -265,6 +438,38 @@ export default function TermsOfService({ title, cbScrollTo }) {
"Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds." "Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds."
)} )}
/> />
<TPopUp
kind="textPopup"
open={agreePopup}
onClose={() => setAgreePopup(false)}
hasButton
button1Text={$L("OK")}
hasText
title={$L("Agreement Complete")}
text={$L("Your agreement has been processed.")}
/>
{/* 새로 추가된 optionalAgreement 팝업 */}
<TPopUp
kind="optionalAgreement"
open={optionalDisagreePopupOpen}
onClose={() => setOptionalDisagreePopupOpen(false)}
onClick={confirmOptionalDisagree}
hasText
text={$L("Are you sure you want to disagree with the optional terms?")}
/>
<TPopUp
kind="textPopup"
open={showCheckboxAlert}
onClose={() => setShowCheckboxAlert(false)}
hasButton
button1Text={$L("OK")}
hasText
title={$L("Notice")}
text={$L("Please check the box to agree to the terms.")}
/>
</div> </div>
</TBody> </TBody>
</> </>

View File

@@ -40,3 +40,144 @@
} }
} }
} }
.optionalContainer {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 40px;
padding-top: 40px;
padding-bottom: 40px;
}
.checkboxContainer {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
.buttonContainer {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
}
.checkboxLabel {
color: #1a1a1a;
font-size: 35px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
}
.disabledLabel {
color: #888888;
}
.agreeButton {
width: 240px;
height: 80px;
background: #c70850;
box-shadow: 0px 0px 50px rgba(0, 0, 0, 0.5);
border-radius: 12px;
color: white;
font-size: 30px;
font-weight: 700;
&:not([disabled]) {
cursor: pointer;
&:hover {
background: #a60641;
}
}
}
.disagreeButton {
width: 240px;
height: 80px;
background: #777d8a;
border-radius: 12px;
color: #e6e6e6;
font-size: 30px;
font-weight: 700;
&:not([disabled]) {
cursor: pointer;
&:hover {
background: #626771;
}
}
}
.disagreePopup {
:global {
.info {
width: 950px !important;
padding: 60px 57px 40px 57px !important;
}
.text {
font-size: 35px !important;
font-weight: 700 !important;
text-align: center !important;
}
.popup_container {
display: flex !important;
flex-direction: column !important;
gap: 30px !important;
}
.button_container {
display: flex !important;
flex-direction: row !important;
justify-content: center !important;
gap: 12px !important;
.button {
width: 380px !important;
height: 80px !important;
}
}
}
}
.disagreePopup :global(.info) {
padding: 0 !important;
}
.disagreePopupContainer {
width: 950px;
padding: 60px 57px 40px 57px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 30px;
box-sizing: border-box;
}
.disagreePopupText {
font-size: 35px;
font-weight: 700;
text-align: center;
color: #1a1a1a;
}
.disagreePopupButtonContainer {
display: flex;
flex-direction: row;
justify-content: center;
gap: 12px;
}
.disagreePopupButton {
width: 380px;
height: 80px;
}

View File

@@ -0,0 +1,171 @@
// src/components/TermsPopup/TermsPopup.jsx
/**
* ShopTime 애플리케이션의 선택적 약관 동의 팝업 컴포넌트
* * @component TermsPopup
* @description
* webOS TV 환경에서 개인정보 기반 추천/광고 동의 팝업을 표시합니다.
* 사용자는 약관 내용을 스크롤하여 확인하고 동의 또는 거부할 수 있습니다.
* * @param {Object} props
* @param {boolean} props.visible - 팝업 표시 여부
* @param {Function} props.onAgree - 동의 버튼 클릭 시 콜백 함수
* @param {Function} props.onClose - 닫기 버튼 클릭 시 콜백 함수
* @param {Object} props.termsData - 약관 데이터 객체 (title, content)
* @param {boolean} props.isAlreadyAgreed - 이미 동의한 약관인지 여부
*/
import React, { useCallback, useEffect, useState } from "react";
import Spotlight from "@enact/spotlight";
import TButton, { TYPES } from "../../../../components/TButton/TButton";
import TButtonScroller from "../../../../components/TButtonScroller/TButtonScroller";
import TPopUp from "../../../../components/TPopUp/TPopUp";
import { $L, scaleH, scaleW } from "../../../../utils/helperMethods";
import css from "./TermsPopup.module.less";
export default function TermsPopup({ visible, onAgree, onClose, termsData, isAlreadyAgreed = false }) {
const [spotlightDisabled, setSpotlightDisabled] = useState(true);
const [resetScroll, setResetScroll] = useState(false);
/**
* 팝업이 표시될 때 Spotlight 활성화
*/
useEffect(() => {
if (visible) {
setSpotlightDisabled(false);
// 팝업이 열리면 스크롤을 맨 위로 리셋
setResetScroll(true);
// 첫 번째 버튼에 포커스 설정 (약간의 지연을 두어 렌더링 완료 후 포커스)
setTimeout(() => {
// 이미 동의된 약관이면 Close 버튼에, 아니면 Agree 버튼에 포커스
const focusTargetId = isAlreadyAgreed ? "terms-close-button" : "terms-agree-button";
Spotlight.focus(focusTargetId);
}, 100);
} else {
setSpotlightDisabled(true);
}
}, [visible, isAlreadyAgreed]);
/**
* 스크롤 리셋 완료 핸들러
*/
const handleResetScrollComplete = useCallback(() => {
setResetScroll(false);
}, []);
/**
* 동의 버튼 클릭 핸들러
*/
const handleAgree = useCallback(() => {
if (onAgree) {
onAgree();
}
}, [onAgree]);
/**
* 닫기 버튼 클릭 핸들러
*/
const handleClose = useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
/**
* 약관 내용 텍스트 - termsData가 있으면 사용, 없으면 기본값
*/
const displayTitle = termsData?.title || $L("Optional Terms");
const displayContent = termsData?.content || `I agree that LG Electronics (LGE) may collect and use Basic Identifier, Commercial Information, Network Activity, Geolocation Data and Inferences, each as defined in the Shop Time Privacy Policy, for personalized and advertising purposes including to display advertisements and offers that are personalized to me or to recommend features, products or contents that might be of interest to me. I understand that I can withdraw my consent at any time by contacting LGE using the contact details [in the Shop Time Privacy Policy] or within [Shop Time application settings] and that any further information that is collected about me will not be used for personalized advertising purposes unless I provide my consent again.`;
const termsContent = displayContent.length > 0 ? displayContent : $L("No terms content available.");
console.log('TermsPopup 렌더링:', {
visible,
displayTitle,
contentLength: displayContent.length,
isAlreadyAgreed
});
if (!visible) {
return null;
}
return (
<TPopUp
kind="termsPopup"
open={visible}
onClose={handleClose}
hasButton={false}
className={css.popup}
>
<div className={css.container}>
{/* 약관 카드 */}
<div className={css.termsCard}>
{/* 헤더 */}
{/* 헤더 - 항상 고정 */}
<div className={css.header}>
<div className={css.title}>
{displayTitle}
</div>
</div>
{/* 내용 영역 */}
<div className={css.contentArea}>
<div className={css.scrollerContainer}>
<TButtonScroller
boxHeight={scaleH(460)}
width={scaleW(980)}
resetScroll={resetScroll}
onResetScrollComplete={handleResetScrollComplete}
className={css.scroller}
>
<div className={css.termsContent}>
{/* <div className={css.termsTitle}>
{$L("Interest Based Recommendation/Advertisement Agreement")}
</div> */}
<div
className={css.termsText}
dangerouslySetInnerHTML={{ __html: termsContent }}
/>
</div>
</TButtonScroller>
</div>
{/* 스크롤바 영역 (시각적 표시용) */}
<div className={css.scrollbarArea}>
<div className={css.scrollbarButton}>
<div className={css.scrollbarIcon} />
</div>
<div className={css.scrollbarTrack}>
<div className={css.scrollbarThumb} />
</div>
<div className={css.scrollbarButton}>
<div className={css.scrollbarIcon} />
</div>
</div>
</div>
</div>
{/* 버튼 영역 */}
<div className={css.buttonArea}>
<div className={css.buttonContainer}> <TButton
id="terms-agree-button"
onClick={handleAgree}
type={TYPES.primary}
className={css.agreeButton}
spotlightDisabled={spotlightDisabled}
disabled={isAlreadyAgreed}
>
{$L("Agree")}
</TButton>
<TButton
id="terms-close-button"
onClick={handleClose}
type={TYPES.secondary}
className={css.closeButton}
spotlightDisabled={spotlightDisabled}
>
{$L("Close")}
</TButton>
</div>
</div>
</div>
</TPopUp>
);
}

View File

@@ -0,0 +1,384 @@
// src/components/TermsPopup/TermsPopup.module.less
// 컬러 변수 정의
@lg-primary-color: #C70850;
@background-color: #E6EBF0;
@white: #FFFFFF;
@border-color: #CCCCCC;
@scrollbar-color: #666666;
@scrollbar-track-color: #CCCCCC;
@button-secondary-color: #777D8A;
@button-text-color: #E6E6E6;
@black: #000000;
@button-shadow: rgba(0, 0, 0, 0.50);
@popup-shadow: rgba(0, 0, 0, 0.30);
// 폰트 변수
@font-family: 'LG Smart UI', sans-serif;
// 크기 변수
@popup-padding-top: 60px;
@popup-padding-bottom: 40px;
@popup-padding-horizontal: 57px;
@header-padding: 17px 31px;
@content-padding: 31px;
@button-width: 240px;
@button-height: 80px;
@button-gap: 12px;
@content-height: 460px;
@border-radius-small: 4px;
@border-radius-medium: 12px;
// TPopUp 오버라이드 스타일
.popup {
// TPopUp 컴포넌트 기본 스타일 오버라이드
:global(.enact_ui_Popup_Popup_popup) {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
padding: @popup-padding-top @popup-padding-horizontal @popup-padding-bottom @popup-padding-horizontal;
background: @background-color;
box-shadow: 0px 20px 12px @popup-shadow;
border-radius: @border-radius-small;
display: flex;
justify-content: flex-start;
align-items: flex-start;
overflow: hidden;
}
}
// 메인 컨테이너
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 40px;
max-width: calc(100vw - 114px); // 좌우 패딩 57px * 2 = 114px 제외
max-height: calc(100vh - 100px); // 상하 패딩 60px + 40px = 100px 제외
}
// 약관 카드
.termsCard {
width: 100%;
flex: 1; // 남은 공간을 모두 차지
background: @white;
border-radius: @border-radius-small;
border: 1px solid @border-color;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
overflow: hidden; // 내용이 넘치지 않도록
}
// 헤더 영역
.header {
width: 100%;
padding: @header-padding;
background: @white;
border-bottom: 1px solid @border-color;
display: flex;
justify-content: flex-start;
align-items: center;
.title {
text-align: center;
color: @lg-primary-color;
font-size: 30px;
font-family: @font-family;
font-weight: 700;
line-height: 36px;
word-wrap: break-word;
}
}
// 내용 영역
.contentArea {
width: 100%;
flex: 1; // 헤더를 제외한 남은 공간 차지
display: flex;
justify-content: flex-start;
align-items: flex-start;
min-height: 0; // flexbox 축소 허용
}
// 스크롤러 컨테이너 - 전체 영역 차지
.scrollerContainer {
width: 100%;
height: 100%; // contentArea의 높이에 맞춤
padding: @content-padding;
display: flex;
justify-content: flex-start;
align-items: flex-start;
min-width: 0; // flexbox 축소 허용
}
// TButtonScroller 커스터마이징
.scroller {
width: 100%;
height: 100%;
// TButtonScroller 컴포넌트 스타일 오버라이드
:global(.enact_ui_Scroller_Scroller_scroller) {
width: 100%;
height: 100%;
}
// 스크롤바를 오른쪽에 표시하고 커스텀 스타일 적용
:global(.enact_ui_Scroller_Scroller_scrollbar) {
position: absolute !important;
right: 0 !important;
top: 0 !important;
width: 44px !important;
height: 100% !important;
background: transparent !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-between !important;
align-items: center !important;
}
// 스크롤바 썸 (가운데 트랙 부분)
:global(.enact_ui_Scroller_Scroller_thumb) {
width: 4px !important;
background: @scrollbar-color !important;
border-radius: 0 !important;
position: absolute !important;
left: 20px !important; // 44px의 중앙
}
// 스크롤바 트랙 영역
:global(.enact_ui_Scroller_Scroller_track) {
width: 4px !important;
background: @scrollbar-track-color !important;
position: absolute !important;
left: 20px !important; // 44px의 중앙
top: 44px !important; // 상단 버튼 높이만큼
bottom: 44px !important; // 하단 버튼 높이만큼
}
// 스크롤바 영역에 상하 버튼 추가 (CSS로 구현)
:global(.enact_ui_Scroller_Scroller_scrollbar)::before {
content: '';
width: 44px;
height: 44px;
background: #9C9898;
box-shadow: 0px 8px 6.6px rgba(156, 152, 152, 0.34);
position: relative;
flex-shrink: 0;
}
:global(.enact_ui_Scroller_Scroller_scrollbar)::after {
content: '';
width: 44px;
height: 44px;
background: #9C9898;
box-shadow: 0px 8px 6.6px rgba(156, 152, 152, 0.34);
position: relative;
flex-shrink: 0;
}
}
// 약관 내용
.termsContent {
width: 100%;
height: auto;
.termsTitle {
color: @black;
font-size: 26px;
font-family: @font-family;
font-weight: 700;
word-wrap: break-word;
margin-bottom: 20px;
}
.termsText {
color: @black;
font-size: 26px;
font-family: @font-family;
font-weight: 400;
word-wrap: break-word;
line-height: 1.4;
}
}
// 스크롤바 영역 제거 (TButtonScroller의 기본 스크롤바 사용)
// .scrollbarArea 클래스 전체 제거
// 버튼 영역
.buttonArea {
width: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0 229px; // 상하 패딩 제거, 좌우만 유지
flex-shrink: 0; // 축소되지 않도록
}
// 버튼 컨테이너
.buttonContainer {
display: flex;
justify-content: flex-start;
align-items: center;
gap: @button-gap;
}
// TButton 커스터마이징
.agreeButton {
width: @button-width;
height: @button-height;
max-width: 450px;
min-width: 150px;
border-radius: @border-radius-medium;
// TButton primary 타입 오버라이드
:global(.enact_ui_Button_Button_button) {
background: @lg-primary-color !important;
box-shadow: 0px 0px 50px @button-shadow;
border: none;
width: 100%;
height: 100%;
border-radius: @border-radius-medium;
transition: all 0.2s ease;
color: @white !important;
font-size: 30px !important;
font-family: @font-family !important;
font-weight: 700 !important;
text-align: center;
&:hover {
transform: translateY(-2px);
box-shadow: 0px 5px 55px @button-shadow;
}
&:active {
transform: translateY(0);
box-shadow: 0px 0px 50px @button-shadow;
}
&:focus {
outline: 3px solid @white;
outline-offset: 2px;
}
}
}
.closeButton {
width: @button-width;
height: @button-height;
max-width: 450px;
min-width: 150px;
border-radius: @border-radius-medium;
// TButton secondary 타입 오버라이드
:global(.enact_ui_Button_Button_button) {
background: @button-secondary-color !important;
border-radius: @border-radius-medium;
border: none;
width: 100%;
height: 100%;
transition: all 0.2s ease;
color: @button-text-color !important;
font-size: 30px !important;
font-family: @font-family !important;
font-weight: 700 !important;
text-align: center;
&:hover {
transform: translateY(-2px);
background: lighten(@button-secondary-color, 10%) !important;
}
&:active {
transform: translateY(0);
}
&:focus {
outline: 3px solid @white;
outline-offset: 2px;
}
}
}
// 반응형 디자인
@media screen and (max-width: 1280px) {
.popup :global(.enact_ui_Popup_Popup_popup) {
padding: 30px;
}
.container {
max-width: calc(100vw - 60px);
max-height: calc(100vh - 60px);
}
.buttonArea {
padding: 0 100px;
}
.header .title,
.termsContent .termsTitle,
.termsContent .termsText {
font-size: 24px;
}
.agreeButton,
.closeButton {
width: 200px;
height: 70px;
:global(.enact_ui_Button_Button_button) {
font-size: 26px !important;
}
}
}
@media screen and (max-width: 960px) {
.popup :global(.enact_ui_Popup_Popup_popup) {
padding: 20px;
}
.container {
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
}
.buttonArea {
padding: 0 50px;
}
.header .title,
.termsContent .termsTitle,
.termsContent .termsText {
font-size: 20px;
}
.agreeButton,
.closeButton {
width: 180px;
height: 60px;
:global(.enact_ui_Button_Button_button) {
font-size: 22px !important;
}
}
.contentArea {
flex-direction: column;
}
.scrollerContainer {
height: 300px;
}
.scrollbarArea {
display: none;
}
}

View File

@@ -0,0 +1,66 @@
// 약관 공통 상수 및 헬퍼 함수 (TermsOfService, TermsOfOptional에서 재사용)
// 약관 유형 상수 정의
export const TERMS_TYPE = {
TERMS_CONDITIONS: "TERMS_CONDITIONS",
PRIVACY_POLICY: "PRIVACY_POLICY",
OPTIONAL_TERMS: "OPTIONAL_TERMS",
};
// 약관 ID 매핑
export const TERMS_ID_MAP = {
[TERMS_TYPE.TERMS_CONDITIONS]: "TID0000222",
[TERMS_TYPE.PRIVACY_POLICY]: "TID0000223",
[TERMS_TYPE.OPTIONAL_TERMS]: "TID0000232"
};
export const TERMS_TPCD_MAP = {
[TERMS_TYPE.TERMS_CONDITIONS]: "MST00402",
[TERMS_TYPE.PRIVACY_POLICY]: "MST00401",
[TERMS_TYPE.OPTIONAL_TERMS]: "MST00405"
};
export const TERMS_TYPE_LIST = [
TERMS_TYPE.TERMS_CONDITIONS,
TERMS_TYPE.PRIVACY_POLICY,
TERMS_TYPE.OPTIONAL_TERMS
];
// 약관 표시 이름 매핑
export const TERMS_DISPLAY_NAMES = {
[TERMS_TYPE.TERMS_CONDITIONS]: "Terms and Conditions",
[TERMS_TYPE.PRIVACY_POLICY]: "Privacy Policy",
[TERMS_TYPE.OPTIONAL_TERMS]: "Optional Terms"
};
// 약관 데이터 파싱 헬퍼 함수
export const parseTermsList = (termsData) => {
if (!termsData) return [];
return termsData.map(term => ({
...term,
displayName: TERMS_DISPLAY_NAMES[term.trmsTpCd] || term.trmsNm
}));
};
// 체크박스 초기 상태 생성 헬퍼
export const getInitialCheckboxStates = () => ({
[TERMS_TYPE.TERMS_CONDITIONS]: false,
[TERMS_TYPE.PRIVACY_POLICY]: false,
[TERMS_TYPE.OPTIONAL_TERMS]: false
});
// 필수 약관 여부 확인 헬퍼
export const isMandatoryTerms = (termsType) => {
return (
termsType === TERMS_TYPE.TERMS_CONDITIONS ||
termsType === TERMS_TYPE.PRIVACY_POLICY
);
};
// HTML 태그 제거 헬퍼
export const stripHtmlTags = (htmlText) => {
if (!htmlText) return "";
return htmlText.replace(/(<([^>]+)>)/gi, "");
};