Files
shoptime/com.twin.app.shoptime/.docs/modal-transition-analysis.md
optrader fd5a171a28 restore: .docs 폴더 복원 및 .gitignore 수정
- claude/ 브랜치에서 누락된 .docs 폴더 복원 완료
- dispatch-async 관련 문서 9개 파일 복원
  * 01-problem.md, 02-solution-dispatch-helper.md
  * 03-solution-async-utils.md, 04-solution-queue-system.md
  * 05-usage-patterns.md, 06-setup-guide.md
  * 07-changelog.md, 08-troubleshooting.md, README.md
- MediaPlayer.v2 관련 문서 4개 파일 복원
  * MediaPlayer-v2-README.md, MediaPlayer-v2-Required-Changes.md
  * MediaPlayer-v2-Risk-Analysis.md, PR-MediaPlayer-v2.md
- 기타 분석 문서 2개 파일 복원
  * modal-transition-analysis.md, video-player-analysis-and-optimization-plan.md
- .gitignore에서 .docs 항목 제거로 문서 추적 가능하도록 수정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: GLM 4.6 <noreply@z.ai>
2025-11-11 10:00:59 +09:00

11 KiB

Modal 전환 기능 상세 분석

작성일: 2025-11-10 목적: MediaPlayer.v2.jsx 설계를 위한 필수 기능 분석


📋 Modal 모드 전환 플로우

1. 시작: Modal 모드로 비디오 재생

// actions/mediaActions.js - startMediaPlayer()
dispatch(startMediaPlayer({
  modal: true,
  modalContainerId: 'some-product-id',
  showUrl: 'video-url.mp4',
  thumbnailUrl: 'thumb.jpg',
  // ...
}));

MediaPanel에서의 처리 (MediaPanel.jsx:114-161):

useEffect(() => {
  if (panelInfo.modal && panelInfo.modalContainerId) {
    // 1. DOM 노드 찾기
    const node = document.querySelector(
      `[data-spotlight-id="${panelInfo.modalContainerId}"]`
    );

    // 2. 위치와 크기 계산
    const { width, height, top, left } = node.getBoundingClientRect();

    // 3. padding/margin 조정
    const totalOffset = 24; // 6*2 + 6*2
    const adjustedWidth = width - totalOffset;
    const adjustedHeight = height - totalOffset;

    // 4. Fixed 위치 스타일 생성
    const style = {
      width: adjustedWidth + 'px',
      height: adjustedHeight + 'px',
      top: (top + totalOffset/2) + 'px',
      left: (left + totalOffset/2) + 'px',
      position: 'fixed',
      overflow: 'hidden'
    };

    setModalStyle(style);
    setModalScale(adjustedWidth / window.innerWidth);
  }
}, [panelInfo, isOnTop]);

VideoPlayer에 전달:

<VideoPlayer
  disabled={panelInfo.modal}           // modal에서는 controls 비활성
  spotlightDisabled={panelInfo.modal}  // modal에서는 spotlight 비활성
  style={panelInfo.modal ? modalStyle : {}}
  modalScale={panelInfo.modal ? modalScale : 1}
  modalClassName={panelInfo.modal && panelInfo.modalClassName}
  onClick={onVideoClick}               // 클릭 시 전환
/>

2. 전환: Modal → Fullscreen

사용자 액션: modal 비디오 클릭

// MediaPanel.jsx:164-174
const onVideoClick = useCallback(() => {
  if (panelInfo.modal) {
    dispatch(switchMediaToFullscreen());
  }
}, [dispatch, panelInfo.modal]);

Redux Action (mediaActions.js:164-208):

export const switchMediaToFullscreen = () => (dispatch, getState) => {
  const modalMediaPanel = panels.find(
    (panel) => panel.name === panel_names.MEDIA_PANEL &&
               panel.panelInfo?.modal
  );

  if (modalMediaPanel) {
    dispatch(updatePanel({
      name: panel_names.MEDIA_PANEL,
      panelInfo: {
        ...modalMediaPanel.panelInfo,
        modal: false  // 🔑 핵심: modal만 false로 변경
      }
    }));
  }
};

MediaPanel 재렌더링:

// panelInfo.modal이 false가 되면 useEffect 재실행
useEffect(() => {
  // modal이 false이면 else if 분기 실행
  else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) {
    // 재생 상태 복원
    if (videoPlayer.current?.getMediaState()?.paused) {
      videoPlayer.current.play();
    }

    // controls 표시
    if (!videoPlayer.current.areControlsVisible()) {
      videoPlayer.current.showControls();
    }
  }
}, [panelInfo, isOnTop]);

