diff --git a/com.twin.app.shoptime/[251013]_RollingUnit_리렌더링_분석_및_해결방법.md b/com.twin.app.shoptime/[251013]_RollingUnit_리렌더링_분석_및_해결방법.md new file mode 100644 index 00000000..cba0d1bb --- /dev/null +++ b/com.twin.app.shoptime/[251013]_RollingUnit_리렌더링_분석_및_해결방법.md @@ -0,0 +1,416 @@ +# [251013] RollingUnit 리렌더링 분석 및 해결방법 + +## 📋 문제 요약 +- **증상**: HomeBanner의 RollingUnit 컴포넌트가 계속 리렌더링됨 +- **특이사항**: + - RandomUnit은 정상 작동 + - JustForYou 배너 관련 위치에서 특히 심함 + - RollingUnit의 자동 순환(10초 롤링)이 제대로 작동하지 않음 +- **원인**: OptionalTerms(선택약관) 관련 코드 추가 후 발생 + +--- + +## 🔍 근본 원인 분석 + +### 1. 불안정한 Props Chain (HomePanel → HomeBanner → RollingUnit) + +**문제 흐름:** +``` +HomePanel.jsx + ↓ nowShelf 상태 변경 + ↓ doSendLogGNB 재생성 (의존성: nowShelf, panelInfo.nowShelf, pageSpotIds) + ↓ handleItemFocus 재생성 (의존성: doSendLogGNB) + ↓ handleItemFocus(el.shptmApphmDspyOptCd) 호출 → 매번 새 함수 생성 +HomeBanner.jsx + ↓ _handleItemFocus 재생성 (의존성: handleItemFocus) + ↓ renderItem 재생성 (의존성: _handleItemFocus, _handleShelfFocus, bannerDataList) +RollingUnit.jsx + ↓ 새로운 props 수신 + ↓ 리렌더링 발생! +``` + +**코드 위치:** +- `HomePanel.jsx` 라인 220-270 +- `HomeBanner.jsx` 라인 81-85, 348-352, 497-542 + +--- + +### 2. OptionalTerms 상태와 조건부 렌더링 문제 ⚠️ **핵심 원인** + +**문제 코드:** `HomeBanner.jsx` 라인 628-680 + +```javascript +const renderLayout = useCallback(() => { + switch (selectTemplate) { + case 'DSP00201': { + return ( + <> + + {renderItem(0, true, true)} + {renderItem(1, true, true)} + + {renderItem(2, false, false)} + + {/* ⚠️ 문제 부분: introTermsAgree 상태에 의존 */} + {introTermsAgree === 'Y' ? ( +
+ +
+ ) : ( + renderItem(3, false, false) + )} + + ); + } + } + return null; +}, [selectTemplate, renderItem, renderSimpleVideoContainer]); +// ❌ 문제: introTermsAgree가 의존성 배열에 없음! +``` + +**문제점:** +1. `introTermsAgree`가 JSX 내부에서 참조되지만 의존성 배열에 없음 +2. `introTermsAgree` 변경 시 React가 최적화를 제대로 하지 못함 +3. 클로저 문제로 인해 예상치 못한 리렌더링 발생 + +--- + +### 3. JustForSwitchBanner의 구조적 문제 + +**문제 코드:** `JustForYouBanner.jsx` 라인 154-230 + +```javascript +export default function JustForSwitchBanner({ + renderItem, // ← HomeBanner에서 받은 renderItem + handleShelfFocus, // ← 계속 변하는 함수 + handleItemFocus, // ← 계속 변하는 함수 + isHorizontal, + spotlightId, +}) { + const [currentIndex, setCurrentIndex] = useState(0); + + const onFocus = useCallback(() => { + if (handleItemFocus) { + handleItemFocus(); // ⚠️ 매번 새로운 함수 참조! + } + }, [handleItemFocus]); // ← 의존성: 계속 변경됨 + + return ( + <> + + + {currentIndex === 0 ? ( + + ) : ( + renderItem(3, false) // ⚠️ RollingUnit 렌더링! + )} + + + + ); +} +``` + +**문제의 연쇄 반응:** +``` +handleItemFocus 변경 +→ JustForSwitchBanner의 onFocus 재생성 +→ JustForYouBanner 리렌더링 +→ currentIndex === 1일 때 renderItem(3) 호출 +→ RollingUnit 리렌더링! +``` + +--- + +### 4. RollingUnit 자동 롤링이 작동하지 않는 이유 + +**코드 위치:** `RollingUnit.jsx` 라인 463-487 + +```javascript +// 10초 롤링 +useEffect(() => { + lastIndexRef.current = rollingDataLength - 1; + previousTimeRef.current = undefined; // ⚠️ 타이머 리셋! + + if (rollingDataLength <= 1 || unitHasFocus) { + doRollingRef.current = false; + window.cancelAnimationFrame(requestRef.current); + return; + } + + doRollingRef.current = true; + requestRef.current = window.requestAnimationFrame(animate); + + return () => { + doRollingRef.current = false; + window.cancelAnimationFrame(requestRef.current); + }; +}, [rollingDataLength, unitHasFocus]); +``` + +**문제:** +- RollingUnit이 계속 리렌더링되면 이 `useEffect`가 계속 실행됨 +- `previousTimeRef.current = undefined`로 타이머가 계속 리셋됨 +- `requestAnimationFrame`이 취소되고 다시 시작됨 +- **결과: 10초 타이머가 초기화되어 자동 롤링이 작동하지 않음** + +--- + +### 5. RandomUnit은 왜 괜찮은가? + +**RandomUnit의 경우:** +```javascript +// HomeBanner.jsx - renderLayout (DSP00201) + + {renderItem(0, true, true)} // ← Rolling 또는 Random + {renderItem(1, true, true)} // ← 주로 Random (고정 위치) + +``` + +**RandomUnit은:** +- ✅ 고정된 위치(index 0, 1)에서 렌더링 +- ✅ 조건부 렌더링 없음 +- ✅ `introTermsAgree` 상태와 무관 +- ✅ JustForSwitchBanner를 통하지 않음 +- ✅ 안정적인 props만 받음 + +--- + +## 💡 해결방법 + +### ✅ 해결방법 1: renderLayout 의존성 배열에 introTermsAgree 추가 (적용됨) + +**파일:** `HomeBanner.jsx` 라인 514 + +**변경 전:** +```javascript +}, [selectTemplate, renderItem, renderSimpleVideoContainer]); +``` + +**변경 후:** +```javascript +}, [selectTemplate, renderItem, renderSimpleVideoContainer, introTermsAgree]); +``` + +**효과:** +- `introTermsAgree` 변경 시 `renderLayout`이 명시적으로 재생성됨 +- React가 변경사항을 정확히 추적할 수 있음 +- 클로저 문제 해결 + +--- + +### 🔧 추가 권장 해결방법 + +#### 해결방법 2: HomePanel의 handleItemFocus 최적화 + +**파일:** `HomePanel.jsx` 라인 265-270 + +**현재 문제:** +```javascript +const handleItemFocus = useCallback( + (containerId, location, title) => () => { + doSendLogGNB(containerId, location, title); + }, + [doSendLogGNB] +); + +// 렌더링 시 매번 새 함수 생성 + +``` + +**해결방법:** +```javascript +// 1. 함수를 직접 전달하지 말고 파라미터를 전달 + + +// 2. HomeBanner에서 필요할 때 호출 +const _handleItemFocus = useCallback(() => { + if (onItemFocus) { + onItemFocus(containerId, location, title); + } +}, [onItemFocus, containerId, location, title]); +``` + +--- + +#### 해결방법 3: JustForSwitchBanner 최적화 + +**파일:** `JustForYouBanner.jsx` 라인 189-195 + +**현재 문제:** +```javascript +const onFocus = useCallback(() => { + if (handleItemFocus) { + handleItemFocus(); + } +}, [handleItemFocus]); // ← 계속 변경됨 +``` + +**해결방법 A: useRef 사용** +```javascript +const handleItemFocusRef = useRef(handleItemFocus); + +useEffect(() => { + handleItemFocusRef.current = handleItemFocus; +}, [handleItemFocus]); + +const onFocus = useCallback(() => { + if (handleItemFocusRef.current) { + handleItemFocusRef.current(); + } +}, []); // ← 의존성 없음, 안정적 +``` + +**해결방법 B: React.memo로 JustForYouBanner 감싸기** +```javascript +const JustForYouBanner = React.memo(function JustForYouBanner({ + onClick, + spotlightId, + onFocus, + isHorizontal, + popupVisible, + activePopup, +}) { + // ... 기존 코드 +}, (prevProps, nextProps) => { + // onFocus가 변경되어도 리렌더링하지 않음 + return ( + prevProps.spotlightId === nextProps.spotlightId && + prevProps.isHorizontal === nextProps.isHorizontal && + prevProps.popupVisible === nextProps.popupVisible && + prevProps.activePopup === nextProps.activePopup + ); +}); +``` + +--- + +#### 해결방법 4: doSendLogGNB 의존성 최적화 + +**파일:** `HomePanel.jsx` 라인 220-263 + +**현재 문제:** +```javascript +const doSendLogGNB = useCallback( + (containerId, location = null, title = null) => { + // ... 로직 + }, + [pageSpotIds, nowShelf, panelInfo.nowShelf] // ← 자주 변경됨 +); +``` + +**해결방법: useRef 사용** +```javascript +const nowShelfRef = useRef(nowShelf); +const panelInfoRef = useRef(panelInfo.nowShelf); + +useEffect(() => { + nowShelfRef.current = nowShelf; + panelInfoRef.current = panelInfo.nowShelf; +}, [nowShelf, panelInfo.nowShelf]); + +const doSendLogGNB = useCallback( + (containerId, location = null, title = null) => { + // nowShelfRef.current 사용 + if (containerId !== nowShelfRef.current && location && title) { + // ... 로직 + } + }, + [pageSpotIds] // ← 의존성 감소 +); +``` + +--- + +#### 해결방법 5: RollingUnit을 React.memo로 감싸기 + +**파일:** `RollingUnit.jsx` 마지막 줄 + +**현재:** +```javascript +export default function RollingUnit({ ... }) { + // ... 코드 +} +``` + +**해결방법:** +```javascript +function RollingUnit({ ... }) { + // ... 기존 코드 +} + +export default React.memo(RollingUnit, (prevProps, nextProps) => { + // props가 실제로 변경되었을 때만 리렌더링 + return ( + prevProps.bannerData === nextProps.bannerData && + prevProps.spotlightId === nextProps.spotlightId && + prevProps.isHorizontal === nextProps.isHorizontal && + prevProps.videoPlayerable === nextProps.videoPlayerable + // handleItemFocus, handleShelfFocus는 비교하지 않음 + ); +}); +``` + +--- + +## 📊 우선순위별 적용 순서 + +### 1단계 (필수) ✅ 완료 +- [x] `renderLayout` 의존성 배열에 `introTermsAgree` 추가 + +### 2단계 (권장) +- [ ] `doSendLogGNB` 의존성 최적화 (useRef 사용) +- [ ] `handleItemFocus` 구조 개선 (파라미터 전달 방식) + +### 3단계 (선택) +- [ ] `JustForSwitchBanner` 최적화 (useRef 또는 React.memo) +- [ ] `RollingUnit`을 React.memo로 감싸기 + +--- + +## 🎯 예상 효과 + +### 1단계 적용 후: +- ✅ `introTermsAgree` 변경 시 명시적 리렌더링 +- ✅ 클로저 문제 해결 +- ⚠️ 여전히 props chain으로 인한 불필요한 리렌더링 가능 + +### 2-3단계 적용 후: +- ✅ 불필요한 리렌더링 대폭 감소 +- ✅ RollingUnit 자동 롤링 정상 작동 +- ✅ 성능 개선 +- ✅ 안정적인 컴포넌트 동작 + +--- + +## 📝 테스트 체크리스트 + +- [ ] RollingUnit이 불필요하게 리렌더링되지 않는지 확인 +- [ ] RollingUnit의 10초 자동 롤링이 정상 작동하는지 확인 +- [ ] OptionalTerms 동의 전/후 배너 전환이 정상적인지 확인 +- [ ] JustForYou 배너와 RollingUnit 간 전환이 부드러운지 확인 +- [ ] RandomUnit이 여전히 정상 작동하는지 확인 +- [ ] 포커스 이동 시 로그가 정상적으로 전송되는지 확인 + +--- + +## 📅 작성 정보 +- **날짜**: 2025-10-13 +- **분석 대상**: RollingUnit 리렌더링 문제 +- **주요 원인**: OptionalTerms 상태 + JustForSwitchBanner 조건부 렌더링 +- **적용된 해결방법**: renderLayout 의존성 배열에 introTermsAgree 추가 diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 6be957df..b9c8d5e1 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -2,218 +2,219 @@ export const types = { // panel actions - PUSH_PANEL: "PUSH_PANEL", - POP_PANEL: "POP_PANEL", - UPDATE_PANEL: "UPDATE_PANEL", - RESET_PANELS: "RESET_PANELS", + PUSH_PANEL: 'PUSH_PANEL', + POP_PANEL: 'POP_PANEL', + UPDATE_PANEL: 'UPDATE_PANEL', + RESET_PANELS: 'RESET_PANELS', // device actions - GET_AUTHENTICATION_CODE: "GET_AUTHENTICATION_CODE", - REGISTER_DEVICE: "REGISTER_DEVICE", - REGISTER_DEVICE_INFO: "REGISTER_DEVICE_INFO", - GET_DEVICE_INFO: "GET_DEVICE_INFO", - CLEAR_REGISTER_DEVICE_INFO: "CLEAR_REGISTER_DEVICE_INFO", - REGISTER_DEVICE_RESET: "REGISTER_DEVICE_RESET", + GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE', + REGISTER_DEVICE: 'REGISTER_DEVICE', + REGISTER_DEVICE_INFO: 'REGISTER_DEVICE_INFO', + GET_DEVICE_INFO: 'GET_DEVICE_INFO', + CLEAR_REGISTER_DEVICE_INFO: 'CLEAR_REGISTER_DEVICE_INFO', + REGISTER_DEVICE_RESET: 'REGISTER_DEVICE_RESET', // common actions - GET_HTTP_HEADER: "GET_HTTP_HEADER", - CHANGE_APP_STATUS: "CHANGE_APP_STATUS", - SEND_BROADCAST: "SEND_BROADCAST", - CHANGE_LOCAL_SETTINGS: "CHANGE_LOCAL_SETTINGS", - GNB_OPENED: "GNB_OPENED", - SET_SHOW_POPUP: "SET_SHOW_POPUP", - SET_SHOW_SECONDARY_POPUP: "SET_SHOW_SECONDARY_POPUP", - SET_HIDE_POPUP: "SET_HIDE_POPUP", - SET_HIDE_SECONDARY_POPUP: "SET_HIDE_SECONDARY_POPUP", - SHOW_OPTIONAL_TERMS_CONFIRM_POPUP: "SHOW_OPTIONAL_TERMS_CONFIRM_POPUP", - HIDE_OPTIONAL_TERMS_CONFIRM_POPUP: "HIDE_OPTIONAL_TERMS_CONFIRM_POPUP", - TOGGLE_OPTIONAL_TERMS_CONFIRM: "TOGGLE_OPTIONAL_TERMS_CONFIRM", + GET_HTTP_HEADER: 'GET_HTTP_HEADER', + CHANGE_APP_STATUS: 'CHANGE_APP_STATUS', + SEND_BROADCAST: 'SEND_BROADCAST', + CHANGE_LOCAL_SETTINGS: 'CHANGE_LOCAL_SETTINGS', + GNB_OPENED: 'GNB_OPENED', + SET_SHOW_POPUP: 'SET_SHOW_POPUP', + SET_SHOW_SECONDARY_POPUP: 'SET_SHOW_SECONDARY_POPUP', + SET_HIDE_POPUP: 'SET_HIDE_POPUP', + SET_HIDE_SECONDARY_POPUP: 'SET_HIDE_SECONDARY_POPUP', + SHOW_OPTIONAL_TERMS_CONFIRM_POPUP: 'SHOW_OPTIONAL_TERMS_CONFIRM_POPUP', + HIDE_OPTIONAL_TERMS_CONFIRM_POPUP: 'HIDE_OPTIONAL_TERMS_CONFIRM_POPUP', + TOGGLE_OPTIONAL_TERMS_CONFIRM: 'TOGGLE_OPTIONAL_TERMS_CONFIRM', // 선택약관 팝업 상태 관리 - SET_OPTIONAL_TERMS_POPUP_SHOWN: "SET_OPTIONAL_TERMS_POPUP_SHOWN", - SET_OPTIONAL_TERMS_USER_DECISION: "SET_OPTIONAL_TERMS_USER_DECISION", - RESET_OPTIONAL_TERMS_SESSION: "RESET_OPTIONAL_TERMS_SESSION", + SET_OPTIONAL_TERMS_POPUP_SHOWN: 'SET_OPTIONAL_TERMS_POPUP_SHOWN', + SET_OPTIONAL_TERMS_USER_DECISION: 'SET_OPTIONAL_TERMS_USER_DECISION', + RESET_OPTIONAL_TERMS_SESSION: 'RESET_OPTIONAL_TERMS_SESSION', // 선택약관 직접 상태 업데이트 (API 호출 없이) - UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: "UPDATE_OPTIONAL_TERMS_AGREE_DIRECT", - UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: "UPDATE_TERMS_AGREEMENT_STATUS_DIRECT", - SET_EXIT_APP: "SET_EXIT_APP", - GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA", - GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN", - LAUNCH_MEMBERSHIP_APP: "LAUNCH_MEMBERSHIP_APP", - SET_FOCUS: "SET_FOCUS", - SET_GNB_MENU: "SET_GNB_MENU", - SET_SYSTEM_NOTICE: "SET_SYSTEM_NOTICE", - SET_SYSTEM_TERMINATION: "SET_SYSTEM_TERMINATION", - SET_DEEP_LINK: "SET_DEEP_LINK", - SET_SECOND_LAYER_INFO: "SET_SECOND_LAYER_INFO", - SET_ERROR_MESSAGE: "SET_ERROR_MESSAGE", - CLEAR_ERROR_MESSAGE: "CLEAR_ERROR_MESSAGE", - GET_DEVICE_MACADDRESS: "GET_DEVICE_MACADDRESS", - SET_DEVICE_REGISTERED: "SET_DEVICE_REGISTERED", + UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: 'UPDATE_OPTIONAL_TERMS_AGREE_DIRECT', + UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: 'UPDATE_TERMS_AGREEMENT_STATUS_DIRECT', + SET_EXIT_APP: 'SET_EXIT_APP', + GET_LOGIN_USER_DATA: 'GET_LOGIN_USER_DATA', + GET_TERMS_AGREE_YN: 'GET_TERMS_AGREE_YN', + LAUNCH_MEMBERSHIP_APP: 'LAUNCH_MEMBERSHIP_APP', + SET_FOCUS: 'SET_FOCUS', + SET_GNB_MENU: 'SET_GNB_MENU', + SET_SYSTEM_NOTICE: 'SET_SYSTEM_NOTICE', + SET_SYSTEM_TERMINATION: 'SET_SYSTEM_TERMINATION', + SET_DEEP_LINK: 'SET_DEEP_LINK', + SET_SECOND_LAYER_INFO: 'SET_SECOND_LAYER_INFO', + SET_ERROR_MESSAGE: 'SET_ERROR_MESSAGE', + CLEAR_ERROR_MESSAGE: 'CLEAR_ERROR_MESSAGE', + GET_DEVICE_MACADDRESS: 'GET_DEVICE_MACADDRESS', + SET_DEVICE_REGISTERED: 'SET_DEVICE_REGISTERED', // billing actions - GET_MY_INFO_BILLING_SEARCH: "GET_MY_INFO_BILLING_SEARCH", + GET_MY_INFO_BILLING_SEARCH: 'GET_MY_INFO_BILLING_SEARCH', // card actions - GET_MY_INFO_CARD_SEARCH: "GET_MY_INFO_CARD_SEARCH", + GET_MY_INFO_CARD_SEARCH: 'GET_MY_INFO_CARD_SEARCH', // shipping actions - GET_MY_INFO_SHIPPING_SEARCH: "GET_MY_INFO_SHIPPING_SEARCH", + GET_MY_INFO_SHIPPING_SEARCH: 'GET_MY_INFO_SHIPPING_SEARCH', // cart actions - GET_MY_INFO_CART_SEARCH: "GET_MY_INFO_CART_SEARCH", - ADD_TO_CART: "ADD_TO_CART", - REMOVE_FROM_CART: "REMOVE_FROM_CART", - UPDATE_CART_ITEM: "UPDATE_CART_ITEM", - CLEAR_CART: "CLEAR_CART", + GET_MY_INFO_CART_SEARCH: 'GET_MY_INFO_CART_SEARCH', + ADD_TO_CART: 'ADD_TO_CART', + REMOVE_FROM_CART: 'REMOVE_FROM_CART', + UPDATE_CART_ITEM: 'UPDATE_CART_ITEM', + CLEAR_CART: 'CLEAR_CART', // appData actions - ADD_MAIN_INDEX: "ADD_MAIN_INDEX", - SEND_SMS: "SEND_SMS", - CLEAR_SMS: "CLEAR_SMS", + ADD_MAIN_INDEX: 'ADD_MAIN_INDEX', + SEND_SMS: 'SEND_SMS', + CLEAR_SMS: 'CLEAR_SMS', // home actions - GET_HOME_TERMS: "GET_HOME_TERMS", - SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP", - SET_OPTIONAL_TERMS_AVAILABILITY: "SET_OPTIONAL_TERMS_AVAILABILITY", - GET_HOME_MENU: "GET_HOME_MENU", - GET_HOME_LAYOUT: "GET_HOME_LAYOUT", - GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS", - GET_HOME_FULL_VIDEO_INFO: "GET_HOME_FULL_VIDEO_INFO", - GET_THEME_CURATION_INFO: "GET_THEME_CURATION_INFO", - GET_THEME_CURATION_DETAIL_INFO: "GET_THEME_CURATION_DETAIL_INFO", - GET_THEME_HOTEL_DETAIL_INFO: "GET_THEME_HOTEL_DETAIL_INFO", - GET_THEME_MENU_SHELF_INFO: "GET_THEME_MENU_SHELF_INFO", - CLEAR_THEME_MENU_SHELF_INFO: "CLEAR_THEME_MENU_SHELF_INFO", - CLEAR_THEME_DETAIL: "CLEAR_THEME_DETAIL", - CHECK_ENTER_THROUGH_GNB: "CHECK_ENTER_THROUGH_GNB", - SET_DEFAULT_FOCUS: "SET_DEFAULT_FOCUS", - SET_BANNER_INDEX: "SET_BANNER_INDEX", - RESET_HOME_INFO: "RESET_HOME_INFO", - UPDATE_HOME_INFO: "UPDATE_HOME_INFO", + GET_HOME_TERMS: 'GET_HOME_TERMS', + SET_TERMS_ID_MAP: 'SET_TERMS_ID_MAP', + SET_OPTIONAL_TERMS_AVAILABILITY: 'SET_OPTIONAL_TERMS_AVAILABILITY', + GET_HOME_MENU: 'GET_HOME_MENU', + GET_HOME_LAYOUT: 'GET_HOME_LAYOUT', + GET_HOME_MAIN_CONTENTS: 'GET_HOME_MAIN_CONTENTS', + GET_HOME_FULL_VIDEO_INFO: 'GET_HOME_FULL_VIDEO_INFO', + GET_THEME_CURATION_INFO: 'GET_THEME_CURATION_INFO', + GET_THEME_CURATION_DETAIL_INFO: 'GET_THEME_CURATION_DETAIL_INFO', + GET_THEME_HOTEL_DETAIL_INFO: 'GET_THEME_HOTEL_DETAIL_INFO', + GET_THEME_MENU_SHELF_INFO: 'GET_THEME_MENU_SHELF_INFO', + CLEAR_THEME_MENU_SHELF_INFO: 'CLEAR_THEME_MENU_SHELF_INFO', + CLEAR_THEME_DETAIL: 'CLEAR_THEME_DETAIL', + CHECK_ENTER_THROUGH_GNB: 'CHECK_ENTER_THROUGH_GNB', + SET_DEFAULT_FOCUS: 'SET_DEFAULT_FOCUS', + SET_BANNER_INDEX: 'SET_BANNER_INDEX', + RESET_HOME_INFO: 'RESET_HOME_INFO', + UPDATE_HOME_INFO: 'UPDATE_HOME_INFO', // brand actions - GET_BRAND_LIST: "GET_BRAND_LIST", - GET_BRAND_LAYOUT_INFO: "GET_BRAND_LAYOUT_INFO", - GET_BRAND_LIVE_CHANNEL_INFO: "GET_BRAND_LIVE_CHANNEL_INFO", - GET_BRAND_CHAN_INFO: "GET_BRAND_CHAN_INFO", - GET_BRAND_TSV_INFO: "GET_BRAND_TSV_INFO", - GET_BRAND_RECOMMENDED_SHOW_INFO: "GET_BRAND_RECOMMENDED_SHOW_INFO", - GET_BRAND_SERIES_INFO: "GET_BRAND_SERIES_INFO", - GET_BRAND_CATEGORY_INFO: "GET_BRAND_CATEGORY_INFO", - GET_BRAND_CATEGORY_PRODUCT_INFO: "GET_BRAND_CATEGORY_PRODUCT_INFO", - GET_BRAND_BEST_SELLER: "GET_BRAND_BEST_SELLER", - GET_BRAND_CREATORS_INFO: "GET_BRAND_CREATORS_INFO", - GET_BRAND_SHOWROOM: "GET_BRAND_SHOWROOM", - GET_BRAND_RECENTLY_AIRED: "GET_BRAND_RECENTLY_AIRED", - SET_BRAND_LIVE_CHANNEL_UPCOMING: "SET_BRAND_LIVE_CHANNEL_UPCOMING", - SET_BRAND_CHAN_INFO: "SET_BRAND_CHAN_INFO", - RESET_BRAND_STATE: "RESET_BRAND_STATE", - RESET_BRAND_STATE_EXCEPT_BRAND_INFO: "RESET_BRAND_STATE_EXCEPT_BRAND_INFO", - RESET_BRAND_LAYOUT_INFO: "RESET_BRAND_LAYOUT_INFO", + GET_BRAND_LIST: 'GET_BRAND_LIST', + GET_BRAND_LAYOUT_INFO: 'GET_BRAND_LAYOUT_INFO', + GET_BRAND_LIVE_CHANNEL_INFO: 'GET_BRAND_LIVE_CHANNEL_INFO', + GET_BRAND_CHAN_INFO: 'GET_BRAND_CHAN_INFO', + GET_BRAND_TSV_INFO: 'GET_BRAND_TSV_INFO', + GET_BRAND_RECOMMENDED_SHOW_INFO: 'GET_BRAND_RECOMMENDED_SHOW_INFO', + GET_BRAND_SERIES_INFO: 'GET_BRAND_SERIES_INFO', + GET_BRAND_CATEGORY_INFO: 'GET_BRAND_CATEGORY_INFO', + GET_BRAND_CATEGORY_PRODUCT_INFO: 'GET_BRAND_CATEGORY_PRODUCT_INFO', + GET_BRAND_BEST_SELLER: 'GET_BRAND_BEST_SELLER', + GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO', + GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM', + GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED', + SET_BRAND_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING', + SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO', + RESET_BRAND_STATE: 'RESET_BRAND_STATE', + RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO', + RESET_BRAND_LAYOUT_INFO: 'RESET_BRAND_LAYOUT_INFO', // main actions - GET_SUB_CATEGORY: "GET_SUB_CATEGORY", - APPEND_SUB_CATEGORY: "APPEND_SUB_CATEGORY", - GET_TOP_20_SHOW: "GET_TOP_20_SHOW", - GET_PRODUCT_DETAIL: "GET_PRODUCT_DETAIL", - GET_YOUMAYLIKE: "GET_YOUMAYLIKE", - SET_MAIN_LIKE_CATEGORY: "SET_MAIN_LIKE_CATEGORY", - SET_MAIN_LIVE_UPCOMING_ALARM: "SET_MAIN_LIVE_UPCOMING_ALARM", - GET_MAIN_LIVE_SHOW_NOW_PRODUCT: "GET_MAIN_LIVE_SHOW_NOW_PRODUCT", - GET_MAIN_LIVE_SHOW: "GET_MAIN_LIVE_SHOW", - GET_MAIN_CATEGORY_SHOW_DETAIL: "GET_MAIN_CATEGORY_SHOW_DETAIL", - CLEAR_PRODUCT_DETAIL: "CLEAR_PRODUCT_DETAIL", - CLEAR_SUB_CATEGORY: "CLEAR_SUB_CATEGORY", - CLEAR_SHOPNOW_INFO: "CLEAR_SHOPNOW_INFO", + GET_SUB_CATEGORY: 'GET_SUB_CATEGORY', + APPEND_SUB_CATEGORY: 'APPEND_SUB_CATEGORY', + GET_TOP_20_SHOW: 'GET_TOP_20_SHOW', + GET_PRODUCT_DETAIL: 'GET_PRODUCT_DETAIL', + GET_YOUMAYLIKE: 'GET_YOUMAYLIKE', + SET_MAIN_LIKE_CATEGORY: 'SET_MAIN_LIKE_CATEGORY', + SET_MAIN_LIVE_UPCOMING_ALARM: 'SET_MAIN_LIVE_UPCOMING_ALARM', + GET_MAIN_LIVE_SHOW_NOW_PRODUCT: 'GET_MAIN_LIVE_SHOW_NOW_PRODUCT', + GET_MAIN_LIVE_SHOW: 'GET_MAIN_LIVE_SHOW', + GET_MAIN_CATEGORY_SHOW_DETAIL: 'GET_MAIN_CATEGORY_SHOW_DETAIL', + CLEAR_PRODUCT_DETAIL: 'CLEAR_PRODUCT_DETAIL', + CLEAR_SUB_CATEGORY: 'CLEAR_SUB_CATEGORY', + CLEAR_SHOPNOW_INFO: 'CLEAR_SHOPNOW_INFO', // myPage actions - GET_MY_RECOMMANDED_KEYWORD: "GET_MY_RECOMMANDED_KEYWORD", - GET_MY_FAQ_INFO: "GET_MY_FAQ_INFO", - GET_NOTICE: "GET_NOTICE", - GET_MY_CUSTOMERS: "GET_MY_CUSTOMERS", - GET_MY_FAVORITE: "GET_MY_FAVORITE", - DELETE_MY_FAVORITE: "DELETE_MY_FAVORITE", - GET_MY_FAVORITE_FLAG: "GET_MY_FAVORITE_FLAG", - SET_MY_TERMS_WITHDRAW: "SET_MY_TERMS_WITHDRAW", - GET_MY_UPCOMING_CHANGE_INFO: "GET_MY_UPCOMING_CHANGE_INFO", - GET_MY_UPCOMING_ALERT_SHOW: "GET_MY_UPCOMING_ALERT_SHOW", - DELETE_MY_UPCOMING_ALERT_SHOW: "DELETE_MY_UPCOMING_ALERT_SHOW", - GET_MY_UPCOMING_ALERT_SHOW_KEYS: "GET_MY_UPCOMING_ALERT_SHOW_KEYS", - SET_MY_UPCOMING_USE_ALERT: "SET_MY_UPCOMING_USE_ALERT", - GET_UPCOMING_ALERT_SHOW_CHANGE_INFO: "GET_UPCOMING_ALERT_SHOW_CHANGE_INFO", - GET_MY_RECENTLY_VIEWED_INFO: "GET_MY_RECENTLY_VIEWED_INFO", - CLEAR_RECENTLY_VIEWED_INFO: "CLEAR_RECENTLY_VIEWED_INFO", - CLEAR_FAVORITES: "CLEAR_FAVORITES", - SET_MYPAGE_TERMS_AGREE: "SET_MYPAGE_TERMS_AGREE", - SET_MYPAGE_TERMS_AGREE_SUCCESS: "SET_MYPAGE_TERMS_AGREE_SUCCESS", - SET_MYPAGE_TERMS_AGREE_FAIL: "SET_MYPAGE_TERMS_AGREE_FAIL", + GET_MY_RECOMMANDED_KEYWORD: 'GET_MY_RECOMMANDED_KEYWORD', + GET_MY_FAQ_INFO: 'GET_MY_FAQ_INFO', + GET_NOTICE: 'GET_NOTICE', + GET_MY_CUSTOMERS: 'GET_MY_CUSTOMERS', + GET_MY_FAVORITE: 'GET_MY_FAVORITE', + DELETE_MY_FAVORITE: 'DELETE_MY_FAVORITE', + GET_MY_FAVORITE_FLAG: 'GET_MY_FAVORITE_FLAG', + SET_MY_TERMS_WITHDRAW: 'SET_MY_TERMS_WITHDRAW', + GET_MY_UPCOMING_CHANGE_INFO: 'GET_MY_UPCOMING_CHANGE_INFO', + GET_MY_UPCOMING_ALERT_SHOW: 'GET_MY_UPCOMING_ALERT_SHOW', + DELETE_MY_UPCOMING_ALERT_SHOW: 'DELETE_MY_UPCOMING_ALERT_SHOW', + GET_MY_UPCOMING_ALERT_SHOW_KEYS: 'GET_MY_UPCOMING_ALERT_SHOW_KEYS', + SET_MY_UPCOMING_USE_ALERT: 'SET_MY_UPCOMING_USE_ALERT', + GET_UPCOMING_ALERT_SHOW_CHANGE_INFO: 'GET_UPCOMING_ALERT_SHOW_CHANGE_INFO', + GET_MY_RECENTLY_VIEWED_INFO: 'GET_MY_RECENTLY_VIEWED_INFO', + CLEAR_RECENTLY_VIEWED_INFO: 'CLEAR_RECENTLY_VIEWED_INFO', + CLEAR_FAVORITES: 'CLEAR_FAVORITES', + SET_MYPAGE_TERMS_AGREE: 'SET_MYPAGE_TERMS_AGREE', + SET_MYPAGE_TERMS_AGREE_SUCCESS: 'SET_MYPAGE_TERMS_AGREE_SUCCESS', + SET_MYPAGE_TERMS_AGREE_FAIL: 'SET_MYPAGE_TERMS_AGREE_FAIL', // onSale actions - GET_HOME_ON_SALE_INFO: "GET_HOME_ON_SALE_INFO", - GET_ON_SALE_INFO: "GET_ON_SALE_INFO", - COPY_CATEGORY_INFO: "COPY_CATEGORY_INFO", - RESET_ON_SALE_STATE: "RESET_ON_SALE_STATE", + GET_HOME_ON_SALE_INFO: 'GET_HOME_ON_SALE_INFO', + GET_ON_SALE_INFO: 'GET_ON_SALE_INFO', + COPY_CATEGORY_INFO: 'COPY_CATEGORY_INFO', + RESET_ON_SALE_STATE: 'RESET_ON_SALE_STATE', // product actions - GET_BEST_SELLER: "GET_BEST_SELLER", - GET_PRODUCT_GROUP: "GET_PRODUCT_GROUP", - GET_PRODUCT_OPTION: "GET_PRODUCT_OPTION", - GET_PRODUCT_IMAGE_LENGTH: "GET_PRODUCT_IMAGE_LENGTH", - GET_VIDEO_INDECATOR_FOCUS: "GET_VIDEO_INDECATOR_FOCUS", - GET_PRODUCT_OPTION_ID: "GET_PRODUCT_OPTION_ID", - CLEAR_PRODUCT_OPTIONS: "CLEAR_PRODUCT_OPTIONS", - GET_USER_REVIEW: "GET_USER_REVIEW", - TOGGLE_SHOW_ALL_REVIEWS: "TOGGLE_SHOW_ALL_REVIEWS", - RESET_SHOW_ALL_REVIEWS: "RESET_SHOW_ALL_REVIEWS", + GET_BEST_SELLER: 'GET_BEST_SELLER', + GET_PRODUCT_GROUP: 'GET_PRODUCT_GROUP', + GET_PRODUCT_OPTION: 'GET_PRODUCT_OPTION', + GET_PRODUCT_IMAGE_LENGTH: 'GET_PRODUCT_IMAGE_LENGTH', + GET_VIDEO_INDECATOR_FOCUS: 'GET_VIDEO_INDECATOR_FOCUS', + GET_PRODUCT_OPTION_ID: 'GET_PRODUCT_OPTION_ID', + CLEAR_PRODUCT_OPTIONS: 'CLEAR_PRODUCT_OPTIONS', + GET_USER_REVIEW: 'GET_USER_REVIEW', + TOGGLE_SHOW_ALL_REVIEWS: 'TOGGLE_SHOW_ALL_REVIEWS', + RESET_SHOW_ALL_REVIEWS: 'RESET_SHOW_ALL_REVIEWS', // search actions - GET_SEARCH: "GET_SEARCH", - RESET_SEARCH: "RESET_SEARCH", - GET_SEARCH_PROCESSED: "GET_SEARCH_PROCESSED", - SET_SEARCH_INIT_PERFORMED: "SET_SEARCH_INIT_PERFORMED", - UPDATE_SEARCH_TIMESTAMP: "UPDATE_SEARCH_TIMESTAMP", + GET_SEARCH: 'GET_SEARCH', + RESET_SEARCH: 'RESET_SEARCH', + GET_SEARCH_PROCESSED: 'GET_SEARCH_PROCESSED', + SET_SEARCH_INIT_PERFORMED: 'SET_SEARCH_INIT_PERFORMED', + UPDATE_SEARCH_TIMESTAMP: 'UPDATE_SEARCH_TIMESTAMP', // event actions - GET_WELCOME_EVENT_INFO: "GET_WELCOME_EVENT_INFO", - GET_EVENT_ISSUED_STATUS: "GET_EVENT_ISSUED_STATUS", - SET_EVENT_ISSUE_REQ: "SET_EVENT_ISSUE_REQ", - SET_EVENT_POP_CLICK_INFO: "SET_EVENT_POP_CLICK_INFO", - CLEAR_EVENT_INFO: "CLEAR_EVENT_INFO", - SKIP_CLICK_INFO: "SKIP_CLICK_INFO", - CLEAR_CURATION_COUPON: "CLEAR_CURATION_COUPON", - CLEAR_EVENT_ISSUED_STATUS_SUCCEESS: "CLEAR_EVENT_ISSUED_STATUS_SUCCEESS", + GET_WELCOME_EVENT_INFO: 'GET_WELCOME_EVENT_INFO', + GET_EVENT_ISSUED_STATUS: 'GET_EVENT_ISSUED_STATUS', + SET_EVENT_ISSUE_REQ: 'SET_EVENT_ISSUE_REQ', + SET_EVENT_POP_CLICK_INFO: 'SET_EVENT_POP_CLICK_INFO', + CLEAR_EVENT_INFO: 'CLEAR_EVENT_INFO', + SKIP_CLICK_INFO: 'SKIP_CLICK_INFO', + CLEAR_CURATION_COUPON: 'CLEAR_CURATION_COUPON', + CLEAR_EVENT_ISSUED_STATUS_SUCCEESS: 'CLEAR_EVENT_ISSUED_STATUS_SUCCEESS', // coupon actions - GET_PRODUCT_COUPON_INFO: "GET_PRODUCT_COUPON_INFO", - GET_PRODUCT_COUPON_SEARCH: "GET_PRODUCT_COUPON_SEARCH", - GET_PRODUCT_COUPON_DOWNLOAD: "GET_PRODUCT_COUPON_DOWNLOAD", - GET_PRODUCT_COUPON_TOTDOWNLOAD: "GET_PRODUCT_COUPON_TOTDOWNLOAD", - CLEAR_COUPON_INFO: "CLEAR_COUPON_INFO", - CLEAR_GET_PRODUCT_COUPON_DOWNLOAD: "CLEAR_GET_PRODUCT_COUPON_DOWNLOAD", + GET_PRODUCT_COUPON_INFO: 'GET_PRODUCT_COUPON_INFO', + GET_PRODUCT_COUPON_SEARCH: 'GET_PRODUCT_COUPON_SEARCH', + GET_PRODUCT_COUPON_DOWNLOAD: 'GET_PRODUCT_COUPON_DOWNLOAD', + GET_PRODUCT_COUPON_TOTDOWNLOAD: 'GET_PRODUCT_COUPON_TOTDOWNLOAD', + CLEAR_COUPON_INFO: 'CLEAR_COUPON_INFO', + CLEAR_GET_PRODUCT_COUPON_DOWNLOAD: 'CLEAR_GET_PRODUCT_COUPON_DOWNLOAD', // checkout actions - GET_CHECKOUT_INFO: "GET_CHECKOUT_INFO", - INSERT_MY_INFO_CHECKOUT_ORDER: "INSERT_MY_INFO_CHECKOUT_ORDER", - GET_TAX_INFOS: "GET_TAX_INFOS", - UPDATE_SELECTED_SHIPPING_ADDR: "UPDATE_SELECTED_SHIPPING_ADDR", - UPDATE_SELECTED_BILLING_ADDR: "UPDATE_SELECTED_BILLING_ADDR", - UPDATE_SELECTED_COUPON: "UPDATE_SELECTED_COUPON", - GET_CHECKOUT_TOTAL_AMT: "GET_CHECKOUT_TOTAL_AMT", - CHECKOUT_DATA_RESET: "CHECKOUT_DATA_RESET", + GET_CHECKOUT_INFO: 'GET_CHECKOUT_INFO', + INSERT_MY_INFO_CHECKOUT_ORDER: 'INSERT_MY_INFO_CHECKOUT_ORDER', + GET_TAX_INFOS: 'GET_TAX_INFOS', + UPDATE_SELECTED_SHIPPING_ADDR: 'UPDATE_SELECTED_SHIPPING_ADDR', + UPDATE_SELECTED_BILLING_ADDR: 'UPDATE_SELECTED_BILLING_ADDR', + UPDATE_SELECTED_COUPON: 'UPDATE_SELECTED_COUPON', + GET_CHECKOUT_TOTAL_AMT: 'GET_CHECKOUT_TOTAL_AMT', + CHECKOUT_DATA_RESET: 'CHECKOUT_DATA_RESET', // order actions - SET_PURCHASE_TERMS_AGREE: "SET_PURCHASE_TERMS_AGREE", - SET_PURCHASE_TERMS_WITHDRAW: "SET_PURCHASE_TERMS_WITHDRAW", - GET_MY_INFO_ORDER_SEARCH: "GET_MY_INFO_ORDER_SEARCH", - GET_MY_INFO_ORDER_SEARCH_CANCEL: "GET_MY_INFO_ORDER_SEARCH_CANCEL", - GET_MY_INFO_ORDER_DETAIL_SEARCH: "GET_MY_INFO_ORDER_DETAIL_SEARCH", - GET_MY_INFO_ORDER_SHIPPING_SEARCH: "GET_MY_INFO_ORDER_SHIPPING_SEARCH", - CLEAR_MY_INFO_ORDER_SEARCH: "CLEAR_MY_INFO_ORDER_SEARCH", + SET_PURCHASE_TERMS_AGREE: 'SET_PURCHASE_TERMS_AGREE', + SET_PURCHASE_TERMS_WITHDRAW: 'SET_PURCHASE_TERMS_WITHDRAW', + GET_MY_INFO_ORDER_SEARCH: 'GET_MY_INFO_ORDER_SEARCH', + GET_MY_INFO_ORDER_SEARCH_CANCEL: 'GET_MY_INFO_ORDER_SEARCH_CANCEL', + GET_MY_INFO_ORDER_DETAIL_SEARCH: 'GET_MY_INFO_ORDER_DETAIL_SEARCH', + GET_MY_INFO_ORDER_SHIPPING_SEARCH: 'GET_MY_INFO_ORDER_SHIPPING_SEARCH', + CLEAR_MY_INFO_ORDER_SEARCH: 'CLEAR_MY_INFO_ORDER_SEARCH', // play controller - GET_CHAT_LOG: "GET_CHAT_LOG", - GET_SUBTITLE: "GET_SUBTITLE", - CLEAR_PLAYER_INFO: "CLEAR_PLAYER_INFO", + GET_CHAT_LOG: 'GET_CHAT_LOG', + GET_SUBTITLE: 'GET_SUBTITLE', + CLEAR_PLAYER_INFO: 'CLEAR_PLAYER_INFO', + UPDATE_VIDEO_PLAY_STATE: 'UPDATE_VIDEO_PLAY_STATE', // 🔽 [추가] 플레이 제어 매니저 액션 타입 /** @@ -224,58 +225,57 @@ export const types = { * SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다. * CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다. */ - SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL", - CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL", + SET_PLAYER_CONTROL: 'SET_PLAYER_CONTROL', + CLEAR_PLAYER_CONTROL: 'CLEAR_PLAYER_CONTROL', // reset action - RESET_REDUX_STATE: "RESET_REDUX_STATE", + RESET_REDUX_STATE: 'RESET_REDUX_STATE', // cancel actions - GET_MY_INFO_ORDER_CANCEL_SEARCH: "GET_MY_INFO_ORDER_CANCEL_SEARCH", - GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH: - "GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH", - UPDATE_ORDER_PARTIAL_CANCEL: "UPDATE_ORDER_PARTIAL_CANCEL", - PAYMENT_TOTAL_CANCEL: "PAYMENT_TOTAL_CANCEL", + GET_MY_INFO_ORDER_CANCEL_SEARCH: 'GET_MY_INFO_ORDER_CANCEL_SEARCH', + GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH: 'GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH', + UPDATE_ORDER_PARTIAL_CANCEL: 'UPDATE_ORDER_PARTIAL_CANCEL', + PAYMENT_TOTAL_CANCEL: 'PAYMENT_TOTAL_CANCEL', // emp actions - GET_SHOPTIME_TERMS: "GET_SHOPTIME_TERMS", + GET_SHOPTIME_TERMS: 'GET_SHOPTIME_TERMS', // pinCode actions - GET_MY_INFO_CARD_PINCODE_CHECK: "GET_MY_INFO_CARD_PINCODE_CHECK", + GET_MY_INFO_CARD_PINCODE_CHECK: 'GET_MY_INFO_CARD_PINCODE_CHECK', // new actions - CANCEL_FOCUS_ELEMENT: "CANCEL_FOCUS_ELEMENT", + CANCEL_FOCUS_ELEMENT: 'CANCEL_FOCUS_ELEMENT', // 약관동의 여부 확인 상태 - GET_TERMS_AGREE_YN_START: "GET_TERMS_AGREE_YN_START", - GET_TERMS_AGREE_YN_SUCCESS: "GET_TERMS_AGREE_YN_SUCCESS", - GET_TERMS_AGREE_YN_FAILURE: "GET_TERMS_AGREE_YN_FAILURE", + GET_TERMS_AGREE_YN_START: 'GET_TERMS_AGREE_YN_START', + GET_TERMS_AGREE_YN_SUCCESS: 'GET_TERMS_AGREE_YN_SUCCESS', + GET_TERMS_AGREE_YN_FAILURE: 'GET_TERMS_AGREE_YN_FAILURE', // device - REQ_REG_DEVICE_INFO: "REQ_REG_DEVICE_INFO", + REQ_REG_DEVICE_INFO: 'REQ_REG_DEVICE_INFO', // 🔽 [추가] 영구재생 비디오 정보 저장 - SET_PERSISTENT_VIDEO_INFO: "SET_PERSISTENT_VIDEO_INFO", + SET_PERSISTENT_VIDEO_INFO: 'SET_PERSISTENT_VIDEO_INFO', // 🔽 [추가] 배너 비디오 제어 액션 타입 /** * HomeBanner의 배너 간 비디오 재생 제어를 위한 액션 타입들. * 첫 번째 배너 상시 재생과 두 번째 배너 포커스 재생을 관리합니다. */ - SET_BANNER_STATE: "SET_BANNER_STATE", - SET_BANNER_FOCUS: "SET_BANNER_FOCUS", - SET_BANNER_AVAILABILITY: "SET_BANNER_AVAILABILITY", - SET_BANNER_TRANSITION: "SET_BANNER_TRANSITION", - PAUSE_PLAYER_CONTROL: "PAUSE_PLAYER_CONTROL", - RESUME_PLAYER_CONTROL: "RESUME_PLAYER_CONTROL", + SET_BANNER_STATE: 'SET_BANNER_STATE', + SET_BANNER_FOCUS: 'SET_BANNER_FOCUS', + SET_BANNER_AVAILABILITY: 'SET_BANNER_AVAILABILITY', + SET_BANNER_TRANSITION: 'SET_BANNER_TRANSITION', + PAUSE_PLAYER_CONTROL: 'PAUSE_PLAYER_CONTROL', + RESUME_PLAYER_CONTROL: 'RESUME_PLAYER_CONTROL', // 🔽 [추가] HomeBanner 동영상 포커스 정책 관리 - SET_CURRENT_FOCUS_BANNER: "SET_CURRENT_FOCUS_BANNER", - UPDATE_VIDEO_POLICY: "UPDATE_VIDEO_POLICY", - SET_MODAL_BORDER: "SET_MODAL_BORDER", - SET_BANNER_VISIBILITY: "SET_BANNER_VISIBILITY", + SET_CURRENT_FOCUS_BANNER: 'SET_CURRENT_FOCUS_BANNER', + UPDATE_VIDEO_POLICY: 'UPDATE_VIDEO_POLICY', + SET_MODAL_BORDER: 'SET_MODAL_BORDER', + SET_BANNER_VISIBILITY: 'SET_BANNER_VISIBILITY', // 🔽 [추가] JustForYou 상품 관리 부분 - GET_RECENTLY_SAW_ITEM: "GET_RECENTLY_SAW_ITEM", - GET_LIKE_BRAND_PRODUCT: "GET_LIKE_BRAND_PRODUCT", - GET_MORE_TO_CONCIDER_AT_THIS_PRICE: "GET_MORE_TO_CONCIDER_AT_THIS_PRICE", + GET_RECENTLY_SAW_ITEM: 'GET_RECENTLY_SAW_ITEM', + GET_LIKE_BRAND_PRODUCT: 'GET_LIKE_BRAND_PRODUCT', + GET_MORE_TO_CONCIDER_AT_THIS_PRICE: 'GET_MORE_TO_CONCIDER_AT_THIS_PRICE', }; diff --git a/com.twin.app.shoptime/src/actions/playActions.js b/com.twin.app.shoptime/src/actions/playActions.js index 0c6524fd..24d20d06 100644 --- a/com.twin.app.shoptime/src/actions/playActions.js +++ b/com.twin.app.shoptime/src/actions/playActions.js @@ -135,6 +135,52 @@ export const resumeModalVideo = () => (dispatch, getState) => { } }; +// 전체화면 비디오를 일시정지 (패널은 유지) +export const pauseFullscreenVideo = () => (dispatch, getState) => { + const panels = getState().panels.panels; + + // 전체화면 PlayerPanel 찾기 (modal이 false인 패널) + const fullscreenPlayerPanel = panels.find( + (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal + ); + + if (fullscreenPlayerPanel) { + console.log('[pauseFullscreenVideo] Pausing fullscreen video'); + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + ...fullscreenPlayerPanel.panelInfo, + isPaused: true, // 일시정지 플래그 추가 + }, + }) + ); + } +}; + +// 전체화면 비디오를 재생 (일시정지 해제) +export const resumeFullscreenVideo = () => (dispatch, getState) => { + const panels = getState().panels.panels; + + // 전체화면 PlayerPanel 찾기 (modal이 false인 패널) + const fullscreenPlayerPanel = panels.find( + (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal + ); + + if (fullscreenPlayerPanel && fullscreenPlayerPanel.panelInfo?.isPaused) { + console.log('[resumeFullscreenVideo] Resuming fullscreen video'); + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + ...fullscreenPlayerPanel.panelInfo, + isPaused: false, // 일시정지 해제 + }, + }) + ); + } +}; + // 채팅 로그 가져오기 IF-LGSP-371 export const getChatLog = ({ patnrId, showId }) => @@ -187,6 +233,20 @@ export const CLEAR_PLAYER_INFO = () => ({ type: types.CLEAR_PLAYER_INFO, }); +/** + * 비디오 재생 상태를 Redux에 업데이트합니다. + * @param {Object} playState - 업데이트할 재생 상태 + * @param {boolean} playState.isPlaying - 재생 중인지 여부 + * @param {boolean} playState.isPaused - 일시정지 상태인지 여부 + * @param {number} playState.currentTime - 현재 재생 시간(초) + * @param {number} playState.duration - 전체 비디오 길이(초) + * @param {number} playState.playbackRate - 재생 속도 + */ +export const updateVideoPlayState = (playState) => ({ + type: types.UPDATE_VIDEO_PLAY_STATE, + payload: playState, +}); + /* 🔽 [추가] 새로운 '플레이 제어 매니저' 액션들 */ /** diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js index b2a7ec56..03709b78 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js @@ -64,6 +64,7 @@ import Overlay from './Overlay'; import TReactPlayer from './TReactPlayer'; import Video from './Video'; import css from './VideoPlayer.module.less'; +import { updateVideoPlayState } from '../../actions/playActions'; const isEnter = is('enter'); const isLeft = is('left'); @@ -714,6 +715,7 @@ const VideoPlayerBase = class extends React.Component { belowContentsVisible: PropTypes.bool, tabContainerVersion: PropTypes.number, tabIndexV2: PropTypes.number, + dispatch: PropTypes.func, }; static contextType = FloatingLayerContext; @@ -1507,6 +1509,19 @@ const VideoPlayerBase = class extends React.Component { updatedState.thumbnailUrl = null; } this.setState(updatedState); + + // Redux에 비디오 재생 상태 업데이트 + if (this.props.dispatch) { + this.props.dispatch( + updateVideoPlayState({ + isPlaying: !updatedState.paused, + isPaused: updatedState.paused, + currentTime: updatedState.currentTime, + duration: updatedState.duration, + playbackRate: updatedState.playbackRate, + }) + ); + } }; renderBottomControl = new Job(() => { diff --git a/com.twin.app.shoptime/src/reducers/playReducer.js b/com.twin.app.shoptime/src/reducers/playReducer.js index 0cd4aca3..579a5ea6 100644 --- a/com.twin.app.shoptime/src/reducers/playReducer.js +++ b/com.twin.app.shoptime/src/reducers/playReducer.js @@ -1,8 +1,15 @@ -import { types } from "../actions/actionTypes"; +import { types } from '../actions/actionTypes'; const initialState = { subTitleBlobs: {}, chatData: null, + videoPlayState: { + isPlaying: false, + isPaused: true, + currentTime: 0, + duration: 0, + playbackRate: 1, + }, }; export const playReducer = (state = initialState, action) => { @@ -19,14 +26,13 @@ export const playReducer = (state = initialState, action) => { ...state, subTitleBlobs: { ...state.subTitleBlobs, - [action.payload.url]: "Error", + [action.payload.url]: 'Error', }, }; } - var srtRegex = - /(.*\n)?(\d\d:\d\d:\d\d),(\d\d\d --> \d\d:\d\d:\d\d),(\d\d\d)/g; - var vttText = "WEBVTT\n\n" + srtText.replace(srtRegex, "$1$2.$3.$4"); - var vttBlob = new Blob([vttText], { type: "text/vtt" }); + var srtRegex = /(.*\n)?(\d\d:\d\d:\d\d),(\d\d\d --> \d\d:\d\d:\d\d),(\d\d\d)/g; + var vttText = 'WEBVTT\n\n' + srtText.replace(srtRegex, '$1$2.$3.$4'); + var vttBlob = new Blob([vttText], { type: 'text/vtt' }); let blobUrl = URL.createObjectURL(vttBlob); return { @@ -43,6 +49,15 @@ export const playReducer = (state = initialState, action) => { chatData: {}, }; } + case types.UPDATE_VIDEO_PLAY_STATE: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + }, + }; + } default: return state; diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx index dcf54e9f..f5b78dcd 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx @@ -84,82 +84,6 @@ export default function HomeBanner({ firstSpot, spotlightId, handleItemFocus, ha } }, [handleItemFocus]); - // 🔽 [추가] 0번째 배너에 대한 영구 비디오 재생을 처리하는 useEffect - // useEffect(() => { - // if ( - // bannerDataList && - // bannerDataList.length > 0 && - // selectTemplate === "DSP00201" - // ) { - // const banner0Data = bannerDataList[0]?.bannerDetailInfos?.[0]; - - // if (banner0Data && banner0Data.showUrl) { - // // DOM 요소가 존재하는지 확인 후 실행 - // console.log("[HomeBanner] banner0Data", banner0Data); - // const checkAndPlay = () => { - // const targetElement = document.querySelector( - // '[data-spotlight-id="banner0"]', - // ); - // console.log("[HomeBanner] targetElement", targetElement); - // if (targetElement) { - // console.log("[HomeBanner] targetElement 존재"); - // dispatch( - // startVideoPlayer({ - // showUrl: banner0Data.showUrl, - // patnrId: banner0Data.patnrId, - // showId: banner0Data.showId, - // shptmBanrTpNm: "MEDIA", - // modal: true, - // modalContainerId: "banner0", - // spotlightDisable: true, - // }), - // ); - // console.log("[HomeBanner] startVideoPlayer 호출"); - // } else { - // // 요소가 없으면 잠시 후 재시도 - // console.log("[HomeBanner] targetElement 없음"); - // setTimeout(checkAndPlay, 100); - // } - // }; - - // // 다음 tick에서 실행하여 렌더링 완료 보장 - // setTimeout(checkAndPlay, 0); - // } - // } - // }, [dispatch, bannerDataList, selectTemplate]); - - // 🔽 [수정] 새로운 1번 배너 포커스/블러 핸들러 - const handleSecondBannerFocus = useCallback(() => { - const secondBannerData = bannerDataList?.[1]; - if (secondBannerData) { - const randomData = secondBannerData.bannerDetailInfos[secondBannerData.randomIndex]; - const videoInfo = { - showUrl: randomData.showUrl, - patnrId: randomData.patnrId, - showId: randomData.showId, - shptmBanrTpNm: randomData.showId ? randomData.shptmBanrTpNm : 'MEDIA', - lgCatCd: randomData.lgCatCd, - chanId: randomData.brdcChnlId, - modal: true, - modalContainerId: 'banner1', - modalClassName: css.videoModal, - isVerticalModal: true, // Assuming second banner is horizontal, so modal is vertical - }; - - // 중앙 제어 시스템에 '미리보기' 재생을 요청 - dispatch(requestPlayControl('banner1_preview', videoInfo)); - } - - if (handleItemFocus) { - handleItemFocus(); - } - }, [dispatch, bannerDataList, handleItemFocus]); - - const handleSecondBannerBlur = useCallback(() => { - // 중앙 제어 시스템에 '미리보기' 제어권 해제를 요청 - dispatch(releasePlayControl('banner1_preview')); - }, [dispatch]); - const termsData = useSelector((state) => state.home.termsData); const termsIdMap = useSelector((state) => state.home.termsIdMap); const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable); @@ -379,27 +303,6 @@ export default function HomeBanner({ firstSpot, spotlightId, handleItemFocus, ha return null; }, [bannerDataList]); - // useEffect(() => { - // if (firstSpot && defaultFocus && popupVisible === false) { - // setTimeout(() => { - // Spotlight.focus(defaultFocus); - // }, 0); - // } - - // if (defaultFocus) { - // dispatch(setDefaultFocus(defaultFocus)); - // } - // }, [defaultFocus, dispatch, popupVisible]); - - // 테스트용 팝업 표시 - // useEffect(() => { - // setTimeout(() => { - // console.log("App.js optionalTermsTest 팝업 표시"); - // setIsOptionalConfirmVisible(true); - // // setIsOptionalTermsVisible(true); - // }, 1000); - // }, []); - // 약관 동의 및 선택 약관 팝업 처리 (TV 환경 최적화) useEffect(() => { if (termsLoading) { @@ -423,77 +326,6 @@ export default function HomeBanner({ firstSpot, spotlightId, handleItemFocus, ha } }, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]); - // const renderItem = useCallback( - // (index, isHorizontal) => { - // const data = bannerDataList?.[index] ?? {}; - - // if (index === 1) { - // return ( - //
- // - //
- // ); - // } - - // return ( - //
- // {data.shptmDspyTpNm === "Rolling" ? ( - // - // ) : data.shptmDspyTpNm === "Random" ? ( - // - // ) : ( - // - // - // - // )} - //
- // ); - // }, - // [ - // bannerDataList, - // _handleItemFocus, - // _handleShelfFocus, - // handleSecondBannerFocus, - // handleSecondBannerBlur, - // ], - // ); - const renderItem = useCallback( (index, isHorizontal, videoPlayerable = false) => { const data = bannerDataList?.[index] ?? {}; @@ -678,7 +510,7 @@ export default function HomeBanner({ firstSpot, spotlightId, handleItemFocus, ha } } return null; - }, [selectTemplate, renderItem, renderSimpleVideoContainer]); + }, [selectTemplate, renderItem, renderSimpleVideoContainer, introTermsAgree]); return ( <> diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index 5b63a5cf..0f7c638e 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -254,6 +254,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props const liveShowInfos = USE_SELECTOR('liveShowInfos', (state) => state.main.liveShowInfos); const vodSubtitleData = USE_SELECTOR('vodSubtitleData', (state) => state.play.subTitleBlobs); const broadcast = USE_SELECTOR('broadcast', (state) => state.common.broadcast); + const videoPlayState = USE_SELECTOR('videoPlayState', (state) => state.play.videoPlayState); const lastPanelAction = USE_SELECTOR('lastPanelAction', (state) => state.panels.lastPanelAction); const nowMenu = USE_SELECTOR('nowMenu', (state) => state.common.menu.nowMenu); @@ -2215,6 +2216,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props broadcast={broadcast} tabContainerVersion={tabContainerVersion} tabIndexV2={tabIndexV2} + dispatch={dispatch} > {typeof window === 'object' && window.PalmSystem && (