From 1bd22683d85de7778199c4ef9213b636d2555c8e Mon Sep 17 00:00:00 2001 From: optrader Date: Mon, 13 Oct 2025 15:15:15 +0900 Subject: [PATCH] =?UTF-8?q?[251013]=20feat:ProductVideoV2=20,=20react-port?= =?UTF-8?q?al=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 10. 13. 15:15:13 ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 6๊ฐœ โ€ข ์ถ”๊ฐ€: +77์ค„ โ€ข ์‚ญ์ œ: -448์ค„ ๐Ÿ“ ์ถ”๊ฐ€๋œ ํŒŒ์ผ: + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/index.js ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ: ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less ๐Ÿ—‘๏ธ ์‚ญ์ œ๋œ ํŒŒ์ผ: - com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md ๐Ÿ”ง ํ•จ์ˆ˜ ๋ณ€๊ฒฝ ๋‚ด์šฉ: ๐Ÿ“„ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): ๐Ÿ”„ Modified: extractProductMeta() ๐Ÿ“„ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.module.less (unknown): โœ… Added: global() ๐Ÿ“„ com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md (mdํŒŒ์ผ): โŒ Deleted: Chain(), handleItemFocus(), useCallback(), renderItem(), JustForSwitchBanner(), useState(), useEffect(), cancelAnimationFrame(), requestAnimationFrame(), doSendLogGNB(), onItemFocus(), useRef(), current(), JustForYouBanner(), RollingUnit() ๐Ÿ”ง ์ฃผ์š” ๋ณ€๊ฒฝ ๋‚ด์šฉ: โ€ข ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฐœ์„  โ€ข ๊ฐœ๋ฐœ ๋ฌธ์„œ ๋ฐ ๊ฐ€์ด๋“œ ๊ฐœ์„  Performance: ์ฝ”๋“œ ์ตœ์ ํ™”๋กœ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ธฐ๋Œ€ --- ...]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md | 416 ------------------ .../src/views/DetailPanel/DetailPanel.jsx | 26 -- .../ProductAllSection/ProductAllSection.jsx | 38 +- .../ProductVideo/ProductVideo.module.less | 47 ++ .../ProductVideo/ProductVideo.v2.jsx | 303 +++++++++++++ .../ProductVideo/index.js | 5 + 6 files changed, 387 insertions(+), 448 deletions(-) delete mode 100644 com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/index.js diff --git a/com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md b/com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md deleted file mode 100644 index cba0d1bb..00000000 --- a/com.twin.app.shoptime/[251013]_RollingUnit_๋ฆฌ๋ Œ๋”๋ง_๋ถ„์„_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ๋ฒ•.md +++ /dev/null @@ -1,416 +0,0 @@ -# [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/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index f6e7a0f1..852b794f 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -590,30 +590,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { [panelPrdtId, productData] ); - // ===== ํŒŒํŠธ๋„ˆ์‚ฌ๋ณ„ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์„ค์ • ๋กœ์ง (ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™”) ===== - // thumbnailUrl960์„ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒํŠธ๋„ˆ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ธฐ๋Šฅ - // Pink Pong ๋“ฑ ํŠน์ • ํŒŒํŠธ๋„ˆ์‚ฌ์—์„œ๋งŒ thumbnailUrl960 ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด์„œ ๋ฐฐ๊ฒฝ์ด ๋ณ€๊ฒฝ๋จ - // ํ˜„์žฌ๋Š” ๊ณ ์ • ๋ฐฐ๊ฒฝ(detailPanelBg)๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ฃผ์„ ์ฒ˜๋ฆฌ - - /* - useLayoutEffect(() => { - const shouldSetBackground = fp.pipe( - () => ({ imageUrl, containerRef }), - ({ imageUrl, containerRef }) => - fp.isNotNil(imageUrl) && fp.isNotNil(containerRef.current) - )(); - - if (shouldSetBackground) { - containerRef.current.style.setProperty("--bg-url", `url('${imageUrl}')`); - } - }, [imageUrl]); - */ - - // console.log('productDataSource :', productDataSource); - - // ์–ธ๋งˆ์šดํŠธ ์‹œ ์ธ๋ฑ์Šค ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•˜๋ฉด: - // useEffect(() => () => setSelectedIndex(0), []) - const handleProductAllSectionReady = useCallback(() => { const spotTime = setTimeout(() => { Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE); @@ -625,8 +601,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { return (
- {/* ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ๋ฐ ๊ทธ๋ผ๋ฐ์ด์…˜ ์ปดํฌ๋„ŒํŠธ - ๋ชจ๋“  ์ฝ˜ํ…์ธ  ๋’ค์— ๋ Œ๋”๋ง */} - {/* launchedFromPlayer: PlayerPanel์—์„œ ์ง„์ž… ์‹œ true, ๋‹ค๋ฅธ ํŒจ๋„์—์„œ ์ง„์ž… ์‹œ false/undefined */} state.main.youmaylikeData); + // ProductVideo ๋ฒ„์ „ ๊ด€๋ฆฌ (1: ๊ธฐ์กด modal ๋ฐฉ์‹, 2: ๋‚ด์žฅ ๋ฐฉ์‹) + const [productVideoVersion, setProductVideoVersion] = useState(2); + // const [currentHeight, setCurrentHeight] = useState(0); //ํ•˜๋‹จ๋ถ€๋ถ„๊นŒ์ง€ ๊ฐ”์„๋•Œ ์ฒดํฌ์šฉ const [documentHeight, setDocumentHeight] = useState(0); @@ -391,6 +395,18 @@ export default function ProductAllSection({ return items; }, [productData]); + // renderItems์— Video๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” boolean ์ƒํƒœ + const hasVideo = useMemo(() => { + return ( + renderItems && renderItems.length > 0 && renderItems.some((item) => item.type === 'video') + ); + }, [renderItems]); + + // ProductVideo (version 1) ์ „์šฉ: ํฌ์ปค์Šค ๊ด€๋ฆฌ ๋ฐ modal ๋™์ž‘์„ ์œ„ํ•œ ํ”Œ๋ž˜๊ทธ + const isProductVideoV1Active = useMemo(() => { + return hasVideo && productVideoVersion === 1; + }, [hasVideo, productVideoVersion]); + const handleShopByMobileOpen = useCallback( pipe(() => true, setMobileSendPopupOpen), [] @@ -714,12 +730,22 @@ export default function ProductAllSection({ {renderItems.length > 0 ? ( renderItems.map((item, index) => item.type === 'video' ? ( - + productVideoVersion === 1 ? ( + + ) : ( + + ) ) : ( { + return Boolean(productInfo?.prdtMediaUrl); + }, [productInfo]); + + // YouTube ๋น„๋””์˜ค ์ฒดํฌ + const isYoutube = useMemo(() => { + const url = productInfo?.prdtMediaUrl; + if (url && url.includes('youtu')) { + return true; + } + return false; + }, [productInfo?.prdtMediaUrl]); + + // ๋น„๋””์˜ค ํƒ€์ž… ๊ฒฐ์ • + const videoType = useMemo(() => { + const url = productInfo?.prdtMediaUrl; + if (url) { + if (url.toLowerCase().endsWith('.mp4')) { + return 'video/mp4'; + } else if (url.toLowerCase().endsWith('.mpd')) { + return 'application/dash+xml'; + } else if (url.toLowerCase().endsWith('.m3u8')) { + return 'application/mpegurl'; + } + } + return 'application/mpegurl'; + }, [productInfo?.prdtMediaUrl]); + + // ์ž๋ง‰ ์„ค์ • + const reactPlayerSubtitleConfig = useMemo(() => { + const subtitleUrl = productInfo?.prdtMediaSubtitlUrl; + if (subtitleUrl) { + return { + file: { + attributes: { + crossOrigin: 'true', + }, + tracks: [{ kind: 'subtitles', src: subtitleUrl, default: true }], + }, + youtube: YOUTUBECONFIG, + }; + } else { + return { + youtube: YOUTUBECONFIG, + }; + } + }, [productInfo?.prdtMediaSubtitlUrl]); + + // VideoPlayer ref ์„ค์ • + const getPlayer = useCallback((ref) => { + videoPlayerRef.current = ref; + }, []); + + // ํฌ์ปค์Šค ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ + const videoContainerOnFocus = useCallback(() => { + if (canPlayVideo && !isPlaying) { + setFocused(true); + } + }, [canPlayVideo, isPlaying]); + + const videoContainerOnBlur = useCallback(() => { + if (canPlayVideo && !isPlaying) { + setFocused(false); + } + }, [canPlayVideo, isPlaying]); + + // ์ธ๋„ค์ผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ - ๋น„๋””์˜ค ์žฌ์ƒ ์‹œ์ž‘ + const handleThumbnailClick = useCallback(() => { + if (canPlayVideo && !isPlaying) { + setIsPlaying(true); + } + }, [canPlayVideo, isPlaying]); + + // ๋น„๋””์˜ค ์ข…๋ฃŒ ํ•ธ๋“ค๋Ÿฌ - ์ธ๋„ค์ผ๋กœ ๋ณต๊ท€ + const handleVideoEnded = useCallback(() => { + setIsPlaying(false); + setIsFullscreen(false); // ์ „์ฒดํ™”๋ฉด๋„ ํ•ด์ œ + }, []); + + // Back ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ - ์ „์ฒดํ™”๋ฉด ํ•ด์ œ ๋˜๋Š” ๋น„๋””์˜ค ์ข…๋ฃŒ + const handleBackButton = useCallback(() => { + if (isFullscreen) { + // ์ „์ฒดํ™”๋ฉด์ด๋ฉด ์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ + setIsFullscreen(false); + } else if (isPlaying) { + // ์ผ๋ฐ˜ ๋ชจ๋“œ์—์„œ ์žฌ์ƒ ์ค‘์ด๋ฉด ์ธ๋„ค์ผ๋กœ + setIsPlaying(false); + } + }, [isFullscreen, isPlaying]); + + // ๋”๋ฏธ ํ•จ์ˆ˜๋“ค (VideoPlayer๊ฐ€ ์š”๊ตฌํ•˜๋Š” props) + const setIsVODPaused = useCallback(() => {}, []); + const setSideContentsVisible = useCallback(() => {}, []); + const handleIndicatorDownClick = useCallback(() => {}, []); + const handleIndicatorUpClick = useCallback(() => {}, []); + const setIsSubtitleActive = useCallback(() => {}, []); + const setCurrentTime = useCallback(() => {}, []); + + // ์ „์ฒดํ™”๋ฉด ํ† ๊ธ€ ํ•ธ๋“ค๋Ÿฌ + const toggleFullscreen = useCallback(() => { + setIsFullscreen((prev) => !prev); + }, []); + + // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ: ๋น„๋””์˜ค ์žฌ์ƒ ์ค‘ ์—”ํ„ฐํ‚ค๋กœ ์ „์ฒดํ™”๋ฉด ํ† ๊ธ€ + const handleKeyDown = useCallback( + (e) => { + // ๋น„๋””์˜ค๊ฐ€ ์žฌ์ƒ ์ค‘์ผ ๋•Œ๋งŒ + if (!isPlaying) return; + + // ์—”ํ„ฐํ‚ค(13) ๋˜๋Š” OK ๋ฒ„ํŠผ + if (e.keyCode === 13 || e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + toggleFullscreen(); + } + }, + [isPlaying, toggleFullscreen] + ); + + // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก (window ๋ ˆ๋ฒจ, Portal์—์„œ๋„ ์ž‘๋™) + useEffect(() => { + if (isPlaying) { + window.addEventListener('keydown', handleKeyDown, true); // capture phase + + return () => { + window.removeEventListener('keydown', handleKeyDown, true); + }; + } + }, [isPlaying, handleKeyDown]); + + // autoPlay ๊ธฐ๋Šฅ: ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ํ›„ 500ms ํ›„ ์ž๋™ ์žฌ์ƒ + useEffect(() => { + if (autoPlay && canPlayVideo && !isPlaying) { + autoPlayTimerRef.current = setTimeout(() => { + setIsPlaying(true); + }, 500); + } + + // cleanup: unmount ์‹œ timer ํ•ด์ œ + return () => { + if (autoPlayTimerRef.current) { + clearTimeout(autoPlayTimerRef.current); + autoPlayTimerRef.current = null; + } + }; + }, [autoPlay, canPlayVideo, isPlaying]); + + if (!canPlayVideo) return null; + + // ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ ๊ฒฐ์ • + // ์ „์ฒดํ™”๋ฉด: SpotlightContainer (self-only) + // ์ผ๋ฐ˜ ์žฌ์ƒ: SpottableComponent (ํฌ์ปค์Šค ๊ฐ€๋Šฅ) + // ์ธ๋„ค์ผ: div + const ContainerComponent = isFullscreen + ? SpotlightContainer + : isPlaying + ? SpottableComponent + : 'div'; + + const containerProps = isFullscreen + ? { + spotlightRestrict: 'self-only', // ํฌ์ปค์Šค๊ฐ€ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก + spotlightId: 'product-video-v2-fullscreen', + } + : isPlaying + ? { + spotlightId: 'product-video-v2-playing', + // ์ผ๋ฐ˜ ์žฌ์ƒ ๋ชจ๋“œ: ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํฌ์ปค์Šค ๋ฐ›์Œ + } + : {}; + + // ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ปจํ…์ธ  + const videoContent = ( + + {!isPlaying ? ( + // ์ธ๋„ค์ผ + ์žฌ์ƒ ๋ฒ„ํŠผ ํ‘œ์‹œ + +
+ +
+ ์žฌ์ƒ +
+
+
+ ) : ( + // VideoPlayer ๋‚ด์žฅ ํ‘œ์‹œ +
+ + {typeof window === 'object' && window.PalmSystem && ( + + )} + {productInfo?.prdtMediaSubtitlUrl && + typeof window === 'object' && + window.PalmSystem && ( + + )} + +
+ )} +
+ ); + + // ์ „์ฒดํ™”๋ฉด์ผ ๋•Œ๋Š” Portal๋กœ body์— ์ง์ ‘ ๋ Œ๋”๋ง + if (isFullscreen) { + return ReactDOM.createPortal(videoContent, document.body); + } + + // ์ผ๋ฐ˜ ๋ชจ๋“œ์ผ ๋•Œ๋Š” ๊ทธ๋ƒฅ ๋ Œ๋”๋ง + return videoContent; +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/index.js b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/index.js new file mode 100644 index 00000000..e6d5ef4d --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/index.js @@ -0,0 +1,5 @@ +// Default export: ๊ธฐ์กด ProductVideo (modal ๋ฐฉ์‹) +export { default } from './ProductVideo'; + +// Named export: ProductVideoV2 (๋‚ด์žฅ ๋ฐฉ์‹) +export { default as ProductVideoV2 } from './ProductVideo.v2';