[251011] fix: ProudctAllSection scroller 리렌더링 최적화-3
🕐 커밋 시간: 2025. 10. 11. 22:04:23 📊 변경 통계: • 총 파일: 5개 • 추가: +123줄 • 삭제: -102줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/playActions.js ~ com.twin.app.shoptime/src/hooks/useReviews/useReviews.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx ~ com.twin.app.shoptime/src/views/UserReview/components/UserReviewItem.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx (javascript): 🔄 Modified: Spottable() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선
This commit is contained in:
@@ -31,7 +31,8 @@ export const startVideoPlayer =
|
||||
const topPanel = panels[panels.length - 1];
|
||||
let panelWorkingAction = pushPanel;
|
||||
|
||||
const panelName = useNewPlayer ? panel_names.PLAYER_PANEL_NEW : panel_names.PLAYER_PANEL;
|
||||
// const panelName = useNewPlayer ? panel_names.PLAYER_PANEL_NEW : panel_names.PLAYER_PANEL;
|
||||
const panelName = panel_names.PLAYER_PANEL;
|
||||
|
||||
if (topPanel && topPanel.name === panelName) {
|
||||
panelWorkingAction = updatePanel;
|
||||
|
||||
@@ -10,11 +10,11 @@ const useReviews = (prdtId, patnrId) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Redux 상태에서 리뷰 데이터 가져오기 - CustomerImages와 동일한 방식
|
||||
const reviewData = useSelector(state => state.product.reviewData);
|
||||
const loadedPrdtId = useSelector(state => state.product.loadedPrdtId);
|
||||
const reviewData = useSelector((state) => state.product.reviewData);
|
||||
const loadedPrdtId = useSelector((state) => state.product.loadedPrdtId);
|
||||
|
||||
// 빈 내용 리뷰 필터링 - 의미있는 리뷰만 표시
|
||||
const allReviews = (reviewData?.reviewList || []).filter(review => {
|
||||
const allReviews = (reviewData?.reviewList || []).filter((review) => {
|
||||
const content = review.rvwCtnt?.trim();
|
||||
return content && content.length > 0;
|
||||
});
|
||||
@@ -22,21 +22,21 @@ const useReviews = (prdtId, patnrId) => {
|
||||
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
|
||||
});
|
||||
// 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' 등
|
||||
value: 'all', // 'all', 1-5, 'positive', 'negative', 'aroma' 등
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasLoadedData, setHasLoadedData] = useState(false);
|
||||
@@ -59,14 +59,14 @@ const useReviews = (prdtId, patnrId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useReviews] loadReviews 시작:', { prdtId, patnrId });
|
||||
// console.log('[useReviews] loadReviews 시작:', { prdtId, patnrId });
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Redux action을 통해 API 호출 - patnrId 추가
|
||||
await dispatch(getUserReviews({ prdtId, patnrId }));
|
||||
setHasLoadedData(true);
|
||||
console.log('[useReviews] loadReviews 완료');
|
||||
// console.log('[useReviews] loadReviews 완료');
|
||||
} catch (error) {
|
||||
console.error('[useReviews] loadReviews 실패:', error);
|
||||
} finally {
|
||||
@@ -77,17 +77,17 @@ const useReviews = (prdtId, patnrId) => {
|
||||
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
|
||||
useEffect(() => {
|
||||
if (prdtId && patnrId && prdtId !== loadedPrdtId) {
|
||||
console.log('[useReviews] prdtId changed, loading new data:', {
|
||||
from: loadedPrdtId,
|
||||
to: prdtId,
|
||||
patnrId
|
||||
});
|
||||
// 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 });
|
||||
// console.log('[useReviews] Using cached data for same prdtId:', { prdtId, patnrId });
|
||||
setHasLoadedData(true); // 캐시된 데이터 사용 시 로드 완료 상태로 설정
|
||||
}
|
||||
}, [prdtId, patnrId, loadedPrdtId, loadReviews]);
|
||||
@@ -112,35 +112,103 @@ const useReviews = (prdtId, patnrId) => {
|
||||
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'
|
||||
'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'
|
||||
'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 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 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));
|
||||
return !prevTokens.some((prevToken) => negationWords.has(prevToken));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -160,19 +228,19 @@ const useReviews = (prdtId, patnrId) => {
|
||||
return {
|
||||
rating: { all: 0 },
|
||||
keyword: {},
|
||||
sentiment: {}
|
||||
sentiment: {},
|
||||
};
|
||||
}
|
||||
|
||||
const counts = {
|
||||
rating: { all: allReviews.length },
|
||||
keyword: {},
|
||||
sentiment: {}
|
||||
sentiment: {},
|
||||
};
|
||||
|
||||
// 별점별 카운트 - 소수점 별점을 정수로 반올림하여 카운팅
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
counts.rating[i] = allReviews.filter(review => {
|
||||
counts.rating[i] = allReviews.filter((review) => {
|
||||
const rating = review.rvwScr || review.rvwRtng || review.rating || 0;
|
||||
return Math.round(rating) === i;
|
||||
}).length;
|
||||
@@ -180,34 +248,34 @@ const useReviews = (prdtId, patnrId) => {
|
||||
|
||||
// 키워드별 카운트
|
||||
const keywords = ['aroma', 'vanilla', 'cinnamon', 'quality'];
|
||||
keywords.forEach(keyword => {
|
||||
counts.keyword[keyword] = allReviews.filter(review =>
|
||||
keywords.forEach((keyword) => {
|
||||
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,
|
||||
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)
|
||||
}))
|
||||
});
|
||||
// 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]);
|
||||
@@ -224,7 +292,7 @@ const useReviews = (prdtId, patnrId) => {
|
||||
if (currentFilter.value === 'all' || currentFilter.value === null) {
|
||||
result = [...allReviews]; // 전체 표시
|
||||
} else {
|
||||
result = allReviews.filter(review => {
|
||||
result = allReviews.filter((review) => {
|
||||
const rating = review.rvwScr || review.rvwRtng || review.rating || 0;
|
||||
return Math.round(rating) === currentFilter.value;
|
||||
});
|
||||
@@ -232,15 +300,11 @@ const useReviews = (prdtId, patnrId) => {
|
||||
break;
|
||||
|
||||
case 'sentiment':
|
||||
result = allReviews.filter(review =>
|
||||
matchesSentiment(review, currentFilter.value)
|
||||
);
|
||||
result = allReviews.filter((review) => matchesSentiment(review, currentFilter.value));
|
||||
break;
|
||||
|
||||
case 'keyword':
|
||||
result = allReviews.filter(review =>
|
||||
matchesKeyword(review, currentFilter.value)
|
||||
);
|
||||
result = allReviews.filter((review) => matchesKeyword(review, currentFilter.value));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -282,44 +346,50 @@ const useReviews = (prdtId, patnrId) => {
|
||||
// 다음 청크 로드 (클라이언트 사이드에서 페이지만 증가)
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore) {
|
||||
console.log('[useReviews] loadMore: 다음 청크 로드', {
|
||||
currentPage,
|
||||
nextPage: currentPage + 1,
|
||||
currentDisplayCount: displayReviews.length,
|
||||
totalFilteredCount: filteredReviews.length
|
||||
});
|
||||
setCurrentPage(prev => prev + 1);
|
||||
// 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
|
||||
));
|
||||
// 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]);
|
||||
}, [
|
||||
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
|
||||
));
|
||||
// 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]);
|
||||
}, [
|
||||
userReviewPanelHasPrev,
|
||||
userReviewPanelPage,
|
||||
userReviewPanelTotalPages,
|
||||
filteredReviews.length,
|
||||
]);
|
||||
|
||||
// 필터 변경 시 UserReviewPanel 페이지도 초기화
|
||||
useEffect(() => {
|
||||
@@ -328,7 +398,7 @@ const useReviews = (prdtId, patnrId) => {
|
||||
|
||||
// Single Filter 적용 함수
|
||||
const applyFilter = useCallback((type, value) => {
|
||||
console.log('[useReviews] 필터 적용:', { type, value });
|
||||
// console.log('[useReviews] 필터 적용:', { type, value });
|
||||
|
||||
// 이전 필터 결과 해제
|
||||
setCurrentFilter({ type, value });
|
||||
@@ -336,38 +406,48 @@ const useReviews = (prdtId, patnrId) => {
|
||||
}, []);
|
||||
|
||||
// 편의 함수들 (기존 인터페이스 호환성)
|
||||
const applyRatingFilter = useCallback((rating) => {
|
||||
console.log('[useReviews] applyRatingFilter called with:', rating);
|
||||
const applyRatingFilter = useCallback(
|
||||
(rating) => {
|
||||
// console.log('[useReviews] applyRatingFilter called with:', rating);
|
||||
applyFilter('rating', rating);
|
||||
}, [applyFilter]);
|
||||
},
|
||||
[applyFilter]
|
||||
);
|
||||
|
||||
const applySentimentFilter = useCallback((sentiment) => {
|
||||
const applySentimentFilter = useCallback(
|
||||
(sentiment) => {
|
||||
applyFilter('sentiment', sentiment);
|
||||
}, [applyFilter]);
|
||||
},
|
||||
[applyFilter]
|
||||
);
|
||||
|
||||
const applyKeywordFilter = useCallback((keyword) => {
|
||||
const applyKeywordFilter = useCallback(
|
||||
(keyword) => {
|
||||
applyFilter('keyword', keyword);
|
||||
}, [applyFilter]);
|
||||
},
|
||||
[applyFilter]
|
||||
);
|
||||
|
||||
// 필터 초기화 함수
|
||||
const clearAllFilters = useCallback(() => {
|
||||
console.log('[useReviews] 모든 필터 초기화');
|
||||
// console.log('[useReviews] 모든 필터 초기화');
|
||||
applyFilter('rating', 'all');
|
||||
}, [applyFilter]);
|
||||
|
||||
// 이미지가 있는 리뷰들만 추려내는 함수
|
||||
const getReviewsWithImages = useMemo(() => {
|
||||
const reviewsWithImages = allReviews.filter(
|
||||
(review) => 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개만 로그
|
||||
});
|
||||
// console.log('[useReviews] 이미지가 있는 리뷰 필터링:', {
|
||||
// totalReviews: allReviews.length,
|
||||
// reviewsWithImages: reviewsWithImages.length,
|
||||
// imageReviews: reviewsWithImages.slice(0, 3) // 처음 3개만 로그
|
||||
// });
|
||||
|
||||
return reviewsWithImages;
|
||||
}, [allReviews]);
|
||||
@@ -387,50 +467,56 @@ const useReviews = (prdtId, patnrId) => {
|
||||
imgUrl,
|
||||
imgSeq: imgSeq || imgIndex + 1,
|
||||
reviewId: review.rvwId,
|
||||
reviewData: review // 전체 리뷰 데이터도 포함
|
||||
reviewData: review, // 전체 리뷰 데이터도 포함
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[useReviews] 이미지 데이터 추출 완료:', {
|
||||
totalImages: images.length,
|
||||
sampleImages: images.slice(0, 3)
|
||||
});
|
||||
// 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)
|
||||
});
|
||||
// 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(() => {
|
||||
return {
|
||||
totalReviews: (reviewDetail && reviewDetail.totRvwCnt) ? reviewDetail.totRvwCnt : allReviews.length, // 전체 리뷰 개수
|
||||
totalReviews:
|
||||
reviewDetail && reviewDetail.totRvwCnt ? reviewDetail.totRvwCnt : allReviews.length, // 전체 리뷰 개수
|
||||
filteredCount: filteredReviews.length, // 필터링된 리뷰 개수
|
||||
displayedCount: displayReviews.length, // 현재 표시 중인 리뷰 개수
|
||||
averageRating: (reviewDetail && reviewDetail.totRvwAvg) ? reviewDetail.totRvwAvg :
|
||||
(reviewDetail && reviewDetail.avgRvwScr) ? reviewDetail.avgRvwScr : 0,
|
||||
totalRatingCount: (reviewDetail && reviewDetail.totRvwCnt) ? reviewDetail.totRvwCnt : allReviews.length
|
||||
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] 데이터 새로고침 시작');
|
||||
// console.log('[useReviews] 데이터 새로고침 시작');
|
||||
setHasLoadedData(false); // 강제로 다시 로드하도록
|
||||
setCurrentPage(0);
|
||||
setCurrentFilter({ type: 'rating', value: 'all' }); // 기본 필터로 초기화
|
||||
@@ -492,8 +578,8 @@ const useReviews = (prdtId, patnrId) => {
|
||||
currentPage,
|
||||
hasMore,
|
||||
isLoading,
|
||||
hasLoadedData
|
||||
}
|
||||
hasLoadedData,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
// src/views/DetailPanel/DetailPanel.new.jsx
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Spinner from '@enact/sandstone/Spinner';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
|
||||
|
||||
import indicatorDefaultImage
|
||||
from '../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import indicatorDefaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
|
||||
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
|
||||
import { getThemeCurationDetailInfo } from '../../actions/homeActions';
|
||||
import {
|
||||
getMainCategoryDetail,
|
||||
getMainYouMayLike,
|
||||
} from '../../actions/mainActions';
|
||||
import {
|
||||
popPanel,
|
||||
updatePanel,
|
||||
} from '../../actions/panelActions';
|
||||
import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions';
|
||||
import { popPanel, updatePanel } from '../../actions/panelActions';
|
||||
import { finishVideoPreview } from '../../actions/playActions';
|
||||
import {
|
||||
clearProductDetail,
|
||||
getProductOptionId,
|
||||
} from '../../actions/productActions';
|
||||
import { clearProductDetail, getProductOptionId } from '../../actions/productActions';
|
||||
import TBody from '../../components/TBody/TBody';
|
||||
import TPanel from '../../components/TPanel/TPanel';
|
||||
import { panel_names } from '../../utils/Config';
|
||||
import fp from '../../utils/fp';
|
||||
import {
|
||||
$L,
|
||||
getQRCodeUrl,
|
||||
} from '../../utils/helperMethods';
|
||||
import { $L, getQRCodeUrl } from '../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||
import DetailPanelBackground from './components/DetailPanelBackground';
|
||||
import THeaderCustom from './components/THeaderCustom';
|
||||
@@ -55,68 +32,49 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
const productData = useSelector((state) => state.main.productData);
|
||||
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
|
||||
const themeProductInfos = useSelector(
|
||||
(state) => state.home.themeCurationDetailInfoData
|
||||
);
|
||||
const themeProductInfos = useSelector((state) => state.home.themeCurationDetailInfoData);
|
||||
const isLoading = useSelector((state) =>
|
||||
fp.pipe(() => state, fp.get("common.appStatus.showLoadingPanel.show"))()
|
||||
fp.pipe(() => state, fp.get('common.appStatus.showLoadingPanel.show'))()
|
||||
);
|
||||
const themeData = useSelector((state) =>
|
||||
fp.pipe(
|
||||
() => state,
|
||||
fp.get("home.productData.themeInfo"),
|
||||
fp.get('home.productData.themeInfo'),
|
||||
(list) => list && list[0]
|
||||
)()
|
||||
);
|
||||
const webOSVersion = useSelector(
|
||||
(state) => state.common.appStatus.webOSVersion
|
||||
);
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
|
||||
// FP 방식으로 상태 관리
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const localRecentItems = useSelector((state) =>
|
||||
fp.pipe(() => state, fp.get("localSettings.recentItems"))()
|
||||
fp.pipe(() => state, fp.get('localSettings.recentItems'))()
|
||||
);
|
||||
const { httpHeader } = useSelector((state) => state.common);
|
||||
const { popupVisible, activePopup } = useSelector(
|
||||
(state) => state.common.popup
|
||||
);
|
||||
const [lgCatCd, setLgCatCd] = useState("");
|
||||
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
|
||||
const [lgCatCd, setLgCatCd] = useState('');
|
||||
const [themeProductInfo, setThemeProductInfo] = useState(null);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const panelType = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("type"))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelType = useMemo(() => fp.pipe(() => panelInfo, fp.get('type'))(), [panelInfo]);
|
||||
const panelCurationId = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("curationId"))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelPatnrId = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("patnrId"))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelPrdtId = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("prdtId"))(),
|
||||
() => fp.pipe(() => panelInfo, fp.get('curationId'))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelPatnrId = useMemo(() => fp.pipe(() => panelInfo, fp.get('patnrId'))(), [panelInfo]);
|
||||
const panelPrdtId = useMemo(() => fp.pipe(() => panelInfo, fp.get('prdtId'))(), [panelInfo]);
|
||||
const panelLiveReqFlag = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("liveReqFlag"))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelBgImgNo = useMemo(
|
||||
() => fp.pipe(() => panelInfo, fp.get("bgImgNo"))(),
|
||||
() => fp.pipe(() => panelInfo, fp.get('liveReqFlag'))(),
|
||||
[panelInfo]
|
||||
);
|
||||
const panelBgImgNo = useMemo(() => fp.pipe(() => panelInfo, fp.get('bgImgNo'))(), [panelInfo]);
|
||||
const productPmtSuptYn = useMemo(
|
||||
() => fp.pipe(() => productData, fp.get("pmtSuptYn"))(),
|
||||
() => fp.pipe(() => productData, fp.get('pmtSuptYn'))(),
|
||||
[productData]
|
||||
);
|
||||
const productGrPrdtProcYn = useMemo(
|
||||
() => fp.pipe(() => productData, fp.get("grPrdtProcYn"))(),
|
||||
() => fp.pipe(() => productData, fp.get('grPrdtProcYn'))(),
|
||||
[productData]
|
||||
);
|
||||
|
||||
@@ -124,7 +82,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
() =>
|
||||
fp.pipe(
|
||||
() => panelType,
|
||||
(type) => (type === "theme" ? themeData : productData)
|
||||
(type) => (type === 'theme' ? themeData : productData)
|
||||
)(),
|
||||
[panelType, themeData, productData]
|
||||
);
|
||||
@@ -132,11 +90,9 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const [productType, setProductType] = useState(null);
|
||||
const [openThemeItemOverlay, setOpenThemeItemOverlay] = useState(false);
|
||||
|
||||
// FP 방식으로 스크롤 상태 관리
|
||||
const [scrollToSection, setScrollToSection] = useState(null);
|
||||
const [pendingScrollSection, setPendingScrollSection] = useState(null);
|
||||
|
||||
// FP 방식으로 상태 업데이트 함수들
|
||||
const updateSelectedIndex = useCallback((newIndex) => {
|
||||
setSelectedIndex(
|
||||
fp.pipe(
|
||||
@@ -150,15 +106,13 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
setOpenThemeItemOverlay(fp.pipe(() => isOpen, Boolean)());
|
||||
}, []);
|
||||
|
||||
// FP 방식으로 이벤트 핸들러 정의
|
||||
const onSpotlightUpTButton = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
Spotlight.focus("spotlightId_backBtn");
|
||||
Spotlight.focus('spotlightId_backBtn');
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(
|
||||
(isCancelClick) => (ev) => {
|
||||
// FP 방식으로 액션 디스패치 체이닝
|
||||
fp.pipe(
|
||||
() => {
|
||||
dispatch(finishVideoPreview());
|
||||
@@ -169,12 +123,12 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const shouldUpdatePanel =
|
||||
fp.pipe(
|
||||
() => panels,
|
||||
fp.get("length"),
|
||||
fp.get('length'),
|
||||
(length) => length === 4
|
||||
)() &&
|
||||
fp.pipe(
|
||||
() => panels,
|
||||
fp.get("1.name"),
|
||||
fp.get('1.name'),
|
||||
(name) => name === panel_names.PLAYER_PANEL
|
||||
)();
|
||||
|
||||
@@ -183,7 +137,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
updatePanel({
|
||||
name: panel_names.PLAYER_PANEL,
|
||||
panelInfo: {
|
||||
thumbnail: fp.pipe(() => panelInfo, fp.get("thumbnailUrl"))(),
|
||||
thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(),
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -198,35 +152,31 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
[dispatch, panelInfo, panels]
|
||||
);
|
||||
|
||||
// FP 방식으로 스크롤 함수 핸들러
|
||||
const handleScrollToSection = useCallback(
|
||||
(sectionId) => {
|
||||
console.log("DetailPanel: handleScrollToSection called with:", sectionId);
|
||||
console.log("DetailPanel: scrollToSection function:", scrollToSection);
|
||||
console.log('DetailPanel: handleScrollToSection called with:', sectionId);
|
||||
console.log('DetailPanel: scrollToSection function:', scrollToSection);
|
||||
|
||||
// FP 방식으로 스크롤 처리 로직
|
||||
const scrollAction = fp.pipe(
|
||||
() => ({ scrollToSection, sectionId }),
|
||||
({ scrollToSection, sectionId }) => {
|
||||
if (fp.isNotNil(scrollToSection)) {
|
||||
return {
|
||||
action: "execute",
|
||||
action: 'execute',
|
||||
scrollFunction: scrollToSection,
|
||||
sectionId,
|
||||
};
|
||||
} else {
|
||||
return { action: "store", sectionId };
|
||||
return { action: 'store', sectionId };
|
||||
}
|
||||
}
|
||||
)();
|
||||
|
||||
// 액션에 따른 처리
|
||||
if (scrollAction.action === "execute") {
|
||||
if (scrollAction.action === 'execute') {
|
||||
scrollAction.scrollFunction(scrollAction.sectionId);
|
||||
} else {
|
||||
console.log(
|
||||
"DetailPanel: scrollToSection function is null, storing pending scroll"
|
||||
);
|
||||
console.log('DetailPanel: scrollToSection function is null, storing pending scroll');
|
||||
setPendingScrollSection(scrollAction.sectionId);
|
||||
}
|
||||
},
|
||||
@@ -236,7 +186,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// ===== 배경 이미지 설정 (컴포넌트로 구현되어 useEffect 불필요) =====
|
||||
// DetailPanelBackground 컴포넌트로 배경 렌더링
|
||||
|
||||
// FP 방식으로 pending scroll 처리 (메모리 누수 방지)
|
||||
useEffect(() => {
|
||||
const shouldExecutePendingScroll = fp.pipe(
|
||||
() => ({ scrollToSection, pendingScrollSection }),
|
||||
@@ -245,10 +194,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
)();
|
||||
|
||||
if (shouldExecutePendingScroll) {
|
||||
console.log(
|
||||
"DetailPanel: executing pending scroll to:",
|
||||
pendingScrollSection
|
||||
);
|
||||
console.log('DetailPanel: executing pending scroll to:', pendingScrollSection);
|
||||
|
||||
// 메모리 누수 방지를 위한 cleanup 함수
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -265,9 +211,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}
|
||||
}, [scrollToSection, pendingScrollSection]);
|
||||
|
||||
// FP 방식으로 초기 데이터 로딩 처리 (메모리 누수 방지)
|
||||
useEffect(() => {
|
||||
// FP 방식으로 액션 디스패치 체이닝
|
||||
const loadInitialData = fp.pipe(
|
||||
() => {
|
||||
// 기본 액션 디스패치
|
||||
@@ -276,7 +220,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
},
|
||||
() => {
|
||||
// 테마 데이터 로딩
|
||||
const isThemeType = panelType === "theme";
|
||||
const isThemeType = panelType === 'theme';
|
||||
|
||||
if (isThemeType) {
|
||||
dispatch(
|
||||
@@ -298,7 +242,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
getMainCategoryDetail({
|
||||
patnrId: panelPatnrId,
|
||||
prdtId: panelPrdtId,
|
||||
liveReqFlag: panelLiveReqFlag || "N",
|
||||
liveReqFlag: panelLiveReqFlag || 'N',
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -319,7 +263,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
panelBgImgNo,
|
||||
]);
|
||||
|
||||
// FP 방식으로 추천 상품 데이터 로딩 (메모리 누수 방지)
|
||||
useEffect(() => {
|
||||
const shouldLoadRecommendations = fp.pipe(() => lgCatCd, fp.isNotEmpty)();
|
||||
|
||||
@@ -345,7 +288,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
setLgCatCd(productData.catCd);
|
||||
} else if (
|
||||
themeProductInfos &&
|
||||
themeProductInfos[selectedIndex]?.pmtSuptYn === "N" &&
|
||||
themeProductInfos[selectedIndex]?.pmtSuptYn === 'N' &&
|
||||
panelInfo?.curationId
|
||||
) {
|
||||
const themeCatCd = themeProductInfos[selectedIndex]?.catCd;
|
||||
@@ -359,7 +302,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// selectedIndex,
|
||||
// themeProductPmtSuptYn: themeProductInfos?.[selectedIndex]?.pmtSuptYn
|
||||
// });
|
||||
setLgCatCd("");
|
||||
setLgCatCd('');
|
||||
}
|
||||
}, [productData, themeProductInfos, selectedIndex, panelInfo?.curationId]);
|
||||
|
||||
@@ -389,16 +332,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// - 순수 유틸로 빌드/업서트 함수 작성 후, 적절한 useEffect에서 호출하세요.
|
||||
// 예) saveRecentItem(panelInfo, selectedIndex)
|
||||
|
||||
// FP 방식으로 cleanup 처리 (메모리 누수 방지)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// FP 방식으로 cleanup 액션 실행
|
||||
fp.pipe(
|
||||
() => {
|
||||
dispatch(clearProductDetail());
|
||||
},
|
||||
() => {
|
||||
setContainerLastFocusedElement(null, ["indicator-GridListContainer"]);
|
||||
setContainerLastFocusedElement(null, ['indicator-GridListContainer']);
|
||||
}
|
||||
)();
|
||||
};
|
||||
@@ -411,9 +352,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// }
|
||||
// }, [panelInfo, selectedIndex])
|
||||
|
||||
// 테마/호텔 기반 인덱스 초기화가 필요하면:
|
||||
// - findIndex 유틸을 만들어 매칭 인덱스를 계산 후 setSelectedIndex에 반영하세요.
|
||||
// FP 방식으로 버전 비교 헬퍼 함수 (curry 적용)
|
||||
const versionComparators = useMemo(
|
||||
() => ({
|
||||
isVersionGTE: fp.curry((target, version) => version >= target),
|
||||
@@ -422,12 +360,9 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
[]
|
||||
);
|
||||
|
||||
// FP 방식으로 조건 체크 헬퍼 함수들 (curry 적용)
|
||||
const conditionCheckers = useMemo(
|
||||
() => ({
|
||||
hasDataAndCondition: fp.curry(
|
||||
(conditionFn, data) => fp.isNotNil(data) && conditionFn(data)
|
||||
),
|
||||
hasDataAndCondition: fp.curry((conditionFn, data) => fp.isNotNil(data) && conditionFn(data)),
|
||||
equalTo: fp.curry((expected, actual) => actual === expected),
|
||||
checkAllConditions: fp.curry((conditions, data) =>
|
||||
fp.reduce(
|
||||
@@ -441,7 +376,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
);
|
||||
|
||||
const getProductType = useCallback(() => {
|
||||
// FP 방식으로 데이터 검증 및 타입 결정 - curry 적용으로 더 함수형 개선
|
||||
const createTypeChecker = fp.curry((type, conditions, sideEffect) =>
|
||||
fp.pipe(
|
||||
() => conditions(),
|
||||
@@ -459,7 +393,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// 테마 타입 체크
|
||||
() =>
|
||||
createTypeChecker(
|
||||
"theme",
|
||||
'theme',
|
||||
() =>
|
||||
fp.pipe(
|
||||
() => ({ panelCurationId, themeData }),
|
||||
@@ -469,10 +403,10 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
() => {
|
||||
const themeProduct = fp.pipe(
|
||||
() => themeData,
|
||||
fp.get("productInfos"),
|
||||
fp.get('productInfos'),
|
||||
fp.get(selectedIndex.toString())
|
||||
)();
|
||||
setProductType("theme");
|
||||
setProductType('theme');
|
||||
setThemeProductInfo(themeProduct);
|
||||
}
|
||||
),
|
||||
@@ -480,7 +414,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// Buy Now 타입 체크 (curry 활용)
|
||||
() =>
|
||||
createTypeChecker(
|
||||
"buyNow",
|
||||
'buyNow',
|
||||
() =>
|
||||
fp.pipe(
|
||||
() => ({
|
||||
@@ -499,21 +433,21 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}) => {
|
||||
const conditions = [
|
||||
() => fp.isNotNil(productData),
|
||||
() => conditionCheckers.equalTo("Y")(productPmtSuptYn),
|
||||
() => conditionCheckers.equalTo("N")(productGrPrdtProcYn),
|
||||
() => conditionCheckers.equalTo('Y')(productPmtSuptYn),
|
||||
() => conditionCheckers.equalTo('N')(productGrPrdtProcYn),
|
||||
() => fp.isNotNil(panelPrdtId),
|
||||
() => versionComparators.isVersionGTE("6.0")(webOSVersion),
|
||||
() => versionComparators.isVersionGTE('6.0')(webOSVersion),
|
||||
];
|
||||
return conditionCheckers.checkAllConditions(conditions)({});
|
||||
}
|
||||
)(),
|
||||
() => setProductType("buyNow")
|
||||
() => setProductType('buyNow')
|
||||
),
|
||||
|
||||
// Shop By Mobile 타입 체크 (curry 활용)
|
||||
() =>
|
||||
createTypeChecker(
|
||||
"shopByMobile",
|
||||
'shopByMobile',
|
||||
() =>
|
||||
fp.pipe(
|
||||
() => ({
|
||||
@@ -532,27 +466,24 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}) => {
|
||||
if (!productData) return false;
|
||||
|
||||
const isDirectMobile =
|
||||
conditionCheckers.equalTo("N")(productPmtSuptYn);
|
||||
const isDirectMobile = conditionCheckers.equalTo('N')(productPmtSuptYn);
|
||||
const conditionalMobileConditions = [
|
||||
() => conditionCheckers.equalTo("Y")(productPmtSuptYn),
|
||||
() => conditionCheckers.equalTo("N")(productGrPrdtProcYn),
|
||||
() => versionComparators.isVersionLT("6.0")(webOSVersion),
|
||||
() => conditionCheckers.equalTo('Y')(productPmtSuptYn),
|
||||
() => conditionCheckers.equalTo('N')(productGrPrdtProcYn),
|
||||
() => versionComparators.isVersionLT('6.0')(webOSVersion),
|
||||
() => fp.isNotNil(panelPrdtId),
|
||||
];
|
||||
const isConditionalMobile =
|
||||
conditionCheckers.checkAllConditions(
|
||||
const isConditionalMobile = conditionCheckers.checkAllConditions(
|
||||
conditionalMobileConditions
|
||||
)({});
|
||||
|
||||
return isDirectMobile || isConditionalMobile;
|
||||
}
|
||||
)(),
|
||||
() => setProductType("shopByMobile")
|
||||
() => setProductType('shopByMobile')
|
||||
),
|
||||
];
|
||||
|
||||
// FP 방식으로 순차적 타입 체크
|
||||
const matchedRule = fp.reduce(
|
||||
(result, rule) => (result.matched ? result : rule()),
|
||||
{ matched: false },
|
||||
@@ -569,13 +500,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
productGrPrdtProcYn,
|
||||
webOSVersion,
|
||||
}),
|
||||
({
|
||||
productData,
|
||||
panelPrdtId,
|
||||
productPmtSuptYn,
|
||||
productGrPrdtProcYn,
|
||||
webOSVersion,
|
||||
}) => ({
|
||||
({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => ({
|
||||
pmtSuptYn: productPmtSuptYn,
|
||||
grPrdtProcYn: productGrPrdtProcYn,
|
||||
prdtId: panelPrdtId,
|
||||
@@ -583,8 +508,8 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
})
|
||||
)();
|
||||
|
||||
console.warn("Unknown product type:", productData);
|
||||
console.warn("Product data properties:", debugInfo);
|
||||
console.warn('Unknown product type:', productData);
|
||||
console.warn('Product data properties:', debugInfo);
|
||||
}
|
||||
}, [
|
||||
panelCurationId,
|
||||
@@ -600,7 +525,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
useEffect(() => {
|
||||
// productData가 로드된 후에만 getProductType 실행
|
||||
if (productData || (panelType === "theme" && themeData)) {
|
||||
if (productData || (panelType === 'theme' && themeData)) {
|
||||
getProductType();
|
||||
}
|
||||
}, [getProductType, productData, themeData, panelType]);
|
||||
@@ -614,11 +539,11 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}, [themeData, selectedIndex]);
|
||||
|
||||
const imageUrl = useMemo(
|
||||
() => fp.pipe(() => productData, fp.get("thumbnailUrl960"))(),
|
||||
() => fp.pipe(() => productData, fp.get('thumbnailUrl960'))(),
|
||||
[productData]
|
||||
);
|
||||
|
||||
// FP 방식으로 타이틀과 aria-label 메모이제이션 (성능 최적화)
|
||||
// 타이틀과 aria-label 메모이제이션 (성능 최적화)
|
||||
const headerTitle = useMemo(
|
||||
() =>
|
||||
fp.pipe(
|
||||
@@ -628,21 +553,20 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
() => ({ panelPrdtId, productData }),
|
||||
({ panelPrdtId, productData }) =>
|
||||
fp.isNotNil(panelPrdtId) &&
|
||||
fp.pipe(() => productData, fp.get("prdtNm"), fp.isNotNil)()
|
||||
? fp.pipe(() => productData, fp.get("prdtNm"))()
|
||||
fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)()
|
||||
? fp.pipe(() => productData, fp.get('prdtNm'))()
|
||||
: null
|
||||
)();
|
||||
|
||||
const themeTitle = fp.pipe(
|
||||
() => ({ panelType, themeData }),
|
||||
({ panelType, themeData }) =>
|
||||
panelType === "theme" &&
|
||||
fp.pipe(() => themeData, fp.get("curationNm"), fp.isNotNil)()
|
||||
? fp.pipe(() => themeData, fp.get("curationNm"))()
|
||||
panelType === 'theme' && fp.pipe(() => themeData, fp.get('curationNm'), fp.isNotNil)()
|
||||
? fp.pipe(() => themeData, fp.get('curationNm'))()
|
||||
: null
|
||||
)();
|
||||
|
||||
return productTitle || themeTitle || "";
|
||||
return productTitle || themeTitle || '';
|
||||
}
|
||||
)(),
|
||||
[panelPrdtId, productData, panelType, themeData]
|
||||
@@ -653,10 +577,9 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
fp.pipe(
|
||||
() => ({ panelPrdtId, productData }),
|
||||
({ panelPrdtId, productData }) =>
|
||||
fp.isNotNil(panelPrdtId) &&
|
||||
fp.pipe(() => productData, fp.get("prdtNm"), fp.isNotNil)()
|
||||
? fp.pipe(() => productData, fp.get("prdtNm"))()
|
||||
: ""
|
||||
fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)()
|
||||
? fp.pipe(() => productData, fp.get('prdtNm'))()
|
||||
: ''
|
||||
)(),
|
||||
[panelPrdtId, productData]
|
||||
);
|
||||
@@ -666,7 +589,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// Pink Pong 등 특정 파트너사에서만 thumbnailUrl960 데이터가 있어서 배경이 변경됨
|
||||
// 현재는 고정 배경(detailPanelBg)만 사용하기 위해 주석 처리
|
||||
|
||||
// FP 방식으로 배경 이미지 설정 (메모리 누수 방지)
|
||||
/*
|
||||
useLayoutEffect(() => {
|
||||
const shouldSetBackground = fp.pipe(
|
||||
@@ -681,7 +603,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}, [imageUrl]);
|
||||
*/
|
||||
|
||||
console.log("productDataSource :", productDataSource);
|
||||
console.log('productDataSource :', productDataSource);
|
||||
|
||||
// 언마운트 시 인덱스 초기화가 필요하면:
|
||||
// useEffect(() => () => setSelectedIndex(0), [])
|
||||
@@ -725,18 +647,12 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
isDefaultContainer
|
||||
>
|
||||
{useMemo(() => {
|
||||
// FP 방식으로 렌더링 조건 결정 (메모이제이션으로 최적화)
|
||||
const renderStates = fp.pipe(
|
||||
() => ({ isLoading, panelInfo, productDataSource, productType }),
|
||||
({ isLoading, panelInfo, productDataSource, productType }) => {
|
||||
const hasRequiredData = fp.pipe(
|
||||
() => [panelInfo, productDataSource, productType],
|
||||
(data) =>
|
||||
fp.reduce(
|
||||
(acc, item) => acc && fp.isNotNil(item),
|
||||
true,
|
||||
data
|
||||
)
|
||||
(data) => fp.reduce((acc, item) => acc && fp.isNotNil(item), true, data)
|
||||
)();
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { startVideoPlayer, finishVideoPreview } from '../../../../actions/playActions';
|
||||
import { startMediaPlayer, finishMediaPreview } from '../../../../actions/mediaActions';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import { panel_names } from '../../../../utils/Config';
|
||||
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
|
||||
@@ -78,8 +79,33 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
|
||||
// modal=true로 재생 중이면 modal=false(전체화면)로 변경
|
||||
const newModalState = isCurrentlyPlayingModal ? false : modalState;
|
||||
|
||||
// dispatch(
|
||||
// startVideoPlayer({
|
||||
// qrCurrentItem: productInfo,
|
||||
// showUrl: productInfo?.prdtMediaUrl,
|
||||
// showNm: productInfo?.prdtNm,
|
||||
// patnrNm: productInfo?.patncNm,
|
||||
// patncLogoPath: productInfo?.patncLogoPath,
|
||||
// orderPhnNo: productInfo?.orderPhnNo,
|
||||
// disclaimer: productInfo?.disclaimer,
|
||||
// subtitle: productInfo?.prdtMediaSubtitlUrl,
|
||||
// lgCatCd: productInfo?.catCd,
|
||||
// patnrId: productInfo?.patnrId,
|
||||
// lgCatNm: productInfo?.catNm,
|
||||
// prdtId: productInfo?.prdtId,
|
||||
// patncNm: productInfo?.patncNm,
|
||||
// prdtNm: productInfo?.prdtNm,
|
||||
// thumbnailUrl: productInfo?.thumbnailUrl960,
|
||||
// shptmBanrTpNm: 'MEDIA',
|
||||
// modal: newModalState,
|
||||
// modalContainerId: 'product-video-player',
|
||||
// modalClassName: modalClassNameChange(),
|
||||
// spotlightDisable: true,
|
||||
// })
|
||||
// );
|
||||
|
||||
dispatch(
|
||||
startVideoPlayer({
|
||||
startMediaPlayer({
|
||||
qrCurrentItem: productInfo,
|
||||
showUrl: productInfo?.prdtMediaUrl,
|
||||
showNm: productInfo?.prdtNm,
|
||||
@@ -112,7 +138,15 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
|
||||
if (isLaunchedFromPlayer) {
|
||||
setIsLaunchedFromPlayer(false);
|
||||
}
|
||||
}, [dispatch, productInfo, canPlayVideo, isLaunchedFromPlayer, modalClassNameChange, panels, modalState]);
|
||||
}, [
|
||||
dispatch,
|
||||
productInfo,
|
||||
canPlayVideo,
|
||||
isLaunchedFromPlayer,
|
||||
modalClassNameChange,
|
||||
panels,
|
||||
modalState,
|
||||
]);
|
||||
|
||||
if (!canPlayVideo) return null;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Light theme 개별 리뷰 아이템 컴포넌트
|
||||
import React, { useCallback } from "react";
|
||||
import Spottable from "@enact/spotlight/Spottable";
|
||||
import classNames from "classnames";
|
||||
import StarRating from "../../DetailPanel/components/StarRating";
|
||||
import css from "./UserReviewItem.module.less";
|
||||
import React, { useCallback } from 'react';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import classNames from 'classnames';
|
||||
import StarRating from '../../DetailPanel/components/StarRating';
|
||||
import css from './UserReviewItem.module.less';
|
||||
|
||||
const SpottableComponent = Spottable("div");
|
||||
const SpottableComponent = Spottable('div');
|
||||
|
||||
/**
|
||||
* UserReviewPanel에서 사용하는 개별 리뷰 아이템 컴포넌트
|
||||
@@ -20,7 +20,7 @@ const UserReviewItem = ({
|
||||
onNextPage,
|
||||
onPrevPage,
|
||||
onClick,
|
||||
isPaging = false
|
||||
isPaging = false,
|
||||
}) => {
|
||||
const handleReviewClick = useCallback(() => {
|
||||
if (onClick) {
|
||||
@@ -29,11 +29,12 @@ const UserReviewItem = ({
|
||||
}, [onClick, review, index]);
|
||||
|
||||
// 키보드 이벤트 핸들러 - 조건부 페이징 처리
|
||||
const handleKeyDown = useCallback((event) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event) => {
|
||||
// 첫번째 리뷰(index 0)에서 위 화살표 처리
|
||||
if (event.key === 'ArrowUp' && index === 0) {
|
||||
if (hasPrev && onPrevPage) {
|
||||
console.log('[UserReviewItem] 첫번째 리뷰에서 위 화살표 - 이전 페이지');
|
||||
// console.log('[UserReviewItem] 첫번째 리뷰에서 위 화살표 - 이전 페이지');
|
||||
onPrevPage();
|
||||
}
|
||||
event.preventDefault(); // 페이징 여부와 상관없이 포커스 사라짐 방지
|
||||
@@ -42,37 +43,31 @@ const UserReviewItem = ({
|
||||
// 세번째 리뷰(index 2)에서 아래 화살표 처리
|
||||
else if (event.key === 'ArrowDown' && index === 2) {
|
||||
if (hasNext && onNextPage) {
|
||||
console.log('[UserReviewItem] 세번째 리뷰에서 아래 화살표 - 다음 페이지');
|
||||
// console.log('[UserReviewItem] 세번째 리뷰에서 아래 화살표 - 다음 페이지');
|
||||
onNextPage();
|
||||
}
|
||||
event.preventDefault(); // 페이징 여부와 상관없이 포커스 사라짐 방지
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, [index, hasNext, hasPrev, onNextPage, onPrevPage]);
|
||||
},
|
||||
[index, hasNext, hasPrev, onNextPage, onPrevPage]
|
||||
);
|
||||
|
||||
// 날짜 포맷팅 함수
|
||||
const formatToYYMMDD = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
const iso = date.toISOString().slice(2, 10);
|
||||
return iso.replace(/-/g, ".");
|
||||
return iso.replace(/-/g, '.');
|
||||
};
|
||||
|
||||
const {
|
||||
reviewImageList,
|
||||
rvwRtng,
|
||||
rvwRgstDtt,
|
||||
rvwCtnt,
|
||||
rvwId,
|
||||
wrtrNknm,
|
||||
rvwWrtrId
|
||||
} = review;
|
||||
const { reviewImageList, rvwRtng, rvwRgstDtt, rvwCtnt, rvwId, wrtrNknm, rvwWrtrId } = review;
|
||||
|
||||
return (
|
||||
<SpottableComponent
|
||||
key={`user-review-item-${rvwId}`}
|
||||
aria-label={`user-review-item-${rvwId}`}
|
||||
className={classNames(css.reviewContentContainer, {
|
||||
[css.paging]: isPaging
|
||||
[css.paging]: isPaging,
|
||||
})}
|
||||
onClick={handleReviewClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -93,27 +88,16 @@ const UserReviewItem = ({
|
||||
{/* 메타 정보 (별점, 작성자, 날짜) */}
|
||||
<div className={css.reviewMeta}>
|
||||
{rvwRtng && (
|
||||
<StarRating
|
||||
rating={rvwRtng}
|
||||
aria-label={`star rating ${rvwRtng} out of 5`}
|
||||
/>
|
||||
<StarRating rating={rvwRtng} aria-label={`star rating ${rvwRtng} out of 5`} />
|
||||
)}
|
||||
{(wrtrNknm || rvwWrtrId) && (
|
||||
<span className={css.reviewAuthor}>
|
||||
{wrtrNknm || rvwWrtrId}
|
||||
</span>
|
||||
)}
|
||||
{rvwRgstDtt && (
|
||||
<span className={css.reviewDate}>
|
||||
{formatToYYMMDD(rvwRgstDtt)}
|
||||
</span>
|
||||
<span className={css.reviewAuthor}>{wrtrNknm || rvwWrtrId}</span>
|
||||
)}
|
||||
{rvwRgstDtt && <span className={css.reviewDate}>{formatToYYMMDD(rvwRgstDtt)}</span>}
|
||||
</div>
|
||||
|
||||
{/* 리뷰 텍스트 */}
|
||||
{rvwCtnt && (
|
||||
<div className={css.reviewText}>{rvwCtnt}</div>
|
||||
)}
|
||||
{rvwCtnt && <div className={css.reviewText}>{rvwCtnt}</div>}
|
||||
</div>
|
||||
</SpottableComponent>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user