fix: 선택약관 4차수정 250622

This commit is contained in:
djaco
2025-06-22 22:19:57 +09:00
parent f0418b3c2b
commit 556a0e8456
23 changed files with 4437 additions and 477 deletions

View File

@@ -1,28 +1,41 @@
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import platform from "@enact/core/platform";
import { Job } from "@enact/core/util";
import ThemeDecorator from "@enact/sandstone/ThemeDecorator";
import React, {
// useMemo,
useCallback,
useEffect,
useRef,
// useState,
} from "react";
import { useSelector, useDispatch } from "react-redux";
// import { I18nContext } from "@enact/i18n";
// import classNames from "classnames";
// import PropTypes from "prop-types";
import Spotlight from "@enact/spotlight";
import { Job } from "@enact/core/util";
import platform from "@enact/core/platform";
import { ThemeDecorator } from "@enact/sandstone/ThemeDecorator";
// import "../../../assets/fontello/css/fontello.css";
import appinfo from "../../webos-meta/appinfo.json";
import {
changeAppStatus,
checkFirstLaunch,
// cancelFocusElement,
// focusElement,
// setExitApp,
// setPreventMouse,
// setShowPopup,
// setTermsAgreeYn,
getTermsAgreeYn,
deleteOldDb8Datas,
getConnectionInfo,
sendBroadCast,
getConnectionStatus,
getConnectionInfo,
getDeviceId,
getHttpHeaderForServiceRequest,
getSystemSettings,
sendBroadCast,
checkFirstLaunch,
setDeepLink,
setGNBMenu,
setSecondLayerInfo,
setShowPopup,
getTermsAgreeYn,
} from "../actions/commonActions";
import { getShoptimeTerms } from "../actions/empActions";
import { getHomeMenu, getHomeTerms } from "../actions/homeActions";
@@ -36,7 +49,14 @@ import usePrevious from "../hooks/usePrevious";
import { lunaTest } from "../lunaSend/lunaTest";
import { store } from "../store/store";
import * as Config from "../utils/Config";
import { $L, clearLaunchParams, getLaunchParams } from "../utils/helperMethods";
import {
// $L,
clearLaunchParams,
// getCountry,
getLaunchParams,
// getUUID,
// resizeTo,
} from "../utils/helperMethods";
import { SpotlightIds } from "../utils/SpotlightIds";
import ErrorBoundary from "../views/ErrorBoundary";
import MainView from "../views/MainView/MainView";
@@ -44,7 +64,11 @@ import css from "./App.module.less";
import { handleBypassLink } from "./bypassLinkHandler";
import { handleDeepLink } from "./deepLinkHandler";
import { sendLogTotalRecommend } from "../actions/logActions";
import { startFocusMonitoring, stopFocusMonitoring } from '../utils/focus-monitor';
// import {
// startFocusMonitoring,
// stopFocusMonitoring,
// } from "../utils/focus-monitor";
// import { PanelHoc } from "../components/TPanel/TPanel";
let foreGroundChangeTimer = null;
@@ -80,7 +104,7 @@ Spotlight.focus = function (elem, containerOption) {
if (!floatLayerNode.contains(current)) {
if (floatLayerNode.lastElementChild) {
const spottableNode = floatLayerNode.lastElementChild.querySelector(
'[data-spotlight-container="true"]'
'[data-spotlight-container="true"]',
);
if (spottableNode) {
originFocus.apply(this, [spottableNode]); // this 바인딩을 유지하여 originFocus 호출
@@ -98,7 +122,7 @@ Spotlight.focus = function (elem, containerOption) {
sendBroadCast({
type: "deActivateTab",
moreInfo: { reason: "focus" },
})
}),
);
}
}
@@ -111,15 +135,15 @@ function AppBase(props) {
const httpHeader = useSelector((state) => state.common.httpHeader);
const httpHeaderRef = useRef(httpHeader);
const webOSVersion = useSelector(
(state) => state.common.appStatus.webOSVersion
(state) => state.common.appStatus.webOSVersion,
);
const deviceId = useSelector((state) => state.common.appStatus.deviceId);
const loginUserData = useSelector(
(state) => state.common.appStatus.loginUserData
(state) => state.common.appStatus.loginUserData,
);
const loginUserDataRef = useRef(loginUserData);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
(state) => state.common.appStatus.cursorVisible,
);
const introTermsAgree = useSelector((state) => state.common.introTermsAgree);
// const optionalTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
@@ -128,15 +152,6 @@ function AppBase(props) {
// 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' && optionalTerm.trmsAgrFlag === 'N' : false;
// }, [termsData]);
useEffect(() => {
if (termsData?.data?.terms) {
dispatch(getTermsAgreeYn());
@@ -146,9 +161,9 @@ function AppBase(props) {
const introTermsAgreeRef = usePrevious(introTermsAgree);
const logEnable = useSelector((state) => state.localSettings.logEnable);
const oldDb8Deleted = useSelector(
(state) => state.localSettings.oldDb8Deleted
(state) => state.localSettings.oldDb8Deleted,
);
const macAddress = useSelector((state) => state.common.macAddress);
// const macAddress = useSelector((state) => state.common.macAddress);
const deviceCountryCode = httpHeader?.["X-Device-Country"] || "";
@@ -170,13 +185,13 @@ function AppBase(props) {
if (!oldDb8Deleted) {
dispatch(deleteOldDb8Datas());
}
}, [oldDb8Deleted]);
}, [oldDb8Deleted, dispatch]);
const hideCursor = useRef(
new Job((func) => {
func();
console.log("hide cursor");
}, 5000)
}, 5000),
);
// 컴포넌트에서 모니터링 시작 - 한시적 모니터링
@@ -199,11 +214,13 @@ function AppBase(props) {
// called by [receive httpHeader, launch, relaunch]
const initService = useCallback(
(haveyInit = true) => {
/*
console.log(
"<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ",
haveyInit,
appinfo
);
*/
if (httpHeaderRef.current) {
if (haveyInit) {
dispatch(changeAppStatus({ connectionFailed: false }));
@@ -211,7 +228,7 @@ function AppBase(props) {
dispatch(
changeAppStatus({
cursorVisible: window.PalmSystem?.cursor?.visibility,
})
}),
);
}
dispatch(getHomeMenu());
@@ -224,7 +241,7 @@ function AppBase(props) {
console.log(
"initService...{haveyInit, launchParams}",
haveyInit,
JSON.stringify(launchParams)
JSON.stringify(launchParams),
);
// pyh TODO: edit or delete later (line 196 ~ 198)
@@ -239,7 +256,7 @@ function AppBase(props) {
contextName: Config.LOG_CONTEXT_NAME.ENTRY,
messageId: Config.LOG_MESSAGE_ID.ENTRY_INFO,
entry_menu: "App",
})
}),
);
}
@@ -249,12 +266,12 @@ function AppBase(props) {
contextName: Config.LOG_CONTEXT_NAME.SHOPTIME,
messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE,
visible: true,
})
}),
);
clearLaunchParams();
}
},
[dispatch]
[dispatch],
);
const handleRelaunchEvent = useCallback(() => {
@@ -265,7 +282,7 @@ function AppBase(props) {
if (typeof window === "object" && window.PalmSystem) {
window.PalmSystem.activate();
}
}, [initService, dispatch]);
}, [initService, introTermsAgreeRef]);
const visibilityChanged = useCallback(() => {
console.log("document is hidden", document.hidden);
@@ -279,7 +296,7 @@ function AppBase(props) {
foreGroundChangeTimer = setTimeout(() => {
console.log(
"visibility changed !!! ==> set to foreground cursorVisible",
JSON.stringify(window.PalmSystem?.cursor?.visibility)
JSON.stringify(window.PalmSystem?.cursor?.visibility),
); // eslint-disable-line no-console
if (platform.platformName !== "webos") {
//for debug
@@ -287,19 +304,19 @@ function AppBase(props) {
changeAppStatus({
isAppForeground: true,
cursorVisible: !platform.touchscreen,
})
}),
);
} else if (typeof window === "object") {
dispatch(
changeAppStatus({
isAppForeground: true,
cursorVisible: window.PalmSystem?.cursor?.visibility,
})
}),
);
}
}, 1000);
}
}, [dispatch, initService]);
}, [dispatch]);
useEffect(() => {
const keyDownEvent = (event) => {
@@ -342,7 +359,7 @@ function AppBase(props) {
document.removeEventListener("wheel", mouseMoveEvent);
}
};
}, [dispatch]);
}, [dispatch, visibilityChanged, handleRelaunchEvent]);
useEffect(() => {
let userDataChanged = false;
@@ -358,7 +375,7 @@ function AppBase(props) {
dispatch(
changeAppStatus({
showLoadingPanel: { show: true, type: "launching" },
})
}),
);
}
dispatch(checkFirstLaunch());
@@ -367,13 +384,13 @@ function AppBase(props) {
getHomeTerms({
mbrNo: loginUserData.userNumber,
trmsTpCdList: "MST00401, MST00402, MST00405", // 선택약관 추가 25.06
})
}),
);
httpHeaderRef.current = httpHeader;
}
loginUserDataRef.current = loginUserData;
}, [httpHeader, deviceId]);
}, [httpHeader, deviceId, dispatch, loginUserData]);
useEffect(() => {
if (
@@ -385,8 +402,7 @@ function AppBase(props) {
) {
dispatch(getShoptimeTerms());
}
}, [webOSVersion, deviceId]);
}, [webOSVersion, deviceId, dispatch, deviceCountryCode]);
// 테스트용 인트로 화면 표시
// useEffect(() => {
@@ -404,29 +420,15 @@ function AppBase(props) {
}
if (introTermsAgree) {
// 필수 약관에 동의한 경우
// if (shouldShowOptionalTermsPopup) {
// 선택 약관 팝업을 띄워야 하는 경우
// 3초 후에 팝업을 띄우도록 설정
// console.log("App.js optionalTermsTest 팝업 표시");
// const timer = setTimeout(() => {
// dispatch(setShowPopup({ activePopup: "optionalTermsConfirm" }));
// }, 3000); // 3000 milliseconds = 3 seconds
// 컴포넌트 언마운트 시 타이머 클리어
// return () => clearTimeout(timer);
// } else {
// 선택 약관 팝업이 필요 없는 경우, 바로 서비스 초기화
initService(true);
// }
} else {
// 필수 약관에 동의하지 않은 경우
dispatch(
pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} })
pushPanel({ name: Config.panel_names.INTRO_PANEL, panelInfo: {} }),
);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
}, [introTermsAgree, dispatch, initService]);
}, [introTermsAgree, dispatch, initService, termsLoading]);
useEffect(() => {
const launchParmas = getLaunchParams();
@@ -442,7 +444,7 @@ function AppBase(props) {
setDeepLink({
contentTarget: launchParmas.contentTarget,
isDeepLink: true,
})
}),
);
}
@@ -452,9 +454,9 @@ function AppBase(props) {
deeplinkId: launchParmas.contentTarget ?? "",
linkTpCd,
logTpNo: Config.LOG_TP_NO.SECOND_LAYER,
})
}),
);
}, [dispatch]);
}, [dispatch, initService]);
return (
<ErrorBoundary>

View File

@@ -194,6 +194,17 @@ export const types = {
GET_SUBTITLE: "GET_SUBTITLE",
CLEAR_PLAYER_INFO: "CLEAR_PLAYER_INFO",
/**
* 홈 화면 배너의 비디오 재생 제어를 위한 액션 타입.
* 여러 컴포넌트가 동시에 비디오를 재생하려고 할 때 충돌을 방지하고,
* 하나의 비디오만 재생되도록 보장하는 역할을 합니다.
*
* SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다.
* CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다.
*/
SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL",
CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL",
// reset action
RESET_REDUX_STATE: "RESET_REDUX_STATE",

View File

@@ -25,18 +25,21 @@ let startVideoTimer = null;
//start Full -> modal mode
let startVideoFocusTimer = null;
export const startVideoPlayer =
({ modal, modalContainerId, modalClassName, spotlightDisable, ...rest }) =>
({ modal, modalContainerId, modalClassName, spotlightDisable, useNewPlayer, ...rest }) =>
(dispatch, getState) => {
const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1];
let panelWorkingAction = pushPanel;
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL) {
const panelName = useNewPlayer ? panel_names.PLAYER_PANEL_NEW : panel_names.PLAYER_PANEL;
if (topPanel && topPanel.name === panelName) {
panelWorkingAction = updatePanel;
}
dispatch(
panelWorkingAction(
{
name: panel_names.PLAYER_PANEL,
name: panelName,
panelInfo: {
modal,
modalContainerId,
@@ -116,3 +119,52 @@ export const getSubTitle =
export const CLEAR_PLAYER_INFO = () => ({
type: types.CLEAR_PLAYER_INFO,
});
/**
* 비디오 재생 제어권을 요청하는 액션.
* 컴포넌트가 비디오를 재생하고 싶을 때 이 액션을 호출합니다.
* 'playerControl' 상태를 확인하여 현재 다른 컴포넌트가 비디오를 제어하고 있지 않은 경우에만
* 비디오 플레이어를 활성화(PlayerPanel을 modal로 push)합니다.
*
* @param {string} ownerId - 비디오 제어권을 요청하는 컴포넌트의 고유 ID.
* @param {object} videoInfo - 'startVideoPlayer'에 필요한 비디오 정보.
*/
export const requestPlayControl =
(ownerId, videoInfo) => (dispatch, getState) => {
const { playerControl } = getState().home;
const currentOwnerId = playerControl.ownerId;
// 이미 다른 컴포넌트가 제어권을 가지고 있다면, 먼저 해제한다. (선점)
if (currentOwnerId && currentOwnerId !== ownerId) {
dispatch(releasePlayControl(currentOwnerId, true)); // fromPreemption = true
}
// 새로운 제어권을 설정하고 비디오를 재생한다.
dispatch({
type: types.SET_PLAYER_CONTROL,
payload: { ownerId },
});
dispatch(startVideoPlayer({ ...videoInfo, modal: true }));
};
/**
* 비디오 재생 제어권을 해제하는 액션.
* 컴포넌트가 비디오 재생을 중단할 때(예: 포커스 잃음, 언마운트) 호출합니다.
* 현재 제어권을 가진 컴포넌트가 자신일 경우에만 'playerControl' 상태를 초기화하고
* 비디오 플레이어를 종료(PlayerPanel을 pop)합니다.
*
* @param {string} ownerId - 비디오 제어권을 해제하려는 컴포넌트의 고유 ID.
* @param {boolean} fromPreemption - 다른 요청에 의해 강제로 해제되었는지 여부.
*/
export const releasePlayControl = (ownerId, fromPreemption = false) => (dispatch, getState) => {
const { playerControl } = getState().home;
// 제어권을 가진 컴포넌트가 자신일 경우에만 해제
// 단, 선점 로직에 의해 호출된 경우는 소유권 확인 없이 즉시 실행
if (fromPreemption || playerControl.ownerId === ownerId) {
dispatch({
type: types.CLEAR_PLAYER_CONTROL,
});
dispatch(finishVideoPreview());
}
};

View File

@@ -145,7 +145,7 @@
border-radius: 10px;
box-sizing: border-box;
.flex();
box-shadow: 0 5px 5px #003 0 6px 7px #0000001a;
box-shadow: 0 5px 5px #003, 0 6px 7px #0000001a;
line-height: normal;
&:focus {
@@ -165,7 +165,7 @@
border-radius: 10px;
box-sizing: border-box;
.flex();
box-shadow: 0 5px 5px #003 0 6px 7px #0000001a;
box-shadow: 0 5px 5px #003, 0 6px 7px #0000001a;
line-height: normal;
&:focus {

View File

@@ -80,6 +80,7 @@ export default memo(function TItemCard({
nowProductId,
nowCategory,
nowProductTitle,
contentId,
...rest
}) {
const dispatch = useDispatch();

View File

@@ -286,6 +286,8 @@ export default function TNewPopUp({
onOptionalAgreeClick, // Agree 버튼용
onOptionalDeclineClick, // Not Now 버튼용
onIntroTermsAgreeClick, // introTerms Agree 버튼용
showAgreeButton = false,
onAgreeClick, // onIntroTermsAgreeClick을 대체할 새로운 prop
...rest
}) {
const dispatch = useDispatch();
@@ -750,20 +752,21 @@ export default function TNewPopUp({
</div>
</div>
<ButtonContainerComp className={getClassName(kind, "buttonContainer")}>
{/* 기획 변경으로 Agree 버튼 임시 숨김 처리
{showAgreeButton && (
<TButton
className={getClassName(kind, "agreeButton")}
onClick={onIntroTermsAgreeClick}
onClick={onAgreeClick}
spotlightId="figma-terms-agree"
type="agree"
type="popup"
>
{$L('Agree')}
</TButton>
*/}
)}
<TButton
className={getClassName(kind, "closeButton")}
onClick={onClose}
spotlightId="figma-terms-close"
type="popup"
>
{$L('Close')}
</TButton>

View File

@@ -1082,22 +1082,18 @@
.figmaTermsButtonContainer {
display: flex;
justify-content: center;
gap: 12px;
align-items: center; // 버튼 수직 정렬을 위해 추가
gap: 15px; // 버튼 사이 간격
}
.figmaTermsAgreeButton {
.size(240px, 80px);
// type="agree"의 포커스 시 font-size 변경을 막음
&.focused,
&:focus { // :focus도 함께 처리
font-size: 30px !important;
}
// 이제 TButton의 type="popup" 스타일을 사용하므로,
// 여기서는 추가적인 스타일이 필요 없습니다.
// margin-right는 gap으로 대체되었습니다.
}
.figmaTermsCloseButton {
// TButton의 기본 스타일을 그대로 사용하도록 크기만 지정
.size(240px, 80px);
// TButton의 type="popup" 스타일을 사용합니다.
}
}
}

View File

@@ -0,0 +1,113 @@
import React, { useState, useEffect, useRef } from "react";
import { $L } from "../utils/helperMethods";
import OptionalTermsInfo from "../views/MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo";
import css from "../views/IntroPanel/IntroPanel.new.module.less";
const getPanelContent = (
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView
) => {
const requiredItemIds = [
"termsCheckbox",
"termsButton",
"privacyCheckbox",
"privacyButton",
];
const isRequiredFocused = requiredItemIds.includes(focusedItem);
// 1순위: 포커스가 필수 약관 관련 아이템에 있는 경우
if (isRequiredFocused) {
// 필수 약관이 동의되지 않았을 때 (경고 메시지 포함)
if (!termsChecked || !privacyChecked) {
return (
<div className={css.requiredInfoPanel}>
<p className={css.infoText}>{$L("Required Consent")}</p>
<p className={css.infoText}>
{$L("(Necessary for using the service)")}
</p>
<div className={css.warningContainer}>
<p className={css.warningText}>
{$L("Please agree to the required Terms & Conditions and")}
</p>
<p className={css.warningText}>
{$L("Privacy Policy to start enjoying Shop Time")}
</p>
</div>
</div>
);
}
// 필수 약관이 모두 동의되었을 때 (단순 안내 메시지)
return (
<div className={css.requiredInfoPanel}>
<p className={css.infoText}>{$L("Required Consent")}</p>
<p className={css.infoText}>
{$L("(Necessary for using the service)")}
</p>
</div>
);
}
// 2순위: 포커스가 그 외 모든 아이템에 있는 경우
return (
<>
{shouldShowBenefitsView ? (
<div className={css.optionalDescription}>
{$L(
'By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features'
)}
</div>
) : (
<OptionalTermsInfo
displayMode="image"
imageTitle={$L("Agree and Enjoy Special Benefits")}
spotlightId="optional-terms-info"
/>
)}
</>
);
};
const useRightPanelContent = (
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView
) => {
const [content, setContent] = useState(() =>
getPanelContent(
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView
)
);
const updateTimeoutRef = useRef(null);
useEffect(() => {
if (updateTimeoutRef.current) {
clearTimeout(updateTimeoutRef.current);
}
updateTimeoutRef.current = setTimeout(() => {
const newContent = getPanelContent(
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView
);
setContent(newContent);
}, 50);
return () => {
if (updateTimeoutRef.current) {
clearTimeout(updateTimeoutRef.current);
}
};
}, [focusedItem, termsChecked, privacyChecked, shouldShowBenefitsView]);
return content;
};
export default useRightPanelContent;

View File

@@ -0,0 +1,40 @@
import { useState, useCallback, useRef } from "react";
const useSafeFocusState = () => {
const [focusedItem, setFocusedItem] = useState(null);
const blurTimeoutRef = useRef(null);
const setFocusAsync = useCallback((item) => {
return new Promise((resolve) => {
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
blurTimeoutRef.current = null;
}
if (process.env.NODE_ENV === "development") {
console.log("[useSafeFocusState] Focus Set:", item);
}
setFocusedItem(item);
resolve(item);
});
}, []);
const clearFocusAsync = useCallback((delay = 0) => {
return new Promise((resolve) => {
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
}
blurTimeoutRef.current = setTimeout(() => {
if (process.env.NODE_ENV === "development") {
console.log("[useSafeFocusState] Focus Cleared");
}
setFocusedItem(null);
blurTimeoutRef.current = null;
resolve();
}, delay);
});
}, []);
return { focusedItem, setFocusAsync, clearFocusAsync };
};
export default useSafeFocusState;

View File

@@ -0,0 +1,95 @@
import { useState, useCallback } from "react";
const useTermsStateMachine = (initialState) => {
const [isTransitioning, setIsTransitioning] = useState(false);
const [state, setState] = useState({
termsChecked: false,
privacyChecked: false,
optionalChecked: false,
selectAllChecked: false,
error: null, // 1. 에러 상태 추가
...initialState,
});
const updateStateAsync = useCallback(
async (updates) => {
if (isTransitioning) {
const err = new Error("State transition already in progress.");
if (process.env.NODE_ENV === "development") {
console.warn("[useTermsStateMachine] Rejected:", err.message);
}
// 2. 에러 상태를 설정하고 throw
setState((s) => ({ ...s, error: err }));
throw err;
}
setIsTransitioning(true);
// 3. 이전 에러 상태 초기화
setState((s) => ({ ...s, error: null }));
try {
const newState = await new Promise((resolve) => {
setState((currentState) => {
const nextState = { ...currentState, ...updates };
const {
termsChecked: currentTerms,
privacyChecked: currentPrivacy,
optionalChecked: currentOptional,
} = currentState;
const termsChecked =
"termsChecked" in updates
? updates.termsChecked
: currentTerms;
const privacyChecked =
"privacyChecked" in updates
? updates.privacyChecked
: currentPrivacy;
const optionalChecked =
"optionalChecked" in updates
? updates.optionalChecked
: currentOptional;
if (
"termsChecked" in updates ||
"privacyChecked" in updates ||
"optionalChecked" in updates
) {
nextState.selectAllChecked =
termsChecked && privacyChecked && optionalChecked;
} else if ("selectAllChecked" in updates) {
nextState.termsChecked = updates.selectAllChecked;
nextState.privacyChecked = updates.selectAllChecked;
nextState.optionalChecked = updates.selectAllChecked;
}
if (process.env.NODE_ENV === "development") {
console.log(
"[useTermsStateMachine] State transition:",
currentState,
"->",
nextState
);
}
resolve(nextState);
return nextState;
});
});
setIsTransitioning(false);
return newState;
} catch (error) {
setIsTransitioning(false);
console.error("[useTermsStateMachine] State update failed:", error);
// 4. 에러 상태를 설정하고 다시 throw
setState((s) => ({ ...s, error }));
throw error;
}
},
[isTransitioning]
);
return { state, updateStateAsync };
};
export default useTermsStateMachine;

View File

@@ -21,6 +21,9 @@ const initialState = {
homeInfo: null,
curationId: "",
curationTitle: "",
playerControl: {
ownerId: null,
},
};
export const homeReducer = (state = initialState, action) => {
@@ -189,6 +192,26 @@ export const homeReducer = (state = initialState, action) => {
}
}
case types.SET_PLAYER_CONTROL: {
return {
...state,
playerControl: {
...state.playerControl,
ownerId: action.payload.ownerId,
},
};
}
case types.CLEAR_PLAYER_CONTROL: {
return {
...state,
playerControl: {
...state.playerControl,
ownerId: null,
},
};
}
default:
return state;
}

View File

@@ -25,6 +25,7 @@ export const panel_names = {
CONFIRM_PANEL: "confirmpanel",
DETAIL_PANEL: "detailpanel",
PLAYER_PANEL: "playerpanel",
PLAYER_PANEL_NEW: "playerpanel.new",
CHECKOUT_PANEL: "checkoutpanel",
THEME_CURATION_PANEL: "themeCurationPanel",
IMAGE_PANEL: "imagepanel",

View File

@@ -6,28 +6,38 @@ import { useDispatch, useSelector } from "react-redux";
import Spotlight from "@enact/spotlight";
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import Spottable from "@enact/spotlight/Spottable";
import { $L, scaleH, scaleW } from '../../../utils/helperMethods';
import { setDefaultFocus, setShowPopup, fetchCurrentUserHomeTerms } from "../../../actions/homeActions";
import { $L, scaleH, scaleW } from "../../../utils/helperMethods";
import {
setDefaultFocus,
setShowPopup,
fetchCurrentUserHomeTerms,
} from "../../../actions/homeActions";
import { changeAppStatus } from "../../../actions/commonActions";
import { setMyPageTermsAgree } from '../../../actions/myPageActions';
import { setMyPageTermsAgree } from "../../../actions/myPageActions";
import { pushPanel } from "../../../actions/panelActions";
import {
requestPlayControl,
releasePlayControl,
} from "../../../actions/playActions";
import CustomImage from "../../../components/CustomImage/CustomImage";
import css from "./HomeBanner.module.less";
import Random from "./RandomUnit";
import Rolling from "./RollingUnit";
import RandomUnitNew from "./RandomUnit.new";
import TNewPopUp from "../../../components/TPopUp/TNewPopUp";
import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller";
// import TButtonScroller from "../../../components/TButtonScroller/TButtonScroller";
import OptionalConfirm from "../../../components/Optional/OptionalConfirm";
import * as Config from "../../../utils/Config";
// import * as Config from "../../../utils/Config";
import PersistentVideoUnit from "./PersistentVideoUnit";
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
"div",
);
const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
"div",
);
export default function HomeBanner({
@@ -38,11 +48,11 @@ export default function HomeBanner({
}) {
const dispatch = useDispatch();
const homeTopDisplayInfo = useSelector(
(state) => state.home.homeTopDisplayInfo
(state) => state.home.homeTopDisplayInfo,
);
const bannerDataList = useSelector(
(state) => state.home.bannerData?.bannerInfos
(state) => state.home.bannerData?.bannerInfos,
);
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
@@ -57,9 +67,39 @@ export default function HomeBanner({
}
}, [handleItemFocus]);
const handleSecondBannerFocus = useCallback(() => {
const secondBannerData = bannerDataList?.[1];
if (secondBannerData) {
const randomData =
secondBannerData.bannerDetailInfos[secondBannerData.randomIndex];
const videoInfo = {
showUrl: randomData.showUrl,
patnrId: randomData.patnrId,
showId: randomData.showId,
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
lgCatCd: randomData.lgCatCd,
chanId: randomData.brdcChnlId,
modal: true,
modalContainerId: "banner1",
modalClassName: css.videoModal,
isVerticalModal: true, // Assuming second banner is horizontal, so modal is vertical
};
dispatch(requestPlayControl("banner1_preview", videoInfo));
}
if (handleItemFocus) {
handleItemFocus();
}
}, [dispatch, bannerDataList, handleItemFocus]);
const handleSecondBannerBlur = useCallback(() => {
dispatch(releasePlayControl("banner1_preview"));
}, [dispatch]);
const termsData = useSelector((state) => state.home.termsData);
const optionalTermsData = useSelector((state) =>
state.home.termsData?.data?.terms.find(term => term.trmsTpCd === "MST00405")
state.home.termsData?.data?.terms.find(
(term) => term.trmsTpCd === "MST00405",
),
);
const termsLoading = useSelector((state) => state.common.termsLoading);
const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
@@ -68,7 +108,8 @@ export default function HomeBanner({
//------------------------------------------------------------------------------
// 팝업표시 상태
const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false);
const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] =
useState(false);
const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false);
// 선택약관 팝업 표시 여부
@@ -80,35 +121,42 @@ export default function HomeBanner({
if (!terms) {
return false;
}
const optionalTerm = terms.find(term => term.trmsTpCd === "MST00405");
return optionalTerm ? optionalTerm.trmsPopFlag === 'Y' && optionalTerm.trmsAgrFlag === 'N' : false;
const optionalTerm = terms.find((term) => term.trmsTpCd === "MST00405");
return optionalTerm
? optionalTerm.trmsPopFlag === "Y" && optionalTerm.trmsAgrFlag === "N"
: false;
}, [termsData, termsLoading, isGnbOpened]);
const handleOptionalAgree = useCallback(() => {
console.log('handleAgree Click');
console.log("handleAgree Click");
// 약관 동의할 항목들 (string array)
const termsList = ["TID0000222", "TID0000223", "TID0000232"];
// 동의하지 않을 항목들 (빈 배열)
const notTermsList = [];
console.log('OptionalTermsConfirm -약관 동의 API 호출 파라미터:', { termsList, notTermsList });
console.log("OptionalTermsConfirm -약관 동의 API 호출 파라미터:", {
termsList,
notTermsList,
});
const callback = (response) => {
if (response.retCode === "000" || response.retCode === 0) {
console.log('약관 동의 성공:', response);
console.log("약관 동의 성공:", response);
// 약관 정보 갱신
dispatch(fetchCurrentUserHomeTerms());
} else {
console.error('약관 동의 실패:', response);
console.error("약관 동의 실패:", response);
}
};
console.log('OptionalTermsConfirm - 약관 동의 API 호출 payload:', { termsList, notTermsList });
console.log("OptionalTermsConfirm - 약관 동의 API 호출 payload:", {
termsList,
notTermsList,
});
dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback));
}, [dispatch]);
const handleOptionalTermsClick = useCallback(() => {
console.log('약관 자세히 보기 클릭');
console.log("약관 자세히 보기 클릭");
setIsOptionalConfirmVisible(false);
setIsOptionalTermsVisible(true);
// 약관 상세 팝업을 띄우는 로직 추가
@@ -120,7 +168,7 @@ export default function HomeBanner({
}, []);
const handleOptionalDeclineClick = useCallback(() => {
console.log('거절/다음에 하기 버튼 클릭');
console.log("거절/다음에 하기 버튼 클릭");
setIsOptionalConfirmVisible(false);
// 거절 처리 로직 추가
}, []);
@@ -139,7 +187,6 @@ export default function HomeBanner({
setIsOptionalTermsVisible(false);
}, []);
//------------------------------------------------------------------------------
const _handleShelfFocus = useCallback(() => {
if (handleShelfFocus) {
@@ -164,7 +211,7 @@ export default function HomeBanner({
}
} else if (
bannerDetailInfos.find(
(el) => el.shptmBanrTpNm === "LIVE" || el.shptmBanrTpNm === "VOD"
(el) => el.shptmBanrTpNm === "LIVE" || el.shptmBanrTpNm === "VOD",
)
) {
targetIndex = i;
@@ -222,6 +269,24 @@ export default function HomeBanner({
const renderItem = useCallback(
(index, isHorizontal) => {
const data = bannerDataList?.[index] ?? {};
if (index === 1) {
return (
<div className={!isHorizontal ? css.imgBox : undefined}>
<RandomUnitNew
bannerData={data}
isHorizontal={isHorizontal}
key={"banner" + index}
spotlightId={"banner" + index}
handleShelfFocus={_handleShelfFocus}
onFocus={handleSecondBannerFocus}
onBlur={handleSecondBannerBlur}
randomNumber={data.randomIndex}
/>
</div>
);
}
return (
<div className={!isHorizontal ? css.imgBox : undefined}>
{data.shptmDspyTpNm === "Rolling" ? (
@@ -263,7 +328,60 @@ export default function HomeBanner({
</div>
);
},
[_handleItemFocus, _handleShelfFocus, bannerDataList]
[
bannerDataList,
_handleItemFocus,
_handleShelfFocus,
handleSecondBannerFocus,
handleSecondBannerBlur,
],
);
const renderItemPersistentVideo = useCallback(
(index, isHorizontal) => {
const data = bannerDataList?.[index] ?? {};
return (
<div className={!isHorizontal ? css.imgBox : undefined}>
{data.shptmDspyTpNm === "Rolling" ? (
<Rolling
bannerData={data}
isHorizontal={isHorizontal}
key={"banner" + index}
spotlightId={"banner" + index}
handleShelfFocus={_handleShelfFocus}
handleItemFocus={_handleItemFocus}
/>
) : data.shptmDspyTpNm === "Random" ? (
<PersistentVideoUnit
bannerData={data}
isHorizontal={isHorizontal}
key={"banner" + index}
spotlightId={"banner" + index}
handleShelfFocus={_handleShelfFocus}
handleItemFocus={_handleItemFocus}
randomNumber={data.randomIndex}
/>
) : (
<SpottableComponent spotlightId={"banner" + index}>
<CustomImage
delay={0}
src={
isHorizontal
? homeTopDisplayInfo.wdthtpImgPath1
: homeTopDisplayInfo.vtctpImgPath1
}
aria-label={
isHorizontal
? homeTopDisplayInfo.wdthtpImgNm1
: homeTopDisplayInfo.vtctpImgNm1
}
/>
</SpottableComponent>
)}
</div>
);
},
[_handleItemFocus, _handleShelfFocus, bannerDataList, homeTopDisplayInfo],
);
const renderLayout = useCallback(() => {
@@ -272,6 +390,7 @@ export default function HomeBanner({
return (
<>
<ContainerBasic className={css.smallBox}>
{/* {renderItemPersistentVideo(0, true)} */}
{renderItem(0, true)}
{renderItem(1, true)}
</ContainerBasic>
@@ -306,7 +425,7 @@ export default function HomeBanner({
}
}
return null;
}, [selectTemplate, renderItem]);
}, [selectTemplate, renderItem, renderItemPersistentVideo]);
return (
<>
@@ -327,41 +446,23 @@ export default function HomeBanner({
onOptionalDeclineClick={handleOptionalDeclineClick}
customPosition={true}
position={{
position: 'absolute',
top: '342px', // 가운데를 기준으로 한 좌표 (1080/2) - 198
left: '0px',
bottom: 'unset',
transform: 'none',
position: "absolute",
top: "342px", // 가운데를 기준으로 한 좌표 (1080/2) - 198
left: "0px",
bottom: "unset",
transform: "none",
}}
/>
{/* 선택약관 자세히 보기 팝업 */}
<TNewPopUp
kind="introTermsPopup"
kind="figmaTermsPopup"
open={isOptionalTermsVisible}
title={$L("Optional Terms")}
text={optionalTermsData?.trmsCntt || ""}
onClose={handleTermsPopupClosed}
onClick={handleTermsPopupClosed}
onIntroTermsAgreeClick={handleTermsPopupAgree}
hasButton
button1Text={$L("Close")}
>
{optionalTermsData && (
<div className={css.termsViewerContent}>
<div className={css.termsViewerTitle}>{$L("Optional Terms")}</div>
<TButtonScroller
boxHeight={scaleH(300)}
width={scaleW(980)}
className={css.termsDescription}
>
<div
className={css.termsDesc}
dangerouslySetInnerHTML={{
__html: optionalTermsData.trmsCntt,
}}
onAgreeClick={handleTermsPopupAgree}
showAgreeButton={true}
/>
</TButtonScroller>
</div>
)}
</TNewPopUp>
</>
);
}

View File

@@ -0,0 +1,155 @@
import React, { useCallback, useEffect } from "react";
import { useDispatch } from "react-redux";
import classNames from "classnames";
import Spottable from "@enact/spotlight/Spottable";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { requestPlayControl, releasePlayControl, startVideoPlayer } from "../../../actions/playActions";
import CustomImage from "../../../components/CustomImage/CustomImage";
import liveShow from "../../../../assets/images/tag-liveshow.png";
import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png";
import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png";
import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png";
import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png";
import css from "./RandomUnit.module.less";
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const PersistentVideoUnit = (props) => {
const { bannerData, spotlightId, isHorizontal, randomNumber, handleItemFocus, handleShelfFocus } = props;
const dispatch = useDispatch();
const randomData = bannerData?.bannerDetailInfos?.[randomNumber];
const requestVideo = useCallback(() => {
if (randomData) {
const videoInfo = {
showUrl: randomData.showUrl,
patnrId: randomData.patnrId,
showId: randomData.showId,
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
lgCatCd: randomData.lgCatCd,
chanId: randomData.brdcChnlId,
modal: true,
modalContainerId: spotlightId,
modalClassName: css.videoModal,
isVerticalModal: !isHorizontal,
};
dispatch(requestPlayControl(spotlightId, videoInfo));
}
}, [dispatch, randomData, spotlightId, isHorizontal]);
useEffect(() => {
requestVideo();
return () => {
dispatch(releasePlayControl(spotlightId));
};
}, [dispatch, requestVideo, spotlightId]);
const handleFocus = useCallback(() => {
requestVideo();
if (handleItemFocus) {
handleItemFocus();
}
}, [requestVideo, handleItemFocus]);
const handleClick = useCallback(() => {
if (randomData) {
const videoInfo = {
showUrl: randomData.showUrl,
patnrId: randomData.patnrId,
showId: randomData.showId,
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
lgCatCd: randomData.lgCatCd,
chanId: randomData.brdcChnlId,
modal: false,
};
dispatch(startVideoPlayer(videoInfo));
}
}, [dispatch, randomData]);
return (
<Container
className={classNames(
css.rollingWrap,
isHorizontal && css.isHorizontalWrap
)}
onFocus={handleShelfFocus}
>
<SpottableComponent
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
onFocus={handleFocus}
onClick={handleClick}
spotlightId={spotlightId}
aria-label={
randomData?.shptmBanrTpNm === "LIVE"
? "LIVE " + randomData?.showNm
: randomData?.showNm
}
alt={"LIVE"}
>
{randomData?.shptmBanrTpNm === "LIVE" && (
<p className={css.liveIcon}>
<CustomImage
delay={0}
src={liveShow}
animationSpeed="fast"
ariaLabel="LIVE icon"
/>
</p>
)}
<div
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
>
{randomData?.tmnlImgPath ? (
<CustomImage
delay={0}
src={randomData.tmnlImgPath}
ariaLabel={randomData.tmnlImgNm}
animationSpeed="fast"
/>
) : (
<CustomImage
delay={0}
src={
randomData?.vtctpYn === "Y" ? emptyVerImage : emptyHorImage
}
animationSpeed="fast"
ariaLabel={randomData?.tmnlImgNm}
/>
)}
</div>
<div className={css.btnPlay}>
{randomData?.tmnlImgPath == null ? "" : <img src={btnPlay} alt="play" />}
</div>
<p className={css.brandIcon}>
{randomData?.showId && (
<CustomImage
delay={0}
src={randomData.showId ? randomData.patncLogoPath : null}
fallbackSrc={defaultLogoImg}
animationSpeed="fast"
ariaLabel={randomData.brdcChnlId}
/>
)}
</p>
</SpottableComponent>
</Container>
);
};
export default PersistentVideoUnit;

View File

@@ -0,0 +1,641 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import classNames from "classnames";
import { useDispatch, useSelector } from "react-redux";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import Spottable from "@enact/spotlight/Spottable";
import { getContainerId } from "@enact/spotlight/src/container";
import btnPlay from "../../../../assets/images/btn/btn-play-thumb-nor.png";
import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png";
import emptyHorImage from "../../../../assets/images/img-home-banner-empty-hor.png";
import emptyVerImage from "../../../../assets/images/img-home-banner-empty-ver.png";
import defaultImageItem from "../../../../assets/images/img-thumb-empty-product@3x.png";
import liveShow from "../../../../assets/images/tag-liveshow.png";
import { changeAppStatus } from "../../../actions/commonActions";
import { updateHomeInfo } from "../../../actions/homeActions";
import {
sendLogTopContents,
sendLogTotalRecommend,
} from "../../../actions/logActions";
import { pushPanel } from "../../../actions/panelActions";
import {
finishVideoPreview,
startVideoPlayer,
} from "../../../actions/playActions";
import CustomImage from "../../../components/CustomImage/CustomImage";
import usePriceInfo from "../../../hooks/usePriceInfo";
import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
LOG_TP_NO,
panel_names,
} from "../../../utils/Config";
import { $L, formatGMTString } from "../../../utils/helperMethods";
import { TEMPLATE_CODE_CONF } from "../HomePanel";
import css from "./RandomUnit.new.module.less";
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
export default function RandomUnitNew({
bannerData,
spotlightId,
isHorizontal,
handleShelfFocus,
randomNumber,
onFocus,
onBlur,
}) {
const dispatch = useDispatch();
const bannerDetailInfos = bannerData.bannerDetailInfos;
const shptmTmplCd = useSelector(
(state) => state.home?.bannerData?.shptmTmplCd
);
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
const homeCategory = useSelector(
(state) => state.home.menuData?.data?.homeCategory
);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
const broadcast = useSelector((state) => state.common.broadcast);
const { curationId, curationTitle } = useSelector((state) => state.home);
const [randomData, setRandomData] = useState("");
const [priceInfos, setpriceInfos] = useState("");
const [videoError, setVideoError] = useState(false);
const [liveIndicies, setLiveIndicies] = useState([]);
const bannerDataRef = useRef(bannerData);
const randomDataRef = useRef(bannerDetailInfos[randomNumber]);
const topContentsLogInfo = useMemo(() => {
if (randomDataRef.current) {
const currentRandomData = randomDataRef.current;
let contId, contNm;
switch (currentRandomData?.shptmBanrTpCd) {
// case: "LIVE" or "VOD"
case "DSP00301":
case "DSP00302":
contId = currentRandomData?.showId;
contNm = currentRandomData?.showNm;
break;
// case: "Image Banner"
case "DSP00303":
contId = currentRandomData?.shptmLnkTpCd;
contNm = currentRandomData?.shptmLnkTpNm;
break;
// case: "Today's Deals"
default:
contId = currentRandomData?.prdtId;
contNm = currentRandomData?.prdtNm;
break;
}
if (
currentRandomData?.shptmLnkTpCd === "DSP00503" || // "Hot Picks"
currentRandomData?.shptmLnkTpCd === "DSP00509" // "Theme"
) {
contNm = contNm + " | " + currentRandomData?.lnkCurationId;
}
return {
banrNo: `${currentRandomData?.banrDpOrd}`,
banrTpNm: currentRandomData?.vtctpYn
? currentRandomData.vtctpYn === "Y"
? "Vertical"
: "Horizontal"
: "",
contId,
contNm,
contTpNm: currentRandomData?.shptmBanrTpNm ?? "",
dspyTpNm: bannerDataRef.current?.shptmDspyTpNm ?? "",
expsOrd: bannerDataRef.current?.banrLctnNo ?? "",
linkTpCd: "",
patncNm: currentRandomData?.patncNm ?? "",
patnrId: currentRandomData?.patnrId ?? "",
tmplCd: shptmTmplCd,
};
}
return {};
}, [shptmTmplCd]);
const sendBannerLog = useCallback(() => {
const data = randomDataRef.current;
if (data && nowMenu === LOG_MENU.HOME_TOP) {
dispatch(
sendLogTotalRecommend({
contextName: LOG_CONTEXT_NAME.HOME,
messageId: LOG_MESSAGE_ID.BANNER,
curationId,
curationTitle,
contentType: data.shptmBanrTpNm,
contentId: data.showId,
contentTitle: data.showNm,
productId: data.prdtId,
productTitle: data.prdtNm,
displayType: "rolling",
partner: data.patncNm,
brand: data.brndNm, // <- 'brnad' 확인
location: data.dspyOrdr,
bannerType: data.vtctpYn === "Y" ? "Vertical" : "Horizontal",
})
);
}
}, [randomDataRef, nowMenu]);
useEffect(() => {
if (bannerDetailInfos && randomNumber) {
const indices = bannerDetailInfos
.map((info, index) => (info.shptmBanrTpNm === "LIVE" ? index : null))
.filter((index) => index !== null && index !== randomNumber);
setLiveIndicies(indices);
}
}, [bannerDetailInfos, randomNumber]);
const videoErrorClick = useCallback(() => {
return dispatch(
pushPanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
})
);
}, [randomData, dispatch]);
const shelfFocus = useCallback(() => {
if (handleShelfFocus) {
handleShelfFocus();
}
}, [handleShelfFocus]);
const categoryData = useMemo(() => {
if (randomData && randomData.shptmLnkTpCd === "DSP00505") {
if (homeCategory && homeCategory.length > 0) {
const foundCategory = homeCategory.find(
(data) => data.lgCatCd === randomData.lgCatCd
);
if (foundCategory) {
return {
lgCatNm: foundCategory.lgCatNm,
COUNT: foundCategory.COUNT,
};
}
return;
}
}
}, [homeCategory, randomData.shptmLnkTpCd]);
const imageBannerClick = useCallback(() => {
let linkInfo = {};
const linkType = randomData.shptmLnkTpCd;
switch (linkType) {
case "DSP00501":
linkInfo = {
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "gnb", patnrId: randomData.patnrId },
};
break;
case "DSP00502":
linkInfo = {
name: panel_names.TRENDING_NOW_PANEL,
panelInfo: {},
};
break;
case "DSP00503":
linkInfo = {
name: panel_names.HOT_PICKS_PANEL,
panelInfo: {
patnrId: randomData.patnrId,
curationId: randomData.lnkCurationId,
},
};
break;
case "DSP00504":
linkInfo = {
name: panel_names.ON_SALE_PANEL,
panelInfo: {
lgCatCd: randomData.lgCatCd,
},
};
break;
case "DSP00505":
if (Object.keys(categoryData).length > 0) {
linkInfo = {
name: panel_names.CATEGORY_PANEL,
panelInfo: {
lgCatCd: randomData.lgCatCd,
lgCatNm: categoryData.lgCatNm,
COUNT: categoryData.COUNT,
currentSpot: null,
dropDownTab: 0,
tab: 0,
focusedContainerId: null,
},
};
}
break;
case "DSP00506":
linkInfo = {
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: randomData.patnrId,
prdtId: randomData.prdtId,
curationId: randomData.lnkCurationId,
},
};
break;
case "DSP00507":
linkInfo = {
patnrId: randomData.patnrId,
showId: randomData.showId,
shptmBanrTpNm: "VOD",
lgCatCd: randomData.lgCatCd,
modal: false,
};
break;
case "DSP00508":
linkInfo = {
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: randomData.patnrId,
curationId: randomData.lnkCurationId,
prdtId: randomData.prdtId,
type: "theme",
},
};
break;
case "DSP00509":
linkInfo = {
name: panel_names.THEME_CURATION_PANEL,
panelInfo: {
curationId: randomData.lnkCurationId,
},
};
break;
default:
linkInfo = {
name: panel_names.HOME_PANEL,
panelInfo: {},
};
break;
}
let action = linkType === "DSP00507" ? startVideoPlayer : pushPanel;
dispatch(action(linkInfo));
sendBannerLog();
dispatch(
sendLogTopContents({
...topContentsLogInfo,
inDt: formatGMTString(new Date()) ?? "",
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
})
);
}, [
categoryData,
dispatch,
randomData?.lgCatCd,
randomData?.lnkCurationId,
randomData?.patnrId,
randomData?.prdtId,
randomData?.showId,
randomData?.shptmLnkTpCd,
topContentsLogInfo,
]);
const todayDealClick = useCallback(() => {
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: randomData.patnrId,
prdtId: randomData.prdtId,
},
})
);
sendBannerLog();
dispatch(
sendLogTopContents({
...topContentsLogInfo,
inDt: formatGMTString(new Date()) ?? "",
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
})
);
}, [
dispatch,
randomData?.patnrId,
randomData?.prdtId,
randomDataRef,
topContentsLogInfo,
]);
const videoClick = useCallback(() => {
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
const currentSpot = Spotlight.getCurrent();
if (lastFocusedTargetId) {
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
lastFocusedTargetId,
focusedContainerId: TEMPLATE_CODE_CONF.TOP,
currentSpot: currentSpot?.getAttribute("data-spotlight-id"),
},
})
);
}
dispatch(
startVideoPlayer({
showUrl: randomData.showUrl,
patnrId: randomData.patnrId,
showId: randomData.showId,
shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : "MEDIA",
lgCatCd: randomData.lgCatCd,
chanId: randomData.brdcChnlId,
modal: false,
modalContainerId: spotlightId,
modalClassName: css.videoModal,
})
);
sendBannerLog();
dispatch(
sendLogTopContents({
...topContentsLogInfo,
inDt: formatGMTString(new Date()) ?? "",
logTpNo: LOG_TP_NO.TOP_CONTENTS.CLICK,
})
);
if (onBlur) {
onBlur();
}
}, [randomData, spotlightId, topContentsLogInfo, nowMenu, randomDataRef, onBlur]);
const { originalPrice, discountedPrice, discountRate, offerInfo } =
usePriceInfo(priceInfos) || {};
useEffect(() => {
let _nowMenu = nowMenu;
let _entryMenu = entryMenu;
if (nowMenu === LOG_MENU.HOME_TOP) {
const params = {
...topContentsLogInfo,
entryMenu: _entryMenu,
inDt: formatGMTString(new Date()) ?? "",
logTpNo: LOG_TP_NO.TOP_CONTENTS.VIEW,
nowMenu: _nowMenu,
};
return () => dispatch(sendLogTopContents(params));
}
}, [dispatch, entryMenu, nowMenu, topContentsLogInfo]);
useEffect(() => {
sendBannerLog();
}, [randomDataRef, nowMenu]);
useEffect(() => {
if (bannerData) {
setRandomData(bannerDetailInfos[randomNumber]);
}
}, [bannerData, dispatch, randomNumber]);
useEffect(() => {
if (randomData && randomData.priceInfo !== null) {
return setpriceInfos(randomData.priceInfo);
}
}, [randomData]);
useEffect(() => {
if (broadcast?.type === "videoError") {
setVideoError(true);
if (liveIndicies.length > 0) {
const nextIndex = liveIndicies[0];
setLiveIndicies((prev) => prev.slice(1));
setRandomData(bannerDetailInfos[nextIndex]);
setTimeout(() => {
setVideoError(false);
}, 0);
}
}
}, [broadcast, liveIndicies, bannerDetailInfos]);
return (
<>
<Container
className={classNames(
css.rollingWrap,
isHorizontal && css.isHorizontalWrap
)}
onFocus={shelfFocus}
>
{randomData?.shptmBanrTpNm == "Image Banner" ? (
<SpottableComponent
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
onClick={imageBannerClick}
spotlightId={spotlightId}
aria-label={
randomData.prdtNm ? randomData.prdtNm : randomData.tmnlImgNm
}
>
<div className={css.imgBanner}>
<CustomImage
delay={0}
src={randomData.tmnlImgPath}
animationSpeed="fast"
ariaLabel={randomData.tmnImgNm}
/>
</div>
</SpottableComponent>
) : randomData?.shptmBanrTpNm == "LIVE" ||
randomData?.shptmBanrTpNm == "VOD" ? (
<SpottableComponent
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
onClick={videoError === true ? videoErrorClick : videoClick}
onFocus={onFocus}
onBlur={onBlur}
spotlightId={spotlightId}
aria-label={
randomData.shptmBanrTpNm == "LIVE"
? "LIVE " + randomData.showNm
: randomData.showNm
}
alt={"LIVE"}
>
{randomData.shptmBanrTpNm == "LIVE" && videoError === false && (
<p className={css.liveIcon}>
<CustomImage
delay={0}
src={liveShow}
animationSpeed="fast"
ariaLabel="LIVE icon"
/>
</p>
)}
{videoError === true && (
<div className={css.errorContents}>
<div>
{randomData.patncLogoPath && (
<img
className={css.errorlogo}
src={randomData.patncLogoPath}
/>
)}
<p className={css.errorText}>
{$L("Click the screen to see more products!")}
</p>
</div>
</div>
)}
{videoError === false && (
<div
className={classNames(
css.itemBox,
isHorizontal && css.isHorizontal
)}
>
{randomData.tmnlImgPath ? (
<CustomImage
delay={0}
src={randomData.tmnlImgPath}
ariaLabel={randomData.tmnImgNm}
animationSpeed="fast"
/>
) : (
<CustomImage
delay={0}
src={
randomData.vtctpYn === "Y" ? emptyVerImage : emptyHorImage
}
animationSpeed="fast"
ariaLabel={randomData.tmnImgNm}
/>
)}
</div>
)}
{videoError === false && (
<div className={css.btnPlay}>
{randomData.tmnlImgPath == null ? "" : <img src={btnPlay} />}
</div>
)}
{videoError === false && (
<p className={css.brandIcon}>
{randomData.showId && (
<CustomImage
delay={0}
src={randomData.showId ? randomData.patncLogoPath : null}
fallbackSrc={defaultLogoImg}
animationSpeed="fast"
ariaLabel={randomData.brdcChnlId}
/>
)}
</p>
)}
</SpottableComponent>
) : randomData?.shptmBanrTpNm == "Today's Deals" ? (
<SpottableComponent
className={classNames(
css.itemBox,
css.todaysDeals,
countryCode === "RU" ? css.ru : "",
countryCode === "DE" ? css.de : "",
isHorizontal && css.isHorizontal
)}
onClick={todayDealClick}
spotlightId={spotlightId}
aria-label={
randomData.prdtNm ? randomData.prdtNm : randomData.tmnlImgNm
}
>
<div className={css.productInfo}>
<div className={css.todaysDealTitle}>{$L("TODAY's DEALS")}</div>
<div
className={css.textBox}
dangerouslySetInnerHTML={{
__html: `${randomData.prdtNm}`,
}}
/>
<div className={css.accBox}>
{parseFloat(originalPrice?.replace("$", "")) === 0
? randomData?.offerInfo
: discountRate
? discountedPrice
: originalPrice}
{discountRate && !isHorizontal && (
<span className={css.saleAccBox}>{originalPrice}</span>
)}
</div>
{isHorizontal &&
parseFloat(originalPrice?.replace("$", "")) !== 0 && (
<span className={css.saleAccBox}>{originalPrice}</span>
)}
</div>
<div className={css.itemImgBox}>
<CustomImage
delay={0}
src={randomData.tmnlImgPath}
animationSpeed="fast"
fallbackSrc={defaultImageItem}
ariaLabel={randomData.tmnlImgNm}
/>
</div>
</SpottableComponent>
) : null}
</Container>
</>
);
}

View File

@@ -0,0 +1,281 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.rollingWrap {
position: relative;
.itemBox {
.size(@w: 486px, @h: 858px);
position: relative;
text-align: center;
.brandIcon {
overflow: hidden;
position: absolute;
right: 30px;
bottom: 30px;
> img {
border-radius: 0;
.size(@w: 60px, @h: 60px);
}
}
.liveIcon {
z-index: 2;
position: absolute;
left: 18px;
top: 18px;
> img {
.size(@w: 108px, @h: 48px);
}
}
.imgBanner {
> img {
border-radius: 10px;
.size(@w: 486px, @h: 858px);
}
}
.btnPlay {
.size(@w: 100%, @h: 100%);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
top: 0;
z-index: 2;
> img {
.size(@w: 120px, @h: 120px);
}
}
&.isHorizontal {
.size(@w: 744px, @h: 420px);
.imgBanner {
> img {
border-radius: 10px;
.size(@w: 744px, @h: 420px);
}
}
}
.errorContents {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.errorlogo {
width: 120px;
height: 120px;
object-fit: cover;
}
.errorText {
margin-top: 30px;
color: #ff0000;
padding-top: 10px;
}
}
&.todaysDeals {
background-image: url(../../../../assets/images/img-home-banner-td-ver@3x.png);
background-size: 486px 858px;
background-position: left top;
border-radius: 10px;
padding: 73px 36px 0;
&.ru {
.productInfo {
.todaysDealTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.todaysDealTitle {
font-size: 59px !important;
line-height: 63px !important;
letter-spacing: -1px !important;
}
}
}
.productInfo {
margin-bottom: 33px;
.todaysDealTitle {
.size(@w:100%,@h:132px);
font-size: 76px;
word-break: break-word;
font-stretch: normal;
color: #151515;
text-align: center;
line-height: 76px;
font-family: @arialFontBold;
}
.textBox {
.size(@w: 100%, @h: 80px);
margin-top: 71px;
.elip(@clamp:2);
font-weight: bold;
font-size: 30px;
color: @COLOR_GRAY06;
line-height: 1.27;
margin-bottom: 6px;
}
.accBox {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 42px;
color: @PRIMARY_COLOR_RED;
line-height: 1.14;
display: inline-block;
.elip(@clamp:1);
> strong {
width: 260px;
font-size: 30px;
line-height: 1.27;
display: block;
.elip(@clamp:2);
}
.saleAccBox {
font-weight: normal;
font-size: 24px;
color: @COLOR_GRAY04;
vertical-align: middle;
text-decoration: line-through;
margin-left: 9px;
}
}
}
.itemImgBox {
> img {
.size(@w: 356px, @h: 356px);
border-radius: 0;
}
}
&.isHorizontal {
background-image: url(../../../../assets/images/img-home-banner-td-hor@3x.png);
background-size: 744px 420px;
background-position: center center;
display: flex;
padding: 0 30px 0 0;
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
-o-border-radius: 10px;
> div {
flex: none;
}
&.ru {
.productInfo {
.todaysDealTitle {
font-size: 58px;
line-height: 60px;
font-family: @arialFontBold;
}
}
}
&.de {
.productInfo {
.todaysDealTitle {
font-size: 59px !important;
line-height: 63px !important;
letter-spacing: -2px !important;
}
}
}
.productInfo {
margin-bottom: 0;
.todaysDealTitle {
.size(@w:305px,@h:114px);
margin-top: 53px;
margin-left: 49px;
font-size: 66px;
word-break: break-word;
color: #151515;
text-align: left;
line-height: 57px;
font-family: @arialBlack;
}
.textBox {
.size(@w: 294px, @h: 80px);
margin: 67px 0 5px 50px;
text-align: left;
}
.accBox {
.size(@w: 320px, @h: 50px);
margin-left: 50px;
text-align: left;
display: block;
.elip(@clamp:1);
}
.saleAccBox {
color: #767676;
display: block;
text-align: left;
margin: 5px 0 0 55px;
text-decoration: line-through;
}
}
.itemImgBox {
.position(@position: absolute, @top: 47px, @left: 389px);
.size(@w: 326px, @h: 326px);
> img {
.size(@w: inherit, @h: inherit);
}
}
}
}
&:focus {
&::after {
.focused(@boxShadow:22px, @borderRadius: 12px);
border: 6px solid @PRIMARY_COLOR_RED;
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
}
}
}
.arrow {
z-index: 9999;
.size(@w: 42px, @h: 42px);
background-size: 42px 42px;
background-position: center center;
&.leftBtn {
.position(@position: absolute, @top: 406px, @left: 18px);
background-image: url("../../../../assets/images/btn/btn_prev_thumb_nor.png");
&:focus {
background-image: url("../../../../assets/images/btn/btn_prev_thumb_foc.png");
}
}
&.rightBtn {
.position(@position: absolute, @top: 406px, @right: 18px);
background-image: url("../../../../assets/images/btn/btn_next_thumb_nor.png");
&:focus {
background-image: url("../../../../assets/images/btn/btn_next_thumb_foc.png");
}
}
}
&.isHorizontalWrap {
border-radius: 10px;
.arrow {
&.leftBtn {
.position(@position: absolute, @top: 189px, @left: 18px);
}
&.rightBtn {
.position(@position: absolute, @top: 189px, @right: 18px);
}
}
}
}
.videoModal {
&::after {
.focused(@boxShadow:0, @borderRadius: 12px);
border: 6px solid @PRIMARY_COLOR_RED;
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
}
}

View File

@@ -75,43 +75,43 @@ const HomePanel = ({ isOnTop }) => {
const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
const homeLayoutInfo = useSelector((state) => state.home.layoutData);
const panelInfo = useSelector(
(state) => state.home.homeInfo?.panelInfo ?? {}
(state) => state.home.homeInfo?.panelInfo ?? {},
);
const panels = useSelector((state) => state.panels.panels);
const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion
(state) => state.common.appStatus?.webOSVersion,
);
const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB);
const defaultFocus = useSelector((state) => state.home.defaultFocus);
const categoryInfos = useSelector(
(state) => state.onSale.homeOnSaleData?.data?.categoryInfos
(state) => state.onSale.homeOnSaleData?.data?.categoryInfos,
);
const categoryItemInfos = useSelector(
(state) => state.main.subCategoryData?.categoryItemInfos
(state) => state.main.subCategoryData?.categoryItemInfos,
);
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
(state) => state.common.popup,
);
const eventPopInfosData = useSelector(
(state) => state.event.eventData.eventPopInfo
(state) => state.event.eventData.eventPopInfo,
);
const eventData = useSelector((state) => state.event.eventData);
const eventClickSuccess = useSelector(
(state) => state.event.eventClickSuccess
(state) => state.event.eventClickSuccess,
);
const homeOnSaleInfos = useSelector(
(state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos
(state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos,
);
const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
(state) => state.product.bestSellerData?.bestSeller,
);
const topInfos = useSelector((state) => state.main.top20ShowData.topInfos);
const isDeepLink = useSelector(
(state) => state.common.deepLinkInfo.isDeepLink
(state) => state.common.deepLinkInfo.isDeepLink,
);
const [btnDisabled, setBtnDisabled] = useState(true);
@@ -120,13 +120,13 @@ const HomePanel = ({ isOnTop }) => {
const [eventPopOpen, setEventPopOpen] = useState(false);
const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf);
const [firstLgCatCd, setFirstLgCatCd] = useState(
panelInfo.currentCatCd ?? null
panelInfo.currentCatCd ?? null,
);
const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null);
const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null);
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const [focusedContainerId, setFocusedContainerId] = useState(
panelInfo.focusedContainerId
panelInfo.focusedContainerId,
);
const isInitialRender = useRef(true);
@@ -139,7 +139,7 @@ const HomePanel = ({ isOnTop }) => {
sendLogTotalRecommend({
messageId: LOG_MESSAGE_ID.HOME,
contextName: LOG_CONTEXT_NAME.HOME,
})
}),
);
}
}, [entryMenu, nowMenu]);
@@ -147,7 +147,7 @@ const HomePanel = ({ isOnTop }) => {
const sortedHomeLayoutInfo = useMemo(() => {
if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) {
const sorted = [...homeLayoutInfo.homeLayoutInfo].sort(
(x, y) => x.expsOrd - y.expsOrd
(x, y) => x.expsOrd - y.expsOrd,
);
return sorted;
}
@@ -183,7 +183,7 @@ const HomePanel = ({ isOnTop }) => {
panelInfo: {
currentSpot: currentSpot,
},
})
}),
);
dispatch(setShowPopup(ACTIVE_POPUP.exitPopup));
@@ -197,7 +197,7 @@ const HomePanel = ({ isOnTop }) => {
contextName: LOG_CONTEXT_NAME.SHOPTIME,
messageId: LOG_MESSAGE_ID.VIEW_CHANGE,
visible: false,
})
}),
);
}, [dispatch]);
@@ -261,7 +261,7 @@ const HomePanel = ({ isOnTop }) => {
shelfLocation: location,
shelfId: containerId,
shelfTitle: title,
})
}),
);
setNowShelf(containerId);
@@ -271,14 +271,14 @@ const HomePanel = ({ isOnTop }) => {
currentSentMenuRef.current = nowMenu;
}
},
[pageSpotIds, nowShelf, panelInfo.nowShelf]
[pageSpotIds, nowShelf, panelInfo.nowShelf],
);
const handleItemFocus = useCallback(
(containerId, location, title) => () => {
doSendLogGNB(containerId, location, title);
},
[doSendLogGNB]
[doSendLogGNB],
);
const renderPageItem = useCallback(() => {
@@ -298,7 +298,7 @@ const HomePanel = ({ isOnTop }) => {
handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd,
el.expsOrd,
el.shptmApphmDspyOptNm
el.shptmApphmDspyOptNm,
)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
@@ -320,7 +320,7 @@ const HomePanel = ({ isOnTop }) => {
handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd,
el.expsOrd,
el.shptmApphmDspyOptNm
el.shptmApphmDspyOptNm,
)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
shelfLocation={el.expsOrd}
@@ -338,7 +338,7 @@ const HomePanel = ({ isOnTop }) => {
handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd,
el.expsOrd,
el.shptmApphmDspyOptNm
el.shptmApphmDspyOptNm,
)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
shelfLocation={el.expsOrd}
@@ -356,7 +356,7 @@ const HomePanel = ({ isOnTop }) => {
handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd,
el.expsOrd,
el.shptmApphmDspyOptNm
el.shptmApphmDspyOptNm,
)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
shelfLocation={el.expsOrd}
@@ -374,7 +374,7 @@ const HomePanel = ({ isOnTop }) => {
handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd,
el.expsOrd,
el.shptmApphmDspyOptNm
el.shptmApphmDspyOptNm,
)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
shelfLocation={el.expsOrd}
@@ -473,7 +473,7 @@ const HomePanel = ({ isOnTop }) => {
cbChangePageRef,
dispatch,
isOnTop,
]
],
);
const bestSellerLoaded = useCallback(() => {
@@ -486,7 +486,7 @@ const HomePanel = ({ isOnTop }) => {
if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) {
dispatch(
changeAppStatus({ showLoadingPanel: { show: true, type: "wait" } })
changeAppStatus({ showLoadingPanel: { show: true, type: "wait" } }),
);
dispatch(getHomeMainContents());
dispatch(getHomeLayout());
@@ -495,7 +495,7 @@ const HomePanel = ({ isOnTop }) => {
homeSaleInfosIncFlag: "Y",
categoryIncFlag: "Y",
saleInfosIncFlag: "N",
})
}),
);
dispatch(getTop20Show());
dispatch(getBestSeller(bestSellerLoaded));
@@ -528,8 +528,8 @@ const HomePanel = ({ isOnTop }) => {
tabType: "CAT00102",
filterType: "CAT00202",
},
1
)
1,
),
);
}
}, [categoryInfos, firstLgCatCd, dispatch]);
@@ -544,7 +544,7 @@ const HomePanel = ({ isOnTop }) => {
} else setEventPopOpen(true);
} else setEventPopOpen(false);
},
[webOSVersion]
[webOSVersion],
);
useEffect(() => {
@@ -583,7 +583,7 @@ const HomePanel = ({ isOnTop }) => {
}
const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`,
);
const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null;
@@ -597,7 +597,7 @@ const HomePanel = ({ isOnTop }) => {
currentCateName: targetSpotlightCateNm,
focusedContainerId: focusedContainerIdRef.current,
},
})
}),
);
};
}, [dispatch]);

View File

@@ -1,14 +1,16 @@
// src: views/IntroPanel/IntroPanel.new.jsx
import React, { useCallback, useEffect, useState, useMemo, useRef } from "react";
import { flushSync } from "react-dom";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} 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,
@@ -17,7 +19,7 @@ import {
} from "../../actions/commonActions";
import { registerDevice } from "../../actions/deviceActions";
import { getWelcomeEventInfo } from "../../actions/eventActions";
import { fetchCurrentUserHomeTerms } from "../../actions/homeActions";
// import { fetchCurrentUserHomeTermsPromise } from "../../actions/homeActions";
import {
sendLogGNB,
sendLogTerms,
@@ -29,27 +31,29 @@ import TButton, { TYPES } from "../../components/TButton/TButton";
// import TButtonTab from "../../components/TButtonTab/TButtonTab";
import TCheckBoxSquare from "../../components/TCheckBox/TCheckBoxSquare";
import TPanel from "../../components/TPanel/TPanel";
import TPopUp, { CONTENT_TYPES } from "../../components/TPopUp/TPopUp";
import TPopUp from "../../components/TPopUp/TPopUp";
import TNewPopUp from "../../components/TPopUp/TNewPopUp";
import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo";
// import OptionalTermsInfo from "../MyPagePanel/MyPageSub/TermsOfService/OptionalTermsInfo";
import useDebugKey from "../../hooks/useDebugKey";
import useSafeFocusState from "../../hooks/useSafeFocusState";
import useTermsStateMachine from "../../hooks/useTermsStateMachine";
import useRightPanelContent from "../../hooks/useRightPanelContent";
import * as Config from "../../utils/Config";
import { panel_names } from "../../utils/Config";
import { $L, scaleH, scaleW } from "../../utils/helperMethods";
import { $L } from "../../utils/helperMethods";
import css from "./IntroPanel.new.module.less";
import { types } from "../../actions/actionTypes";
import { focusById } from "../../utils/spotlight-utils";
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
"div",
);
export default function IntroPanel({
children,
isTabActivated,
handleCancel,
// children,
// isTabActivated,
// handleCancel,
spotlightId,
...rest
}) {
@@ -58,36 +62,50 @@ export default function IntroPanel({
useDebugKey({});
const dispatch = useDispatch();
const blurTimeout = useRef(null);
// const blurTimeout = useRef(null);
const termsData = useSelector((state) => state.home.termsData);
const { popupVisible, activePopup, ...popupState } = useSelector(
(state) => state.common.popup
(state) => state.common.popup,
);
const eventInfos = useSelector((state) => state.event.eventData);
// const eventInfos = useSelector((state) => state.event.eventData);
const regDeviceData = useSelector((state) => state.device.regDeviceData);
const regDeviceInfoData = useSelector(
(state) => state.device.regDeviceInfoData
);
// const regDeviceInfoData = useSelector(
// (state) => state.device.regDeviceInfoData
// );
// registerDevice API 호출 중 여부
const [isProcessing, setIsProcessing] = useState(false);
const [showExitMessagePopup, setShowExitMessagePopup] = useState(false);
const [isRequiredFocused, setIsRequiredFocused] = useState(false);
// const [isRequiredFocused, setIsRequiredFocused] = useState(false);
const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState();
const { state: termsState, updateStateAsync } = useTermsStateMachine();
const {
termsChecked,
privacyChecked,
optionalChecked,
selectAllChecked,
error: termsError, // 훅의 에러 상태를 가져옴
} = termsState;
const introTermsData = useMemo(() => {
return termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402"
) || [];
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"
) || [];
return (
termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406",
) || []
);
}, [termsData]);
const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion
(state) => state.common.appStatus?.webOSVersion,
);
// WebOS 버전별 UI 표시 모드 결정
// 이미지 표시: 4.0, 5.0, 23, 24
@@ -98,30 +116,27 @@ export default function IntroPanel({
const version = String(webOSVersion);
// 텍스트 표시 버전들
const textVersions = ['4.5', '6.0', '22'];
const textVersions = ["4.5", "6.0", "22"];
// 이미지 표시 버전들
const imageVersions = ['4.0', '5.0', '23', '24'];
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);
if (process.env.NODE_ENV === "development") {
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(false); // Terms & Conditions 기본 체크
const [privacyChecked, setPrivacyChecked] = useState(false); // Privacy Policy 기본 체크
const [optionalChecked, setOptionalChecked] = useState(false); // Optional Terms 기본 체크 안됨
const [selectAllChecked, setSelectAllChecked] = useState(false);
const [focusedItem, setFocusedItem] = useState(null); // 포커스 추적 상태 추가
useEffect(() => {
dispatch({ type: types.REGISTER_DEVICE_RESET });
@@ -139,9 +154,11 @@ export default function IntroPanel({
// 디버깅용 WebOS 버전 로그
useEffect(() => {
console.log('🔍 IntroPanel WebOS 버전 정보:');
console.log(' - webOSVersion:', webOSVersion);
console.log(' - shouldShowBenefitsView:', shouldShowBenefitsView);
if (process.env.NODE_ENV === "development") {
console.log("🔍 IntroPanel WebOS 버전 정보:");
console.log(" - webOSVersion:", webOSVersion);
console.log(" - shouldShowBenefitsView:", shouldShowBenefitsView);
}
}, [webOSVersion, shouldShowBenefitsView]);
useEffect(() => {
@@ -153,22 +170,13 @@ export default function IntroPanel({
contextName: Config.LOG_CONTEXT_NAME.SHOPTIME,
messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE,
visible: false,
})
}),
);
}, 3000);
return () => clearTimeout(timer);
}
}, [showExitMessagePopup, dispatch]);
// Select All 상태 업데이트
useEffect(() => {
const allChecked = termsChecked && privacyChecked && optionalChecked;
setSelectAllChecked(allChecked);
if (allChecked) {
Spotlight.focus("agreeButton");
}
}, [termsChecked, privacyChecked, optionalChecked]);
// 컴포넌트 마운트 후 0.5초 뒤에 selectAllCheckbox으로 강제 포커스
useEffect(() => {
const focusTimer = setTimeout(() => {
@@ -180,6 +188,17 @@ export default function IntroPanel({
};
}, []);
// [추가] useTermsStateMachine의 에러 상태를 감지하여 팝업으로 표시
useEffect(() => {
if (termsError) {
dispatch(setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: termsError.message,
button1Text: $L("OK")
}));
}
}, [termsError, dispatch]);
// 약관 팝업 동의여부에 따른 이벤트 핸들러
const handleTermsAgree = useCallback(() => {
if (!currentTerms) {
@@ -187,23 +206,22 @@ export default function IntroPanel({
}
const termType = currentTerms.trmsTpCd;
if (termType === "MST00402") {
setTermsChecked(true);
updateStateAsync({ termsChecked: true });
} else if (termType === "MST00401") {
setPrivacyChecked(true);
updateStateAsync({ privacyChecked: true });
} else if (termType === "MST00405") {
// Optional Terms
setOptionalChecked(true);
updateStateAsync({ optionalChecked: true });
}
// 팝업 닫기
dispatch(setHidePopup());
}, [currentTerms, dispatch]);
}, [currentTerms, dispatch, updateStateAsync]);
const handleTermsClick = useCallback(
(trmsTpCdList) => {
if (introTermsData) {
const selectedTerms = introTermsData.find(
(term) => term.trmsTpCd === trmsTpCdList
(term) => term.trmsTpCd === trmsTpCdList,
);
setCurrentTerms(selectedTerms);
@@ -216,14 +234,14 @@ export default function IntroPanel({
dispatch(sendLogTerms({ logTpNo }));
}
},
[introTermsData, dispatch]
[introTermsData, dispatch],
);
const handleOptionalTermsClick = useCallback(
(trmsTpCdList) => {
if (optionalTermsData) {
const selectedTerms = optionalTermsData.find(
(term) => term.trmsTpCd === trmsTpCdList
(term) => term.trmsTpCd === trmsTpCdList,
);
setCurrentTerms(selectedTerms);
@@ -236,10 +254,9 @@ export default function IntroPanel({
// dispatch(sendLogTerms({ logTpNo }));
}
},
[optionalTermsData, dispatch]
[optionalTermsData, dispatch],
);
const onClose = useCallback(() => {
dispatch(setHidePopup());
}, [dispatch]);
@@ -276,13 +293,15 @@ export default function IntroPanel({
agreeTerms.push("TID0000232"); // MST00405 -> TID0000232 (선택약관)
}
console.log('최종 전송될 agreeTerms:', agreeTerms);
if (process.env.NODE_ENV === "development") {
console.log("최종 전송될 agreeTerms:", agreeTerms);
}
dispatch(
registerDevice(
{ agreeTerms: agreeTerms },
(regDeviceData) => {
if (regDeviceData && regDeviceData.retCode === 0) {
(newRegDeviceData) => {
if (newRegDeviceData && newRegDeviceData.retCode === 0) {
dispatch(
getWelcomeEventInfo((eventInfos) => {
if (
@@ -301,7 +320,7 @@ export default function IntroPanel({
}
dispatch(
sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE })
sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }),
);
if (displayWelcomeEventPanel) {
@@ -309,13 +328,13 @@ export default function IntroPanel({
pushPanel({
name: panel_names.WELCOME_EVENT_PANEL,
panelInfo: { eventInfos: eventInfos.data },
})
}),
);
}
}
dispatch(popPanel(panel_names.INTRO_PANEL));
setIsProcessing(false);
})
}),
);
} else {
dispatch(
@@ -323,7 +342,7 @@ export default function IntroPanel({
title: $L("Error"),
text: $L("Device registration failed. Please try again."),
button1Text: $L("OK"),
})
}),
);
setIsProcessing(false);
}
@@ -334,11 +353,11 @@ export default function IntroPanel({
title: $L("Error"),
text: $L("Device registration failed. Please try again."),
button1Text: $L("OK"),
})
}),
);
setIsProcessing(false);
}
)
},
),
);
}, [
termsChecked,
@@ -353,16 +372,18 @@ export default function IntroPanel({
useEffect(() => {
// isProcessing이 true일 때만 실패 체크 (= handleAgree 클릭 후에만)
if (isProcessing && regDeviceData && regDeviceData.retCode !== 0) {
if (process.env.NODE_ENV === "development") {
console.error(
`[IntroPanel] registerDevice 실패: isProcessing=${isProcessing}, retCode=${regDeviceData.retCode}`,
regDeviceData
regDeviceData,
);
}
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: $L("Device registration failed. Please try again."),
button1Text: $L("OK"),
})
}),
);
setIsProcessing(false);
}
@@ -382,7 +403,7 @@ export default function IntroPanel({
contextName: Config.LOG_CONTEXT_NAME.SHOPTIME,
messageId: Config.LOG_MESSAGE_ID.VIEW_CHANGE,
visible: false,
})
}),
);
}, [dispatch]);
@@ -392,101 +413,138 @@ export default function IntroPanel({
}
}, [dispatch, activePopup]);
const handleFocus = useCallback((item) => {
if (blurTimeout.current) {
clearTimeout(blurTimeout.current);
blurTimeout.current = null;
}
setFocusedItem(item);
}, []);
const handleFocus = useCallback(
(item) => {
setFocusAsync(item);
},
[setFocusAsync]
);
const handleBlur = useCallback(() => {
blurTimeout.current = setTimeout(() => {
setFocusedItem(null);
}, 0);
}, []);
clearFocusAsync(0);
}, [clearFocusAsync]);
// 체크박스 핸들러들
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);
}, []);
const rightPanelContent = useMemo(() => {
const requiredItemIds = [
"termsCheckbox",
"termsButton",
"privacyCheckbox",
"privacyButton",
];
if (!requiredItemIds.includes(focusedItem)) {
return (
<>
{shouldShowBenefitsView ? (
<div className={css.optionalDescription}>
{$L(
'By checking "Optional terms", you allow Shop Time to use your activity (views, purchases, searches, etc.) to show you more relevant content, product recommendations, special offers, and ads. If you do not check, you can still use all basic Shop Time features'
)}
</div>
) : (
<OptionalTermsInfo
displayMode="image"
imageTitle={$L("Agree and Enjoy Special Benefits")}
spotlightId="optional-terms-info"
/>
)}
</>
);
const handleTermsToggle = useCallback(
async ({ selected }) => {
try {
const newState = await updateStateAsync({ termsChecked: selected });
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
setTimeout(() => Spotlight.focus("agreeButton"), 100);
}
const hasRequiredUnchecked = !termsChecked || !privacyChecked;
// 우선순위 1: 필수 약관이 체크 안됨 → 항상 경고 메시지
if (hasRequiredUnchecked) {
return (
<div className={css.requiredInfoPanel}>
<p className={css.infoText}>{$L("Required Consent")}</p>
<p className={css.infoText}>
{$L("(Necessary for using the service)")}
</p>
<div className={css.warningContainer}>
<p className={css.warningText}>
{$L("Please agree to the required Terms & Conditions and")}
</p>
<p className={css.warningText}>
{$L("Privacy Policy to start enjoying Shop Time")}
</p>
</div>
</div>
);
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("Toggle failed:", error);
}
// 우선순위 2: 필수 약관에 포커스 있음 → 필수 약관 안내
return (
<div className={css.requiredInfoPanel}>
<p className={css.infoText}>{$L("Required Consent")}</p>
<p className={css.infoText}>
{$L("(Necessary for using the service)")}
</p>
</div>
}
},
[updateStateAsync]
);
}, [termsChecked, privacyChecked, focusedItem, shouldShowBenefitsView]);
const handlePrivacyToggle = useCallback(
async ({ selected }) => {
try {
const newState = await updateStateAsync({ privacyChecked: selected });
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
setTimeout(() => Spotlight.focus("agreeButton"), 100);
}
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("Toggle failed:", error);
}
}
},
[updateStateAsync]
);
const handleOptionalToggle = useCallback(
async ({ selected }) => {
try {
const newState = await updateStateAsync({ optionalChecked: selected });
if (newState.termsChecked && newState.privacyChecked && newState.optionalChecked) {
setTimeout(() => Spotlight.focus("agreeButton"), 100);
}
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("Toggle failed:", error);
}
}
},
[updateStateAsync]
);
const handleSelectAllToggle = useCallback(
async ({ selected }) => {
try {
const newState = await updateStateAsync({ selectAllChecked: selected });
if (newState.selectAllChecked) {
setTimeout(() => Spotlight.focus("agreeButton"), 100);
}
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("Toggle failed:", error);
}
}
},
[updateStateAsync]
);
const handleTermsClickMST00402 = useCallback(
() => handleTermsClick("MST00402"),
[handleTermsClick],
);
const handleTermsClickMST00401 = useCallback(
() => handleTermsClick("MST00401"),
[handleTermsClick],
);
const handleOptionalTermsClickMST00405 = useCallback(
() => handleOptionalTermsClick("MST00405"),
[handleOptionalTermsClick],
);
const handleFocusTermsCheckbox = useCallback(
() => handleFocus("termsCheckbox"),
[handleFocus],
);
const handleFocusTermsButton = useCallback(
() => handleFocus("termsButton"),
[handleFocus],
);
const handleFocusPrivacyCheckbox = useCallback(
() => handleFocus("privacyCheckbox"),
[handleFocus],
);
const handleFocusPrivacyButton = useCallback(
() => handleFocus("privacyButton"),
[handleFocus],
);
const handleFocusOptionalCheckbox = useCallback(
() => handleFocus("optionalCheckbox"),
[handleFocus],
);
const handleFocusOptionalButton = useCallback(
() => handleFocus("optionalButton"),
[handleFocus],
);
const handleFocusSelectAllCheckbox = useCallback(
() => handleFocus("selectAllCheckbox"),
[handleFocus],
);
const handleFocusAgreeButton = useCallback(
() => handleFocus("agreeButton"),
[handleFocus],
);
const handleFocusDisagreeButton = useCallback(
() => handleFocus("disagreeButton"),
[handleFocus],
);
const rightPanelContent = useRightPanelContent(
focusedItem,
termsChecked,
privacyChecked,
shouldShowBenefitsView
);
useEffect(() => {
Spotlight.focus();
@@ -500,19 +558,23 @@ export default function IntroPanel({
// }, [dispatch, regDeviceInfoData]);
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."
"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;
// [추가] 약관 종류에 따라 팝업 제목을 반환하는 헬퍼 함수
const getTermsPopupTitle = (terms) => {
if (!terms) return '';
if (!terms) return "";
switch (terms.trmsTpCd) {
case "MST00401": return $L("Privacy Policy");
case "MST00402": return $L("Terms & Conditions");
case "MST00405": return $L("Optional Terms");
default: return '';
case "MST00401":
return $L("Privacy Policy");
case "MST00402":
return $L("Terms & Conditions");
case "MST00405":
return $L("Optional Terms");
default:
return "";
}
};
@@ -528,9 +590,7 @@ export default function IntroPanel({
{/* 첫 번째 영역: 헤더 섹션 */}
<div className={css.headerSection}>
<div className={css.titleContainer}>
<div className={css.welcomeText}>
{$L("Welcome to")}
</div>
<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>
@@ -538,9 +598,7 @@ export default function IntroPanel({
</div>
</div>
<div className={css.descriptionContainer}>
<div className={css.descriptionText}>
{description}
</div>
<div className={css.descriptionText}>{description}</div>
</div>
</div>
@@ -553,15 +611,15 @@ export default function IntroPanel({
className={css.customeCheckbox}
selected={termsChecked}
onToggle={handleTermsToggle}
onFocus={() => handleFocus('termsCheckbox')}
onFocus={handleFocusTermsCheckbox}
onBlur={handleBlur}
spotlightId="termsCheckbox"
ariaLabel={$L("Terms & Conditions checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleTermsClick("MST00402")}
onFocus={() => handleFocus('termsButton')}
onClick={handleTermsClickMST00402}
onFocus={handleFocusTermsButton}
onBlur={handleBlur}
spotlightId="termsButton"
type={TYPES.terms}
@@ -579,15 +637,15 @@ export default function IntroPanel({
className={css.customeCheckbox}
selected={privacyChecked}
onToggle={handlePrivacyToggle}
onFocus={() => handleFocus('privacyCheckbox')}
onFocus={handleFocusPrivacyCheckbox}
onBlur={handleBlur}
spotlightId="privacyCheckbox"
ariaLabel={$L("Privacy Policy checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleTermsClick("MST00401")}
onFocus={() => handleFocus('privacyButton')}
onClick={handleTermsClickMST00401}
onFocus={handleFocusPrivacyButton}
onBlur={handleBlur}
spotlightId="privacyButton"
type={TYPES.terms}
@@ -605,29 +663,25 @@ export default function IntroPanel({
className={css.customeCheckbox}
selected={optionalChecked}
onToggle={handleOptionalToggle}
onFocus={() => handleFocus('optionalCheckbox')}
onFocus={handleFocusOptionalCheckbox}
onBlur={handleBlur}
spotlightId="optionalCheckbox"
ariaLabel={$L("Optional Terms checkbox")}
/>
<TButton
className={css.termsButton}
onClick={() => handleOptionalTermsClick("MST00405")}
onFocus={() => handleFocus('optionalButton')}
onClick={handleOptionalTermsClickMST00405}
onFocus={handleFocusOptionalButton}
onBlur={handleBlur}
spotlightId="optionalButton"
type={TYPES.terms}
ariaLabel={$L("View Optional Terms")}
>
<span className={css.termsText}>
{$L("Optional Terms")}
</span>
<span className={css.termsText}>{$L("Optional Terms")}</span>
</TButton>
</div>
</div>
<div className={css.termsRightPanel}>
{rightPanelContent}
</div>
<div className={css.termsRightPanel}>{rightPanelContent}</div>
</div>
{/* 세 번째 영역: Select All */}
@@ -636,14 +690,12 @@ export default function IntroPanel({
className={css.selectAllCheckbox}
selected={selectAllChecked}
onToggle={handleSelectAllToggle}
onFocus={() => handleFocus('selectAllCheckbox')}
onFocus={handleFocusSelectAllCheckbox}
onBlur={handleBlur}
spotlightId="selectAllCheckbox"
ariaLabel={$L("Select All checkbox")}
/>
<span className={css.selectAllText}>
{$L("Select All")}
</span>
<span className={css.selectAllText}>{$L("Select All")}</span>
</div>
{/* 네 번째 영역: 버튼 섹션 */}
@@ -651,7 +703,7 @@ export default function IntroPanel({
<TButton
className={css.agreeButton}
onClick={handleAgree}
onFocus={() => handleFocus('agreeButton')}
onFocus={handleFocusAgreeButton}
onBlur={handleBlur}
spotlightId="agreeButton"
type={TYPES.agree}
@@ -664,7 +716,7 @@ export default function IntroPanel({
<TButton
className={css.disagreeButton}
onClick={handleDisagree}
onFocus={() => handleFocus('disagreeButton')}
onFocus={handleFocusDisagreeButton}
onBlur={handleBlur}
spotlightId="disagreeButton"
type={TYPES.agree}
@@ -701,8 +753,7 @@ export default function IntroPanel({
open={popupVisible}
kind="figmaTermsPopup"
title={getTermsPopupTitle(currentTerms)}
text={currentTerms?.trmsCntt || ''}
onIntroTermsAgreeClick={handleTermsAgree} // Agree 버튼 핸들러 연결
text={currentTerms?.trmsCntt || ""}
onClose={onClose} // Close 버튼 핸들러 연결
/>
</>
@@ -745,10 +796,11 @@ export default function IntroPanel({
open={showExitMessagePopup}
hasText
title={$L("Exit Shop Time")}
text={$L("Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds.")}
text={$L(
"Thank you for using the Shop Time, and we hope to see you again. The app will close in 3 seconds.",
)}
/>
)}
</Region>
);
}

View File

@@ -2,13 +2,13 @@
@import "../../style/utils.module.less";
@COLOR_GREEN: #207847;
@COLOR_RED: #C91D53;
@COLOR_GRAY: #807F81;
@COLOR_GRAY02: #CFCFCF;
@COLOR_GRAY03: #57585A;
@COLOR_GRAY04: #C5C6C9;
@COLOR_GRAY05: #F8F8F8;
@COLOR_GRAY06: #57585A;
@COLOR_RED: #c91d53;
@COLOR_GRAY: #807f81;
@COLOR_GRAY02: #cfcfcf;
@COLOR_GRAY03: #57585a;
@COLOR_GRAY04: #c5c6c9;
@COLOR_GRAY05: #f8f8f8;
@COLOR_GRAY06: #57585a;
.panel {
> section {
@@ -47,9 +47,9 @@
display: inline-flex;
.welcomeText {
color: #807F81;
color: #807f81;
font-size: 62px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 400;
line-height: 62px;
word-wrap: break-word;
@@ -57,27 +57,27 @@
.brandContainer {
.shopText {
color: #57585A;
color: #57585a;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-family: "LG Smart_Korean";
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
}
.oText {
color: #C91D53;
color: #c91d53;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-family: "LG Smart_Korean";
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
}
.timeText {
color: #57585A;
color: #57585a;
font-size: 70px;
font-family: 'LG Smart_Korean';
font-family: "LG Smart_Korean";
font-weight: 700;
line-height: 75px;
word-wrap: break-word;
@@ -95,9 +95,9 @@
.descriptionText {
width: 1012.49px;
text-align: center;
color: #807F81;
color: #807f81;
font-size: 36px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 400;
line-height: 43px;
word-wrap: break-word;
@@ -126,7 +126,8 @@
justify-content: flex-start;
align-items: center;
gap: 20px;
display: inline-flex; .checkbox {
display: inline-flex;
.checkbox {
/* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */
width: 45px;
height: 45px;
@@ -137,9 +138,9 @@
height: 120px;
padding: 0 50px;
background: @COLOR_WHITE;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.20);
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2);
border-radius: 6px;
border: 1px solid #CFCFCF;
border: 1px solid #cfcfcf;
justify-content: space-between;
align-items: center;
display: flex;
@@ -150,7 +151,7 @@
.termsText {
color: black;
font-size: 35px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
@@ -166,7 +167,7 @@
&:focus,
&:focus-visible,
&:hover {
outline: 4px #C91D53 solid !important;
outline: 4px #c91d53 solid !important;
outline-offset: 2px !important;
transform: translateY(-2px) !important;
box-shadow: 0 4px 12px rgba(201, 29, 83, 0.3) !important;
@@ -177,12 +178,13 @@
}
}
}
} .termsRightPanel {
}
.termsRightPanel {
flex: 1 1 0;
align-self: stretch;
padding-left: 60px;
padding-right: 60px;
border-left: 1px #C5C6C9 solid;
border-left: 1px #c5c6c9 solid;
justify-content: center;
align-items: flex-start;
display: flex;
@@ -195,7 +197,8 @@
justify-content: center;
align-items: center;
gap: 15px;
display: inline-flex; .selectAllCheckbox {
display: inline-flex;
.selectAllCheckbox {
/* TCheckBoxSquare 컴포넌트가 스타일링을 담당 */
width: 45px;
height: 45px;
@@ -204,7 +207,7 @@
.selectAllText {
color: @COLOR_BLACK;
font-size: 35px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 35px;
word-wrap: break-word;
@@ -282,27 +285,30 @@
// 기본 상자 스타일
&:before {
content: '';
content: "";
width: 42px;
height: 42px;
background-color: @COLOR_WHITE;
border: 2px solid @COLOR_GRAY02;
border-radius: 4px;
display: block;
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
// ✅ 선택된 상태: 배경색과 테두리 모두 붉은색 + 굵은 테두리
&.selected:before {
background-color: #C91D53 !important;
border: 4px solid #C91D53 !important; // 굵은 테두리로 변경
background-color: #c91d53 !important;
border: 4px solid #c91d53 !important; // 굵은 테두리로 변경
box-shadow: 0 0 8px rgba(201, 29, 83, 0.3); // 약간의 그림자 효과
}
// 포커스 받았지만 선택 안됨
&.focused:not(.selected):before {
background-color: rgba(201, 29, 83, 0.1);
border: 4px solid #C91D53 !important;
border: 4px solid #c91d53 !important;
box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important;
}
@@ -315,13 +321,13 @@
// 포커스 받음 (선택된 상태가 아닌 경우에만 적용)
&.focused:not(.selected):before {
border: 4px solid #C91D53 !important;
border: 4px solid #c91d53 !important;
box-shadow: 0 0 10px rgba(199, 8, 80, 0.3) !important;
}
// 체크마크
&.selected:after {
content: '✓';
content: "✓";
color: @COLOR_WHITE;
font-size: 24px;
font-weight: bold;
@@ -356,9 +362,9 @@
.optionalDescription {
width: 595px;
color: #807F81;
color: #807f81;
font-size: 26px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 400;
line-height: 43px;
word-wrap: break-word;

View File

@@ -76,6 +76,7 @@ import LoadingPanel from "../LoadingPanel/LoadingPanel";
import MyPagePanel from "../MyPagePanel/MyPagePanel";
import OnSalePanel from "../OnSalePanel/OnSalePanel";
import PlayerPanel from "../PlayerPanel/PlayerPanel";
import PlayerPanelNew from "../PlayerPanel/PlayerPanel.new";
import SearchPanel from "../SearchPanel/SearchPanel";
import ThemeCurationPanel from "../ThemeCurationPanel/ThemeCurationPanel";
import TrendingNowPanel from "../TrendingNowPanel/TrendingNowPanel";
@@ -108,6 +109,7 @@ const panelMap = {
[Config.panel_names.VIDEO_TEST_PANEL]: VideoTestPanel,
[Config.panel_names.DETAIL_PANEL]: DetailPanel,
[Config.panel_names.PLAYER_PANEL]: PlayerPanel,
[Config.panel_names.PLAYER_PANEL_NEW]: PlayerPanelNew,
[Config.panel_names.CHECKOUT_PANEL]: CheckOutPanel,
[Config.panel_names.WELCOME_EVENT_PANEL]: WelcomeEventPanel,
[Config.panel_names.THEME_CURATION_PANEL]: ThemeCurationPanel,
@@ -139,7 +141,8 @@ export default function MainView({ className, initService }) {
const panels = useSelector((state) => state.panels.panels);
const lastPanelAction = useSelector((state) => state.panels.lastPanelAction);
const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const menuData = useSelector((state) => state.home.menuData?.data); const {
const menuData = useSelector((state) => state.home.menuData?.data);
const {
popupVisible,
activePopup,
data: errorCode,
@@ -154,10 +157,10 @@ export default function MainView({ className, initService }) {
deviceId,
} = useSelector((state) => state.common.appStatus);
const skipEndOfServicePopup = useSelector(
(state) => state.localSettings.skipEndOfServicePopup
(state) => state.localSettings.skipEndOfServicePopup,
);
const isInternetConnected = useSelector(
(state) => state.common.appStatus.isInternetConnected
(state) => state.common.appStatus.isInternetConnected,
);
const deviceCountryCode = httpHeader?.["X-Device-Country"] || "";
@@ -172,9 +175,8 @@ export default function MainView({ className, initService }) {
const [showEndOfServicePopup, setShowEndOfServicePopup] = useState(false);
const topPanel = panels[panels.length - 1];
useEffect(() => {
console.log('🔍 MainView 팝업 상태 변경:', {
console.log("🔍 MainView 팝업 상태 변경:", {
popupVisible,
activePopup,
});
@@ -202,6 +204,8 @@ export default function MainView({ className, initService }) {
let renderingPanels = [];
if (
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1]?.name ===
Config.panel_names.PLAYER_PANEL_NEW ||
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL
) {
renderingPanels = panels.slice(-2);
@@ -212,7 +216,8 @@ export default function MainView({ className, initService }) {
<>
{(isHomeOnTop ||
(panels.length === 1 &&
panels[0]?.name === Config.panel_names.PLAYER_PANEL)) && (
(panels[0]?.name === Config.panel_names.PLAYER_PANEL ||
panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW))) && (
<HomePanel
key={Config.panel_names.HOME_PANEL}
isOnTop={isHomeOnTop}
@@ -234,7 +239,9 @@ export default function MainView({ className, initService }) {
if (
index === 0 &&
renderingPanels.length === 2 &&
renderingPanels[1].name === Config.panel_names.PLAYER_PANEL &&
(renderingPanels[1].name === Config.panel_names.PLAYER_PANEL ||
renderingPanels[1].name ===
Config.panel_names.PLAYER_PANEL_NEW) &&
renderingPanels[1].panelInfo.modal
) {
isPanelOnTop = true;
@@ -309,8 +316,8 @@ export default function MainView({ className, initService }) {
mandatoryIncludeYn: "Y",
termsList: ["MST00401", "MST00402"],
},
reload
)
reload,
),
);
}
}, [dispatch, activePopup]);
@@ -362,10 +369,10 @@ export default function MainView({ className, initService }) {
dispatch(
changeAppStatus({
cursorVisible: ev.visibility || ev.detail.visibility,
})
}),
);
},
[dispatch]
[dispatch],
);
useEffect(() => {
@@ -377,7 +384,7 @@ export default function MainView({ className, initService }) {
dispatch(
changeAppStatus({
cursorVisible: window.cursorEvent && window.cursorEvent.visibility,
})
}),
);
}
return () => {
@@ -390,7 +397,7 @@ export default function MainView({ className, initService }) {
const popupTimerRef = useRef(null); // 타이머 ID를 저장할 상태 변수
const { upComingAlertShow } = useSelector(
(state) => state.myPage.upComingData
(state) => state.myPage.upComingData,
);
useEffect(() => {
@@ -423,7 +430,7 @@ export default function MainView({ className, initService }) {
patnrId: patnrId.toString(),
showId,
showNm,
})
}),
);
}
}, [activePopup, alertItems, popupVisible]);
@@ -465,7 +472,7 @@ export default function MainView({ className, initService }) {
upComingAlertShow.alertShows?.length > 0
) {
const alertList = upComingAlertShow.alertShows.filter((show) =>
isSameDateTime(show.strtDt)
isSameDateTime(show.strtDt),
);
if (alertList.length > 0) {
@@ -518,7 +525,7 @@ export default function MainView({ className, initService }) {
patnrId: patnrId.toString(),
showId,
showNm,
})
}),
);
dispatch(resetPanels());
@@ -526,7 +533,7 @@ export default function MainView({ className, initService }) {
pushPanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "upcoming", patnrId: patnrId.toString() },
})
}),
);
dispatch(setHidePopup());
setIntervalActive((prev) => !prev);
@@ -575,7 +582,7 @@ export default function MainView({ className, initService }) {
patnrId: patnrId.toString(),
showId,
showNm,
})
}),
);
setIntervalActive((prev) => !prev);
@@ -639,7 +646,12 @@ export default function MainView({ className, initService }) {
}
}, [webOSVersion]);
const handleErrorPopupClose = useCallback(() => {
console.log('handleErrorPopupClose 호출됨! activePopup:', activePopup, 'popupData:', popupData);
console.log(
"handleErrorPopupClose 호출됨! activePopup:",
activePopup,
"popupData:",
popupData,
);
if (popupData?.shouldPopPanel) {
dispatch(popPanel());
}
@@ -678,7 +690,7 @@ export default function MainView({ className, initService }) {
loadingComplete && (
<div
className={classNames(
css.container
css.container,
// showLoadingPanel.type === "launching" ? css.transparent : null
)}
>
@@ -731,9 +743,10 @@ export default function MainView({ className, initService }) {
popupData.errorCode,
popupData.errorMsg,
popupData.retDetailCode,
popupData.returnBindStrings
popupData.returnBindStrings,
)}
</p> <TButton className={css.popupBtn} onClick={handleErrorPopupClose}>
</p>{" "}
<TButton className={css.popupBtn} onClick={handleErrorPopupClose}>
{$L("OK")}
</TButton>
</div>
@@ -781,8 +794,6 @@ export default function MainView({ className, initService }) {
/>
) : null}
<SystemNotification />
{loadingComplete &&
activePopup === Config.ACTIVE_POPUP.endOfServicePopup &&

View File

@@ -1535,8 +1535,24 @@ const PlayerPanel = ({
if (broadcast.type === "videoError") {
return null;
}
// For previews, always use the direct URL from panelInfo.
if (panelInfo.modal) {
return panelInfo.showUrl;
}
// For fullscreen, the playlist is the primary source.
if (playListInfo && playListInfo.length > 0 && playListInfo[selectedIndex]?.showUrl) {
return playListInfo[selectedIndex]?.showUrl;
}
// Fallback for fullscreen if playlist isn't ready, use panelInfo URL.
if (!panelInfo.modal && panelInfo.showUrl) {
return panelInfo.showUrl;
}
return playListInfo && playListInfo[selectedIndex]?.showUrl;
}, [playListInfo, selectedIndex, broadcast]);
}, [panelInfo, playListInfo, selectedIndex, broadcast]);
const isYoutube = useMemo(() => {
if (currentPlayingUrl && currentPlayingUrl.includes("youtu")) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
@videoBackgroundColor: black;
.videoContainer {
position: absolute;
background-color: @videoBackgroundColor;
z-index: 21;
.playButton {
.size(@w: 60px, @h: 60px);
background-image: url("../../../assets/images/btn/btn-video-play-nor@3x.png");
background-size: cover;
&:focus {
.size(@w: 60px, @h: 60px);
border-radius: 50%;
background-color: #c70850;
opacity: 0.5;
}
}
.pauseButton {
.size(@w: 60px, @h: 60px);
background-image: url("../../../assets/images/btn/btn-voc-pause-nor@3x.png");
background-size: cover;
&:focus {
.size(@w: 60px, @h: 60px);
border-radius: 50%;
background-color: #c70850;
opacity: 0.5;
}
}
.toOpenBtn {
.size(@w: 147px, @h: 243px);
min-width: 60px !important;
margin: 192px 0 136px 512px;
text-align: center;
background-image: url(../../../assets/images/btn/btn-toopen-foc.svg);
}
.videoReduce {
.size(@w:78px, @h:78px);
background: url("../../../assets/images/btn/btn-video-min-nor@3x.png")
no-repeat center center/60px 60px;
position: absolute;
right: 60px;
bottom: 150px;
z-index: 3;
&:focus {
.size(@w:78px, @h:78px);
background-color: rgba(199, 8, 80, 0.5);
border-radius: 50%;
}
}
&.modal,
&.background {
width: 1px;
height: 1px;
left: -1px;
top: -1px;
pointer-events: none;
z-index: 1;
background-color: @videoBackgroundColor;
overflow: visible;
.tabContainer,
.arrow,
.toOpenBtn {
display: none;
}
}
&.hideSubtitle {
video::cue {
visibility: hidden;
}
}
}