[251217] fix: LiveChannelContents TScrollerLiveContents 추가
🕐 커밋 시간: 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: 코드 최적화로 성능 개선 기대
This commit is contained in:
@@ -5,9 +5,8 @@ import { useDispatch } from 'react-redux';
|
|||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
|
|
||||||
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
import { sendLogTotalRecommend } from '../../../../actions/logActions';
|
||||||
// <<<<<<< HEAD
|
|
||||||
import { updatePanel } from '../../../../actions/panelActions';
|
import { updatePanel } from '../../../../actions/panelActions';
|
||||||
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
|
import TScrollerLiveChannel from './TScrollerLiveChannel';
|
||||||
import {
|
import {
|
||||||
LOG_CONTEXT_NAME,
|
LOG_CONTEXT_NAME,
|
||||||
LOG_MENU,
|
LOG_MENU,
|
||||||
@@ -20,22 +19,6 @@ import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContent
|
|||||||
import css from './LiveChannelContents.module.less';
|
import css from './LiveChannelContents.module.less';
|
||||||
import cssV2 from './LiveChannelContents.v2.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({
|
export default function LiveChannelContents({
|
||||||
liveInfos,
|
liveInfos,
|
||||||
currentTime,
|
currentTime,
|
||||||
@@ -222,7 +205,7 @@ export default function LiveChannelContents({
|
|||||||
<>
|
<>
|
||||||
<div className={containerClass}>
|
<div className={containerClass}>
|
||||||
{liveInfos && liveInfos.length > 0 ? (
|
{liveInfos && liveInfos.length > 0 ? (
|
||||||
<TVirtualGridList
|
<TScrollerLiveChannel
|
||||||
cbScrollTo={handleScrollTo}
|
cbScrollTo={handleScrollTo}
|
||||||
dataSize={liveInfos.length}
|
dataSize={liveInfos.length}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
@@ -230,7 +213,6 @@ export default function LiveChannelContents({
|
|||||||
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
|
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
|
||||||
itemHeight={version === 2 ? 155 : 236}
|
itemHeight={version === 2 ? 155 : 236}
|
||||||
spacing={version === 2 ? 30 : 12}
|
spacing={version === 2 ? 30 : 12}
|
||||||
noScrollByWheel={false}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ListEmptyContents tabIndex={tabIndex} />
|
<ListEmptyContents tabIndex={tabIndex} />
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={classNames(css.tScrollerLiveChannelContainer, className)}
|
||||||
|
style={containerStyle}
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
>
|
||||||
|
<div className={css.itemsWrapper} style={itemsWrapperStyle}>
|
||||||
|
{Array.from({ length: dataSize }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`item-${index}`}
|
||||||
|
ref={(el) => 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 })}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user