// VideoPlayer에 전달되는 props 변경
<VideoPlayer
  disabled={false}              // controls 활성화
  spotlightDisabled={false}     // spotlight 활성화
  style={{}}                     // fixed position 제거 → 전체화면
  modalScale={1}
  modalClassName={undefined}
/>

3. 복귀: Fullscreen → Modal (Back 버튼)

// MediaPanel.jsx:176-194
const onClickBack = useCallback((ev) => {
  // modalContainerId가 있으면 modal에서 왔던 것
  if (panelInfo.modalContainerId && !panelInfo.modal) {
    dispatch(PanelActions.popPanel());
    ev?.stopPropagation();
    return;
  }

  // 일반 fullscreen이면 그냥 닫기
  if (!panelInfo.modal) {
    dispatch(PanelActions.popPanel());
    ev?.stopPropagation();
  }
}, [dispatch, panelInfo]);

🔑 핵심 메커니즘

1. 같은 MediaPanel 재사용

  • modal → fullscreen 전환 시 패널을 새로 만들지 않음
  • updatePanelpanelInfo.modal만 변경
  • 비디오 재생 상태 유지 (같은 컴포넌트 인스턴스)

2. 스타일 동적 계산

// modal=true
style={{
  position: 'fixed',
  top: '100px',
  left: '200px',
  width: '400px',
  height: '300px'
}}

// modal=false
style={{}}  // 전체화면 (기본 CSS)

3. Pause/Resume 관리

// modal에서 다른 패널이 위로 올라오면
useEffect(() => {
  if (panelInfo?.modal) {
    if (!isOnTop) {
      dispatch(pauseModalMedia());  // isPaused: true
    } else if (isOnTop && panelInfo.isPaused) {
      dispatch(resumeModalMedia()); // isPaused: false
    }
  }
}, [isOnTop, panelInfo, dispatch]);

// VideoPlayer에서 isPaused 감지하여 play/pause 제어
useEffect(() => {
  if (panelInfo?.modal && videoPlayer.current) {
    if (panelInfo.isPaused) {
      videoPlayer.current.pause();
    } else if (panelInfo.isPaused === false) {
      videoPlayer.current.play();
    }
  }
}, [panelInfo?.isPaused, panelInfo?.modal]);

📐 MediaPlayer.v2.jsx가 지원해야 할 기능

필수 Props (추가)

{
  // 기존
  src,
  autoPlay,
  loop,
  onEnded,
  onError,
  thumbnailUrl,
  videoComponent,

  // Modal 전환 관련 (필수)
  disabled,           // modal=true일 때 true
  spotlightDisabled,  // modal=true일 때 true
  onClick,            // modal일 때 클릭 → switchMediaToFullscreen
  style,              // modal일 때 fixed position style
  modalClassName,     // modal일 때 추가 className
  modalScale,         // modal일 때 scale 값 (QR코드 등에 사용)

  // 패널 정보
  panelInfo: {
    modal,            // modal 모드 여부
    modalContainerId, // modal 기준 컨테이너 ID
    isPaused,         // 일시정지 여부 (다른 패널 위로 올라옴)
    showUrl,          // 비디오 URL
    thumbnailUrl,     // 썸네일 URL
  },

  // 콜백
  onBackButton,       // Back 버튼 핸들러

  // Spotlight
  spotlightId,
}

필수 기능

1. Modal 모드 스타일 적용

const containerStyle = useMemo(() => {
  if (panelInfo?.modal && style) {
    return style; // MediaPanel에서 계산한 fixed position
  }
  return {}; // 전체화면
}, [panelInfo?.modal, style]);

2. Modal 클릭 처리

const handleVideoClick = useCallback(() => {
  if (panelInfo?.modal && onClick) {
    onClick(); // switchMediaToFullscreen 호출
    return;
  }

  // fullscreen이면 controls 토글
  toggleControls();
}, [panelInfo?.modal, onClick]);

3. isPaused 상태 동기화

useEffect(() => {
  if (panelInfo?.modal && videoRef.current) {
    if (panelInfo.isPaused) {
      videoRef.current.pause();
    } else if (panelInfo.isPaused === false) {
      videoRef.current.play();
    }
  }
}, [panelInfo?.isPaused, panelInfo?.modal]);

4. Modal → Fullscreen 전환 시 재생 복원

