fix: IntroPanel수정 , webOSVersion처리 수정,UI 수정

This commit is contained in:
djaco
2025-06-26 15:32:22 +09:00
parent 9efacf80c6
commit 9d8135b3e6
9 changed files with 520 additions and 109 deletions

View File

@@ -146,6 +146,7 @@ function AppBase(props) {
(state) => state.common.appStatus.cursorVisible
);
const introTermsAgree = useSelector((state) => state.common.introTermsAgree);
const deviceRegistered = useSelector((state) => state.common.deviceRegistered);
// const optionalTermsAgree = useSelector((state) => state.common.optionalTermsAgree);
const termsLoading = useSelector((state) => state.common.termsLoading);
// termsFlag 전체 상태 확인
@@ -418,8 +419,8 @@ function AppBase(props) {
// 약관 동의 여부 확인 전에는 아무것도 하지 않음
return;
}
if (introTermsAgree) {
console.log("[App.js] deviceRegistered", deviceRegistered);
if (introTermsAgree || deviceRegistered) {
initService(true);
} else {
// 필수 약관에 동의하지 않은 경우
@@ -428,7 +429,7 @@ function AppBase(props) {
);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
}, [introTermsAgree, dispatch, initService, termsLoading]);
}, [introTermsAgree, deviceRegistered, dispatch, initService, termsLoading]);
useEffect(() => {
const launchParmas = getLaunchParams();

View File

@@ -40,6 +40,7 @@ export const types = {
SET_ERROR_MESSAGE: "SET_ERROR_MESSAGE",
CLEAR_ERROR_MESSAGE: "CLEAR_ERROR_MESSAGE",
GET_DEVICE_MACADDRESS: "GET_DEVICE_MACADDRESS",
SET_DEVICE_REGISTERED: "SET_DEVICE_REGISTERED",
// billing actions
GET_MY_INFO_BILLING_SEARCH: "GET_MY_INFO_BILLING_SEARCH",

View File

@@ -566,9 +566,14 @@ export const setSecondLayerInfo = (secondLayerInfo) => ({
payload: secondLayerInfo,
});
export const setGNBMenu = (menu) => ({
export const setGNBMenu = (gnbMenu) => ({
type: types.SET_GNB_MENU,
payload: menu,
payload: gnbMenu,
});
export const setDeviceRegistered = (isRegistered) => ({
type: types.SET_DEVICE_REGISTERED,
payload: isRegistered,
});
export const clearErrorMessage = () => ({

View File

@@ -1,10 +1,18 @@
import { URLS } from "../api/apiConfig";
import { runDelayedAction, setTokenRefreshing, TAxios } from "../api/TAxios";
import {
runDelayedAction,
setTokenRefreshing,
TAxios,
TAxiosAdvancedPromise,
} from "../api/TAxios";
import * as lunaSend from "../lunaSend";
import { types } from "./actionTypes";
import { changeLocalSettings } from "./commonActions";
import { fetchCurrentUserHomeTerms } from "./homeActions";
const MAX_RETRY_COUNT = 3;
const RETRY_DELAY = 2000; // 2 seconds
// IF-LGSP-000 인증코드 요청
export const getAuthenticationCode = () => (dispatch, getState) => {
setTokenRefreshing(true);

View File

@@ -1,6 +1,9 @@
import axios from 'axios';
import Spotlight from '@enact/spotlight';
import { useDispatch } from 'react-redux';
import { useState } from 'react';
import { fetchCurrentUserHomeTermsSafe } from '../actions/homeActions';
import { types } from '../actions/actionTypes';
import {
@@ -187,10 +190,11 @@ export const TAxios = (
dispatch(getReAuthenticationCode());
}
pushQueue();
if (onFail) onFail(res);
return;
}
//RefreshToken 만료
// RefreshToken 만료
if (res?.data?.retCode === 402 || res?.data?.retCode === 501) {
if (baseUrl === URLS.GET_RE_AUTHENTICATION_CODE) {
dispatch(getAuthenticationCode());
@@ -200,6 +204,7 @@ export const TAxios = (
}
pushQueue();
}
if (onFail) onFail(res);
return;
}
// 602 요청 국가 불일치
@@ -246,3 +251,330 @@ export const TAxios = (
pushQueue();
}
};
// 안전한 에러 처리를 위한 TAxiosPromise
export const TAxiosPromise = (
dispatch,
getState,
type,
baseUrl,
urlParams = {},
params = {},
noTokenRefresh = false
) => {
return new Promise((resolve, reject) => {
TAxios(
dispatch,
getState,
type,
baseUrl,
urlParams,
params,
// onSuccess - 항상 성공 객체로 반환
(response) => {
resolve({
success: true,
data: response.data,
response: response,
error: null
});
},
// onFail - 에러도 resolve로 처리하여 throw 방지
(error) => {
console.error(`TAxiosPromise failed for ${baseUrl}:`, error);
resolve({
success: false,
data: null,
response: null,
error: error
});
},
noTokenRefresh
);
});
};
// 더 세밀한 제어를 위한 확장된 Promise 버전
export const TAxiosAdvancedPromise = (
dispatch,
getState,
type,
baseUrl,
urlParams = {},
params = {},
options = {}
) => {
const {
noTokenRefresh = false,
timeout = 30000,
retries = 0,
retryDelay = 1000,
throwOnError = false, // 개발자가 명시적으로 원할 때만 throw
} = options;
return new Promise((resolve, reject) => {
let attempts = 0;
const maxAttempts = retries + 1;
const attemptRequest = () => {
attempts++;
console.log(`TAxiosPromise attempt ${attempts}/${maxAttempts} for ${baseUrl}`);
const timeoutId = setTimeout(() => {
const timeoutError = new Error(`Request timeout after ${timeout}ms for ${baseUrl}`);
if (throwOnError) {
reject(timeoutError);
} else {
resolve({
success: false,
data: null,
response: null,
error: timeoutError
});
}
}, timeout);
TAxios(
dispatch,
getState,
type,
baseUrl,
urlParams,
params,
// onSuccess
(response) => {
clearTimeout(timeoutId);
console.log(`TAxiosPromise success on attempt ${attempts} for ${baseUrl}`);
resolve({
success: true,
data: response.data,
response: response,
error: null
});
},
// onFail
(error) => {
clearTimeout(timeoutId);
console.error(`TAxiosPromise error on attempt ${attempts} for ${baseUrl}:`, error);
// 재시도 로직
if (attempts < maxAttempts) {
console.log(`Retrying in ${retryDelay}ms... (${attempts}/${maxAttempts})`);
setTimeout(() => {
attemptRequest();
}, retryDelay);
} else {
// 최종 실패 시에도 resolve로 처리 (throwOnError가 false인 경우)
if (throwOnError) {
reject(error);
} else {
resolve({
success: false,
data: null,
response: null,
error: error
});
}
}
},
noTokenRefresh
);
};
attemptRequest();
});
};
// HTTP 메소드별 편의 함수들 (안전한 버전)
export const TAxiosGet = async (dispatch, getState, baseUrl, urlParams = {}, options = {}) => {
return await TAxiosPromise(dispatch, getState, 'get', baseUrl, urlParams, {}, options.noTokenRefresh);
};
export const TAxiosPost = async (dispatch, getState, baseUrl, params = {}, options = {}) => {
return await TAxiosPromise(dispatch, getState, 'post', baseUrl, {}, params, options.noTokenRefresh);
};
// 안전한 다중 요청 처리
export const TAxiosAll = async (requests) => {
try {
const results = await Promise.all(requests);
// 모든 결과를 안전하게 처리
const successResults = [];
const failedResults = [];
results.forEach((result, index) => {
if (result.success) {
successResults.push({ index, result: result.data });
} else {
failedResults.push({ index, error: result.error });
}
});
return {
success: failedResults.length === 0,
successResults,
failedResults,
allResults: results
};
} catch (error) {
console.error('TAxiosAll unexpected error:', error);
return {
success: false,
successResults: [],
failedResults: [{ index: -1, error }],
allResults: []
};
}
};
// 순차 요청 처리 (안전 버전)
export const TAxiosSequential = async (requests) => {
const results = [];
const errors = [];
for (let i = 0; i < requests.length; i++) {
try {
const result = await requests[i];
if (result.success) {
results.push({ index: i, data: result.data });
} else {
errors.push({ index: i, error: result.error });
console.error(`TAxiosSequential failed at request ${i}:`, result.error);
}
} catch (error) {
errors.push({ index: i, error });
console.error(`TAxiosSequential unexpected error at request ${i}:`, error);
}
}
return {
success: errors.length === 0,
results,
errors
};
};
// 안전한 Redux Thunk 헬퍼
export const createSafeApiThunk = (apiCall) => {
return (...args) => async (dispatch, getState) => {
try {
const result = await apiCall(dispatch, getState, ...args);
return result; // 이미 안전한 형태로 반환됨
} catch (error) {
console.error('API thunk unexpected error:', error);
return {
success: false,
data: null,
response: null,
error
};
}
};
};
// 실제 사용 예시들 (안전한 버전)
export const safeUsageExamples = {
// 1. 기본 안전한 사용법
basicSafeUsage: async (dispatch, getState) => {
const result = await TAxiosPromise(
dispatch,
getState,
'get',
URLS.GET_HOME_TERMS,
{ trmsTpCdList: "MST00401, MST00402", mbrNo: "12345" }
);
if (result.success) {
console.log('Success:', result.data);
return result.data;
} else {
console.error('API call failed:', result.error);
// 에러를 throw하지 않고 기본값 반환하거나 사용자에게 안내
return null;
}
},
// 2. retCode 체크를 포함한 안전한 처리
safeWithRetCodeCheck: async (dispatch, getState) => {
const result = await TAxiosGet(
dispatch,
getState,
URLS.GET_HOME_TERMS,
{ trmsTpCdList: "MST00401, MST00402", mbrNo: "12345" }
);
if (!result.success) {
console.error('Network error:', result.error);
return { success: false, message: '네트워크 오류가 발생했습니다.' };
}
if (result.data.retCode !== 0) {
console.error('API error:', result.data.retCode, result.data.retMsg);
return {
success: false,
message: result.data.retMsg || 'API 오류가 발생했습니다.'
};
}
return { success: true, data: result.data };
},
// 3. 여러 요청의 안전한 처리
safeParallelRequests: async (dispatch, getState) => {
const requests = [
TAxiosGet(dispatch, getState, URLS.GET_HOME_TERMS, { mbrNo: "12345" }),
TAxiosGet(dispatch, getState, URLS.GET_USER_INFO, { mbrNo: "12345" }),
TAxiosPost(dispatch, getState, URLS.UPDATE_SETTINGS, { setting: "value" })
];
const result = await TAxiosAll(requests);
if (result.success) {
console.log('All requests succeeded');
return result.successResults.map(item => item.result);
} else {
console.error('Some requests failed:', result.failedResults);
// 부분적 성공도 처리 가능
return {
successData: result.successResults.map(item => item.result),
errors: result.failedResults
};
}
}
};
// 컴포넌트에서의 안전한 사용법
export const ComponentUsageExample = () => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleFetchTerms = async () => {
setLoading(true);
setError(null);
const result = await dispatch(fetchCurrentUserHomeTermsSafe());
setLoading(false);
if (result.success) {
console.log("Terms fetched successfully");
// 성공 처리 (예: 성공 토스트 표시)
} else {
console.error("Failed to fetch terms:", result.message);
setError(result.message);
// 에러 처리 (예: 에러 토스트 표시)
}
};
return (
<div>
<button onClick={handleFetchTerms} disabled={loading}>
{loading ? '로딩 중...' : '약관 정보 가져오기'}
</button>
{error && <div className="error-message">{error}</div>}
</div>
);
};

View File

@@ -1045,7 +1045,7 @@
.figmaTermsContentContainer {
display: flex;
flex-direction: column;
gap: 40px;
// gap: 40px;
}
.figmaTermsCard {
@@ -1073,6 +1073,7 @@
display: flex;
align-items: center;
justify-content: center;
// margin-bottom: 40px;
.scrollerContent {
padding: 31px;
@@ -1085,6 +1086,7 @@
display: flex;
justify-content: center;
align-items: center; // 버튼 수직 정렬을 위해 추가
margin-top: 40px;
// gap: 15px; // 버튼 사이 간격
}

View File

@@ -75,6 +75,8 @@ const initialState = {
macAddress: { wifi: "", wired: "", p2p: "" },
connectionFailed: false,
deviceRegistered: false,
termsAgreementStatus: {
MST00401: false, // 개인정보처리방침 (필수)
MST00402: false, // 이용약관 (필수)
@@ -84,6 +86,11 @@ const initialState = {
export const commonReducer = (state = initialState, action) => {
switch (action.type) {
case types.SET_DEVICE_REGISTERED:
return {
...state,
deviceRegistered: action.payload,
};
case types.CHANGE_APP_STATUS: {
let isUpdated = false;

View File

@@ -15,6 +15,7 @@ import {
setExitApp,
setHidePopup,
setShowPopup,
setDeviceRegistered,
// setTermsAgreeYn,
} from "../../actions/commonActions";
import { registerDevice } from "../../actions/deviceActions";
@@ -50,6 +51,9 @@ const Container = SpotlightContainerDecorator(
"div",
);
const MAX_RETRY_ATTEMPTS = 3;
const RETRY_DELAY_MS = 1500; // 1.5 seconds
export default function IntroPanel({
// children,
// isTabActivated,
@@ -70,6 +74,7 @@ export default function IntroPanel({
);
// const eventInfos = useSelector((state) => state.event.eventData);
const regDeviceData = useSelector((state) => state.device.regDeviceData);
const deviceRegistered = useSelector((state) => state.common.deviceRegistered);
// const regDeviceInfoData = useSelector(
// (state) => state.device.regDeviceInfoData
// );
@@ -114,26 +119,28 @@ export default function IntroPanel({
const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion,
);
// const webOSVersion = 4.5;
// WebOS 버전별 UI 표시 모드 결정
// 이미지 표시: 4.0, 5.0, 23, 24
// 텍스트 표시: 4.5, 6.0, 22
const shouldShowBenefitsView = useMemo(() => {
if (!webOSVersion) return false;
const version = String(webOSVersion);
const versionNum = Number.parseFloat(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);
const shouldShowText = textVersions.includes(versionNum);
if (process.env.NODE_ENV === "development") {
console.log("🔍 WebOS 버전별 UI 모드:");
console.log(" - webOSVersion:", version);
console.log(" - webOSVersion:", versionNum);
console.log(" - shouldShowText (텍스트 모드):", shouldShowText);
console.log(" - 텍스트 버전들:", textVersions);
console.log(" - 이미지 버전들:", imageVersions);
@@ -202,6 +209,14 @@ export default function IntroPanel({
}
}, [termsError, dispatch]);
useEffect(() => {
console.log("[IntroPanel] deviceRegistered", deviceRegistered);
if (deviceRegistered) {
console.log("[IntroPanel] deviceRegistered 팝업 닫기");
dispatch(popPanel(panel_names.INTRO_PANEL));
}
}, [deviceRegistered, dispatch]);
// 약관 팝업 동의여부에 따른 이벤트 핸들러
const handleTermsAgree = useCallback(() => {
if (!currentTerms) {
@@ -287,114 +302,122 @@ export default function IntroPanel({
}, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가
// [추가] 실제 처리 로직 분리
const executeAgree = useCallback(() => {
console.log("[IntroPanel] executeAgree 실행 시작");
const executeAgree = useCallback(
(retryCount = 0) => {
console.log(`[IntroPanel] executeAgree 실행 시작 (시도: ${retryCount + 1})`);
setIsProcessing(true);
// Redux에서 가져온 termsIdMap을 사용하여 동적으로 약관 ID 매핑
const agreeTerms = [];
// 기존 타임아웃을 5초로 단축
processingTimeoutRef.current = setTimeout(() => {
console.warn(
"[IntroPanel] executeAgree 타임아웃 - isProcessing 강제 해제",
);
setIsProcessing(false);
}, 5000);
if (termsChecked && termsIdMap["MST00402"]) {
agreeTerms.push(termsIdMap["MST00402"]); // 이용약관
}
if (privacyChecked && termsIdMap["MST00401"]) {
agreeTerms.push(termsIdMap["MST00401"]); // 개인정보처리방침
}
if (optionalChecked && termsIdMap["MST00405"]) {
agreeTerms.push(termsIdMap["MST00405"]); // 선택약관
}
// 약관 동의 처리 시작 시 로딩 상태로 설정
dispatch({ type: types.GET_TERMS_AGREE_YN_START });
// Redux에서 가져온 termsIdMap을 사용하여 동적으로 약관 ID 매핑
const agreeTerms = [];
if (termsChecked && termsIdMap["MST00402"]) {
agreeTerms.push(termsIdMap["MST00402"]); // 이용약관
}
if (privacyChecked && termsIdMap["MST00401"]) {
agreeTerms.push(termsIdMap["MST00401"]); // 개인정보처리방침
}
if (optionalChecked && termsIdMap["MST00405"]) {
agreeTerms.push(termsIdMap["MST00405"]); // 선택약관
}
if (process.env.NODE_ENV === "development") {
console.log("[IntroPanel] 현재 termsIdMap:", termsIdMap);
console.log("[IntroPanel] 최종 전송될 agreeTerms:", agreeTerms);
}
console.log("[IntroPanel] agreeTerms!!", agreeTerms);
dispatch(
registerDevice(
{ agreeTerms: agreeTerms },
(newRegDeviceData) => {
if (newRegDeviceData && newRegDeviceData.retCode === 0) {
dispatch(
getWelcomeEventInfo((eventInfos) => {
if (
eventInfos &&
Object.keys(eventInfos.data).length > 0 &&
webOSVersion
) {
let displayWelcomeEventPanel = false;
if (process.env.NODE_ENV === "development") {
console.log("[IntroPanel] 현재 termsIdMap:", termsIdMap);
console.log("[IntroPanel] 최종 전송될 agreeTerms:", agreeTerms);
}
console.log("[IntroPanel] agreeTerms!!", agreeTerms);
dispatch(
registerDevice(
{ agreeTerms: agreeTerms },
(newRegDeviceData) => {
// ================== SUCCESS ==================
if (newRegDeviceData && newRegDeviceData.retCode === 0) {
dispatch(setDeviceRegistered(true));
dispatch(
getWelcomeEventInfo((eventInfos) => {
if (
eventInfos.data?.welcomeEventFlag === "Y" ||
(eventInfos.data?.welcomeBillCpnEventFlag === "Y" &&
Number(webOSVersion) >= 6)
eventInfos &&
Object.keys(eventInfos.data).length > 0 &&
webOSVersion
) {
displayWelcomeEventPanel = true;
}
let displayWelcomeEventPanel = false;
dispatch(
sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE }),
);
if (
eventInfos.data?.welcomeEventFlag === "Y" ||
(eventInfos.data?.welcomeBillCpnEventFlag === "Y" &&
Number(webOSVersion) >= 6)
) {
displayWelcomeEventPanel = true;
}
if (displayWelcomeEventPanel) {
dispatch(
pushPanel({
name: panel_names.WELCOME_EVENT_PANEL,
panelInfo: { eventInfos: eventInfos.data },
}),
sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.AGREE })
);
if (displayWelcomeEventPanel) {
dispatch(
pushPanel({
name: panel_names.WELCOME_EVENT_PANEL,
panelInfo: { eventInfos: eventInfos.data },
})
);
}
}
}
dispatch(popPanel(panel_names.INTRO_PANEL));
setIsProcessing(false);
})
);
} else {
// retCode가 0이 아닌 경우도 실패로 간주하고 재시도
if (retryCount < MAX_RETRY_ATTEMPTS - 1) {
setTimeout(() => {
executeAgree(retryCount + 1);
}, RETRY_DELAY_MS);
} else {
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: $L(
"A temporary network error occurred. Please try again in a moment."
),
button1Text: $L("OK"),
})
);
setIsProcessing(false);
clearTimeout(processingTimeoutRef.current); // 타임아웃 정리
}),
);
} else {
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: $L("Device registration failed. Please try again."),
button1Text: $L("OK"),
}),
);
setIsProcessing(false);
clearTimeout(processingTimeoutRef.current); // 타임아웃 정리
}
}
},
() => {
// ================== FAIL ==================
console.error(`[IntroPanel] registerDevice 실패 (시도: ${retryCount + 1})`);
if (retryCount < MAX_RETRY_ATTEMPTS - 1) {
setTimeout(() => {
executeAgree(retryCount + 1);
}, RETRY_DELAY_MS);
} else {
console.error("[IntroPanel] 최대 재시도 횟수 초과.");
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: $L(
"A temporary network error occurred. Please try again in a moment."
),
button1Text: $L("OK"),
})
);
setIsProcessing(false);
}
}
},
() => {
dispatch(
setShowPopup(Config.ACTIVE_POPUP.alertPopup, {
title: $L("Error"),
text: $L("Device registration failed. Please try again."),
button1Text: $L("OK"),
}),
);
setIsProcessing(false);
clearTimeout(processingTimeoutRef.current); // 타임아웃 정리
},
),
);
}, [
termsChecked,
privacyChecked,
optionalChecked,
dispatch,
webOSVersion,
termsIdMap,
]);
)
);
},
[
termsChecked,
privacyChecked,
optionalChecked,
dispatch,
webOSVersion,
termsIdMap,
]
);
// [추가] 재시도 메커니즘 시작
const startRetryMechanism = useCallback(() => {
@@ -451,6 +474,7 @@ export default function IntroPanel({
}
// 실제 처리 실행
setIsProcessing(true);
executeAgree();
}, [
termsChecked,
@@ -661,6 +685,8 @@ export default function IntroPanel({
const title = "welcome to shoptime!";
delete rest.isOnTop;
const uiMode = shouldShowBenefitsView ? "Text Mode" : "Image Mode";
// [추가] 약관 종류에 따라 팝업 제목을 반환하는 헬퍼 함수
const getTermsPopupTitle = (terms) => {
if (!terms) return "";
@@ -823,6 +849,13 @@ export default function IntroPanel({
{$L("Do Not Agree")}
</TButton>
</div>
{/* --- DEBUG INFO [START] --- */}
<div className={css.debugInfo}>
<div>WebOS: {webOSVersion}</div>
<div>Mode: {uiMode}</div>
</div>
{/* --- DEBUG INFO [END] --- */}
</Container>
</TPanel>

View File

@@ -148,6 +148,7 @@
transition: all 0.3s ease;
will-change: transform;
margin-bottom: 20px;
margin-left: 10px;
.termsText {
color: black;
@@ -416,3 +417,24 @@
line-height: 43px;
word-wrap: break-word;
}
.termsButton {
margin-left: 10px;
}
.debugInfo {
position: absolute;
bottom: 20px;
right: 20px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
font-size: 18px;
font-weight: bold;
z-index: 999;
border-radius: 8px;
opacity: 0.8;
text-align: right;
line-height: 1.4;
pointer-events: none; /* Make it non-interactive */
}