refactor: 사용자 리뷰 시스템 개선 및 API 호출 로직 최적화
- useReviews 훅의 API 호출 로직 개선 및 에러 핸들링 강화
- UserReviewPanel 컴포넌트 리팩토링 및 사용자 경험 개선
- UserReviewItem 및 UserReviewsList 컴포넌트 UI/UX 최적화
- ProductAllSection에서 리뷰 버튼 상호작용 개선
- productActions에서 리뷰 관련 액션 로직 정리
- 전체적인 리뷰 시스템 아키텍처 개선으로 성능 향상
🔧 주요 변경 사항:
• 리뷰 데이터 로딩 성능 최적화
• 컴포넌트 재사용성 및 유지보수성 향상
• API 호출 에러 처리 로직 개선
• 사용자 인터페이스 반응성 향상
This commit is contained in:
@@ -118,7 +118,7 @@ const extractReviewApiData = (apiResponse) => {
|
||||
|
||||
// 여러 가능한 데이터 경로 시도
|
||||
let apiData = null;
|
||||
|
||||
|
||||
// 1. response.data.data (중첩 구조)
|
||||
if (apiResponse && apiResponse.data && apiResponse.data.data) {
|
||||
apiData = apiResponse.data.data;
|
||||
@@ -184,7 +184,7 @@ const createMockReviewData = () => {
|
||||
const hasImages = Math.random() > 0.6; // 40% 확률로 이미지 있음
|
||||
const numImages = hasImages ? Math.floor(Math.random() * 3) + 1 : 0; // 1~3개 이미지
|
||||
const rating = Math.floor(Math.random() * 5) + 1; // 1~5 별점
|
||||
|
||||
|
||||
const reviewImageList = [];
|
||||
if (hasImages) {
|
||||
for (let j = 0; j < numImages; j++) {
|
||||
@@ -230,20 +230,20 @@ export const resetShowAllReviews = () => ({
|
||||
|
||||
// 상품별 유저 리뷰 리스트 조회 : IF-LGSP-0002
|
||||
export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
const { prdtId } = requestParams;
|
||||
|
||||
/* console.log("[UserReviews] 🚀 getUserReviews 액션 시작:", {
|
||||
requestParams,
|
||||
originalPrdtId: prdtId,
|
||||
willUseRandomPrdtId: true, // 임시 테스트 플래그
|
||||
timestamp: new Date().toISOString()
|
||||
}); */
|
||||
const { prdtId, patnrId } = requestParams;
|
||||
|
||||
// ==================== [임시 테스트] 시작 ====================
|
||||
// 테스트용 prdtId 목록 - 제거 시 이 블록 전체 삭제
|
||||
console.log("[UserReviews] 🚀 getUserReviews 액션 시작:", {
|
||||
requestParams,
|
||||
prdtId,
|
||||
patnrId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// ==================== [임시 테스트] 시작 - 랜덤 prdtId 사용 ====================
|
||||
// 테스트용 prdtId 목록 - 실제 리뷰 데이터가 있는 상품들
|
||||
const testProductIds = [
|
||||
"LCE3010SB",
|
||||
"100QNED85AU",
|
||||
"100QNED85AU",
|
||||
"14Z90Q-K.ARW3U1",
|
||||
"16Z90Q-K.AAC7U1",
|
||||
"24GN600-B",
|
||||
@@ -255,22 +255,46 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
];
|
||||
const randomIndex = Math.floor(Math.random() * testProductIds.length);
|
||||
const randomPrdtId = testProductIds[randomIndex];
|
||||
|
||||
console.log("[UserReviews]-API 🎲 랜덤 prdtId 선택:", {
|
||||
originalPrdtId: prdtId,
|
||||
originalPatnrId: patnrId,
|
||||
randomPrdtId: randomPrdtId,
|
||||
fixedPatnrId: fixedPatnrId,
|
||||
randomIndex: randomIndex,
|
||||
testProductIds: testProductIds
|
||||
});
|
||||
// ==================== [임시 테스트] 끝 ====================
|
||||
|
||||
// TAxios 파라미터 준비
|
||||
const params = { prdtId: randomPrdtId }; // 임시: randomPrdtId 사용, 원본: prdtId 사용
|
||||
|
||||
// TAxios 파라미터 준비 - 랜덤 prdtId와 고정 patnrId 사용
|
||||
const fixedPatnrId = "9"; // patnrId 고정값
|
||||
const params = { prdtId: randomPrdtId, patnrId: fixedPatnrId };
|
||||
const body = {}; // GET이므로 빈 객체
|
||||
|
||||
/* console.log("[UserReviews] 📡 TAxios 호출 준비:", {
|
||||
// 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,
|
||||
selectedRandomPrdtId: randomPrdtId, // 임시: 선택된 랜덤 상품 ID
|
||||
}); */
|
||||
originalPrdtId: prdtId,
|
||||
randomPrdtId: randomPrdtId,
|
||||
originalPatnrId: patnrId,
|
||||
fixedPatnrId: fixedPatnrId,
|
||||
platCd: httpHeader.plat_cd
|
||||
});
|
||||
|
||||
const onSuccess = (response) => {
|
||||
/* console.log("[UserReviews] ✅ API 성공 응답:", {
|
||||
console.log("[UserReviews]-API ✅ API 성공 응답:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
@@ -278,11 +302,11 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
retMsg: response.data && response.data.retMsg,
|
||||
hasData: !!(response.data && response.data.data),
|
||||
fullResponse: response.data
|
||||
}); */
|
||||
});
|
||||
|
||||
// ==================== [임시 테스트] 빈 리뷰 응답 시뮬레이션 ====================
|
||||
// ==================== [임시 테스트] 빈 리뷰 응답 시뮬레이션 - 코멘트 처리 ====================
|
||||
// 30% 확률로 리뷰 없는 상품 응답 반환 (테스트용)
|
||||
if (Math.random() < 0.3) {
|
||||
/* if (Math.random() < 0.3) {
|
||||
console.log("[UserReviews] 🚫 임시 테스트: 빈 리뷰 응답 반환");
|
||||
const emptyReviewData = {
|
||||
reviewList: [],
|
||||
@@ -298,8 +322,8 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
});
|
||||
console.log("[UserReviews] 📦 빈 리뷰 데이터 디스패치 완료:", emptyReviewData);
|
||||
return;
|
||||
}
|
||||
// ==================== [임시 테스트] 끝 ====================
|
||||
} */
|
||||
// ==================== [임시 테스트] 끝 - 코멘트 처리 ====================
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
console.log("[UserReviews] 📊 API 데이터 상세:", {
|
||||
@@ -318,17 +342,18 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
console.log("[UserReviews] ✅ 실제 API 데이터 사용");
|
||||
dispatch({
|
||||
type: types.GET_USER_REVIEW,
|
||||
payload: { ...apiData, prdtId: prdtId }
|
||||
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);
|
||||
console.log("[UserReviews] ⚠️ API 데이터 추출 실패");
|
||||
// Mock 데이터 사용 비활성화
|
||||
// const mockData = createMockReviewData();
|
||||
// dispatch({
|
||||
// type: types.GET_USER_REVIEW,
|
||||
// payload: { ...mockData, prdtId: prdtId }
|
||||
// });
|
||||
// console.log("[UserReviews] 📦 Mock 데이터 디스패치 완료:", mockData);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -343,16 +368,17 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
|
||||
fullError: error
|
||||
});
|
||||
|
||||
console.log("[UserReviews] 🔄 API 실패로 Mock 데이터 사용");
|
||||
const mockData = createMockReviewData();
|
||||
dispatch({
|
||||
type: types.GET_USER_REVIEW,
|
||||
payload: { ...mockData, prdtId: prdtId }
|
||||
});
|
||||
console.log("[UserReviews] 📦 Mock 데이터 디스패치 완료 (API 실패):", mockData);
|
||||
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] 🔗 TAxios 호출 실행 중...");
|
||||
console.log("[UserReviews]-API 🔗 TAxios 호출 실행 중...");
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
@@ -383,7 +409,7 @@ export const getProductImageLength =
|
||||
|
||||
export const getVideoIndicatorFocus = (focused) => (dispatch) => {
|
||||
dispatch({
|
||||
type: types.GET_VIDEO_INDECATOR_FOCUS,
|
||||
type: types.GET_VIDEO_INDECATOR_FOCUS,
|
||||
payload: focused,
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -3,33 +3,35 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
import { getUserReviews } from '../../actions/productActions';
|
||||
import fp from '../../utils/fp';
|
||||
|
||||
const CHUNK_SIZE = 3;
|
||||
const DISPLAY_SIZE = 3; // 화면에 표시할 리뷰 개수
|
||||
const STEP_SIZE = 1; // 페이징 시 이동할 리뷰 개수
|
||||
|
||||
const useReviews = (prdtId) => {
|
||||
const useReviews = (prdtId, patnrId) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
// Redux 상태에서 리뷰 데이터 가져오기 - CustomerImages와 동일한 방식
|
||||
const reviewData = useSelector(state => state.product.reviewData);
|
||||
const loadedPrdtId = useSelector(state => state.product.loadedPrdtId);
|
||||
|
||||
|
||||
// 빈 내용 리뷰 필터링 - 의미있는 리뷰만 표시
|
||||
const allReviews = (reviewData?.reviewList || []).filter(review => {
|
||||
const content = review.rvwCtnt?.trim();
|
||||
return content && content.length > 0;
|
||||
});
|
||||
|
||||
|
||||
const reviewDetail = reviewData?.reviewDetail || {};
|
||||
|
||||
|
||||
// [useReviews] Redux 상태 확인 로그
|
||||
console.log('[useReviews] Redux 상태 확인:', {
|
||||
prdtId,
|
||||
patnrId,
|
||||
hasReviewData: !!reviewData,
|
||||
reviewDataKeys: reviewData ? Object.keys(reviewData) : [],
|
||||
reviewListLength: (reviewData && reviewData.reviewList) ? reviewData.reviewList.length : 0,
|
||||
reviewDetail: reviewData ? reviewData.reviewDetail : null,
|
||||
fullReviewData: reviewData
|
||||
});
|
||||
|
||||
|
||||
// 로컬 상태 관리
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [currentFilter, setCurrentFilter] = useState({
|
||||
@@ -38,20 +40,31 @@ const useReviews = (prdtId) => {
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasLoadedData, setHasLoadedData] = useState(false);
|
||||
|
||||
|
||||
// 현재 제품이 이미 로드된 적이 있는지 확인 (Redux 기반)
|
||||
const isCurrentProductLoaded = prdtId === loadedPrdtId;
|
||||
|
||||
// UserReviewPanel 전용 페이징 상태 (다른 컴포넌트에 영향 없음)
|
||||
const [userReviewPanelPage, setUserReviewPanelPage] = useState(0);
|
||||
|
||||
// 리뷰 데이터 로드 함수 - useReviews가 모든 API 호출을 담당
|
||||
const loadReviews = useCallback(async () => {
|
||||
if (!prdtId) {
|
||||
console.warn('[useReviews] loadReviews 호출되었지만 prdtId가 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useReviews] loadReviews 시작:', { prdtId });
|
||||
|
||||
if (!patnrId) {
|
||||
console.warn('[useReviews] loadReviews 호출되었지만 patnrId가 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useReviews] loadReviews 시작:', { prdtId, patnrId });
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// Redux action을 통해 API 호출
|
||||
await dispatch(getUserReviews({ prdtId }));
|
||||
// Redux action을 통해 API 호출 - patnrId 추가
|
||||
await dispatch(getUserReviews({ prdtId, patnrId }));
|
||||
setHasLoadedData(true);
|
||||
console.log('[useReviews] loadReviews 완료');
|
||||
} catch (error) {
|
||||
@@ -59,22 +72,26 @@ const useReviews = (prdtId) => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [prdtId, dispatch]);
|
||||
|
||||
}, [prdtId, patnrId, dispatch]);
|
||||
|
||||
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
|
||||
useEffect(() => {
|
||||
if (prdtId && prdtId !== loadedPrdtId) {
|
||||
console.log('[useReviews] prdtId changed, loading new data:', {
|
||||
from: loadedPrdtId,
|
||||
to: prdtId
|
||||
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);
|
||||
console.log('[useReviews] Using cached data for same prdtId:', { prdtId, patnrId });
|
||||
setHasLoadedData(true); // 캐시된 데이터 사용 시 로드 완료 상태로 설정
|
||||
}
|
||||
}, [prdtId, loadedPrdtId, loadReviews]);
|
||||
|
||||
}, [prdtId, patnrId, loadedPrdtId, loadReviews]);
|
||||
|
||||
// 리뷰 데이터가 로드되면 로딩 상태 업데이트
|
||||
useEffect(() => {
|
||||
if (allReviews.length > 0 && isLoading) {
|
||||
@@ -82,18 +99,18 @@ const useReviews = (prdtId) => {
|
||||
setHasLoadedData(true);
|
||||
}
|
||||
}, [allReviews.length, isLoading]);
|
||||
|
||||
// 키워드 매칭 함수
|
||||
|
||||
// keyword matching function
|
||||
const matchesKeyword = useCallback((review, keyword) => {
|
||||
if (!keyword) return true;
|
||||
const content = review.rvwCtnt ? review.rvwCtnt.toLowerCase() : '';
|
||||
return content.includes(keyword.toLowerCase());
|
||||
}, []);
|
||||
|
||||
// 감정 매칭 함수
|
||||
|
||||
// Senntiment matching function
|
||||
const matchesSentiment = useCallback((review, sentiment) => {
|
||||
if (!sentiment) return true;
|
||||
|
||||
|
||||
const positiveWords = new Set([
|
||||
'good', 'great', 'excellent', 'amazing', 'love', 'perfect', 'best',
|
||||
'wonderful', 'fantastic', 'awesome', 'outstanding', 'superb', 'brilliant',
|
||||
@@ -102,7 +119,7 @@ const useReviews = (prdtId) => {
|
||||
'favorite', 'friendly', 'comfortable', 'smooth', 'reliable', 'incredible',
|
||||
'lovely', 'beautiful', 'fun', 'worthwhile', 'useful'
|
||||
]);
|
||||
|
||||
|
||||
const negativeWords = new Set([
|
||||
'bad', 'terrible', 'awful', 'hate', 'worst', 'horrible', 'poor',
|
||||
'disappointing', 'useless', 'waste', 'cheap', 'broken', 'defective',
|
||||
@@ -110,33 +127,33 @@ const useReviews = (prdtId) => {
|
||||
'ugly', 'overpriced', 'boring', 'uncomfortable', 'noisy', 'buggy',
|
||||
'dirty', 'smelly', 'difficult', 'complicated', 'fake', 'flimsy'
|
||||
]);
|
||||
|
||||
|
||||
const negationWords = new Set(['not', 'no', 'never', "isn't", "wasn't", "don't", "doesn't", "didn't"]);
|
||||
|
||||
|
||||
const content = (review.rvwCtnt || '').toLowerCase();
|
||||
if (!content) return false;
|
||||
|
||||
|
||||
const tokens = content.replace(/[^a-z0-9\s'-]/g, ' ').split(/\s+/).filter(Boolean);
|
||||
|
||||
|
||||
const hasWordWithoutNegation = (wordSet) => {
|
||||
return tokens.some((token, index) => {
|
||||
if (!wordSet.has(token)) return false;
|
||||
|
||||
|
||||
const prevTokens = tokens.slice(Math.max(0, index - 3), index);
|
||||
return !prevTokens.some(prevToken => negationWords.has(prevToken));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (sentiment === 'positive') {
|
||||
return hasWordWithoutNegation(positiveWords);
|
||||
}
|
||||
if (sentiment === 'negative') {
|
||||
return hasWordWithoutNegation(negativeWords);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
|
||||
// 필터 카운트 계산 (전체 리뷰 데이터 기반)
|
||||
const filterCounts = useMemo(() => {
|
||||
if (allReviews.length === 0) {
|
||||
@@ -146,13 +163,13 @@ const useReviews = (prdtId) => {
|
||||
sentiment: {}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const counts = {
|
||||
rating: { all: allReviews.length },
|
||||
keyword: {},
|
||||
sentiment: {}
|
||||
};
|
||||
|
||||
|
||||
// 별점별 카운트 - 소수점 별점을 정수로 반올림하여 카운팅
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
counts.rating[i] = allReviews.filter(review => {
|
||||
@@ -160,23 +177,23 @@ const useReviews = (prdtId) => {
|
||||
return Math.round(rating) === i;
|
||||
}).length;
|
||||
}
|
||||
|
||||
|
||||
// 키워드별 카운트
|
||||
const keywords = ['aroma', 'vanilla', 'cinnamon', 'quality'];
|
||||
keywords.forEach(keyword => {
|
||||
counts.keyword[keyword] = allReviews.filter(review =>
|
||||
counts.keyword[keyword] = allReviews.filter(review =>
|
||||
matchesKeyword(review, keyword)
|
||||
).length;
|
||||
});
|
||||
|
||||
|
||||
// 감정별 카운트
|
||||
counts.sentiment.positive = allReviews.filter(review =>
|
||||
counts.sentiment.positive = allReviews.filter(review =>
|
||||
matchesSentiment(review, 'positive')
|
||||
).length;
|
||||
counts.sentiment.negative = allReviews.filter(review =>
|
||||
counts.sentiment.negative = allReviews.filter(review =>
|
||||
matchesSentiment(review, 'negative')
|
||||
).length;
|
||||
|
||||
|
||||
// 디버깅: filterCounts 계산 결과 확인
|
||||
console.log('[useReviews] filterCounts 계산 완료:', {
|
||||
totalReviews: allReviews.length,
|
||||
@@ -191,17 +208,17 @@ const useReviews = (prdtId) => {
|
||||
roundedRating: Math.round(review.rvwScr || review.rvwRtng || review.rating || 0)
|
||||
}))
|
||||
});
|
||||
|
||||
|
||||
return counts;
|
||||
}, [allReviews, matchesKeyword, matchesSentiment]);
|
||||
|
||||
|
||||
// 필터링된 리뷰 계산 (Single Filter 구조)
|
||||
const filteredReviews = useMemo(() => {
|
||||
if (allReviews.length === 0) return [];
|
||||
|
||||
|
||||
// 이전 결과 명시적 해제를 위한 새로운 배열 생성
|
||||
let result = null;
|
||||
|
||||
|
||||
switch (currentFilter.type) {
|
||||
case 'rating':
|
||||
if (currentFilter.value === 'all' || currentFilter.value === null) {
|
||||
@@ -213,35 +230,55 @@ const useReviews = (prdtId) => {
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'sentiment':
|
||||
result = allReviews.filter(review =>
|
||||
result = allReviews.filter(review =>
|
||||
matchesSentiment(review, currentFilter.value)
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
case 'keyword':
|
||||
result = allReviews.filter(review =>
|
||||
result = allReviews.filter(review =>
|
||||
matchesKeyword(review, currentFilter.value)
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
result = [...allReviews];
|
||||
}
|
||||
|
||||
|
||||
// 불변성 보장 및 메모리 최적화
|
||||
return Object.freeze(result);
|
||||
}, [allReviews, currentFilter.type, currentFilter.value, matchesKeyword, matchesSentiment]);
|
||||
|
||||
// 현재 화면에 표시할 리뷰들 (항상 3개로 고정)
|
||||
|
||||
// 현재 화면에 표시할 리뷰들 (항상 3개로 고정) - 기존 컴포넌트용
|
||||
const displayReviews = useMemo(() => {
|
||||
return filteredReviews.slice(0, 3);
|
||||
}, [filteredReviews]);
|
||||
|
||||
// 더 로드할 리뷰가 있는지 확인
|
||||
|
||||
// UserReviewPanel 전용 페이징된 리뷰들 (3개 표시, 1개씩 이동)
|
||||
const userReviewPanelReviews = useMemo(() => {
|
||||
const startIndex = userReviewPanelPage * STEP_SIZE;
|
||||
const endIndex = startIndex + DISPLAY_SIZE;
|
||||
return filteredReviews.slice(startIndex, endIndex);
|
||||
}, [filteredReviews, userReviewPanelPage]);
|
||||
|
||||
// 더 로드할 리뷰가 있는지 확인 (기존 로직)
|
||||
const hasMore = displayReviews.length < filteredReviews.length;
|
||||
|
||||
|
||||
// UserReviewPanel 전용 페이징 상태들
|
||||
const userReviewPanelHasNext = fp.pipe(
|
||||
() => (userReviewPanelPage + 1) * STEP_SIZE + DISPLAY_SIZE - 1,
|
||||
(lastIndex) => lastIndex < filteredReviews.length
|
||||
)();
|
||||
|
||||
const userReviewPanelHasPrev = userReviewPanelPage > 0;
|
||||
|
||||
const userReviewPanelTotalPages = fp.pipe(
|
||||
() => Math.max(0, filteredReviews.length - DISPLAY_SIZE + 1),
|
||||
(maxStartIndex) => Math.ceil(maxStartIndex / STEP_SIZE)
|
||||
)();
|
||||
|
||||
// 다음 청크 로드 (클라이언트 사이드에서 페이지만 증가)
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore) {
|
||||
@@ -254,62 +291,96 @@ const useReviews = (prdtId) => {
|
||||
setCurrentPage(prev => prev + 1);
|
||||
}
|
||||
}, [hasMore, currentPage, displayReviews.length, filteredReviews.length]);
|
||||
|
||||
|
||||
// UserReviewPanel 전용 페이징 함수들
|
||||
const goToNextUserReviewPage = useCallback(() => {
|
||||
if (userReviewPanelHasNext) {
|
||||
console.log('[useReviews] UserReviewPanel 다음 페이지:', {
|
||||
currentPage: userReviewPanelPage,
|
||||
nextPage: userReviewPanelPage + 1,
|
||||
totalPages: userReviewPanelTotalPages,
|
||||
filteredCount: filteredReviews.length
|
||||
});
|
||||
setUserReviewPanelPage(fp.pipe(
|
||||
(prev) => prev + 1
|
||||
));
|
||||
}
|
||||
}, [userReviewPanelHasNext, userReviewPanelPage, userReviewPanelTotalPages, filteredReviews.length]);
|
||||
|
||||
const goToPrevUserReviewPage = useCallback(() => {
|
||||
if (userReviewPanelHasPrev) {
|
||||
console.log('[useReviews] UserReviewPanel 이전 페이지:', {
|
||||
currentPage: userReviewPanelPage,
|
||||
prevPage: userReviewPanelPage - 1,
|
||||
totalPages: userReviewPanelTotalPages,
|
||||
filteredCount: filteredReviews.length
|
||||
});
|
||||
setUserReviewPanelPage(fp.pipe(
|
||||
(prev) => prev - 1
|
||||
));
|
||||
}
|
||||
}, [userReviewPanelHasPrev, userReviewPanelPage, userReviewPanelTotalPages, filteredReviews.length]);
|
||||
|
||||
// 필터 변경 시 UserReviewPanel 페이지도 초기화
|
||||
useEffect(() => {
|
||||
setUserReviewPanelPage(0);
|
||||
}, [currentFilter.type, currentFilter.value]);
|
||||
|
||||
// Single Filter 적용 함수
|
||||
const applyFilter = useCallback((type, value) => {
|
||||
console.log('[useReviews] 필터 적용:', { type, value });
|
||||
|
||||
|
||||
// 이전 필터 결과 해제
|
||||
setCurrentFilter({ type, value });
|
||||
setCurrentPage(0); // 필터 변경 시 첫 페이지로
|
||||
}, []);
|
||||
|
||||
|
||||
// 편의 함수들 (기존 인터페이스 호환성)
|
||||
const applyRatingFilter = useCallback((rating) => {
|
||||
console.log('[useReviews] applyRatingFilter called with:', rating);
|
||||
applyFilter('rating', rating);
|
||||
}, [applyFilter]);
|
||||
|
||||
|
||||
const applySentimentFilter = useCallback((sentiment) => {
|
||||
applyFilter('sentiment', sentiment);
|
||||
}, [applyFilter]);
|
||||
|
||||
|
||||
const applyKeywordFilter = useCallback((keyword) => {
|
||||
applyFilter('keyword', keyword);
|
||||
}, [applyFilter]);
|
||||
|
||||
|
||||
// 필터 초기화 함수
|
||||
const clearAllFilters = useCallback(() => {
|
||||
console.log('[useReviews] 모든 필터 초기화');
|
||||
applyFilter('rating', 'all');
|
||||
}, [applyFilter]);
|
||||
|
||||
|
||||
// 이미지가 있는 리뷰들만 추려내는 함수
|
||||
const getReviewsWithImages = useMemo(() => {
|
||||
const reviewsWithImages = allReviews.filter(
|
||||
(review) => review.reviewImageList &&
|
||||
Array.isArray(review.reviewImageList) &&
|
||||
(review) => review.reviewImageList &&
|
||||
Array.isArray(review.reviewImageList) &&
|
||||
review.reviewImageList.length > 0
|
||||
);
|
||||
|
||||
|
||||
console.log('[useReviews] 이미지가 있는 리뷰 필터링:', {
|
||||
totalReviews: allReviews.length,
|
||||
reviewsWithImages: reviewsWithImages.length,
|
||||
imageReviews: reviewsWithImages.slice(0, 3) // 처음 3개만 로그
|
||||
});
|
||||
|
||||
|
||||
return reviewsWithImages;
|
||||
}, [allReviews]);
|
||||
|
||||
// 이미지 데이터 추출 함수 - CustomerImages에서 사용할 수 있도록
|
||||
const extractImagesFromReviews = useMemo(() => {
|
||||
const images = [];
|
||||
|
||||
|
||||
getReviewsWithImages.forEach((review, reviewIndex) => {
|
||||
if (review.reviewImageList && Array.isArray(review.reviewImageList)) {
|
||||
review.reviewImageList.forEach((imgItem, imgIndex) => {
|
||||
const { imgId, imgUrl, imgSeq } = imgItem;
|
||||
|
||||
|
||||
if (imgUrl && imgUrl.trim() !== '') {
|
||||
images.push({
|
||||
imgId: imgId || `img-${reviewIndex}-${imgIndex}`,
|
||||
@@ -322,12 +393,12 @@ const useReviews = (prdtId) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('[useReviews] 이미지 데이터 추출 완료:', {
|
||||
totalImages: images.length,
|
||||
sampleImages: images.slice(0, 3)
|
||||
});
|
||||
|
||||
|
||||
return images;
|
||||
}, [getReviewsWithImages]);
|
||||
|
||||
@@ -344,19 +415,19 @@ const useReviews = (prdtId) => {
|
||||
});
|
||||
return allReviews.slice(0, 5);
|
||||
}, [allReviews, prdtId, hasLoadedData, isLoading, reviewData]);
|
||||
|
||||
|
||||
// 통계 정보
|
||||
const stats = useMemo(() => {
|
||||
return {
|
||||
totalReviews: allReviews.length, // 전체 리뷰 개수
|
||||
totalReviews: (reviewDetail && reviewDetail.totRvwCnt) ? reviewDetail.totRvwCnt : allReviews.length, // 전체 리뷰 개수
|
||||
filteredCount: filteredReviews.length, // 필터링된 리뷰 개수
|
||||
displayedCount: displayReviews.length, // 현재 표시 중인 리뷰 개수
|
||||
averageRating: (reviewDetail && reviewDetail.totRvwAvg) ? reviewDetail.totRvwAvg :
|
||||
averageRating: (reviewDetail && reviewDetail.totRvwAvg) ? reviewDetail.totRvwAvg :
|
||||
(reviewDetail && reviewDetail.avgRvwScr) ? reviewDetail.avgRvwScr : 0,
|
||||
totalRatingCount: (reviewDetail && reviewDetail.totRvwCnt) ? reviewDetail.totRvwCnt : allReviews.length
|
||||
};
|
||||
}, [allReviews.length, filteredReviews.length, displayReviews.length, reviewDetail]);
|
||||
|
||||
|
||||
// 데이터 새로고침 - 강제로 다시 로드
|
||||
const refreshData = useCallback(() => {
|
||||
console.log('[useReviews] 데이터 새로고침 시작');
|
||||
@@ -365,45 +436,55 @@ const useReviews = (prdtId) => {
|
||||
setCurrentFilter({ type: 'rating', value: 'all' }); // 기본 필터로 초기화
|
||||
loadReviews();
|
||||
}, [loadReviews]);
|
||||
|
||||
|
||||
return {
|
||||
// 🔥 핵심 API 함수 - useReviews가 모든 API 호출 담당
|
||||
loadReviews, // 리뷰 데이터 로드 (prdtId 기반)
|
||||
refreshData, // 데이터 강제 새로고침
|
||||
|
||||
|
||||
// 📊 리뷰 데이터
|
||||
displayReviews, // 현재 화면에 표시할 리뷰들 (청킹된)
|
||||
displayReviews, // 현재 화면에 표시할 리뷰들 (청킹된) - 기존 컴포넌트용
|
||||
previewReviews, // DetailPanel용 미리보기 리뷰 (첫 5개)
|
||||
allReviews, // 전체 원본 리뷰 (필터링 안된, 빈 내용 제외)
|
||||
filteredReviews, // 필터링된 전체 리뷰
|
||||
hasReviews: allReviews.length > 0, // 리뷰 존재 여부 (DetailPanel 조건부 렌더링용)
|
||||
|
||||
hasReviews: allReviews.length > 0 && hasLoadedData && !isLoading && isCurrentProductLoaded, // 리뷰 존재 여부 (현재 제품이 로드된 경우에만 true)
|
||||
|
||||
// 🔄 UserReviewPanel 전용 페이징 데이터
|
||||
userReviewPanelReviews, // UserReviewPanel용 페이징된 리뷰들 (3개 표시, 1개씩 이동)
|
||||
userReviewPanelPage, // 현재 페이지 번호 (0부터 시작)
|
||||
userReviewPanelHasNext, // 다음 페이지 존재 여부
|
||||
userReviewPanelHasPrev, // 이전 페이지 존재 여부
|
||||
userReviewPanelTotalPages, // 전체 페이지 수
|
||||
goToNextUserReviewPage, // 다음 페이지로 이동
|
||||
goToPrevUserReviewPage, // 이전 페이지로 이동
|
||||
|
||||
// 🖼️ 이미지 관련 데이터 - CustomerImages 전용
|
||||
getReviewsWithImages, // 이미지가 있는 리뷰들만 필터링
|
||||
extractImagesFromReviews, // 이미지 데이터만 추출 (reviewData 포함)
|
||||
|
||||
|
||||
// 📄 클라이언트 사이드 페이지네이션
|
||||
hasMore, // 더 로드할 리뷰가 있는지
|
||||
loadMore, // 다음 청크 표시 (클라이언트에서 슬라이싱)
|
||||
currentPage, // 현재 페이지 (0부터 시작)
|
||||
|
||||
|
||||
// 🔍 필터링 시스템
|
||||
currentFilter, // 현재 활성화된 필터 { type, value }
|
||||
filterCounts, // 각 필터별 리뷰 개수 (실시간 계산)
|
||||
applyFilter, // 통합 필터 적용 함수
|
||||
applyRatingFilter, // 별점 필터 적용
|
||||
applyKeywordFilter, // 키워드 필터 적용
|
||||
applyKeywordFilter, // 키워드 필터 적용
|
||||
applySentimentFilter, // 감정 필터 적용
|
||||
clearAllFilters, // 모든 필터 초기화
|
||||
|
||||
|
||||
// ⚡ 상태 관리
|
||||
isLoading, // API 로딩 상태
|
||||
hasLoadedData, // 데이터 로드 완료 여부
|
||||
stats, // 통계 정보
|
||||
|
||||
|
||||
// 🐛 디버그 정보
|
||||
_debug: {
|
||||
prdtId,
|
||||
patnrId,
|
||||
allReviews: allReviews.slice(0, 3), // 처음 3개만
|
||||
currentFilter,
|
||||
filteredCount: filteredReviews.length,
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function ProductAllSection({
|
||||
stats,
|
||||
isLoading: reviewsLoading,
|
||||
hasReviews, // 리뷰 존재 여부 플래그 추가
|
||||
} = useReviews(productData.prdtId);
|
||||
} = useReviews(productData.prdtId, panelInfo && panelInfo.patnrId);
|
||||
|
||||
// YouMayAlsoLike 데이터 확인
|
||||
const youmaylikeProductData = useSelector(
|
||||
@@ -517,7 +517,7 @@ export default function ProductAllSection({
|
||||
onClick={handleUserReviewsClick}
|
||||
spotlightId="user-reviews-button"
|
||||
>
|
||||
{$L("USER REVIEWS")}
|
||||
{$L("USER REVIEWS")} ({reviewTotalCount})
|
||||
</TButton>
|
||||
)}
|
||||
{hasYouMayAlsoLike && (
|
||||
|
||||
@@ -10,7 +10,7 @@ const SpottableComponent = Spottable("div");
|
||||
|
||||
const ShowUserReviews = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
// Redux에서 리뷰 데이터와 제품 데이터 가져오기
|
||||
const reviewListData = useSelector(
|
||||
(state) => state.product.reviewData && state.product.reviewData.reviewList
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import classNames from "classnames";
|
||||
|
||||
@@ -15,25 +15,41 @@ import css from "./UserReviewPanel.module.less";
|
||||
|
||||
const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
const dispatch = useDispatch();
|
||||
// panelInfo에서 prdtId 추출
|
||||
// panelInfo에서 prdtId와 patnrId 추출
|
||||
const prdtId = fp.pipe(
|
||||
() => panelInfo,
|
||||
fp.get('prdtId'),
|
||||
fp.defaultTo(null)
|
||||
)();
|
||||
|
||||
// useReviews hook 사용 - Single Filter 시스템
|
||||
const patnrId = fp.pipe(
|
||||
() => panelInfo,
|
||||
fp.get('patnrId'),
|
||||
fp.defaultTo(null)
|
||||
)();
|
||||
|
||||
// useReviews hook 사용 - UserReviewPanel 전용 페이징 포함 (patnrId 추가)
|
||||
const {
|
||||
previewReviews,
|
||||
displayReviews,
|
||||
filteredReviews,
|
||||
userReviewPanelReviews, // 페이징된 리뷰들 (3개씩)
|
||||
userReviewPanelPage, // 현재 페이지
|
||||
userReviewPanelHasNext, // 다음 페이지 존재 여부
|
||||
userReviewPanelHasPrev, // 이전 페이지 존재 여부
|
||||
userReviewPanelTotalPages, // 전체 페이지 수
|
||||
goToNextUserReviewPage, // 다음 페이지로 이동
|
||||
goToPrevUserReviewPage, // 이전 페이지로 이동
|
||||
applyRatingFilter,
|
||||
applySentimentFilter,
|
||||
clearAllFilters,
|
||||
currentFilter,
|
||||
filterCounts,
|
||||
stats
|
||||
} = useReviews(prdtId);
|
||||
} = useReviews(prdtId, patnrId);
|
||||
|
||||
// 포커스 복원을 위한 ref
|
||||
const lastFocusedReviewIndex = useRef(0);
|
||||
|
||||
// Redux에서 제품 데이터 가져오기 (기존 유지)
|
||||
const productData = useSelector((state) => state.main.productData || {});
|
||||
@@ -82,11 +98,33 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
fp.defaultTo('상품명 정보가 없습니다')
|
||||
)();
|
||||
|
||||
// 필터링된 리뷰에서 처음 4개만 사용 (UserReviewPanel용)
|
||||
const userReviewPanelReviews = fp.pipe(
|
||||
() => filteredReviews || [],
|
||||
(reviews) => reviews.slice(0, 4)
|
||||
)();
|
||||
// 페이징 후 포커스 복원 함수 - 중간 리뷰(index 1)로 포커스
|
||||
const restoreFocusAfterPaging = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
const targetElement = document.querySelector(`[data-spotlight-id="user-review-1"]`);
|
||||
if (targetElement && targetElement.focus) {
|
||||
targetElement.focus();
|
||||
console.log('[UserReviewPanel] 중간 리뷰로 포커스 복원 완료');
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 개선된 페이징 함수들
|
||||
const handleNextPage = useCallback(() => {
|
||||
if (userReviewPanelHasNext) {
|
||||
console.log('[UserReviewPanel] 다음 페이지로 이동');
|
||||
goToNextUserReviewPage();
|
||||
restoreFocusAfterPaging();
|
||||
}
|
||||
}, [userReviewPanelHasNext, goToNextUserReviewPage, restoreFocusAfterPaging]);
|
||||
|
||||
const handlePrevPage = useCallback(() => {
|
||||
if (userReviewPanelHasPrev) {
|
||||
console.log('[UserReviewPanel] 이전 페이지로 이동');
|
||||
goToPrevUserReviewPage();
|
||||
restoreFocusAfterPaging();
|
||||
}
|
||||
}, [userReviewPanelHasPrev, goToPrevUserReviewPage, restoreFocusAfterPaging]);
|
||||
|
||||
// 통계 정보 - stats에서 가져오기
|
||||
const reviewCount = stats.totalReviews || 0; // 전체 리뷰 개수
|
||||
@@ -167,10 +205,10 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
|
||||
return (
|
||||
<TPanel
|
||||
isTabActivated={false}
|
||||
className={classNames(css.userReviewPanel, className)}
|
||||
handleCancel={handleCancel}
|
||||
spotlightId={spotlightId}
|
||||
tabIndex={0}
|
||||
isTabActivated={false}
|
||||
>
|
||||
<UserReviewHeader
|
||||
title="User Reviews"
|
||||
@@ -233,7 +271,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
</div>
|
||||
<div className={css.reviewsSection__filters__group}>
|
||||
<FilterItemButton
|
||||
text={`All star(${filterCounts?.rating?.all || reviewCount || 0})`}
|
||||
text={`All stars(${filterCounts?.rating?.all || reviewCount || 0})`}
|
||||
onClick={handleAllStarsFilter}
|
||||
spotlightId="filter-all-stars"
|
||||
ariaLabel="Filter by all star ratings"
|
||||
@@ -241,7 +279,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 'all'}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`5 star (${filterCounts?.rating?.[5] || 0})`}
|
||||
text={`5 stars (${filterCounts?.rating?.[5] || 0})`}
|
||||
onClick={handle5StarsFilter}
|
||||
spotlightId="filter-5-stars"
|
||||
ariaLabel="Filter by 5 star ratings"
|
||||
@@ -250,7 +288,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 5}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`4 star (${filterCounts?.rating?.[4] || 0})`}
|
||||
text={`4 stars (${filterCounts?.rating?.[4] || 0})`}
|
||||
onClick={handle4StarsFilter}
|
||||
spotlightId="filter-4-stars"
|
||||
ariaLabel="Filter by 4 star ratings"
|
||||
@@ -259,7 +297,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 4}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`3 star (${filterCounts?.rating?.[3] || 0})`}
|
||||
text={`3 stars (${filterCounts?.rating?.[3] || 0})`}
|
||||
onClick={handle3StarsFilter}
|
||||
spotlightId="filter-3-stars"
|
||||
ariaLabel="Filter by 3 star ratings"
|
||||
@@ -268,7 +306,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 3}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`2 star (${filterCounts?.rating?.[2] || 0})`}
|
||||
text={`2 stars (${filterCounts?.rating?.[2] || 0})`}
|
||||
onClick={handle2StarsFilter}
|
||||
spotlightId="filter-2-stars"
|
||||
ariaLabel="Filter by 2 star ratings"
|
||||
@@ -277,7 +315,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
isActive={currentFilter.type === 'rating' && currentFilter.value === 2}
|
||||
/>
|
||||
<FilterItemButton
|
||||
text={`1 star (${filterCounts?.rating?.[1] || 0})`}
|
||||
text={`1 stars (${filterCounts?.rating?.[1] || 0})`}
|
||||
onClick={handle1StarsFilter}
|
||||
spotlightId="filter-1-stars"
|
||||
ariaLabel="Filter by 1 star ratings"
|
||||
@@ -361,10 +399,16 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
|
||||
<UserReviewsList
|
||||
prdtId={prdtId}
|
||||
className={css.userReviewsList}
|
||||
reviewsData={userReviewPanelReviews} // 필터링된 처음 4개 리뷰
|
||||
reviewsData={userReviewPanelReviews} // 페이징된 3개 리뷰 (1개씩 이동)
|
||||
totalReviewCount={reviewCount} // 전체 리뷰 개수
|
||||
filteredReviewCount={filteredCount} // 필터링된 리뷰 개수
|
||||
currentFilter={currentFilter} // 현재 적용된 필터
|
||||
currentPage={userReviewPanelPage} // 현재 페이지
|
||||
totalPages={userReviewPanelTotalPages} // 전체 페이지 수
|
||||
hasNext={userReviewPanelHasNext} // 다음 페이지 존재 여부
|
||||
hasPrev={userReviewPanelHasPrev} // 이전 페이지 존재 여부
|
||||
onNextPage={handleNextPage} // 다음 페이지 핸들러
|
||||
onPrevPage={handlePrevPage} // 이전 페이지 핸들러
|
||||
showAllReviews
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,10 @@ const UserReviewItem = ({
|
||||
review,
|
||||
index,
|
||||
isLastReview = false,
|
||||
hasNext = false,
|
||||
hasPrev = false,
|
||||
onNextPage,
|
||||
onPrevPage,
|
||||
onClick
|
||||
}) => {
|
||||
const handleReviewClick = useCallback(() => {
|
||||
@@ -22,6 +26,28 @@ const UserReviewItem = ({
|
||||
}
|
||||
}, [onClick, review, index]);
|
||||
|
||||
// 키보드 이벤트 핸들러 - 조건부 페이징 처리
|
||||
const handleKeyDown = useCallback((event) => {
|
||||
// 첫번째 리뷰(index 0)에서 위 화살표 처리
|
||||
if (event.key === 'ArrowUp' && index === 0) {
|
||||
if (hasPrev && onPrevPage) {
|
||||
console.log('[UserReviewItem] 첫번째 리뷰에서 위 화살표 - 이전 페이지');
|
||||
onPrevPage();
|
||||
}
|
||||
event.preventDefault(); // 페이징 여부와 상관없이 포커스 사라짐 방지
|
||||
event.stopPropagation();
|
||||
}
|
||||
// 세번째 리뷰(index 2)에서 아래 화살표 처리
|
||||
else if (event.key === 'ArrowDown' && index === 2) {
|
||||
if (hasNext && onNextPage) {
|
||||
console.log('[UserReviewItem] 세번째 리뷰에서 아래 화살표 - 다음 페이지');
|
||||
onNextPage();
|
||||
}
|
||||
event.preventDefault(); // 페이징 여부와 상관없이 포커스 사라짐 방지
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, [index, hasNext, hasPrev, onNextPage, onPrevPage]);
|
||||
|
||||
// 날짜 포맷팅 함수
|
||||
const formatToYYMMDD = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
@@ -45,7 +71,9 @@ const UserReviewItem = ({
|
||||
aria-label={`user-review-item-${rvwId}`}
|
||||
className={css.reviewContentContainer}
|
||||
onClick={handleReviewClick}
|
||||
spotlightId={isLastReview ? 'user-review-at-last' : `user-review-${index}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
spotlightId={`user-review-${index}`}
|
||||
data-spotlight-id={`user-review-${index}`}
|
||||
>
|
||||
{/* 리뷰 이미지 */}
|
||||
{reviewImageList && reviewImageList.length > 0 && (
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
// Light theme 리뷰 리스트 컴포넌트
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
|
||||
import { $L } from '../../../utils/helperMethods';
|
||||
import UserReviewItem from './UserReviewItem';
|
||||
import css from './UserReviewsList.module.less';
|
||||
import UserReviewsScroller from './UserReviewsScroller';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
|
||||
import UserReviewItem from "./UserReviewItem";
|
||||
import UserReviewsScroller from "./UserReviewsScroller";
|
||||
import { $L } from "../../../utils/helperMethods";
|
||||
import css from "./UserReviewsList.module.less";
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "default-element",
|
||||
preserveId: true,
|
||||
leaveFor: {
|
||||
left: "filter-all-stars",
|
||||
left: "filter-all-stars"
|
||||
},
|
||||
restrict: "none",
|
||||
spotlightDirection: "vertical",
|
||||
spotlightDirection: "vertical"
|
||||
},
|
||||
"div"
|
||||
);
|
||||
@@ -33,11 +26,15 @@ const Container = SpotlightContainerDecorator(
|
||||
const UserReviewsList = ({
|
||||
prdtId,
|
||||
className,
|
||||
reviewsData = [], // UserReviewPanel에서 전달받은 필터링된 처음 4개 리뷰
|
||||
reviewsData = [], // UserReviewPanel에서 전달받은 필터링된 리뷰들
|
||||
totalReviewCount = 0, // 전체 리뷰 개수
|
||||
filteredReviewCount = 0, // 필터링된 리뷰 개수
|
||||
currentFilter = { type: "rating", value: "all" }, // Single Filter 구조
|
||||
showAllReviews = true,
|
||||
currentFilter = { type: 'rating', value: 'all' }, // Single Filter 구조
|
||||
hasNext = false, // 다음 페이지 존재 여부
|
||||
hasPrev = false, // 이전 페이지 존재 여부
|
||||
onNextPage, // 다음 페이지 핸들러
|
||||
onPrevPage, // 이전 페이지 핸들러
|
||||
showAllReviews = true
|
||||
}) => {
|
||||
const [selectedReviewIndex, setSelectedReviewIndex] = useState(null);
|
||||
|
||||
@@ -46,7 +43,7 @@ const UserReviewsList = ({
|
||||
console.log("[UserReviewsList] Review item clicked:", {
|
||||
rvwId: review.rvwId,
|
||||
index: index,
|
||||
review: review,
|
||||
review: review
|
||||
});
|
||||
setSelectedReviewIndex(index);
|
||||
}, []);
|
||||
@@ -62,15 +59,9 @@ const UserReviewsList = ({
|
||||
hasData: reviewsData && reviewsData.length > 0,
|
||||
prdtId: prdtId,
|
||||
dataSource: "props", // Redux가 아닌 props에서 받음
|
||||
isFiltered: currentFilter.value !== "all",
|
||||
isFiltered: currentFilter.value !== 'all'
|
||||
});
|
||||
}, [
|
||||
reviewsData,
|
||||
totalReviewCount,
|
||||
filteredReviewCount,
|
||||
currentFilter,
|
||||
prdtId,
|
||||
]);
|
||||
}, [reviewsData, totalReviewCount, filteredReviewCount, currentFilter, prdtId]);
|
||||
|
||||
return (
|
||||
<Container className={css.reviewsListContainer}>
|
||||
@@ -78,26 +69,22 @@ const UserReviewsList = ({
|
||||
<div className={css.reviewsListHeader}>
|
||||
<div className={css.reviewsListHeaderText}>
|
||||
<span className={css.reviewsListHeaderCount}>
|
||||
{currentFilter.value !== "all"
|
||||
? filteredReviewCount
|
||||
: totalReviewCount}
|
||||
</span>{" "}
|
||||
Customer Reviews
|
||||
{currentFilter.value !== 'all' ? filteredReviewCount : totalReviewCount}
|
||||
</span> Customer Reviews
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리뷰 스크롤러 */}
|
||||
<UserReviewsScroller className={css.reviewsScroller}>
|
||||
{/*
|
||||
<div className={css.showReviewsText}>
|
||||
{$L(
|
||||
currentFilter.value !== 'all' ?
|
||||
`Showing ${reviewsData ? reviewsData.length : 0} out of ${filteredReviewCount} filtered reviews` :
|
||||
`Showing ${reviewsData ? reviewsData.length : 0} out of ${totalReviewCount} reviews`
|
||||
)}
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* 리뷰 아이템들 - props로 받은 데이터 사용 (이미 4개로 제한됨) */}
|
||||
{/* 리뷰 아이템들 - props로 받은 데이터 사용 */}
|
||||
<div className={css.reviewItems}>
|
||||
{reviewsData && reviewsData.length > 0 ? (
|
||||
reviewsData.map((review, index, array) => {
|
||||
@@ -108,15 +95,17 @@ const UserReviewsList = ({
|
||||
review={review}
|
||||
index={index}
|
||||
isLastReview={isLastReview}
|
||||
hasNext={hasNext}
|
||||
hasPrev={hasPrev}
|
||||
onNextPage={onNextPage}
|
||||
onPrevPage={onPrevPage}
|
||||
onClick={handleReviewClick}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={css.noReviews}>
|
||||
{totalReviewCount === 0
|
||||
? "No reviews available"
|
||||
: "Loading reviews..."}
|
||||
{totalReviewCount === 0 ? 'No reviews available' : 'Loading reviews...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user