MediaPanel 실제 사용 컨텍스트 분석 및 필수 수정 사항 문서화 주요 발견사항: - DetailPanel → ProductVideo → MediaPanel 사용 패턴 분석 - Modal 모드: 오버레이 없음 (정상) - Fullscreen 모드: MediaSlider 필수 (누락됨) 필수 수정 사항: - MediaSlider 추가 (리모컨으로 진행 위치 조정) - Times 컴포넌트 추가 (현재/전체 시간 표시) - proportionLoaded/Played 상태 추가 - Slider 이벤트 핸들러 구현 조건부 렌더링: - Modal 모드 (modal=true): Slider 숨김 - Fullscreen 모드 (modal=false): Slider 표시 예상 작업 시간: 1.5시간
405 lines
9.7 KiB
Markdown
405 lines
9.7 KiB
Markdown
# MediaPlayer.v2 필수 수정 사항
|
||
|
||
**작성일**: 2025-11-10
|
||
**발견 사항**: MediaPanel의 실제 사용 컨텍스트 분석
|
||
|
||
---
|
||
|
||
## 🔍 실제 사용 패턴 분석
|
||
|
||
### 사용 위치
|
||
```
|
||
DetailPanel
|
||
→ ProductAllSection
|
||
→ ProductVideo
|
||
→ startMediaPlayer()
|
||
→ MediaPanel
|
||
→ MediaPlayer (VideoPlayer)
|
||
```
|
||
|
||
### 동작 플로우
|
||
|
||
#### 1️⃣ **Modal 모드 시작** (작은 화면)
|
||
```javascript
|
||
// ProductVideo.jsx:174-198
|
||
dispatch(startMediaPlayer({
|
||
modal: true, // 작은 화면 모드
|
||
modalContainerId: 'product-video-player',
|
||
showUrl: productInfo.prdtMediaUrl,
|
||
thumbnailUrl: productInfo.thumbnailUrl960,
|
||
// ...
|
||
}));
|
||
```
|
||
|
||
**Modal 모드 특징**:
|
||
- 화면 일부 영역에 fixed position으로 표시
|
||
- **오버레이 없음** (controls, slider 모두 숨김)
|
||
- 클릭만 가능 (전체화면으로 전환)
|
||
|
||
#### 2️⃣ **Fullscreen 모드 전환** (최대화면)
|
||
```javascript
|
||
// ProductVideo.jsx:164-168
|
||
if (isCurrentlyPlayingModal) {
|
||
dispatch(switchMediaToFullscreen()); // modal: false로 변경
|
||
}
|
||
```
|
||
|
||
**Fullscreen 모드 특징**:
|
||
- 전체 화면 표시
|
||
- **리모컨 엔터 키 → 오버레이 표시 필수**
|
||
- ✅ Back 버튼
|
||
- ✅ **비디오 진행 바 (MediaSlider)** ← 필수!
|
||
- ✅ 현재 시간 / 전체 시간 (Times)
|
||
- ✅ Play/Pause 버튼 (MediaControls)
|
||
|
||
---
|
||
|
||
## 🚨 현재 MediaPlayer.v2의 문제점
|
||
|
||
### ❌ 제거된 필수 기능
|
||
|
||
```javascript
|
||
// MediaPlayer.v2.jsx - 현재 상태
|
||
{controlsVisible && !isModal && (
|
||
<div className={css.simpleControls}>
|
||
<button onClick={...}>{paused ? '▶' : '⏸'}</button> // Play/Pause만
|
||
<button onClick={onBackButton}>← Back</button>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**문제**:
|
||
1. ❌ **MediaSlider (seek bar) 없음** - 리모컨으로 진행 위치 조정 불가
|
||
2. ❌ **Times 컴포넌트 없음** - 현재 시간/전체 시간 표시 안 됨
|
||
3. ❌ **proportionLoaded, proportionPlayed 상태 없음**
|
||
|
||
---
|
||
|
||
## ✅ 기존 MediaPlayer.jsx의 올바른 구현
|
||
|
||
### Modal vs Fullscreen 조건부 렌더링
|
||
|
||
```javascript
|
||
// MediaPlayer.jsx:2415-2461
|
||
{noSlider ? null : (
|
||
<div className={css.sliderContainer}>
|
||
{/* Times - 전체 시간 */}
|
||
{this.state.mediaSliderVisible && type ? (
|
||
<Times
|
||
noCurrentTime
|
||
total={this.state.duration}
|
||
formatter={durFmt}
|
||
type={type}
|
||
/>
|
||
) : null}
|
||
|
||
{/* Times - 현재 시간 */}
|
||
{this.state.mediaSliderVisible && type ? (
|
||
<Times
|
||
noTotalTime
|
||
current={this.state.currentTime}
|
||
formatter={durFmt}
|
||
/>
|
||
) : null}
|
||
|
||
{/* MediaSlider - modal이 아닐 때만 표시 */}
|
||
{!panelInfo.modal && (
|
||
<MediaSlider
|
||
backgroundProgress={this.state.proportionLoaded}
|
||
disabled={disabled || this.state.sourceUnavailable}
|
||
value={this.state.proportionPlayed}
|
||
visible={this.state.mediaSliderVisible}
|
||
spotlightDisabled={
|
||
spotlightDisabled || !this.state.mediaControlsVisible
|
||
}
|
||
onChange={this.onSliderChange}
|
||
onKnobMove={this.handleKnobMove}
|
||
onKeyDown={this.handleSliderKeyDown}
|
||
// ...
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**핵심 조건**:
|
||
```javascript
|
||
!panelInfo.modal // Modal이 아닐 때만 MediaSlider 표시
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 MediaPlayer.v2 수정 필요 사항
|
||
|
||
### 1. 상태 추가
|
||
|
||
```javascript
|
||
// 현재 (7개)
|
||
const [currentTime, setCurrentTime] = useState(0);
|
||
const [duration, setDuration] = useState(0);
|
||
const [paused, setPaused] = useState(!autoPlay);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(null);
|
||
const [controlsVisible, setControlsVisible] = useState(false);
|
||
const [sourceUnavailable, setSourceUnavailable] = useState(true);
|
||
|
||
// 추가 필요 (2개)
|
||
const [proportionLoaded, setProportionLoaded] = useState(0); // 로딩된 비율
|
||
const [proportionPlayed, setProportionPlayed] = useState(0); // 재생된 비율
|
||
```
|
||
|
||
### 2. Import 추가
|
||
|
||
```javascript
|
||
import { MediaSlider, Times, secondsToTime } from '../MediaPlayer';
|
||
import DurationFmt from 'ilib/lib/DurationFmt';
|
||
import { memoize } from '@enact/core/util';
|
||
```
|
||
|
||
### 3. DurationFmt 헬퍼 추가
|
||
|
||
```javascript
|
||
const memoGetDurFmt = memoize(
|
||
() => new DurationFmt({
|
||
length: 'medium',
|
||
style: 'clock',
|
||
useNative: false,
|
||
})
|
||
);
|
||
|
||
const getDurFmt = () => {
|
||
if (typeof window === 'undefined') return null;
|
||
return memoGetDurFmt();
|
||
};
|
||
```
|
||
|
||
### 4. handleUpdate 수정 (proportionLoaded/Played 계산)
|
||
|
||
```javascript
|
||
const handleUpdate = useCallback((ev) => {
|
||
const el = videoRef.current;
|
||
if (!el) return;
|
||
|
||
const newCurrentTime = el.currentTime || 0;
|
||
const newDuration = el.duration || 0;
|
||
|
||
setCurrentTime(newCurrentTime);
|
||
setDuration(newDuration);
|
||
setPaused(el.paused);
|
||
setLoading(el.loading || false);
|
||
setError(el.error || null);
|
||
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
|
||
|
||
// 추가: proportion 계산
|
||
setProportionLoaded(el.proportionLoaded || 0);
|
||
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
|
||
|
||
// 콜백 호출
|
||
if (ev.type === 'timeupdate' && onTimeUpdate) {
|
||
onTimeUpdate(ev);
|
||
}
|
||
// ...
|
||
}, [onTimeUpdate, sourceUnavailable]);
|
||
```
|
||
|
||
### 5. Slider 이벤트 핸들러 추가
|
||
|
||
```javascript
|
||
const handleSliderChange = useCallback(({ value }) => {
|
||
const time = value * duration;
|
||
seek(time);
|
||
}, [duration, seek]);
|
||
|
||
const handleKnobMove = useCallback((ev) => {
|
||
if (!videoRef.current) return;
|
||
|
||
const seconds = Math.floor(ev.proportion * videoRef.current.duration);
|
||
if (!isNaN(seconds)) {
|
||
// 스크럽 시 시간 표시 업데이트 등
|
||
// 필요시 onScrub 콜백 호출
|
||
}
|
||
}, []);
|
||
|
||
const handleSliderKeyDown = useCallback((ev) => {
|
||
// Spotlight 키 이벤트 처리
|
||
// 위/아래 키로 controls 이동 등
|
||
}, []);
|
||
```
|
||
|
||
### 6. Controls UI 수정
|
||
|
||
```javascript
|
||
{/* Modal이 아닐 때만 전체 controls 표시 */}
|
||
{controlsVisible && !isModal && (
|
||
<div className={css.controlsContainer}>
|
||
{/* Slider Section */}
|
||
<div className={css.sliderContainer}>
|
||
{/* Times - 전체 시간 */}
|
||
<Times
|
||
noCurrentTime
|
||
total={duration}
|
||
formatter={getDurFmt()}
|
||
type={type}
|
||
/>
|
||
|
||
{/* Times - 현재 시간 */}
|
||
<Times
|
||
noTotalTime
|
||
current={currentTime}
|
||
formatter={getDurFmt()}
|
||
/>
|
||
|
||
{/* MediaSlider */}
|
||
<MediaSlider
|
||
backgroundProgress={proportionLoaded}
|
||
disabled={disabled || sourceUnavailable}
|
||
value={proportionPlayed}
|
||
visible={controlsVisible}
|
||
spotlightDisabled={spotlightDisabled}
|
||
onChange={handleSliderChange}
|
||
onKnobMove={handleKnobMove}
|
||
onKeyDown={handleSliderKeyDown}
|
||
spotlightId="media-slider-v2"
|
||
/>
|
||
</div>
|
||
|
||
{/* Controls Section */}
|
||
<div className={css.controlsButtons}>
|
||
<button className={css.playPauseBtn} onClick={...}>
|
||
{paused ? '▶' : '⏸'}
|
||
</button>
|
||
|
||
{onBackButton && (
|
||
<button className={css.backBtn} onClick={onBackButton}>
|
||
← Back
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
### 7. CSS 추가
|
||
|
||
```less
|
||
// VideoPlayer.module.less
|
||
|
||
.controlsContainer {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 20px;
|
||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
||
z-index: 10;
|
||
}
|
||
|
||
.sliderContainer {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.controlsButtons {
|
||
display: flex;
|
||
gap: 20px;
|
||
justify-content: center;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 수정 전/후 비교
|
||
|
||
### 현재 MediaPlayer.v2 (문제)
|
||
|
||
```
|
||
Modal 모드 (modal=true):
|
||
✅ 오버레이 없음 (정상)
|
||
✅ 클릭으로 전환 (정상)
|
||
|
||
Fullscreen 모드 (modal=false):
|
||
❌ MediaSlider 없음 (문제!)
|
||
❌ Times 없음 (문제!)
|
||
✅ Play/Pause 버튼 (정상)
|
||
✅ Back 버튼 (정상)
|
||
```
|
||
|
||
### 수정 후 MediaPlayer.v2 (정상)
|
||
|
||
```
|
||
Modal 모드 (modal=true):
|
||
✅ 오버레이 없음
|
||
✅ 클릭으로 전환
|
||
|
||
Fullscreen 모드 (modal=false):
|
||
✅ MediaSlider (seek bar)
|
||
✅ Times (현재/전체 시간)
|
||
✅ Play/Pause 버튼
|
||
✅ Back 버튼
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 우선순위
|
||
|
||
### High Priority (필수)
|
||
1. ✅ **MediaSlider 추가** - 리모컨으로 진행 위치 조정
|
||
2. ✅ **Times 컴포넌트 추가** - 시간 표시
|
||
3. ✅ **proportionLoaded/Played 상태** - slider 동작
|
||
|
||
### Medium Priority (권장)
|
||
4. Slider 이벤트 핸들러 세부 구현
|
||
5. Spotlight 키 네비게이션 (위/아래로 slider ↔ buttons)
|
||
6. CSS 스타일 개선
|
||
|
||
### Low Priority (선택)
|
||
7. Scrub 시 썸네일 표시 (기존에도 없음)
|
||
8. 추가 피드백 UI
|
||
|
||
---
|
||
|
||
## 🔧 구현 순서
|
||
|
||
1. **Phase 1**: 상태 및 import 추가 (10분)
|
||
2. **Phase 2**: MediaSlider 렌더링 (20분)
|
||
3. **Phase 3**: Times 컴포넌트 추가 (10분)
|
||
4. **Phase 4**: 이벤트 핸들러 구현 (20분)
|
||
5. **Phase 5**: CSS 스타일 조정 (10분)
|
||
6. **Phase 6**: 테스트 및 디버깅 (30분)
|
||
|
||
**총 예상 시간**: 약 1.5시간
|
||
|
||
---
|
||
|
||
## ✅ 체크리스트
|
||
|
||
- [ ] proportionLoaded, proportionPlayed 상태 추가
|
||
- [ ] MediaSlider, Times import
|
||
- [ ] DurationFmt 헬퍼 추가
|
||
- [ ] handleUpdate에서 proportion 계산
|
||
- [ ] handleSliderChange 구현
|
||
- [ ] handleKnobMove 구현
|
||
- [ ] handleSliderKeyDown 구현
|
||
- [ ] Controls UI에 slider 추가
|
||
- [ ] Times 컴포넌트 추가
|
||
- [ ] CSS 스타일 추가
|
||
- [ ] Modal 모드에서 slider 숨김 확인
|
||
- [ ] Fullscreen 모드에서 slider 표시 확인
|
||
- [ ] 리모컨으로 seek 동작 테스트
|
||
|
||
---
|
||
|
||
## 📝 결론
|
||
|
||
MediaPlayer.v2는 **MediaSlider와 Times가 필수**입니다.
|
||
|
||
이유:
|
||
1. DetailPanel → ProductVideo에서만 사용
|
||
2. Fullscreen 모드에서 리모컨 사용자가 비디오 진행 위치를 조정해야 함
|
||
3. 현재/전체 시간 표시 필요
|
||
|
||
**→ "간소화"는 맞지만, "필수 기능 제거"는 아님**
|
||
**→ MediaSlider는 제거 불가, 단 Modal 모드에서만 조건부 숨김**
|