🕐 커밋 시간: 2025. 10. 19. 21:45:39 📊 변경 통계: • 총 파일: 11개 • 추가: +119줄 • 삭제: -101줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/productActions.js ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.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/ProductContentSection/UserReviews/UserReviews.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabButton/PlayerTabButton.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/FeaturedShowContents.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/actions/productActions.js (javascript): 🔄 Modified: resetShowAllReviews() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx (javascript): 🔄 Modified: SpotlightContainerDecorator() 📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabButton/PlayerTabButton.jsx (javascript): ❌ Deleted: handleTabOnClick() 📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx (javascript): 🔄 Modified: Spottable() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선
634 lines
21 KiB
JavaScript
634 lines
21 KiB
JavaScript
import { useState, useMemo, useEffect, useCallback } from 'react';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import { getUserReviews } from '../../actions/productActions';
|
|
import fp from '../../utils/fp';
|
|
|
|
const DISPLAY_SIZE = 3; // 화면에 표시할 리뷰 개수
|
|
const STEP_SIZE = 1; // 페이징 시 이동할 리뷰 개수
|
|
|
|
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({
|
|
type: 'rating', // 'rating', 'keyword', 'sentiment'
|
|
value: 'all', // 'all', 1-5, 'positive', 'negative', 'aroma' 등
|
|
});
|
|
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;
|
|
}
|
|
|
|
if (!patnrId) {
|
|
console.warn('[useReviews] loadReviews 호출되었지만 patnrId가 없음');
|
|
return;
|
|
}
|
|
|
|
// console.log('[useReviews] loadReviews 시작:', { prdtId, patnrId });
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Redux action을 통해 API 호출 - patnrId 추가
|
|
await dispatch(getUserReviews({ prdtId, patnrId }));
|
|
setHasLoadedData(true);
|
|
// console.log('[useReviews] loadReviews 완료');
|
|
} catch (error) {
|
|
console.error('[useReviews] loadReviews 실패:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [prdtId, patnrId, dispatch]);
|
|
|
|
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
|
|
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); // 캐시된 데이터 사용 시 로드 완료 상태로 설정
|
|
}
|
|
}, [prdtId, patnrId, loadedPrdtId, loadReviews]);
|
|
|
|
// 리뷰 데이터가 로드되면 로딩 상태 업데이트
|
|
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 페이지도 초기화
|
|
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.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(() => {
|
|
// API와 실제 리뷰 개수 불일치 확인
|
|
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 {
|
|
totalReviews: actualReviewCount, // 실제로 받은 리뷰 개수 사용 (API 제한 반영)
|
|
filteredCount: filteredReviews.length, // 필터링된 리뷰 개수
|
|
displayedCount: displayReviews.length, // 현재 표시 중인 리뷰 개수
|
|
averageRating:
|
|
reviewDetail && reviewDetail.totRvwAvg
|
|
? reviewDetail.totRvwAvg
|
|
: reviewDetail && reviewDetail.avgRvwScr
|
|
? reviewDetail.avgRvwScr
|
|
: 0,
|
|
totalRatingCount: actualReviewCount, // 실제로 받은 리뷰 개수 사용
|
|
};
|
|
}, [allReviews.length, filteredReviews.length, displayReviews.length, reviewDetail]);
|
|
|
|
// 데이터 새로고침 - 강제로 다시 로드
|
|
const refreshData = useCallback(() => {
|
|
// console.log('[useReviews] 데이터 새로고침 시작');
|
|
setHasLoadedData(false); // 강제로 다시 로드하도록
|
|
setCurrentPage(0);
|
|
setCurrentFilter({ type: 'rating', value: 'all' }); // 기본 필터로 초기화
|
|
loadReviews();
|
|
}, [loadReviews]);
|
|
|
|
return {
|
|
// 🔥 핵심 API 함수 - useReviews가 모든 API 호출 담당
|
|
loadReviews, // 리뷰 데이터 로드 (prdtId 기반)
|
|
refreshData, // 데이터 강제 새로고침
|
|
|
|
// 📊 리뷰 데이터
|
|
displayReviews, // 현재 화면에 표시할 리뷰들 (청킹된) - 기존 컴포넌트용
|
|
previewReviews, // DetailPanel용 미리보기 리뷰 (첫 5개)
|
|
allReviews, // 전체 원본 리뷰 (필터링 안된, 빈 내용 제외)
|
|
filteredReviews, // 필터링된 전체 리뷰
|
|
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, // 키워드 필터 적용
|
|
applySentimentFilter, // 감정 필터 적용
|
|
clearAllFilters, // 모든 필터 초기화
|
|
|
|
// ⚡ 상태 관리
|
|
isLoading, // API 로딩 상태
|
|
hasLoadedData, // 데이터 로드 완료 여부
|
|
stats, // 통계 정보
|
|
|
|
// 🐛 디버그 정보
|
|
_debug: {
|
|
prdtId,
|
|
patnrId,
|
|
allReviews: allReviews.slice(0, 3), // 처음 3개만
|
|
currentFilter,
|
|
filteredCount: filteredReviews.length,
|
|
displayedCount: displayReviews.length,
|
|
currentPage,
|
|
hasMore,
|
|
isLoading,
|
|
hasLoadedData,
|
|
},
|
|
};
|
|
};
|
|
|
|
export default useReviews;
|