6 Commits

Author SHA1 Message Date
daac18afa8 [251217] fix: LiveChannelContents PageUp/Down시 첫번째로 배너위치
🕐 커밋 시간: 2025. 12. 17. 20:11:18

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +46줄
  • 삭제: -33줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-17 20:11:19 +09:00
adfede9b44 [251217] fix: LiveChannelContents.jsx 아이템포커스 테두리 수정
🕐 커밋 시간: 2025. 12. 17. 20:02:01

📊 변경 통계:
  • 총 파일: 2개

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/style/utils.module.less
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerItemCard/PlayerItemCard.v2.module.less

🔧 주요 변경 내용:
  • 공통 유틸리티 함수 최적화
2025-12-17 20:02:02 +09:00
1fae88878f [251217] fix: MediaPanel.v3.jsx 비디오복원 방어로직 추가
🕐 커밋 시간: 2025. 12. 17. 19:51:27

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +117줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-12-17 19:51:27 +09:00
4ecb03002f [251217] fix: VideoPlayer.v3.jsx times 전체화면에만 표시
🕐 커밋 시간: 2025. 12. 17. 18:19:37

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화
2025-12-17 18:19:37 +09:00
27e1e3bb6a [251217] fix: VideoPlayer.v3.jsx slider , times layout
🕐 커밋 시간: 2025. 12. 17. 18:16:49

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +3줄
  • 삭제: -86줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-17 18:16:49 +09:00
b9d52d452c [251217] fix: VideoPlayer.v3.jsx Spinner제거
🕐 커밋 시간: 2025. 12. 17. 18:04:43

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +18줄
  • 삭제: -6줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
2025-12-17 18:04:44 +09:00
8 changed files with 174 additions and 116 deletions

View File

@@ -2467,9 +2467,9 @@ const VideoPlayerBase = class extends React.Component {
</p> </p>
} }
<div className={css.loaderWrap}> {/* <div className={css.loaderWrap}>
<Loader /> <Loader />
</div> </div> */}
</> </>
) : null} ) : null}
{this.state.mediaControlsVisible && ( {this.state.mediaControlsVisible && (
@@ -2534,7 +2534,7 @@ const VideoPlayerBase = class extends React.Component {
videoVerticalVisible && css.videoVertical videoVerticalVisible && css.videoVertical
)} )}
> >
{this.state.mediaSliderVisible && type ? ( {this.state.mediaSliderVisible && type && !panelInfo.modal ? (
<Times <Times
noCurrentTime noCurrentTime
total={type === 'LIVE' ? liveTotalTime : this.state.duration} total={type === 'LIVE' ? liveTotalTime : this.state.duration}
@@ -2542,7 +2542,7 @@ const VideoPlayerBase = class extends React.Component {
type={type} type={type}
/> />
) : null} ) : null}
{this.state.mediaSliderVisible && type ? ( {this.state.mediaSliderVisible && type && !panelInfo.modal ? (
<Times <Times
noTotalTime noTotalTime
current={type === 'LIVE' ? currentLiveTimeSeconds : this.state.currentTime} current={type === 'LIVE' ? currentLiveTimeSeconds : this.state.currentTime}

View File

@@ -718,7 +718,7 @@
} }
.sliderContainer { .sliderContainer {
// display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
margin-left: 60px; margin-left: 60px;
@@ -727,6 +727,10 @@
bottom: -20px; bottom: -20px;
> *:first-child { > *:first-child {
text-align: right; text-align: right;
margin-right: 12px;
}
> *:nth-child(2) {
margin-right: 12px;
} }
.enact-locale-rtl({ .enact-locale-rtl({
@@ -787,79 +791,4 @@
}); });
} }
// ========== MediaPlayer.v2 Controls ==========
.controlsContainer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px 40px 30px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, transparent 100%);
z-index: 10;
display: flex;
flex-direction: column;
gap: 16px;
}
.sliderContainer {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.times {
min-width: 80px;
text-align: center;
}
.controlsButtons {
display: flex;
gap: 20px;
justify-content: center;
align-items: center;
}
.playPauseBtn {
width: 60px;
height: 60px;
font-size: 24px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.95);
}
}
.backBtn {
padding: 12px 24px;
font-size: 18px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.98);
}
}

