diff --git a/com.twin.app.shoptime/package.json b/com.twin.app.shoptime/package.json
index 9e783ca7..5d5b1a6a 100644
--- a/com.twin.app.shoptime/package.json
+++ b/com.twin.app.shoptime/package.json
@@ -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"
+ }
}
diff --git a/com.twin.app.shoptime/src/App/App.js b/com.twin.app.shoptime/src/App/App.js
index 44790b9f..92759b1d 100644
--- a/com.twin.app.shoptime/src/App/App.js
+++ b/com.twin.app.shoptime/src/App/App.js
@@ -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,
})
);
}
diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js
index a8e089fb..a1d78cb1 100644
--- a/com.twin.app.shoptime/src/actions/actionTypes.js
+++ b/com.twin.app.shoptime/src/actions/actionTypes.js
@@ -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",
diff --git a/com.twin.app.shoptime/src/actions/homeActions.js b/com.twin.app.shoptime/src/actions/homeActions.js
index bfb8c26e..89491994 100644
--- a/com.twin.app.shoptime/src/actions/homeActions.js
+++ b/com.twin.app.shoptime/src/actions/homeActions.js
@@ -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) || "서버 오류가 발생했습니다."
};
}
};
diff --git a/com.twin.app.shoptime/src/reducers/homeReducer.js b/com.twin.app.shoptime/src/reducers/homeReducer.js
index ae55c058..c4b94b28 100644
--- a/com.twin.app.shoptime/src/reducers/homeReducer.js
+++ b/com.twin.app.shoptime/src/reducers/homeReducer.js
@@ -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 = [];
diff --git a/com.twin.app.shoptime/src/utils/dataHelpers.js b/com.twin.app.shoptime/src/utils/dataHelpers.js
new file mode 100644
index 00000000..4744ee60
--- /dev/null
+++ b/com.twin.app.shoptime/src/utils/dataHelpers.js
@@ -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 =>
{productPrice}원
+ *