[251104] fix: UserReviewsPanel Review Filters-2

🕐 커밋 시간: 2025. 11. 04. 12:27:44

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +155줄
  • 삭제: -43줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/CheckOutPanel/CheckOutPanel.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/components/BuyOption.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/components/FilterItemButton.module.less
  ~ com.twin.app.shoptime/src/views/UserReview/components/UserReviewItem.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/components/UserReviewsList.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 중간 규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-11-04 12:27:45 +09:00
parent 74f362bbbc
commit 219582aaf2
7 changed files with 155 additions and 43 deletions

View File

@@ -361,17 +361,15 @@ export default function CheckOutPanel({ panelInfo }) {
return () => {
console.log('[BuyOption][CheckOutPanel] cleanup useEffect - calling resetCheckoutData');
// Mock 모드에서도 상태 초기화 필요 (Firefox 홀수/짝수번 패턴 문제 해결)
// API Mode에서만 checkout data 초기화 필요
// Mock Mode에서는 popup 상태만 정리 (Redux checkout state 유지)
if (!BUYNOW_CONFIG.isMockMode()) {
dispatch(resetCheckoutData());
} else {
console.log('[BuyOption][CheckOutPanel] Mock Mode - Partial cleanup to prevent state accumulation');
// Mock Mode에서도 팝업 상태와 관련된 부분 초기화
dispatch(setHidePopup());
// empTermsData 초기화를 위한 액션 디스패치 (empActions에서 reset 액션이 있는지 확인 필요)
// dispatch({ type: 'RESET_EMP_TERMS' });
console.log('[BuyOption][CheckOutPanel] Mock Mode - Cleaning up popup state only');
}
dispatch(setHidePopup());
};
}, [dispatch]);

View File

