From c7ac0d7460f602a1d6139787a5b16b5bf106c051 Mon Sep 17 00:00:00 2001 From: optrader Date: Sat, 22 Nov 2025 16:46:51 +0900 Subject: [PATCH] [251122] fix: DetailPaneel->ProductAllSection Focus-4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 11. 22. 16:46:50 ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 7๊ฐœ โ€ข ์ถ”๊ฐ€: +184์ค„ โ€ข ์‚ญ์ œ: -1320์ค„ ๐Ÿ“ ์ถ”๊ฐ€๋œ ํŒŒ์ผ: + com.twin.app.shoptime/src/hooks/useDetailFocus/index.js + com.twin.app.shoptime/src/hooks/useDetailFocus/useDetailFocus.js ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ: ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ๐Ÿ—‘๏ธ ์‚ญ์ œ๋œ ํŒŒ์ผ: - com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md - com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md - com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md - com.twin.app.shoptime/[251116]_video_state_management_design.md ๐Ÿ”ง ํ•จ์ˆ˜ ๋ณ€๊ฒฝ ๋‚ด์šฉ: ๐Ÿ“„ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): ๐Ÿ”„ Modified: extractProductMeta() โŒ Deleted: tryFocusUp() ๐Ÿ“„ com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md (mdํŒŒ์ผ): โŒ Deleted: Before(), After() ๐Ÿ“„ com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md (mdํŒŒ์ผ): โŒ Deleted: useCallback(), showControls(), areControlsVisible(), toggleControls(), useLayoutEffect(), useEffect(), clearTimeout(), dispatch(), forEach(), getVideoNode(), addEventListener() ๐Ÿ“„ com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md (mdํŒŒ์ผ): โŒ Deleted: clearTimeout(), clearAllVideoTimers(), pause(), useEffect(), setTimeout(), useCallback(), resume(), dispatch(), stopPropagation(), preventDefault(), disconnect(), updateProportionLoaded(), setInterval(), clearInterval(), useRef() ๐Ÿ“„ com.twin.app.shoptime/[251116]_video_state_management_design.md (mdํŒŒ์ผ): โŒ Deleted: curry(), dispatch(), useSelector() ๐Ÿ”ง ์ฃผ์š” ๋ณ€๊ฒฝ ๋‚ด์šฉ: โ€ข ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฐœ์„  โ€ข ๊ฐœ๋ฐœ ๋ฌธ์„œ ๋ฐ ๊ฐ€์ด๋“œ ๊ฐœ์„  Performance: ์ฝ”๋“œ ์ตœ์ ํ™”๋กœ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ธฐ๋Œ€ --- .../DEBUG_MODE_IMPLEMENTATION.md | 221 --------- .../MEDIAPANEL_CLEANUP_IMPROVEMENTS.md | 430 ------------------ .../TIMER_CLEANUP_SUMMARY.md | 398 ---------------- .../[251116]_video_state_management_design.md | 220 --------- .../src/hooks/useDetailFocus/index.js | 1 + .../hooks/useDetailFocus/useDetailFocus.js | 120 +++++ .../ProductAllSection/ProductAllSection.jsx | 237 ++++++++-- 7 files changed, 308 insertions(+), 1319 deletions(-) delete mode 100644 com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md delete mode 100644 com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md delete mode 100644 com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md delete mode 100644 com.twin.app.shoptime/[251116]_video_state_management_design.md create mode 100644 com.twin.app.shoptime/src/hooks/useDetailFocus/index.js create mode 100644 com.twin.app.shoptime/src/hooks/useDetailFocus/useDetailFocus.js diff --git a/com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md b/com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md deleted file mode 100644 index 8507e85a..00000000 --- a/com.twin.app.shoptime/DEBUG_MODE_IMPLEMENTATION.md +++ /dev/null @@ -1,221 +0,0 @@ -# DEBUG_MODE ์กฐ๊ฑด๋ถ€ ๋กœ๊น… ๊ตฌํ˜„ ์™„๋ฃŒ - -**์ž‘์—… ์ผ์‹œ**: 2025-11-12 -**์ž‘์—… ๋ฒ”์œ„**: ProductVideo.v2.jsx, MediaPanel.jsx - ---- - -## ๐Ÿ“‹ ์ž‘์—… ๊ฐœ์š” - -ProductVideo.v2.jsx์™€ MediaPanel.jsx์˜ ๋ชจ๋“  ๋กœ๊ทธ ์ถœ๋ ฅ์„ `DEBUG_MODE = true/false` ํ”Œ๋ž˜๊ทธ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. - ---- - -## โœ… ๊ตฌํ˜„ ๋‚ด์šฉ - -### 1. DEBUG_MODE ์„ค์ • - -๊ฐ ํŒŒ์ผ์˜ ์ตœ์ƒ๋‹จ์— DEBUG_MODE ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค: - -```javascript -// โœ… DEBUG ๋ชจ๋“œ ์„ค์ • -const DEBUG_MODE = true; // false๋กœ ์„ค์ •ํ•˜๋ฉด ๋ชจ๋“  ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™” -``` - -**์„ค์ • ๋ณ€๊ฒฝ ๋ฐฉ๋ฒ•:** -- ํ”„๋กœ๋•์…˜: `const DEBUG_MODE = false;` ๋กœ ๋ณ€๊ฒฝ -- ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ: `const DEBUG_MODE = true;` ์œ ์ง€ - -### 2. debugLog ํ—ฌํผ ํ•จ์ˆ˜ - -DEBUG_MODE๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๋ž˜ํผ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค: - -```javascript -// โœ… DEBUG_MODE ๊ธฐ๋ฐ˜ console ๋ž˜ํผ -const debugLog = (...args) => { - if (DEBUG_MODE) { - console.log(...args); - } -}; -``` - -**ํŠน์ง•:** -- `console.log(...)` ๋Œ€์‹  `debugLog(...)` ์‚ฌ์šฉ -- DEBUG_MODE๊ฐ€ false์ด๋ฉด ๋กœ๊ทธ ์ถœ๋ ฅ ์•ˆ ๋จ -- ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ ๊ฑฐ์˜ ์—†์Œ (์กฐ๊ฑด ์ฒดํฌ๋งŒ ์ˆ˜ํ–‰) - -### 3. console ๋ฉ”์„œ๋“œ๋ณ„ ์ฒ˜๋ฆฌ - -| ๋ฉ”์„œ๋“œ | ์ฒ˜๋ฆฌ ๋ฐฉ์‹ | ํŒŒ์ผ | -|--------|----------|------| -| `console.log()` | `debugLog()` ๋กœ ๋ณ€๊ฒฝ | ProductVideo.v2.jsx, MediaPanel.jsx | -| `console.warn()` | `if (DEBUG_MODE) console.warn()` | ProductVideo.v2.jsx, MediaPanel.jsx | -| `console.error()` | `if (DEBUG_MODE) console.error()` | ProductVideo.v2.jsx | - ---- - -## ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„ - -### ProductVideo.v2.jsx -``` -- console.log() โ†’ debugLog(): ์•ฝ 40+ ๊ฐœ -- console.warn() โ†’ if (DEBUG_MODE) console.warn(): 2๊ฐœ -- console.error() โ†’ if (DEBUG_MODE) console.error(): 1๊ฐœ -``` - -### MediaPanel.jsx -``` -- console.log() โ†’ debugLog(): ์•ฝ 10+ ๊ฐœ -- console.warn() โ†’ if (DEBUG_MODE) console.warn(): 1๊ฐœ -``` - ---- - -## ๐ŸŽฏ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• - -### DEBUG ๋กœ๊ทธ ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ ๋ชจ๋“œ) -```javascript -const DEBUG_MODE = true; // โœ… ๋ชจ๋“  ๋กœ๊ทธ ์ถœ๋ ฅ๋จ -``` - -### DEBUG ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™” (ํ”„๋กœ๋•์…˜) -```javascript -const DEBUG_MODE = false; // โŒ ๋ชจ๋“  ๋กœ๊ทธ ์ˆจ๊น€ -``` - -### ํ•œ ์ค„ ๋ณ€๊ฒฝ์œผ๋กœ ์ „์ฒด ๋กœ๊น… ์ œ์–ด -๊ฐ ํŒŒ์ผ์˜ ๋‘ ๋ฒˆ์งธ ์ค„๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค: - -**ProductVideo.v2.jsx Line 36** -```javascript -const DEBUG_MODE = true; // ๋ณ€๊ฒฝ: true โ†” false -``` - -**MediaPanel.jsx Line 25** -```javascript -const DEBUG_MODE = true; // ๋ณ€๊ฒฝ: true โ†” false -``` - ---- - -## ๐Ÿ’ก ์žฅ์  - -1. **์„ฑ๋Šฅ ์ตœ์ ํ™”** - - ํ”„๋กœ๋•์…˜์—์„œ ๋กœ๊ทธ ์˜ค๋ฒ„ํ—ค๋“œ ์ œ๊ฑฐ - - ์กฐ๊ฑด ๊ฒ€์‚ฌ๋งŒ ์ˆ˜ํ–‰ (์ฝ˜์†” I/O ์—†์Œ) - -2. **๊ฐœ๋ฐœ ํŽธ์˜์„ฑ** - - ํ•œ ์ค„ ๋ณ€๊ฒฝ์œผ๋กœ ์ „์ฒด ๋กœ๊น… ์ œ์–ด - - ํŒŒ์ผ ์ˆ˜์ • ์—†์ด ENV ๋ณ€์ˆ˜๋กœ ์ œ์–ด ๊ฐ€๋Šฅ (ํ–ฅํ›„) - -3. **๋””๋ฒ„๊น… ์šฉ์ด** - - ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๊ทธ ํ™œ์„ฑํ™” - - ๋กœ๊ทธ ์–‘ ์ œ์–ด๋กœ ์ฝ˜์†” ์ง€์ €๋ถ„ํ•จ ๋ฐฉ์ง€ - -4. **์œ ์ง€๋ณด์ˆ˜ ํŽธํ•จ** - - ๊ธฐ์กด console ํ˜ธ์ถœ ๊ทธ๋Œ€๋กœ ์œ ์ง€ - - ๋กœ๊ทธ ์ฝ”๋“œ ์‚ญ์ œ ๋ถˆํ•„์š” - ---- - -## ๐Ÿ”ง ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ ์„ค์ • -```javascript -const DEBUG_MODE = process.env.REACT_APP_DEBUG === 'true'; -``` - -### 2. ์„ธ๋ถ€ ๋กœ๊ทธ ๋ ˆ๋ฒจ ๊ตฌ๋ถ„ -```javascript -const LOG_LEVEL = { - ERROR: 0, - WARN: 1, - INFO: 2, - DEBUG: 3, -}; - -const debugLog = (level, ...args) => { - if (LOG_LEVEL[level] <= getCurrentLogLevel()) { - console.log(...args); - } -}; -``` - -### 3. Redux DevTools ํ†ตํ•ฉ -```javascript -const debugLog = (...args) => { - if (DEBUG_MODE) { - console.log(...args); - // Redux DevTools ์— ์ถ”๊ฐ€ ์ •๋ณด ๊ธฐ๋ก - } -}; -``` - ---- - -## โœ… ๊ฒ€์ฆ ํ•ญ๋ชฉ - -- [x] ProductVideo.v2.jsx: ๋ชจ๋“  console.log โ†’ debugLog ๋ณ€๊ฒฝ -- [x] ProductVideo.v2.jsx: console.warn/error ์กฐ๊ฑด๋ถ€ ์ฒ˜๋ฆฌ -- [x] MediaPanel.jsx: ๋ชจ๋“  console.log โ†’ debugLog ๋ณ€๊ฒฝ -- [x] MediaPanel.jsx: console.warn ์กฐ๊ฑด๋ถ€ ์ฒ˜๋ฆฌ -- [x] debugLog ํ•จ์ˆ˜ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) -- [x] DEBUG_MODE ์„ค์ • ๊ฐ€๋Šฅ - ---- - -## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ - -1. **์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ** - - DEBUG_MODE = true์ผ ๋•Œ ๋ชจ๋“  ๋กœ๊ทธ ์ •์ƒ ์ถœ๋ ฅ ํ™•์ธ - - DEBUG_MODE = false์ผ ๋•Œ ๋ชจ๋“  ๋กœ๊ทธ ์ˆจ๊ฒจ์ง€๋Š”์ง€ ํ™•์ธ - -2. **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ** - - ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ์—์„œ ์„ฑ๋Šฅ ๊ฐœ์„  ํ™•์ธ - -3. **ENV ๋ณ€์ˆ˜ ์—ฐ๋™** - - `.env.development`, `.env.production` ์„ค์ • - - ๋นŒ๋“œ ์‹œ ์ž๋™์œผ๋กœ DEBUG_MODE ์„ค์ • - ---- - -## ๐Ÿ“ ์ฝ”๋“œ ์˜ˆ์‹œ - -### Before (์ˆ˜์ • ์ „) -```javascript -console.log('๐ŸŽฌ [handleThumbnailClick] ์ธ๋„ค์ผ ํด๋ฆญ๋จ', {...}); -console.warn('[ProductVideoV2] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); -console.error('๐Ÿ–ฅ๏ธ [toggleControls] ๋””์ŠคํŒจ์น˜ ์—๋Ÿฌ:', error); -``` - -### After (์ˆ˜์ • ํ›„) -```javascript -debugLog('๐ŸŽฌ [handleThumbnailClick] ์ธ๋„ค์ผ ํด๋ฆญ๋จ', {...}); -if (DEBUG_MODE) console.warn('[ProductVideoV2] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); -if (DEBUG_MODE) console.error('๐Ÿ–ฅ๏ธ [toggleControls] ๋””์ŠคํŒจ์น˜ ์—๋Ÿฌ:', error); -``` - ---- - -## ๐Ÿ“Œ ์ฃผ์˜์‚ฌํ•ญ - -1. **์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ** - - ๊ธฐ์กด์˜ ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ console.log๋Š” ์œ ์ง€๋จ - - ํ•„์š”์‹œ ๋‚˜์ค‘์— ์‚ญ์ œ ๊ฐ€๋Šฅ - -2. **debugLog ํ•จ์ˆ˜ ์œ„์น˜** - - ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜ ์™ธ๋ถ€์— ์„ ์–ธ๋จ - - ๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ (์„ฑ๋Šฅ ์ตœ์ ํ™”) - -3. **ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ** - - ๋ฐฐํฌ ์ „์— DEBUG_MODE๋ฅผ false๋กœ ๋ฐ˜๋“œ์‹œ ๋ณ€๊ฒฝํ•  ๊ฒƒ - ---- - -## โœจ ๊ฒฐ๋ก  - -ProductVideo.v2.jsx์™€ MediaPanel.jsx์˜ ๋ชจ๋“  ๋กœ๊ทธ ์ถœ๋ ฅ์„ DEBUG_MODE ํ”Œ๋ž˜๊ทธ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„์™„๋ฃŒ. -์ด๋ฅผ ํ†ตํ•ด ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ์ค‘์—๋Š” ๋””๋ฒ„๊น… ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, -ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋กœ๊ทธ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - -**์ž‘์—… ์ƒํƒœ**: โœ… ์™„๋ฃŒ diff --git a/com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md b/com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md deleted file mode 100644 index 64b75cc3..00000000 --- a/com.twin.app.shoptime/MEDIAPANEL_CLEANUP_IMPROVEMENTS.md +++ /dev/null @@ -1,430 +0,0 @@ -# MediaPanel.jsx ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ๋ฐ ํด๋ฆฐ์—… ๊ฐœ์„  - -**์ž‘์—… ์ผ์‹œ**: 2025-11-12 -**ํŒŒ์ผ**: MediaPanel.jsx -**์ƒํƒœ**: โœ… ์™„๋ฃŒ (์ฝ”๋“œ ์ˆ˜์ •๋งŒ, git/npm ๋ฏธ์‹คํ–‰) - ---- - -## ๐Ÿ“‹ ์ž‘์—… ๊ฐœ์š” - -MediaPanel.jsx์˜ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์•ˆ์ „ํ•œ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ๋ฅผ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐœ์„ ์‚ฌํ•ญ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค: - -- โœ… ์•ˆ์ „ํ•œ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ž˜ํผ -- โœ… ๊ฐ•ํ™”๋œ ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ํด๋ฆฐ์—… -- โœ… DOM ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” ๋ฐ ์ •๋ฆฌ -- โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” -- โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”์  ๋ฐ ์ •๋ฆฌ - ---- - -## ๐Ÿ”ง ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ž˜ํผ (safePlayerCall) - -**์œ„์น˜**: Line 107-117 - -```javascript -// โœ… ์•ˆ์ „ํ•œ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ -const safePlayerCall = useCallback((methodName, ...args) => { - if (videoPlayer.current && typeof videoPlayer.current[methodName] === 'function') { - try { - return videoPlayer.current[methodName](...args); - } catch (err) { - if (DEBUG_MODE) console.warn(`[MediaPanel] ${methodName} ํ˜ธ์ถœ ์‹คํŒจ:`, err); - } - } - return null; -}, []); -``` - -**์žฅ์ :** -- null/undefined ์•ˆ์ „ ๊ฒ€์‚ฌ -- ๋ฉ”์„œ๋“œ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ -- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ†ต์ผ -- ํŠธ๋ผ์ด-์บ์น˜๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - -**์‚ฌ์šฉ ์˜ˆ:** -```javascript -safePlayerCall('play'); -safePlayerCall('toggleControls'); -const mediaState = safePlayerCall('getMediaState'); -``` - -### 2. ๋ ˆํผ๋Ÿฐ์Šค ์ถ”์  Ref ์ถ”๊ฐ€ - -**์œ„์น˜**: Line 64 - -```javascript -const mediaEventListenersRef = useRef([]); // โœ… ๋ฏธ๋””์–ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”์  -``` - -**๋ชฉ์ :** -- ๋“ฑ๋ก๋œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๊ด€๋ฆฌ -- ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ๊ฐ€๋Šฅ -- ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ - -### 3. isOnTop ๋ณ€๊ฒฝ ์‹œ ์•ˆ์ „ํ•œ ์ œ์–ด - -**์œ„์น˜**: Line 178-188 - -**Before:** -```javascript -if (videoPlayer.current?.getMediaState()?.paused) { - videoPlayer.current.play(); -} -if (videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible()) { - videoPlayer.current.showControls(); -} -``` - -**After:** -```javascript -// โœ… ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ๋กœ null/undefined ์ฒดํฌ -const mediaState = safePlayerCall('getMediaState'); -if (mediaState?.paused) { - safePlayerCall('play'); -} - -const isControlsHidden = videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible(); -if (isControlsHidden) { - safePlayerCall('showControls'); -} -``` - -**๊ฐœ์„ ์ :** -- mediaState null ์ฒดํฌ ๊ฐ•ํ™” -- ๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด ํ˜ธ์ถœ์„ ์•ˆ์ „ํ•œ ๋ž˜ํผ๋กœ ํ†ต์ผ -- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ผ๊ด€์„ฑ - -### 4. ๋น„๋””์˜ค ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ ๊ฐœ์„  - -**์œ„์น˜**: Line 199-208 - -**Before:** -```javascript -if (videoPlayer.current && typeof videoPlayer.current.toggleControls === 'function') { - videoPlayer.current.toggleControls(); -} -``` - -**After:** -```javascript -safePlayerCall('toggleControls'); -``` - -**๊ฐœ์„ ์ :** -- ์ฝ”๋“œ ๊ฐ„๊ฒฐ์„ฑ -- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ†ต์ผ - -### 5. ๋’ค๋กœ๊ฐ€๊ธฐ ์‹œ ๋น„๋””์˜ค ์ •์ง€ - -**์œ„์น˜**: Line 212-213 - -```javascript -// โœ… ๋’ค๋กœ๊ฐ€๊ธฐ ์‹œ ๋น„๋””์˜ค ์ •์ง€ -safePlayerCall('pause'); -``` - -**ํšจ๊ณผ:** -- ํŒจ๋„ ๋‹ซ์„ ๋•Œ ๋น„๋””์˜ค ์ž๋™ ์ •์ง€ -- ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ์‹œ์ž‘ - -### 6. DOM ์Šคํƒ€์ผ ์„ค์ • ๋ฐ ์ •๋ฆฌ - -**์œ„์น˜**: Line 353-376 - -**Before:** -```javascript -useLayoutEffect(() => { - const videoContainer = document.querySelector(`.${css.videoContainer}`); - if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) { - videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`; - videoContainer.style.backgroundColor = 'black'; - } -}, [panelInfo.thumbnailUrl, videoLoaded]); -``` - -**After:** -```javascript -// โœ… useLayoutEffect: DOM ์Šคํƒ€์ผ ์„ค์ • (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) -useLayoutEffect(() => { - const videoContainer = document.querySelector(`.${css.videoContainer}`); - if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) { - try { - videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`; - videoContainer.style.backgroundColor = 'black'; - } catch (err) { - if (DEBUG_MODE) console.warn('[MediaPanel] ์ธ๋„ค์ผ ์Šคํƒ€์ผ ์„ค์ • ์‹คํŒจ:', err); - } - } - - // โœ… cleanup: ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ DOM ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” - return () => { - if (videoContainer) { - try { - videoContainer.style.background = ''; - videoContainer.style.backgroundColor = ''; - } catch (err) { - // ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ - } - } - }; -}, [panelInfo.thumbnailUrl, videoLoaded]); -``` - -**๊ฐœ์„ ์ :** -- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ -- cleanup ํ•จ์ˆ˜๋กœ DOM ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” -- ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ - -### 7. mediainfoHandler ๊ฐ•ํ™” - -**์œ„์น˜**: Line 280-326 - -**๊ฐœ์„  ์‚ฌํ•ญ:** -- safePlayerCall ์‚ฌ์šฉ์œผ๋กœ null ์•ˆ์ •์„ฑ -- hlsError ์ฒ˜๋ฆฌ ๊ฐ•ํ™” -- timeupdate ์ด๋ฒคํŠธ์—์„œ mediaState ์ฒดํฌ -- error ์ด๋ฒคํŠธ์—์„œ null ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต - -```javascript -case 'timeupdate': { - const mediaState = safePlayerCall('getMediaState'); - if (mediaState) { - setCurrentTime(mediaState.currentTime || 0); // โœ… ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต - } - break; -} -``` - -### 8. ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ์ „์ฒด ํด๋ฆฐ์—… ๊ฐ•ํ™” - -**์œ„์น˜**: Line 382-429 - -**๊ฐœ์„  ์‚ฌํ•ญ:** - -```javascript -useEffect(() => { - return () => { - // โœ… onEnded ํƒ€์ด๋จธ ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - onEndedTimerRef.current = null; - } - - // โœ… Redux ์ƒํƒœ ์ •๋ฆฌ - dispatch(stopMediaAutoClose?.()) || null; - - // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ ๋ฐ ์ •๋ฆฌ - if (videoPlayer.current) { - try { - safePlayerCall('pause'); - safePlayerCall('hideControls'); - } catch (err) { - if (DEBUG_MODE) console.warn('[MediaPanel] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', err); - } - videoPlayer.current = null; // โœ… ref ์ดˆ๊ธฐํ™” - } - - // โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ - if (mediaEventListenersRef.current && mediaEventListenersRef.current.length > 0) { - mediaEventListenersRef.current.forEach(({ element, event, handler }) => { - try { - element?.removeEventListener?.(event, handler); - } catch (err) { - // ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ์‹คํŒจ๋Š” ๋ฌด์‹œ - } - }); - mediaEventListenersRef.current = []; - } - - // โœ… Spotlight ์ƒํƒœ ์ดˆ๊ธฐํ™” - try { - Spotlight.resume?.(); - } catch (err) { - // Spotlight ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ - } - }; -}, [dispatch, safePlayerCall]); -``` - -**์ •๋ฆฌ ํ•ญ๋ชฉ:** -1. โœ… onEnded ํƒ€์ด๋จธ ์ •๋ฆฌ -2. โœ… Redux ์ƒํƒœ ์ •๋ฆฌ -3. โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ -4. โœ… ํ”Œ๋ ˆ์ด์–ด ref ์ดˆ๊ธฐํ™” -5. โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ -6. โœ… Spotlight ์ƒํƒœ ๋ณต๊ตฌ - ---- - -## ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„ - -| ํ•ญ๋ชฉ | ์ˆ˜๋Ÿ‰ | -|------|------| -| ์ƒˆ๋กœ์šด Ref | 1๊ฐœ (mediaEventListenersRef) | -| ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ | 1๊ฐœ (safePlayerCall) | -| ๊ฐœ์„ ๋œ useEffect | 2๊ฐœ | -| ๊ฐœ์„ ๋œ ์ฝœ๋ฐฑ | 3๊ฐœ | -| ์ถ”๊ฐ€๋œ ํด๋ฆฐ์—… ๋กœ์ง | 6๊ฐœ ํ•ญ๋ชฉ | -| ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” | 4๊ฐœ ์ง€์  | - ---- - -## ๐ŸŽฏ ํšจ๊ณผ - -### ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ -- โœ… ํƒ€์ด๋จธ ๋ช…์‹œ์  ์ •๋ฆฌ -- โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”์  ๋ฐ ์ •๋ฆฌ -- โœ… ref ์ดˆ๊ธฐํ™” -- โœ… Redux ์ƒํƒœ ์ •๋ฆฌ - -### ์•ˆ์ •์„ฑ ํ–ฅ์ƒ -- โœ… null/undefined ์ฒดํฌ ๊ฐ•ํ™” -- โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ†ต์ผ -- โœ… ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ฐฉ์ง€ -- โœ… ํŠธ๋ผ์ด-์บ์น˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - -### ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  -- โœ… ๋ฐ˜๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ -- โœ… ์ผ๊ด€๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ -- โœ… ๋ช…ํ™•ํ•œ ์ฃผ์„ -- โœ… ์•ˆ์ „ํ•œ ๋””ํดํŠธ๊ฐ’ ์‚ฌ์šฉ - ---- - -## ๐Ÿ” ํ˜ธํ™˜์„ฑ ํ™•์ธ - -### ๊ธฐ์กด ๊ธฐ๋Šฅ ๋ณด์กด -- โœ… ๋น„๋””์˜ค ์žฌ์ƒ/์ •์ง€ ๋™์ž‘ ์œ ์ง€ -- โœ… controls ํ‘œ์‹œ/์ˆจ๊น€ ๋กœ์ง ์œ ์ง€ -- โœ… modal โ†” fullscreen ์ „ํ™˜ ์œ ์ง€ -- โœ… onEnded ์ฝœ๋ฐฑ ๋™์ž‘ ์œ ์ง€ -- โœ… ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋™์ž‘ ์œ ์ง€ - -### ์ถ”๊ฐ€ ๋ณดํ˜ธ -- โœ… null ์ฐธ์กฐ ์˜ˆ์™ธ ๋ฐฉ์ง€ -- โœ… ์ž˜๋ชป๋œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ฐฉ์ง€ -- โœ… DOM ์ ‘๊ทผ ์—๋Ÿฌ ๋ฐฉ์ง€ -- โœ… ํƒ€์ด๋จธ ์ค‘๋ณต ์ •๋ฆฌ ๋ฐฉ์ง€ - ---- - -## ๐Ÿ“Œ ์ฃผ์˜์‚ฌํ•ญ - -### DEBUG_MODE ์„ค์ • -```javascript -const DEBUG_MODE = false; // ํ”„๋กœ๋•์…˜ -const DEBUG_MODE = true; // ๊ฐœ๋ฐœ/๋””๋ฒ„๊น… -``` - -- DEBUG_MODE = false์ผ ๋•Œ: ๋ชจ๋“  ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ˆจ๊น€ -- DEBUG_MODE = true์ผ ๋•Œ: ๋ชจ๋“  ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ํ‘œ์‹œ - -### safePlayerCall ์‚ฌ์šฉ ๊ทœ์น™ -1. ์กด์žฌํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋งŒ ์‚ฌ์šฉ -2. ๋ฐ˜ํ™˜๊ฐ’์ด ํ•„์š”ํ•˜๋ฉด null ์ฒดํฌ -3. ํ•ญ์ƒ try-catch๋กœ ๊ฐ์‹ธ์ง - -```javascript -// โœ… Good -const state = safePlayerCall('getMediaState'); -if (state) { /* ... */ } - -// โœ… Good -safePlayerCall('play'); - -// โŒ Bad - ์กด์žฌํ•˜๋Š” ๋ฉ”์„œ๋“œ๋Š” ์ง์ ‘ ํ˜ธ์ถœ -videoPlayer.current.getVideoNode(); -``` - ---- - -## ๐Ÿš€ ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ - -1. **์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ž๋™ ์ถ”์ ** - ```javascript - const addTrackedListener = useCallback((element, event, handler) => { - element.addEventListener(event, handler); - mediaEventListenersRef.current.push({ element, event, handler }); - }, []); - ``` - -2. **์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง** - - ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋กœ๊น… - - ํƒ€์ด๋จธ ์ •๋ฆฌ ์‹œ๊ฐ„ ์ธก์ • - -3. **ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€** - - ๋ฐ˜๋ณต ๋งˆ์šดํŠธ/์–ธ๋งˆ์šดํŠธ ํ…Œ์ŠคํŠธ - - ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํ…Œ์ŠคํŠธ - - ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ - ---- - -## โœ… ๊ฒ€์ฆ ํ•ญ๋ชฉ - -- [x] ๊ธฐ์กด ๊ธฐ๋Šฅ ๋™์ž‘ ํ™•์ธ -- [x] ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ๋กœ์ง ์ถ”๊ฐ€ -- [x] null/undefined ์•ˆ์ „์„ฑ ๊ฐ•ํ™” -- [x] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ†ต์ผ -- [x] ํด๋ฆฐ์—… ํ•จ์ˆ˜ ์™„์„ฑ -- [x] ์ฃผ์„ ๋ฐ ๋ฌธ์„œํ™” ์™„๋ฃŒ - ---- - -## ๐Ÿ“ ์ฝ”๋“œ ์˜ˆ์‹œ - -### safePlayerCall ์‚ฌ์šฉ ์˜ˆ -```javascript -// Before -if (videoPlayer.current?.getMediaState()?.paused) { - videoPlayer.current.play(); -} - -// After -const mediaState = safePlayerCall('getMediaState'); -if (mediaState?.paused) { - safePlayerCall('play'); -} -``` - -### ์–ธ๋งˆ์šดํŠธ ํด๋ฆฐ์—… -```javascript -useEffect(() => { - return () => { - // ํƒ€์ด๋จธ ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - } - - // Redux ์ •๋ฆฌ - dispatch(stopMediaAutoClose?.()); - - // ํ”Œ๋ ˆ์ด์–ด ์ •๋ฆฌ - safePlayerCall('pause'); - videoPlayer.current = null; - - // ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ - mediaEventListenersRef.current.forEach(({ element, event, handler }) => { - element?.removeEventListener?.(event, handler); - }); - }; -}, [dispatch, safePlayerCall]); -``` - ---- - -## โœจ ๊ฒฐ๋ก  - -MediaPanel.jsx์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ๋ฐ ํด๋ฆฐ์—… ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค: - -1. **์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ** - safePlayerCall ๋ž˜ํผ -2. **๊ฐ•ํ™”๋œ ํด๋ฆฐ์—…** - 6๊ฐœ ํ•ญ๋ชฉ ์ •๋ฆฌ -3. **์—๋Ÿฌ ์ฒ˜๋ฆฌ** - ํ†ต์ผ๋œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ -4. **๋ฆฌ์Šค๋„ˆ ์ถ”์ ** - ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๊ด€๋ฆฌ ์ค€๋น„ -5. **DOM ์ •๋ฆฌ** - ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” - -์ด๋ฅผ ํ†ตํ•ด ์žฅ์‹œ๊ฐ„ ์‚ฌ์šฉ ์‹œ์—๋„ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ฉ๋‹ˆ๋‹ค. - -**์ž‘์—… ์ƒํƒœ**: โœ… ์™„๋ฃŒ (์ฝ”๋“œ ์ˆ˜์ •๋งŒ, git/npm ๋ฏธ์‹คํ–‰) diff --git a/com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md b/com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md deleted file mode 100644 index e3d30ed6..00000000 --- a/com.twin.app.shoptime/TIMER_CLEANUP_SUMMARY.md +++ /dev/null @@ -1,398 +0,0 @@ -# ํƒ€์ด๋จธ ํด๋ฆฐ์—… ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ์ž‘์—… ์™„๋ฃŒ ๋ณด๊ณ  - -**์ž‘์—… ์ผ์‹œ**: 2025-11-12 -**์ž‘์—… ๋ฒ”์œ„**: ProductVideo.v2.jsx, MediaPanel.jsx, MediaPlayer.v2.jsx - ---- - -## ๐Ÿ“‹ ์ž‘์—… ๊ฐœ์š” - -๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ๋“ค์—์„œ ํƒ€์ด๋จธ์™€ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ œ๋Œ€๋กœ ์ •๋ฆฌ๋˜์ง€ ์•Š์•„ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๊ฐœ์„  ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค: - -- โœ… **setTimeout/setInterval ํƒ€์ด๋จธ์˜ ๋ช…์‹œ์  ์ •๋ฆฌ** -- โœ… **์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์˜ ์ ์ ˆํ•œ ๋“ฑ๋ก/ํ•ด์ œ** -- โœ… **Ref๋ฅผ ํ†ตํ•œ ํƒ€์ด๋จธ ์ถ”์  ๋ฐ ์ •๋ฆฌ** -- โœ… **์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ** - ---- - -## ๐Ÿ”ง ProductVideo.v2.jsx ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. autoPlay ํƒ€์ด๋จธ ์ •๋ฆฌ ๊ฐ•ํ™” -**ํŒŒ์ผ ์œ„์น˜**: Line 566-597 - -```javascript -// Before -return () => { - if (autoPlayTimerRef.current) { - clearTimeout(autoPlayTimerRef.current); - autoPlayTimerRef.current = null; - } - clearAllVideoTimers(); - if (videoPlayerRef.current) { - try { - videoPlayerRef.current.pause(); - } catch (error) { - console.warn('[ProductVideoV2] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); - } - } -}; - -// After -return () => { - // โœ… autoPlay timer ์ •๋ฆฌ - if (autoPlayTimerRef.current) { - clearTimeout(autoPlayTimerRef.current); - autoPlayTimerRef.current = null; - } - // โœ… ์ „์—ญ ๋น„๋””์˜ค ํƒ€์ด๋จธ ์ •๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) - clearAllVideoTimers?.(); // Optional chaining ์ถ”๊ฐ€ - // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ - if (videoPlayerRef.current) { - try { - videoPlayerRef.current.pause?.(); // Optional chaining ์ถ”๊ฐ€ - } catch (error) { - console.warn('[ProductVideoV2] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); - } - } -}; -``` - -**๊ฐœ์„ ์ **: -- Optional chaining (`?.`) ์ถ”๊ฐ€๋กœ null/undefined ์ฒดํฌ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ -- `isPlaying` dependency ์ œ๊ฑฐ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) -- ๋ช…ํ™•ํ•œ ์ฃผ์„์œผ๋กœ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ๊ฐœ์„  - -### 2. ์ „์ฒดํ™”๋ฉด ์ „ํ™˜ ์‹œ ํƒ€์ด๋จธ ์ •๋ฆฌ -**ํŒŒ์ผ ์œ„์น˜**: Line 615-647 - -```javascript -// Before -useEffect(() => { - if (isPlaying && videoPlayerRef.current) { - // ... - const timeoutId = setTimeout(() => { - // ... - }, 100); - return () => clearTimeout(timeoutId); - } -}, [isFullscreen, isPlaying]); - -// After -useEffect(() => { - if (isPlaying && videoPlayerRef.current) { - // ... - const timeoutId = setTimeout(() => { - // ... - }, 100); - // โœ… cleanup: ํƒ€์ด๋จธ ์ •๋ฆฌ - return () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - }; - } -}, [isFullscreen, isPlaying]); -``` - -**๊ฐœ์„ ์ **: -- Null ์ฒดํฌ ์ถ”๊ฐ€๋กœ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ -- ๋ช…ํ™•ํ•œ cleanup ํ•จ์ˆ˜ ์ž‘์„ฑ - -### 3. ์ „์—ญ document ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ ๋ช…ํ™•ํ™” -**ํŒŒ์ผ ์œ„์น˜**: Line 504-537 - -**๊ฐœ์„ ์ **: -- ๋ช…ํ™•ํ•œ ์ฃผ์„์œผ๋กœ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก/ํ•ด์ œ ์˜๋„ ํ‘œ๋ช… -- cleanup ํ•จ์ˆ˜์—์„œ ์ผ๊ด€๋œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ - ---- - -## ๐ŸŽฌ MediaPanel.jsx ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. onEnded ํƒ€์ด๋จธ ๊ด€๋ฆฌ ๊ฐœ์„  -**ํŒŒ์ผ ์œ„์น˜**: Line 52-53 (ref ์ถ”๊ฐ€), Line 285-308 (์ฝœ๋ฐฑ ๊ฐœ์„ ) - -```javascript -// Added ref for timer tracking -const onEndedTimerRef = useRef(null); // โœ… onEnded ํƒ€์ด๋จธ ๊ด€๋ฆฌ - -// Before -const onEnded = useCallback( - (e) => { - Spotlight.pause(); - setTimeout(() => { - Spotlight.resume(); - dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); - }, 1500); - e?.stopPropagation(); - e?.preventDefault(); - }, - [dispatch] -); - -// After -const onEnded = useCallback( - (e) => { - Spotlight.pause(); - // โœ… ์ด์ „ ํƒ€์ด๋จธ๊ฐ€ ์žˆ์œผ๋ฉด ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - } - // โœ… ์ƒˆ๋กœ์šด ํƒ€์ด๋จธ ์ €์žฅ (cleanup ์‹œ ์ •๋ฆฌ์šฉ) - onEndedTimerRef.current = setTimeout(() => { - Spotlight.resume(); - dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); - onEndedTimerRef.current = null; - }, 1500); - e?.stopPropagation(); - e?.preventDefault(); - }, - [dispatch] -); -``` - -**๊ฐœ์„ ์ **: -- useRef๋ฅผ ํ†ตํ•œ ํƒ€์ด๋จธ ์ถ”์ ์œผ๋กœ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ -- ๋ช…์‹œ์  ํƒ€์ด๋จธ ์ •๋ฆฌ ๋กœ์ง - -### 2. ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ํƒ€์ด๋จธ ์ •๋ฆฌ -**ํŒŒ์ผ ์œ„์น˜**: Line 322-340 (์‹ ๊ทœ useEffect ์ถ”๊ฐ€) - -```javascript -// โœ… ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ด๋จธ ์ •๋ฆฌ -useEffect(() => { - return () => { - // onEnded ํƒ€์ด๋จธ ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - onEndedTimerRef.current = null; - } - // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ - if (videoPlayer.current) { - try { - videoPlayer.current.pause?.(); - } catch (error) { - console.warn('[MediaPanel] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); - } - } - }; -}, []); -``` - -**๊ฐœ์„ ์ **: -- ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ด๋จธ ์ •๋ฆฌ -- ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๊ฐ•์ œ ์ •์ง€๋กœ ๋ฆฌ์†Œ์Šค ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ - -### 3. Modal ์Šคํƒ€์ผ ์„ค์ • ์‹œ ResizeObserver ์ •๋ฆฌ -**ํŒŒ์ผ ์œ„์น˜**: Line 114-171 - -```javascript -// โœ… modal ์Šคํƒ€์ผ ์„ค์ • -useEffect(() => { - let resizeObserver = null; - // ... ์Šคํƒ€์ผ ์„ค์ • ๋กœ์ง - // โœ… cleanup: resize observer ์ •๋ฆฌ - return () => { - if (resizeObserver) { - resizeObserver.disconnect(); - } - }; -}, [panelInfo, isOnTop]); -``` - -**๊ฐœ์„ ์ **: -- ResizeObserver ์ดˆ๊ธฐํ™”๋กœ ๋ฏธ๋ž˜ ๊ตฌํ˜„ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ์ค€๋น„ - ---- - -## ๐Ÿ“น MediaPlayer.v2.jsx ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. proportionLoaded ์—…๋ฐ์ดํŠธ ํƒ€์ด๋จธ ์ตœ์ ํ™” -**ํŒŒ์ผ ์œ„์น˜**: Line 411-431 - -```javascript -// Before -useEffect(() => { - updateProportionLoaded(); - const interval = setInterval(() => { - updateProportionLoaded(); - }, 1000); - return () => clearInterval(interval); -}, [updateProportionLoaded]); - -// After -useEffect(() => { - updateProportionLoaded(); - // โœ… 1์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ (๋น„๋””์˜ค ์žฌ์ƒ ์ค‘์ผ ๋•Œ๋งŒ) - let intervalId = null; - if (!paused) { - intervalId = setInterval(() => { - updateProportionLoaded(); - }, 1000); - } - // โœ… cleanup: interval ์ •๋ฆฌ - return () => { - if (intervalId !== null) { - clearInterval(intervalId); - } - }; -}, [updateProportionLoaded, paused]); -``` - -**๊ฐœ์„ ์ **: -- ๋น„๋””์˜ค ์ผ์‹œ์ •์ง€ ์ค‘์—๋Š” interval ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ (๋ถˆํ•„์š”ํ•œ ํƒ€์ด๋จธ ์ œ๊ฑฐ) -- `paused` dependency ์ถ”๊ฐ€๋กœ ์ƒํƒœ ๋ณ€ํ™” ๊ฐ์ง€ -- ๋ช…์‹œ์  null ์ฒดํฌ๋กœ ์ •๋ฆฌ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ - -### 2. ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ์ „์ฒด cleanup ๊ฐ•ํ™” -**ํŒŒ์ผ ์œ„์น˜**: Line 433-454 - -```javascript -// โœ… Cleanup: ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ด๋จธ ๋ฐ ์ƒํƒœ ์ •๋ฆฌ -useEffect(() => { - return () => { - // โœ… controlsTimeoutRef ์ •๋ฆฌ - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - controlsTimeoutRef.current = null; - } - // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ - if (videoRef.current) { - try { - videoRef.current.pause?.(); - } catch (error) { - console.warn('[MediaPlayer.v2] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', error); - } - } - // โœ… MediaPlayer ์–ธ๋งˆ์šดํŠธ ์‹œ Redux ์ƒํƒœ ์ •๋ฆฌ - dispatch(stopMediaAutoClose()); - }; -}, [dispatch]); -``` - -**๊ฐœ์„ ์ **: -- ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๊ฐ•์ œ ์ •์ง€ ์ถ”๊ฐ€ -- Optional chaining์œผ๋กœ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ -- ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ถ”๊ฐ€ - -### 3. hideControls ๋ฉ”์„œ๋“œ ์ฃผ์„ ์ถ”๊ฐ€ -**ํŒŒ์ผ ์œ„์น˜**: Line 290-299 - -**๊ฐœ์„ ์ **: -- ํƒ€์ด๋จธ ์ •๋ฆฌ ์˜๋„ ๋ช…ํ™•ํ™”๋ฅผ ์œ„ํ•œ ์ฃผ์„ ์ถ”๊ฐ€ - ---- - -## ๐ŸŽฏ ํ•ต์‹ฌ ๊ฐœ์„  ํŒจํ„ด - -### 1. **Ref๋ฅผ ํ†ตํ•œ ํƒ€์ด๋จธ ์ถ”์ ** -```javascript -const timerRef = useRef(null); - -const startTimer = () => { - if (timerRef.current) { - clearTimeout(timerRef.current); - } - timerRef.current = setTimeout(() => { - // ... - timerRef.current = null; - }, delay); -}; - -useEffect(() => { - return () => { - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - }; -}, []); -``` - -### 2. **Optional Chaining์œผ๋กœ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ** -```javascript -// Before -videoRef.current.pause(); - -// After -videoRef.current.pause?.(); -``` - -### 3. **์กฐ๊ฑด๋ถ€ ํƒ€์ด๋จธ ์ƒ์„ฑ** -```javascript -// Before - ํ•ญ์ƒ interval ์ƒ์„ฑ -const interval = setInterval(() => { - updateProportionLoaded(); -}, 1000); - -// After - ํ•„์š”ํ•  ๋•Œ๋งŒ ์ƒ์„ฑ -let intervalId = null; -if (!paused) { - intervalId = setInterval(() => { - updateProportionLoaded(); - }, 1000); -} -``` - ---- - -## โœ… ๊ฒ€์ฆ ํ•ญ๋ชฉ - -๋‹ค์Œ ํ•ญ๋ชฉ๋“ค์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: - -- [x] **autoPlay ํƒ€์ด๋จธ** ์ •๋ฆฌ ๊ฐ•ํ™” (ProductVideo.v2.jsx) -- [x] **์ „์ฒดํ™”๋ฉด ์ „ํ™˜ ํƒ€์ด๋จธ** ์ •๋ฆฌ (ProductVideo.v2.jsx) -- [x] **Document ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ** ์ •๋ฆฌ ๋ช…ํ™•ํ™” (ProductVideo.v2.jsx) -- [x] **onEnded ํƒ€์ด๋จธ** Ref ์ถ”์  (MediaPanel.jsx) -- [x] **์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ cleanup** ๊ฐ•ํ™” (MediaPanel.jsx) -- [x] **Modal ์Šคํƒ€์ผ ์„ค์ •** ResizeObserver ์ •๋ฆฌ ์ค€๋น„ (MediaPanel.jsx) -- [x] **proportionLoaded ์—…๋ฐ์ดํŠธ** ํƒ€์ด๋จธ ์ตœ์ ํ™” (MediaPlayer.v2.jsx) -- [x] **์ „์ฒด cleanup ํ•จ์ˆ˜** ๊ฐ•ํ™” (MediaPlayer.v2.jsx) - ---- - -## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ - -### ๊ถŒ์žฅ ์‚ฌํ•ญ - -1. **Redux Actions ๊ฒ€ํ† ** - - `clearAllVideoTimers()` ์•ก์…˜์ด ์‹ค์ œ๋กœ ๋ชจ๋“  ํƒ€์ด๋จธ๋ฅผ ์ •๋ฆฌํ•˜๋Š”์ง€ ํ™•์ธ - - `startMediaAutoClose()`, `stopMediaAutoClose()` ํƒ€์ด๋จธ ์ •๋ฆฌ ๋กœ์ง ๊ฒ€ํ†  - -2. **VideoPlayer/Media ์ปดํฌ๋„ŒํŠธ** - - webOS Media ์ปดํฌ๋„ŒํŠธ์˜ ํƒ€์ด๋จธ ์ •๋ฆฌ ๋กœ์ง ํ™•์ธ - - TReactPlayer์˜ cleanup ๋กœ์ง ๊ฒ€ํ†  - -3. **ํ…Œ์ŠคํŠธ** - - ์žฅ์‹œ๊ฐ„ ๋น„๋””์˜ค ์žฌ์ƒ ํ›„ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง - - ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฐ˜๋ณต ์žฌ์ƒ/์ •์ง€ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํ™•์ธ - - ์ „์ฒดํ™”๋ฉด ์ „ํ™˜ ์‹œ ๋ฆฌ์†Œ์Šค ๋ˆ„์ˆ˜ ํ™•์ธ - -4. **์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง** - - Chrome DevTools Memory tab์—์„œ ํž™ ์Šค๋ƒ…์ƒท ๋น„๊ต - - ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ/์–ธ๋งˆ์šดํŠธ ๋ฐ˜๋ณต ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์ฆ๊ฐ ํ™•์ธ - ---- - -## ๐Ÿ“ ์ฃผ์š” ๋ณ€๊ฒฝ ์š”์•ฝ - -| ํŒŒ์ผ | ๋ณ€๊ฒฝ ์‚ฌํ•ญ | ๋ผ์ธ | ๊ฐœ์„  ํšจ๊ณผ | -|------|---------|------|---------| -| ProductVideo.v2.jsx | autoPlay ํƒ€์ด๋จธ ์ •๋ฆฌ ๊ฐ•ํ™” | 566-597 | ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ | -| ProductVideo.v2.jsx | ์ „์ฒดํ™”๋ฉด ์ „ํ™˜ ํƒ€์ด๋จธ ์ •๋ฆฌ | 615-647 | ํƒ€์ด๋จธ ์ค‘๋ณต ๋ฐฉ์ง€ | -| ProductVideo.v2.jsx | Document ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ | 504-537 | ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ | -| MediaPanel.jsx | onEnded ํƒ€์ด๋จธ Ref ์ถ”์  | 52-53, 285-308 | ํƒ€์ด๋จธ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ | -| MediaPanel.jsx | ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ cleanup | 322-340 | ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ | -| MediaPanel.jsx | Modal ์Šคํƒ€์ผ ResizeObserver | 114-171 | ์˜ต์ €๋ฒ„ ์ •๋ฆฌ ์ค€๋น„ | -| MediaPlayer.v2.jsx | proportionLoaded ํƒ€์ด๋จธ ์ตœ์ ํ™” | 411-431 | ๋ถˆํ•„์š”ํ•œ ํƒ€์ด๋จธ ์ œ๊ฑฐ | -| MediaPlayer.v2.jsx | ์ „์ฒด cleanup ๊ฐ•ํ™” | 433-454 | ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ | - ---- - -## โœจ ๊ฒฐ๋ก  - -๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ํƒ€์ด๋จธ์™€ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. -์ด๋ฅผ ํ†ตํ•ด ์žฅ์‹œ๊ฐ„ ๋น„๋””์˜ค ์žฌ์ƒ ์‹œ์—๋„ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ฉ๋‹ˆ๋‹ค. - -**์ž‘์—… ์ƒํƒœ**: โœ… ์™„๋ฃŒ diff --git a/com.twin.app.shoptime/[251116]_video_state_management_design.md b/com.twin.app.shoptime/[251116]_video_state_management_design.md deleted file mode 100644 index c96fd3f2..00000000 --- a/com.twin.app.shoptime/[251116]_video_state_management_design.md +++ /dev/null @@ -1,220 +0,0 @@ -# [251116] ์ƒˆ๋กœ์šด ๋น„๋””์˜ค ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌํ˜„ - -## ๊ฐœ์š” - -๊ธฐ์กด์˜ videoPlayReducer๋Š” ์œ ์ง€ํ•˜๋˜, PlayerPanel๊ณผ VideoPlayer.js๋ฅผ ์œ„ํ•œ ์ƒˆ๋กœ์šด ๋น„๋””์˜ค ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ playerReducer์— ๊ตฌํ˜„ํ•œ๋‹ค. ์žฌ์ƒ ์ƒํƒœ์™€ ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๋” ์ •๋ฐ€ํ•œ ๋น„๋””์˜ค ์ƒํƒœ ์ œ์–ด๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค. - -## ์„ค๊ณ„ ์›์น™ - -1. **๊ธฐ์กด videoPlayReducer ์œ ์ง€**: ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ ์ค‘์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค -2. **playerReducer์— ์ƒˆ๋กœ์šด ์ƒํƒœ ์‹œ์Šคํ…œ ๊ตฌํ˜„**: PlayerPanel๊ณผ VideoPlayer.js ์ „์šฉ -3. **์ด์ค‘ ์ƒํƒœ ๊ด€๋ฆฌ**: ์žฌ์ƒ ์ƒํƒœ(Playback Status) + ํ™”๋ฉด ์ƒํƒœ(Display Status) -4. **๊ธฐ์กด ํŒจํ„ด ๋”ฐ๋ฅด๊ธฐ**: FP ์Šคํƒ€์ผ์˜ curry, get, set ํ™œ์šฉ - -## ์ƒˆ๋กœ์šด ์ƒํƒœ ๊ตฌ์กฐ - -### ์ƒ์ˆ˜ ์ •์˜ (playerActions.js) - -```javascript -// ์žฌ์ƒ ์ƒํƒœ -export const PLAYBACK_STATUS = { - LOADING: 'loading', // ๋น„๋””์˜ค ๋กœ๋”ฉ ์ค‘ - LOAD_SUCCESS: 'load_success', // ๋น„๋””์˜ค ๋กœ๋”ฉ ์„ฑ๊ณต - LOAD_ERROR: 'load_error', // ๋น„๋””์˜ค ๋กœ๋”ฉ ์˜ค๋ฅ˜ - PLAYING: 'playing', // ๋น„๋””์˜ค ์žฌ์ƒ ์ค‘ - NOT_PLAYING: 'not_playing', // ๋น„๋””์˜ค ์žฌ์ƒ ์•„๋‹˜ (์ •์ง€/์ผ์‹œ์ •์ง€) - BUFFERING: 'buffering' // ๋ฒ„ํผ๋ง ์ค‘ -}; - -// ํ™”๋ฉด ์ƒํƒœ -export const DISPLAY_STATUS = { - HIDDEN: 'hidden', // ํ™”๋ฉด์— ์•ˆ๋ณด์ž„ - VISIBLE: 'visible', // ํ™”๋ฉด์— ๋ณด์ž„ - MINIMIZED: 'minimized', // ์ตœ์†Œํ™”๋จ - FULLSCREEN: 'fullscreen' // ์ „์ฒดํ™”๋ฉด -}; -``` - -### ์ดˆ๊ธฐ ์ƒํƒœ (playerReducer) - -```javascript -// ๊ธฐ์กด playerReducer ์ƒํƒœ์— ์ถ”๊ฐ€ -const initialState = { - // ... ๊ธฐ์กด ์ƒํƒœ๋“ค - - playerVideoState: { - // ํ˜„์žฌ ์ƒํƒœ - playback: PLAYBACK_STATUS.NOT_PLAYING, - display: DISPLAY_STATUS.HIDDEN, - videoId: null, - progress: 0, - error: null, - timestamp: null - } -}; -``` - -## ์•ก์…˜ ํƒ€์ž… ๋ฐ ํ•จ์ˆ˜ - -### ์•ก์…˜ ํƒ€์ž… - -```javascript -export const PLAYER_VIDEO_ACTIONS = { - // ์žฌ์ƒ ์ƒํƒœ ์•ก์…˜ - SET_PLAYBACK_LOADING: 'SET_PLAYBACK_LOADING', - SET_PLAYBACK_SUCCESS: 'SET_PLAYBACK_SUCCESS', - SET_PLAYBACK_ERROR: 'SET_PLAYBACK_ERROR', - SET_PLAYBACK_PLAYING: 'SET_PLAYBACK_PLAYING', - SET_PLAYBACK_NOT_PLAYING: 'SET_PLAYBACK_NOT_PLAYING', - SET_PLAYBACK_BUFFERING: 'SET_PLAYBACK_BUFFERING', - - // ํ™”๋ฉด ์ƒํƒœ ์•ก์…˜ - SET_DISPLAY_HIDDEN: 'SET_DISPLAY_HIDDEN', - SET_DISPLAY_VISIBLE: 'SET_DISPLAY_VISIBLE', - SET_DISPLAY_MINIMIZED: 'SET_DISPLAY_MINIMIZED', - SET_DISPLAY_FULLSCREEN: 'SET_DISPLAY_FULLSCREEN', - - // ๋ณตํ•ฉ ์•ก์…˜ - SET_VIDEO_LOADING: 'SET_VIDEO_LOADING', - SET_VIDEO_PLAYING: 'SET_VIDEO_PLAYING', - SET_VIDEO_STOPPED: 'SET_VIDEO_STOPPED', - SET_VIDEO_MINIMIZED_PLAYING: 'SET_VIDEO_MINIMIZED_PLAYING', -}; -``` - -### ์•ก์…˜ ํ•จ์ˆ˜ - -```javascript -// ๊ธฐ๋ณธ ์•ก์…˜ ํ•จ์ˆ˜๋“ค (FP ์Šคํƒ€์ผ) -export const setPlaybackLoading = curry((videoId, displayMode = 'visible') => ({ - type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_LOADING, - payload: { - playback: PLAYBACK_STATUS.LOADING, - display: displayMode, - videoId, - progress: 0, - error: null, - timestamp: Date.now() - } -})); - -export const setPlaybackPlaying = curry((videoId, displayMode = 'fullscreen') => ({ - type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_PLAYING, - payload: { - playback: PLAYBACK_STATUS.PLAYING, - display: displayMode, - videoId, - progress: 100, - error: null, - timestamp: Date.now() - } -})); - -export const setPlaybackError = curry((videoId, error) => ({ - type: PLAYER_VIDEO_ACTIONS.SET_PLAYBACK_ERROR, - payload: { - playback: PLAYBACK_STATUS.LOAD_ERROR, - display: DISPLAY_STATUS.VISIBLE, - videoId, - error, - progress: 0, - timestamp: Date.now() - } -})); - -export const setVideoStopped = () => ({ - type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_STOPPED, - payload: { - playback: PLAYBACK_STATUS.NOT_PLAYING, - display: DISPLAY_STATUS.HIDDEN, - videoId: null, - error: null, - progress: 0, - timestamp: Date.now() - } -})); -``` - -## ์ƒํƒœ ์‚ฌ์šฉ ์˜ˆ์‹œ - -### PlayerPanel.jsx - -```javascript -import { - setPlaybackLoading, - setPlaybackPlaying, - setPlaybackError, - setVideoStopped -} from '../actions/playerActions'; - -// ๋น„๋””์˜ค ๋กœ๋”ฉ ์‹œ์ž‘ -const handleVideoLoadStart = (videoId) => { - dispatch(setPlaybackLoading(videoId, 'fullscreen')); -}; - -// ๋น„๋””์˜ค ์žฌ์ƒ ์‹œ์ž‘ -const handleVideoPlay = (videoId) => { - dispatch(setPlaybackPlaying(videoId, 'fullscreen')); -}; - -// ๋น„๋””์˜ค ์—๋Ÿฌ ๋ฐœ์ƒ -const handleVideoError = (videoId, error) => { - dispatch(setPlaybackError(videoId, error)); -}; - -// ์ƒํƒœ ํ™•์ธ -const videoState = useSelector(state => state.player.playerVideoState); -const isLoading = videoState.playback === PLAYBACK_STATUS.LOADING; -const isPlaying = videoState.playback === PLAYBACK_STATUS.PLAYING; -const hasError = videoState.playback === PLAYBACK_STATUS.LOAD_ERROR; -const isFullscreen = videoState.display === DISPLAY_STATUS.FULLSCREEN; -``` - -### VideoPlayer.js - -```javascript -// ํ˜„์žฌ ์ƒํƒœ์— ๋”ฐ๋ฅธ UI ๋ Œ๋”๋ง -const renderVideoState = () => { - const { playback, display, error, progress } = videoState; - - if (playback === PLAYBACK_STATUS.LOADING) { - return ; - } - - if (playback === PLAYBACK_STATUS.LOAD_ERROR) { - return ; - } - - if (playback === PLAYBACK_STATUS.BUFFERING) { - return ; - } - - if (playback === PLAYBACK_STATUS.PLAYING && display === DISPLAY_STATUS.FULLSCREEN) { - return ; - } - - return null; -}; -``` - -## ๊ตฌํ˜„ ์ˆœ์„œ - -1. [ ] playerActions.js์— ์ƒ์ˆ˜ ๋ฐ ์•ก์…˜ ํ•จ์ˆ˜๋“ค ์ถ”๊ฐ€ -2. [ ] playerReducer.js์— ์ดˆ๊ธฐ ์ƒํƒœ ๋ฐ ํ•ธ๋“ค๋Ÿฌ๋“ค ์ถ”๊ฐ€ -3. [ ] PlayerPanel.jsx์—์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ ์‹œ์Šคํ…œ์œผ๋กœ ์ „ํ™˜ -4. [ ] VideoPlayer.js์—์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ ์‹œ์Šคํ…œ์œผ๋กœ ์ „ํ™˜ -5. [ ] ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ - -## ์žฅ์  - -1. **์ •๋ฐ€ํ•œ ์ƒํƒœ ์ œ์–ด**: ์žฌ์ƒ ์ƒํƒœ์™€ ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๋ณ„๋„๋กœ ๊ด€๋ฆฌ -2. **๋ช…ํ™•ํ•œ ์ƒํƒœ ์˜๋ฏธ**: ๊ฐ ์ƒํƒœ๊ฐ€ ๋ช…ํ™•ํ•œ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง -3. **ํ™•์žฅ์„ฑ**: ์ƒˆ๋กœ์šด ์ƒํƒœ ์ถ”๊ฐ€๊ฐ€ ์šฉ์ด -4. **์œ ์ง€๋ณด์ˆ˜์„ฑ**: ๊ธฐ์กด ์ฝ”๋“œ ์˜ํ–ฅ ์ตœ์†Œํ™” -5. **์žฌ์‚ฌ์šฉ์„ฑ**: ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ํ™œ์šฉ ๊ฐ€๋Šฅ - -## ์ฃผ์˜์‚ฌํ•ญ - -- ๊ธฐ์กด videoPlayReducer์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ -- PlayerPanel๊ณผ VideoPlayer.js์—๋งŒ ์ง‘์ค‘ํ•˜์—ฌ ๊ตฌํ˜„ -- ๊ธฐ์กด ๋น„๋””์˜ค ์žฌ์ƒ ๋กœ์ง๊ณผ ํ˜ธํ™˜์„ฑ ์œ ์ง€ \ No newline at end of file diff --git a/com.twin.app.shoptime/src/hooks/useDetailFocus/index.js b/com.twin.app.shoptime/src/hooks/useDetailFocus/index.js new file mode 100644 index 00000000..aba091c4 --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/useDetailFocus/index.js @@ -0,0 +1 @@ +export { default } from './useDetailFocus'; diff --git a/com.twin.app.shoptime/src/hooks/useDetailFocus/useDetailFocus.js b/com.twin.app.shoptime/src/hooks/useDetailFocus/useDetailFocus.js new file mode 100644 index 00000000..094ed5d6 --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/useDetailFocus/useDetailFocus.js @@ -0,0 +1,120 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import Spotlight from '@enact/spotlight'; + +/** + * useDetailFocus - ํฌ์ปค์Šค ์ด๋™ ๋ณด์ •์šฉ Hook + * + * ProductAllSection์˜ ๋ณต์žกํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ์ธํ•œ ํฌ์ปค์Šค ์†์‹ค์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด + * arrow key์— ๋”ฐ๋ผ ๋‹ค์Œ ํฌ์ปค์Šค ํ•ญ๋ชฉ์„ queue ํ˜•ํƒœ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , + * ํƒ€์ด๋จธ๋กœ ํฌ์ปค์Šค ์ด๋™์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * useEffect์˜ ์˜์กด์„ฑ๋ฐฐ์—ด์— ๋”ฐ๋ผ ํƒ€์ด๋จธ๊ฐ€ ๊ด€๋ฆฌ๋˜๊ณ , + * ์ปดํฌ๋„ŒํŠธ unmount ์‹œ ์ž๋™์œผ๋กœ cleanup๋ฉ๋‹ˆ๋‹ค. + * + * @param {number} delayMs - ํฌ์ปค์Šค ์ด๋™ ์ง€์—ฐ ์‹œ๊ฐ„ (๊ธฐ๋ณธ๊ฐ’: 250ms) + * @returns {Object} { enqueueFocus } + * + * @example + * const { enqueueFocus } = useDetailFocus(250); + * + * const handleArrowDown = (e) => { + * e.stopPropagation(); + * enqueueFocus('next-button-id'); + * }; + */ +export default function useDetailFocus(delayMs = 500) { + const focusQueueRef = useRef([]); + const [queueTick, setQueueTick] = useState(0); + const timerRef = useRef(null); + + /** + * ํฌ์ปค์Šค ID๋ฅผ queue์— ์ถ”๊ฐ€ (์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ, ํƒ€์ด๋จธ๋Š” useEffect์—์„œ ๊ด€๋ฆฌ) + * @param {string} focusId - ํฌ์ปค์Šคํ•  ์š”์†Œ์˜ ID + */ + const enqueueFocus = useCallback( + (focusId) => { + if (!focusId) { + console.warn('[FocusDetail] โš ๏ธ focusId๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค'); + return; + } + + // ํ์— ์ถ”๊ฐ€ํ•˜๊ณ  tick์„ ์˜ฌ๋ ค useEffect๋ฅผ ํŠธ๋ฆฌ๊ฑฐ + focusQueueRef.current.push(focusId); + console.log(`[FocusDetail] ๐Ÿ“‹ Queue์— ID ์ถ”๊ฐ€: ${focusId} (${delayMs}ms ํ›„ ํฌ์ปค์Šค)`); + setQueueTick((tick) => tick + 1); + }, + [delayMs] + ); + + /** + * focusQueue ์ƒํƒœ์— ๋”ฐ๋ผ ํƒ€์ด๋จธ ๊ด€๋ฆฌ + * focusQueue๊ฐ€ ์„ค์ •๋˜๋ฉด ํƒ€์ด๋จธ ์‹œ์ž‘ + * ์ปดํฌ๋„ŒํŠธ unmount ์‹œ useEffect cleanup์—์„œ ์ž๋™์œผ๋กœ ํƒ€์ด๋จธ ์ •๋ฆฌ + */ + useEffect(() => { + // queue์— ์•„๋ฌด๊ฒƒ๋„ ์—†์œผ๋ฉด ์ข…๋ฃŒ + if (!focusQueueRef.current.length) { + console.log(`[FocusDetail] ๐Ÿ“ญ Queue ๋น„์–ด์žˆ์Œ (ํฌ์ปค์Šค ๋ณด์ • ๋ถˆํ•„์š”)`); + return undefined; + } + + // ๊ธฐ์กด ํƒ€์ด๋จธ๊ฐ€ ์žˆ์œผ๋ฉด ์ทจ์†Œ + if (timerRef.current) { + console.log( + `[FocusDetail] โน๏ธ ๊ธฐ์กด ํƒ€์ด๋จธ ์ทจ์†Œ - ๋Œ€๊ธฐ ์ค‘์ธ Queue: ${focusQueueRef.current.join(',')}` + ); + clearTimeout(timerRef.current); + } + + // ์ƒˆ๋กœ์šด ํƒ€์ด๋จธ ์„ค์ • + const targetId = focusQueueRef.current[focusQueueRef.current.length - 1]; // ๋งˆ์ง€๋ง‰ ID + console.log(`[FocusDetail] โฑ๏ธ ํƒ€์ด๋จธ ์‹œ์ž‘ - ${delayMs}ms ํ›„ ํฌ์ปค์Šค ์ด๋™: ${targetId}`); + timerRef.current = setTimeout(() => { + console.log(`[FocusDetail] โฑ๏ธ ํƒ€์ด๋จธ ๋งŒ๋ฃŒ - ํฌ์ปค์Šค ์ด๋™ ์‹œ๋„: ${targetId}`); + + // ํ˜„์žฌ ํฌ์ปค์Šค๋œ ์š”์†Œ ํ™•์ธ + const currentElement = Spotlight.getCurrent(); + const currentId = currentElement?.dataset?.spotlightId || currentElement?.id || 'unknown'; + console.log(`[FocusDetail] ๐Ÿ“Œ ํ˜„์žฌ ํฌ์ปค์Šค ์ƒํƒœ: ${currentId}, ํƒ€๊นƒ: ${targetId}`); + + try { + const success = Spotlight.focus(targetId); + + if (!success) { + const afterElement = Spotlight.getCurrent(); + const afterId = afterElement?.dataset?.spotlightId || afterElement?.id || 'unknown'; + console.warn(`[FocusDetail] โŒ ํฌ์ปค์Šค ์ด๋™ ์‹คํŒจ: ${targetId} (ํ˜„์žฌ: ${afterId})`); + console.warn( + `[FocusDetail] ๐Ÿ“‹ ์š”์†Œ ์กด์žฌ ํ™•์ธ: ${document.querySelector(`[data-spotlight-id="${targetId}"]`) ? 'โœ… ์กด์žฌ' : 'โŒ ์—†์Œ'}` + ); + } else { + console.log(`[FocusDetail] โœ… ํฌ์ปค์Šค ์ด๋™ ์„ฑ๊ณต: ${targetId}`); + } + } catch (error) { + console.error('[FocusDetail] ๐Ÿ’ฅ ํฌ์ปค์Šค ์ด๋™ ์ค‘ ์—๋Ÿฌ:', error); + } finally { + // ํƒ€์ด๋จธ ์ •๋ฆฌ + console.log(`[FocusDetail] ๐Ÿงน ํƒ€์ด๋จธ ์ •๋ฆฌ - ์ฒ˜๋ฆฌ ์™„๋ฃŒ: ${targetId}`); + timerRef.current = null; + // Queue ์ดˆ๊ธฐํ™” + focusQueueRef.current = []; + } + }, delayMs); + + // cleanup: ์˜์กด์„ฑ๋ฐฐ์—ด ๋ณ€๊ฒฝ ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ unmount ์‹œ ํƒ€์ด๋จธ ์ •๋ฆฌ + return () => { + if (timerRef.current) { + console.log( + `[FocusDetail] ๐Ÿงน useEffect cleanup - ๋Œ€๊ธฐ ์ค‘์ธ ํƒ€์ด๋จธ ์ทจ์†Œ (Queue: ${focusQueueRef.current.join(',')})` + ); + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + }, [queueTick, delayMs]); + + return { + enqueueFocus, + focusQueue: focusQueueRef.current, // ๋””๋ฒ„๊น…์šฉ + }; +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx index 602594bd..4905afb9 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -34,6 +34,7 @@ import TPopUp from '../../../components/TPopUp/TPopUp.jsx'; import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList.jsx'; import useReviews from '../../../hooks/useReviews/useReviews'; import useScrollTo from '../../../hooks/useScrollTo'; +import useDetailFocus from '../../../hooks/useDetailFocus'; import { BUYNOW_CONFIG } from '../../../utils/BuyNowConfig'; import { panel_names } from '../../../utils/Config'; import * as Config from '../../../utils/Config.js'; @@ -81,7 +82,10 @@ const Container = SpotlightContainerDecorator( { enterTo: 'last-focused', preserveld: true, - leaveFor: { right: 'content-scroller-container' }, + leaveFor: { + right: 'content-scroller-container', + up: 'spotlight-product-info-section-container', + }, spotlightDirection: 'vertical', }, 'div' @@ -156,6 +160,9 @@ export default function ProductAllSection({ }) { const dispatch = useDispatch(); + // ํฌ์ปค์Šค ์ด๋™ ๋ณด์ • Hook (0.25์ดˆ ํƒ€์ด๋จธ) + const { enqueueFocus } = useDetailFocus(500); + // Redux ์ƒํƒœ const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion); const groupInfos = useSelector((state) => state.product.groupInfo); @@ -833,52 +840,81 @@ export default function ProductAllSection({ [] ); - // SHOP BY MOBILE ๋ฒ„ํŠผ์—์„œ arrow up ์‹œ: BUY NOW > COUPON > BACK ์ˆœ์œผ๋กœ ์ด๋™, ํฌ์ปค์Šค ์ดํƒˆ ๋ณด์ • + // SHOP BY MOBILE ๋ฒ„ํŠผ์—์„œ arrow up ์‹œ + // focusUpMap์„ ์‚ฌ์šฉํ•ด์„œ ์œ„์ชฝ ๋ฒ„ํŠผ์œผ๋กœ ์ด๋™ + // focusUpMap์ด ์—†์œผ๋ฉด (์ฒซ ํ–‰) BackBtn์œผ๋กœ ์ด๋™ const handleSpotlightUpToBackButton = useCallback( (e) => { e.stopPropagation(); + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP ๊ฐ์ง€`); - const tryFocusUp = () => { - if (shopByMobileUpTarget && Spotlight.focus(shopByMobileUpTarget)) { - return shopByMobileUpTarget; + // focusUpMap์—์„œ ShopByMobile์˜ ์œ„์ชฝ ๋ฒ„ํŠผ์„ ์ฐพ์Œ + const targetId = focusUpMap[shopByMobileId]; + + if (targetId) { + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP ํƒ€๊นƒ: ${targetId}`); + if (targetId === 'detail-buy-now-button') { + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP: BuyNow ๋ฒ„ํŠผ ์žˆ์Œ`); + } else if (targetId === 'detail-add-to-cart-button') { + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP: AddToCart ๋ฒ„ํŠผ ์žˆ์Œ`); + } else if (targetId === 'detail-coupon-button') { + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP: Coupon ๋ฒ„ํŠผ ์žˆ์Œ`); } - return 'spotlightId_backBtn'; - }; - - const targetId = tryFocusUp(); - - // ํฌ์ปค์Šค๊ฐ€ ๋ฐ”๋กœ ๋น ์ง€๋Š” ์ผ€์ด์Šค ๋ณด์ • - setTimeout(() => { - const current = Spotlight.getCurrent(); - const currentId = current?.dataset?.spotlightId; - if (!current || currentId !== targetId) { - tryFocusUp(); + // ์ฆ‰์‹œ ํฌ์ปค์Šค ์‹œ๋„ + if (!Spotlight.focus(targetId)) { + enqueueFocus(targetId); } - }, 0); + } else { + // focusUpMap์— ์—†์œผ๋ฉด (์ฒซ ํ–‰์ด๊ฑฐ๋‚˜ ์œ„์ชฝ์ด ์—†์Œ) BackBtn์œผ๋กœ + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow UP: ์œ„์ชฝ ๋ฒ„ํŠผ ์—†์Œ, BackBtn์œผ๋กœ ์ด๋™`); + if (!Spotlight.focus('spotlightId_backBtn')) { + enqueueFocus('spotlightId_backBtn'); + } + } }, - [shopByMobileUpTarget] + [focusUpMap, shopByMobileId, enqueueFocus] ); - // BUY NOW, ADD TO CART ๋ฒ„ํŠผ์—์„œ arrow up ์‹œ: ํ•ญ์ƒ ํ—ค๋” ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์œผ๋กœ + // BUY NOW, ADD TO CART ๋ฒ„ํŠผ์—์„œ arrow up ์‹œ + // Coupon ๋ฒ„ํŠผ์ด ์žˆ์œผ๋ฉด Coupon์œผ๋กœ, ์—†์œผ๋ฉด Back์œผ๋กœ ์ด๋™ + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ๋ณ€๊ฒฝ const handleSpotlightUpFromBuyButtons = useCallback( (e) => { e.stopPropagation(); + + let targetId; if (promotions && promotions.length > 0) { - Spotlight.focus('detail-coupon-button'); + // ์ฟ ํฐ์ด ์—ฌ๋Ÿฌ ๊ฐœ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ฟ ํฐ ๋ฒ„ํŠผ์œผ๋กœ ํฌ์ปค์Šค + targetId = `detail-coupon-button-0`; + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow/AddToCart - Arrow UP ๊ฐ์ง€ (Coupon ์žˆ์Œ)`); + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow/AddToCart - Arrow UP ํƒ€๊นƒ ๊ฒฐ์ •: ${targetId}`); } else { - // ์ฟ ํฐ ๋ฒ„ํŠผ์ด ์—†์„ ๋•Œ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์œผ๋กœ ํฌ์ปค์Šค ์ด๋™ (ํŠน์ • Chrome ๋ฒ„์ „์—์„œ์˜ ํฌ์ปค์Šค ๋ฌธ์ œ ํ•ด๊ฒฐ) - setTimeout(() => { - Spotlight.focus('spotlightId_backBtn'); - }, 0); + targetId = 'spotlightId_backBtn'; + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow/AddToCart - Arrow UP ๊ฐ์ง€ (Coupon ์—†์Œ)`); + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow/AddToCart - Arrow UP ํƒ€๊นƒ ๊ฒฐ์ •: Back Btn`); + } + + // ์ฆ‰์‹œ ํฌ์ปค์Šค ์‹œ๋„, ์‹คํŒจ ์‹œ ๋ณด์ • ํ + if (!Spotlight.focus(targetId)) { + enqueueFocus(targetId); } }, - [promotions] + [promotions, enqueueFocus] ); - const handleSpotlightUpFromCouponButtons = useCallback((e) => { - e.stopPropagation(); - Spotlight.focus('spotlightId_backBtn'); - }, []); + // Coupon ๋ฒ„ํŠผ์—์„œ arrow up ์‹œ: Container์˜ leaveFor ์„ค์ •์œผ๋กœ ์ฒ˜๋ฆฌ + // stopPropagation์„ ํ•˜์ง€ ์•Š์•„์„œ Container๊ฐ€ up ๋ฐฉํ–ฅ์œผ๋กœ ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ํ•จ + const handleSpotlightUpFromCouponButtons = useCallback( + (e) => { + e.preventDefault(); + console.log(`๐ŸŽซ [FocusDetail] Coupon - Arrow UP ๊ฐ์ง€`); + console.log(`๐ŸŽซ [FocusDetail] Coupon - Arrow UP ์ฒ˜๋ฆฌ: Container์˜ leaveFor(up) ์„ค์ • ์‚ฌ์šฉ`); + if (!Spotlight.focus('spotlightId_backBtn')) { + enqueueFocus('spotlightId_backBtn'); + } + }, + [enqueueFocus] + ); // SHOP BY MOBILE โ†‘ ํƒ€๊นƒ ๊ณ„์‚ฐ const shopByMobileUpTarget = useMemo(() => { @@ -893,11 +929,13 @@ export default function ProductAllSection({ ); // ๋ฒ„ํŠผ ์Šคํƒ(์œ„โ†’์•„๋ž˜) ๊ตฌ์„ฑ: ์‹ค์ œ ๋ Œ๋”๋ง ์ˆœ์„œ์— ๋งž์ถฐ ํ–‰(row) ๋‹จ์œ„๋กœ ์„ค์ • - const { focusDownMap, focusOrder, focusRows } = useMemo(() => { + const { focusDownMap, focusUpMap, focusOrder, focusRows } = useMemo(() => { const rows = []; if (promotions && promotions.length > 0) { - rows.push(['detail-coupon-button']); + // ์ฟ ํฐ์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ฐ๊ฐ ๊ณ ์œ  ID๋กœ ์ถ”๊ฐ€ + const couponButtonIds = promotions.map((_, idx) => `detail-coupon-button-${idx}`); + rows.push(couponButtonIds); } if (isBillingProductVisible) { @@ -925,10 +963,22 @@ export default function ProductAllSection({ return acc; }, {}); + // ์œ„ ๋ฐฉํ–ฅ: ๊ฐ ํ–‰์˜ ๋ชจ๋“  ์š”์†Œ๊ฐ€ ์ด์ „ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์š”์†Œ๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ๋งคํ•‘ + const upMap = rows.reduce((acc, row, idx) => { + if (idx > 0) { + // ์ด์ „ ํ–‰์ด ์žˆ์œผ๋ฉด ๊ทธ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์š”์†Œ๋กœ ๋งคํ•‘ + const prevRowLast = rows[idx - 1][rows[idx - 1].length - 1]; + row.forEach((id) => { + acc[id] = prevRowLast; + }); + } + return acc; + }, {}); + // order๋Š” ํ–‰์„ ํ‰ํƒ„ํ™”ํ•œ ์ˆœ์„œ const order = rows.flat(); - return { focusDownMap: downMap, focusOrder: order, focusRows: rows }; + return { focusDownMap: downMap, focusUpMap: upMap, focusOrder: order, focusRows: rows }; }, [isBillingProductVisible, panelInfo, isReviewDataComplete, promotions, shopByMobileId]); // ๊ณตํ†ต โ†“ ์ด๋™ ํ•ธ๋“ค๋Ÿฌ @@ -944,11 +994,12 @@ export default function ProductAllSection({ if (!moved) { const fallback = focusOrder.find((id) => id !== currentId); if (fallback) { - Spotlight.focus(fallback); + // Hook์„ ํ†ตํ•œ ํฌ์ปค์Šค ์ด๋™ ๋ณด์ • (0.5์ดˆ ํƒ€์ด๋จธ๋กœ ์žฌ์‹œ๋„) + enqueueFocus(fallback); } } }, - [focusDownMap, focusOrder] + [focusDownMap, focusOrder, enqueueFocus] ); const handleSpotlightDown = useCallback((e) => { @@ -956,24 +1007,107 @@ export default function ProductAllSection({ }, []); // COUPON์—์„œ ์•„๋ž˜๋กœ ์ด๋™ ์‹œ - const handleSpotlightDownFromCoupon = useMemo( - () => buildSpotlightDownHandler('detail-coupon-button'), - [buildSpotlightDownHandler] + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ๋ณ€๊ฒฝ + const handleSpotlightDownFromCoupon = useCallback( + (e, currentCouponId) => { + e.stopPropagation(); + e.preventDefault(); + console.log(`๐ŸŽซ [FocusDetail] Coupon - Arrow DOWN ๊ฐ์ง€ (ID: ${currentCouponId})`); + + const nextId = focusDownMap[currentCouponId]; + console.log(`๐ŸŽซ [FocusDetail] Coupon - Arrow DOWN ๋‹ค์Œ ํƒ€๊นƒ: ${nextId}`); + + if (nextId) { + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ + enqueueFocus(nextId); + } else { + // ๋‹ค์Œ ํ–‰์ด ์—†์œผ๋ฉด focusOrder์—์„œ ํ˜„์žฌ ์ฟ ํฐ ๋ฒ„ํŠผ์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๋ฒ„ํŠผ์„ ์ฐพ์Œ + const fallback = focusOrder.find((id) => !id.startsWith('detail-coupon-button')); + console.log(`๐ŸŽซ [FocusDetail] Coupon - Arrow DOWN focusDownMap์— ์—†์Œ, ํด๋ฐฑ: ${fallback}`); + if (fallback) { + enqueueFocus(fallback); + } + } + }, + [focusDownMap, focusOrder, enqueueFocus] ); // SHOP BY MOBILE์—์„œ ์•„๋ž˜๋กœ ์ด๋™ ์‹œ - const handleSpotlightDownFromShopByMobile = useMemo( - () => buildSpotlightDownHandler(shopByMobileId), - [buildSpotlightDownHandler, shopByMobileId] + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ๋ณ€๊ฒฝ + const handleSpotlightDownFromShopByMobile = useCallback( + (e) => { + e.stopPropagation(); + e.preventDefault(); + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow DOWN ๊ฐ์ง€`); + + const nextId = focusDownMap[shopByMobileId]; + console.log(`๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow DOWN ๋‹ค์Œ ํƒ€๊นƒ: ${nextId}`); + + if (nextId) { + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ + enqueueFocus(nextId); + } else { + const fallback = focusOrder.find((id) => id !== shopByMobileId); + console.log( + `๐Ÿ›ต [FocusDetail] ShopByMobile - Arrow DOWN focusDownMap์— ์—†์Œ, ํด๋ฐฑ: ${fallback}` + ); + if (fallback) { + enqueueFocus(fallback); + } + } + }, + [shopByMobileId, focusDownMap, focusOrder, enqueueFocus] ); - // BUY NOW / ADD TO CART์—์„œ ์•„๋ž˜๋กœ ์ด๋™ ์‹œ - const handleSpotlightDownFromBuyNow = useMemo( - () => buildSpotlightDownHandler('detail-buy-now-button'), - [buildSpotlightDownHandler] + // BUY NOW์—์„œ ์•„๋ž˜๋กœ ์ด๋™ ์‹œ + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ๋ณ€๊ฒฝ + const handleSpotlightDownFromBuyNow = useCallback( + (e) => { + e.stopPropagation(); + e.preventDefault(); + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow - Arrow DOWN ๊ฐ์ง€`); + + const nextId = focusDownMap['detail-buy-now-button']; + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow - Arrow DOWN ๋‹ค์Œ ํƒ€๊นƒ: ${nextId}`); + + if (nextId) { + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ + enqueueFocus(nextId); + } else { + const fallback = focusOrder.find((id) => id !== 'detail-buy-now-button'); + console.log(`๐Ÿ’ณ [FocusDetail] BuyNow - Arrow DOWN focusDownMap์— ์—†์Œ, ํด๋ฐฑ: ${fallback}`); + if (fallback) { + enqueueFocus(fallback); + } + } + }, + [focusDownMap, focusOrder, enqueueFocus] ); - const handleSpotlightDownFromAddToCart = useMemo( - () => buildSpotlightDownHandler('detail-add-to-cart-button'), - [buildSpotlightDownHandler] + + // ADD TO CART์—์„œ ์•„๋ž˜๋กœ ์ด๋™ ์‹œ + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋„๋ก ๋ณ€๊ฒฝ + const handleSpotlightDownFromAddToCart = useCallback( + (e) => { + e.stopPropagation(); + e.preventDefault(); + console.log(`๐Ÿ’ณ [FocusDetail] AddToCart - Arrow DOWN ๊ฐ์ง€`); + + const nextId = focusDownMap['detail-add-to-cart-button']; + console.log(`๐Ÿ’ณ [FocusDetail] AddToCart - Arrow DOWN ๋‹ค์Œ ํƒ€๊นƒ: ${nextId}`); + + if (nextId) { + // ํ•ญ์ƒ Hook์„ ํ†ตํ•ด ํฌ์ปค์Šค๋ฅผ ์ด๋™ + enqueueFocus(nextId); + } else { + const fallback = focusOrder.find((id) => id !== 'detail-add-to-cart-button'); + console.log( + `๐Ÿ’ณ [FocusDetail] AddToCart - Arrow DOWN focusDownMap์— ์—†์Œ, ํด๋ฐฑ: ${fallback}` + ); + if (fallback) { + enqueueFocus(fallback); + } + } + }, + [focusDownMap, focusOrder, enqueueFocus] ); const onFavoriteFlagChanged = useCallback( @@ -1276,6 +1410,8 @@ export default function ProductAllSection({ scrollExpandTimerRef.current = null; } + // ๐Ÿ”ฝ useDetailFocus ํฌ์ปค์Šค ํƒ€์ด๋จธ๋Š” Hook์˜ useEffect cleanup์—์„œ ์ž๋™์œผ๋กœ ์ •๋ฆฌ๋จ + console.log('[ProductAllSection] cleanup ์™„๋ฃŒ on unmount'); }; }, []); @@ -1337,6 +1473,7 @@ export default function ProductAllSection({ {userNumber && promotions.map((promotion, idx) => { + const couponButtonId = `detail-coupon-button-${idx}`; return (
@@ -1346,14 +1483,14 @@ export default function ProductAllSection({
{ handleCouponClick(idx, promotion); }} onSpotlightUp={handleSpotlightUpFromCouponButtons} - onSpotlightDown={handleSpotlightDownFromCoupon} - data-spotlight-next-down={focusDownMap['detail-coupon-button']} + onSpotlightDown={(e) => handleSpotlightDownFromCoupon(e, couponButtonId)} + data-spotlight-next-down={focusDownMap[couponButtonId]} size="detail_very_small" >
COUPON