[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:
2025-11-05 05:06:01 +09:00
parent 25a00397e2
commit 5568da6e5e
5 changed files with 253 additions and 90 deletions

View File

@@ -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,
});
};

View File

@@ -55,7 +55,7 @@ const useVideoMove = (options = {}) => {
};
const cleanup = () => {
log('cleanup: 타이머 정리');
// log('cleanup: 타이머 정리');
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;

View File

@@ -88,7 +88,7 @@ const useVideoMove = (options = {}) => {
};
const cleanup = () => {
log('cleanup: 타이머 정리');
// log('cleanup: 타이머 정리');
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;

View File

@@ -55,7 +55,7 @@ const useVideoMove = (options = {}) => {
};
const cleanup = () => {
log('cleanup: 타이머 정리');
// log('cleanup: 타이머 정리');
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;

View File

@@ -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") {