From 13e32298a7cb33e25e0f5801e969a506671d5627 Mon Sep 17 00:00:00 2001 From: optrader Date: Wed, 17 Dec 2025 15:39:40 +0900 Subject: [PATCH] =?UTF-8?q?[251217]=20fix:=20LiveChannelContents=20TScroll?= =?UTF-8?q?erLiveContents=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ• 컀밋 μ‹œκ°„: 2025. 12. 17. 15:39:39 πŸ“Š λ³€κ²½ 톡계: β€’ 총 파일: 3개 β€’ μΆ”κ°€: +2쀄 β€’ μ‚­μ œ: -20쀄 πŸ“ μΆ”κ°€λœ 파일: + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less πŸ“ μˆ˜μ •λœ 파일: ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx πŸ”§ μ£Όμš” λ³€κ²½ λ‚΄μš©: β€’ μ½”λ“œ 정리 및 μ΅œμ ν™” Performance: μ½”λ“œ μ΅œμ ν™”λ‘œ μ„±λŠ₯ κ°œμ„  κΈ°λŒ€ --- .../TabContents/LiveChannelContents.jsx | 22 +- .../TabContents/TScrollerLiveChannel.jsx | 250 ++++++++++++++++++ .../TScrollerLiveChannel.module.less | 41 +++ 3 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx index 5168f882..f3234e0f 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx @@ -5,9 +5,8 @@ import { useDispatch } from 'react-redux'; import Spotlight from '@enact/spotlight'; import { sendLogTotalRecommend } from '../../../../actions/logActions'; -// <<<<<<< HEAD import { updatePanel } from '../../../../actions/panelActions'; -import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; +import TScrollerLiveChannel from './TScrollerLiveChannel'; import { LOG_CONTEXT_NAME, LOG_MENU, @@ -20,22 +19,6 @@ import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContent import css from './LiveChannelContents.module.less'; import cssV2 from './LiveChannelContents.v2.module.less'; -// ======= -// import { updatePanel } from "../../../../actions/panelActions"; -// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList"; -// import { -// LOG_CONTEXT_NAME, -// LOG_MENU, -// LOG_MESSAGE_ID, -// panel_names, -// } from "../../../../utils/Config"; -// import { $L } from "../../../../utils/helperMethods"; -// import PlayerItemCard, { TYPES } from "../../PlayerItemCard/PlayerItemCard"; -// import ListEmptyContents from "../TabContents/ListEmptyContents/ListEmptyContents"; -// import css from "./LiveChannelContents.module.less"; -// import { sendLogTotalRecommend } from "../../../../actions/logActions"; -// >>>>>>> gitlab/develop - export default function LiveChannelContents({ liveInfos, currentTime, @@ -222,7 +205,7 @@ export default function LiveChannelContents({ <>
{liveInfos && liveInfos.length > 0 ? ( - ) : ( diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx new file mode 100644 index 00000000..fc142a58 --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx @@ -0,0 +1,250 @@ +import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import classNames from 'classnames'; +import { scaleH, scaleW } from '../../../../utils/helperMethods'; +import css from './TScrollerLiveChannel.module.less'; + +/** + * TScrollerLiveChannel - Live Channel용 κ°„λ‹¨ν•œ 슀크둀 μ»΄ν¬λ„ŒνŠΈ + * + * TVirtualGridList의 가상화 λŒ€μ‹  λͺ¨λ“  μ•„μ΄ν…œμ„ DOM에 λ Œλ”λ§ + * 20개 미만의 μ•„μ΄ν…œμ— μ΅œμ ν™”λ˜μ–΄ 있음 + * + * @param {number} dataSize - μ•„μ΄ν…œ 개수 + * @param {string} direction - 'horizontal' λ˜λŠ” 'vertical' + * @param {function} renderItem - μ•„μ΄ν…œ λ Œλ”λ§ ν•¨μˆ˜ ({ index }) + * @param {number} itemWidth - μ•„μ΄ν…œ λ„ˆλΉ„ + * @param {number} itemHeight - μ•„μ΄ν…œ 높이 + * @param {number} spacing - μ•„μ΄ν…œ κ°„ 간격 + * @param {function} cbScrollTo - 슀크둀 ν•¨μˆ˜λ₯Ό 받을 콜백 + * @param {string} className - μΆ”κ°€ CSS 클래슀 + * @param {string} spotlightId - Spotlight 포컀슀 ID prefix + */ +export default function TScrollerLiveChannel({ + dataSize, + direction = 'horizontal', + renderItem, + itemWidth, + itemHeight, + spacing, + cbScrollTo, + className, + spotlightId, +}) { + const scrollContainerRef = useRef(null); + const itemsRef = useRef([]); + + // 슀크둀 μ»¨ν…Œμ΄λ„ˆ 크기 계산 + const containerStyle = useMemo(() => { + if (direction === 'horizontal') { + return { + display: 'flex', + overflowX: 'auto', + overflowY: 'hidden', + width: '100%', + height: scaleH(itemHeight), + alignItems: 'center', + }; + } else { + return { + display: 'flex', + flexDirection: 'column', + overflowY: 'auto', + overflowX: 'hidden', + width: '100%', + }; + } + }, [direction, itemHeight]); + + // μ•„μ΄ν…œ 래퍼 μŠ€νƒ€μΌ 계산 + const itemsWrapperStyle = useMemo(() => { + if (direction === 'horizontal') { + return { + display: 'flex', + flexDirection: 'row', + gap: scaleW(spacing), + padding: `0 ${scaleW(spacing)}px`, + alignItems: 'center', + }; + } else { + return { + display: 'flex', + flexDirection: 'column', + gap: scaleH(spacing), + padding: `${scaleH(spacing)}px 0`, + }; + } + }, [direction, spacing]); + + // 슀크둀 ν•¨μˆ˜ 생성 + const scrollToIndex = useCallback( + (index, options = {}) => { + const container = scrollContainerRef.current; + if (!container || !itemsRef.current[index]) return; + + const item = itemsRef.current[index]; + const { animate = true } = options; + + if (direction === 'horizontal') { + // μˆ˜ν‰ 슀크둀: ν˜„μž¬ μ•„μ΄ν…œ + λ‹€μŒ μ•„μ΄ν…œκΉŒμ§€ 보이도둝 + const itemLeft = item.offsetLeft; + const itemWidth = item.offsetWidth; + const containerWidth = container.clientWidth; + + // λ‹€μŒ μ•„μ΄ν…œλ„ 일뢀 보일 수 μžˆλ„λ‘ 슀크둀 + // ν˜„μž¬ μ•„μ΄ν…œ + λ‹€μŒ μ•„μ΄ν…œμ˜ 일뢀가 λ³΄μ΄λŠ” μœ„μΉ˜λ‘œ 슀크둀 + const nextItem = itemsRef.current[index + 1]; + let scrollLeft = itemLeft - scaleW(spacing); + + if (nextItem) { + // λ‹€μŒ μ•„μ΄ν…œμ˜ μ™Όμͺ½ 끝이 μ»¨ν…Œμ΄λ„ˆμ˜ 였λ₯Έμͺ½ 끝과 같은 μœ„μΉ˜κ°€ λ˜λ„λ‘ + const nextItemLeft = nextItem.offsetLeft; + const nextItemWidth = nextItem.offsetWidth; + const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing); + + // 두 κ°€μ§€ μ€‘μ—μ„œ ν˜„μž¬ μ•„μ΄ν…œμ΄ 더 잘 λ³΄μ΄λŠ” μͺ½ 선택 + scrollLeft = Math.min(scrollLeft, targetScrollLeft); + } + + // 음수 슀크둀 λ°©μ§€ + scrollLeft = Math.max(0, scrollLeft); + + if (animate) { + container.scrollTo({ + left: scrollLeft, + behavior: 'smooth', + }); + } else { + container.scrollLeft = scrollLeft; + } + } else { + // 수직 슀크둀: ν˜„μž¬ μ•„μ΄ν…œ + λ‹€μŒ μ•„μ΄ν…œκΉŒμ§€ 보이도둝 + const itemTop = item.offsetTop; + const itemHeight = item.offsetHeight; + const containerHeight = container.clientHeight; + + // λ‹€μŒ μ•„μ΄ν…œλ„ 일뢀 보일 수 μžˆλ„λ‘ 슀크둀 + const nextItem = itemsRef.current[index + 1]; + let scrollTop = itemTop - scaleH(spacing); + + if (nextItem) { + // λ‹€μŒ μ•„μ΄ν…œμ˜ μœ„μͺ½ 끝이 μ»¨ν…Œμ΄λ„ˆμ˜ μ•„λž˜μͺ½ 끝과 같은 μœ„μΉ˜κ°€ λ˜λ„λ‘ + const nextItemTop = nextItem.offsetTop; + const nextItemHeight = nextItem.offsetHeight; + const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing); + + // 두 κ°€μ§€ μ€‘μ—μ„œ ν˜„μž¬ μ•„μ΄ν…œμ΄ 더 잘 λ³΄μ΄λŠ” μͺ½ 선택 + scrollTop = Math.min(scrollTop, targetScrollTop); + } + + // 음수 슀크둀 λ°©μ§€ + scrollTop = Math.max(0, scrollTop); + + if (animate) { + container.scrollTo({ + top: scrollTop, + behavior: 'smooth', + }); + } else { + container.scrollTop = scrollTop; + } + } + }, + [direction, spacing] + ); + + // TVirtualGridList와 ν˜Έν™˜λ˜λŠ” 콜백 μΈν„°νŽ˜μ΄μŠ€ 제곡 + useEffect(() => { + if (cbScrollTo) { + cbScrollTo((options) => { + const { index, animate = true, focus = true } = options; + if (typeof index === 'number' && index >= 0 && index < dataSize) { + scrollToIndex(index, { animate }); + } + }); + } + }, [cbScrollTo, scrollToIndex, dataSize]); + + // μ•„μ΄ν…œ ref ν• λ‹Ή ν•¨μˆ˜ + const setItemRef = useCallback((el, index) => { + if (el) { + itemsRef.current[index] = el; + } else { + delete itemsRef.current[index]; + } + }, []); + + // 포컀슀된 μ•„μ΄ν…œμ„ 화면에 μ™„μ „νžˆ 보이도둝 슀크둀 + const handleItemFocus = useCallback( + (index) => { + const container = scrollContainerRef.current; + const item = itemsRef.current[index]; + + if (!container || !item) return; + + if (direction === 'horizontal') { + const itemLeft = item.offsetLeft; + const itemWidth = item.offsetWidth; + const containerWidth = container.clientWidth; + const containerScrollLeft = container.scrollLeft; + + // μ•„μ΄ν…œμ΄ μ™„μ „νžˆ λ³΄μ΄λŠ”μ§€ 확인 + const itemRight = itemLeft + itemWidth; + const containerRight = containerScrollLeft + containerWidth; + + // μ•„μ΄ν…œμ΄ μ™Όμͺ½μœΌλ‘œ 밖에 λ‚˜κ°€ 있으면 μ™Όμͺ½ 끝에 맞좀 + if (itemLeft < containerScrollLeft) { + container.scrollLeft = itemLeft - scaleW(spacing); + } + // μ•„μ΄ν…œμ΄ 였λ₯Έμͺ½μœΌλ‘œ 밖에 λ‚˜κ°€ 있으면 였λ₯Έμͺ½ 끝에 맞좀 + else if (itemRight > containerRight) { + container.scrollLeft = itemRight - containerWidth + scaleW(spacing); + } + } else { + const itemTop = item.offsetTop; + const itemHeight = item.offsetHeight; + const containerHeight = container.clientHeight; + const containerScrollTop = container.scrollTop; + + // μ•„μ΄ν…œμ΄ μ™„μ „νžˆ λ³΄μ΄λŠ”μ§€ 확인 + const itemBottom = itemTop + itemHeight; + const containerBottom = containerScrollTop + containerHeight; + + // μ•„μ΄ν…œμ΄ μœ„λ‘œ 밖에 λ‚˜κ°€ 있으면 μœ„μͺ½ 끝에 맞좀 + if (itemTop < containerScrollTop) { + container.scrollTop = itemTop - scaleH(spacing); + } + // μ•„μ΄ν…œμ΄ μ•„λž˜λ‘œ 밖에 λ‚˜κ°€ 있으면 μ•„λž˜μͺ½ 끝에 맞좀 + else if (itemBottom > containerBottom) { + container.scrollTop = itemBottom - containerHeight + scaleH(spacing); + } + } + }, + [direction, spacing] + ); + + return ( +
+
+ {Array.from({ length: dataSize }).map((_, index) => ( +
setItemRef(el, index)} + className={css.item} + style={{ + width: direction === 'horizontal' ? scaleW(itemWidth) : 'auto', + height: direction === 'horizontal' ? 'auto' : scaleH(itemHeight), + flexShrink: 0, + }} + onFocus={() => handleItemFocus(index)} + > + {renderItem({ index })} +
+ ))} +
+
+ ); +} diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less new file mode 100644 index 00000000..0da7f769 --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less @@ -0,0 +1,41 @@ +.tScrollerLiveChannelContainer { + position: relative; + width: 100%; + height: 100%; + + // μŠ€ν¬λ‘€λ°” μŠ€νƒ€μΌ + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; + + &:hover { + background: rgba(255, 255, 255, 0.5); + } + } +} + +.itemsWrapper { + width: 100%; + height: 100%; +} + +.item { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + + // 포컀슀 μƒνƒœ 처리 + &:focus-within { + outline: none; + } +}