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;