[251029] fix: UseReviews V2적용

🕐 커밋 시간: 2025. 10. 29. 17:02:12

📊 변경 통계:
  • 총 파일: 4개
  • 추가: +120줄
  • 삭제: -263줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/productActions.js
  ~ com.twin.app.shoptime/src/actions/searchActions.js
  ~ com.twin.app.shoptime/src/hooks/useReviews/useReviews.js
  ~ com.twin.app.shoptime/src/reducers/productReducer.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 중간 규모 기능 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-10-29 17:02:13 +09:00
parent 63bedb697e
commit f9f85dc043
4 changed files with 120 additions and 263 deletions

View File

@@ -86,69 +86,40 @@ export const getProductOption = createGetThunk({
tag: 'getProductOption',
});
// FP: 실제 API 응답에서 data 부분만 추출 (간소화)
const extractReviewApiData = (apiResponse) => {
try {
// console.log("[UserReviews] 🔍 extractReviewApiData - 전체 API 응답 분석:", {
// fullApiResponse: apiResponse,
// hasData: !!(apiResponse && apiResponse.data),
// retCode: apiResponse && apiResponse.retCode,
// retMsg: apiResponse && apiResponse.retMsg,
// responseKeys: apiResponse ? Object.keys(apiResponse) : [],
// dataKeys: apiResponse && apiResponse.data ? Object.keys(apiResponse.data) : []
// });
// 여러 가능한 데이터 경로 시도
let apiData = null;
// 1. response.data.data (중첩 구조)
if (apiResponse && apiResponse.data && apiResponse.data.data) {
apiData = apiResponse.data.data;
// console.log("[UserReviews] ✅ 데이터 경로 1: response.data.data 사용");
}
// 2. response.data (단일 구조)
else if (
apiResponse &&
apiResponse.data &&
(apiResponse.data.reviewList || apiResponse.data.reviewDetail)
) {
apiData = apiResponse.data;
// console.log("[UserReviews] ✅ 데이터 경로 2: response.data 사용");
}
// 3. response 직접 (최상위)
else if (apiResponse && (apiResponse.reviewList || apiResponse.reviewDetail)) {
apiData = apiResponse;
// console.log("[UserReviews] ✅ 데이터 경로 3: response 직접 사용");
}
if (!apiData) {
// console.warn("[UserReviews] ❌ 모든 데이터 경로에서 추출 실패:", {
// hasResponseData: !!(apiResponse && apiResponse.data),
// hasReviewList: !!(apiResponse && apiResponse.data && apiResponse.data.reviewList),
// hasReviewDetail: !!(apiResponse && apiResponse.data && apiResponse.data.reviewDetail),
// hasNestedData: !!(apiResponse && apiResponse.data && apiResponse.data.data),
// fullResponse: apiResponse
// });
return null;
}
// 추출된 데이터 검증
/* console.log("[UserReviews] 📊 추출된 데이터 검증:", {
hasReviewList: !!apiData.reviewList,
hasReviewDetail: !!apiData.reviewDetail,
reviewListLength: apiData.reviewList ? apiData.reviewList.length : 0,
reviewDetailKeys: apiData.reviewDetail ? Object.keys(apiData.reviewDetail) : [],
totRvwCnt: apiData.reviewDetail && apiData.reviewDetail.totRvwCnt,
totRvwAvg: apiData.reviewDetail && apiData.reviewDetail.totRvwAvg,
extractedData: apiData
}); */
return apiData;
} catch (error) {
console.error('[UserReviews] ❌ extractReviewApiData 에러:', error);
return null;
}
};
// ==================== [V1 API] 코멘트 처리: 기존 API 응답 추출 함수 (더 이상 사용하지 않음) ====================
// const extractReviewApiData = (apiResponse) => {
// try {
// // 여러 가능한 데이터 경로 시도
// let apiData = null;
//
// // 1. response.data.data (중첩 구조)
// if (apiResponse && apiResponse.data && apiResponse.data.data) {
// apiData = apiResponse.data.data;
// }
// // 2. response.data (단일 구조)
// else if (
// apiResponse &&
// apiResponse.data &&
// (apiResponse.data.reviewList || apiResponse.data.reviewDetail)
// ) {
// apiData = apiResponse.data;
// }
// // 3. response 직접 (최상위)
// else if (apiResponse && (apiResponse.reviewList || apiResponse.reviewDetail)) {
// apiData = apiResponse;
// }
//
// if (!apiData) {
// return null;
// }
//
// return apiData;
// } catch (error) {
// console.error('[UserReviews] ❌ extractReviewApiData 에러:', error);
// return null;
// }
// };
// ==================== [V1 API] 코멘트 처리 끝 ====================
// IF-LGSP-101용 API 응답에서 reviewList + reviewDetail 추출
const extractReviewListApiData = (apiResponse) => {
@@ -288,158 +259,32 @@ export const resetShowAllReviews = () => ({
type: types.RESET_SHOW_ALL_REVIEWS,
});
// ==================== [V1 API] 코멘트 처리: 기존 API 호출 함수 (더 이상 사용하지 않음 - getUserReviewList 사용) ====================
// 상품별 유저 리뷰 리스트 조회 : IF-LGSP-0002
export const getUserReviews = (requestParams) => (dispatch, getState) => {
const { prdtId, patnrId } = requestParams;
// console.log("[UserReviews] 🚀 getUserReviews 액션 시작:", {
// requestParams,
// prdtId,
// patnrId,
// timestamp: new Date().toISOString()
// });
// ==================== [기존 테스트 코드] 시작 - 랜덤 prdtId 사용 (주석 처리) ====================
// 테스트용 prdtId 목록 - 실제 리뷰 데이터가 있는 상품들
/* const testProductIds = [
'LCE3010SB',
'100QNED85AU',
'14Z90Q-K.ARW3U1',
'16Z90Q-K.AAC7U1',
'24GN600-B',
'50UT8000AUA',
'A949KTMS',
'AGF76631064',
'C5323B0',
'DLE3600V',
];
const randomIndex = Math.floor(Math.random() * testProductIds.length);
const randomPrdtId = testProductIds[randomIndex];
// console.log("[UserReviews]-API 🎲 랜덤 prdtId 선택:", {
// originalPrdtId: prdtId,
// originalPatnrId: patnrId,
// randomPrdtId: randomPrdtId,
// fixedPatnrId: '9',
// randomIndex: randomIndex,
// testProductIds: testProductIds
// });
*/
// ==================== [기존 테스트 코드] 끝 ====================
// TAxios 파라미터 준비 - 실제 prdtId와 patnrId 사용
const params = { prdtId, patnrId };
const body = {}; // GET이므로 빈 객체
// plat_cd 값 확인을 위한 httpHeader 로깅
const currentState = getState();
const httpHeader = currentState.common.httpHeader || {};
// console.log("[UserReviews] <20> plat_cd 값 확인:", {
// platCd: httpHeader.plat_cd,
// fullHttpHeader: httpHeader,
// hasHttpHeader: !!httpHeader,
// httpHeaderKeys: Object.keys(httpHeader)
// });
// console.log("[UserReviews]-API <20>📡 TAxios 호출 준비:", {
// method: "get",
// url: URLS.GET_USER_REVEIW,
// params,
// body,
// prdtId: prdtId,
// patnrId: patnrId,
// platCd: httpHeader.plat_cd
// });
const onSuccess = (response) => {
// console.log('[UserReviews]-API ✅ API 성공 응답:', {
// status: response.status,
// statusText: response.statusText,
// headers: response.headers,
// retCode: response.data && response.data.retCode,
// retMsg: response.data && response.data.retMsg,
// hasData: !!(response.data && response.data.data),
// fullResponse: response.data,
// });
// ==================== [임시 테스트] 빈 리뷰 응답 시뮬레이션 - 코멘트 처리 ====================
// 30% 확률로 리뷰 없는 상품 응답 반환 (테스트용)
/* if (Math.random() < 0.3) {
console.log("[UserReviews] 🚫 임시 테스트: 빈 리뷰 응답 반환");
const emptyReviewData = {
reviewList: [],
reviewDetail: {
totRvwCnt: 0,
totRvwAvg: 0,
avgRvwScr: 0
}
};
dispatch({
type: types.GET_USER_REVIEW,
payload: { ...emptyReviewData, prdtId: prdtId }
});
console.log("[UserReviews] 📦 빈 리뷰 데이터 디스패치 완료:", emptyReviewData);
return;
} */
// ==================== [임시 테스트] 끝 - 코멘트 처리 ====================
// if (response.data && response.data.data) {
// console.log('[UserReviews] 📊 API 데이터 상세:', {
// reviewListLength: response.data.data.reviewList ? response.data.data.reviewList.length : 0,
// reviewDetail: response.data.data.reviewDetail,
// reviewList_sample:
// (response.data.data.reviewList && response.data.data.reviewList[0]) || 'empty',
// totRvwCnt: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwCnt,
// totRvwAvg: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwAvg,
// });
// }
// 실제 API 응답에서 data 부분 추출
const apiData = extractReviewApiData(response.data);
if (apiData) {
console.log('[UserReviews] ✅ 실제 API 데이터 사용');
dispatch({
type: types.GET_USER_REVIEW,
payload: { ...apiData, prdtId: prdtId }, // 원래 prdtId로 저장 (UI에서 사용)
});
console.log('[UserReviews] 📦 실제 API 데이터 디스패치 완료:', apiData);
} else {
console.log('[UserReviews] ⚠️ API 데이터 추출 실패');
// Mock 데이터 사용 비활성화
// const mockData = createMockReviewData();
// dispatch({
// type: types.GET_USER_REVIEW,
// payload: { ...mockData, prdtId: prdtId }
// });
// console.log("[UserReviews] 📦 Mock 데이터 디스패치 완료:", mockData);
}
};
const onFail = (error) => {
console.error('[UserReviews] ❌ API 실패:', {
message: error.message,
status: error.response && error.response.status,
statusText: error.response && error.response.statusText,
responseData: error.response && error.response.data,
requestParams: requestParams,
url: URLS.GET_USER_REVEIW,
fullError: error,
});
console.log('[UserReviews] 🔄 API 실패 - Mock 데이터 사용 비활성화');
// Mock 데이터 사용 비활성화
// const mockData = createMockReviewData();
// dispatch({
// type: types.GET_USER_REVIEW,
// payload: { ...mockData, prdtId: prdtId }
// });
// console.log("[UserReviews] 📦 Mock 데이터 디스패치 완료 (API 실패):", mockData);
};
// console.log("[UserReviews]-API 🔗 TAxios 호출 실행 중...");
TAxios(dispatch, getState, 'get', URLS.GET_USER_REVEIW, params, body, onSuccess, onFail);
};
// export const getUserReviews = (requestParams) => (dispatch, getState) => {
// const { prdtId, patnrId } = requestParams;
// const params = { prdtId, patnrId };
// const body = {};
// const currentState = getState();
// const httpHeader = currentState.common.httpHeader || {};
//
// const onSuccess = (response) => {
// const apiData = extractReviewApiData(response.data);
// if (apiData) {
// dispatch({
// type: types.GET_USER_REVIEW,
// payload: { ...apiData, prdtId: prdtId },
// });
// }
// };
//
// const onFail = (error) => {
// // API 실패 처리
// };
//
// TAxios(dispatch, getState, 'get', URLS.GET_USER_REVEIW, params, body, onSuccess, onFail);
// };
// ==================== [V1 API] 코멘트 처리 끝 ====================
export const getProductOptionId = (id) => (dispatch) => {
dispatch({ type: types.GET_PRODUCT_OPTION_ID, payload: id });

View File

@@ -178,16 +178,23 @@ export const getShopperHouseSearch =
return;
}
// 📥 [VoiceInput] API 응답 성공 로그
// 📥 [ShopperHouseAPI] API 응답 성공 로그
const resultData = response.data.data.result;
const results = resultData.results || [];
const receivedSearchId = results.length > 0 ? results[0].searchId : null;
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
console.log('[VoiceInput] 📥 API 응답 성공');
console.log('[VoiceInput] ├─ searchId:', receivedSearchId || '(없음)');
console.log('[VoiceInput] ├─ 결과 수:', results.length);
console.log('[VoiceInput] └─ relativeQueries:', relativeQueries || '(없음)');
// 상품 개수 계산: results[0].docs 배열 길이
const productCount = results.length > 0 && results[0].docs ? results[0].docs.length : 0;
const elapsedTime = ((new Date().getTime() - currentSearchKey) / 1000).toFixed(2);
console.log('*[ShopperHouseAPI] ✅ onSuccess - API 응답 성공');
console.log('*[ShopperHouseAPI] ├─ searchId:', receivedSearchId === null ? '(NULL)' : receivedSearchId);
console.log('*[ShopperHouseAPI] ├─ 상품 개수:', productCount);
console.log('*[ShopperHouseAPI] ├─ relativeQueries:', relativeQueries || '(없음)');
console.log('*[ShopperHouseAPI] ├─ 소요 시간:', elapsedTime + '초');
console.log('*[ShopperHouseAPI] └─ timestamp:', new Date().toISOString());
dispatch({
type: types.GET_SHOPPERHOUSE_SEARCH,
@@ -213,15 +220,15 @@ export const getShopperHouseSearch =
// ✅ TAxios 재인증 오류 필터링 (기존 방식 그대로 활용)
if (retCode === 401) {
console.log('[VoiceInput] ⚠️ Access Token 만료 - TAxios가 자동으로 재요청합니다');
console.log('[VoiceInput] └─ TAxios가 자동으로 재인증하고 재시도합니다');
console.log('*[ShopperHouseAPI] ⚠️ onFail - Access Token 만료 (401)');
console.log('*[ShopperHouseAPI] └─ TAxios가 자동으로 재인증하고 재시도합니다');
// 401 에러는 Redux에 저장하지 않음 (TAxios 자동 재시도 대기)
return;
}
if (retCode === 402 || retCode === 501) {
console.log('[VoiceInput] ⚠️ RefreshToken 만료 - TAxios가 자동으로 재발급합니다');
console.log('[VoiceInput] └─ TAxios가 자동으로 토큰 재발급하고 재시도합니다');
console.log('*[ShopperHouseAPI] ⚠️ onFail - RefreshToken 만료 (' + retCode + ')');
console.log('*[ShopperHouseAPI] └─ TAxios가 자동으로 토큰 재발급하고 재시도합니다');
// 402/501 에러는 Redux에 저장하지 않음 (TAxios 자동 재시도 대기)
return;
}
@@ -232,17 +239,19 @@ export const getShopperHouseSearch =
errorMessage?.includes('Network Error') ||
errorMessage?.includes('timeout')
) {
console.log('[VoiceInput] ⚠️ 일시적인 네트워크 오류 - 재시도 필요');
console.log('[VoiceInput] TAxios 큐에 의해 자동 재시도될 수 있습니다');
console.log('*[ShopperHouseAPI] ⚠️ onFail - 일시적인 네트워크 오류');
console.log('*[ShopperHouseAPI] status:', status);
console.log('*[ShopperHouseAPI] └─ errorMessage:', errorMessage);
// 일시적인 네트워크 오류는 Redux에 저장하지 않음
return;
}
// ✨ 그 외의 실제 API 오류들만 Redux에 저장
console.log('[VoiceInput] 📥 실제 API 오류 발생 - Redux에 저장');
console.log('[VoiceInput] ├─ retCode:', retCode);
console.log('[VoiceInput] ├─ status:', status);
console.log('[VoiceInput] ─ error:', errorMessage || JSON.stringify(error));
console.log('*[ShopperHouseAPI] ❌ onFail - 실제 API 오류 발생');
console.log('*[ShopperHouseAPI] ├─ retCode:', retCode);
console.log('*[ShopperHouseAPI] ├─ status:', status);
console.log('*[ShopperHouseAPI] ─ errorMessage:', errorMessage);
console.log('*[ShopperHouseAPI] └─ retMsg:', error?.data?.retMsg || '(없음)');
dispatch(
setShopperHouseError({
@@ -267,7 +276,10 @@ export const getShopperHouseSearch =
if (searchId) {
params.searchid = searchId;
}
console.log('[ShopperHouse] getShopperHouseSearch params: ', JSON.stringify(params));
console.log('*[ShopperHouseAPI] getShopperHouseSearch params: ', JSON.stringify(params));
console.log('*[ShopperHouseAPI] ├─ query:', query);
console.log('*[ShopperHouseAPI] ├─ searchId:', searchId === null ? '(NULL)' : searchId);
console.log('*[ShopperHouseAPI] └─ timestamp:', new Date().toISOString());
TAxios(dispatch, getState, 'post', URLS.GET_SHOPPERHOUSE_SEARCH, {}, params, onSuccess, onFail);
};

View File

@@ -100,18 +100,18 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
const isCurrentProductLoaded = prdtId === loadedPrdtId;
// Redux 상태 선택 로그
useEffect(() => {
console.log('[useReviews] 🔍 Redux 상태 선택:', {
reviewVersion,
prdtId,
patnrId,
loadedPrdtId,
isCurrentProductLoaded,
selectedReduxState: reviewVersion === 1 ? 'reviewData' : 'reviewListData',
hasReviewData: !!reviewData,
reviewDataLength: reviewData?.reviewList?.length || 0
});
}, [reviewVersion, prdtId, patnrId, loadedPrdtId, isCurrentProductLoaded, reviewData]);
// useEffect(() => {
// console.log('[useReviews] 🔍 Redux 상태 선택:', {
// reviewVersion,
// prdtId,
// patnrId,
// loadedPrdtId,
// isCurrentProductLoaded,
// selectedReduxState: reviewVersion === 1 ? 'reviewData' : 'reviewListData',
// hasReviewData: !!reviewData,
// reviewDataLength: reviewData?.reviewList?.length || 0
// });
// }, [reviewVersion, prdtId, patnrId, loadedPrdtId, isCurrentProductLoaded, reviewData]);
// UserReviewPanel 전용 페이징 상태 (다른 컴포넌트에 영향 없음)
const [userReviewPanelPage, setUserReviewPanelPage] = useState(0);
@@ -130,12 +130,12 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
setIsLoading(true);
console.log('[useReviews] 📋 loadReviews 호출됨:', {
prdtId,
patnrId,
reviewVersion,
willCallApi: reviewVersion === 1 ? 'getUserReviews (v1)' : 'getUserReviewList (v2)'
});
// console.log('[useReviews] 📋 loadReviews 호출됨:', {
// prdtId,
// patnrId,
// reviewVersion,
// willCallApi: reviewVersion === 1 ? 'getUserReviews (v1)' : 'getUserReviewList (v2)'
// });
try {
if (reviewVersion === 1) {
@@ -144,7 +144,7 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
await dispatch(getUserReviews({ prdtId, patnrId }));
} else {
// 신 API 호출 (v2)
console.log('[useReviews] 🔄 getUserReviewList 호출 중... (v2)');
// console.log('[useReviews] 🔄 getUserReviewList 호출 중... (v2)');
await dispatch(getUserReviewList({
prdtId,
patnrId,
@@ -167,14 +167,14 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
if (prdtId && patnrId) {
const needsLoad = prdtId !== loadedPrdtId;
console.log('[useReviews] 🔄 로드 필요 여부 체크:', {
prdtId,
patnrId,
loadedPrdtId,
reviewVersion,
needsLoad,
reason: needsLoad ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
});
// console.log('[useReviews] 🔄 로드 필요 여부 체크:', {
// prdtId,
// patnrId,
// loadedPrdtId,
// reviewVersion,
// needsLoad,
// reason: needsLoad ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
// });
if (needsLoad) {
// prdtId 변경 시 로딩 상태 즉시 설정으로 UI 깜빡임 방지

View File

@@ -43,12 +43,12 @@ const handleProductOptionId = curry((state, action) =>
);
// 유저 리뷰 데이터 핸들러 (v1 - 기존 API)
const handleUserReview = curry((state, action) => {
const reviewData = get('payload', action);
const prdtId = get(['payload', 'prdtId'], action);
// const handleUserReview = curry((state, action) => {
// const reviewData = get('payload', action);
// const prdtId = get(['payload', 'prdtId'], action);
return set('reviewData', reviewData, set('loadedPrdtId', prdtId, state));
});
// return set('reviewData', reviewData, set('loadedPrdtId', prdtId, state));
// });
// 유저 리뷰 리스트 데이터 핸들러 (v2 - 신 API)
const handleUserReviewList = curry((state, action) => {
@@ -86,7 +86,7 @@ const handlers = {
[types.GET_VIDEO_INDECATOR_FOCUS]: handleVideoIndicatorFocus,
[types.CLEAR_PRODUCT_DETAIL]: handleClearProductDetail,
[types.GET_PRODUCT_OPTION_ID]: handleProductOptionId,
[types.GET_USER_REVIEW]: handleUserReview,
// [types.GET_USER_REVIEW]: handleUserReview,
[types.GET_USER_REVIEW_LIST]: handleUserReviewList,
[types.TOGGLE_SHOW_ALL_REVIEWS]: handleToggleShowAllReviews,
[types.RESET_SHOW_ALL_REVIEWS]: handleResetShowAllReviews,