fix: 선택약관 미존재 조건 반영

This commit is contained in:
djaco
2025-07-03 10:45:17 +09:00
parent 56bfac5c95
commit b993b2ebec
8 changed files with 398 additions and 51 deletions

View File

@@ -43,6 +43,7 @@
"axios": "^0.21.1",
"google-libphonenumber": "^3.2.34",
"ilib": "^14.3.0",
"lodash": "4.17.21",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"prop-types": "^15.6.2",
@@ -52,9 +53,12 @@
"react-player": "^1.15.3",
"react-redux": "^7.2.3",
"redux": "^3.7.2",
"redux-thunk": "^2.3.0"
"redux-thunk": "2.3.0"
},
"browserslist": [
"chrome 38"
]
],
"devDependencies": {
"prettier": "^3.5.3"
}
}

View File

@@ -154,7 +154,8 @@ function AppBase(props) {
const termsData = useSelector((state) => state.home.termsData);
useEffect(() => {
if (termsData?.data?.terms) {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (termsData && termsData.data && termsData.data.terms) {
dispatch(getTermsAgreeYn());
}
}, [termsData, dispatch]);
@@ -166,7 +167,8 @@ function AppBase(props) {
);
// const macAddress = useSelector((state) => state.common.macAddress);
const deviceCountryCode = httpHeader?.["X-Device-Country"] || "";
// Chromium68 호환성을 위해 Optional Chaining 제거
const deviceCountryCode = httpHeader && httpHeader["X-Device-Country"] || "";
useEffect(() => {
if (!cursorVisible && !Spotlight.getCurrent()) {
@@ -214,23 +216,31 @@ function AppBase(props) {
// called by [receive httpHeader, launch, relaunch]
const initService = useCallback(
(haveyInit = true) => {
/*
// console.log(
// "<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ",
// haveyInit,
// appinfo
// );
console.log(
"<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ",
haveyInit,
appinfo
"[App.js] initService,httpHeaderRef.current",
httpHeaderRef.current
);
*/
console.log("[App.js] haveyInit", haveyInit);
if (httpHeaderRef.current) {
if (haveyInit) {
dispatch(changeAppStatus({ connectionFailed: false }));
if (typeof window === "object" && window.PalmSystem) {
dispatch(
changeAppStatus({
cursorVisible: window.PalmSystem?.cursor?.visibility,
})
);
dispatch(
changeAppStatus({
// Chromium68 호환성을 위해 Optional Chaining 제거
cursorVisible: window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility,
})
);
}
dispatch(getHomeMenu());
dispatch(getMyRecommandedKeyword());
@@ -246,11 +256,12 @@ function AppBase(props) {
);
// pyh TODO: edit or delete later (line 196 ~ 198)
if (launchParams?.bypass) {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (launchParams && launchParams.bypass) {
dispatch(handleBypassLink(launchParams.bypass));
}
if (launchParams?.contentTarget) {
dispatch(handleDeepLink(launchParams?.contentTarget));
if (launchParams && launchParams.contentTarget) {
dispatch(handleDeepLink(launchParams.contentTarget));
} else {
dispatch(
sendLogTotalRecommend({
@@ -295,10 +306,11 @@ function AppBase(props) {
// set foreground flag using delay time.
clearTimeout(foreGroundChangeTimer);
foreGroundChangeTimer = setTimeout(() => {
console.log(
"visibility changed !!! ==> set to foreground cursorVisible",
JSON.stringify(window.PalmSystem?.cursor?.visibility)
); // eslint-disable-line no-console
console.log(
"visibility changed !!! ==> set to foreground cursorVisible",
// Chromium68 호환성을 위해 Optional Chaining 제거
JSON.stringify(window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility)
); // eslint-disable-line no-console
if (platform.platformName !== "webos") {
//for debug
dispatch(
@@ -311,7 +323,8 @@ function AppBase(props) {
dispatch(
changeAppStatus({
isAppForeground: true,
cursorVisible: window.PalmSystem?.cursor?.visibility,
// Chromium68 호환성을 위해 Optional Chaining 제거
cursorVisible: window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility,
})
);
}

View File

@@ -59,6 +59,7 @@ export const types = {
// home actions
GET_HOME_TERMS: "GET_HOME_TERMS",
SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP",
SET_OPTIONAL_TERMS_AVAILABILITY: "SET_OPTIONAL_TERMS_AVAILABILITY",
GET_HOME_MENU: "GET_HOME_MENU",
GET_HOME_LAYOUT: "GET_HOME_LAYOUT",
GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS",

View File

@@ -17,12 +17,20 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
});
// 약관 ID 매핑을 별도로 생성하여 저장
if (response.data?.data?.terms) {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (response.data && response.data.data && response.data.data.terms) {
const termsIdMap = {};
let hasOptionalTerms = false; // MST00405 존재 여부 확인
response.data.data.terms.forEach(term => {
if (term.trmsTpCd && term.trmsId) {
termsIdMap[term.trmsTpCd] = term.trmsId;
}
// MST00405 선택약관 존재 여부 확인
if (term.trmsTpCd === "MST00405") {
hasOptionalTerms = true;
}
});
dispatch({
@@ -30,8 +38,21 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
payload: termsIdMap,
});
// 선택약관 존재 여부 상태 설정
// TODO: 테스트용 - 임시로 false 강제 설정
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
dispatch({
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
payload: finalOptionalTermsValue,
});
console.log("[optionalTermsAvailable] 실제값:", hasOptionalTerms, "강제설정값:", finalOptionalTermsValue);
if (process.env.NODE_ENV === "development") {
console.log("약관 ID 매핑 생성:", termsIdMap);
console.log("선택약관 존재 여부:", hasOptionalTerms);
}
}
@@ -80,12 +101,20 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => {
});
// 약관 ID 매핑을 별도로 생성하여 저장
if (response.data?.data?.terms) {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (response.data && response.data.data && response.data.data.terms) {
const termsIdMap = {};
let hasOptionalTerms = false; // MST00405 존재 여부 확인
response.data.data.terms.forEach(term => {
if (term.trmsTpCd && term.trmsId) {
termsIdMap[term.trmsTpCd] = term.trmsId;
}
// MST00405 선택약관 존재 여부 확인
if (term.trmsTpCd === "MST00405") {
hasOptionalTerms = true;
}
});
dispatch({
@@ -93,8 +122,20 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => {
payload: termsIdMap,
});
// 선택약관 존재 여부 상태 설정
// TODO: 테스트용 - 임시로 false 강제 설정
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
dispatch({
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
payload: finalOptionalTermsValue,
});
console.log("[optionalTermsAvailable] 실제값:", hasOptionalTerms, "강제설정값:", finalOptionalTermsValue);
if (process.env.NODE_ENV === "development") {
console.log("약관 ID 매핑 생성:", termsIdMap);
console.log("선택약관 존재 여부:", hasOptionalTerms);
}
}
@@ -169,12 +210,20 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
});
// 약관 ID 매핑을 별도로 생성하여 저장
if (result.data?.data?.terms) {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (result.data && result.data.data && result.data.data.terms) {
const termsIdMap = {};
let hasOptionalTerms = false; // MST00405 존재 여부 확인
result.data.data.terms.forEach(term => {
if (term.trmsTpCd && term.trmsId) {
termsIdMap[term.trmsTpCd] = term.trmsId;
}
// MST00405 선택약관 존재 여부 확인
if (term.trmsTpCd === "MST00405") {
hasOptionalTerms = true;
}
});
dispatch({
@@ -182,8 +231,19 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
payload: termsIdMap,
});
// 선택약관 존재 여부 상태 설정 2025-07-03
// TODO: 테스트용 - 임시로 false 강제 설정
const forceDisableOptionalTerms = false; // 테스트 완료 후 false로 변경
const finalOptionalTermsValue = forceDisableOptionalTerms ? false : hasOptionalTerms;
dispatch({
type: types.SET_OPTIONAL_TERMS_AVAILABILITY,
payload: finalOptionalTermsValue,
});
if (process.env.NODE_ENV === "development") {
console.log("약관 ID 매핑 생성:", termsIdMap);
console.log("선택약관 존재 여부 - 실제값:", hasOptionalTerms, "강제설정값:", finalOptionalTermsValue);
}
}
@@ -195,11 +255,12 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
return { success: true, data: result.data };
} else {
// retCode가 0이 아닌 일반적인 API 에러
console.error("API returned non-zero retCode:", result.data?.retCode);
// Chromium68 호환성을 위해 Optional Chaining 제거
console.error("API returned non-zero retCode:", result.data && result.data.retCode);
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
return {
success: false,
message: result.data?.retMsg || "서버 오류가 발생했습니다."
message: (result.data && result.data.retMsg) || "서버 오류가 발생했습니다."
};
}
};

View File

@@ -26,6 +26,7 @@ const initialState = {
isPaused: false,
},
termsIdMap: {}, // added new property to initialState
optionalTermsAvailable: false, // 선택약관 존재 여부
};
export const homeReducer = (state = initialState, action) => {
@@ -42,6 +43,12 @@ export const homeReducer = (state = initialState, action) => {
termsIdMap: action.payload,
};
case types.SET_OPTIONAL_TERMS_AVAILABILITY:
return {
...state,
optionalTermsAvailable: action.payload,
};
case types.GET_HOME_MENU: {
let menuItems = [];

View File

@@ -0,0 +1,223 @@
// src/utils/dataHelpers.js
/**
* @fileoverview webOS TV 데이터 처리 유틸리티 함수들
*
* Chromium68 기반 구형 webOS TV 호환성을 위해 Optional Chaining(?.) 대신
* lodash를 사용하여 안전한 객체 속성 접근을 제공합니다.
*
* Optional Chaining은 Chrome 80+에서만 지원되므로,
* Chromium68 환경에서는 문법 오류가 발생할 수 있습니다.
*
* @author Your Team
* @since 2.0.0
* @requires lodash
*/
import { get, isEmpty, isArray } from 'lodash';
/**
* 객체의 중첩된 속성에 안전하게 접근합니다.
* Optional Chaining(obj?.prop?.nested) 대신 사용하는 호환성 함수입니다.
*
* @param {Object|Array|null|undefined} obj - 접근할 객체
* @param {string|Array} path - 접근할 속성 경로 (점 표기법 또는 배열)
* @param {*} [defaultValue=null] - 값이 없을 때 반환할 기본값
* @returns {*} 찾은 값 또는 기본값
*
* @example
* // 기존 Optional Chaining 방식 (Chromium68에서 작동 안함)
* // const city = user?.profile?.address?.city;
*
* // 호환성을 위한 새로운 방식
* const user = { profile: { address: { city: 'Seoul' } } };
* const city = safeGet(user, 'profile.address.city'); // 'Seoul'
* const country = safeGet(user, 'profile.address.country', 'Unknown'); // 'Unknown'
*
* // 배열 접근
* const items = [{ name: 'Apple' }, { name: 'Banana' }];
* const firstItem = safeGet(items, '[0].name'); // 'Apple'
* const thirdItem = safeGet(items, '[2].name', 'Not Found'); // 'Not Found'
*
* // webOS API 응답 처리
* const deviceInfo = safeGet(webOSResponse, 'device.modelName', 'Unknown TV');
*/
export const safeGet = (obj, path, defaultValue = null) =>
get(obj, path, defaultValue);
/**
* 데이터가 유효한 배열인지 확인합니다.
* API 응답이나 상태 데이터가 렌더링 가능한 배열 형태인지 검증할 때 사용합니다.
*
* @param {*} data - 검증할 데이터
* @returns {boolean} 유효한 배열이면 true, 아니면 false
*
* @example
* // API 응답 검증
* const apiResponse = { data: { products: [] } };
* const products = safeGet(apiResponse, 'data.products', []);
*
* if (hasValidData(products)) {
* // 안전하게 배열 렌더링
* return products.map(product => <ProductCard key={product.id} {...product} />);
* } else {
* return <EmptyState message="상품이 없습니다" />;
* }
*
* // 다양한 케이스 테스트
* hasValidData([1, 2, 3]); // true
* hasValidData([]); // false (빈 배열)
* hasValidData(null); // false
* hasValidData(undefined); // false
* hasValidData({}); // false (객체)
* hasValidData('string'); // false (문자열)
*/
export const hasValidData = (data) =>
!isEmpty(data) && isArray(data);
/**
* API 응답에서 데이터 부분을 안전하게 추출합니다.
* 일반적인 API 응답 구조에서 실제 데이터 객체를 가져올 때 사용합니다.
*
* @param {Object} response - API 응답 객체
* @param {string} [dataPath='data'] - 데이터가 위치한 경로
* @returns {Object} 추출된 데이터 객체 (기본값: 빈 객체)
*
* @example
* // 일반적인 API 응답 구조
* const apiResponse = {
* status: 'success',
* data: {
* products: [...],
* pagination: { ... },
* metadata: { ... }
* }
* };
*
* // 데이터 추출
* const responseData = getApiData(apiResponse); // { products: [...], pagination: {...}, ... }
* const products = safeGet(responseData, 'products', []);
*
* // 커스텀 경로 지정
* const nestedResponse = {
* result: {
* payload: {
* items: [...]
* }
* }
* };
* const payloadData = getApiData(nestedResponse, 'result.payload'); // { items: [...] }
*
* // webOS 서비스 응답 처리
* const webOSServiceResponse = {
* returnValue: true,
* response: {
* deviceList: [...]
* }
* };
* const serviceData = getApiData(webOSServiceResponse, 'response'); // { deviceList: [...] }
*
* // 오류나 빈 응답 처리
* const emptyResponse = null;
* const safeData = getApiData(emptyResponse); // {} (빈 객체)
*/
export const getApiData = (response, dataPath = 'data') =>
get(response, dataPath, {});
/**
* 추가 유틸리티 함수들 - webOS TV 환경에서 자주 사용되는 패턴들
*/
/**
* 객체에 특정 속성이 존재하는지 안전하게 확인합니다.
*
* @param {Object} obj - 확인할 객체
* @param {string} path - 확인할 속성 경로
* @returns {boolean} 속성이 존재하면 true
*
* @example
* const settings = { user: { preferences: { theme: 'dark' } } };
*
* // 기존 방식 (Chromium68에서 위험)
* // const hasTheme = settings?.user?.preferences?.theme !== undefined;
*
* // 호환성 방식
* const hasTheme = safeHas(settings, 'user.preferences.theme'); // true
* const hasLanguage = safeHas(settings, 'user.preferences.language'); // false
*/
export const safeHas = (obj, path) => {
return get(obj, path) !== undefined;
};
/**
* 배열의 첫 번째 요소를 안전하게 가져옵니다.
*
* @param {Array} array - 대상 배열
* @param {*} [defaultValue=null] - 기본값
* @returns {*} 첫 번째 요소 또는 기본값
*
* @example
* const products = [{ id: 1, name: 'TV' }, { id: 2, name: 'Phone' }];
* const firstProduct = safeFirst(products); // { id: 1, name: 'TV' }
*
* const emptyArray = [];
* const firstItem = safeFirst(emptyArray, { id: 0, name: 'Default' }); // { id: 0, name: 'Default' }
*/
export const safeFirst = (array, defaultValue = null) => {
return hasValidData(array) ? array[0] : defaultValue;
};
/**
* 문자열이 비어있지 않은지 확인합니다.
*
* @param {string} str - 확인할 문자열
* @returns {boolean} 유효한 문자열이면 true
*
* @example
* isValidString('Hello'); // true
* isValidString(''); // false
* isValidString(null); // false
* isValidString(undefined); // false
* isValidString(' '); // false (공백만 있는 경우)
*/
export const isValidString = (str) => {
return typeof str === 'string' && str.trim().length > 0;
};
// 사용 예시를 위한 실제 webOS TV 컴포넌트에서의 활용
/**
* @example 실제 컴포넌트에서의 사용법
*
* import { safeGet, hasValidData, getApiData } from '../utils/dataHelpers';
*
* const ProductList = ({ apiResponse }) => {
* // API 응답에서 안전하게 데이터 추출
* const responseData = getApiData(apiResponse);
* const products = safeGet(responseData, 'products', []);
* const totalCount = safeGet(responseData, 'pagination.total', 0);
*
* // 데이터 유효성 검사
* if (!hasValidData(products)) {
* return <div>상품이 없습니다.</div>;
* }
*
* return (
* <div>
* <h2>상품 목록 ({totalCount}개)</h2>
* {products.map(product => {
* const productName = safeGet(product, 'name', '이름 없음');
* const productPrice = safeGet(product, 'price.amount', 0);
* const productImage = safeGet(product, 'images[0].url', '/default-image.jpg');
*
* return (
* <div key={product.id}>
* <img src={productImage} alt={productName} />
* <h3>{productName}</h3>
* <p>{productPrice}원</p>
* </div>
* );
* })}
* </div>
* );
* };
*/

View File

@@ -146,11 +146,16 @@ export default function HomeBanner({
const termsData = useSelector((state) => state.home.termsData);
const termsIdMap = useSelector((state) => state.home.termsIdMap);
const optionalTermsData = useSelector((state) =>
state.home.termsData?.data?.terms.find(
(term) => term.trmsTpCd === "MST00405",
),
);
const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable);
const optionalTermsData = useSelector((state) => {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (state.home.termsData && state.home.termsData.data && state.home.termsData.data.terms) {
return state.home.termsData.data.terms.find(
(term) => term.trmsTpCd === "MST00405"
);
}
return null;
});
const termsLoading = useSelector((state) => state.common.termsLoading);
const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
// 선택약관 동의여부
@@ -166,13 +171,21 @@ export default function HomeBanner({
// 선택약관 팝업 표시 여부 ===================================================
const shouldShowOptionalTermsPopup = useMemo(() => {
console.log('[HomeBanner] Step 1: termsLoading, isGnbOpened, optionalTermsAgreed 상태 확인', { termsLoading, isGnbOpened, optionalTermsAgreed });
if (termsLoading || isGnbOpened || optionalTermsAgreed) {
console.log('[HomeBanner] Early return: 조건 불만족 (termsLoading || isGnbOpened || optionalTermsAgreed)');
console.log('[HomeBanner] Step 1: 상태 확인', {
termsLoading,
isGnbOpened,
optionalTermsAgreed,
optionalTermsAvailable
});
// optionalTermsAvailable = false면 팝업 표시 안함
if (termsLoading || isGnbOpened || optionalTermsAgreed || !optionalTermsAvailable) {
console.log('[HomeBanner] Early return: 조건 불만족');
return false;
}
const terms = termsData?.data?.terms;
// Chromium68 호환성을 위해 Optional Chaining 제거
const terms = termsData && termsData.data && termsData.data.terms;
console.log('[HomeBanner] Step 2: termsData 확인', terms);
if (!terms) {
console.log('[HomeBanner] Early return: terms가 존재하지 않음');
@@ -188,7 +201,7 @@ export default function HomeBanner({
console.log('[HomeBanner] Step 4: 최종 결과', result);
return result;
}, [termsData, termsLoading, isGnbOpened, optionalTermsAgreed]);
}, [termsData, termsLoading, isGnbOpened, optionalTermsAgreed, optionalTermsAvailable]);
// 선택약관 팝업 표시 여부 ===================================================

View File

@@ -8,6 +8,7 @@ import React, {
useState,
} from "react";
import { useDispatch, useSelector } from "react-redux";
import BasicIntroPanel from "./IntroPanel.jsx";
import Region from "@enact/sandstone/Region";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
@@ -54,7 +55,23 @@ const Container = SpotlightContainerDecorator(
const MAX_RETRY_ATTEMPTS = 5;
const RETRY_DELAY_MS = 1500; // 1.5 seconds
export default function IntroPanel({
// 조건부 IntroPanel 컴포넌트 (선택약관 존재 여부에 따라 다른 컴포넌트 렌더링)
export default function IntroPanel(props) {
const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable);
// 선택약관이 없으면 기존 IntroPanel.jsx 사용
if (!optionalTermsAvailable) {
console.log('[IntroPanel.new] optionalTermsAvailable = false, 기존 IntroPanel 사용');
return <BasicIntroPanel {...props} />;
}
// 선택약관이 있으면 고급 IntroPanel 사용
console.log('[IntroPanel.new] optionalTermsAvailable = true, 고급 IntroPanel 사용');
return <IntroPanelWithOptional {...props} />;
}
// 선택약관 포함 고급 IntroPanel 컴포넌트
function IntroPanelWithOptional({
// children,
// isTabActivated,
// handleCancel,
@@ -101,24 +118,32 @@ export default function IntroPanel({
} = termsState;
const introTermsData = useMemo(() => {
return (
termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402",
) || []
);
// Chromium68 호환성을 위해 Optional Chaining 제거
if (termsData && termsData.data && termsData.data.terms) {
return termsData.data.terms.filter(
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402"
);
}
return [];
}, [termsData]);
const optionalTermsData = useMemo(() => {
return (
termsData?.data?.terms.filter(
(item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406",
) || []
);
// Chromium68 호환성을 위해 Optional Chaining 제거
if (termsData && termsData.data && termsData.data.terms) {
return termsData.data.terms.filter(
(item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406"
);
}
return [];
}, [termsData]);
const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion,
);
const webOSVersion = useSelector((state) => {
// Chromium68 호환성을 위해 Optional Chaining 제거
if (state.common && state.common.appStatus && state.common.appStatus.webOSVersion) {
return state.common.appStatus.webOSVersion;
}
return null;
});
// const webOSVersion = 4.5;
// WebOS 버전별 UI 표시 모드 결정