[251031] feat: UserReview Filter IF-LGSP-100 적용

🕐 커밋 시간: 2025. 10. 31. 12:38:53

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/productActions.js
  ~ com.twin.app.shoptime/src/api/apiConfig.js
  ~ com.twin.app.shoptime/src/hooks/useReviews/useReviews.js
  ~ com.twin.app.shoptime/src/reducers/productReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchInputOverlay.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 대규모 기능 개발
  • 모듈 구조 개선
This commit is contained in:
2025-10-31 12:38:54 +09:00
parent 92b57ad3b7
commit 240ffa889e
7 changed files with 248 additions and 26 deletions

View File

@@ -169,6 +169,7 @@ export const types = {
CLEAR_PRODUCT_OPTIONS: 'CLEAR_PRODUCT_OPTIONS',
GET_USER_REVIEW: 'GET_USER_REVIEW',
GET_USER_REVIEW_LIST: 'GET_USER_REVIEW_LIST',
GET_REVIEW_FILTERS: 'GET_REVIEW_FILTERS',
TOGGLE_SHOW_ALL_REVIEWS: 'TOGGLE_SHOW_ALL_REVIEWS',
RESET_SHOW_ALL_REVIEWS: 'RESET_SHOW_ALL_REVIEWS',

View File

@@ -311,24 +311,34 @@ export const getVideoIndicatorFocus = (focused) => (dispatch) => {
// User Review List 추가 조회 IF-LGSP-101
export const getUserReviewList = (requestParams) => (dispatch, getState) => {
const {
prdtId,
patnrId,
filterTpCd = 'ALL',
filterTpVal,
pageSize = 5,
pageNo = 1
const {
prdtId,
patnrId,
filterTpCd = 'ALL',
filterTpVal,
pageSize = 5,
pageNo = 1
} = requestParams;
const params = {
prdtId,
patnrId,
const params = {
prdtId,
patnrId,
filterTpCd,
pageSize,
pageNo
pageNo,
// 우선순위 1: cntryCd 기본값 'US' 설정 (TV 환경에서는 자동으로 header로 전달됨)
cntryCd: 'US'
};
if (filterTpCd !== 'ALL' && filterTpVal) {
// 우선순위 2: filterTpVal 조건부 필수 검증 강화
if (filterTpCd !== 'ALL') {
if (!filterTpVal) {
console.warn('[UserReviewList] ⚠️ filterTpCd가 ALL이 아니면 filterTpVal은 필수입니다', {
filterTpCd,
filterTpVal,
message: 'filterTpVal이 제공되지 않았습니다'
});
}
params.filterTpVal = filterTpVal;
}
@@ -406,4 +416,146 @@ export const getUserReviewList = (requestParams) => (dispatch, getState) => {
};
TAxios(dispatch, getState, 'get', URLS.GET_USER_REVIEW_LIST, params, body, onSuccess, onFail);
};
// Review Filters 추출 함수 (IF-LGSP-100)
const extractReviewFiltersApiData = (apiResponse) => {
try {
console.log('[ReviewFilters] 📥 extractReviewFiltersApiData 호출 - 원본 응답:', apiResponse);
let data = null;
// 응답 구조 분석
if (apiResponse && apiResponse.data) {
// data 경로에서 추출
const apiData = apiResponse.data;
const reviewFilterInfos = apiData.reviewFilterInfos || {};
data = reviewFilterInfos;
console.log('[ReviewFilters] 📊 apiResponse.data 경로에서 추출:', {
patnrId: data.patnrId,
prdtId: data.prdtId,
filtersLength: data.filters ? data.filters.length : 0,
filters: data.filters
});
} else if (apiResponse) {
// 직접 경로에서 추출
data = apiResponse.reviewFilterInfos || apiResponse;
console.log('[ReviewFilters] 📊 직접 경로에서 추출:', {
patnrId: data.patnrId,
prdtId: data.prdtId,
filtersLength: data.filters ? data.filters.length : 0,
filters: data.filters
});
}
if (!data || !data.filters) {
console.warn('[ReviewFilters] ⚠️ filters가 없음:', apiResponse);
return null;
}
console.log('[ReviewFilters] ✅ 추출 완료:', {
patnrId: data.patnrId,
prdtId: data.prdtId,
filtersLength: data.filters.length
});
return data;
} catch (error) {
console.error('[ReviewFilters] ❌ extractReviewFiltersApiData 에러:', error);
return null;
}
};
// Review Filters 조회 IF-LGSP-100
export const getReviewFilters = (requestParams) => (dispatch, getState) => {
const {
prdtId,
patnrId
} = requestParams;
const params = {
prdtId,
patnrId,
// 우선순위 1: cntryCd 기본값 'US' 설정 (TV 환경에서는 자동으로 header로 전달됨)
cntryCd: 'US'
};
const body = {};
console.log('[ReviewFilters] 🚀 API 요청 시작:', {
requestParams,
params,
body,
url: URLS.GET_REVIEW_FILTERS,
timestamp: new Date().toISOString()
});
const onSuccess = (response) => {
console.log('[ReviewFilters] ✅ API 성공 응답:', {
status: response.status,
statusText: response.statusText,
retCode: response.data && response.data.retCode,
retMsg: response.data && response.data.retMsg,
hasData: !!(response.data && response.data.data),
fullResponse: response.data,
});
const filtersData = extractReviewFiltersApiData(response.data);
console.log('[ReviewFilters] 📊 추출된 필터 데이터:', {
hasData: !!filtersData,
patnrId: filtersData && filtersData.patnrId,
prdtId: filtersData && filtersData.prdtId,
filtersLength: filtersData && filtersData.filters ? filtersData.filters.length : 0,
filters: filtersData && filtersData.filters
});
if (filtersData) {
console.log('[ReviewFilters] 🔴 dispatch 직전 상태:', {
actionType: types.GET_REVIEW_FILTERS,
typeValue: 'GET_REVIEW_FILTERS',
patnrId: patnrId,
prdtId: prdtId
});
const action = {
type: types.GET_REVIEW_FILTERS,
payload: {
...filtersData,
prdtId: prdtId,
patnrId: patnrId
},
};
console.log('[ReviewFilters] 🟡 dispatch할 액션:', JSON.stringify(action, null, 2));
dispatch(action);
console.log('[ReviewFilters] 📦 데이터 디스패치 완료:', {
patnrId,
prdtId,
filtersLength: filtersData.filters ? filtersData.filters.length : 0
});
} else {
console.warn('[ReviewFilters] ⚠️ 필터 데이터 추출 실패');
}
};
const onFail = (error) => {
console.error('[ReviewFilters] ❌ API 실패:', {
message: error.message,
status: error.response && error.response.status,
statusText: error.response && error.response.statusText,
responseData: error.response && error.response.data,
requestParams: requestParams,
params: params,
url: URLS.GET_REVIEW_FILTERS,
fullError: error,
});
};
TAxios(dispatch, getState, 'get', URLS.GET_REVIEW_FILTERS, params, body, onSuccess, onFail);
};

View File

@@ -59,6 +59,8 @@ export const URLS = {
GET_PRODUCT_GROUP: "/lgsp/v1/product/group.lge",
GET_PRODUCT_OPTION: "/lgsp/v1/product/option.lge",
GET_USER_REVEIW: "/lgsp/v1/product/reviews.lge",
// IF-LGSP-100 신규 - Review Filters API 추가 251031
GET_REVIEW_FILTERS: "/lgsp/v1/product/review/filter.lge",
// IF-LGSP-101 신규 - Reviews API 추가 251029
GET_USER_REVIEW_LIST: "/lgsp/v1/product/review/list.lge",

View File

@@ -1,6 +1,6 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getUserReviews, getUserReviewList } from '../../actions/productActions';
import { getUserReviews, getUserReviewList, getReviewFilters } from '../../actions/productActions';
import fp from '../../utils/fp';
const DISPLAY_SIZE = 3; // 화면에 표시할 리뷰 개수
@@ -67,6 +67,22 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
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);
// 빈 내용 리뷰 필터링 - 의미있는 리뷰만 표시
const allReviews = (reviewData?.reviewList || []).filter((review) => {
const content = review.rvwCtnt?.trim();
@@ -152,6 +168,13 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
pageSize: 100,
pageNo: 1
}));
// IF-LGSP-100 필터 데이터 조회
console.log('[useReviews] 🔄 getReviewFilters 호출 중... (IF-LGSP-100)');
await dispatch(getReviewFilters({
prdtId,
patnrId
}));
}
setHasLoadedData(true);
} catch (error) {
@@ -163,20 +186,24 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
// prdtId가 변경되면 자동으로 리뷰 데이터 로드 (싱글톤 패턴)
// 중요: reviewVersion이 다르면 다른 Redux 상태를 사용하므로 reviewVersion도 체크해야 함
// 우선순위 3: 필터 캐싱 로직 완성
useEffect(() => {
if (prdtId && patnrId) {
const needsLoad = prdtId !== loadedPrdtId;
const needsReviewLoad = prdtId !== loadedPrdtId;
const needsFiltersLoad = prdtId !== loadedFiltersPrdtId;
// console.log('[useReviews] 🔄 로드 필요 여부 체크:', {
// prdtId,
// patnrId,
// loadedPrdtId,
// reviewVersion,
// needsLoad,
// reason: needsLoad ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
// });
console.log('[useReviews] 🔄 로드 필요 여부 체크 (캐싱 로직 포함):', {
prdtId,
patnrId,
loadedPrdtId,
loadedFiltersPrdtId,
reviewVersion,
needsReviewLoad,
needsFiltersLoad,
reason: (needsReviewLoad || needsFiltersLoad) ? 'prdtId가 변경됨' : '캐시된 데이터 사용'
});
if (needsLoad) {
if (needsReviewLoad || needsFiltersLoad) {
// prdtId 변경 시 로딩 상태 즉시 설정으로 UI 깜빡임 방지
setIsLoading(true);
setHasLoadedData(false);
@@ -186,7 +213,7 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
setHasLoadedData(true);
}
}
}, [prdtId, patnrId, loadedPrdtId, loadReviews]);
}, [prdtId, patnrId, loadedPrdtId, loadedFiltersPrdtId, loadReviews]);
// 리뷰 데이터가 로드되면 로딩 상태 업데이트
useEffect(() => {
@@ -732,6 +759,10 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
hasLoadedData, // 데이터 로드 완료 여부
stats, // 통계 정보
// 🔤 필터 옵션 데이터 (IF-LGSP-100)
reviewFiltersData, // 필터 옵션 전체 데이터
filters: reviewFiltersData?.filters || [], // 필터 배열 (RATING, KEYWORDS, SENTIMENT)
// 🐛 디버그 정보
_debug: {
prdtId,
@@ -745,6 +776,7 @@ const useReviews = (prdtId, patnrId, _deprecatedReviewVersion) => {
hasMore,
isLoading,
hasLoadedData,
reviewFiltersData,
},
};
};

View File

@@ -11,6 +11,9 @@ const initialState = {
// ReviewVersion 2 (신 API)
reviewListData: null,
loadedListPrdtId: null,
// Review Filters (IF-LGSP-100)
reviewFiltersData: null,
loadedFiltersPrdtId: null,
// 기타
showAllReviews: false,
};
@@ -78,6 +81,21 @@ const handleResetShowAllReviews = curry((state, action) => {
return set('showAllReviews', false, state);
});
// 리뷰 필터 데이터 핸들러 (v2 - 신 API)
const handleReviewFilters = curry((state, action) => {
const reviewFiltersData = get('payload', action);
const prdtId = get(['payload', 'prdtId'], action);
console.log('[productReducer_reviewFilters] 🟡 handleReviewFilters:', {
prdtId,
reviewFiltersDataKeys: reviewFiltersData ? Object.keys(reviewFiltersData) : 'null',
filtersLength: reviewFiltersData?.filters?.length || 0,
filters: reviewFiltersData?.filters
});
return set('reviewFiltersData', reviewFiltersData, set('loadedFiltersPrdtId', prdtId, state));
});
const handlers = {
[types.GET_BEST_SELLER]: handleBestSeller,
[types.GET_PRODUCT_OPTION]: handleProductOption,
@@ -88,6 +106,7 @@ const handlers = {
[types.GET_PRODUCT_OPTION_ID]: handleProductOptionId,
// [types.GET_USER_REVIEW]: handleUserReview,
[types.GET_USER_REVIEW_LIST]: handleUserReviewList,
[types.GET_REVIEW_FILTERS]: handleReviewFilters,
[types.TOGGLE_SHOW_ALL_REVIEWS]: handleToggleShowAllReviews,
[types.RESET_SHOW_ALL_REVIEWS]: handleResetShowAllReviews,
};

View File

@@ -276,6 +276,22 @@ export default function ProductAllSection({
selectedReduxState: _debug?.reviewVersion === 1 ? 'reviewData' : 'reviewListData'
});
// 별점 높은 순으로 정렬된 상위 5개 리뷰 (방법 2: useMemo 최적화)
const topRatedPreviewReviews = useMemo(() => {
if (!previewReviews || previewReviews.length === 0) {
return [];
}
return previewReviews
.slice() // 원본 배열 보존
.sort((a, b) => {
const ratingA = a.rvwRtng || 0;
const ratingB = b.rvwRtng || 0;
return ratingB - ratingA; // 내림차순 (높은 별점부터)
})
.slice(0, 5);
}, [previewReviews]);
// YouMayAlsoLike 데이터 확인
const hasYouMayAlsoLike = youmaylikeData && youmaylikeData.length > 0;
@@ -950,7 +966,7 @@ export default function ProductAllSection({
productInfo={productData}
panelInfo={panelInfo}
reviewsData={{
previewReviews: previewReviews.slice(0, 5), // 처음 5개
previewReviews: topRatedPreviewReviews, // 별점 높은 순 상위 5개
stats: stats,
isLoading: reviewsLoading,
}}

View File

@@ -142,7 +142,7 @@ const SearchInputOverlay = ({
// ✨ [Phase 3] 동적 placeholder 텍스트
const placeholderText = useMemo(() => {
if (inputFocused) {
return 'Start typing to search...';
return 'Search products or brands';
}
return 'Ready to input..';
}, [inputFocused]);