[251012] fix: ProductAllSection 렌더링 최적화-1
🕐 커밋 시간: 2025. 10. 12. 08:02:13 📊 변경 통계: • 총 파일: 3개 • 추가: +48줄 • 삭제: -313줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx 🗑️ 삭제된 파일: - com.twin.app.shoptime/src/views/MediaPanel/README.md 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: SpotlightContainerDecorator(), extractProductMeta() 📄 com.twin.app.shoptime/src/views/MediaPanel/README.md (md파일): ❌ Deleted: dispatch(), useDispatch(), useRef() 🔧 주요 변경 내용: • 개발 문서 및 가이드 개선 Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
// import { throttle } from 'lodash';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -93,7 +93,6 @@ const HorizontalContainer = SpotlightContainerDecorator(
|
||||
'div'
|
||||
);
|
||||
|
||||
// FP: Pure function to determine product data based on typeP
|
||||
const getProductData = curry((productType, themeProductInfo, productInfo) =>
|
||||
pipe(
|
||||
when(
|
||||
@@ -105,8 +104,6 @@ const getProductData = curry((productType, themeProductInfo, productInfo) =>
|
||||
)(productInfo)
|
||||
);
|
||||
|
||||
// FP: Pure function to derive favorite flag
|
||||
// 명확성을 위해 단순화된 버전으로 변경
|
||||
const deriveFavoriteFlag = curry((favoriteOverride, productData) => {
|
||||
// favoriteOverride가 null/undefined가 아니면 그 값을 사용
|
||||
if (isNotNil(favoriteOverride)) {
|
||||
@@ -116,7 +113,6 @@ const deriveFavoriteFlag = curry((favoriteOverride, productData) => {
|
||||
return pipe(get('favorYn'), defaultTo('N'))(productData);
|
||||
});
|
||||
|
||||
// FP: Pure function to extract review grade and order phone
|
||||
const extractProductMeta = (productInfo) => ({
|
||||
revwGrd: get('revwGrd', productInfo),
|
||||
orderPhnNo: get('orderPhnNo', productInfo),
|
||||
@@ -301,28 +297,27 @@ export default function ProductAllSection({
|
||||
}, []);
|
||||
|
||||
// 디버깅: 실제 이미지 및 동영상 데이터 확인
|
||||
useEffect(() => {
|
||||
console.log('[ProductId] ProductAllSection productData check:', {
|
||||
hasProductData: !!productData,
|
||||
productDataPrdtId: productData && productData.prdtId,
|
||||
imgUrls600: productData && productData.imgUrls600,
|
||||
imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
|
||||
? 'array'
|
||||
: typeof (productData && productData.imgUrls600),
|
||||
// 동영상 관련 정보 추가
|
||||
prdtMediaUrl: productData && productData.prdtMediaUrl,
|
||||
thumbnailUrl960: productData && productData.thumbnailUrl960,
|
||||
hasVideo: !!(productData && productData.prdtMediaUrl),
|
||||
renderItemsLength: renderItems.length,
|
||||
renderItems: renderItems,
|
||||
productData: productData,
|
||||
});
|
||||
}, [productData, renderItems]);
|
||||
// useEffect(() => {
|
||||
// console.log('[ProductId] ProductAllSection productData check:', {
|
||||
// hasProductData: !!productData,
|
||||
// productDataPrdtId: productData && productData.prdtId,
|
||||
// imgUrls600: productData && productData.imgUrls600,
|
||||
// imgUrls600Length: productData && productData.imgUrls600 && productData.imgUrls600.length,
|
||||
// imgUrls600Type: Array.isArray(productData && productData.imgUrls600)
|
||||
// ? 'array'
|
||||
// : typeof (productData && productData.imgUrls600),
|
||||
// // 동영상 관련 정보 추가
|
||||
// prdtMediaUrl: productData && productData.prdtMediaUrl,
|
||||
// thumbnailUrl960: productData && productData.thumbnailUrl960,
|
||||
// hasVideo: !!(productData && productData.prdtMediaUrl),
|
||||
// renderItemsLength: renderItems.length,
|
||||
// renderItems: renderItems,
|
||||
// productData: productData,
|
||||
// });
|
||||
// }, [productData, renderItems]);
|
||||
|
||||
const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]);
|
||||
|
||||
// FP: derive favorite flag from props with local override, avoid non-I/O useEffect
|
||||
const [favoriteOverride, setFavoriteOverride] = useState(null);
|
||||
const favoriteFlag = useMemo(
|
||||
() => deriveFavoriteFlag(favoriteOverride, productData),
|
||||
@@ -344,7 +339,7 @@ export default function ProductAllSection({
|
||||
// User Reviews 스크롤 핸들러 추가
|
||||
const handleUserReviewsClick = useCallback(
|
||||
() => scrollToSection('scroll-marker-user-reviews'),
|
||||
[]
|
||||
[scrollToSection]
|
||||
);
|
||||
|
||||
const scrollContainerRef = useRef(null);
|
||||
@@ -383,25 +378,21 @@ export default function ProductAllSection({
|
||||
|
||||
const { getScrollTo, scrollTop } = useScrollTo();
|
||||
|
||||
// FP: Pure function for mobile popup state change
|
||||
const handleShopByMobileOpen = useCallback(
|
||||
pipe(() => true, setMobileSendPopupOpen),
|
||||
[]
|
||||
);
|
||||
|
||||
// FP: Pure function for focus navigation to back button
|
||||
const handleSpotlightUpToBackButton = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
Spotlight.focus('spotlightId_backBtn');
|
||||
}, []);
|
||||
|
||||
// FP: Pure function for favorite flag change
|
||||
const onFavoriteFlagChanged = useCallback(
|
||||
(newFavoriteFlag) => setFavoriteOverride(newFavoriteFlag),
|
||||
[]
|
||||
);
|
||||
|
||||
// FP: Pure function for theme item button click with side effects
|
||||
const handleThemeItemButtonClick = useCallback(
|
||||
pipe(
|
||||
() => setOpenThemeItemOverlay(true),
|
||||
@@ -413,21 +404,22 @@ export default function ProductAllSection({
|
||||
[setOpenThemeItemOverlay]
|
||||
);
|
||||
|
||||
// FP: Pure function for scroll to section with early returns handled functionally
|
||||
const scrollToSection = curry((sectionId) =>
|
||||
pipe(
|
||||
when(isEmpty, () => null),
|
||||
andThen(() => document.getElementById(sectionId)),
|
||||
when(isNil, () => null),
|
||||
andThen((targetElement) => {
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
const y = targetRect.top;
|
||||
return scrollTop({ y, animate: true });
|
||||
})
|
||||
)(sectionId)
|
||||
const scrollToSection = useCallback(
|
||||
curry((sectionId) =>
|
||||
pipe(
|
||||
when(isEmpty, () => null),
|
||||
andThen(() => document.getElementById(sectionId)),
|
||||
when(isNil, () => null),
|
||||
andThen((targetElement) => {
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
const y = targetRect.top;
|
||||
return scrollTop({ y, animate: true });
|
||||
})
|
||||
)(sectionId)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
// FP: Curried scroll handlers
|
||||
const handleProductDetailsClick = useCallback(
|
||||
() => scrollToSection('scroll-marker-product-details'),
|
||||
[scrollToSection]
|
||||
@@ -471,7 +463,7 @@ export default function ProductAllSection({
|
||||
}
|
||||
}
|
||||
},
|
||||
[documentHeight, isBottom, youMayAlsoLikelRef]
|
||||
[documentHeight, isBottom]
|
||||
);
|
||||
|
||||
const productFocus = useCallback(() => {
|
||||
@@ -504,7 +496,7 @@ export default function ProductAllSection({
|
||||
(descriptionRef.current?.scrollHeight || 0) +
|
||||
(reviewRef.current?.scrollHeight || 0)
|
||||
);
|
||||
}, [productDetailRef.current, descriptionRef.current, hasReviews, hasYouMayAlsoLike]);
|
||||
}, [hasReviews, hasYouMayAlsoLike]);
|
||||
|
||||
//spot관련
|
||||
useEffect(() => {
|
||||
|
||||
@@ -60,16 +60,16 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
|
||||
// modal/full screen에 따른 일시정지/재생 처리
|
||||
useEffect(() => {
|
||||
console.log('[MediaPanel] ========== isOnTop useEffect ==========');
|
||||
console.log('[MediaPanel] isOnTop:', isOnTop);
|
||||
console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
|
||||
// console.log('[MediaPanel] ========== isOnTop useEffect ==========');
|
||||
// console.log('[MediaPanel] isOnTop:', isOnTop);
|
||||
// console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
|
||||
|
||||
if (panelInfo && panelInfo.modal) {
|
||||
if (!isOnTop) {
|
||||
console.log('[MediaPanel] Not on top - pausing video');
|
||||
// console.log('[MediaPanel] Not on top - pausing video');
|
||||
dispatch(pauseModalMedia());
|
||||
} else if (isOnTop && panelInfo.isPaused) {
|
||||
console.log('[MediaPanel] Back on top - resuming video');
|
||||
// console.log('[MediaPanel] Back on top - resuming video');
|
||||
dispatch(resumeModalMedia());
|
||||
}
|
||||
}
|
||||
@@ -79,10 +79,10 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
useEffect(() => {
|
||||
if (panelInfo?.modal && videoPlayer.current) {
|
||||
if (panelInfo.isPaused) {
|
||||
console.log('[MediaPanel] Executing pause via videoPlayer.current');
|
||||
// console.log('[MediaPanel] Executing pause via videoPlayer.current');
|
||||
videoPlayer.current.pause();
|
||||
} else if (panelInfo.isPaused === false) {
|
||||
console.log('[MediaPanel] Executing play via videoPlayer.current');
|
||||
// console.log('[MediaPanel] Executing play via videoPlayer.current');
|
||||
videoPlayer.current.play();
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
// 비디오 클릭 시 modal → fullscreen 전환
|
||||
const onVideoClick = useCallback(() => {
|
||||
if (panelInfo.modal) {
|
||||
console.log('[MediaPanel] Video clicked - switching to fullscreen');
|
||||
// console.log('[MediaPanel] Video clicked - switching to fullscreen');
|
||||
dispatch(switchMediaToFullscreen());
|
||||
}
|
||||
}, [dispatch, panelInfo.modal]);
|
||||
@@ -263,7 +263,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
|
||||
const onEnded = useCallback(
|
||||
(e) => {
|
||||
console.log('[MediaPanel] Video ended');
|
||||
// console.log('[MediaPanel] Video ended');
|
||||
// 비디오 종료 시 패널 닫기
|
||||
Spotlight.pause();
|
||||
setTimeout(() => {
|
||||
@@ -288,11 +288,11 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
setVideoLoaded(false);
|
||||
}, [currentPlayingUrl]);
|
||||
|
||||
console.log('[MediaPanel] ========== Rendering ==========');
|
||||
console.log('[MediaPanel] isOnTop:', isOnTop);
|
||||
console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
|
||||
console.log('[MediaPanel] currentPlayingUrl:', currentPlayingUrl);
|
||||
console.log('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current);
|
||||
// console.log('[MediaPanel] ========== Rendering ==========');
|
||||
// console.log('[MediaPanel] isOnTop:', isOnTop);
|
||||
// console.log('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2));
|
||||
// console.log('[MediaPanel] currentPlayingUrl:', currentPlayingUrl);
|
||||
// console.log('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current);
|
||||
|
||||
return (
|
||||
<TPanel
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
# MediaPanel 사용 가이드
|
||||
|
||||
MediaPanel은 순수 비디오 재생에 특화된 간소화된 패널입니다. PlayerPanel과 달리 탭, 채팅, QR코드 등의 오버레이 없이 오직 비디오 재생만 지원합니다.
|
||||
|
||||
## 주요 특징
|
||||
|
||||
- ✅ **단순성**: PlayerPanel의 1/8 크기 (~350줄 vs 2,300줄)
|
||||
- ✅ **Modal/Fullscreen 모드**: `modal` prop으로 제어
|
||||
- ✅ **자막 지원**: 선택적 자막 표시
|
||||
- ✅ **오버레이 없음**: 탭, 채팅, Shop Now 등 제거
|
||||
- ✅ **충돌 방지**: PlayerPanel과 독립적으로 동작
|
||||
|
||||
## 사용 방법
|
||||
|
||||
### 1. Modal 모드 (부분 화면)
|
||||
|
||||
```javascript
|
||||
import { startMediaPlayer } from '../../actions/mediaActions';
|
||||
|
||||
// Modal 모드로 비디오 재생
|
||||
dispatch(startMediaPlayer({
|
||||
modal: true,
|
||||
modalContainerId: 'some-container-id', // 비디오가 표시될 컨테이너 ID
|
||||
modalClassName: css.videoModal, // 추가 스타일 클래스
|
||||
showUrl: 'https://example.com/video.m3u8',
|
||||
thumbnailUrl: 'https://example.com/thumb.jpg',
|
||||
subtitle: 'https://example.com/subtitle.vtt', // 선택사항
|
||||
}));
|
||||
```
|
||||
|
||||
### 2. Fullscreen 모드 (전체 화면)
|
||||
|
||||
```javascript
|
||||
import { pushPanel } from '../../actions/panelActions';
|
||||
import { panel_names } from '../../utils/Config';
|
||||
|
||||
// Fullscreen 모드로 비디오 재생
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.MEDIA_PANEL,
|
||||
panelInfo: {
|
||||
modal: false,
|
||||
showUrl: 'https://example.com/video.m3u8',
|
||||
thumbnailUrl: 'https://example.com/thumb.jpg',
|
||||
subtitle: 'https://example.com/subtitle.vtt', // 선택사항
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 3. Modal → Fullscreen 전환
|
||||
|
||||
Modal 모드에서 사용자가 비디오를 클릭하면 자동으로 Fullscreen으로 전환할 수 있습니다:
|
||||
|
||||
```javascript
|
||||
// MediaPanel 내부에서 자동 처리됨
|
||||
// modalContainerId가 있으면 Back 버튼으로 Modal로 복귀 가능
|
||||
```
|
||||
|
||||
### 4. 비디오 종료
|
||||
|
||||
```javascript
|
||||
import { finishMediaPreview } from '../../actions/mediaActions';
|
||||
|
||||
// Modal 모드 종료
|
||||
dispatch(finishMediaPreview());
|
||||
|
||||
// 또는 일반 패널 닫기
|
||||
dispatch(popPanel());
|
||||
```
|
||||
|
||||
## Props 상세
|
||||
|
||||
### panelInfo 객체
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `modal` | boolean | Yes | true: 부분화면, false: 전체화면 |
|
||||
| `showUrl` | string | Yes | 비디오 URL (.m3u8, .mp4, .mpd 지원) |
|
||||
| `thumbnailUrl` | string | No | 썸네일 이미지 URL |
|
||||
| `subtitle` | string | No | 자막 파일 URL (.vtt) |
|
||||
| `modalContainerId` | string | No* | Modal 모드 시 컨테이너 ID (*modal=true면 권장) |
|
||||
| `modalClassName` | string | No | 추가 CSS 클래스 |
|
||||
| `modalStyle` | object | No | 직접 스타일 지정 (modalContainerId 대신) |
|
||||
| `modalScale` | number | No | Modal 스케일 (modalContainerId 대신) |
|
||||
| `isPaused` | boolean | No | 일시정지 상태 (자동 관리됨) |
|
||||
|
||||
## API 레퍼런스
|
||||
|
||||
### mediaActions.js에서 제공하는 액션들
|
||||
|
||||
#### `startMediaPlayer(params)`
|
||||
MediaPanel을 시작합니다.
|
||||
```javascript
|
||||
import { startMediaPlayer } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(startMediaPlayer({
|
||||
modal: true, // 필수: modal/fullscreen 모드
|
||||
showUrl: 'video.m3u8', // 필수: 비디오 URL
|
||||
modalContainerId: 'id', // modal=true일 때 권장
|
||||
thumbnailUrl: 'thumb.jpg',// 선택
|
||||
subtitle: 'subtitle.vtt', // 선택
|
||||
}));
|
||||
```
|
||||
|
||||
#### `finishMediaPreview()`
|
||||
Modal 모드의 MediaPanel을 종료합니다.
|
||||
```javascript
|
||||
import { finishMediaPreview } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(finishMediaPreview());
|
||||
```
|
||||
|
||||
#### `finishModalMediaForce()`
|
||||
강제로 modal MediaPanel을 종료합니다 (스택 어디에 있든).
|
||||
```javascript
|
||||
import { finishModalMediaForce } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(finishModalMediaForce());
|
||||
```
|
||||
|
||||
#### `pauseModalMedia()`
|
||||
Modal MediaPanel을 일시정지합니다 (패널은 유지).
|
||||
```javascript
|
||||
import { pauseModalMedia } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(pauseModalMedia());
|
||||
```
|
||||
|
||||
#### `resumeModalMedia()`
|
||||
Modal MediaPanel을 재생합니다.
|
||||
```javascript
|
||||
import { resumeModalMedia } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(resumeModalMedia());
|
||||
```
|
||||
|
||||
#### `switchMediaToFullscreen()`
|
||||
MediaPanel을 fullscreen 모드로 전환합니다.
|
||||
```javascript
|
||||
import { switchMediaToFullscreen } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(switchMediaToFullscreen());
|
||||
```
|
||||
|
||||
#### `switchMediaToModal(modalContainerId, modalClassName)`
|
||||
MediaPanel을 modal 모드로 전환합니다.
|
||||
```javascript
|
||||
import { switchMediaToModal } from '../../actions/mediaActions';
|
||||
|
||||
dispatch(switchMediaToModal('container-id', css.modalClass));
|
||||
```
|
||||
|
||||
## 지원 비디오 포맷
|
||||
|
||||
- **HLS**: `.m3u8` (권장)
|
||||
- **MP4**: `.mp4`
|
||||
- **DASH**: `.mpd`
|
||||
- **YouTube**: URL에 "youtu" 포함 시 자동 감지
|
||||
|
||||
## PlayerPanel과의 차이점
|
||||
|
||||
| 기능 | PlayerPanel | MediaPanel |
|
||||
|------|-------------|------------|
|
||||
| 비디오 재생 | ✅ | ✅ |
|
||||
| Modal/Fullscreen | ✅ | ✅ |
|
||||
| 자막 | ✅ | ✅ |
|
||||
| TabContainer | ✅ | ❌ |
|
||||
| Shop Now | ✅ | ❌ |
|
||||
| 채팅 | ✅ | ❌ |
|
||||
| QR Code | ✅ | ❌ |
|
||||
| 재생목록 | ✅ | ❌ |
|
||||
| Live Channel | ✅ | ❌ |
|
||||
| 코드 복잡도 | 매우 높음 | 낮음 |
|
||||
| 사용 사례 | 쇼핑 라이브/VOD | 순수 미디어 재생 |
|
||||
|
||||
## 실제 사용 예시
|
||||
|
||||
### 예시 1: 홈 배너에서 비디오 미리보기
|
||||
|
||||
```javascript
|
||||
const HomeBanner = () => {
|
||||
const dispatch = useDispatch();
|
||||
const videoContainerRef = useRef(null);
|
||||
|
||||
const handleVideoPreview = () => {
|
||||
dispatch(startMediaPlayer({
|
||||
modal: true,
|
||||
modalContainerId: 'home-banner-video',
|
||||
showUrl: bannerData.videoUrl,
|
||||
thumbnailUrl: bannerData.thumbnailUrl,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={videoContainerRef}
|
||||
data-spotlight-id="home-banner-video"
|
||||
onClick={handleVideoPreview}
|
||||
>
|
||||
<img src={bannerData.thumbnailUrl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 예시 2: 상세 페이지에서 전체화면 재생
|
||||
|
||||
```javascript
|
||||
const DetailPanel = ({ productData }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePlayVideo = () => {
|
||||
dispatch(pushPanel({
|
||||
name: panel_names.MEDIA_PANEL,
|
||||
panelInfo: {
|
||||
modal: false,
|
||||
showUrl: productData.demoVideoUrl,
|
||||
thumbnailUrl: productData.videoCover,
|
||||
subtitle: productData.subtitleUrl,
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TButton onClick={handlePlayVideo}>
|
||||
Play Demo Video
|
||||
</TButton>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 이벤트 핸들링
|
||||
|
||||
### onEnded
|
||||
비디오 종료 시 자동으로 패널이 닫힙니다 (1.5초 후).
|
||||
|
||||
### onError
|
||||
비디오 에러 발생 시 `sendBroadCast` 액션으로 에러 정보를 전송합니다.
|
||||
|
||||
### onClickBack
|
||||
- Modal 모드: 패널 닫기
|
||||
- Fullscreen with modalContainerId: Modal로 전환
|
||||
- Fullscreen: 패널 닫기
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **modalContainerId 필수**: Modal 모드 사용 시 `modalContainerId`를 반드시 지정해야 정확한 위치에 비디오가 표시됩니다.
|
||||
|
||||
2. **spotlight-id 설정**: modalContainerId로 사용할 엘리먼트에 `data-spotlight-id` 속성을 반드시 설정하세요.
|
||||
|
||||
3. **VideoPlayer 공유**: MediaPanel과 PlayerPanel은 동일한 VideoPlayer 컴포넌트를 사용하므로 동시에 여러 패널을 열면 안 됩니다.
|
||||
|
||||
4. **자막 포맷**: 자막은 WebVTT (.vtt) 포맷만 지원합니다.
|
||||
|
||||
## 디버깅
|
||||
|
||||
콘솔 로그를 확인하세요:
|
||||
- `[MediaPanel] isOnTop:` - 패널 상태 변경
|
||||
- `[MediaPanel] Not on top - pausing video` - Modal이 백그라운드로 이동
|
||||
- `[MediaPanel] Back on top - resuming video` - Modal이 포그라운드로 복귀
|
||||
- `[MediaPanel] Video ended` - 비디오 종료
|
||||
|
||||
## 라이센스
|
||||
|
||||
ShopTime 프로젝트의 일부로 동일한 라이센스가 적용됩니다.
|
||||
Reference in New Issue
Block a user