Files
shoptime/com.twin.app.shoptime/src/hooks/useReviews/useReviews.js
optrader 7da55ea1ae [251124] fix: PlayerPanel,VideoPlayer 최적화-6
🕐 커밋 시간: 2025. 11. 24. 19:23:39

📊 변경 통계:
  • 총 파일: 8개
  • 추가: +142줄
  • 삭제: -31줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/playActions.js
  ~ com.twin.app.shoptime/src/components/MediaItem/MediaItem.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaTitle.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js
  ~ com.twin.app.shoptime/src/hooks/useReviews/useReviews.js
  ~ com.twin.app.shoptime/src/utils/helperMethods.js
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • 공통 유틸리티 함수 최적화
  • 중간 규모 기능 개선
  • 모듈 구조 개선
2025-11-24 19:23:41 +09:00

855 lines
29 KiB
JavaScript

import { useState, useMemo, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getUserReviewList, getReviewFilters, clearReviewFilter } from '../../actions/productActions';
import fp from '../../utils/fp';
const DISPLAY_SIZE = 3; // 화면에 표시할 리뷰 개수
const STEP_SIZE = 1; // 페이징 시 이동할 리뷰 개수
// 🔧 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 상태에서 리뷰 데이터 가져오기 - 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;
});
// Redux 상태에서 필터 데이터 가져오기 (IF-LGSP-100)
const reviewFiltersData = useSelector((state) => {
const data = state.product.reviewFiltersData;
// console.log('[useReviews_reviewFilters] 📥 Redux reviewFiltersData 선택:', {
// dataExists: !!data,
// dataKeys: data ? Object.keys(data) : 'null',
// filtersLength: data?.filters?.length || 0,
// filters: data?.filters
// });
return data;
});
const loadedFiltersPrdtId = useSelector((state) => state.product.loadedFiltersPrdtId);
// Redux 상태에서 필터링된 리뷰 데이터 가져오기
const filteredReviewListData = useSelector((state) => state.product.filteredReviewListData);
const currentReviewFilter = useSelector((state) => state.product.currentReviewFilter);
// console.log('[useReviews_filteredReviewList] 📥 Redux filteredReviewListData 선택:', {
// filteredReviewListDataExists: !!filteredReviewListData,
// filteredReviewListDataKeys: filteredReviewListData ? Object.keys(filteredReviewListData) : 'null',
// reviewListLength: filteredReviewListData?.reviewList?.length || 0,
// currentReviewFilter
// });
// 활성 리뷰 데이터 결정 (필터링된 데이터가 있으면 사용, 없으면 ALL 데이터 사용)
const activeReviewData = useMemo(() => {
if (filteredReviewListData) {
// console.log('[useReviews] 🟢 activeReviewData: filteredReviewListData 사용');
return filteredReviewListData;
}
// console.log('[useReviews] 🟢 activeReviewData: reviewListData (ALL) 사용');
return reviewData;
}, [filteredReviewListData, reviewData]);
// 빈 내용 리뷰 필터링 - 의미있는 리뷰만 표시
const allReviews = (activeReviewData?.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({
type: 'rating', // 'rating', 'keyword', 'sentiment'
value: 'all', // 'all', 1-5, 'positive', 'negative', 'aroma' 등
});
const [isLoading, setIsLoading] = useState(false);
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);
// 리뷰 데이터 로드 함수 - reviewVersion에 따라 API 선택
const loadReviews = useCallback(async () => {
if (!prdtId) {
// console.warn('[useReviews] loadReviews 호출되었지만 prdtId가 없음');
return;
}
if (!patnrId) {
// console.warn('[useReviews] loadReviews 호출되었지만 patnrId가 없음');
return;
}
setIsLoading(true);
// console.log('[useReviews] 📋 loadReviews 호출됨:', {
// prdtId,
// patnrId,
// reviewVersion,
// willCallApi: reviewVersion === 1 ? 'getUserReviews (v1)' : 'getUserReviewList (v2)'
// });
try {
// 신 API 호출 (v2)
// console.log('[useReviews] 🔄 getUserReviewList 호출 중... (v2)');
await dispatch(getUserReviewList({
prdtId,
patnrId,
filterTpCd: 'ALL',
pageSize: 100,
pageNo: 1
}));
// IF-LGSP-100 필터 데이터 조회
// console.log('[useReviews] 🔄 getReviewFilters 호출 중... (IF-LGSP-100)');
await dispatch(getReviewFilters({
prdtId,
patnrId
}));
setHasLoadedData(true);
} catch (error) {
console.error('[useReviews] loadReviews 실패:', error);
} finally {
setIsLoading(false);
}
}, [prdtId, patnrId, reviewVersion, dispatch]);
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
// 중요: reviewVersion이 다르면 다른 Redux 상태를 사용하므로 reviewVersion도 체크해야 함
// 우선순위 3: 필터 캐싱 로직 완성
useEffect(() => {
if (prdtId && patnrId) {
const needsReviewLoad = prdtId !== loadedPrdtId;
const needsFiltersLoad = prdtId !== loadedFiltersPrdtId;
// console.log('[useReviews] 🔄 로드 필요 여부 체크 (캐싱 로직 포함):', {
// prdtId,
// patnrId,
// loadedPrdtId,
// loadedFiltersPrdtId,
// reviewVersion,
// needsReviewLoad,
// needsFiltersLoad,
// reason: (needsReviewLoad || needsFiltersLoad) ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
// });
if (needsReviewLoad || needsFiltersLoad) {
// prdtId 변경 시 로딩 상태 즉시 설정으로 UI 깜빡임 방지
setIsLoading(true);
setHasLoadedData(false);
loadReviews();
} else {
// 캐시된 데이터 사용
setHasLoadedData(true);
}
}
}, [prdtId, patnrId, loadedPrdtId, loadedFiltersPrdtId, loadReviews]);
// ✅ CRITICAL FIX: prdtId가 변경되면 Redux의 필터 상태를 초기화
// 이전 상품의 필터가 새로운 상품에 잘못 적용되는 문제를 해결
useEffect(() => {
if (prdtId && currentReviewFilter) {
const hasPrdtIdChanged = prdtId !== loadedPrdtId;
if (hasPrdtIdChanged) {
console.log('[useReviews] 🔄 prdtId 변경 감지 - 필터 초기화:', {
previousPrdtId: loadedPrdtId,
newPrdtId: prdtId,
clearedFilter: currentReviewFilter,
reason: '새로운 상품으로 전환되었으므로 이전 필터 제거'
});
// Redux의 필터 상태 초기화
dispatch(clearReviewFilter());
}
}
}, [prdtId, loadedPrdtId, currentReviewFilter, dispatch]);
// 리뷰 데이터가 로드되면 로딩 상태 업데이트
useEffect(() => {
if (allReviews.length > 0 && isLoading) {
setIsLoading(false);
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',
'positive',
'happy',
'satisfied',
'pleased',
'delightful',
'enjoyed',
'recommend',
'nice',
'helpful',
'valuable',
'impressive',
'remarkable',
'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',
'unhappy',
'unsatisfied',
'frustrating',
'annoying',
'regret',
'slow',
'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) {
return {
rating: { all: 0 },
keyword: {},
sentiment: {},
};
}
// 별점이 없는 리뷰들 찾기
const noStarReviews = allReviews.filter((review) => {
const rating = review.rvwScr || review.rvwRtng || review.rating;
return !rating || rating === 0;
});
// 별점이 없는 리뷰들 로그 출력
// if (noStarReviews.length > 0) {
// console.log('[UserReviews]-NoStar 별점 없는 리뷰들:', {
// totalReviews: allReviews.length,
// noStarCount: noStarReviews.length,
// noStarReviews: noStarReviews.map((review) => ({
// rvwId: review.rvwId,
// rvwScr: review.rvwScr,
// rvwRtng: review.rvwRtng,
// rating: review.rating,
// rvwCtnt: review.rvwCtnt?.substring(0, 50) + '...',
// wrtrNknm: review.wrtrNknm,
// })),
// });
// }
// 별점이 있는 리뷰만 카운트 (1~5점)
let ratingSum = 0;
for (let i = 1; i <= 5; i++) {
const count = allReviews.filter((review) => {
const rating = review.rvwScr || review.rvwRtng || review.rating || 0;
return Math.round(rating) === i;
}).length;
ratingSum += count;
}
const counts = {
rating: { all: ratingSum }, // 별점이 있는 리뷰만 카운트
keyword: {},
sentiment: {},
};
// 별점별 카운트 - 소수점 별점을 정수로 반올림하여 카운팅
for (let i = 1; i <= 5; i++) {
counts.rating[i] = allReviews.filter((review) => {
const rating = review.rvwScr || review.rvwRtng || review.rating || 0;
return Math.round(rating) === i;
}).length;
}
// 키워드별 카운트
const keywords = ['aroma', 'vanilla', 'cinnamon', 'quality'];
keywords.forEach((keyword) => {
counts.keyword[keyword] = allReviews.filter((review) =>
matchesKeyword(review, keyword)
).length;
});
// 감정별 카운트
counts.sentiment.positive = allReviews.filter((review) =>
matchesSentiment(review, 'positive')
).length;
counts.sentiment.negative = allReviews.filter((review) =>
matchesSentiment(review, 'negative')
).length;
// 디버깅: filterCounts 계산 결과 확인
// console.log('[useReviews] filterCounts 계산 완료:', {
// totalReviews: allReviews.length,
// ratingCounts: counts.rating,
// keywordCounts: counts.keyword,
// sentimentCounts: counts.sentiment,
// sampleReviews: allReviews.slice(0, 3).map(review => ({
// rvwId: review.rvwId,
// rvwScr: review.rvwScr,
// rvwRtng: review.rvwRtng,
// rating: review.rating,
// 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) {
// 'all' 필터: 별점이 있는 리뷰만 표시 (별점 없는 리뷰 제외)
result = allReviews.filter((review) => {
const rating = review.rvwScr || review.rvwRtng || review.rating;
return rating && rating > 0;
});
} else {
result = allReviews.filter((review) => {
const rating = review.rvwScr || review.rvwRtng || review.rating || 0;
return Math.round(rating) === currentFilter.value;
});
}
break;
case 'sentiment':
result = allReviews.filter((review) => matchesSentiment(review, currentFilter.value));
break;
case 'keyword':
result = allReviews.filter((review) => matchesKeyword(review, currentFilter.value));
break;
default:
result = [...allReviews];
}
// 불변성 보장 및 메모리 최적화
return Object.freeze(result);
}, [allReviews, currentFilter.type, currentFilter.value, matchesKeyword, matchesSentiment]);
// 현재 화면에 표시할 리뷰들 (항상 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) {
// console.log('[useReviews] loadMore: 다음 청크 로드', {
// currentPage,
// nextPage: currentPage + 1,
// currentDisplayCount: displayReviews.length,
// totalFilteredCount: filteredReviews.length
// });
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 페이지도 초기화
// ✅ 클라이언트 필터 + API 필터 모두 감시
useEffect(() => {
console.log('[useReviews] 필터 변경 감지 - userReviewPanelPage 초기화:', {
filterType: currentFilter.type,
filterValue: currentFilter.value,
apiFilter: currentReviewFilter,
});
setUserReviewPanelPage(0);
}, [currentFilter.type, currentFilter.value, currentReviewFilter]);
// 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.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}`,
imgUrl,
imgSeq: imgSeq || imgIndex + 1,
reviewId: review.rvwId,
reviewData: review, // 전체 리뷰 데이터도 포함
});
}
});
}
});
// console.log('[useReviews] 이미지 데이터 추출 완료:', {
// totalImages: images.length,
// sampleImages: images.slice(0, 3)
// });
return images;
}, [getReviewsWithImages]);
// DetailPanel용 - 처음 5개만 가져오기 (필터링 안된 원본에서)
const previewReviews = useMemo(() => {
// console.log('[useReviews] previewReviews 계산:', {
// allReviewsLength: allReviews.length,
// previewCount: Math.min(allReviews.length, 5),
// prdtId,
// hasLoadedData,
// isLoading,
// reviewData,
// allReviews: allReviews.slice(0, 3)
// });
return allReviews.slice(0, 5);
}, [allReviews, prdtId, hasLoadedData, isLoading, reviewData]);
// 통계 정보
// ✅ 핵심: 필터링 여부에 따라 다른 데이터 사용
const stats = useMemo(() => {
// 필터링 활성 여부 확인
const isFilterActive = !!filteredReviewListData && !!currentReviewFilter;
// 초기 로드된 리뷰 데이터의 reviewDetail (ALL 필터 기반)
const initialReviewDetail = reviewData?.reviewDetail || {};
// 필터링된 리뷰 데이터의 reviewDetail (필터 적용 후)
const filteredReviewDetail = filteredReviewListData?.reviewDetail || {};
// ⭐ 사용할 reviewDetail 선택
// - 필터 미적용: initialReviewDetail (전체 리뷰 기반)
// - 필터 적용: filteredReviewDetail (필터링된 리뷰 기반)
const activeReviewDetail = isFilterActive ? filteredReviewDetail : initialReviewDetail;
const totalReviewsCount = activeReviewDetail?.totRvwCnt || allReviews.length;
const averageRating = activeReviewDetail?.totRvwAvg || 0;
const result = {
// ✅ 필터 미적용: 전체 리뷰 개수
// ✅ 필터 적용: 필터링된 리뷰 개수 (API의 totRvwCnt)
totalReviews: totalReviewsCount,
filteredCount: filteredReviews.length,
displayedCount: displayReviews.length,
// ✅ 필터 미적용: 전체 평균 평점
// ✅ 필터 적용: 필터링된 리뷰의 평균 평점
averageRating: averageRating,
totalRatingCount: totalReviewsCount,
// 🔍 디버깅용: 필터링 상태
_isFilterActive: isFilterActive,
_activeSource: isFilterActive ? 'filteredReviewDetail' : 'initialReviewDetail'
};
// console.log('[useReviews_useReviewList] 📊 stats 계산:', {
// isFilterActive,
// totalReviewsCount,
// averageRating,
// activeSource: isFilterActive ? 'filteredReviewDetail' : 'initialReviewDetail',
// initialReviewDetail: {
// totRvwCnt: initialReviewDetail.totRvwCnt,
// totRvwAvg: initialReviewDetail.totRvwAvg
// },
// filteredReviewDetail: {
// totRvwCnt: filteredReviewDetail.totRvwCnt,
// totRvwAvg: filteredReviewDetail.totRvwAvg
// },
// allReviewsLength: allReviews.length,
// prdtId
// });
return result;
}, [allReviews.length, filteredReviews.length, displayReviews.length, reviewData, filteredReviewListData, currentReviewFilter, reviewDetail, hasLoadedData, isLoading, isCurrentProductLoaded, reviewVersion, loadedPrdtId, prdtId]);
// 데이터 새로고침 - 강제로 다시 로드
const refreshData = useCallback(() => {
// console.log('[useReviews] 데이터 새로고침 시작');
setHasLoadedData(false); // 강제로 다시 로드하도록
setCurrentPage(0);
setCurrentFilter({ type: 'rating', value: 'all' }); // 기본 필터로 초기화
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 기반)
refreshData, // 데이터 강제 새로고침
// 📊 리뷰 데이터
displayReviews, // 현재 화면에 표시할 리뷰들 (청킹된) - 기존 컴포넌트용
previewReviews, // DetailPanel용 미리보기 리뷰 (첫 5개)
allReviews, // 전체 원본 리뷰 (필터링 안된, 빈 내용 제외)
filteredReviews, // 필터링된 전체 리뷰
hasReviews, // 리뷰 존재 여부 (현재 제품이 로드된 경우에만 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, // 키워드 필터 적용
applySentimentFilter, // 감정 필터 적용
clearAllFilters, // 모든 필터 초기화
// ⚡ 상태 관리
isLoading, // API 로딩 상태
hasLoadedData, // 데이터 로드 완료 여부
stats, // 통계 정보
// 🔤 필터 옵션 데이터 (IF-LGSP-100)
reviewFiltersData, // 필터 옵션 전체 데이터
filters: reviewFiltersData?.filters || [], // 필터 배열 (RATING, KEYWORDS, SENTIMENT)
// 🎯 필터링된 리뷰 데이터 (API 기반 필터링)
filteredReviewListData, // API에서 받은 필터링된 리뷰 데이터
currentReviewFilter, // 현재 활성화된 필터 { filterTpCd, filterTpVal }
activeReviewData, // 활성 리뷰 데이터 (filteredReviewListData 또는 reviewListData)
// 🐛 디버그 정보
_debug: {
prdtId,
patnrId,
reviewVersion,
allReviews: allReviews.slice(0, 3), // 처음 3개만
currentFilter,
filteredCount: filteredReviews.length,
displayedCount: displayReviews.length,
currentPage,
hasMore,
isLoading,
hasLoadedData,
reviewFiltersData,
filteredReviewListData,
currentReviewFilter,
activeReviewData,
},
};
};
export default useReviews;