fix: 선택약관 미존재 조건 반영
This commit is contained in:
@@ -43,6 +43,7 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"google-libphonenumber": "^3.2.34",
|
"google-libphonenumber": "^3.2.34",
|
||||||
"ilib": "^14.3.0",
|
"ilib": "^14.3.0",
|
||||||
|
"lodash": "4.17.21",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@@ -52,9 +53,12 @@
|
|||||||
"react-player": "^1.15.3",
|
"react-player": "^1.15.3",
|
||||||
"react-redux": "^7.2.3",
|
"react-redux": "^7.2.3",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-thunk": "^2.3.0"
|
"redux-thunk": "2.3.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"chrome 38"
|
"chrome 38"
|
||||||
]
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ function AppBase(props) {
|
|||||||
const termsData = useSelector((state) => state.home.termsData);
|
const termsData = useSelector((state) => state.home.termsData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (termsData?.data?.terms) {
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
if (termsData && termsData.data && termsData.data.terms) {
|
||||||
dispatch(getTermsAgreeYn());
|
dispatch(getTermsAgreeYn());
|
||||||
}
|
}
|
||||||
}, [termsData, dispatch]);
|
}, [termsData, dispatch]);
|
||||||
@@ -166,7 +167,8 @@ function AppBase(props) {
|
|||||||
);
|
);
|
||||||
// const macAddress = useSelector((state) => state.common.macAddress);
|
// const macAddress = useSelector((state) => state.common.macAddress);
|
||||||
|
|
||||||
const deviceCountryCode = httpHeader?.["X-Device-Country"] || "";
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
const deviceCountryCode = httpHeader && httpHeader["X-Device-Country"] || "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cursorVisible && !Spotlight.getCurrent()) {
|
if (!cursorVisible && !Spotlight.getCurrent()) {
|
||||||
@@ -214,23 +216,31 @@ function AppBase(props) {
|
|||||||
|
|
||||||
// called by [receive httpHeader, launch, relaunch]
|
// called by [receive httpHeader, launch, relaunch]
|
||||||
const initService = useCallback(
|
const initService = useCallback(
|
||||||
|
|
||||||
(haveyInit = true) => {
|
(haveyInit = true) => {
|
||||||
/*
|
|
||||||
|
// console.log(
|
||||||
|
// "<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ",
|
||||||
|
// haveyInit,
|
||||||
|
// appinfo
|
||||||
|
// );
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"<<<<<<<<<<<<< appinfo >>>>>>>>>>>>{heavyInit, appinfo} ",
|
"[App.js] initService,httpHeaderRef.current",
|
||||||
haveyInit,
|
httpHeaderRef.current
|
||||||
appinfo
|
|
||||||
);
|
);
|
||||||
*/
|
console.log("[App.js] haveyInit", haveyInit);
|
||||||
|
|
||||||
if (httpHeaderRef.current) {
|
if (httpHeaderRef.current) {
|
||||||
if (haveyInit) {
|
if (haveyInit) {
|
||||||
dispatch(changeAppStatus({ connectionFailed: false }));
|
dispatch(changeAppStatus({ connectionFailed: false }));
|
||||||
if (typeof window === "object" && window.PalmSystem) {
|
if (typeof window === "object" && window.PalmSystem) {
|
||||||
dispatch(
|
dispatch(
|
||||||
changeAppStatus({
|
changeAppStatus({
|
||||||
cursorVisible: window.PalmSystem?.cursor?.visibility,
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
})
|
cursorVisible: window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility,
|
||||||
);
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
dispatch(getHomeMenu());
|
dispatch(getHomeMenu());
|
||||||
dispatch(getMyRecommandedKeyword());
|
dispatch(getMyRecommandedKeyword());
|
||||||
@@ -246,11 +256,12 @@ function AppBase(props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// pyh TODO: edit or delete later (line 196 ~ 198)
|
// pyh TODO: edit or delete later (line 196 ~ 198)
|
||||||
if (launchParams?.bypass) {
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
if (launchParams && launchParams.bypass) {
|
||||||
dispatch(handleBypassLink(launchParams.bypass));
|
dispatch(handleBypassLink(launchParams.bypass));
|
||||||
}
|
}
|
||||||
if (launchParams?.contentTarget) {
|
if (launchParams && launchParams.contentTarget) {
|
||||||
dispatch(handleDeepLink(launchParams?.contentTarget));
|
dispatch(handleDeepLink(launchParams.contentTarget));
|
||||||
} else {
|
} else {
|
||||||
dispatch(
|
dispatch(
|
||||||
sendLogTotalRecommend({
|
sendLogTotalRecommend({
|
||||||
@@ -295,10 +306,11 @@ function AppBase(props) {
|
|||||||
// set foreground flag using delay time.
|
// set foreground flag using delay time.
|
||||||
clearTimeout(foreGroundChangeTimer);
|
clearTimeout(foreGroundChangeTimer);
|
||||||
foreGroundChangeTimer = setTimeout(() => {
|
foreGroundChangeTimer = setTimeout(() => {
|
||||||
console.log(
|
console.log(
|
||||||
"visibility changed !!! ==> set to foreground cursorVisible",
|
"visibility changed !!! ==> set to foreground cursorVisible",
|
||||||
JSON.stringify(window.PalmSystem?.cursor?.visibility)
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
); // eslint-disable-line no-console
|
JSON.stringify(window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility)
|
||||||
|
); // eslint-disable-line no-console
|
||||||
if (platform.platformName !== "webos") {
|
if (platform.platformName !== "webos") {
|
||||||
//for debug
|
//for debug
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -311,7 +323,8 @@ function AppBase(props) {
|
|||||||
dispatch(
|
dispatch(
|
||||||
changeAppStatus({
|
changeAppStatus({
|
||||||
isAppForeground: true,
|
isAppForeground: true,
|
||||||
cursorVisible: window.PalmSystem?.cursor?.visibility,
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
cursorVisible: window.PalmSystem && window.PalmSystem.cursor && window.PalmSystem.cursor.visibility,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const types = {
|
|||||||
// home actions
|
// home actions
|
||||||
GET_HOME_TERMS: "GET_HOME_TERMS",
|
GET_HOME_TERMS: "GET_HOME_TERMS",
|
||||||
SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP",
|
SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP",
|
||||||
|
SET_OPTIONAL_TERMS_AVAILABILITY: "SET_OPTIONAL_TERMS_AVAILABILITY",
|
||||||
GET_HOME_MENU: "GET_HOME_MENU",
|
GET_HOME_MENU: "GET_HOME_MENU",
|
||||||
GET_HOME_LAYOUT: "GET_HOME_LAYOUT",
|
GET_HOME_LAYOUT: "GET_HOME_LAYOUT",
|
||||||
GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS",
|
GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS",
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 약관 ID 매핑을 별도로 생성하여 저장
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
||||||
if (response.data?.data?.terms) {
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
if (response.data && response.data.data && response.data.data.terms) {
|
||||||
const termsIdMap = {};
|
const termsIdMap = {};
|
||||||
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
||||||
|
|
||||||
response.data.data.terms.forEach(term => {
|
response.data.data.terms.forEach(term => {
|
||||||
if (term.trmsTpCd && term.trmsId) {
|
if (term.trmsTpCd && term.trmsId) {
|
||||||
termsIdMap[term.trmsTpCd] = term.trmsId;
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MST00405 선택약관 존재 여부 확인
|
||||||
|
if (term.trmsTpCd === "MST00405") {
|
||||||
|
hasOptionalTerms = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -30,8 +38,21 @@ export const getHomeTerms = (props) => (dispatch, getState) => {
|
|||||||
payload: termsIdMap,
|
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") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.log("약관 ID 매핑 생성:", termsIdMap);
|
console.log("약관 ID 매핑 생성:", termsIdMap);
|
||||||
|
console.log("선택약관 존재 여부:", hasOptionalTerms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +101,20 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 약관 ID 매핑을 별도로 생성하여 저장
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
||||||
if (response.data?.data?.terms) {
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
if (response.data && response.data.data && response.data.data.terms) {
|
||||||
const termsIdMap = {};
|
const termsIdMap = {};
|
||||||
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
||||||
|
|
||||||
response.data.data.terms.forEach(term => {
|
response.data.data.terms.forEach(term => {
|
||||||
if (term.trmsTpCd && term.trmsId) {
|
if (term.trmsTpCd && term.trmsId) {
|
||||||
termsIdMap[term.trmsTpCd] = term.trmsId;
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MST00405 선택약관 존재 여부 확인
|
||||||
|
if (term.trmsTpCd === "MST00405") {
|
||||||
|
hasOptionalTerms = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -93,8 +122,20 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => {
|
|||||||
payload: termsIdMap,
|
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") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.log("약관 ID 매핑 생성:", termsIdMap);
|
console.log("약관 ID 매핑 생성:", termsIdMap);
|
||||||
|
console.log("선택약관 존재 여부:", hasOptionalTerms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +210,20 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 약관 ID 매핑을 별도로 생성하여 저장
|
// 약관 ID 매핑을 별도로 생성하여 저장
|
||||||
if (result.data?.data?.terms) {
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
|
if (result.data && result.data.data && result.data.data.terms) {
|
||||||
const termsIdMap = {};
|
const termsIdMap = {};
|
||||||
|
let hasOptionalTerms = false; // MST00405 존재 여부 확인
|
||||||
|
|
||||||
result.data.data.terms.forEach(term => {
|
result.data.data.terms.forEach(term => {
|
||||||
if (term.trmsTpCd && term.trmsId) {
|
if (term.trmsTpCd && term.trmsId) {
|
||||||
termsIdMap[term.trmsTpCd] = term.trmsId;
|
termsIdMap[term.trmsTpCd] = term.trmsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MST00405 선택약관 존재 여부 확인
|
||||||
|
if (term.trmsTpCd === "MST00405") {
|
||||||
|
hasOptionalTerms = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -182,8 +231,19 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
|
|||||||
payload: termsIdMap,
|
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") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.log("약관 ID 매핑 생성:", termsIdMap);
|
console.log("약관 ID 매핑 생성:", termsIdMap);
|
||||||
|
console.log("선택약관 존재 여부 - 실제값:", hasOptionalTerms, "강제설정값:", finalOptionalTermsValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +255,12 @@ export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) =>
|
|||||||
return { success: true, data: result.data };
|
return { success: true, data: result.data };
|
||||||
} else {
|
} else {
|
||||||
// retCode가 0이 아닌 일반적인 API 에러
|
// 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 });
|
dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE });
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: result.data?.retMsg || "서버 오류가 발생했습니다."
|
message: (result.data && result.data.retMsg) || "서버 오류가 발생했습니다."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const initialState = {
|
|||||||
isPaused: false,
|
isPaused: false,
|
||||||
},
|
},
|
||||||
termsIdMap: {}, // added new property to initialState
|
termsIdMap: {}, // added new property to initialState
|
||||||
|
optionalTermsAvailable: false, // 선택약관 존재 여부
|
||||||
};
|
};
|
||||||
|
|
||||||
export const homeReducer = (state = initialState, action) => {
|
export const homeReducer = (state = initialState, action) => {
|
||||||
@@ -42,6 +43,12 @@ export const homeReducer = (state = initialState, action) => {
|
|||||||
termsIdMap: action.payload,
|
termsIdMap: action.payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case types.SET_OPTIONAL_TERMS_AVAILABILITY:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
optionalTermsAvailable: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
case types.GET_HOME_MENU: {
|
case types.GET_HOME_MENU: {
|
||||||
let menuItems = [];
|
let menuItems = [];
|
||||||
|
|
||||||
|
|||||||
223
com.twin.app.shoptime/src/utils/dataHelpers.js
Normal file
223
com.twin.app.shoptime/src/utils/dataHelpers.js
Normal 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>
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
*/
|
||||||
@@ -146,11 +146,16 @@ export default function HomeBanner({
|
|||||||
|
|
||||||
const termsData = useSelector((state) => state.home.termsData);
|
const termsData = useSelector((state) => state.home.termsData);
|
||||||
const termsIdMap = useSelector((state) => state.home.termsIdMap);
|
const termsIdMap = useSelector((state) => state.home.termsIdMap);
|
||||||
const optionalTermsData = useSelector((state) =>
|
const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable);
|
||||||
state.home.termsData?.data?.terms.find(
|
const optionalTermsData = useSelector((state) => {
|
||||||
(term) => term.trmsTpCd === "MST00405",
|
// 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 termsLoading = useSelector((state) => state.common.termsLoading);
|
||||||
const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
|
const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
|
||||||
// 선택약관 동의여부
|
// 선택약관 동의여부
|
||||||
@@ -166,13 +171,21 @@ export default function HomeBanner({
|
|||||||
|
|
||||||
// 선택약관 팝업 표시 여부 ===================================================
|
// 선택약관 팝업 표시 여부 ===================================================
|
||||||
const shouldShowOptionalTermsPopup = useMemo(() => {
|
const shouldShowOptionalTermsPopup = useMemo(() => {
|
||||||
console.log('[HomeBanner] Step 1: termsLoading, isGnbOpened, optionalTermsAgreed 상태 확인', { termsLoading, isGnbOpened, optionalTermsAgreed });
|
console.log('[HomeBanner] Step 1: 상태 확인', {
|
||||||
if (termsLoading || isGnbOpened || optionalTermsAgreed) {
|
termsLoading,
|
||||||
console.log('[HomeBanner] Early return: 조건 불만족 (termsLoading || isGnbOpened || optionalTermsAgreed)');
|
isGnbOpened,
|
||||||
|
optionalTermsAgreed,
|
||||||
|
optionalTermsAvailable
|
||||||
|
});
|
||||||
|
|
||||||
|
// optionalTermsAvailable = false면 팝업 표시 안함
|
||||||
|
if (termsLoading || isGnbOpened || optionalTermsAgreed || !optionalTermsAvailable) {
|
||||||
|
console.log('[HomeBanner] Early return: 조건 불만족');
|
||||||
return false;
|
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);
|
console.log('[HomeBanner] Step 2: termsData 확인', terms);
|
||||||
if (!terms) {
|
if (!terms) {
|
||||||
console.log('[HomeBanner] Early return: terms가 존재하지 않음');
|
console.log('[HomeBanner] Early return: terms가 존재하지 않음');
|
||||||
@@ -188,7 +201,7 @@ export default function HomeBanner({
|
|||||||
|
|
||||||
console.log('[HomeBanner] Step 4: 최종 결과', result);
|
console.log('[HomeBanner] Step 4: 최종 결과', result);
|
||||||
return result;
|
return result;
|
||||||
}, [termsData, termsLoading, isGnbOpened, optionalTermsAgreed]);
|
}, [termsData, termsLoading, isGnbOpened, optionalTermsAgreed, optionalTermsAvailable]);
|
||||||
|
|
||||||
// 선택약관 팝업 표시 여부 ===================================================
|
// 선택약관 팝업 표시 여부 ===================================================
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import BasicIntroPanel from "./IntroPanel.jsx";
|
||||||
import Region from "@enact/sandstone/Region";
|
import Region from "@enact/sandstone/Region";
|
||||||
import Spotlight from "@enact/spotlight";
|
import Spotlight from "@enact/spotlight";
|
||||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||||
@@ -54,7 +55,23 @@ const Container = SpotlightContainerDecorator(
|
|||||||
const MAX_RETRY_ATTEMPTS = 5;
|
const MAX_RETRY_ATTEMPTS = 5;
|
||||||
const RETRY_DELAY_MS = 1500; // 1.5 seconds
|
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,
|
// children,
|
||||||
// isTabActivated,
|
// isTabActivated,
|
||||||
// handleCancel,
|
// handleCancel,
|
||||||
@@ -101,24 +118,32 @@ export default function IntroPanel({
|
|||||||
} = termsState;
|
} = termsState;
|
||||||
|
|
||||||
const introTermsData = useMemo(() => {
|
const introTermsData = useMemo(() => {
|
||||||
return (
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
termsData?.data?.terms.filter(
|
if (termsData && termsData.data && termsData.data.terms) {
|
||||||
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402",
|
return termsData.data.terms.filter(
|
||||||
) || []
|
(item) => item.trmsTpCd === "MST00401" || item.trmsTpCd === "MST00402"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}, [termsData]);
|
}, [termsData]);
|
||||||
|
|
||||||
const optionalTermsData = useMemo(() => {
|
const optionalTermsData = useMemo(() => {
|
||||||
return (
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
termsData?.data?.terms.filter(
|
if (termsData && termsData.data && termsData.data.terms) {
|
||||||
(item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406",
|
return termsData.data.terms.filter(
|
||||||
) || []
|
(item) => item.trmsTpCd === "MST00405" || item.trmsTpCd === "MST00406"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}, [termsData]);
|
}, [termsData]);
|
||||||
|
|
||||||
const webOSVersion = useSelector(
|
const webOSVersion = useSelector((state) => {
|
||||||
(state) => state.common.appStatus?.webOSVersion,
|
// Chromium68 호환성을 위해 Optional Chaining 제거
|
||||||
);
|
if (state.common && state.common.appStatus && state.common.appStatus.webOSVersion) {
|
||||||
|
return state.common.appStatus.webOSVersion;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
// const webOSVersion = 4.5;
|
// const webOSVersion = 4.5;
|
||||||
// WebOS 버전별 UI 표시 모드 결정
|
// WebOS 버전별 UI 표시 모드 결정
|
||||||
|
|||||||
Reference in New Issue
Block a user