🕐 커밋 시간: 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 컴포넌트 아키텍처 개선 • 공통 유틸리티 함수 최적화 • 중간 규모 기능 개선 • 모듈 구조 개선
855 lines
29 KiB
JavaScript
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;
|