[251011] fix: MediaPlayer작업

🕐 커밋 시간: 2025. 10. 11. 22:30:02

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +169줄
  • 삭제: -161줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.backup.jsx

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/productActions.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx (javascript):
    🔄 Modified: Spottable()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
This commit is contained in:
2025-10-11 22:30:05 +09:00
parent d3117351c0
commit c8416b90f3
5 changed files with 458 additions and 335 deletions

View File

@@ -1,37 +1,37 @@
import { URLS } from "../api/apiConfig";
import { TAxios } from "../api/TAxios";
import { types } from "./actionTypes";
import { changeAppStatus } from "./commonActions";
import { reduce, set, get } from "../utils/fp";
import { URLS } from '../api/apiConfig';
import { TAxios } from '../api/TAxios';
import { types } from './actionTypes';
import { changeAppStatus } from './commonActions';
import { reduce, set, get } from '../utils/fp';
// CustomerImages용 리뷰 이미지 import
import reviewSampleImage from "../../assets/images/image-review-sample-1.png";
import reviewSampleImage from '../../assets/images/image-review-sample-1.png';
// Best Seller 상품 목록 조회 IF-LGSP-303
// FP helpers
const pickParams = (keys) => (src) =>
reduce(
(acc, key) =>
src && src[key] !== null && src[key] !== undefined
? set(key, src[key], acc)
: acc,
src && src[key] !== null && src[key] !== undefined ? set(key, src[key], acc) : acc,
{},
keys
);
// Generic request thunk factory
const createRequestThunk = ({
method,
url,
type,
params = () => ({}),
data = () => ({}),
tag,
onSuccessExtra = () => (dispatch, getState, response) => {},
onFailExtra = () => (dispatch, getState, error) => {},
selectPayload = (response) => get("data.data", response),
}) =>
(props) => (dispatch, getState) => {
const createRequestThunk =
({
method,
url,
type,
params = () => ({}),
data = () => ({}),
tag,
onSuccessExtra = () => (dispatch, getState, response) => {},
onFailExtra = () => (dispatch, getState, error) => {},
selectPayload = (response) => get('data.data', response),
}) =>
(props) =>
(dispatch, getState) => {
const query = params(props);
const body = data(props);
@@ -46,75 +46,57 @@ const createRequestThunk = ({
onFailExtra(props, dispatch, getState, error);
};
TAxios(
dispatch,
getState,
method,
url,
query,
body,
onSuccess,
onFail
);
TAxios(dispatch, getState, method, url, query, body, onSuccess, onFail);
};
// Specialization for GET
const createGetThunk = ({ url, type, params = () => ({}), tag }) =>
createRequestThunk({ method: "get", url, type, params, tag });
createRequestThunk({ method: 'get', url, type, params, tag });
export const getBestSeller = (callback) => (dispatch, getState) => {
const onSuccess = (response) => {
console.log("getBestSeller onSuccess", response.data);
dispatch({ type: types.GET_BEST_SELLER, payload: get("data.data", response) });
console.log('getBestSeller onSuccess', response.data);
dispatch({ type: types.GET_BEST_SELLER, payload: get('data.data', response) });
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
callback && callback();
};
const onFail = (error) => {
console.error("getBestSeller onFail", error);
console.error('getBestSeller onFail', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
callback && callback();
};
TAxios(
dispatch,
getState,
"get",
URLS.GET_PRODUCT_BESTSELLER,
{},
{},
onSuccess,
onFail
);
TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT_BESTSELLER, {}, {}, onSuccess, onFail);
};
// Detail 옵션상품 정보 조회 IF-LGSP-319
export const getProductGroup = createGetThunk({
url: URLS.GET_PRODUCT_GROUP,
type: types.GET_PRODUCT_GROUP,
params: pickParams(["patnrId", "prdtId"]),
tag: "getProductGroup",
params: pickParams(['patnrId', 'prdtId']),
tag: 'getProductGroup',
});
// Detail 옵션상품 정보 조회 IF-LGSP-320
export const getProductOption = createGetThunk({
url: URLS.GET_PRODUCT_OPTION,
type: types.GET_PRODUCT_OPTION,
params: pickParams(["patnrId", "prdtId"]),
tag: "getProductOption",
params: pickParams(['patnrId', 'prdtId']),
tag: 'getProductOption',
});
// FP: 실제 API 응답에서 data 부분만 추출 (간소화)
const extractReviewApiData = (apiResponse) => {
try {
console.log("[UserReviews] 🔍 extractReviewApiData - 전체 API 응답 분석:", {
fullApiResponse: apiResponse,
hasData: !!(apiResponse && apiResponse.data),
retCode: apiResponse && apiResponse.retCode,
retMsg: apiResponse && apiResponse.retMsg,
responseKeys: apiResponse ? Object.keys(apiResponse) : [],
dataKeys: apiResponse && apiResponse.data ? Object.keys(apiResponse.data) : []
});
// console.log("[UserReviews] 🔍 extractReviewApiData - 전체 API 응답 분석:", {
// fullApiResponse: apiResponse,
// hasData: !!(apiResponse && apiResponse.data),
// retCode: apiResponse && apiResponse.retCode,
// retMsg: apiResponse && apiResponse.retMsg,
// responseKeys: apiResponse ? Object.keys(apiResponse) : [],
// dataKeys: apiResponse && apiResponse.data ? Object.keys(apiResponse.data) : []
// });
// 여러 가능한 데이터 경로 시도
let apiData = null;
@@ -122,27 +104,31 @@ const extractReviewApiData = (apiResponse) => {
// 1. response.data.data (중첩 구조)
if (apiResponse && apiResponse.data && apiResponse.data.data) {
apiData = apiResponse.data.data;
console.log("[UserReviews] ✅ 데이터 경로 1: response.data.data 사용");
// console.log("[UserReviews] ✅ 데이터 경로 1: response.data.data 사용");
}
// 2. response.data (단일 구조)
else if (apiResponse && apiResponse.data && (apiResponse.data.reviewList || apiResponse.data.reviewDetail)) {
else if (
apiResponse &&
apiResponse.data &&
(apiResponse.data.reviewList || apiResponse.data.reviewDetail)
) {
apiData = apiResponse.data;
console.log("[UserReviews] ✅ 데이터 경로 2: response.data 사용");
// console.log("[UserReviews] ✅ 데이터 경로 2: response.data 사용");
}
// 3. response 직접 (최상위)
else if (apiResponse && (apiResponse.reviewList || apiResponse.reviewDetail)) {
apiData = apiResponse;
console.log("[UserReviews] ✅ 데이터 경로 3: response 직접 사용");
// console.log("[UserReviews] ✅ 데이터 경로 3: response 직접 사용");
}
if (!apiData) {
console.warn("[UserReviews] ❌ 모든 데이터 경로에서 추출 실패:", {
hasResponseData: !!(apiResponse && apiResponse.data),
hasReviewList: !!(apiResponse && apiResponse.data && apiResponse.data.reviewList),
hasReviewDetail: !!(apiResponse && apiResponse.data && apiResponse.data.reviewDetail),
hasNestedData: !!(apiResponse && apiResponse.data && apiResponse.data.data),
fullResponse: apiResponse
});
// console.warn("[UserReviews] ❌ 모든 데이터 경로에서 추출 실패:", {
// hasResponseData: !!(apiResponse && apiResponse.data),
// hasReviewList: !!(apiResponse && apiResponse.data && apiResponse.data.reviewList),
// hasReviewDetail: !!(apiResponse && apiResponse.data && apiResponse.data.reviewDetail),
// hasNestedData: !!(apiResponse && apiResponse.data && apiResponse.data.data),
// fullResponse: apiResponse
// });
return null;
}
@@ -159,7 +145,7 @@ const extractReviewApiData = (apiResponse) => {
return apiData;
} catch (error) {
console.error("[UserReviews] ❌ extractReviewApiData 에러:", error);
console.error('[UserReviews] ❌ extractReviewApiData 에러:', error);
return null;
}
};
@@ -167,16 +153,16 @@ const extractReviewApiData = (apiResponse) => {
// Mock 데이터 생성 함수 (재사용성 위해 분리) - 100개 리뷰와 많은 이미지 포함
const createMockReviewData = () => {
const reviewTexts = [
"The shoes are really stylish and comfortable for daily wear. I love the design and how lightweight they feel.",
"Great value for the price! The quality is better than I expected. Shipping was fast and the product arrived in perfect condition.",
"Amazing product! Really happy with this purchase. The color is exactly as shown in the pictures.",
"Good quality overall but took a while to arrive. Packaging was excellent though.",
"Perfect fit and very comfortable. Would definitely buy again from this seller.",
"Beautiful design and great materials. My family loves it!",
"Exceeded my expectations. Really impressed with the craftsmanship.",
"Good product but could be better. Price is reasonable for what you get.",
"Love the style and functionality. Works exactly as described.",
"Fantastic quality! Highly recommend this to anyone looking for this type of product."
'The shoes are really stylish and comfortable for daily wear. I love the design and how lightweight they feel.',
'Great value for the price! The quality is better than I expected. Shipping was fast and the product arrived in perfect condition.',
'Amazing product! Really happy with this purchase. The color is exactly as shown in the pictures.',
'Good quality overall but took a while to arrive. Packaging was excellent though.',
'Perfect fit and very comfortable. Would definitely buy again from this seller.',
'Beautiful design and great materials. My family loves it!',
'Exceeded my expectations. Really impressed with the craftsmanship.',
'Good product but could be better. Price is reasonable for what you get.',
'Love the style and functionality. Works exactly as described.',
'Fantastic quality! Highly recommend this to anyone looking for this type of product.',
];
const reviewList = [];
@@ -191,7 +177,7 @@ const createMockReviewData = () => {
reviewImageList.push({
imgId: `mock-img-${i}-${j + 1}`,
imgUrl: reviewSampleImage,
imgSeq: j + 1
imgSeq: j + 1,
});
}
}
@@ -204,7 +190,7 @@ const createMockReviewData = () => {
rvwRgstDtt: `2024-01-${String((i % 30) + 1).padStart(2, '0')}`,
wrtrNknm: `User${i}`,
rvwWrtrId: `user${i}`,
reviewImageList
reviewImageList,
});
}
@@ -213,95 +199,95 @@ const createMockReviewData = () => {
reviewDetail: {
totRvwCnt: 100,
totRvwAvg: 4.2,
avgRvwScr: 4.2
}
avgRvwScr: 4.2,
},
};
};
// showAllReviews 상태 토글
export const toggleShowAllReviews = () => ({
type: types.TOGGLE_SHOW_ALL_REVIEWS
type: types.TOGGLE_SHOW_ALL_REVIEWS,
});
// showAllReviews 상태 초기화 (ProductAllSection 마운트 시 사용)
export const resetShowAllReviews = () => ({
type: types.RESET_SHOW_ALL_REVIEWS
type: types.RESET_SHOW_ALL_REVIEWS,
});
// 상품별 유저 리뷰 리스트 조회 : IF-LGSP-0002
export const getUserReviews = (requestParams) => (dispatch, getState) => {
const { prdtId, patnrId } = requestParams;
console.log("[UserReviews] 🚀 getUserReviews 액션 시작:", {
requestParams,
prdtId,
patnrId,
timestamp: new Date().toISOString()
});
// console.log("[UserReviews] 🚀 getUserReviews 액션 시작:", {
// requestParams,
// prdtId,
// patnrId,
// timestamp: new Date().toISOString()
// });
// ==================== [임시 테스트] 시작 - 랜덤 prdtId 사용 ====================
// 테스트용 prdtId 목록 - 실제 리뷰 데이터가 있는 상품들
const testProductIds = [
"LCE3010SB",
"100QNED85AU",
"14Z90Q-K.ARW3U1",
"16Z90Q-K.AAC7U1",
"24GN600-B",
"50UT8000AUA",
"A949KTMS",
"AGF76631064",
"C5323B0",
"DLE3600V"
'LCE3010SB',
'100QNED85AU',
'14Z90Q-K.ARW3U1',
'16Z90Q-K.AAC7U1',
'24GN600-B',
'50UT8000AUA',
'A949KTMS',
'AGF76631064',
'C5323B0',
'DLE3600V',
];
const randomIndex = Math.floor(Math.random() * testProductIds.length);
const randomPrdtId = testProductIds[randomIndex];
console.log("[UserReviews]-API 🎲 랜덤 prdtId 선택:", {
originalPrdtId: prdtId,
originalPatnrId: patnrId,
randomPrdtId: randomPrdtId,
fixedPatnrId: fixedPatnrId,
randomIndex: randomIndex,
testProductIds: testProductIds
});
// console.log("[UserReviews]-API 🎲 랜덤 prdtId 선택:", {
// originalPrdtId: prdtId,
// originalPatnrId: patnrId,
// randomPrdtId: randomPrdtId,
// fixedPatnrId: fixedPatnrId,
// randomIndex: randomIndex,
// testProductIds: testProductIds
// });
// ==================== [임시 테스트] 끝 ====================
// TAxios 파라미터 준비 - 랜덤 prdtId와 고정 patnrId 사용
const fixedPatnrId = "9"; // patnrId 고정값
const fixedPatnrId = '9'; // patnrId 고정값
const params = { prdtId: randomPrdtId, patnrId: fixedPatnrId };
const body = {}; // GET이므로 빈 객체
// plat_cd 값 확인을 위한 httpHeader 로깅
const currentState = getState();
const httpHeader = currentState.common.httpHeader || {};
console.log("[UserReviews] <20> plat_cd 값 확인:", {
platCd: httpHeader.plat_cd,
fullHttpHeader: httpHeader,
hasHttpHeader: !!httpHeader,
httpHeaderKeys: Object.keys(httpHeader)
});
// console.log("[UserReviews] <20> plat_cd 값 확인:", {
// platCd: httpHeader.plat_cd,
// fullHttpHeader: httpHeader,
// hasHttpHeader: !!httpHeader,
// httpHeaderKeys: Object.keys(httpHeader)
// });
console.log("[UserReviews]-API <20>📡 TAxios 호출 준비:", {
method: "get",
url: URLS.GET_USER_REVEIW,
params,
body,
originalPrdtId: prdtId,
randomPrdtId: randomPrdtId,
originalPatnrId: patnrId,
fixedPatnrId: fixedPatnrId,
platCd: httpHeader.plat_cd
});
// console.log("[UserReviews]-API <20>📡 TAxios 호출 준비:", {
// method: "get",
// url: URLS.GET_USER_REVEIW,
// params,
// body,
// originalPrdtId: prdtId,
// randomPrdtId: randomPrdtId,
// originalPatnrId: patnrId,
// fixedPatnrId: fixedPatnrId,
// platCd: httpHeader.plat_cd
// });
const onSuccess = (response) => {
console.log("[UserReviews]-API ✅ API 성공 응답:", {
console.log('[UserReviews]-API ✅ API 성공 응답:', {
status: response.status,
statusText: response.statusText,
headers: response.headers,
retCode: response.data && response.data.retCode,
retMsg: response.data && response.data.retMsg,
hasData: !!(response.data && response.data.data),
fullResponse: response.data
fullResponse: response.data,
});
// ==================== [임시 테스트] 빈 리뷰 응답 시뮬레이션 - 코멘트 처리 ====================
@@ -326,12 +312,13 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
// ==================== [임시 테스트] 끝 - 코멘트 처리 ====================
if (response.data && response.data.data) {
console.log("[UserReviews] 📊 API 데이터 상세:", {
console.log('[UserReviews] 📊 API 데이터 상세:', {
reviewListLength: response.data.data.reviewList ? response.data.data.reviewList.length : 0,
reviewDetail: response.data.data.reviewDetail,
reviewList_sample: response.data.data.reviewList && response.data.data.reviewList[0] || "empty",
reviewList_sample:
(response.data.data.reviewList && response.data.data.reviewList[0]) || 'empty',
totRvwCnt: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwCnt,
totRvwAvg: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwAvg
totRvwAvg: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwAvg,
});
}
@@ -339,14 +326,14 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
const apiData = extractReviewApiData(response.data);
if (apiData) {
console.log("[UserReviews] ✅ 실제 API 데이터 사용");
console.log('[UserReviews] ✅ 실제 API 데이터 사용');
dispatch({
type: types.GET_USER_REVIEW,
payload: { ...apiData, prdtId: prdtId } // 원래 prdtId로 저장 (UI에서 사용)
payload: { ...apiData, prdtId: prdtId }, // 원래 prdtId로 저장 (UI에서 사용)
});
console.log("[UserReviews] 📦 실제 API 데이터 디스패치 완료:", apiData);
console.log('[UserReviews] 📦 실제 API 데이터 디스패치 완료:', apiData);
} else {
console.log("[UserReviews] ⚠️ API 데이터 추출 실패");
console.log('[UserReviews] ⚠️ API 데이터 추출 실패');
// Mock 데이터 사용 비활성화
// const mockData = createMockReviewData();
// dispatch({
@@ -358,17 +345,17 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
};
const onFail = (error) => {
console.error("[UserReviews] ❌ API 실패:", {
console.error('[UserReviews] ❌ API 실패:', {
message: error.message,
status: error.response && error.response.status,
statusText: error.response && error.response.statusText,
responseData: error.response && error.response.data,
requestParams: requestParams,
url: URLS.GET_USER_REVEIW,
fullError: error
fullError: error,
});
console.log("[UserReviews] 🔄 API 실패 - Mock 데이터 사용 비활성화");
console.log('[UserReviews] 🔄 API 실패 - Mock 데이터 사용 비활성화');
// Mock 데이터 사용 비활성화
// const mockData = createMockReviewData();
// dispatch({
@@ -378,17 +365,8 @@ export const getUserReviews = (requestParams) => (dispatch, getState) => {
// console.log("[UserReviews] 📦 Mock 데이터 디스패치 완료 (API 실패):", mockData);
};
console.log("[UserReviews]-API 🔗 TAxios 호출 실행 중...");
TAxios(
dispatch,
getState,
"get",
URLS.GET_USER_REVEIW,
params,
body,
onSuccess,
onFail
);
// console.log("[UserReviews]-API 🔗 TAxios 호출 실행 중...");
TAxios(dispatch, getState, 'get', URLS.GET_USER_REVEIW, params, body, onSuccess, onFail);
};
export const getProductOptionId = (id) => (dispatch) => {
@@ -409,7 +387,7 @@ export const getProductImageLength =
export const getVideoIndicatorFocus = (focused) => (dispatch) => {
dispatch({
type: types.GET_VIDEO_INDECATOR_FOCUS,
type: types.GET_VIDEO_INDECATOR_FOCUS,
payload: focused,
});
};
};

View File

@@ -0,0 +1,174 @@
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';
import css from './ProductVideo.module.less';
const SpottableComponent = Spottable('div');
export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
const dispatch = useDispatch();
// PlayerPanel 상태 체크를 위한 selectors 추가 (Indicator.jsx와 동일)
const panels = useSelector((state) => state.panels.panels);
const [isLaunchedFromPlayer, setIsLaunchedFromPlayer] = useState(false);
const [focused, setFocused] = useState(false);
const [modalState, setModalState] = useState(true); // 모달 상태 관리 추가
const topPanel = panels[panels.length - 1];
// Indicator.jsx의 PlayerPanel 상태 체크 로직 추가 + 모달 상태 복원
useEffect(() => {
if (
topPanel &&
topPanel.name === panel_names.PLAYER_PANEL &&
topPanel.panelInfo.modal === false
) {
return; // PlayerPanel이 전체화면 모드일 때는 처리하지 않음
}
// PlayerPanel이 닫혔을 때 modalState를 true로 복원
if (!topPanel || topPanel.name !== panel_names.PLAYER_PANEL) {
setModalState(true);
}
}, [topPanel]);
// Indicator.jsx의 canPlayVideo 로직 완전 이식 (selectedIndex === 0 조건 포함)
const canPlayVideo = useMemo(() => {
return Boolean(productInfo?.prdtMediaUrl);
}, [productInfo]);
// Indicator.jsx의 modalClassNameChange 로직 추가
const modalClassNameChange = useCallback(() => {
if (focused) {
return css.videoModal;
}
return '';
}, [focused]);
// focus 이벤트 핸들러 추가 (Indicator.jsx와 동일)
const videoContainerOnFocus = useCallback(() => {
if (canPlayVideo) {
setFocused(true);
}
}, [canPlayVideo]);
const videoContainerOnBlur = useCallback(() => {
if (canPlayVideo) {
setFocused(false);
// ProductVideo에서 포커스가 벗어나면 비디오 재생 종료
dispatch(finishVideoPreview());
}
}, [canPlayVideo, dispatch]);
// Indicator.jsx의 handleVideoOnClick 로직 완전히 이식 + 모달 토글 기능 추가
const handleVideoClick = useCallback(() => {
if (canPlayVideo) {
const currentTopPanel = panels[panels.length - 1];
// 현재 PlayerPanel이 modal=true로 재생 중인지 확인
const isCurrentlyPlayingModal =
currentTopPanel &&
currentTopPanel.name === panel_names.PLAYER_PANEL &&
currentTopPanel.panelInfo.modal === true;
// 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(
startMediaPlayer({
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,
})
);
// 모달 상태가 변경된 경우 상태 업데이트
if (isCurrentlyPlayingModal) {
setModalState(false);
}
}
if (isLaunchedFromPlayer) {
setIsLaunchedFromPlayer(false);
}
}, [
dispatch,
productInfo,
canPlayVideo,
isLaunchedFromPlayer,
modalClassNameChange,
panels,
modalState,
]);
if (!canPlayVideo) return null;
return (
<SpottableComponent
className={css.videoContainer}
onClick={handleVideoClick}
onFocus={videoContainerOnFocus}
onBlur={videoContainerOnBlur}
spotlightId="product-video-player"
aria-label={`${productInfo?.prdtNm} 동영상 재생`}
>
<div className={css.videoThumbnailWrapper}>
<CustomImage
src={thumbnailUrl}
alt={`${productInfo?.prdtNm} 동영상 썸네일`}
className={css.videoThumbnail}
/>
<div className={css.playButtonOverlay}>
<img src={playImg} alt="재생" />
</div>
</div>
</SpottableComponent>
);
}

View File

@@ -1,7 +1,6 @@
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';
@@ -13,7 +12,7 @@ const SpottableComponent = Spottable('div');
export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
const dispatch = useDispatch();
// PlayerPanel 상태 체크를 위한 selectors 추가 (Indicator.jsx와 동일)
// MediaPanel 상태 체크를 위한 selectors 추가
const panels = useSelector((state) => state.panels.panels);
const [isLaunchedFromPlayer, setIsLaunchedFromPlayer] = useState(false);
const [focused, setFocused] = useState(false);
@@ -21,28 +20,28 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
const topPanel = panels[panels.length - 1];
// Indicator.jsx의 PlayerPanel 상태 체크 로직 추가 + 모달 상태 복원
// MediaPanel 상태 체크 로직 + 모달 상태 복원
useEffect(() => {
if (
topPanel &&
topPanel.name === panel_names.PLAYER_PANEL &&
topPanel.name === panel_names.MEDIA_PANEL &&
topPanel.panelInfo.modal === false
) {
return; // PlayerPanel이 전체화면 모드일 때는 처리하지 않음
return; // MediaPanel이 전체화면 모드일 때는 처리하지 않음
}
// PlayerPanel이 닫혔을 때 modalState를 true로 복원
if (!topPanel || topPanel.name !== panel_names.PLAYER_PANEL) {
// MediaPanel이 닫혔을 때 modalState를 true로 복원
if (!topPanel || topPanel.name !== panel_names.MEDIA_PANEL) {
setModalState(true);
}
}, [topPanel]);
// Indicator.jsx의 canPlayVideo 로직 완전 이식 (selectedIndex === 0 조건 포함)
// 비디오 재생 가능 여부 체크
const canPlayVideo = useMemo(() => {
return Boolean(productInfo?.prdtMediaUrl);
}, [productInfo]);
// Indicator.jsx의 modalClassNameChange 로직 추가
// 모달 CSS 클래스 변경 로직
const modalClassNameChange = useCallback(() => {
if (focused) {
return css.videoModal;
@@ -50,7 +49,7 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
return '';
}, [focused]);
// focus 이벤트 핸들러 추가 (Indicator.jsx와 동일)
// 포커스 이벤트 핸들러
const videoContainerOnFocus = useCallback(() => {
if (canPlayVideo) {
setFocused(true);
@@ -60,50 +59,44 @@ export default function ProductVideo({ productInfo, videoUrl, thumbnailUrl }) {
const videoContainerOnBlur = useCallback(() => {
if (canPlayVideo) {
setFocused(false);
// ProductVideo에서 포커스가 벗어나면 비디오 재생 종료
dispatch(finishVideoPreview());
}
}, [canPlayVideo, dispatch]);
// Indicator.jsx의 handleVideoOnClick 로직 완전히 이식 + 모달 토글 기능 추가
// MediaPanel이 modal로 열려있지 않을 때만 종료
// (열려있을 때는 MediaPanel 자체에서 생명주기 관리)
const currentTopPanel = panels[panels.length - 1];
const isMediaPanelModalOpen =
currentTopPanel &&
currentTopPanel.name === panel_names.MEDIA_PANEL &&
currentTopPanel.panelInfo.modal === true;
console.log('[ProductVideo] onBlur:', {
isMediaPanelModalOpen,
currentTopPanelName: currentTopPanel?.name,
willClosePanel: !isMediaPanelModalOpen,
});
if (!isMediaPanelModalOpen) {
console.log('[ProductVideo] Closing MediaPanel from onBlur');
dispatch(finishMediaPreview());
} else {
console.log('[ProductVideo] MediaPanel is open, skipping finishMediaPreview');
}
}
}, [canPlayVideo, dispatch, panels]);
// MediaPanel 비디오 클릭 핸들러 + 모달 토글 기능
const handleVideoClick = useCallback(() => {
if (canPlayVideo) {
const currentTopPanel = panels[panels.length - 1];
// 현재 PlayerPanel이 modal=true로 재생 중인지 확인
// 현재 MediaPanel이 modal=true로 재생 중인지 확인
const isCurrentlyPlayingModal =
currentTopPanel &&
currentTopPanel.name === panel_names.PLAYER_PANEL &&
currentTopPanel.name === panel_names.MEDIA_PANEL &&
currentTopPanel.panelInfo.modal === true;
// 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(
startMediaPlayer({
qrCurrentItem: productInfo,

View File

@@ -1,19 +1,9 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { toggleShowAllReviews } from '../../../../actions/productActions';
@@ -22,26 +12,25 @@ import useScrollTo from '../../../../hooks/useScrollTo';
import { $L } from '../../../../utils/helperMethods';
import StarRating from '../../components/StarRating';
import THeaderDetail from '../../components/THeaderDetail';
import UserReviewsScroller
from '../../components/UserReviewsScroller/UserReviewsScroller';
import UserReviewsScroller from '../../components/UserReviewsScroller/UserReviewsScroller';
import CustomerImages from './CustomerImages/CustomerImages';
import css from './UserReviews.module.less';
import UserReviewsPopup from './UserReviewsPopup/UserReviewsPopup';
const SpottableComponent = Spottable("div");
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator(
{
enterTo: "default-element",
enterTo: 'default-element',
preserveld: true,
leaveFor: {
left: "spotlight-product-info-section-container",
up: "view-all-reviews-button",
left: 'spotlight-product-info-section-container',
up: 'view-all-reviews-button',
},
restrict: "none",
spotlightDirection: "vertical",
restrict: 'none',
spotlightDirection: 'vertical',
},
"div"
'div'
);
export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
@@ -62,28 +51,28 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
// 팝업 상태 관리 - 모드와 선택된 이미지 인덱스 추가
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [popupMode, setPopupMode] = useState("customer-images"); // "customer-images", "all-images"
const [popupMode, setPopupMode] = useState('customer-images'); // "customer-images", "all-images"
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
// Redux에서 showAllReviews 상태 가져오기
const showAllReviews = useSelector((state) => state.product.showAllReviews);
// 디버깅: showAllReviews 상태 변경 확인
useEffect(() => {
console.log("[UserReviews] showAllReviews state changed:", {
showAllReviews,
reviewListLength:
(actualReviewsData.previewReviews &&
actualReviewsData.previewReviews.length) ||
0,
willShowCount: showAllReviews
? (actualReviewsData.previewReviews &&
actualReviewsData.previewReviews.length) ||
0
: 5,
hasReviewsData: !!reviewsData,
isFromProductAllSection: !!reviewsData,
});
}, [showAllReviews, actualReviewsData.previewReviews, reviewsData]);
// useEffect(() => {
// console.log("[UserReviews] showAllReviews state changed:", {
// showAllReviews,
// reviewListLength:
// (actualReviewsData.previewReviews &&
// actualReviewsData.previewReviews.length) ||
// 0,
// willShowCount: showAllReviews
// ? (actualReviewsData.previewReviews &&
// actualReviewsData.previewReviews.length) ||
// 0
// : 5,
// hasReviewsData: !!reviewsData,
// isFromProductAllSection: !!reviewsData,
// });
// }, [showAllReviews, actualReviewsData.previewReviews, reviewsData]);
// showAllReviews 상태 변경 시 TScroller 스크롤 영역 강제 재계산
useEffect(() => {
@@ -94,12 +83,12 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
setTimeout(() => {
if (tScrollerRef.current) {
// TScroller의 스크롤 영역을 강제로 업데이트
if (typeof tScrollerRef.current.calculateMetrics === "function") {
if (typeof tScrollerRef.current.calculateMetrics === 'function') {
tScrollerRef.current.calculateMetrics();
}
// 또는 scrollTo를 호출해서 스크롤 영역 업데이트
if (typeof tScrollerRef.current.scrollTo === "function") {
if (typeof tScrollerRef.current.scrollTo === 'function') {
tScrollerRef.current.scrollTo({
position: { y: 0 },
animate: false,
@@ -122,25 +111,25 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
};
// [UserReviews] 데이터 수신 확인 로그
useEffect(() => {
console.log("[UserReviews] 실제 데이터 확인:", {
reviewListLength: (reviewListData && reviewListData.length) || 0,
totalReviews: actualReviewsData.stats.totalReviews,
averageRating: actualReviewsData.stats.averageRating,
isLoading: actualReviewsData.isLoading,
hasData: reviewListData && reviewListData.length > 0,
dataSource: reviewsData
? "ProductAllSection props"
: "useReviews fallback",
});
}, [reviewListData, actualReviewsData, reviewsData]);
// useEffect(() => {
// console.log("[UserReviews] 실제 데이터 확인:", {
// reviewListLength: (reviewListData && reviewListData.length) || 0,
// totalReviews: actualReviewsData.stats.totalReviews,
// averageRating: actualReviewsData.stats.averageRating,
// isLoading: actualReviewsData.isLoading,
// hasData: reviewListData && reviewListData.length > 0,
// dataSource: reviewsData
// ? "ProductAllSection props"
// : "useReviews fallback",
// });
// }, [reviewListData, actualReviewsData, reviewsData]);
// ✅ useReviews Hook이 모든 API 호출을 담당하므로 별도 API 호출 불필요
const formatToYYMMDD = (dateStr) => {
const date = new Date(dateStr);
const iso = date.toISOString().slice(2, 10);
return iso.replace(/-/g, ".");
return iso.replace(/-/g, '.');
};
// 리뷰 클릭으로 User Reviews 모드 팝업 열기
@@ -154,18 +143,18 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
(review) => review.rvwId === clickedReview.rvwId
);
console.log(
"[UserReviews] Review clicked, opening popup in User Reviews mode:",
{
previewIndex: reviewIndex,
realIndex,
clickedReviewId: clickedReview.rvwId,
totalReviews: fallbackReviews.allReviews.length,
}
);
// console.log(
// "[UserReviews] Review clicked, opening popup in User Reviews mode:",
// {
// previewIndex: reviewIndex,
// realIndex,
// clickedReviewId: clickedReview.rvwId,
// totalReviews: fallbackReviews.allReviews.length,
// }
// );
setSelectedImageIndex(realIndex >= 0 ? realIndex : reviewIndex);
setPopupMode("user-reviews");
setPopupMode('user-reviews');
setIsPopupOpen(true);
},
[reviewListData, fallbackReviews.allReviews]
@@ -174,37 +163,34 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
// 팝업 관련 핸들러들
// +View More 버튼으로 Customer Images 모드 팝업 열기
const handleOpenPopup = useCallback(() => {
console.log("[UserReviews] Opening popup in Customer Images mode");
setPopupMode("customer-images");
// console.log("[UserReviews] Opening popup in Customer Images mode");
setPopupMode('customer-images');
setIsPopupOpen(true);
}, []);
// 이미지 클릭으로 All-Images 모드 팝업 열기
const handleOpenAllImagesPopup = useCallback((imageIndex = 0) => {
console.log(
"[UserReviews] Opening popup in All-Images mode, image index:",
imageIndex
);
console.log('[UserReviews] Opening popup in All-Images mode, image index:', imageIndex);
setSelectedImageIndex(imageIndex);
setPopupMode("all-images");
setPopupMode('all-images');
setIsPopupOpen(true);
}, []);
const handleClosePopup = useCallback(() => {
console.log("[UserReviews] Closing popup and resetting mode");
// console.log("[UserReviews] Closing popup and resetting mode");
setIsPopupOpen(false);
setPopupMode("customer-images"); // 모드를 초기값으로 리셋
setPopupMode('customer-images'); // 모드를 초기값으로 리셋
setSelectedImageIndex(0); // 이미지 인덱스도 초기값으로 리셋
}, []);
// 팝업 모드 변경 핸들러
const handleModeChange = useCallback((newMode, imageIndex = 0) => {
console.log("[UserReviews] Mode change requested:", {
newMode,
imageIndex,
});
// console.log("[UserReviews] Mode change requested:", {
// newMode,
// imageIndex,
// });
setPopupMode(newMode);
if (newMode === "all-images" || newMode === "user-reviews") {
if (newMode === 'all-images' || newMode === 'user-reviews') {
setSelectedImageIndex(imageIndex);
}
}, []);
@@ -225,21 +211,14 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
verticalScrollbar="auto"
cbScrollTo={getScrollTo}
forceUpdate={showAllReviews}
key={
showAllReviews
? `all-${(reviewListData && reviewListData.length) || 0}`
: "limited-5"
}
key={showAllReviews ? `all-${(reviewListData && reviewListData.length) || 0}` : 'limited-5'}
>
<THeaderDetail
title={$L(`USER REVIEWS (${reviewTotalCount})`)}
className={classNames(css.tHeader, css.tHeaderDetail)}
>
{reviewDetailData && reviewDetailData.totRvwAvg && (
<StarRating
rating={reviewDetailData.totRvwAvg}
className={css.averageOverallRating}
/>
<StarRating rating={reviewDetailData.totRvwAvg} className={css.averageOverallRating} />
)}
</THeaderDetail>
<CustomerImages
@@ -256,48 +235,39 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
</div>
{reviewListData &&
(() => {
const reviewsToShow = showAllReviews
? reviewListData
: reviewListData.slice(0, 5);
console.log("[UserReviews] Reviews to render:", {
showAllReviews,
totalReviews: reviewListData.length,
reviewsToShowCount: reviewsToShow.length,
isShowingAll: showAllReviews,
reviewListData: reviewListData,
});
const reviewsToShow = showAllReviews ? reviewListData : reviewListData.slice(0, 5);
// console.log("[UserReviews] Reviews to render:", {
// showAllReviews,
// totalReviews: reviewListData.length,
// reviewsToShowCount: reviewsToShow.length,
// isShowingAll: showAllReviews,
// reviewListData: reviewListData,
// });
// 실제 렌더링될 각 리뷰 로그
reviewsToShow.forEach((review, index) => {
console.log(
`[UserReviews] Review ${index + 1}/${reviewsToShow.length}:`,
{
rvwId: review.rvwId,
rvwCtnt:
(review.rvwCtnt && review.rvwCtnt.substring(0, 50)) +
"...",
rvwRtng: review.rvwRtng,
hasImages:
(review.reviewImageList &&
review.reviewImageList.length) ||
0,
rvwRgstDtt: review.rvwRgstDtt,
fullReview: review,
}
);
});
// reviewsToShow.forEach((review, index) => {
// console.log(
// `[UserReviews] Review ${index + 1}/${reviewsToShow.length}:`,
// {
// rvwId: review.rvwId,
// rvwCtnt:
// (review.rvwCtnt && review.rvwCtnt.substring(0, 50)) +
// "...",
// rvwRtng: review.rvwRtng,
// hasImages:
// (review.reviewImageList &&
// review.reviewImageList.length) ||
// 0,
// rvwRgstDtt: review.rvwRgstDtt,
// fullReview: review,
// }
// );
// });
return reviewsToShow;
})().map((review, index, array) => {
const {
reviewImageList,
rvwRtng,
rvwRgstDtt,
rvwCtnt,
rvwId,
wrtrNknm,
rvwWrtrId,
} = review;
const { reviewImageList, rvwRtng, rvwRgstDtt, rvwCtnt, rvwId, wrtrNknm, rvwWrtrId } =
review;
const isLastReview = index === array.length - 1;
/* console.log(`[UserReviews] Rendering review ${index}:`, {
rvwId,
@@ -312,35 +282,24 @@ export default function UserReviews({ productInfo, panelInfo, reviewsData }) {
aria-label={`user-reviews-:${rvwId}`}
className={css.reviewContentContainer}
onClick={() => handleReviewClick(index)}
spotlightId={
isLastReview
? "user-review-at-last"
: `user-review-${index}`
}
spotlightId={isLastReview ? 'user-review-at-last' : `user-review-${index}`}
>
{reviewImageList && reviewImageList.length > 0 && (
<img
className={css.reviewThumbnail}
src={reviewImageList[0].imgUrl}
/>
<img className={css.reviewThumbnail} src={reviewImageList[0].imgUrl} />
)}
<div className={css.reviewContent}>
<div className={css.reviewMeta}>
{rvwRtng && (
<StarRating
rating={rvwRtng}
aria-label={"star rating " + rvwRtng + " out of 5"}
aria-label={'star rating ' + rvwRtng + ' out of 5'}
/>
)}
{(wrtrNknm || rvwWrtrId) && (
<span className={css.reviewAuthor}>
{wrtrNknm || rvwWrtrId}
</span>
<span className={css.reviewAuthor}>{wrtrNknm || rvwWrtrId}</span>
)}
{rvwRgstDtt && (
<span className={css.reviewDate}>
{formatToYYMMDD(rvwRgstDtt)}
</span>
<span className={css.reviewDate}>{formatToYYMMDD(rvwRgstDtt)}</span>
)}
</div>
{rvwCtnt && <div className={css.reviewText}>{rvwCtnt}</div>}

View File

@@ -7,7 +7,11 @@ import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import { sendBroadCast } from '../../actions/commonActions';
import { pauseModalMedia, resumeModalMedia } from '../../actions/mediaActions';
import {
pauseModalMedia,
resumeModalMedia,
switchMediaToFullscreen,
} from '../../actions/mediaActions';
import * as PanelActions from '../../actions/panelActions';
import TPanel from '../../components/TPanel/TPanel';
import Media from '../../components/VideoPlayer/Media';
@@ -89,6 +93,11 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
videoPlayer.current = ref;
}, []);
// VideoPlayer가 MEDIA 타입에서 setIsVODPaused를 호출하므로 더미 함수 제공
const setIsVODPaused = useCallback(() => {
// MediaPanel에서는 VOD pause 상태 관리 불필요 (단순 재생만)
}, []);
// modal 스타일 설정
useEffect(() => {
if (panelInfo.modal && panelInfo.modalContainerId) {
@@ -124,6 +133,14 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
}
}, [panelInfo, isOnTop]);
// 비디오 클릭 시 modal → fullscreen 전환
const onVideoClick = useCallback(() => {
if (panelInfo.modal) {
console.log('[MediaPanel] Video clicked - switching to fullscreen');
dispatch(switchMediaToFullscreen());
}
}, [dispatch, panelInfo.modal]);
const onClickBack = useCallback(
(ev) => {
// modal에서 full로 전환된 경우 다시 modal로 돌아감
@@ -280,6 +297,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
noAutoPlay={false}
autoCloseTimeout={3000}
onBackButton={onClickBack}
onClick={onVideoClick}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}
src={currentPlayingUrl}
@@ -302,6 +320,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
captionEnable={false}
setIsSubtitleActive={setIsSubtitleActive}
setCurrentTime={setCurrentTime}
setIsVODPaused={setIsVODPaused}
>
{typeof window === 'object' && window.PalmSystem && (
<source src={currentPlayingUrl} type={videoType} />