[251105] fix: UserReviewFilter 오류시 재시도
🕐 커밋 시간: 2025. 11. 05. 05:06:01 📊 변경 통계: • 총 파일: 5개 • 추가: +252줄 • 삭제: -89줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/productActions.js ~ com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.bak.js ~ com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.js ~ com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.original.js ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 대규모 기능 개발
This commit is contained in:
@@ -126,6 +126,16 @@ const extractReviewListApiData = (apiResponse) => {
|
||||
try {
|
||||
console.log('[UserReviewList] 📥 extractReviewListApiData 호출 - 원본 응답:', apiResponse);
|
||||
|
||||
// ⭐ 핵심: retCode가 0인지 먼저 확인 (HTTP 200이어도 API 에러일 수 있음)
|
||||
if (apiResponse && apiResponse.retCode !== 0) {
|
||||
console.error('[UserReviewList] ❌ API 에러 - retCode !== 0:', {
|
||||
retCode: apiResponse.retCode,
|
||||
retMsg: apiResponse.retMsg,
|
||||
fullResponse: apiResponse
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let data = null;
|
||||
|
||||
// 응답 구조 분석
|
||||
@@ -311,7 +321,9 @@ export const getVideoIndicatorFocus = (focused) => (dispatch) => {
|
||||
|
||||
// 순차 페이징으로 모든 리뷰 데이터를 수집하는 함수 (TV 앱 성능 최적화)
|
||||
// Option 2: 순차 페칭 (메모리 효율, 서버 부하 감소)
|
||||
const fetchAllReviewsWithSequentialPaging = async (dispatch, getState, requestParams) => {
|
||||
// ⭐ 재시도 로직 포함: 타임아웃/미응답 케이스 대비
|
||||
const fetchAllReviewsWithSequentialPaging = async (dispatch, getState, requestParams, retryCount = 0) => {
|
||||
const MAX_RETRIES = 2; // 최대 2회 재시도 (총 3회 시도)
|
||||
const {
|
||||
prdtId,
|
||||
patnrId,
|
||||
@@ -325,7 +337,9 @@ const fetchAllReviewsWithSequentialPaging = async (dispatch, getState, requestPa
|
||||
patnrId,
|
||||
filterTpCd,
|
||||
filterTpVal,
|
||||
pageSize
|
||||
pageSize,
|
||||
retryCount,
|
||||
isRetry: retryCount > 0
|
||||
});
|
||||
|
||||
let allReviews = [];
|
||||
@@ -354,20 +368,101 @@ const fetchAllReviewsWithSequentialPaging = async (dispatch, getState, requestPa
|
||||
}
|
||||
|
||||
// 2. API 호출 (Promise 기반으로 변경하여 에러 처리 가능하게 함)
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const onSuccess = (res) => resolve(res);
|
||||
const onFail = (err) => reject(err);
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_USER_REVIEW_LIST, params, {}, onSuccess, onFail);
|
||||
// ⭐ 타임아웃 추가: TAxios의 콜백이 호출되지 않는 경우를 대비 (모든 오류 상황 처리)
|
||||
const REQUEST_TIMEOUT = 5000; // 5초 타임아웃 (재인증, 팝업 등 오류 상황 처리 포함)
|
||||
|
||||
console.log(`[UserReviewList] 🔄 API 요청 시작 (page ${pageNo}):`, {
|
||||
prdtId,
|
||||
patnrId,
|
||||
filterTpCd,
|
||||
pageSize,
|
||||
pageNo
|
||||
});
|
||||
|
||||
console.log(`[UserReviewList] 📄 페이지 ${pageNo} 응답:`, {
|
||||
const response = await Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
let callbackCalled = false;
|
||||
|
||||
const onSuccess = (res) => {
|
||||
if (callbackCalled) {
|
||||
console.warn(`[UserReviewList] ⚠️ onSuccess 중복 호출 (page ${pageNo})`);
|
||||
return;
|
||||
}
|
||||
callbackCalled = true;
|
||||
|
||||
console.log(`[UserReviewList] ✅ API 응답 수신 (page ${pageNo}):`, {
|
||||
status: res?.status,
|
||||
statusText: res?.statusText,
|
||||
retCode: res?.data?.retCode,
|
||||
dataExists: !!res?.data,
|
||||
reviewDetailExists: !!res?.data?.data?.reviewDetail
|
||||
});
|
||||
resolve(res);
|
||||
};
|
||||
|
||||
const onFail = (err) => {
|
||||
if (callbackCalled) {
|
||||
console.warn(`[UserReviewList] ⚠️ onFail 중복 호출 (page ${pageNo})`);
|
||||
return;
|
||||
}
|
||||
callbackCalled = true;
|
||||
|
||||
console.error(`[UserReviewList] ❌ API 콜백 에러 발생 (page ${pageNo}):`, {
|
||||
errorMessage: err?.message,
|
||||
errorStatus: err?.response?.status,
|
||||
errorStatusText: err?.response?.statusText,
|
||||
errorRetCode: err?.data?.retCode,
|
||||
errorRetMsg: err?.data?.retMsg,
|
||||
errorType: typeof err
|
||||
});
|
||||
reject(err);
|
||||
};
|
||||
|
||||
// API 호출
|
||||
console.log(`[UserReviewList] 📡 TAxios 호출 (page ${pageNo})`);
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_USER_REVIEW_LIST, params, {}, onSuccess, onFail);
|
||||
}),
|
||||
// 타임아웃 Promise (onFail이 호출되지 않은 경우에 대비)
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
const timeoutError = new Error(`API request timeout without callback (page ${pageNo})`);
|
||||
console.error(`[UserReviewList] ⏱️ API 응답 타임아웃 (page ${pageNo}):`, {
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
prdtId,
|
||||
patnrId,
|
||||
pageNo,
|
||||
reason: '5초 이내 onSuccess/onFail 콜백이 호출되지 않음'
|
||||
});
|
||||
reject(timeoutError);
|
||||
}, REQUEST_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
|
||||
// ⭐ 핵심: HTTP 200이어도 response.data.retCode를 반드시 확인해야 함
|
||||
const retCode = response?.data?.retCode;
|
||||
|
||||
console.log(`[UserReviewList] 📄 페이지 ${pageNo} 응답 상태 확인:`, {
|
||||
pageNo,
|
||||
reviewListLength: response.data?.data?.reviewDetail?.reviewList?.length || 0,
|
||||
totRvwCnt: response.data?.data?.reviewDetail?.totRvwCnt,
|
||||
rvwListCnt: response.data?.data?.reviewDetail?.rvwListCnt
|
||||
httpStatus: response?.status,
|
||||
retCode: retCode,
|
||||
retMsg: response?.data?.retMsg,
|
||||
reviewListLength: response?.data?.data?.reviewDetail?.reviewList?.length || 0,
|
||||
totRvwCnt: response?.data?.data?.reviewDetail?.totRvwCnt
|
||||
});
|
||||
|
||||
// 3. 응답 데이터 추출
|
||||
// retCode가 0이 아니면 API 에러 (HTTP 200이어도 실제 데이터 없을 수 있음)
|
||||
if (retCode !== 0) {
|
||||
console.error(`[UserReviewList] ❌ API 에러 - retCode !== 0 (page ${pageNo}):`, {
|
||||
retCode,
|
||||
retMsg: response?.data?.retMsg,
|
||||
pageNo,
|
||||
prdtId,
|
||||
totalCollected: allReviews.length
|
||||
});
|
||||
throw new Error(`API Error: retCode=${retCode}, message=${response?.data?.retMsg}`);
|
||||
}
|
||||
|
||||
// 3. 응답 데이터 추출 (retCode 검증은 extractReviewListApiData 내부에서 다시 수행)
|
||||
const reviewData = extractReviewListApiData(response.data);
|
||||
|
||||
if (!reviewData || !reviewData.reviewList) {
|
||||
@@ -444,12 +539,46 @@ const fetchAllReviewsWithSequentialPaging = async (dispatch, getState, requestPa
|
||||
|
||||
return finalPayload;
|
||||
} catch (error) {
|
||||
// ⭐ 핵심: 다양한 형태의 에러를 안전하게 처리
|
||||
const errorMessage = error?.message || (error instanceof Error ? error.toString() : JSON.stringify(error));
|
||||
const httpStatus = error?.response?.status;
|
||||
const apiRetCode = error?.response?.data?.retCode;
|
||||
const apiRetMsg = error?.response?.data?.retMsg;
|
||||
|
||||
console.error('[fetchAllReviewsWithSequentialPaging] ❌ 에러 발생:', {
|
||||
error: error.message,
|
||||
errorMessage: errorMessage,
|
||||
errorType: typeof error,
|
||||
httpStatus: httpStatus,
|
||||
apiRetCode: apiRetCode,
|
||||
apiRetMsg: apiRetMsg,
|
||||
prdtId,
|
||||
patnrId,
|
||||
pageNo,
|
||||
currentCollected: allReviews.length
|
||||
currentCollected: allReviews.length,
|
||||
retryCount,
|
||||
maxRetries: MAX_RETRIES
|
||||
});
|
||||
|
||||
// ⭐ 타임아웃 에러인 경우 재시도
|
||||
const isTimeoutError = errorMessage.includes('timeout') || errorMessage.includes('without callback');
|
||||
if (isTimeoutError && retryCount < MAX_RETRIES) {
|
||||
console.log(`[fetchAllReviewsWithSequentialPaging] 🔄 타임아웃으로 인한 재시도 (${retryCount + 1}/${MAX_RETRIES}):`, {
|
||||
prdtId,
|
||||
patnrId,
|
||||
pageNo,
|
||||
retryCount,
|
||||
delayMs: 1000 * (retryCount + 1)
|
||||
});
|
||||
|
||||
// 지수 백오프: 1초, 2초 대기 후 재시도
|
||||
const delayMs = 1000 * (retryCount + 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
|
||||
// 재귀 호출로 재시도
|
||||
return fetchAllReviewsWithSequentialPaging(dispatch, getState, requestParams, retryCount + 1);
|
||||
}
|
||||
|
||||
// 타임아웃 아니거나 최대 재시도 횟수 초과
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -478,14 +607,40 @@ export const getUserReviewList = (requestParams) => async (dispatch, getState) =
|
||||
console.log('[getUserReviewList] ✅ 모든 리뷰 수집 완료:', {
|
||||
totalReviews: result.reviewList.length,
|
||||
totRvwCnt: result.reviewDetail?.totRvwCnt,
|
||||
prdtId
|
||||
prdtId,
|
||||
filterTpCd,
|
||||
filterTpVal
|
||||
});
|
||||
} catch (error) {
|
||||
// ⭐ 핵심: 다양한 형태의 에러를 안전하게 처리
|
||||
const errorMessage = error?.message || (error instanceof Error ? error.toString() : JSON.stringify(error));
|
||||
const httpStatus = error?.response?.status;
|
||||
const apiRetCode = error?.response?.data?.retCode;
|
||||
const apiRetMsg = error?.response?.data?.retMsg;
|
||||
|
||||
console.error('[getUserReviewList] ❌ 순차 페이징 중 에러 발생:', {
|
||||
error: error.message,
|
||||
errorMessage: errorMessage,
|
||||
errorType: typeof error,
|
||||
httpStatus: httpStatus,
|
||||
apiRetCode: apiRetCode,
|
||||
apiRetMsg: apiRetMsg,
|
||||
prdtId,
|
||||
patnrId
|
||||
patnrId,
|
||||
filterTpCd,
|
||||
filterTpVal,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
// Redux 상태에 에러 정보 저장 (선택사항)
|
||||
// dispatch({
|
||||
// type: types.GET_USER_REVIEW_LIST_ERROR,
|
||||
// payload: {
|
||||
// prdtId,
|
||||
// errorMessage,
|
||||
// httpStatus,
|
||||
// apiRetCode
|
||||
// }
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -496,12 +651,19 @@ const extractReviewFiltersApiData = (apiResponse) => {
|
||||
|
||||
let data = null;
|
||||
|
||||
// ⭐ 핵심: retCode가 0인지 먼저 확인 (HTTP 200이어도 API 에러일 수 있음)
|
||||
// 응답 구조: { retCode: 0, retMsg: "Success", data: { reviewFilterInfos: {...} } }
|
||||
// retCode 확인 (최상위 레벨)
|
||||
if (!apiResponse || apiResponse.retCode !== 0) {
|
||||
console.log('[ReviewFilters] ⚠️ retCode가 0이 아님 (NoData):', {
|
||||
retCode: apiResponse?.retCode,
|
||||
retMsg: apiResponse?.retMsg
|
||||
if (!apiResponse) {
|
||||
console.warn('[ReviewFilters] ⚠️ apiResponse가 null/undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
const retCode = apiResponse.retCode;
|
||||
if (retCode !== 0) {
|
||||
console.error('[ReviewFilters] ❌ API 에러 - retCode !== 0:', {
|
||||
retCode: retCode,
|
||||
retMsg: apiResponse?.retMsg,
|
||||
fullResponse: apiResponse
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -562,66 +724,67 @@ export const getReviewFilters = (requestParams) => (dispatch, getState) => {
|
||||
});
|
||||
|
||||
const onSuccess = (response) => {
|
||||
console.log('[ReviewFilters] ✅ API 성공 응답:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
retCode: response.data && response.data.retCode,
|
||||
retMsg: response.data && response.data.retMsg,
|
||||
hasData: !!(response.data && response.data.data),
|
||||
fullResponse: response.data,
|
||||
// ⭐ 핵심: HTTP 200이어도 retCode를 먼저 확인
|
||||
const retCode = response?.data?.retCode;
|
||||
const retMsg = response?.data?.retMsg;
|
||||
|
||||
console.log('[ReviewFilters] ✅ API 응답 수신 (retCode 확인):', {
|
||||
httpStatus: response?.status,
|
||||
retCode: retCode,
|
||||
retMsg: retMsg,
|
||||
hasData: !!(response?.data?.data),
|
||||
dataExists: !!response?.data
|
||||
});
|
||||
|
||||
// retCode !== 0이면 extractReviewFiltersApiData에서 처리하고 null 반환됨
|
||||
const filtersData = extractReviewFiltersApiData(response.data);
|
||||
|
||||
console.log('[ReviewFilters] 📊 추출된 필터 데이터:', {
|
||||
hasData: !!filtersData,
|
||||
patnrId: filtersData && filtersData.patnrId,
|
||||
prdtId: filtersData && filtersData.prdtId,
|
||||
filtersLength: filtersData && filtersData.filters ? filtersData.filters.length : 0,
|
||||
filters: filtersData && filtersData.filters
|
||||
if (!filtersData) {
|
||||
console.warn('[ReviewFilters] ⚠️ 필터 데이터 추출 실패:', {
|
||||
retCode: retCode,
|
||||
retMsg: retMsg,
|
||||
reason: retCode !== 0 ? 'retCode !== 0' : 'filters 데이터 없음'
|
||||
});
|
||||
return; // 실패 시 dispatch하지 않음
|
||||
}
|
||||
|
||||
console.log('[ReviewFilters] 📊 필터 데이터 추출 성공:', {
|
||||
patnrId: filtersData.patnrId,
|
||||
prdtId: filtersData.prdtId,
|
||||
filtersLength: filtersData.filters ? filtersData.filters.length : 0
|
||||
});
|
||||
|
||||
if (filtersData) {
|
||||
console.log('[ReviewFilters] 🔴 dispatch 직전 상태:', {
|
||||
actionType: types.GET_REVIEW_FILTERS,
|
||||
typeValue: 'GET_REVIEW_FILTERS',
|
||||
patnrId: patnrId,
|
||||
prdtId: prdtId
|
||||
});
|
||||
const action = {
|
||||
type: types.GET_REVIEW_FILTERS,
|
||||
payload: {
|
||||
...filtersData,
|
||||
prdtId: prdtId,
|
||||
patnrId: patnrId
|
||||
},
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.GET_REVIEW_FILTERS,
|
||||
payload: {
|
||||
...filtersData,
|
||||
prdtId: prdtId,
|
||||
patnrId: patnrId
|
||||
},
|
||||
};
|
||||
console.log('[ReviewFilters] 📦 Redux dispatch:', {
|
||||
actionType: types.GET_REVIEW_FILTERS,
|
||||
patnrId: patnrId,
|
||||
prdtId: prdtId,
|
||||
filtersLength: filtersData.filters ? filtersData.filters.length : 0
|
||||
});
|
||||
|
||||
console.log('[ReviewFilters] 🟡 dispatch할 액션:', JSON.stringify(action, null, 2));
|
||||
|
||||
dispatch(action);
|
||||
|
||||
console.log('[ReviewFilters] 📦 데이터 디스패치 완료:', {
|
||||
patnrId,
|
||||
prdtId,
|
||||
filtersLength: filtersData.filters ? filtersData.filters.length : 0
|
||||
});
|
||||
} else {
|
||||
console.warn('[ReviewFilters] ⚠️ 필터 데이터 추출 실패');
|
||||
}
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
console.error('[ReviewFilters] ❌ API 실패:', {
|
||||
message: error.message,
|
||||
status: error.response && error.response.status,
|
||||
statusText: error.response && error.response.statusText,
|
||||
responseData: error.response && error.response.data,
|
||||
errorMessage: error?.message || '알 수 없는 에러',
|
||||
errorType: typeof error,
|
||||
httpStatus: error?.response?.status,
|
||||
httpStatusText: error?.response?.statusText,
|
||||
responseRetCode: error?.response?.data?.retCode,
|
||||
responseRetMsg: error?.response?.data?.retMsg,
|
||||
responseData: error?.response?.data,
|
||||
requestParams: requestParams,
|
||||
params: params,
|
||||
url: URLS.GET_REVIEW_FILTERS,
|
||||
fullError: error,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const useVideoMove = (options = {}) => {
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
log('cleanup: 타이머 정리');
|
||||
// log('cleanup: 타이머 정리');
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
|
||||
@@ -88,7 +88,7 @@ const useVideoMove = (options = {}) => {
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
log('cleanup: 타이머 정리');
|
||||
// log('cleanup: 타이머 정리');
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
|
||||
@@ -55,7 +55,7 @@ const useVideoMove = (options = {}) => {
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
log('cleanup: 타이머 정리');
|
||||
// log('cleanup: 타이머 정리');
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function HomeBanner({
|
||||
// 🔽 컴포넌트 언마운트 시 비디오 리소스 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log('[HomeBanner] 컴포넌트 언마운트 - 비디오 리소스 정리');
|
||||
// console.log('[HomeBanner] 컴포넌트 언마운트 - 비디오 리소스 정리');
|
||||
cleanup();
|
||||
|
||||
// 전역 비디오 타이머 정리 (메모리 누수 방지)
|
||||
@@ -178,14 +178,14 @@ export default function HomeBanner({
|
||||
|
||||
// 선택약관 팝업 표시 여부 ===================================================
|
||||
const shouldShowOptionalTermsPopup = useMemo(() => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] Step 1: 상태 확인", {
|
||||
termsLoading,
|
||||
isGnbOpened,
|
||||
optionalTermsAvailable,
|
||||
optionalTermsPopupFlow,
|
||||
});
|
||||
}
|
||||
// if (process.env.NODE_ENV === "development") {
|
||||
// console.log("[HomeBanner] Step 1: 상태 확인", {
|
||||
// termsLoading,
|
||||
// isGnbOpened,
|
||||
// optionalTermsAvailable,
|
||||
// optionalTermsPopupFlow,
|
||||
// });
|
||||
// }
|
||||
|
||||
// 1. 기본 조건 확인
|
||||
if (termsLoading || isGnbOpened || !optionalTermsAvailable) {
|
||||
@@ -212,9 +212,9 @@ export default function HomeBanner({
|
||||
|
||||
// 3. 서버 데이터 확인
|
||||
const terms = termsData && termsData.data && termsData.data.terms;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] Step 2: termsData 확인", terms);
|
||||
}
|
||||
// if (process.env.NODE_ENV === "development") {
|
||||
// console.log("[HomeBanner] Step 2: termsData 확인", terms);
|
||||
// }
|
||||
if (!terms) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] Early return: terms가 존재하지 않음");
|
||||
@@ -223,17 +223,17 @@ export default function HomeBanner({
|
||||
}
|
||||
|
||||
const optionalTerm = terms.find((term) => term.trmsTpCd === "MST00405");
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] Step 3: optionalTerm 검색 결과", optionalTerm);
|
||||
}
|
||||
// if (process.env.NODE_ENV === "development") {
|
||||
// console.log("[HomeBanner] Step 3: optionalTerm 검색 결과", optionalTerm);
|
||||
// }
|
||||
|
||||
const result = optionalTerm
|
||||
? optionalTerm.trmsPopFlag === "Y" && optionalTerm.trmsAgrFlag === "N"
|
||||
: false;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] Step 4: 최종 결과", result);
|
||||
}
|
||||
// if (process.env.NODE_ENV === "development") {
|
||||
// console.log("[HomeBanner] Step 4: 최종 결과", result);
|
||||
// }
|
||||
return result;
|
||||
}, [
|
||||
termsData.data?.terms,
|
||||
@@ -246,9 +246,9 @@ export default function HomeBanner({
|
||||
// 선택약관 팝업 표시 여부 ===================================================
|
||||
|
||||
const handleOptionalAgree = useCallback(() => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[HomeBanner] handleAgree Click");
|
||||
}
|
||||
// if (process.env.NODE_ENV === "development") {
|
||||
// console.log("[HomeBanner] handleAgree Click");
|
||||
// }
|
||||
|
||||
if (!termsIdMap || Object.keys(termsIdMap).length === 0) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
|
||||
Reference in New Issue
Block a user