useEffect(() => {
  // modal에서 fullscreen으로 전환되었을 때
  if (prevPanelInfo?.modal && !panelInfo?.modal) {
    if (videoRef.current?.paused) {
      videoRef.current.play();
    }
    setControlsVisible(true);
  }
}, [panelInfo?.modal]);

5. Controls/Spotlight 비활성화

const shouldDisableControls = panelInfo?.modal || disabled;
const shouldDisableSpotlight = panelInfo?.modal || spotlightDisabled;

🚫 여전히 제거 가능한 기능

Modal 전환과 무관한 기능들:

❌ QR코드 오버레이 (PlayerPanel 전용)
❌ 전화번호 오버레이 (PlayerPanel 전용)
❌ 테마 인디케이터 (PlayerPanel 전용)
❌ MediaSlider (seek bar) - 단순 재생만
❌ 복잡한 피드백 시스템 (miniFeedback, 8개 Job)
❌ Announce/Accessibility 복잡계
❌ FloatingLayer
❌ Redux 통합 (updateVideoPlayState)
❌ TabContainer 동기화 (PlayerPanel 전용)
❌ MediaTitle, infoComponents
❌ jumpBy, fastForward, rewind
❌ playbackRate 조정

📊 최종 상태 변수 (9개)

const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [paused, setPaused] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [controlsVisible, setControlsVisible] = useState(false);

// Modal 관련 (MediaPanel에서 계산하므로 state 불필요)
// modalStyle, modalScale → props로 받음

📊 최종 Props 목록 (~18개)

MediaPlayerV2.propTypes = {
  // 비디오 소스
  src: PropTypes.string.isRequired,
  type: PropTypes.string,
  thumbnailUrl: PropTypes.string,

  // 재생 제어
  autoPlay: PropTypes.bool,
  loop: PropTypes.bool,

  // Modal 전환
  disabled: PropTypes.bool,
  spotlightDisabled: PropTypes.bool,
  onClick: PropTypes.func,
  style: PropTypes.object,
  modalClassName: PropTypes.string,
  modalScale: PropTypes.number,

  // 패널 정보
  panelInfo: PropTypes.shape({
    modal: PropTypes.bool,
    modalContainerId: PropTypes.string,
    isPaused: PropTypes.bool,
    showUrl: PropTypes.string,
    thumbnailUrl: PropTypes.string,
  }),

  // 콜백
  onEnded: PropTypes.func,
  onError: PropTypes.func,
  onBackButton: PropTypes.func,

  // Spotlight
  spotlightId: PropTypes.string,

  // 비디오 컴포넌트
  videoComponent: PropTypes.elementType,
};

🎯 구현 우선순위

Phase 1: 기본 재생 (1일)

  • 비디오 element 렌더링 (Media / TReactPlayer)
  • 기본 play/pause 제어
  • 로딩 상태 및 썸네일 표시
  • API 제공 (getMediaState, play, pause)

Phase 2: Modal 전환 (1일)

  • Modal 스타일 적용 (props.style)
  • Modal 클릭 → Fullscreen 전환
  • isPaused 상태 동기화
  • disabled/spotlightDisabled 처리

Phase 3: Controls (1일)

  • 최소한의 controls UI (재생/일시정지만)
  • Controls 자동 숨김/보임
  • Spotlight 포커스 관리 (기본만)

Phase 4: 테스트 및 최적화 (1일)

  • 메모리 프로파일링
  • 전환 애니메이션 부드럽게
  • Edge case 처리

💡 예상 개선 효과 (수정)

항목 현재 개선 후 개선율
코드 라인 2,595 ~700 73% 감소
상태 변수 20+ 6~9 60% 감소
Props 70+ ~18 74% 감소
타이머/Job 8 1~2 80% 감소
필수 기능 100% 100% 유지
메모리 점유 높음 낮음 예상 40%+ 감소
렌더링 속도 느림 빠름 예상 2배 향상

결론

Modal 전환 기능은 복잡해 보이지만, 실제로는:

  1. MediaPanel에서 스타일 계산 (modalStyle, modalScale)
  2. MediaPlayer는 받은 style을 그대로 적용
  3. modal 플래그에 따라 controls/spotlight 활성화 여부만 제어

따라서 MediaPlayer.v2.jsx는:

  • Modal 전환 로직 구현 필요 없음
  • Props 받아서 적용만 하면 됨
  • 핵심 복잡도는 MediaPanel에 있음

→ 여전히 대폭 간소화 가능!