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;
+ }
+}