@@ -174,7 +174,7 @@ export default function ProductAllSection({
const youmaylikeData = useSelector((state) => state.main.youmaylikeData);
// ProductVideo 버전 관리 (1: 기존 modal 방식, 2: 내장 방식 , 3: 비디오 생략)
const [productVideoVersion, setProductVideoVersion] = useState(2);
const [productVideoVersion, setProductVideoVersion] = useState(3);
// const [currentHeight, setCurrentHeight] = useState(0);
//하단부분까지 갔을때 체크용

View File

@@ -35,7 +35,7 @@ import {
popPanel,
pushPanel,
} from '../../../actions/panelActions';
import { finishVideoPreview } from '../../../actions/playActions';
import { clearAllVideoTimers } from '../../../actions/playActions';
import {
getProductOption,
getProductOptionId,
@@ -374,8 +374,23 @@ const BuyOption = ({
const { mbrId, prdtId, prodSno } = response.data.productList[0];
const cartTpSno = `${mbrId}_${prdtId}_${prodSno}`;
// dispatch(popPanel(Config.panel_names.DETAIL_PANEL));
dispatch(finishVideoPreview());
dispatch(finishMediaPreview());
clearAllVideoTimers(); // ProductVideoV2의 타이머 정리 (일반 함수 직접 호출)
dispatch(finishMediaPreview()); // MediaPanel 정리
// 🔴 CRITICAL: DetailPanel 뒤에 있을 수 있는 PlayerPanel도 함께 제거 (API Mode)
dispatch((dispatchFn, getState) => {
const panels = getState().panels?.panels || [];
const playerPanelExists = panels.some(p =>
p.name === Config.panel_names.PLAYER_PANEL ||
p.name === Config.panel_names.PLAYER_PANEL_NEW
);
if (playerPanelExists) {
console.log('[BuyOption] ⚠️ API Mode - Found PlayerPanel in stack - removing before checkout');
dispatchFn(popPanel(Config.panel_names.PLAYER_PANEL));
}
});
dispatch(
pushPanel({
name: Config.panel_names.CHECKOUT_PANEL,
@@ -620,10 +635,26 @@ const BuyOption = ({
console.log('[BuyOption] logInfo:', logInfo);
console.log('[BuyOption] Dispatching pushPanel to CHECKOUT_PANEL');
// CheckOutPanel 이동 전에 PlayerPanel/MediaPanel 상태 정리
console.log('[BuyOption] Mock Mode - Cleaning up PlayerPanel/MediaPanel before checkout');
dispatch(finishVideoPreview());
dispatch(finishMediaPreview());
// CheckOutPanel 이동 전에 ProductVideoV2 타이머 및 MediaPanel/PlayerPanel 정리
console.log('[BuyOption] Mock Mode - Cleaning up ProductVideoV2 timers and all media panels before checkout');
clearAllVideoTimers(); // ProductVideoV2의 타이머 정리 (일반 함수 직접 호출)
dispatch(finishMediaPreview()); // MediaPanel 정리
// 🔴 CRITICAL: DetailPanel 뒤에 있을 수 있는 PlayerPanel도 함께 제거
// (finishMediaPreview는 MediaPanel만 처리하므로 PlayerPanel이 남아있을 수 있음)
// dispatch는 thunk function을 받을 수 있으므로, 이를 활용하여 getState로 panels 접근
dispatch((dispatchFn, getState) => {
const panels = getState().panels?.panels || [];
const playerPanelExists = panels.some(p =>
p.name === Config.panel_names.PLAYER_PANEL ||
p.name === Config.panel_names.PLAYER_PANEL_NEW
);
if (playerPanelExists) {
console.log('[BuyOption] ⚠️ Found PlayerPanel in stack - removing before checkout');
dispatchFn(popPanel(Config.panel_names.PLAYER_PANEL));
}
});
// Mock 모드: 선택 상품의 정보를 panelInfo에 담아서 전달
// CheckOutPanel에서 이 정보로 Mock 상품 데이터 생성
@@ -724,7 +755,7 @@ const BuyOption = ({
name: Config.panel_names.CHECKOUT_PANEL,
panelInfo: fallbackPanelInfo,
})
);
);
} else {
// 정상 케이스: checkoutPanelInfo 사용
dispatch(

View File

@@ -11,6 +11,7 @@ import useReviews, { REVIEW_VERSION } from '../../hooks/useReviews/useReviews';
import fp from '../../utils/fp';
import { panel_names } from '../../utils/Config';
import StarRating from '../DetailPanel/components/StarRating';
import UserReviewsPopup from '../DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup';
import FilterItemButton from './components/FilterItemButton';
import UserReviewsList from './components/UserReviewsList';
import UserReviewHeader from './UserReviewHeader';
@@ -46,10 +47,19 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
filters,
filteredReviewListData,
currentReviewFilter,
// 전체 리뷰 데이터 (팝업용)
allReviews,
getReviewsWithImages,
extractImagesFromReviews,
} = useReviews(prdtId, patnrId); // REVIEW_VERSION 상수에 따라 자동으로 API 선택
const [isPaging, setIsPaging] = useState(false);
// 팝업 상태 관리
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [popupMode, setPopupMode] = useState('user-reviews');
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const productImage = fp.pipe(
() => panelInfo,
fp.get('productImage'),
@@ -372,6 +382,41 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
dispatch(popPanel());
}, [dispatch, bgVideoInfo]);
// 리뷰 클릭 핸들러 - ProductAllSection의 UserReviews.jsx 패턴
const handleReviewClick = useCallback(
(review, index) => {
console.log('[UserReviewPanel] Review clicked, opening popup:', {
rvwId: review.rvwId,
index,
});
// 전체 리뷰에서 클릭한 리뷰의 실제 인덱스 찾기
const realIndex = allReviews.findIndex((r) => r.rvwId === review.rvwId);
setSelectedImageIndex(realIndex >= 0 ? realIndex : index);
setPopupMode('user-reviews');
setIsPopupOpen(true);
},
[allReviews]
);
// 팝업 닫기 핸들러
const handleClosePopup = useCallback(() => {
console.log('[UserReviewPanel] Closing popup');
setIsPopupOpen(false);
setPopupMode('user-reviews');
setSelectedImageIndex(0);
}, []);
// 팝업 모드 변경 핸들러
const handleModeChange = useCallback((newMode, imageIndex = 0) => {
console.log('[UserReviewPanel] Mode change requested:', { newMode, imageIndex });
setPopupMode(newMode);
if (newMode === 'all-images' || newMode === 'user-reviews') {
setSelectedImageIndex(imageIndex);
}
}, []);
return (
<TPanel
className={classNames(css.userReviewPanel, className)}
@@ -427,7 +472,7 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
</div>
<div className={css.reviewsSection__filters__group}>
<FilterItemButton
text={`All star(${ratingFilterData['ALL'] || reviewCount || 0})`}
text={`All (${ratingFilterData['ALL'] || reviewCount || 0})`}
onClick={handleAllStarsFilter}
spotlightId="filter-all-stars"
ariaLabel="Filter by all star ratings"
@@ -564,27 +609,31 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
) : (
// 폴백: API 데이터 없을 경우 기존 하드코딩된 버튼 표시
<>
<FilterItemButton
text={`Positive (${filterCounts?.sentiment?.positive || 0})`}
onClick={handlePositiveClick}
spotlightId="filter-positive"
ariaLabel="Filter by positive sentiment"
dataSpotlightUp="filter-quality"
dataSpotlightDown="filter-negative"
isActive={
currentFilter.type === 'sentiment' && currentFilter.value === 'positive'
}
/>
<FilterItemButton
text={`Negative (${filterCounts?.sentiment?.negative || 0})`}
onClick={handleNegativeClick}
spotlightId="filter-negative"
ariaLabel="Filter by negative sentiment"
dataSpotlightUp="filter-positive"
isActive={
currentFilter.type === 'sentiment' && currentFilter.value === 'negative'
}
/>
{(filterCounts?.sentiment?.positive || 0) > 0 && (
<FilterItemButton
text={`Positive (${filterCounts?.sentiment?.positive || 0})`}
onClick={handlePositiveClick}
spotlightId="filter-positive"
ariaLabel="Filter by positive sentiment"
dataSpotlightUp="filter-quality"
dataSpotlightDown="filter-negative"
isActive={
currentFilter.type === 'sentiment' && currentFilter.value === 'positive'
}
/>
)}
{(filterCounts?.sentiment?.negative || 0) > 0 && (
<FilterItemButton
text={`Negative (${filterCounts?.sentiment?.negative || 0})`}
onClick={handleNegativeClick}
spotlightId="filter-negative"
ariaLabel="Filter by negative sentiment"
dataSpotlightUp={filterCounts?.sentiment?.positive > 0 ? "filter-positive" : "filter-quality"}
isActive={
currentFilter.type === 'sentiment' && currentFilter.value === 'negative'
}
/>
)}
</>
)}
</div>
@@ -609,10 +658,32 @@ const UserReviewPanel = ({ className, panelInfo, spotlightId }) => {
onPrevPage={handlePrevPage}
isPaging={isPaging}
showAllReviews
panelInfo={{
prdtId: prdtId,
patnrId: patnrId,
productImage: productImage,
brandLogo: brandLogo,
productName: productName,
avgRating: avgRating,
reviewCount: reviewCount,
}}
onReviewClick={handleReviewClick}
/>
</div>
</div>
</TBody>
{/* UserReviewsPopup 추가 - ProductAllSection과 동일한 방식 */}
<UserReviewsPopup
open={isPopupOpen}
onClose={handleClosePopup}
mode={popupMode}
images={extractImagesFromReviews}
selectedImageIndex={selectedImageIndex}
reviewsWithImages={getReviewsWithImages}
allReviews={allReviews}
onModeChange={handleModeChange}
/>
</TPanel>
);
};

View File

@@ -39,15 +39,15 @@
}
}
// 선택된 상태: 회색 배경 (포커스보다 우선도 높게)
// 선택된 상태: 기본 배경색과 동일하게 (활성화 표시 안 함)
&--active {
display: flex;
padding: 20px;
flex-direction: column;
align-items: flex-start;
background: #7a808d !important; // 회색 (선택됨)
background: #4a4c50 !important; // 기본 배경색과 동일
border-radius: 100px;
border: 1px solid #7a808d !important;
border: 1px solid #585858 !important;
white-space: nowrap;
}
@@ -61,7 +61,7 @@
&--active {
text-align: center;
color: white;
color: #eaeaea; // 기본 텍스트색과 동일
font-size: 24px;
font-family: "LG Smart UI";
font-weight: 400;

View File

@@ -47,6 +47,11 @@ const UserReviewItem = ({
return iso.replace(/-/g, '.');
};
// review가 없으면 렌더링하지 않음
if (!review) {
return null;
}
const { reviewImageList, rvwRtng, rvwRgstDtt, rvwCtnt, rvwId, wrtrNknm, rvwWrtrId } = review;
return (

View File

@@ -30,6 +30,8 @@ const UserReviewsList = ({
onNextPage,
onPrevPage,
isPaging = false,
panelInfo = null,
onReviewClick, // 상위에서 전달받은 리뷰 클릭 핸들러
}) => {
const handleReviewClick = useCallback((review, index) => {
console.log('[UserReviewsList] Review item clicked:', {
@@ -37,7 +39,12 @@ const UserReviewsList = ({
index: index,
review: review,
});
}, []);
// 상위로 클릭 이벤트 전달
if (onReviewClick) {
onReviewClick(review, index);
}
}, [onReviewClick]);
// ✅ API 필터 활성화 여부 확인
// 필터가 활성화되면 filteredReviewCount (필터링된 리뷰 개수) 사용