[251029] fix: searchId null , UserReviewsV2

🕐 커밋 시간: 2025. 10. 29. 15:24:15

📊 변경 통계:
  • 총 파일: 9개
  • 추가: +409줄
  • 삭제: -97줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ 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
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/ShowUserReviews.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 대규모 기능 개발
  • 모듈 구조 개선
This commit is contained in:
2025-10-29 15:24:17 +09:00
parent 720314b02c
commit 9637b7413d
9 changed files with 410 additions and 98 deletions

View File

@@ -168,6 +168,7 @@ export const types = {
GET_PRODUCT_OPTION_ID: 'GET_PRODUCT_OPTION_ID',
CLEAR_PRODUCT_OPTIONS: 'CLEAR_PRODUCT_OPTIONS',
GET_USER_REVIEW: 'GET_USER_REVIEW',
GET_USER_REVIEW_LIST: 'GET_USER_REVIEW_LIST',
TOGGLE_SHOW_ALL_REVIEWS: 'TOGGLE_SHOW_ALL_REVIEWS',
RESET_SHOW_ALL_REVIEWS: 'RESET_SHOW_ALL_REVIEWS',

View File

@@ -150,22 +150,74 @@ const extractReviewApiData = (apiResponse) => {
}
};
// IF-LGSP-101용 API 응답에서 reviewDetail 추출
// IF-LGSP-101용 API 응답에서 reviewList + reviewDetail 추출
const extractReviewListApiData = (apiResponse) => {
try {
let reviewDetail = null;
console.log('[UserReviewList] 📥 extractReviewListApiData 호출 - 원본 응답:', apiResponse);
if (apiResponse && apiResponse.data && apiResponse.data.reviewDetail) {
reviewDetail = apiResponse.data.reviewDetail;
} else if (apiResponse && apiResponse.reviewDetail) {
reviewDetail = apiResponse.reviewDetail;
let data = null;
// 응답 구조 분석
if (apiResponse && apiResponse.data) {
// data 경로에서 추출
const apiData = apiResponse.data;
// 중요: v2 API는 reviewList가 reviewDetail 안에 있을 수 있음
let reviewList = apiData.reviewList || [];
const reviewDetail = apiData.reviewDetail || {};
// reviewDetail.reviewList에 실제 데이터가 있으면 사용
if (reviewDetail.reviewList && Array.isArray(reviewDetail.reviewList) && reviewList.length === 0) {
reviewList = reviewDetail.reviewList;
console.log('[UserReviewList] 🔄 reviewDetail.reviewList에서 데이터 추출됨');
}
data = {
reviewList: reviewList,
reviewDetail: reviewDetail
};
console.log('[UserReviewList] 📊 apiResponse.data 경로에서 추출:', {
reviewListLength: data.reviewList.length,
reviewDetailKeys: Object.keys(data.reviewDetail),
reviewDetail: data.reviewDetail,
reviewListSample: data.reviewList.length > 0 ? data.reviewList[0] : 'empty'
});
} else if (apiResponse) {
// 직접 경로에서 추출
let reviewList = apiResponse.reviewList || [];
const reviewDetail = apiResponse.reviewDetail || {};
// reviewDetail.reviewList에 실제 데이터가 있으면 사용
if (reviewDetail.reviewList && Array.isArray(reviewDetail.reviewList) && reviewList.length === 0) {
reviewList = reviewDetail.reviewList;
console.log('[UserReviewList] 🔄 reviewDetail.reviewList에서 데이터 추출됨');
}
data = {
reviewList: reviewList,
reviewDetail: reviewDetail
};
console.log('[UserReviewList] 📊 직접 경로에서 추출:', {
reviewListLength: data.reviewList.length,
reviewDetailKeys: Object.keys(data.reviewDetail),
reviewDetail: data.reviewDetail,
reviewListSample: data.reviewList.length > 0 ? data.reviewList[0] : 'empty'
});
}
if (!reviewDetail) {
if (!data || (!data.reviewList && !data.reviewDetail)) {
console.warn('[UserReviewList] ⚠️ reviewList와 reviewDetail 모두 없음:', apiResponse);
return null;
}
return reviewDetail;
console.log('[UserReviewList] ✅ 추출 완료:', {
reviewListLength: data.reviewList.length,
reviewDetail: data.reviewDetail
});
return data;
} catch (error) {
console.error('[UserReviewList] ❌ extractReviewListApiData 에러:', error);
return null;
@@ -455,31 +507,43 @@ export const getUserReviewList = (requestParams) => (dispatch, getState) => {
fullResponse: response.data,
});
const reviewDetail = extractReviewListApiData(response.data);
const reviewData = extractReviewListApiData(response.data);
console.log('[UserReviewList] 📊 추출된 reviewDetail:', {
hasReviewDetail: !!reviewDetail,
reviewDetail: reviewDetail,
totRvwCnt: reviewDetail && reviewDetail.totRvwCnt,
totRvwAvg: reviewDetail && reviewDetail.totRvwAvg,
rvwListCnt: reviewDetail && reviewDetail.rvwListCnt,
reviewListLength: reviewDetail && reviewDetail.reviewList ? reviewDetail.reviewList.length : 0,
console.log('[UserReviewList] 📊 추출된 리뷰 데이터:', {
hasData: !!reviewData,
reviewListLength: reviewData && reviewData.reviewList ? reviewData.reviewList.length : 0,
reviewDetail: reviewData && reviewData.reviewDetail,
totRvwCnt: reviewData && reviewData.reviewDetail && reviewData.reviewDetail.totRvwCnt,
totRvwAvg: reviewData && reviewData.reviewDetail && reviewData.reviewDetail.totRvwAvg,
});
if (reviewDetail) {
dispatch({
type: types.GET_USER_REVIEW_LIST,
payload: {
reviewDetail,
prdtId: prdtId
},
if (reviewData) {
console.log('[UserReviewList] 🔴 dispatch 직전 상태:', {
actionType: types.GET_USER_REVIEW_LIST,
typeValue: 'GET_USER_REVIEW_LIST',
reviewListLength: reviewData.reviewList.length,
prdtId: prdtId
});
const action = {
type: types.GET_USER_REVIEW_LIST,
payload: {
...reviewData, // reviewList + reviewDetail 모두 포함
prdtId: prdtId
},
};
console.log('[UserReviewList] 🟡 dispatch할 액션:', JSON.stringify(action, null, 2));
dispatch(action);
console.log('[UserReviewList] 📦 데이터 디스패치 완료:', {
reviewDetail,
reviewListLength: reviewData.reviewList.length,
reviewDetail: reviewData.reviewDetail,
prdtId
});
} else {
console.warn('[UserReviewList] ⚠️ reviewDetail 추출 실패');
console.warn('[UserReviewList] ⚠️ 리뷰 데이터 추출 실패');
}
};

View File

@@ -101,7 +101,7 @@ export const getShopperHouseSearch =
(dispatch, getState) => {
// 이전 데이터 초기화 -> shopperHouseData만 초기화
dispatch({ type: types.CLEAR_SHOPPERHOUSE_DATA });
// dispatch({ type: types.CLEAR_SHOPPERHOUSE_DATA });
// 새로운 검색 시작 - 고유 키 생성
const currentSearchKey = new Date().getTime();

View File

@@ -1,17 +1,71 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getUserReviews } from '../../actions/productActions';
import { getUserReviews, getUserReviewList } from '../../actions/productActions';
import fp from '../../utils/fp';
const DISPLAY_SIZE = 3; // 화면에 표시할 리뷰 개수
const STEP_SIZE = 1; // 페이징 시 이동할 리뷰 개수
const useReviews = (prdtId, patnrId) => {
// 🔧 REVIEW_VERSION: 1 = 기존 API (getUserReviews), 2 = 신 API (getUserReviewList)
// 이 값을 변경하면 전체 앱에서 API 버전이 변경됩니다
export const REVIEW_VERSION = 2; // ← 여기서 1 또는 2로 변경
console.log('[useReviews] 🔑 REVIEW_VERSION 설정:', REVIEW_VERSION);
// reviewVersion 파라미터는 더 이상 사용하지 않음 (호환성 유지를 위해 파라미터는 남겨둠)
const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
const reviewVersion = REVIEW_VERSION; // 항상 REVIEW_VERSION 상수 사용
console.log('[useReviews] 🟢 useReviews Hook 호출됨 (REVIEW_VERSION 사용):', {
prdtId,
patnrId,
reviewVersion,
usingGlobalVersion: true
});
const dispatch = useDispatch();
// Redux 상태에서 리뷰 데이터 가져오기 - CustomerImages와 동일한 방식
const reviewData = useSelector((state) => state.product.reviewData);
const loadedPrdtId = useSelector((state) => state.product.loadedPrdtId);
// Redux 상태에서 리뷰 데이터 가져오기 - reviewVersion에 따라 선택
const reviewData = useSelector((state) => {
// v2 디버깅: 전체 product state 확인
if (reviewVersion === 2) {
console.log('[useReviews_useReviewList] 📥 전체 Redux state.product 상태:', {
keys: Object.keys(state.product),
reviewData: state.product.reviewData,
reviewListData: state.product.reviewListData,
loadedPrdtId: state.product.loadedPrdtId,
loadedListPrdtId: state.product.loadedListPrdtId
});
}
const data = reviewVersion === 1
? state.product.reviewData
: state.product.reviewListData;
console.log('[useReviews_useReviewList] 📥 Redux reviewData 선택:', {
reviewVersion,
selectedState: reviewVersion === 1 ? 'reviewData' : 'reviewListData',
dataExists: !!data,
dataKeys: data ? Object.keys(data) : 'null',
reviewListLength: data?.reviewList?.length || 0
});
return data;
});
const loadedPrdtId = useSelector((state) => {
const id = reviewVersion === 1
? state.product.loadedPrdtId
: state.product.loadedListPrdtId;
console.log('[useReviews_useReviewList] 🔑 Redux loadedPrdtId 선택:', {
reviewVersion,
selectedState: reviewVersion === 1 ? 'loadedPrdtId' : 'loadedListPrdtId',
loadedPrdtId: id
});
return id;
});
// 빈 내용 리뷰 필터링 - 의미있는 리뷰만 표시
const allReviews = (reviewData?.reviewList || []).filter((review) => {
@@ -42,12 +96,27 @@ const useReviews = (prdtId, patnrId) => {
const [hasLoadedData, setHasLoadedData] = useState(false);
// 현재 제품이 이미 로드된 적이 있는지 확인 (Redux 기반)
// reviewVersion에 따라 다른 loadedPrdtId 비교
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]);
// UserReviewPanel 전용 페이징 상태 (다른 컴포넌트에 영향 없음)
const [userReviewPanelPage, setUserReviewPanelPage] = useState(0);
// 리뷰 데이터 로드 함수 - useReviews가 모든 API 호출을 담당
// 리뷰 데이터 로드 함수 - reviewVersion에 따라 API 선택
const loadReviews = useCallback(async () => {
if (!prdtId) {
console.warn('[useReviews] loadReviews 호출되었지만 prdtId가 없음');
@@ -59,36 +128,63 @@ const useReviews = (prdtId, patnrId) => {
return;
}
// console.log('[useReviews] loadReviews 시작:', { prdtId, patnrId });
setIsLoading(true);
console.log('[useReviews] 📋 loadReviews 호출됨:', {
prdtId,
patnrId,
reviewVersion,
willCallApi: reviewVersion === 1 ? 'getUserReviews (v1)' : 'getUserReviewList (v2)'
});
try {
// Redux action을 통해 API 호출 - patnrId 추가
await dispatch(getUserReviews({ prdtId, patnrId }));
if (reviewVersion === 1) {
// 기존 API 호출
console.log('[useReviews] 🔄 getUserReviews 호출 중... (v1)');
await dispatch(getUserReviews({ prdtId, patnrId }));
} else {
// 신 API 호출 (v2)
console.log('[useReviews] 🔄 getUserReviewList 호출 중... (v2)');
await dispatch(getUserReviewList({
prdtId,
patnrId,
filterTpCd: 'ALL',
pageSize: 100,
pageNo: 1
}));
}
setHasLoadedData(true);
// console.log('[useReviews] loadReviews 완료');
} catch (error) {
console.error('[useReviews] loadReviews 실패:', error);
} finally {
setIsLoading(false);
}
}, [prdtId, patnrId, dispatch]);
}, [prdtId, patnrId, reviewVersion, dispatch]);
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
// 중요: reviewVersion이 다르면 다른 Redux 상태를 사용하므로 reviewVersion도 체크해야 함
useEffect(() => {
if (prdtId && patnrId && prdtId !== loadedPrdtId) {
// console.log('[useReviews] prdtId changed, loading new data:', {
// from: loadedPrdtId,
// to: prdtId,
// patnrId
// });
// prdtId 변경 시 로딩 상태 즉시 설정으로 UI 깜빡임 방지
setIsLoading(true);
setHasLoadedData(false);
loadReviews();
} else if (prdtId === loadedPrdtId) {
// console.log('[useReviews] Using cached data for same prdtId:', { prdtId, patnrId });
setHasLoadedData(true); // 캐시된 데이터 사용 시 로드 완료 상태로 설정
if (prdtId && patnrId) {
const needsLoad = prdtId !== loadedPrdtId;
console.log('[useReviews] 🔄 로드 필요 여부 체크:', {
prdtId,
patnrId,
loadedPrdtId,
reviewVersion,
needsLoad,
reason: needsLoad ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
});
if (needsLoad) {
// prdtId 변경 시 로딩 상태 즉시 설정으로 UI 깜빡임 방지
setIsLoading(true);
setHasLoadedData(false);
loadReviews();
} else {
// 캐시된 데이터 사용
setHasLoadedData(true);
}
}
}, [prdtId, patnrId, loadedPrdtId, loadReviews]);
@@ -538,16 +634,7 @@ const useReviews = (prdtId, patnrId) => {
const apiTotalCount = reviewDetail && reviewDetail.totRvwCnt ? reviewDetail.totRvwCnt : 0;
const actualReviewCount = allReviews.length;
// if (apiTotalCount > actualReviewCount && actualReviewCount > 0) {
// console.log('[UserReviews]-API Mismatch API 개수와 실제 개수 불일치:', {
// apiTotalCount,
// actualReviewCount,
// difference: apiTotalCount - actualReviewCount,
// reason: 'API가 최대 100개만 반환하는 페이징 제한',
// });
// }
return {
const result = {
totalReviews: actualReviewCount, // 실제로 받은 리뷰 개수 사용 (API 제한 반영)
filteredCount: filteredReviews.length, // 필터링된 리뷰 개수
displayedCount: displayReviews.length, // 현재 표시 중인 리뷰 개수
@@ -559,7 +646,21 @@ const useReviews = (prdtId, patnrId) => {
: 0,
totalRatingCount: actualReviewCount, // 실제로 받은 리뷰 개수 사용
};
}, [allReviews.length, filteredReviews.length, displayReviews.length, reviewDetail]);
console.log('[useReviews_useReviewList] 📊 stats 계산:', {
actualReviewCount,
apiTotalCount,
allReviewsLength: allReviews.length,
hasLoadedData,
isLoading,
isCurrentProductLoaded,
reviewVersion,
loadedPrdtId,
prdtId
});
return result;
}, [allReviews.length, filteredReviews.length, displayReviews.length, reviewDetail, hasLoadedData, isLoading, isCurrentProductLoaded, reviewVersion, loadedPrdtId, prdtId]);
// 데이터 새로고침 - 강제로 다시 로드
const refreshData = useCallback(() => {
@@ -570,6 +671,23 @@ const useReviews = (prdtId, patnrId) => {
loadReviews();
}, [loadReviews]);
// hasReviews 계산
const hasReviews = allReviews.length > 0 && hasLoadedData && !isLoading && isCurrentProductLoaded;
// hasReviews 계산 로그
useEffect(() => {
console.log('[useReviews_useReviewList] 🔴 hasReviews 계산:', {
allReviewsLength: allReviews.length,
hasLoadedData,
isLoading,
isCurrentProductLoaded,
loadedPrdtId,
prdtId,
resultHasReviews: hasReviews,
reviewVersion
});
}, [hasReviews, allReviews.length, hasLoadedData, isLoading, isCurrentProductLoaded, loadedPrdtId, prdtId, reviewVersion]);
return {
// 🔥 핵심 API 함수 - useReviews가 모든 API 호출 담당
loadReviews, // 리뷰 데이터 로드 (prdtId 기반)
@@ -580,7 +698,7 @@ const useReviews = (prdtId, patnrId) => {
previewReviews, // DetailPanel용 미리보기 리뷰 (첫 5개)
allReviews, // 전체 원본 리뷰 (필터링 안된, 빈 내용 제외)
filteredReviews, // 필터링된 전체 리뷰
hasReviews: allReviews.length > 0 && hasLoadedData && !isLoading && isCurrentProductLoaded, // 리뷰 존재 여부 (현재 제품이 로드된 경우에만 true)
hasReviews, // 리뷰 존재 여부 (현재 제품이 로드된 경우에만 true)
// 🔄 UserReviewPanel 전용 페이징 데이터
userReviewPanelReviews, // UserReviewPanel용 페이징된 리뷰들 (3개 표시, 1개씩 이동)
@@ -618,6 +736,7 @@ const useReviews = (prdtId, patnrId) => {
_debug: {
prdtId,
patnrId,
reviewVersion,
allReviews: allReviews.slice(0, 3), // 처음 3개만
currentFilter,
filteredCount: filteredReviews.length,

View File

@@ -5,9 +5,14 @@ const initialState = {
bestSellerData: {},
productImageLength: 0,
prdtOptInfo: {},
reviewData: null, // 리뷰 데이터 추가
showAllReviews: false, // 전체 리뷰 보기 상태
loadedPrdtId: null, // 현재 로드된 상품 ID
// ReviewVersion 1 (기존 API)
reviewData: null,
loadedPrdtId: null,
// ReviewVersion 2 (신 API)
reviewListData: null,
loadedListPrdtId: null,
// 기타
showAllReviews: false,
};
// FP: handlers map (curried), pure and immutable updates only
@@ -37,21 +42,29 @@ const handleProductOptionId = curry((state, action) =>
set('prodOptCdCval', get('payload', action), state)
);
// 유저 리뷰 데이터 핸들러 추가
// 유저 리뷰 데이터 핸들러 (v1 - 기존 API)
const handleUserReview = curry((state, action) => {
const reviewData = get('payload', action);
const prdtId = get(['payload', 'prdtId'], action);
// console.log("[UserReviews] Reducer - Storing review data:", {
// prdtId: prdtId,
// hasData: !!reviewData,
// reviewListLength: reviewData && reviewData.reviewList ? reviewData.reviewList.length : 0,
// totalCount: reviewData && reviewData.reviewDetail ? reviewData.reviewDetail.totRvwCnt : 0
// });
return set('reviewData', reviewData, set('loadedPrdtId', prdtId, state));
});
// 유저 리뷰 리스트 데이터 핸들러 (v2 - 신 API)
const handleUserReviewList = curry((state, action) => {
const reviewListData = get('payload', action);
const prdtId = get(['payload', 'prdtId'], action);
console.log('[productReducer_useReviewList] 🟡 handleUserReviewList:', {
prdtId,
reviewListDataKeys: reviewListData ? Object.keys(reviewListData) : 'null',
reviewListLength: reviewListData?.reviewList?.length || 0,
reviewDetail: reviewListData?.reviewDetail
});
return set('reviewListData', reviewListData, set('loadedListPrdtId', prdtId, state));
});
// showAllReviews 토글 핸들러
const handleToggleShowAllReviews = curry((state, action) => {
const currentValue = get('showAllReviews', state);
@@ -73,13 +86,38 @@ const handlers = {
[types.GET_VIDEO_INDECATOR_FOCUS]: handleVideoIndicatorFocus,
[types.CLEAR_PRODUCT_DETAIL]: handleClearProductDetail,
[types.GET_PRODUCT_OPTION_ID]: handleProductOptionId,
[types.GET_USER_REVIEW]: handleUserReview, // GET_USER_REVIEW 핸들러 추가
[types.TOGGLE_SHOW_ALL_REVIEWS]: handleToggleShowAllReviews, // showAllReviews 토글 핸들러
[types.RESET_SHOW_ALL_REVIEWS]: handleResetShowAllReviews, // showAllReviews 초기화 핸들러
[types.GET_USER_REVIEW]: handleUserReview,
[types.GET_USER_REVIEW_LIST]: handleUserReviewList,
[types.TOGGLE_SHOW_ALL_REVIEWS]: handleToggleShowAllReviews,
[types.RESET_SHOW_ALL_REVIEWS]: handleResetShowAllReviews,
};
export const productReducer = (state = initialState, action = {}) => {
const type = get('type', action);
const handler = handlers[type];
return handler ? handler(state, action) : state;
// v2 디버깅: GET_USER_REVIEW_LIST 액션 확인
if (type === 'GET_USER_REVIEW_LIST') {
console.log('[productReducer_useReviewList] 🟢 GET_USER_REVIEW_LIST 액션 수신:', {
actionType: type,
payloadKeys: action.payload ? Object.keys(action.payload) : 'null',
reviewListLength: action.payload?.reviewList?.length || 0,
prdtId: action.payload?.prdtId,
handler: !!handler
});
}
const newState = handler ? handler(state, action) : state;
// v2 디버깅: 상태 업데이트 확인
if (type === 'GET_USER_REVIEW_LIST') {
console.log('[productReducer_useReviewList] 🔵 상태 업데이트 완료:', {
reviewListDataExists: !!newState.reviewListData,
reviewListDataKeys: newState.reviewListData ? Object.keys(newState.reviewListData) : 'null',
loadedListPrdtId: newState.loadedListPrdtId,
reviewListLength: newState.reviewListData?.reviewList?.length || 0
});
}
return newState;
};

View File

@@ -261,8 +261,17 @@ export default function ProductAllSection({
stats,
isLoading: reviewsLoading,
hasReviews, // 리뷰 존재 여부 플래그 추가
_debug,
} = useReviews(productData.prdtId, panelInfo && panelInfo.patnrId);
console.log('[ProductAllSection_useReviewList] 🟡 반환 데이터:', {
reviewVersion: _debug?.reviewVersion,
previewReviewsLength: previewReviews?.length || 0,
statsTotal: stats?.totalReviews,
hasReviews,
selectedReduxState: _debug?.reviewVersion === 1 ? 'reviewData' : 'reviewListData'
});
// YouMayAlsoLike 데이터 확인
const hasYouMayAlsoLike = youmaylikeData && youmaylikeData.length > 0;
@@ -323,6 +332,7 @@ export default function ProductAllSection({
useState(false);
const reviewTotalCount = stats.totalReviews;
const { getScrollTo, scrollTop } = useScrollTo();
const scrollToSection = useCallback(
@@ -343,7 +353,9 @@ export default function ProductAllSection({
// User Reviews 스크롤 핸들러 추가
const handleUserReviewsClick = useCallback(
() => scrollToSection('scroll-marker-user-reviews'),
() => {
scrollToSection('scroll-marker-user-reviews');
},
[scrollToSection]
);
@@ -761,16 +773,23 @@ export default function ProductAllSection({
{$L('PRODUCT DETAILS')}
</TButton>
{hasReviews && (
<TButton
className={classNames(
css.userReviewsButton,
activeButton === 'review' ? css.active : ''
)}
onClick={handleUserReviewsClick}
spotlightId="user-reviews-button"
>
{$L('USER REVIEWS')} ({reviewTotalCount})
</TButton>
<>
{console.log('[ProductAllSection_useReviewList] 🎯 버튼 렌더링:', {
hasReviews,
reviewTotalCount,
reviewVersion: _debug?.reviewVersion
})}
<TButton
className={classNames(
css.userReviewsButton,
activeButton === 'review' ? css.active : ''
)}
onClick={handleUserReviewsClick}
spotlightId="user-reviews-button"
>
{$L('USER REVIEWS')} ({reviewTotalCount})
</TButton>
</>
)}
{hasYouMayAlsoLike && (
<TButton

View File

@@ -49,6 +49,13 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
isLoading: fallbackReviews.isLoading,
};
console.log('[UserReviews_useReviewList] 🔵 데이터:', {
dataSource: reviewsData ? 'ProductAllSection props' : 'fallback useReviews',
fallbackVersion: fallbackReviews._debug?.reviewVersion,
previewReviewsLength: actualReviewsData.previewReviews?.length || 0,
statsTotal: actualReviewsData.stats?.totalReviews
});
// 팝업 상태 관리 - 모드와 선택된 이미지 인덱스 추가
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [popupMode, setPopupMode] = useState('customer-images'); // "customer-images", "all-images"
@@ -307,6 +314,23 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
</SpottableComponent>
);
})}
{/* View All Reviews Button - 선택적으로 표시 */}
{reviewTotalCount > 5 && !showAllReviews && (
<div className={css.viewAllReviewsSection}>
<Spottable
className={css.viewAllReviewsButton}
spotlightId="view-all-reviews-button"
onClick={() => {
console.log('[UserReviews] View All Reviews button clicked');
dispatch(toggleShowAllReviews());
}}
>
<div className={css.viewAllReviewsContent}>
<span className={css.viewAllReviewsText}>{$L('View All Reviews +')}</span>
</div>
</Spottable>
</div>
)}
</div>
</UserReviewsScroller>

View File

@@ -4,6 +4,7 @@ import Spottable from '@enact/spotlight/Spottable';
import Spotlight from '@enact/spotlight';
import { pushPanel } from '../../actions/panelActions';
import { panel_names } from '../../utils/Config';
import useReviews, { REVIEW_VERSION } from '../../hooks/useReviews/useReviews';
import css from './ShowUserReviews.module.less';
const SpottableComponent = Spottable('div');
@@ -11,13 +12,24 @@ const SpottableComponent = Spottable('div');
const ShowUserReviews = ({ hasVideo, launchedFromPlayer, onFocus, onBlur }) => {
const dispatch = useDispatch();
// v2 API의 Redux 상태 사용
const reviewListData = useSelector(
(state) => state.product.reviewData && state.product.reviewData.reviewList
(state) => state.product.reviewListData && state.product.reviewListData.reviewList
);
const reviewData = useSelector((state) => state.product.reviewData || {});
const reviewData = useSelector((state) => state.product.reviewListData || {});
const productData = useSelector((state) => state.main.productData || {});
// useReviews Hook에서 필요한 정보 추가 확인
const { stats } = useReviews(productData && productData.prdtId);
const handleShowUserReviewsClick = useCallback(() => {
console.log('[ShowUserReviews] 🎯 Show User Reviews 버튼 클릭:', {
reviewVersion: REVIEW_VERSION,
reviewListDataLength: reviewListData?.length || 0,
statsAvgRating: stats?.averageRating,
statsTotalReviews: stats?.totalReviews
});
dispatch(
pushPanel({
name: panel_names.USER_REVIEW_PANEL,
@@ -30,8 +42,8 @@ const ShowUserReviews = ({ hasVideo, launchedFromPlayer, onFocus, onBlur }) => {
'https://placehold.co/150x150',
brandLogo: productData.patncLogoPath || 'https://placehold.co/50x50',
productName: productData.prdtNm || '상품명 정보가 없습니다',
avgRating: reviewData.reviewDetail?.avgRvwScr || 5,
reviewCount: reviewData.reviewDetail?.totRvwCnt || reviewListData?.length || 0,
avgRating: stats?.averageRating || 5,
reviewCount: stats?.totalReviews || reviewListData?.length || 0,
bgVideoInfo: {
hasVideo: hasVideo,
launchedFromPlayer: launchedFromPlayer ?? false,
@@ -39,7 +51,7 @@ const ShowUserReviews = ({ hasVideo, launchedFromPlayer, onFocus, onBlur }) => {
},
})
);
}, [dispatch, productData, reviewData, reviewListData, hasVideo, launchedFromPlayer]);
}, [dispatch, productData, reviewData, reviewListData, stats, hasVideo, launchedFromPlayer]);
const handleKeyDown = useCallback((event) => {
if (event.key === 'ArrowUp') {
@@ -74,19 +86,31 @@ const ShowUserReviews = ({ hasVideo, launchedFromPlayer, onFocus, onBlur }) => {
}, []);
useEffect(() => {
console.log('[ShowUserReviews] Component mounted/updated', {
console.log('[ShowUserReviews_v2] 컴포넌트 마운트/업데이트', {
hasReviewData: !!reviewListData,
reviewCount: reviewListData?.length || 0,
statsTotalReviews: stats?.totalReviews,
shouldShowButton: reviewListData && reviewListData.length > 5,
spotlightUpTarget: 'user-review-at-last',
reviewVersion: REVIEW_VERSION
});
}, [reviewListData]);
}, [reviewListData, stats]);
// v2 API 데이터로 렌더링 조건 수정: 5개 이상일 때만 보이기
if (!reviewListData || reviewListData.length <= 5) {
console.log('[ShowUserReviews_v2] ❌ 버튼 숨김:', {
reason: !reviewListData ? 'reviewListData가 없음' : 'reviewListData.length <= 5',
reviewListLength: reviewListData?.length || 0
});
return null;
}
return (
<div className={css.showUserReviews__wrapper}>
{console.log('[ShowUserReviews_v2] ✅ 버튼 렌더링 시작:', {
reviewListLength: reviewListData?.length,
version: REVIEW_VERSION
})}
<SpottableComponent
className={css.showUserReviews__button}
onClick={handleShowUserReviewsClick}

View File

@@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux';
import { popPanel, updatePanel } from '../../actions/panelActions';
import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel';
import useReviews from '../../hooks/useReviews/useReviews';
import useReviews, { REVIEW_VERSION } from '../../hooks/useReviews/useReviews';
import fp from '../../utils/fp';
import { panel_names } from '../../utils/Config';
import StarRating from '../DetailPanel/components/StarRating';
@@ -15,6 +15,10 @@ import UserReviewsList from './components/UserReviewsList';
import UserReviewHeader from './UserReviewHeader';
import css from './UserReviewPanel.module.less';
// 버전에 따른 UI 설정
const VERSION_LABEL = REVIEW_VERSION === 1 ? '[v1 - 기존 API]' : '[v2 - 신 API]';
console.log('[UserReviewPanel] 🔧 REVIEW_VERSION:', REVIEW_VERSION, VERSION_LABEL);
const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
const dispatch = useDispatch();
@@ -36,7 +40,8 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
currentFilter,
filterCounts,
stats,
} = useReviews(prdtId, patnrId);
_debug,
} = useReviews(prdtId, patnrId); // REVIEW_VERSION 상수에 따라 자동으로 API 선택
const [isPaging, setIsPaging] = useState(false);
@@ -129,6 +134,24 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
[handleSentimentFilter]
);
// UserReviewPanel 데이터 상태 로깅
useEffect(() => {
if (_debug) {
console.log(`[UserReviewPanel] 📊 데이터 상태 ${VERSION_LABEL}:`, {
reviewVersion: _debug.reviewVersion,
versionLabel: VERSION_LABEL,
totalReviews: stats.totalReviews,
filteredCount: stats.filteredCount,
currentFilter: currentFilter,
userReviewPanelPage,
userReviewPanelReviewsCount: userReviewPanelReviews?.length || 0,
hasNext: userReviewPanelHasNext,
hasPrev: userReviewPanelHasPrev,
totalPages: userReviewPanelTotalPages,
});
}
}, [_debug, stats, currentFilter, userReviewPanelPage, userReviewPanelReviews, userReviewPanelHasNext, userReviewPanelHasPrev, userReviewPanelTotalPages]);
useEffect(() => {
return () => {
clearAllFilters();
@@ -168,7 +191,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
isTabActivated={false}
>
<UserReviewHeader
title="User Reviews"
title={`User Reviews ${VERSION_LABEL}`}
onBackButton={handleBackButton}
onClick={handleBackButton}
className={css.header}