View File

@@ -10,7 +10,6 @@
.position(@position: absolute, @top: 0, @left: 0, @right: 0, @bottom: 0); .position(@position: absolute, @top: 0, @left: 0, @right: 0, @bottom: 0);
z-index: 19; z-index: 19;
border: 4px solid @PRIMARY_COLOR_RED; border: 4px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 @boxShadow 0 rgba(0, 0, 0, 0.5);
border-radius: @borderRadius; border-radius: @borderRadius;
content: ""; content: "";
} }

View File

@@ -183,6 +183,57 @@ const ButtonStackContainer = SpotlightContainerDecorator(
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 저장/검증 함수
// PlayerPanel의 배너 위치 검증 패턴을 ProductAllSection에 적용
/**
* ProductAllSection의 스크롤 컨테이너 위치 수집 함수
* @returns {Object|null} 스크롤 컨테이너의 위치 정보 (top, left) 또는 null
*/
const collectProductScrollPosition = () => {
const scrollContainer = document.querySelector('[data-spotlight-id="main-content-scroller"]');
if (scrollContainer) {
const { top, left } = scrollContainer.getBoundingClientRect();
const position = {
top: Math.round(top),
left: Math.round(left)
};
console.log('[ProductAllSection] 스크롤 컨테이너 위치 수집:', position);
return position;
}
console.warn('[ProductAllSection] 스크롤 컨테이너를 찾을 수 없음');
return null;
};
/**
* ProductAllSection 스크롤 컨테이너 위치 검증 함수
* @param {Object} savedPosition - 저장된 초기 위치
* @param {Object} currentPosition - 현재 위치
* @returns {boolean} 위치가 일치하는지 여부
*/
const isProductScrollPositionValid = (savedPosition, currentPosition) => {
if (!savedPosition || !currentPosition) {
console.warn('[ProductAllSection] 저장된 위치 또는 현재 위치가 없음');
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - savedPosition.top) <= tolerance &&
Math.abs(currentPosition.left - savedPosition.left) <= tolerance;
console.log('[ProductAllSection] 스크롤 위치 검증:', {
expected: savedPosition,
current: currentPosition,
matching: isMatching,
tolerance
});
return isMatching;
};
const getProductData = curry( const getProductData = curry(
(productType, themeProductInfo, themeProducts, selectedIndex, productInfo) => (productType, themeProductInfo, themeProducts, selectedIndex, productInfo) =>
pipe( pipe(
@@ -304,6 +355,9 @@ export default function ProductAllSection({
// handleScrollToImages의 timeout을 추적하기 위한 ref // handleScrollToImages의 timeout을 추적하기 위한 ref
const scrollToImagesTimeoutRef = useRef(null); const scrollToImagesTimeoutRef = useRef(null);
// 🔽 [251217] 스크롤 컨테이너 초기 위치 저장 ref (VideoPlayer modal 전환 시 위치 검증용)
const scrollPositionOnMountRef = useRef(null);
// ProductAllSection 초기 로딩 시 Skeleton 표시를 위한 상태 // ProductAllSection 초기 로딩 시 Skeleton 표시를 위한 상태
const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -344,6 +398,19 @@ export default function ProductAllSection({
); );
}, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]); }, [selectedPatnrId, selectedPrdtId, userNumber, dispatch]);
// 🔽 [251217] ProductAllSection 마운트 시 스크롤 컨테이너 위치 저장
useEffect(() => {
// 초기 렌더링 후 스크롤 컨테이너의 위치를 저장 (1회만 실행)
// 이 위치는 나중에 VideoPlayer의 modal 전환 시 유효성 검증에 사용됨
const savedPosition = collectProductScrollPosition();
if (savedPosition) {
scrollPositionOnMountRef.current = savedPosition;
// 🔽 window 객체에도 저장하여 MediaPanel.v3.jsx에서 접근 가능하게 함
window.productScrollPositionOnMount = savedPosition;
console.log('[ProductAllSection] ✅ 초기 스크롤 위치 저장 완료:', savedPosition);
}
}, []); // 마운트 시 1회만 실행
useEffect(() => { useEffect(() => {
// 필수 값이 모두 있을 때만 호출 // 필수 값이 모두 있을 때만 호출
if (selectedPatnrId && selectedPrdtId) { if (selectedPatnrId && selectedPrdtId) {

View File

@@ -93,6 +93,36 @@ const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => {
} }
}; };
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 수집 함수
const collectProductScrollPosition = () => {
const scrollContainer = document.querySelector('[data-spotlight-id="main-content-scroller"]');
if (scrollContainer) {
const { top, left } = scrollContainer.getBoundingClientRect();
const position = {
top: Math.round(top),
left: Math.round(left)
};
return position;
}
return null;
};
// 🔽 [251217] ProductAllSection 스크롤 컨테이너 위치 검증 함수
const isProductScrollPositionValid = (savedPosition, currentPosition) => {
if (!savedPosition || !currentPosition) {
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - savedPosition.top) <= tolerance &&
Math.abs(currentPosition.left - savedPosition.left) <= tolerance;
return isMatching;
};
const getLogTpNo = (type, nowMenu) => { const getLogTpNo = (type, nowMenu) => {
if (type === 'LIVE') { if (type === 'LIVE') {
switch (nowMenu) { switch (nowMenu) {
@@ -1107,6 +1137,26 @@ const MediaPanel = React.forwardRef(
}, [dispatch]); }, [dispatch]);
const enterFullscreen = useCallback(() => { const enterFullscreen = useCallback(() => {
// 🔽 [251217] ProductAllSection 스크롤 위치 검증
// DetailPanel에서 ProductAllSection의 초기 위치와 현재 위치를 비교하여 검증
const savedPosition = window.productScrollPositionOnMount;
const currentPosition = collectProductScrollPosition();
if (savedPosition && currentPosition) {
const isValid = isProductScrollPositionValid(savedPosition, currentPosition);
if (!isValid) {
console.warn('[MediaPanel] ⚠️ ProductAllSection 스크롤 위치 검증 실패 - 전체화면 재생 가능하지만 경고 표시', {
savedPosition,
currentPosition,
});
// 사용자 피드백: 위치가 일치하지 않음을 콘솔에 기록 (비디오는 계속 재생)
} else {
console.log('[MediaPanel] ✅ ProductAllSection 스크롤 위치 검증 성공');
}
} else {
console.warn('[MediaPanel] ⚠️ ProductAllSection 위치 정보 없음 - 검증 스킵');
}
isTransitioningToFullscreen.current = true; isTransitioningToFullscreen.current = true;
dispatch(switchMediaToFullscreen()); dispatch(switchMediaToFullscreen());
}, [dispatch]); }, [dispatch]);

View File

@@ -167,8 +167,8 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 4px solid @PRIMARY_COLOR_RED;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none; pointer-events: none;
} }
} }
@@ -298,8 +298,8 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 4px solid @PRIMARY_COLOR_RED;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 0 22px @PRIMARY_COLOR_RED;
pointer-events: none; pointer-events: none;
} }
} }

View File

@@ -75,7 +75,7 @@ export default function LiveChannelContents({
// currentVideoShowId와 일치하는 배너의 인덱스 찾기 // currentVideoShowId와 일치하는 배너의 인덱스 찾기
const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId); const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId);
if (index !== -1) { if (index !== -1) {
scrollToRef.current({ index, animate: true, focus: false }); scrollToRef.current({ index, animate: true, focus: false, alignToStart: true });
} }
} }
}, [currentVideoShowId, liveInfos]); }, [currentVideoShowId, liveInfos]);

View File

@@ -82,28 +82,34 @@ export default function TScrollerLiveChannel({
if (!container || !itemsRef.current[index]) return; if (!container || !itemsRef.current[index]) return;
const item = itemsRef.current[index]; const item = itemsRef.current[index];
const { animate = true } = options; const { animate = true, alignToStart = false } = options;
if (direction === 'horizontal') { if (direction === 'horizontal') {
// 수평 스크롤: 현재 아이템 + 다음 아이템까지 보이도록 // 수평 스크롤: 스크롤 가능 여부 판단
const isScrollable = container.scrollWidth > container.clientWidth;
let scrollLeft = 0;
if (alignToStart && isScrollable) {
// 첫 번째 위치로 스크롤
scrollLeft = item.offsetLeft - scaleW(spacing);
} else if (!alignToStart) {
// 기존 로직: 현재 아이템 + 다음 아이템까지 보이도록
const itemLeft = item.offsetLeft; const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth; const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth; const containerWidth = container.clientWidth;
// 다음 아이템도 일부 보일 수 있도록 스크롤
// 현재 아이템 + 다음 아이템의 일부가 보이는 위치로 스크롤
const nextItem = itemsRef.current[index + 1]; const nextItem = itemsRef.current[index + 1];
let scrollLeft = itemLeft - scaleW(spacing); scrollLeft = itemLeft - scaleW(spacing);
if (nextItem) { if (nextItem) {
// 다음 아이템의 왼쪽 끝이 컨테이너의 오른쪽 끝과 같은 위치가 되도록
const nextItemLeft = nextItem.offsetLeft; const nextItemLeft = nextItem.offsetLeft;
const nextItemWidth = nextItem.offsetWidth; const nextItemWidth = nextItem.offsetWidth;
const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing); const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollLeft = Math.min(scrollLeft, targetScrollLeft); scrollLeft = Math.min(scrollLeft, targetScrollLeft);
} }
}
// 음수 스크롤 방지 // 음수 스크롤 방지
scrollLeft = Math.max(0, scrollLeft); scrollLeft = Math.max(0, scrollLeft);
@@ -117,24 +123,31 @@ export default function TScrollerLiveChannel({
container.scrollLeft = scrollLeft; container.scrollLeft = scrollLeft;
} }
} else { } else {
// 수직 스크롤: 현재 아이템 + 다음 아이템까지 보이도록 // 수직 스크롤: 스크롤 가능 여부 판단
const isScrollable = container.scrollHeight > container.clientHeight;
let scrollTop = 0;
if (alignToStart && isScrollable) {
// 첫 번째 위치로 스크롤
scrollTop = item.offsetTop - scaleH(spacing);
} else if (!alignToStart) {
// 기존 로직: 현재 아이템 + 다음 아이템까지 보이도록
const itemTop = item.offsetTop; const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight; const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight; const containerHeight = container.clientHeight;
// 다음 아이템도 일부 보일 수 있도록 스크롤
const nextItem = itemsRef.current[index + 1]; const nextItem = itemsRef.current[index + 1];
let scrollTop = itemTop - scaleH(spacing); scrollTop = itemTop - scaleH(spacing);
if (nextItem) { if (nextItem) {
// 다음 아이템의 위쪽 끝이 컨테이너의 아래쪽 끝과 같은 위치가 되도록
const nextItemTop = nextItem.offsetTop; const nextItemTop = nextItem.offsetTop;
const nextItemHeight = nextItem.offsetHeight; const nextItemHeight = nextItem.offsetHeight;
const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing); const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollTop = Math.min(scrollTop, targetScrollTop); scrollTop = Math.min(scrollTop, targetScrollTop);
} }
}
// 음수 스크롤 방지 // 음수 스크롤 방지
scrollTop = Math.max(0, scrollTop); scrollTop = Math.max(0, scrollTop);
@@ -156,9 +169,9 @@ export default function TScrollerLiveChannel({
useEffect(() => { useEffect(() => {
if (cbScrollTo) { if (cbScrollTo) {
cbScrollTo((options) => { cbScrollTo((options) => {
const { index, animate = true, focus = true } = options; const { index, animate = true, focus = true, alignToStart = false } = options;
if (typeof index === 'number' && index >= 0 && index < dataSize) { if (typeof index === 'number' && index >= 0 && index < dataSize) {
scrollToIndex(index, { animate }); scrollToIndex(index, { animate, alignToStart });
} }
}); });
} }