From 6f62c7b65c54754efb77dfbcebb9a9c2fc378a55 Mon Sep 17 00:00:00 2001 From: optrader Date: Tue, 2 Dec 2025 06:22:10 +0000 Subject: [PATCH] Remove .docs --- .docs/MediaPlayer-v2-README.md | 413 --------- .docs/MediaPlayer-v2-Required-Changes.md | 404 --------- .docs/MediaPlayer-v2-Risk-Analysis.md | 789 ----------------- .docs/PR-MediaPlayer-v2.md | 164 ---- .docs/dispatch-async/01-problem.md | 210 ----- .../02-solution-dispatch-helper.md | 541 ------------ .../dispatch-async/03-solution-async-utils.md | 711 ---------------- .../04-solution-queue-system.md | 644 -------------- .docs/dispatch-async/05-usage-patterns.md | 804 ------------------ .docs/dispatch-async/06-setup-guide.md | 396 --------- .docs/dispatch-async/07-changelog.md | 314 ------- .docs/dispatch-async/08-troubleshooting.md | 606 ------------- .docs/dispatch-async/README.md | 137 --- .docs/modal-transition-analysis.md | 437 ---------- ...o-player-analysis-and-optimization-plan.md | 214 ----- 15 files changed, 6784 deletions(-) delete mode 100644 .docs/MediaPlayer-v2-README.md delete mode 100644 .docs/MediaPlayer-v2-Required-Changes.md delete mode 100644 .docs/MediaPlayer-v2-Risk-Analysis.md delete mode 100644 .docs/PR-MediaPlayer-v2.md delete mode 100644 .docs/dispatch-async/01-problem.md delete mode 100644 .docs/dispatch-async/02-solution-dispatch-helper.md delete mode 100644 .docs/dispatch-async/03-solution-async-utils.md delete mode 100644 .docs/dispatch-async/04-solution-queue-system.md delete mode 100644 .docs/dispatch-async/05-usage-patterns.md delete mode 100644 .docs/dispatch-async/06-setup-guide.md delete mode 100644 .docs/dispatch-async/07-changelog.md delete mode 100644 .docs/dispatch-async/08-troubleshooting.md delete mode 100644 .docs/dispatch-async/README.md delete mode 100644 .docs/modal-transition-analysis.md delete mode 100644 .docs/video-player-analysis-and-optimization-plan.md diff --git a/.docs/MediaPlayer-v2-README.md b/.docs/MediaPlayer-v2-README.md deleted file mode 100644 index b42e8a09..00000000 --- a/.docs/MediaPlayer-v2-README.md +++ /dev/null @@ -1,413 +0,0 @@ -# MediaPlayer.v2 - 최적화된 비디오 플레이어 - -**위치**: `src/components/VideoPlayer/MediaPlayer.v2.jsx` - ---- - -## 📊 개요 - -webOS 환경에 최적화된 경량 비디오 플레이어 컴포넌트입니다. -기존 MediaPlayer.jsx의 핵심 기능은 유지하면서 불필요한 복잡도를 제거했습니다. - -### 주요 개선사항 - -| 항목 | 기존 | v2 | 개선율 | -|------|------|-----|--------| -| **코드 라인** | 2,595 | 388 | **85%↓** | -| **상태 변수** | 20+ | 7 | **65%↓** | -| **Props** | 70+ | 18 | **74%↓** | -| **타이머/Job** | 8 | 1 | **87%↓** | -| **필수 기능** | 100% | 100% | **✅ 유지** | - ---- - -## ✨ 주요 기능 - -### 1. Modal ↔ Fullscreen 전환 -```javascript -// Modal 모드로 시작 - dispatch(switchMediaToFullscreen())} - style={modalStyle} // MediaPanel에서 계산 -/> - -// 클릭 시 자동으로 Fullscreen으로 전환 -``` - -### 2. 기본 재생 제어 -```javascript -const playerRef = useRef(); - -// API 메서드 -playerRef.current.play(); -playerRef.current.pause(); -playerRef.current.seek(30); -playerRef.current.getMediaState(); -playerRef.current.showControls(); -playerRef.current.hideControls(); -``` - -### 3. isPaused 동기화 -```javascript -// Modal 모드에서 다른 패널이 위로 올라오면 자동 일시정지 - -``` - -### 4. webOS / 브라우저 자동 감지 -```javascript -// webOS: Media 컴포넌트 -// 브라우저: TReactPlayer -// YouTube: TReactPlayer - -// 자동으로 적절한 컴포넌트 선택 - - -``` - ---- - -## 📐 Props - -### 필수 Props - -```typescript -interface MediaPlayerV2Props { - // 비디오 소스 (필수) - src: string; -} -``` - -### 선택 Props - -```typescript -interface MediaPlayerV2Props { - // 비디오 설정 - type?: string; // 기본: 'video/mp4' - thumbnailUrl?: string; - - // 재생 제어 - autoPlay?: boolean; // 기본: false - loop?: boolean; // 기본: false - muted?: boolean; // 기본: false - - // Modal 전환 - disabled?: boolean; // Modal에서 true - spotlightDisabled?: boolean; - onClick?: () => void; // Modal 클릭 시 - style?: CSSProperties; // Modal fixed position - modalClassName?: string; - modalScale?: number; - - // 패널 정보 - panelInfo?: { - modal?: boolean; - modalContainerId?: string; - isPaused?: boolean; - }; - - // 콜백 - onEnded?: (e: Event) => void; - onError?: (e: Event) => void; - onBackButton?: (e: Event) => void; - onLoadStart?: (e: Event) => void; - onTimeUpdate?: (e: Event) => void; - onLoadedData?: (e: Event) => void; - onLoadedMetadata?: (e: Event) => void; - onDurationChange?: (e: Event) => void; - - // Spotlight - spotlightId?: string; // 기본: 'mediaPlayerV2' - - // 비디오 컴포넌트 - videoComponent?: React.ComponentType; - - // ReactPlayer 설정 - reactPlayerConfig?: object; - - // 기타 - children?: React.ReactNode; // , tags - className?: string; -} -``` - ---- - -## 💻 사용 예제 - -### 기본 사용 - -```javascript -import MediaPlayerV2 from '../components/VideoPlayer/MediaPlayer.v2'; - -function MyComponent() { - return ( - console.log('Video ended')} - /> - ); -} -``` - -### Modal 모드 (MediaPanel에서 사용) - -```javascript -import MediaPlayerV2 from '../components/VideoPlayer/MediaPlayer.v2'; - -function MediaPanel({ panelInfo }) { - const [modalStyle, setModalStyle] = useState({}); - - useEffect(() => { - if (panelInfo.modal && panelInfo.modalContainerId) { - const node = document.querySelector( - `[data-spotlight-id="${panelInfo.modalContainerId}"]` - ); - const rect = node.getBoundingClientRect(); - - setModalStyle({ - position: 'fixed', - top: rect.top + 'px', - left: rect.left + 'px', - width: rect.width + 'px', - height: rect.height + 'px', - }); - } - }, [panelInfo]); - - const handleVideoClick = () => { - if (panelInfo.modal) { - dispatch(switchMediaToFullscreen()); - } - }; - - return ( - - ); -} -``` - -### API 사용 - -```javascript -import { useRef } from 'react'; -import MediaPlayerV2 from '../components/VideoPlayer/MediaPlayer.v2'; - -function MyComponent() { - const playerRef = useRef(); - - const handlePlay = () => { - playerRef.current?.play(); - }; - - const handlePause = () => { - playerRef.current?.pause(); - }; - - const handleSeek = (time) => { - playerRef.current?.seek(time); - }; - - const getState = () => { - const state = playerRef.current?.getMediaState(); - console.log(state); - // { - // currentTime: 10.5, - // duration: 120, - // paused: false, - // loading: false, - // error: null, - // playbackRate: 1, - // proportionPlayed: 0.0875 - // } - }; - - return ( - <> - - - - - - - - ); -} -``` - -### webOS 태그 사용 - -```javascript - - - - -``` - -### YouTube 재생 - -```javascript - -``` - ---- - -## 🔧 API 메서드 - -ref를 통해 다음 메서드에 접근할 수 있습니다: - -```typescript -interface MediaPlayerV2API { - // 재생 제어 - play(): void; - pause(): void; - seek(timeIndex: number): void; - - // 상태 조회 - getMediaState(): { - currentTime: number; - duration: number; - paused: boolean; - loading: boolean; - error: Error | null; - playbackRate: number; - proportionPlayed: number; - }; - - // Controls 제어 - showControls(): void; - hideControls(): void; - toggleControls(): void; - areControlsVisible(): boolean; - - // Video Node 접근 - getVideoNode(): HTMLVideoElement | ReactPlayerInstance; -} -``` - ---- - -## 🎯 제거된 기능 - -다음 기능들은 MediaPanel 사용 케이스에 불필요하여 제거되었습니다: - -``` -❌ MediaSlider (seek bar) -❌ jumpBy, fastForward, rewind -❌ playbackRate 조정 -❌ QR코드 오버레이 -❌ 전화번호 오버레이 -❌ 테마 인디케이터 -❌ 복잡한 피드백 시스템 (8개 Job → 1개 setTimeout) -❌ FloatingLayer -❌ Redux 통합 -❌ TabContainer 동기화 -❌ Announce/Accessibility 복잡계 -❌ MediaTitle, infoComponents -``` - -필요하다면 기존 MediaPlayer.jsx를 사용하세요. - ---- - -## 🚀 성능 - -### 메모리 사용량 - -- **타이머**: 8개 Job → 1개 setTimeout -- **이벤트 리스너**: 최소화 (video element events만) -- **상태 변수**: 7개 (20+개에서 감소) - -### 렌더링 성능 - -- **useMemo**: 계산 비용이 큰 값 캐싱 -- **useCallback**: 함수 재생성 방지 -- **조건부 렌더링**: 불필요한 DOM 요소 제거 - ---- - -## 🔄 마이그레이션 가이드 - -### 기존 MediaPlayer.jsx에서 마이그레이션 - -대부분의 props는 호환됩니다: - -```javascript -// 기존 -import { VideoPlayer } from '../components/VideoPlayer/MediaPlayer'; - -// 새로운 -import MediaPlayerV2 from '../components/VideoPlayer/MediaPlayer.v2'; -``` - -제거된 props: -- `jumpBy`, `initialJumpDelay`, `jumpDelay` -- `playbackRateHash` -- `onFastForward`, `onRewind`, `onJumpBackward`, `onJumpForward` -- `feedbackHideDelay`, `miniFeedbackHideDelay` -- `noMediaSliderFeedback`, `noMiniFeedback`, `noSlider` -- `title`, `infoComponents` -- 기타 PlayerPanel 전용 props - ---- - -## 📝 Notes - -### Modal 전환 작동 방식 - -1. **MediaPanel**이 `getBoundingClientRect()`로 스타일 계산 -2. **MediaPlayerV2**는 받은 `style`을 그대로 적용 -3. `modal` 플래그에 따라 controls/spotlight 활성화 제어 - -→ **MediaPlayerV2는 전환 로직 구현 불필요** - -### webOS 호환성 - -- `window.PalmSystem` 존재 시 `Media` 컴포넌트 사용 -- 브라우저에서는 `TReactPlayer` 사용 -- YouTube URL은 항상 `TReactPlayer` 사용 - ---- - -## 🐛 알려진 제약사항 - -1. **Seek bar 없음**: 단순 재생만 지원 -2. **빠르기 조정 없음**: 배속 재생 미지원 -3. **간단한 Controls**: 재생/일시정지 버튼만 - -복잡한 컨트롤이 필요하다면 기존 `MediaPlayer.jsx` 사용을 권장합니다. - ---- - -## 📚 관련 문서 - -- [비디오 플레이어 분석 문서](.docs/video-player-analysis-and-optimization-plan.md) -- [Modal 전환 상세 분석](.docs/modal-transition-analysis.md) diff --git a/.docs/MediaPlayer-v2-Required-Changes.md b/.docs/MediaPlayer-v2-Required-Changes.md deleted file mode 100644 index b4469eef..00000000 --- a/.docs/MediaPlayer-v2-Required-Changes.md +++ /dev/null @@ -1,404 +0,0 @@ -# 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 && ( -
- // Play/Pause만 - -
-)} -``` - -**문제**: -1. ❌ **MediaSlider (seek bar) 없음** - 리모컨으로 진행 위치 조정 불가 -2. ❌ **Times 컴포넌트 없음** - 현재 시간/전체 시간 표시 안 됨 -3. ❌ **proportionLoaded, proportionPlayed 상태 없음** - ---- - -## ✅ 기존 MediaPlayer.jsx의 올바른 구현 - -### Modal vs Fullscreen 조건부 렌더링 - -```javascript -// MediaPlayer.jsx:2415-2461 -{noSlider ? null : ( -
- {/* Times - 전체 시간 */} - {this.state.mediaSliderVisible && type ? ( - - ) : null} - - {/* Times - 현재 시간 */} - {this.state.mediaSliderVisible && type ? ( - - ) : null} - - {/* MediaSlider - modal이 아닐 때만 표시 */} - {!panelInfo.modal && ( - - )} -
-)} -``` - -**핵심 조건**: -```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 && ( -
- {/* Slider Section */} -
- {/* Times - 전체 시간 */} - - - {/* Times - 현재 시간 */} - - - {/* MediaSlider */} - -
- - {/* Controls Section */} -
- - - {onBackButton && ( - - )} -
-
-)} -``` - -### 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 모드에서만 조건부 숨김** diff --git a/.docs/MediaPlayer-v2-Risk-Analysis.md b/.docs/MediaPlayer-v2-Risk-Analysis.md deleted file mode 100644 index 3e5f3733..00000000 --- a/.docs/MediaPlayer-v2-Risk-Analysis.md +++ /dev/null @@ -1,789 +0,0 @@ -# MediaPlayer.v2 위험 분석 및 문제 발생 확률 - -**분석일**: 2025-11-10 -**대상 파일**: `src/components/VideoPlayer/MediaPlayer.v2.jsx` (586 lines) - ---- - -## 🎯 분석 방법론 - -각 위험 요소에 대해 다음 기준으로 확률 계산: - -``` -P(failure) = (1 - error_handling) × platform_dependency × complexity_factor - -error_handling: 0.0 (없음) ~ 1.0 (완벽) -platform_dependency: 1.0 (독립) ~ 2.0 (높은 의존) -complexity_factor: 1.0 (단순) ~ 1.5 (복잡) -``` - ---- - -## 🚨 High Risk Issues (확률 ≥ 20%) - -### 1. proportionLoaded 계산 실패 (TReactPlayer) -**위치**: MediaPlayer.v2.jsx:181 - -```javascript -setProportionLoaded(el.proportionLoaded || 0); -``` - -**문제**: -- `el.proportionLoaded`는 webOS Media 컴포넌트 전용 속성 -- TReactPlayer (브라우저/YouTube)에서는 **undefined** -- MediaSlider의 `backgroundProgress`가 항상 0으로 표시됨 - -**영향**: -- ❌ 로딩 진행 바(버퍼링 표시) 작동 안 함 -- ✅ 재생 자체는 정상 작동 (proportionPlayed는 별도 계산) - -**발생 조건**: -- 브라우저 환경 (!window.PalmSystem) -- YouTube URL 재생 -- videoComponent prop으로 TReactPlayer 전달 - -**확률 계산**: -``` -error_handling = 0.0 (fallback만 있고 실제 계산 없음) -platform_dependency = 1.8 (TReactPlayer에서 높은 확률로 발생) -complexity_factor = 1.0 - -P(failure) = (1 - 0.0) × 1.8 × 1.0 = 1.8 → 90% (매우 높음) -``` - -**실제 발생 확률**: **60%** (webOS에서는 정상, 브라우저/YouTube에서만 발생) - -**권장 수정**: -```javascript -// TReactPlayer에서는 buffered 사용 -const calculateProportionLoaded = useCallback(() => { - if (!videoRef.current) return 0; - - if (ActualVideoComponent === Media) { - return videoRef.current.proportionLoaded || 0; - } - - // TReactPlayer/HTMLVideoElement - const video = videoRef.current; - if (video.buffered && video.buffered.length > 0 && video.duration) { - return video.buffered.end(video.buffered.length - 1) / video.duration; - } - - return 0; -}, [ActualVideoComponent]); -``` - ---- - -### 2. seek() 호출 시 duration 미확정 상태 -**위치**: MediaPlayer.v2.jsx:258-265 - -```javascript -const seek = useCallback((timeIndex) => { - if (videoRef.current && !isNaN(videoRef.current.duration)) { - videoRef.current.currentTime = Math.min( - Math.max(0, timeIndex), - videoRef.current.duration - ); - } -}, []); -``` - -**문제**: -- `isNaN(videoRef.current.duration)` 체크만으로 불충분 -- `duration === Infinity` 상태 (라이브 스트림) -- `duration === 0` 상태 (메타데이터 로딩 전) - -**영향**: -- seek() 호출이 무시됨 (조용한 실패) -- 사용자는 MediaSlider를 움직여도 반응 없음 - -**발생 조건**: -- 비디오 로딩 초기 (loadedmetadata 이전) -- MediaSlider를 빠르게 조작 -- 라이브 스트림 URL - -**확률 계산**: -``` -error_handling = 0.6 (isNaN 체크는 있으나 edge case 미처리) -platform_dependency = 1.2 (모든 플랫폼에서 발생 가능) -complexity_factor = 1.2 (타이밍 이슈) - -P(failure) = (1 - 0.6) × 1.2 × 1.2 = 0.576 → 58% -``` - -**실제 발생 확률**: **25%** (빠른 조작 시, 라이브 스트림 제외) - -**권장 수정**: -```javascript -const seek = useCallback((timeIndex) => { - if (!videoRef.current) return; - - const video = videoRef.current; - const dur = video.duration; - - // duration 유효성 체크 강화 - if (isNaN(dur) || dur === 0 || dur === Infinity) { - console.warn('[MediaPlayer.v2] seek failed: invalid duration', dur); - return; - } - - video.currentTime = Math.min(Math.max(0, timeIndex), dur); -}, []); -``` - ---- - -### 3. DurationFmt 로딩 실패 (ilib 의존성) -**위치**: MediaPlayer.v2.jsx:42-53 - -```javascript -const memoGetDurFmt = memoize( - () => new DurationFmt({ - length: 'medium', - style: 'clock', - useNative: false, - }) -); - -const getDurFmt = () => { - if (typeof window === 'undefined') return null; - return memoGetDurFmt(); -}; -``` - -**문제**: -- `ilib/lib/DurationFmt` import 실패 시 런타임 에러 -- SSR 환경에서 `typeof window === 'undefined'`는 체크하지만 -- 브라우저에서 ilib이 없으면 **크래시** - -**영향**: -- ❌ Times 컴포넌트가 렌더링 실패 -- ❌ MediaPlayer.v2 전체가 렌더링 안 됨 - -**발생 조건**: -- ilib가 번들에 포함되지 않음 -- Webpack/Rollup 설정 오류 -- node_modules 누락 - -**확률 계산**: -``` -error_handling = 0.2 (null 반환만, try-catch 없음) -platform_dependency = 1.0 (라이브러리 의존) -complexity_factor = 1.1 (memoization) - -P(failure) = (1 - 0.2) × 1.0 × 1.1 = 0.88 → 88% -``` - -**실제 발생 확률**: **5%** (일반적으로 ilib는 프로젝트에 포함되어 있음) - -**권장 수정**: -```javascript -const getDurFmt = () => { - if (typeof window === 'undefined') return null; - - try { - return memoGetDurFmt(); - } catch (error) { - console.error('[MediaPlayer.v2] DurationFmt creation failed:', error); - return null; - } -}; - -// Times 렌더링에서 fallback - secondsToTime(time) }} - // ... -/> -``` - ---- - -## ⚠️ Medium Risk Issues (확률 10-20%) - -### 4. handleUpdate의 sourceUnavailable 상태 동기화 오류 -**위치**: MediaPlayer.v2.jsx:178 - -```javascript -setSourceUnavailable((el.loading && sourceUnavailable) || el.error); -``` - -**문제**: -- `sourceUnavailable`이 useCallback 의존성에 포함됨 (line 197) -- 상태 업데이트가 이전 상태에 의존 → **stale closure 위험** -- loading이 끝나도 sourceUnavailable이 true로 고정될 수 있음 - -**영향**: -- MediaSlider가 계속 disabled 상태 -- play/pause 버튼 작동 안 함 - -**발생 조건**: -- 네트워크 지연으로 loading이 길어짐 -- 여러 번 연속으로 src 변경 - -**확률 계산**: -``` -error_handling = 0.7 (로직은 있으나 의존성 이슈) -platform_dependency = 1.3 (모든 환경) -complexity_factor = 1.3 (상태 의존) - -P(failure) = (1 - 0.7) × 1.3 × 1.3 = 0.507 → 51% -``` - -**실제 발생 확률**: **15%** (특정 시나리오에서만) - -**권장 수정**: -```javascript -// sourceUnavailable을 의존성에서 제거하고 함수형 업데이트 사용 -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((prevUnavailable) => - (el.loading && prevUnavailable) || el.error - ); - - setProportionLoaded(el.proportionLoaded || 0); - setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0); - - // 콜백 호출 - if (ev.type === 'timeupdate' && onTimeUpdate) { - onTimeUpdate(ev); - } - if (ev.type === 'loadeddata' && onLoadedData) { - onLoadedData(ev); - } - if (ev.type === 'loadedmetadata' && onLoadedMetadata) { - onLoadedMetadata(ev); - } - if (ev.type === 'durationchange' && onDurationChange) { - onDurationChange(ev); - } -}, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange]); -// sourceUnavailable 제거! -``` - ---- - -### 5. Modal → Fullscreen 전환 시 controls 미표시 -**위치**: MediaPlayer.v2.jsx:327-336 - -```javascript -const prevModalRef = useRef(isModal); -useEffect(() => { - // Modal에서 Fullscreen으로 전환되었을 때 - if (prevModalRef.current && !isModal) { - if (videoRef.current?.paused) { - play(); - } - showControls(); - } - prevModalRef.current = isModal; -}, [isModal, play, showControls]); -``` - -**문제**: -- `showControls()`는 3초 타이머 설정 -- 사용자가 리모컨으로 아무것도 안 하면 **controls가 자동 사라짐** -- 전환 직후 사용자 경험 저하 - -**영향**: -- 전환 후 3초 뒤 controls 숨김 -- 사용자는 다시 Enter 키 눌러야 함 - -**발생 조건**: -- Modal → Fullscreen 전환 후 3초 이내 조작 없음 - -**확률 계산**: -``` -error_handling = 0.8 (의도된 동작이지만 UX 문제) -platform_dependency = 1.0 -complexity_factor = 1.0 - -P(failure) = (1 - 0.8) × 1.0 × 1.0 = 0.2 → 20% -``` - -**실제 발생 확률**: **20%** (UX 이슈지만 치명적이진 않음) - -**권장 수정**: -```javascript -// Fullscreen 전환 시 controls를 더 오래 표시 -const showControlsExtended = useCallback(() => { - setControlsVisible(true); - - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - - // Fullscreen 전환 시에는 10초로 연장 - controlsTimeoutRef.current = setTimeout(() => { - setControlsVisible(false); - }, 10000); -}, []); - -useEffect(() => { - if (prevModalRef.current && !isModal) { - if (videoRef.current?.paused) { - play(); - } - showControlsExtended(); // 연장 버전 사용 - } - prevModalRef.current = isModal; -}, [isModal, play, showControlsExtended]); -``` - ---- - -### 6. YouTube URL 감지 로직의 불완전성 -**위치**: MediaPlayer.v2.jsx:125-127 - -```javascript -const isYoutube = useMemo(() => { - return src && src.includes('youtu'); -}, [src]); -``` - -**문제**: -- `includes('youtu')` 검사가 너무 단순 -- 오탐: "my-youtube-tutorial.mp4" → true -- 미탐: "https://m.youtube.com" (드물지만 가능) - -**영향**: -- 일반 mp4 파일을 TReactPlayer로 재생 시도 -- 또는 YouTube를 Media로 재생 시도 (webOS에서 실패) - -**발생 조건**: -- 파일명에 'youtu' 포함 -- 비표준 YouTube URL - -**확률 계산**: -``` -error_handling = 0.4 (간단한 체크만) -platform_dependency = 1.2 -complexity_factor = 1.1 - -P(failure) = (1 - 0.4) × 1.2 × 1.1 = 0.792 → 79% -``` - -**실제 발생 확률**: **10%** (파일명 충돌은 드묾) - -**권장 수정**: -```javascript -const isYoutube = useMemo(() => { - if (!src) return false; - - try { - const url = new URL(src); - return ['youtube.com', 'youtu.be', 'm.youtube.com'].some(domain => - url.hostname.includes(domain) - ); - } catch { - // URL 파싱 실패 시 문자열 검사 - return /https?:\/\/(www\.|m\.)?youtu(\.be|be\.com)/.test(src); - } -}, [src]); -``` - ---- - -## 🟢 Low Risk Issues (확률 < 10%) - -### 7. controlsTimeoutRef 메모리 누수 -**위치**: MediaPlayer.v2.jsx:339-345 - -```javascript -useEffect(() => { - return () => { - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - }; -}, []); -``` - -**문제**: -- cleanup은 있지만 여러 경로에서 타이머 생성 -- `showControls()`, `hideControls()` 여러 번 호출 시 -- 이전 타이머가 쌓일 수 있음 - -**영향**: -- 메모리 누수 (매우 경미) -- controls 표시/숨김 타이밍 꼬임 - -**발생 조건**: -- 빠른 반복 조작 (Enter 키 연타) - -**확률 계산**: -``` -error_handling = 0.9 (cleanup 존재) -platform_dependency = 1.0 -complexity_factor = 1.0 - -P(failure) = (1 - 0.9) × 1.0 × 1.0 = 0.1 → 10% -``` - -**실제 발생 확률**: **5%** - -**현재 코드는 충분**: `showControls`에서 이미 `clearTimeout` 호출 중 - ---- - -### 8. SpotlightContainerDecorator defaultElement 오류 -**위치**: MediaPlayer.v2.jsx:33-39 - -```javascript -const RootContainer = SpotlightContainerDecorator( - { - enterTo: 'default-element', - defaultElement: [`.${css.controlsHandleAbove}`], - }, - 'div' -); -``` - -**문제**: -- `css.controlsHandleAbove`가 동적 생성 (CSS Modules) -- CSS 클래스명 변경 시 Spotlight 포커스 실패 - -**영향**: -- 리모컨으로 진입 시 포커스 안 잡힐 수 있음 - -**발생 조건**: -- CSS Modules 빌드 설정 변경 -- 클래스명 minification - -**확률 계산**: -``` -error_handling = 0.85 (Enact 기본 fallback 있음) -platform_dependency = 1.0 -complexity_factor = 1.0 - -P(failure) = (1 - 0.85) × 1.0 × 1.0 = 0.15 → 15% -``` - -**실제 발생 확률**: **3%** (빌드 설정이 안정적이면 문제없음) - -**권장 확인**: 빌드 후 실제 클래스명 확인 - ---- - -### 9. handleKnobMove 미구현 -**위치**: MediaPlayer.v2.jsx:286-294 - -```javascript -const handleKnobMove = useCallback((ev) => { - if (!videoRef.current) return; - - const seconds = Math.floor(ev.proportion * videoRef.current.duration); - if (!isNaN(seconds)) { - // Scrub 시 시간 표시 업데이트 - // 필요시 onScrub 콜백 호출 가능 - } -}, []); -``` - -**문제**: -- 주석만 있고 실제 구현 없음 -- Scrub 시 시간 표시 업데이트 안 됨 - -**영향**: -- UX 저하 (scrub 중 미리보기 시간 없음) -- 기능적으로는 정상 작동 (onChange가 실제 seek 담당) - -**발생 조건**: -- 항상 (구현 안 됨) - -**확률 계산**: -``` -error_handling = 1.0 (의도된 미구현) -platform_dependency = 1.0 -complexity_factor = 1.0 - -P(failure) = 0 (기능 누락이지 버그 아님) -``` - -**실제 발생 확률**: **0%** (선택 기능) - -**권장 추가** (선택): -```javascript -const [scrubTime, setScrubTime] = useState(null); - -const handleKnobMove = useCallback((ev) => { - if (!videoRef.current) return; - - const seconds = Math.floor(ev.proportion * videoRef.current.duration); - if (!isNaN(seconds)) { - setScrubTime(seconds); - } -}, []); - -// Times 렌더링 시 - -``` - ---- - -### 10. videoProps의 ActualVideoComponent 의존성 -**위치**: MediaPlayer.v2.jsx:360-397 - -```javascript -const videoProps = useMemo(() => { - const baseProps = { - ref: videoRef, - autoPlay: !paused, - loop, - muted, - onLoadStart: handleLoadStart, - onUpdate: handleUpdate, - onEnded: handleEnded, - onError: handleErrorEvent, - }; - - // webOS Media 컴포넌트 - if (ActualVideoComponent === Media) { - return { - ...baseProps, - className: css.media, - controls: false, - mediaComponent: 'video', - }; - } - - // ReactPlayer (브라우저 또는 YouTube) - if (ActualVideoComponent === TReactPlayer) { - return { - ...baseProps, - url: src, - playing: !paused, - width: '100%', - height: '100%', - videoRef: videoRef, - config: reactPlayerConfig, - }; - } - - return baseProps; -}, [ActualVideoComponent, src, paused, loop, muted, handleLoadStart, handleUpdate, handleEnded, handleErrorEvent, reactPlayerConfig]); -``` - -**문제**: -- Media와 TReactPlayer의 props 인터페이스가 다름 -- `ref` vs `videoRef` -- `autoPlay` vs `playing` -- 타입 불일치 가능성 - -**영향**: -- 컴포넌트 전환 시 props 미전달 -- ref 연결 실패 가능성 - -**발생 조건**: -- videoComponent prop으로 커스텀 컴포넌트 전달 -- 플랫폼 전환 테스트 (webOS ↔ 브라우저) - -**확률 계산**: -``` -error_handling = 0.8 (분기 처리 있음) -platform_dependency = 1.2 -complexity_factor = 1.2 - -P(failure) = (1 - 0.8) × 1.2 × 1.2 = 0.288 → 29% -``` - -**실제 발생 확률**: **8%** (기본 사용 시 문제없음) - -**권장 확인**: 각 컴포넌트의 ref 연결 테스트 - ---- - -## 📊 종합 위험도 평가 - -### 위험도별 요약 - -| 등급 | 확률 범위 | 문제 수 | 치명도 | 조치 필요성 | -|------|-----------|---------|--------|-------------| -| **High** | ≥ 20% | 3 | 중~고 | **즉시** | -| **Medium** | 10-20% | 3 | 중 | 단기 | -| **Low** | < 10% | 4 | 저 | 선택 | - -### High Risk 문제 (즉시 수정 권장) - -1. **proportionLoaded 계산 실패** (60%) - - 영향: 버퍼링 표시 안 됨 - - 치명도: 중 (재생 자체는 정상) - - 수정 난이도: 중 - -2. **seek() duration 미확정** (25%) - - 영향: 초기 seek 실패 - - 치명도: 중 (사용자 경험 저하) - - 수정 난이도: 쉬움 - -3. **DurationFmt 로딩 실패** (5%) - - 영향: 전체 크래시 - - 치명도: 고 (렌더링 실패) - - 수정 난이도: 쉬움 - -### 전체 치명적 실패 확률 - -``` -P(critical_failure) = P(DurationFmt 실패) = 5% - -P(기능_저하) = 1 - (1 - 0.60) × (1 - 0.25) × (1 - 0.15) × (1 - 0.20) - = 1 - 0.40 × 0.75 × 0.85 × 0.80 - = 1 - 0.204 - = 0.796 → 79.6% -``` - -**해석**: -- **치명적 실패 (크래시)**: 5% -- **기능 저하 (일부 작동 안 됨)**: 약 80% (하나 이상의 문제 발생) -- **완벽한 작동**: 약 20% - ---- - -## 🎯 우선순위별 수정 계획 - -### Phase 1: 치명적 버그 수정 (1-2시간) - -1. **DurationFmt try-catch 추가** (15분) - ```javascript - const getDurFmt = () => { - if (typeof window === 'undefined') return null; - try { - return memoGetDurFmt(); - } catch (error) { - console.error('[MediaPlayer.v2] DurationFmt failed:', error); - return { format: (time) => secondsToTime(time?.millisecond / 1000 || 0) }; - } - }; - ``` - -2. **seek() 검증 강화** (20분) - ```javascript - const seek = useCallback((timeIndex) => { - if (!videoRef.current) return; - - const video = videoRef.current; - const dur = video.duration; - - if (isNaN(dur) || dur === 0 || dur === Infinity) { - console.warn('[MediaPlayer.v2] seek failed: invalid duration', dur); - return; - } - - video.currentTime = Math.min(Math.max(0, timeIndex), dur); - }, []); - ``` - -3. **proportionLoaded 플랫폼별 계산** (30분) - ```javascript - const updateProportionLoaded = useCallback(() => { - if (!videoRef.current) return 0; - - if (ActualVideoComponent === Media) { - setProportionLoaded(videoRef.current.proportionLoaded || 0); - } else { - // TReactPlayer/HTMLVideoElement - const video = videoRef.current; - if (video.buffered?.length > 0 && video.duration) { - const loaded = video.buffered.end(video.buffered.length - 1) / video.duration; - setProportionLoaded(loaded); - } else { - setProportionLoaded(0); - } - } - }, [ActualVideoComponent]); - - // handleUpdate에서 호출 - useEffect(() => { - const interval = setInterval(updateProportionLoaded, 1000); - return () => clearInterval(interval); - }, [updateProportionLoaded]); - ``` - -### Phase 2: UX 개선 (2-3시간) - -4. **sourceUnavailable 함수형 업데이트** (15분) -5. **YouTube URL 정규식 검증** (15분) -6. **Modal 전환 시 controls 연장** (20분) - -### Phase 3: 선택적 기능 추가 (필요 시) - -7. handleKnobMove scrub 미리보기 -8. 더 상세한 에러 핸들링 - ---- - -## 🧪 테스트 케이스 - -수정 후 다음 시나리오 테스트 필수: - -### 필수 테스트 - -1. **webOS 네이티브** - - [ ] Modal 모드 → Fullscreen 전환 - - [ ] MediaSlider seek 동작 - - [ ] proportionLoaded 버퍼링 표시 - - [ ] Times 시간 포맷팅 - -2. **브라우저 (TReactPlayer)** - - [ ] mp4 재생 - - [ ] proportionLoaded 계산 (buffered API) - - [ ] seek 동작 - - [ ] Times fallback - -3. **YouTube** - - [ ] URL 감지 - - [ ] TReactPlayer 선택 - - [ ] 재생 제어 - -4. **에러 케이스** - - [ ] ilib 누락 시 fallback - - [ ] duration 로딩 전 seek - - [ ] 네트워크 끊김 시 sourceUnavailable - ---- - -## 📝 결론 - -### 현재 상태 - -**총평**: MediaPlayer.v2는 **프로토타입으로는 우수**하지만, **프로덕션 배포 전 수정 필수** - -### 주요 문제점 - -1. ✅ **구조적 설계**: 우수 (Modal/Fullscreen 분리, 상태 최소화) -2. ⚠️ **에러 핸들링**: 부족 (High Risk 3건) -3. ⚠️ **플랫폼 호환성**: 불완전 (proportionLoaded) -4. ✅ **성능 최적화**: 우수 (useMemo, useCallback) - -### 권장 조치 - -**최소 요구사항 (Phase 1)**: -- DurationFmt try-catch -- seek() 검증 강화 -- proportionLoaded 플랫폼별 계산 - -**완료 후 예상 안정성**: -- 치명적 실패: 5% → **0.1%** -- 기능 저하: 80% → **20%** -- 완벽한 작동: 20% → **80%** - -**예상 작업 시간**: 1-2시간 (Phase 1만) -**배포 가능 시점**: Phase 1 완료 후 + 테스트 2-3시간 - ---- - -**다음 단계**: Phase 1 수정 사항 구현 시작? diff --git a/.docs/PR-MediaPlayer-v2.md b/.docs/PR-MediaPlayer-v2.md deleted file mode 100644 index a456bfbe..00000000 --- a/.docs/PR-MediaPlayer-v2.md +++ /dev/null @@ -1,164 +0,0 @@ -# Pull Request: MediaPlayer.v2 Implementation - -**브랜치**: `claude/video-player-pane-011CUyjw9w5H9pPsrLk8NsZs` - -**제목**: feat: Implement optimized MediaPlayer.v2 for webOS with Phase 1 & 2 stability improvements - ---- - -## 🎯 Summary - -webOS 플랫폼을 위한 최적화된 비디오 플레이어 `MediaPlayer.v2.jsx` 구현 및 Phase 1, Phase 2 안정성 개선 완료. - -기존 MediaPlayer (2,595 lines)를 658 lines로 75% 축소하면서, Modal ↔ Fullscreen 전환 기능과 리모컨 제어를 완벽히 지원합니다. - ---- - -## 📊 성능 개선 결과 - -| 항목 | 기존 MediaPlayer | MediaPlayer.v2 | 개선율 | -|------|-----------------|---------------|--------| -| **코드 라인 수** | 2,595 | 658 | **-75%** | -| **상태 변수** | 20+ | 9 | **-55%** | -| **Job 타이머** | 8 | 1 | **-87%** | -| **Props** | 70+ | 25 | **-64%** | -| **안정성** | 20% | **95%** | **+375%** | - ---- - -## ✨ 주요 기능 - -### Core Features -- ✅ Modal (modal=true) 모드: 오버레이 없이 클릭만으로 확대 -- ✅ Fullscreen (modal=false) 모드: MediaSlider, Times, 버튼 등 완전한 컨트롤 제공 -- ✅ webOS Media 및 TReactPlayer 자동 감지 및 전환 -- ✅ YouTube URL 지원 (정규식 검증) -- ✅ Spotlight 리모컨 포커스 관리 - -### Phase 1 Critical Fixes (필수 수정) -1. **DurationFmt try-catch 추가** (실패: 5% → 0.1%) - - ilib 로딩 실패 시 fallback formatter 제공 - - 치명적 크래시 방지 - -2. **seek() duration 검증 강화** (실패: 25% → 5%) - - NaN, 0, Infinity 모두 체크 - - 비디오 로딩 초기 seek 실패 방지 - -3. **proportionLoaded 플랫폼별 계산** (실패: 60% → 5%) - - webOS Media: `proportionLoaded` 속성 사용 - - TReactPlayer: `buffered` API 사용 - - 1초마다 자동 업데이트 - -### Phase 2 Stability Improvements (안정성 향상) -4. **sourceUnavailable 함수형 업데이트** (실패: 15% → 3%) - - stale closure 버그 제거 - - 함수형 업데이트 패턴 적용 - -5. **YouTube URL 정규식 검증** (오탐: 10% → 2%) - - URL 객체로 hostname 파싱 - - 파일명 충돌 오탐 방지 - -6. **Modal 전환 시 controls 연장** (UX +20%) - - Fullscreen 전환 시 10초로 연장 표시 - - 리모컨 조작 준비 시간 제공 - ---- - -## 📁 변경 파일 - -### 신규 생성 -- `com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx` (658 lines) - -### 문서 추가 -- `.docs/video-player-analysis-and-optimization-plan.md` - 초기 분석 -- `.docs/modal-transition-analysis.md` - Modal 전환 메커니즘 분석 -- `.docs/MediaPlayer-v2-Required-Changes.md` - 필수 기능 명세 -- `.docs/MediaPlayer-v2-Risk-Analysis.md` - 위험 분석 및 확률 계산 - ---- - -## 🧪 안정성 평가 - -### 최종 결과 -- ✅ **완벽한 작동**: 95% (초기 20% → 95%) -- ⚠️ **기능 저하**: 5% (초기 80% → 5%) -- ❌ **치명적 실패**: 0.1% (초기 5% → 0.1%) - -### 개별 문제 해결 -| 문제 | 초기 확률 | **최종 확률** | 상태 | -|------|----------|-------------|------| -| proportionLoaded 실패 | 60% | **5%** | ✅ | -| seek() 실패 | 25% | **5%** | ✅ | -| DurationFmt 크래시 | 5% | **0.1%** | ✅ | -| sourceUnavailable 버그 | 15% | **3%** | ✅ | -| YouTube URL 오탐 | 10% | **2%** | ✅ | -| controls UX 저하 | 20% | **0%** | ✅ | - ---- - -## 🔧 기술 스택 - -- React Hooks (useState, useRef, useEffect, useCallback, useMemo, forwardRef) -- Enact Framework (Spotlight, SpotlightContainerDecorator) -- webOS Media Component -- react-player (TReactPlayer) -- ilib DurationFmt - ---- - -## 📝 커밋 히스토리 - -1. `de7c95e` docs: Add video player analysis and optimization documentation -2. `05e5458` feat: Implement optimized MediaPlayer.v2 for webOS -3. `64d1e55` docs: Add MediaPlayer.v2 required changes analysis -4. `726dcd9` feat: Add MediaSlider and Times to MediaPlayer.v2 -5. `a1dc79c` docs: Add MediaPlayer.v2 risk analysis and failure probability calculations -6. `10b6942` fix: Add Phase 1 critical fixes to MediaPlayer.v2 -7. `679c37a` feat: Add Phase 2 stability improvements to MediaPlayer.v2 - ---- - -## ✅ 테스트 권장사항 - -### 필수 테스트 -- [ ] webOS 네이티브: Modal → Fullscreen 전환 -- [ ] webOS 네이티브: MediaSlider seek 정확도 -- [ ] 브라우저: TReactPlayer buffered API 동작 -- [ ] YouTube: URL 감지 및 재생 -- [ ] 리모컨: Spotlight 포커스 이동 - -### 에러 케이스 -- [ ] ilib 없을 때 fallback -- [ ] duration 로딩 전 seek -- [ ] 네트워크 끊김 시 동작 - ---- - -## 🚀 배포 준비 상태 - -**프로덕션 배포 가능**: Phase 1 + Phase 2 완료로 95% 안정성 확보 - ---- - -## 📚 관련 이슈 - -webOS 비디오 플레이어 성능 개선 및 메모리 최적화 요청 - ---- - -## 🔍 Review Points - -- MediaPlayer.v2.jsx의 Modal/Fullscreen 로직 확인 -- proportionLoaded 플랫폼별 계산 검증 -- Phase 1/2 수정사항 확인 -- 리모컨 Spotlight 포커스 동작 확인 -- 메모리 사용량 개선 검증 - ---- - -## 🎬 다음 단계 - -1. PR 리뷰 및 머지 -2. MediaPanel에 MediaPlayer.v2 통합 -3. webOS 디바이스 테스트 -4. 성능 벤치마크 diff --git a/.docs/dispatch-async/01-problem.md b/.docs/dispatch-async/01-problem.md deleted file mode 100644 index ebae1fdb..00000000 --- a/.docs/dispatch-async/01-problem.md +++ /dev/null @@ -1,210 +0,0 @@ -# 문제 상황: Dispatch 비동기 순서 미보장 - -## 🔴 핵심 문제 - -Redux-thunk는 비동기 액션을 지원하지만, **여러 개의 dispatch를 순차적으로 호출할 때 실행 순서가 보장되지 않습니다.** - -## 📝 기존 코드의 문제점 - -### 예제 1: homeActions.js - -**파일**: `src/actions/homeActions.js` - -```javascript -export const getHomeTerms = (props) => (dispatch, getState) => { - const onSuccess = (response) => { - if (response.data.retCode === 0) { - // 첫 번째 dispatch - dispatch({ - type: types.GET_HOME_TERMS, - payload: response.data, - }); - - // 두 번째 dispatch - dispatch({ - type: types.SET_TERMS_ID_MAP, - payload: termsIdMap, - }); - - // ⚠️ 문제: setTimeout으로 순서 보장 시도 - setTimeout(() => { - dispatch(getTermsAgreeYn()); - }, 0); - } - }; - - TAxios(dispatch, getState, "get", URLS.GET_HOME_TERMS, ..., onSuccess, onFail); -}; -``` - -**문제점**: -1. `setTimeout(fn, 0)`은 임시방편일 뿐, 명확한 해결책이 아님 -2. 코드 가독성이 떨어짐 -3. 타이밍 이슈로 인한 버그 가능성 -4. 유지보수가 어려움 - -### 예제 2: cartActions.js - -**파일**: `src/actions/cartActions.js` - -```javascript -export const addToCart = (props) => (dispatch, getState) => { - const onSuccess = (response) => { - // 첫 번째 dispatch: 카트에 추가 - dispatch({ - type: types.ADD_TO_CART, - payload: response.data.data, - }); - - // 두 번째 dispatch: 카트 정보 재조회 - // ⚠️ 문제: 순서가 보장되지 않음 - dispatch(getMyInfoCartSearch({ mbrNo })); - }; - - TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, ..., onSuccess, onFail); -}; -``` - -**문제점**: -1. `getMyInfoCartSearch`가 `ADD_TO_CART`보다 먼저 실행될 수 있음 -2. 카트 정보가 업데이트되기 전에 재조회가 실행될 수 있음 -3. 순서가 보장되지 않아 UI에 잘못된 데이터가 표시될 수 있음 - -## 🤔 왜 순서가 보장되지 않을까? - -### Redux-thunk의 동작 방식 - -```javascript -// Redux-thunk는 이렇게 동작합니다 -function dispatch(action) { - if (typeof action === 'function') { - // thunk action인 경우 - return action(dispatch, getState); - } else { - // plain action인 경우 - return next(action); - } -} -``` - -### 문제 시나리오 - -```javascript -// 이렇게 작성하면 -dispatch({ type: 'ACTION_1' }); // Plain action - 즉시 실행 -dispatch(asyncAction()); // Thunk - 비동기 실행 -dispatch({ type: 'ACTION_2' }); // Plain action - 즉시 실행 - -// 실제 실행 순서는 -// 1. ACTION_1 (동기) -// 2. ACTION_2 (동기) -// 3. asyncAction의 내부 dispatch들 (비동기) - -// 즉, asyncAction이 완료되기 전에 ACTION_2가 실행됩니다! -``` - -## 🎯 해결해야 할 과제 - -1. **순서 보장**: 여러 dispatch가 의도한 순서대로 실행되도록 -2. **에러 처리**: 중간에 에러가 발생해도 체인이 끊기지 않도록 -3. **가독성**: 코드가 직관적이고 유지보수하기 쉽도록 -4. **재사용성**: 여러 곳에서 쉽게 사용할 수 있도록 -5. **호환성**: 기존 코드와 호환되도록 - -## 📊 실제 발생 가능한 버그 - -### 시나리오 1: 카트 추가 후 조회 - -```javascript -// 의도한 순서 -1. ADD_TO_CART dispatch -2. 상태 업데이트 -3. getMyInfoCartSearch dispatch -4. 최신 카트 정보 조회 - -// 실제 실행 순서 (문제) -1. ADD_TO_CART dispatch -2. getMyInfoCartSearch dispatch (너무 빨리 실행!) -3. 이전 카트 정보 조회 (아직 상태 업데이트 안됨) -4. 상태 업데이트 -→ 결과: UI에 이전 데이터가 표시됨 -``` - -### 시나리오 2: 패널 열고 닫기 - -```javascript -// 의도한 순서 -1. PUSH_PANEL (검색 패널 열기) -2. UPDATE_PANEL (검색 결과 표시) -3. POP_PANEL (이전 패널 닫기) - -// 실제 실행 순서 (문제) -1. PUSH_PANEL -2. POP_PANEL (너무 빨리 실행!) -3. UPDATE_PANEL (이미 닫힌 패널을 업데이트) -→ 결과: 패널이 제대로 표시되지 않음 -``` - -## 🔧 기존 해결 방법과 한계 - -### 방법 1: setTimeout 사용 - -```javascript -dispatch(action1()); -setTimeout(() => { - dispatch(action2()); -}, 0); -``` - -**한계**: -- 명확한 순서 보장 없음 -- 타이밍에 의존적 -- 코드 가독성 저하 -- 유지보수 어려움 - -### 방법 2: 콜백 중첩 - -```javascript -const action1 = (callback) => (dispatch, getState) => { - dispatch({ type: 'ACTION_1' }); - if (callback) callback(); -}; - -dispatch(action1(() => { - dispatch(action2(() => { - dispatch(action3()); - })); -})); -``` - -**한계**: -- 콜백 지옥 -- 에러 처리 복잡 -- 코드 가독성 최악 - -### 방법 3: async/await - -```javascript -export const complexAction = () => async (dispatch, getState) => { - await dispatch(action1()); - await dispatch(action2()); - await dispatch(action3()); -}; -``` - -**한계**: -- Chrome 68 호환성 문제 (프로젝트 요구사항) -- 모든 action이 Promise를 반환해야 함 -- 기존 코드 대량 수정 필요 - -## 🎯 다음 단계 - -이제 이러한 문제들을 해결하기 위한 3가지 솔루션을 살펴보겠습니다: - -1. [dispatchHelper.js](./02-solution-dispatch-helper.md) - Promise 체인 기반 헬퍼 함수 -2. [asyncActionUtils.js](./03-solution-async-utils.md) - Promise 기반 비동기 처리 유틸리티 -3. [큐 기반 패널 액션 시스템](./04-solution-queue-system.md) - 미들웨어 기반 큐 시스템 - ---- - -**다음**: [해결 방법 1: dispatchHelper.js →](./02-solution-dispatch-helper.md) diff --git a/.docs/dispatch-async/02-solution-dispatch-helper.md b/.docs/dispatch-async/02-solution-dispatch-helper.md deleted file mode 100644 index 54496858..00000000 --- a/.docs/dispatch-async/02-solution-dispatch-helper.md +++ /dev/null @@ -1,541 +0,0 @@ -# 해결 방법 1: dispatchHelper.js - -## 📦 개요 - -**파일**: `src/utils/dispatchHelper.js` -**작성일**: 2025-11-05 -**커밋**: `9490d72 [251105] feat: dispatchHelper.js` - -Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음입니다. - -## 🎯 핵심 함수 - -1. `createSequentialDispatch` - 순차적 dispatch 실행 -2. `createApiThunkWithChain` - API 후 dispatch 자동 체이닝 -3. `withLoadingState` - 로딩 상태 자동 관리 -4. `createConditionalDispatch` - 조건부 dispatch -5. `createParallelDispatch` - 병렬 dispatch - ---- - -## 1️⃣ createSequentialDispatch - -### 설명 - -여러 dispatch를 **Promise 체인**을 사용하여 순차적으로 실행합니다. - -### 사용법 - -```javascript -import { createSequentialDispatch } from '../utils/dispatchHelper'; - -// 기본 사용 -dispatch(createSequentialDispatch([ - { type: types.SET_LOADING, payload: true }, - { type: types.UPDATE_DATA, payload: data }, - { type: types.SET_LOADING, payload: false } -])); - -// thunk와 plain action 혼합 -dispatch(createSequentialDispatch([ - { type: types.GET_HOME_TERMS, payload: response.data }, - { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, - getTermsAgreeYn() // thunk action -])); - -// 옵션 사용 -dispatch(createSequentialDispatch([ - fetchUserData(), - fetchCartData(), - fetchOrderData() -], { - delay: 100, // 각 dispatch 간 100ms 지연 - stopOnError: true // 에러 발생 시 중단 -})); -``` - -### Before & After - -#### Before (setTimeout 방식) - -```javascript -const onSuccess = (response) => { - dispatch({ type: types.GET_HOME_TERMS, payload: response.data }); - dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap }); - setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0); -}; -``` - -#### After (createSequentialDispatch) - -```javascript -const onSuccess = (response) => { - dispatch(createSequentialDispatch([ - { type: types.GET_HOME_TERMS, payload: response.data }, - { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, - getTermsAgreeYn() - ])); -}; -``` - -### 구현 원리 - -**파일**: `src/utils/dispatchHelper.js:96-129` - -```javascript -export const createSequentialDispatch = (dispatchActions, options) => - (dispatch, getState) => { - const config = options || {}; - const delay = config.delay || 0; - const stopOnError = config.stopOnError !== undefined ? config.stopOnError : false; - - // Promise 체인으로 순차 실행 - return dispatchActions.reduce( - (promise, action, index) => { - return promise - .then(() => { - // delay가 설정되어 있고 첫 번째가 아닌 경우 지연 - if (delay > 0 && index > 0) { - return new Promise((resolve) => setTimeout(resolve, delay)); - } - return Promise.resolve(); - }) - .then(() => { - // action 실행 - const result = dispatch(action); - - // Promise인 경우 대기 - if (result && typeof result.then === 'function') { - return result; - } - return Promise.resolve(result); - }) - .catch((error) => { - console.error('createSequentialDispatch error at index', index, error); - - // stopOnError가 true면 에러를 다시 throw - if (stopOnError) { - throw error; - } - - // stopOnError가 false면 계속 진행 - return Promise.resolve(); - }); - }, - Promise.resolve() - ); - }; -``` - -**핵심 포인트**: -1. `Array.reduce()`로 Promise 체인 구성 -2. 각 action이 완료되면 다음 action 실행 -3. thunk가 Promise를 반환하면 대기 -4. 에러 처리 옵션 지원 - ---- - -## 2️⃣ createApiThunkWithChain - -### 설명 - -API 호출 후 성공 콜백에서 여러 dispatch를 자동으로 체이닝합니다. -TAxios의 onSuccess/onFail 패턴과 완벽하게 호환됩니다. - -### 사용법 - -```javascript -import { createApiThunkWithChain } from '../utils/dispatchHelper'; - -// 기본 사용 -export const addToCart = (props) => - createApiThunkWithChain( - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'post', URLS.ADD_TO_CART, {}, props, onSuccess, onFail); - }, - [ - (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }), - (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo }) - ] - ); - -// 에러 처리 포함 -export const registerDevice = (params) => - createApiThunkWithChain( - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'post', URLS.REGISTER_DEVICE, {}, params, onSuccess, onFail); - }, - [ - (response) => ({ type: types.REGISTER_DEVICE, payload: response.data.data }), - getAuthenticationCode(), - fetchCurrentUserHomeTerms() - ], - (error) => ({ type: types.API_ERROR, payload: error }) - ); -``` - -### Before & After - -#### Before - -```javascript -export const addToCart = (props) => (dispatch, getState) => { - const onSuccess = (response) => { - dispatch({ type: types.ADD_TO_CART, payload: response.data.data }); - dispatch(getMyInfoCartSearch({ mbrNo })); - }; - - const onFail = (error) => { - console.error(error); - }; - - TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, {}, props, onSuccess, onFail); -}; -``` - -#### After - -```javascript -export const addToCart = (props) => - createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF), - [ - (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }), - (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo }) - ] - ); -``` - -### 구현 원리 - -**파일**: `src/utils/dispatchHelper.js:170-211` - -```javascript -export const createApiThunkWithChain = ( - apiCallFactory, - successDispatchActions, - errorDispatch -) => (dispatch, getState) => { - const actions = successDispatchActions || []; - - const enhancedOnSuccess = (response) => { - // 성공 시 순차적으로 dispatch 실행 - actions.forEach((action, index) => { - setTimeout(() => { - if (typeof action === 'function') { - // action이 함수인 경우 (동적 action creator) - // response를 인자로 전달하여 실행 - const dispatchAction = action(response); - dispatch(dispatchAction); - } else { - // action이 객체인 경우 (plain action) - dispatch(action); - } - }, 0); - }); - }; - - const enhancedOnFail = (error) => { - console.error('createApiThunkWithChain error:', error); - - if (errorDispatch) { - if (typeof errorDispatch === 'function') { - const dispatchAction = errorDispatch(error); - dispatch(dispatchAction); - } else { - dispatch(errorDispatch); - } - } - }; - - // API 호출 실행 - return apiCallFactory(dispatch, getState, enhancedOnSuccess, enhancedOnFail); -}; -``` - -**핵심 포인트**: -1. API 호출의 onSuccess/onFail 콜백을 래핑 -2. 성공 시 여러 action을 순차 실행 -3. response를 각 action에 전달 가능 -4. 에러 처리 action 지원 - ---- - -## 3️⃣ withLoadingState - -### 설명 - -API 호출 thunk의 로딩 상태를 자동으로 관리합니다. -`changeAppStatus`로 `showLoadingPanel`을 자동 on/off합니다. - -### 사용법 - -```javascript -import { withLoadingState } from '../utils/dispatchHelper'; - -// 기본 로딩 관리 -export const getProductDetail = (props) => - withLoadingState( - (dispatch, getState) => { - return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}) - .then((response) => { - dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); - }); - } - ); - -// 성공/에러 시 추가 dispatch -export const fetchUserData = (userId) => - withLoadingState( - fetchUser(userId), - { - loadingType: 'spinner', - successDispatch: [ - fetchCart(userId), - fetchOrders(userId) - ], - errorDispatch: [ - (error) => ({ type: types.SHOW_ERROR_MESSAGE, payload: error.message }) - ] - } - ); -``` - -### Before & After - -#### Before - -```javascript -export const getProductDetail = (props) => (dispatch, getState) => { - // 로딩 시작 - dispatch(changeAppStatus({ showLoadingPanel: { show: true } })); - - const onSuccess = (response) => { - dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); - // 로딩 종료 - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - }; - - const onFail = (error) => { - console.error(error); - // 로딩 종료 - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - }; - - TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}, onSuccess, onFail); -}; -``` - -#### After - -```javascript -export const getProductDetail = (props) => - withLoadingState( - (dispatch, getState) => { - return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}) - .then((response) => { - dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); - }); - } - ); -``` - -### 구현 원리 - -**파일**: `src/utils/dispatchHelper.js:252-302` - -```javascript -export const withLoadingState = (thunk, options) => (dispatch, getState) => { - const config = options || {}; - const loadingType = config.loadingType || 'wait'; - const successDispatch = config.successDispatch || []; - const errorDispatch = config.errorDispatch || []; - - // 로딩 시작 - dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: loadingType } })); - - // thunk 실행 - const result = dispatch(thunk); - - // Promise인 경우 처리 - if (result && typeof result.then === 'function') { - return result - .then((res) => { - // 로딩 종료 - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - - // 성공 시 추가 dispatch 실행 - successDispatch.forEach((action) => { - if (typeof action === 'function') { - dispatch(action(res)); - } else { - dispatch(action); - } - }); - - return res; - }) - .catch((error) => { - // 로딩 종료 - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - - // 에러 시 추가 dispatch 실행 - errorDispatch.forEach((action) => { - if (typeof action === 'function') { - dispatch(action(error)); - } else { - dispatch(action); - } - }); - - throw error; - }); - } - - // 동기 실행인 경우 - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - return result; -}; -``` - -**핵심 포인트**: -1. 로딩 시작/종료를 자동 관리 -2. Promise 기반 thunk만 지원 -3. 성공/실패 시 추가 action 실행 가능 -4. 에러 발생 시에도 로딩 상태 복원 - ---- - -## 4️⃣ createConditionalDispatch - -### 설명 - -getState() 결과를 기반으로 조건에 따라 다른 dispatch를 실행합니다. - -### 사용법 - -```javascript -import { createConditionalDispatch } from '../utils/dispatchHelper'; - -// 단일 action 조건부 실행 -dispatch(createConditionalDispatch( - (state) => state.common.appStatus.isAlarmEnabled === 'Y', - addReservation(reservationData), - deleteReservation(showId) -)); - -// 여러 action 배열로 실행 -dispatch(createConditionalDispatch( - (state) => state.common.appStatus.loginUserData.userNumber, - [ - fetchUserProfile(), - fetchUserCart(), - fetchUserOrders() - ], - [ - { type: types.SHOW_LOGIN_REQUIRED_POPUP } - ] -)); - -// false 조건 없이 -dispatch(createConditionalDispatch( - (state) => state.cart.items.length > 0, - proceedToCheckout() -)); -``` - ---- - -## 5️⃣ createParallelDispatch - -### 설명 - -여러 API 호출을 병렬로 실행하고 모든 결과를 기다립니다. -`Promise.all`을 사용합니다. - -### 사용법 - -```javascript -import { createParallelDispatch } from '../utils/dispatchHelper'; - -// 여러 API를 동시에 호출 -dispatch(createParallelDispatch([ - fetchUserProfile(), - fetchUserCart(), - fetchUserOrders() -], { withLoading: true })); -``` - ---- - -## 📊 실제 사용 예제 - -### homeActions.js 개선 - -```javascript -// Before -export const getHomeTerms = (props) => (dispatch, getState) => { - const onSuccess = (response) => { - if (response.data.retCode === 0) { - dispatch({ type: types.GET_HOME_TERMS, payload: response.data }); - dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap }); - setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0); - } - }; - TAxios(dispatch, getState, "get", URLS.GET_HOME_TERMS, ..., onSuccess, onFail); -}; - -// After -export const getHomeTerms = (props) => - createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, "get", URLS.GET_HOME_TERMS, ..., onS, onF), - [ - { type: types.GET_HOME_TERMS, payload: response.data }, - { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, - getTermsAgreeYn() - ] - ); -``` - -### cartActions.js 개선 - -```javascript -// Before -export const addToCart = (props) => (dispatch, getState) => { - const onSuccess = (response) => { - dispatch({ type: types.ADD_TO_CART, payload: response.data.data }); - dispatch(getMyInfoCartSearch({ mbrNo })); - }; - TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, ..., onSuccess, onFail); -}; - -// After -export const addToCart = (props) => - createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF), - [ - (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }), - (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo }) - ] - ); -``` - ---- - -## ✅ 장점 - -1. **간결성**: setTimeout 제거로 코드가 깔끔해짐 -2. **가독성**: 의도가 명확하게 드러남 -3. **재사용성**: 헬퍼 함수를 여러 곳에서 사용 가능 -4. **에러 처리**: 옵션으로 에러 처리 전략 선택 가능 -5. **호환성**: 기존 코드와 호환 (선택적 사용) - -## ⚠️ 주의사항 - -1. **Promise 기반**: 모든 함수가 Promise를 반환하도록 설계됨 -2. **Chrome 68**: async/await 없이 Promise.then() 사용 -3. **기존 패턴**: TAxios의 onSuccess/onFail 패턴 유지 - ---- - -**다음**: [해결 방법 2: asyncActionUtils.js →](./03-solution-async-utils.md) diff --git a/.docs/dispatch-async/03-solution-async-utils.md b/.docs/dispatch-async/03-solution-async-utils.md deleted file mode 100644 index dadd1183..00000000 --- a/.docs/dispatch-async/03-solution-async-utils.md +++ /dev/null @@ -1,711 +0,0 @@ -# 해결 방법 2: asyncActionUtils.js - -## 📦 개요 - -**파일**: `src/utils/asyncActionUtils.js` -**작성일**: 2025-11-06 -**커밋**: `f9290a1 [251106] fix: Dispatch Queue implementation` - -Promise 기반의 비동기 액션 처리와 **상세한 성공/실패 기준**을 제공합니다. - -## 🎯 핵심 개념 - -### 프로젝트 특화 성공 기준 - -이 프로젝트에서 API 호출 성공은 **2가지 조건**을 모두 만족해야 합니다: - -1. ✅ **HTTP 상태 코드**: 200-299 범위 -2. ✅ **retCode**: 0 또는 '0' - -```javascript -// HTTP 200이지만 retCode가 1인 경우 -{ - status: 200, // ✅ HTTP는 성공 - data: { - retCode: 1, // ❌ retCode는 실패 - message: "권한이 없습니다" - } -} -// → 이것은 실패입니다! -``` - -### Promise 체인이 끊기지 않는 설계 - -**핵심 원칙**: 모든 비동기 작업은 **reject 없이 resolve만 사용**합니다. - -```javascript -// ❌ 일반적인 방식 (Promise 체인이 끊김) -return new Promise((resolve, reject) => { - if (error) { - reject(error); // 체인이 끊김! - } -}); - -// ✅ 이 프로젝트의 방식 (체인 유지) -return new Promise((resolve) => { - if (error) { - resolve({ - success: false, - error: { code: 'ERROR', message: '에러 발생' } - }); - } -}); -``` - ---- - -## 🔑 핵심 함수 - -1. `isApiSuccess` - API 성공 여부 판단 -2. `fetchApi` - Promise 기반 fetch 래퍼 -3. `tAxiosToPromise` - TAxios를 Promise로 변환 -4. `wrapAsyncAction` - 비동기 액션을 Promise로 래핑 -5. `withTimeout` - 타임아웃 지원 -6. `executeParallelAsyncActions` - 병렬 실행 - ---- - -## 1️⃣ isApiSuccess - -### 설명 - -API 응답이 성공인지 판단하는 **프로젝트 표준 함수**입니다. - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:21-34` - -```javascript -export const isApiSuccess = (response, responseData) => { - // 1️⃣ HTTP 상태 코드 확인 (200-299 성공 범위) - if (!response.ok || response.status < 200 || response.status >= 300) { - return false; - } - - // 2️⃣ retCode 확인 - 0 또는 '0'이어야 성공 - if (responseData && responseData.retCode !== undefined) { - return responseData.retCode === 0 || responseData.retCode === '0'; - } - - // retCode가 없는 경우 HTTP 상태 코드만으로 판단 - return response.ok; -}; -``` - -### 사용 예제 - -```javascript -// 성공 케이스 -isApiSuccess( - { ok: true, status: 200 }, - { retCode: 0, data: { ... } } -); // → true - -isApiSuccess( - { ok: true, status: 200 }, - { retCode: '0', data: { ... } } -); // → true - -// 실패 케이스 -isApiSuccess( - { ok: true, status: 200 }, - { retCode: 1, message: "권한 없음" } -); // → false (retCode가 0이 아님) - -isApiSuccess( - { ok: false, status: 500 }, - { retCode: 0, data: { ... } } -); // → false (HTTP 상태 코드가 500) - -isApiSuccess( - { ok: false, status: 404 }, - { retCode: 0 } -); // → false (404 에러) -``` - ---- - -## 2️⃣ fetchApi - -### 설명 - -**표준 fetch API를 Promise로 래핑**하여 프로젝트 성공 기준에 맞춰 처리합니다. - -### 핵심 특징 - -- ✅ 항상 `resolve` 사용 (reject 없음) -- ✅ HTTP 상태 + retCode 모두 확인 -- ✅ JSON 파싱 에러도 처리 -- ✅ 네트워크 에러도 처리 -- ✅ 상세한 로깅 - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:57-123` - -```javascript -export const fetchApi = (url, options = {}) => { - console.log('[asyncActionUtils] 🌐 FETCH_API_START', { url, method: options.method || 'GET' }); - - return new Promise((resolve) => { // ⚠️ 항상 resolve만 사용! - fetch(url, { - headers: { - 'Content-Type': 'application/json', - ...options.headers - }, - ...options - }) - .then(response => { - // JSON 파싱 - return response.json() - .then(responseData => { - console.log('[asyncActionUtils] 📊 API_RESPONSE', { - status: response.status, - ok: response.ok, - retCode: responseData.retCode, - success: isApiSuccess(response, responseData) - }); - - // ✅ 성공/실패 여부와 관계없이 항상 resolve - resolve({ - response, - data: responseData, - success: isApiSuccess(response, responseData), - error: !isApiSuccess(response, responseData) ? { - code: responseData.retCode || response.status, - message: responseData.message || getApiErrorMessage(responseData.retCode || response.status), - httpStatus: response.status - } : null - }); - }) - .catch(parseError => { - console.error('[asyncActionUtils] ❌ JSON_PARSE_ERROR', parseError); - - // ✅ JSON 파싱 실패도 resolve로 처리 - resolve({ - response, - data: null, - success: false, - error: { - code: 'PARSE_ERROR', - message: '응답 데이터 파싱에 실패했습니다', - originalError: parseError - } - }); - }); - }) - .catch(error => { - console.error('[asyncActionUtils] 💥 FETCH_ERROR', error); - - // ✅ 네트워크 에러 등도 resolve로 처리 - resolve({ - response: null, - data: null, - success: false, - error: { - code: 'NETWORK_ERROR', - message: error.message || '네트워크 오류가 발생했습니다', - originalError: error - } - }); - }); - }); -}; -``` - -### 사용 예제 - -```javascript -import { fetchApi } from '../utils/asyncActionUtils'; - -// 기본 사용 -const result = await fetchApi('/api/products/123', { - method: 'GET' -}); - -if (result.success) { - console.log('성공:', result.data); - // HTTP 200-299 + retCode 0/'0' -} else { - console.error('실패:', result.error); - // error.code, error.message 사용 가능 -} - -// POST 요청 -const result = await fetchApi('/api/cart', { - method: 'POST', - body: JSON.stringify({ productId: 123 }) -}); - -// 헤더 추가 -const result = await fetchApi('/api/user', { - method: 'GET', - headers: { - 'Authorization': 'Bearer token123' - } -}); -``` - -### 반환 구조 - -```javascript -// 성공 시 -{ - response: Response, // fetch Response 객체 - data: { ... }, // 파싱된 JSON 데이터 - success: true, // 성공 플래그 - error: null // 에러 없음 -} - -// 실패 시 (HTTP 에러) -{ - response: Response, - data: { retCode: 1, message: "권한 없음" }, - success: false, - error: { - code: 1, - message: "권한 없음", - httpStatus: 200 - } -} - -// 실패 시 (네트워크 에러) -{ - response: null, - data: null, - success: false, - error: { - code: 'NETWORK_ERROR', - message: '네트워크 오류가 발생했습니다', - originalError: Error - } -} -``` - ---- - -## 3️⃣ tAxiosToPromise - -### 설명 - -프로젝트에서 사용하는 **TAxios를 Promise로 변환**합니다. - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:138-204` - -```javascript -export const tAxiosToPromise = ( - TAxios, - dispatch, - getState, - method, - baseUrl, - urlParams, - params, - options = {} -) => { - return new Promise((resolve) => { - console.log('[asyncActionUtils] 🔄 TAXIOS_TO_PROMISE_START', { method, baseUrl }); - - const enhancedOnSuccess = (response) => { - console.log('[asyncActionUtils] ✅ TAXIOS_SUCCESS', { retCode: response?.data?.retCode }); - - // TAxios 성공 콜백도 성공 기준 적용 - const isSuccess = response?.data && ( - response.data.retCode === 0 || - response.data.retCode === '0' - ); - - resolve({ - response, - data: response.data, - success: isSuccess, - error: !isSuccess ? { - code: response.data?.retCode || 'UNKNOWN_ERROR', - message: response.data?.message || getApiErrorMessage(response.data?.retCode || 'UNKNOWN_ERROR') - } : null - }); - }; - - const enhancedOnFail = (error) => { - console.error('[asyncActionUtils] ❌ TAXIOS_FAIL', error); - - resolve({ // ⚠️ reject가 아닌 resolve - response: null, - data: null, - success: false, - error: { - code: error.retCode || 'TAXIOS_ERROR', - message: error.message || 'API 호출에 실패했습니다', - originalError: error - } - }); - }; - - try { - TAxios( - dispatch, - getState, - method, - baseUrl, - urlParams, - params, - enhancedOnSuccess, - enhancedOnFail, - options.noTokenRefresh || false, - options.responseType - ); - } catch (error) { - console.error('[asyncActionUtils] 💥 TAXIOS_EXECUTION_ERROR', error); - - resolve({ - response: null, - data: null, - success: false, - error: { - code: 'EXECUTION_ERROR', - message: 'API 호출 실행 중 오류가 발생했습니다', - originalError: error - } - }); - } - }); -}; -``` - -### 사용 예제 - -```javascript -import { tAxiosToPromise } from '../utils/asyncActionUtils'; -import { TAxios } from '../utils/TAxios'; - -export const getProductDetail = (productId) => async (dispatch, getState) => { - const result = await tAxiosToPromise( - TAxios, - dispatch, - getState, - 'get', - URLS.GET_PRODUCT_DETAIL, - {}, - { productId }, - {} - ); - - if (result.success) { - dispatch({ - type: types.GET_PRODUCT_DETAIL, - payload: result.data.data - }); - } else { - console.error('상품 조회 실패:', result.error); - } -}; -``` - ---- - -## 4️⃣ wrapAsyncAction - -### 설명 - -비동기 액션 함수를 Promise로 래핑하여 **표준화된 결과 구조**를 반환합니다. - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:215-270` - -```javascript -export const wrapAsyncAction = (asyncAction, context = {}) => { - return new Promise((resolve) => { - const { dispatch, getState } = context; - - console.log('[asyncActionUtils] 🎯 WRAP_ASYNC_ACTION_START'); - - // 성공 콜백 - 항상 resolve 호출 - const onSuccess = (result) => { - console.log('[asyncActionUtils] ✅ WRAP_ASYNC_SUCCESS', result); - - resolve({ - response: result.response || result, - data: result.data || result, - success: true, - error: null - }); - }; - - // 실패 콜백 - 항상 resolve 호출 (reject 하지 않음) - const onFail = (error) => { - console.error('[asyncActionUtils] ❌ WRAP_ASYNC_FAIL', error); - - resolve({ - response: null, - data: null, - success: false, - error: { - code: error.retCode || error.code || 'ASYNC_ACTION_ERROR', - message: error.message || error.errorMessage || '비동기 작업에 실패했습니다', - originalError: error - } - }); - }; - - try { - // 비동기 액션 실행 - const result = asyncAction(dispatch, getState, onSuccess, onFail); - - // Promise를 반환하는 경우도 처리 - if (result && typeof result.then === 'function') { - result - .then(onSuccess) - .catch(onFail); - } - } catch (error) { - console.error('[asyncActionUtils] 💥 WRAP_ASYNC_EXECUTION_ERROR', error); - onFail(error); - } - }); -}; -``` - -### 사용 예제 - -```javascript -import { wrapAsyncAction } from '../utils/asyncActionUtils'; - -// 비동기 액션 정의 -const myAsyncAction = (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URL, {}, {}, onSuccess, onFail); -}; - -// Promise로 래핑하여 사용 -const result = await wrapAsyncAction(myAsyncAction, { dispatch, getState }); - -if (result.success) { - console.log('성공:', result.data); -} else { - console.error('실패:', result.error.message); -} -``` - ---- - -## 5️⃣ withTimeout - -### 설명 - -Promise에 **타임아웃**을 적용합니다. - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:354-373` - -```javascript -export const withTimeout = ( - promise, - timeoutMs, - timeoutMessage = '작업 시간이 초과되었습니다' -) => { - return Promise.race([ - promise, - new Promise((resolve) => { - setTimeout(() => { - console.error('[asyncActionUtils] ⏰ PROMISE_TIMEOUT', { timeoutMs }); - resolve({ - response: null, - data: null, - success: false, - error: { - code: 'TIMEOUT', - message: timeoutMessage, - timeout: timeoutMs - } - }); - }, timeoutMs); - }) - ]); -}; -``` - -### 사용 예제 - -```javascript -import { withTimeout, fetchApi } from '../utils/asyncActionUtils'; - -// 5초 타임아웃 -const result = await withTimeout( - fetchApi('/api/slow-endpoint'), - 5000, - '요청이 시간초과 되었습니다' -); - -if (result.success) { - console.log('성공:', result.data); -} else if (result.error.code === 'TIMEOUT') { - console.error('타임아웃 발생'); -} else { - console.error('기타 에러:', result.error); -} -``` - ---- - -## 6️⃣ executeParallelAsyncActions - -### 설명 - -여러 비동기 액션을 **병렬로 실행**하고 모든 결과를 기다립니다. - -### 구현 - -**파일**: `src/utils/asyncActionUtils.js:279-299` - -```javascript -export const executeParallelAsyncActions = (asyncActions, context = {}) => { - console.log('[asyncActionUtils] 🚀 EXECUTE_PARALLEL_START', { count: asyncActions.length }); - - const promises = asyncActions.map(action => - wrapAsyncAction(action, context) - ); - - return Promise.all(promises) - .then(results => { - console.log('[asyncActionUtils] ✅ EXECUTE_PARALLEL_SUCCESS', { - successCount: results.filter(r => r.success).length, - failCount: results.filter(r => !r.success).length - }); - return results; - }) - .catch(error => { - console.error('[asyncActionUtils] ❌ EXECUTE_PARALLEL_ERROR', error); - return []; - }); -}; -``` - -### 사용 예제 - -```javascript -import { executeParallelAsyncActions } from '../utils/asyncActionUtils'; - -// 3개의 API를 동시에 호출 -const results = await executeParallelAsyncActions([ - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URL1, {}, {}, onSuccess, onFail); - }, - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URL2, {}, {}, onSuccess, onFail); - }, - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URL3, {}, {}, onSuccess, onFail); - } -], { dispatch, getState }); - -// 결과 처리 -results.forEach((result, index) => { - if (result.success) { - console.log(`API ${index + 1} 성공:`, result.data); - } else { - console.error(`API ${index + 1} 실패:`, result.error); - } -}); -``` - ---- - -## 📊 실제 사용 시나리오 - -### 시나리오 1: API 호출 후 후속 처리 - -```javascript -import { tAxiosToPromise } from '../utils/asyncActionUtils'; - -export const addToCartAndRefresh = (productId) => async (dispatch, getState) => { - // 1. 카트에 추가 - const addResult = await tAxiosToPromise( - TAxios, - dispatch, - getState, - 'post', - URLS.ADD_TO_CART, - {}, - { productId }, - {} - ); - - if (addResult.success) { - // 2. 카트 추가 성공 시 카트 정보 재조회 - dispatch({ type: types.ADD_TO_CART, payload: addResult.data.data }); - - const cartResult = await tAxiosToPromise( - TAxios, - dispatch, - getState, - 'get', - URLS.GET_CART, - {}, - { mbrNo: addResult.data.data.mbrNo }, - {} - ); - - if (cartResult.success) { - dispatch({ type: types.GET_CART, payload: cartResult.data.data }); - } - } else { - console.error('카트 추가 실패:', addResult.error); - } -}; -``` - -### 시나리오 2: 타임아웃이 있는 API 호출 - -```javascript -import { tAxiosToPromise, withTimeout } from '../utils/asyncActionUtils'; - -export const getLargeData = () => async (dispatch, getState) => { - const result = await withTimeout( - tAxiosToPromise( - TAxios, - dispatch, - getState, - 'get', - URLS.GET_LARGE_DATA, - {}, - {}, - {} - ), - 10000, // 10초 타임아웃 - '데이터 조회 시간이 초과되었습니다' - ); - - if (result.success) { - dispatch({ type: types.GET_LARGE_DATA, payload: result.data.data }); - } else if (result.error.code === 'TIMEOUT') { - // 타임아웃 처리 - dispatch({ type: types.SHOW_TIMEOUT_MESSAGE }); - } else { - // 기타 에러 처리 - console.error('조회 실패:', result.error); - } -}; -``` - ---- - -## ✅ 장점 - -1. **성공 기준 명확화**: HTTP + retCode 모두 확인 -2. **체인 보장**: reject 없이 resolve만 사용하여 Promise 체인 유지 -3. **상세한 로깅**: 모든 단계에서 로그 출력 -4. **타임아웃 지원**: 응답 없는 API 처리 가능 -5. **에러 처리**: 모든 에러를 표준 구조로 반환 - -## ⚠️ 주의사항 - -1. **Chrome 68 호환**: async/await 사용 가능하지만 주의 필요 -2. **항상 resolve**: reject 사용하지 않음 -3. **success 플래그**: 반드시 `result.success` 확인 필요 - ---- - -**다음**: [해결 방법 3: 큐 기반 패널 액션 시스템 →](./04-solution-queue-system.md) diff --git a/.docs/dispatch-async/04-solution-queue-system.md b/.docs/dispatch-async/04-solution-queue-system.md deleted file mode 100644 index 50801468..00000000 --- a/.docs/dispatch-async/04-solution-queue-system.md +++ /dev/null @@ -1,644 +0,0 @@ -# 해결 방법 3: 큐 기반 패널 액션 시스템 - -## 📦 개요 - -**관련 파일**: -- `src/actions/queuedPanelActions.js` -- `src/middleware/panelQueueMiddleware.js` -- `src/reducers/panelReducer.js` -- `src/store/store.js` (미들웨어 등록 필요) - -**작성일**: 2025-11-06 -**커밋**: -- `5bd2774 [251106] feat: Queued Panel functions` -- `f9290a1 [251106] fix: Dispatch Queue implementation` - -미들웨어 기반의 **액션 큐 처리 시스템**으로, 패널 액션들을 순차적으로 실행합니다. - -## ⚠️ 사전 요구사항 - -큐 시스템을 사용하려면 **반드시** store에 panelQueueMiddleware를 등록해야 합니다. - -**파일**: `src/store/store.js` - -```javascript -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - -export const store = createStore( - rootReducer, - applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware) -); -``` - -미들웨어를 등록하지 않으면 큐에 액션이 추가되어도 자동으로 처리되지 않습니다! - -## 🎯 핵심 개념 - -### 왜 큐 시스템이 필요한가? - -패널 관련 액션들은 특히 순서가 중요합니다: - -```javascript -// 문제 상황 -dispatch(pushPanel({ name: 'SEARCH' })); // 검색 패널 열기 -dispatch(updatePanel({ results: [...] })); // 검색 결과 업데이트 -dispatch(popPanel('LOADING')); // 로딩 패널 닫기 - -// 실제 실행 순서 (문제!) -// → popPanel이 먼저 실행될 수 있음 -// → updatePanel이 pushPanel보다 먼저 실행될 수 있음 -``` - -### 큐 시스템의 동작 방식 - -``` -[큐에 추가] → [미들웨어 감지] → [순차 처리] → [완료] - ↓ ↓ ↓ ↓ - ENQUEUE 자동 감지 시작 PROCESS_QUEUE 다음 액션 -``` - ---- - -## 🔑 주요 컴포넌트 - -### 1. queuedPanelActions.js - -패널 액션을 큐에 추가하는 액션 크리에이터들 - -### 2. panelQueueMiddleware.js - -큐에 액션이 추가되면 자동으로 처리를 시작하는 미들웨어 - -### 3. panelReducer.js - -큐 상태를 관리하는 리듀서 - ---- - -## 📋 기본 패널 액션 - -### 1. pushPanelQueued - -패널을 큐에 추가하여 순차적으로 열기 - -```javascript -import { pushPanelQueued } from '../actions/queuedPanelActions'; - -// 기본 사용 -dispatch(pushPanelQueued( - { name: panel_names.SEARCH_PANEL }, - false // duplicatable -)); - -// 중복 허용 -dispatch(pushPanelQueued( - { name: panel_names.PRODUCT_DETAIL, productId: 123 }, - true // 중복 허용 -)); -``` - -### 2. popPanelQueued - -패널을 큐를 통해 제거 - -```javascript -import { popPanelQueued } from '../actions/queuedPanelActions'; - -// 마지막 패널 제거 -dispatch(popPanelQueued()); - -// 특정 패널 제거 -dispatch(popPanelQueued(panel_names.SEARCH_PANEL)); -``` - -### 3. updatePanelQueued - -패널 정보를 큐를 통해 업데이트 - -```javascript -import { updatePanelQueued } from '../actions/queuedPanelActions'; - -dispatch(updatePanelQueued({ - name: panel_names.SEARCH_PANEL, - panelInfo: { - results: [...], - totalCount: 100 - } -})); -``` - -### 4. resetPanelsQueued - -모든 패널을 초기화 - -```javascript -import { resetPanelsQueued } from '../actions/queuedPanelActions'; - -// 빈 패널로 초기화 -dispatch(resetPanelsQueued()); - -// 특정 패널들로 초기화 -dispatch(resetPanelsQueued([ - { name: panel_names.HOME } -])); -``` - -### 5. enqueueMultiplePanelActions - -여러 패널 액션을 한 번에 큐에 추가 - -```javascript -import { enqueueMultiplePanelActions, pushPanelQueued, updatePanelQueued, popPanelQueued } - from '../actions/queuedPanelActions'; - -dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: panel_names.SEARCH_PANEL }), - updatePanelQueued({ name: panel_names.SEARCH_PANEL, panelInfo: { query: 'test' } }), - popPanelQueued(panel_names.LOADING_PANEL) -])); -``` - ---- - -## 🚀 비동기 패널 액션 - -### 1. enqueueAsyncPanelAction - -비동기 작업(API 호출 등)을 큐에 추가하여 순차 실행 - -**파일**: `src/actions/queuedPanelActions.js:173-199` - -```javascript -import { enqueueAsyncPanelAction } from '../actions/queuedPanelActions'; - -dispatch(enqueueAsyncPanelAction({ - id: 'search_products_123', // 고유 ID - - // 비동기 액션 (TAxios 등) - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.SEARCH_PRODUCTS, - {}, - { keyword: 'test' }, - onSuccess, - onFail - ); - }, - - // 성공 콜백 - onSuccess: (response) => { - console.log('검색 성공:', response); - dispatch(pushPanelQueued({ - name: panel_names.SEARCH_RESULT, - results: response.data.results - })); - }, - - // 실패 콜백 - onFail: (error) => { - console.error('검색 실패:', error); - dispatch(pushPanelQueued({ - name: panel_names.ERROR, - message: error.message - })); - }, - - // 완료 콜백 (성공/실패 모두 호출) - onFinish: (isSuccess, result) => { - console.log('검색 완료:', isSuccess ? '성공' : '실패'); - }, - - // 타임아웃 (ms) - timeout: 10000 // 10초 -})); -``` - -### 동작 흐름 - -``` -1. enqueueAsyncPanelAction 호출 - ↓ -2. ENQUEUE_ASYNC_PANEL_ACTION dispatch - ↓ -3. executeAsyncAction 자동 실행 - ↓ -4. wrapAsyncAction으로 Promise 래핑 - ↓ -5. withTimeout으로 타임아웃 적용 - ↓ -6. 결과에 따라 onSuccess 또는 onFail 호출 - ↓ -7. COMPLETE_ASYNC_PANEL_ACTION 또는 FAIL_ASYNC_PANEL_ACTION dispatch -``` - ---- - -## 🔗 API 호출 후 패널 액션 - -### createApiWithPanelActions - -API 호출 후 여러 패널 액션을 자동으로 실행 - -**파일**: `src/actions/queuedPanelActions.js:355-394` - -```javascript -import { createApiWithPanelActions } from '../actions/queuedPanelActions'; - -dispatch(createApiWithPanelActions({ - // API 호출 - apiCall: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.SEARCH_PRODUCTS, - {}, - { keyword: 'laptop' }, - onSuccess, - onFail - ); - }, - - // API 성공 후 실행할 패널 액션들 - panelActions: [ - // Plain action - { type: 'PUSH_PANEL', payload: { name: panel_names.SEARCH_PANEL } }, - - // Dynamic action (response 사용) - (response) => updatePanelQueued({ - name: panel_names.SEARCH_PANEL, - panelInfo: { - results: response.data.results, - totalCount: response.data.totalCount - } - }), - - // 또 다른 패널 액션 - popPanelQueued(panel_names.LOADING_PANEL) - ], - - // API 성공 콜백 - onApiSuccess: (response) => { - console.log('API 성공:', response.data.totalCount, '개 검색됨'); - }, - - // API 실패 콜백 - onApiFail: (error) => { - console.error('API 실패:', error); - dispatch(pushPanelQueued({ - name: panel_names.ERROR, - message: '검색에 실패했습니다' - })); - } -})); -``` - -### 사용 예제: 상품 검색 - -```javascript -export const searchProducts = (keyword) => - createApiWithPanelActions({ - apiCall: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.SEARCH_PRODUCTS, - {}, - { keyword }, - onSuccess, - onFail - ); - }, - panelActions: [ - // 1. 로딩 패널 닫기 - popPanelQueued(panel_names.LOADING_PANEL), - - // 2. 검색 결과 패널 열기 - (response) => pushPanelQueued({ - name: panel_names.SEARCH_RESULT, - results: response.data.results - }), - - // 3. 검색 히스토리 업데이트 - (response) => updatePanelQueued({ - name: panel_names.SEARCH_HISTORY, - panelInfo: { lastSearch: keyword } - }) - ], - onApiSuccess: (response) => { - console.log(`${response.data.totalCount}개의 상품을 찾았습니다`); - } - }); -``` - ---- - -## 🔄 비동기 액션 시퀀스 - -### createAsyncPanelSequence - -여러 비동기 액션을 **순차적으로** 실행 - -**파일**: `src/actions/queuedPanelActions.js:401-445` - -```javascript -import { createAsyncPanelSequence } from '../actions/queuedPanelActions'; - -dispatch(createAsyncPanelSequence([ - // 첫 번째 비동기 액션 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_USER_INFO, {}, {}, onSuccess, onFail); - }, - onSuccess: (response) => { - console.log('사용자 정보 조회 성공'); - dispatch(pushPanelQueued({ - name: panel_names.USER_INFO, - userInfo: response.data.data - })); - }, - onFail: (error) => { - console.error('사용자 정보 조회 실패:', error); - } - }, - - // 두 번째 비동기 액션 (첫 번째 완료 후 실행) - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - const userInfo = getState().user.info; - TAxios( - dispatch, - getState, - 'get', - URLS.GET_CART, - {}, - { mbrNo: userInfo.mbrNo }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - console.log('카트 정보 조회 성공'); - dispatch(updatePanelQueued({ - name: panel_names.USER_INFO, - panelInfo: { cartCount: response.data.data.length } - })); - }, - onFail: (error) => { - console.error('카트 정보 조회 실패:', error); - } - }, - - // 세 번째 비동기 액션 (두 번째 완료 후 실행) - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_ORDERS, {}, {}, onSuccess, onFail); - }, - onSuccess: (response) => { - console.log('주문 정보 조회 성공'); - dispatch(pushPanelQueued({ - name: panel_names.ORDER_LIST, - orders: response.data.data - })); - }, - onFail: (error) => { - console.error('주문 정보 조회 실패:', error); - // 실패 시 시퀀스 중단 - } - } -])); -``` - -### 동작 흐름 - -``` -Action 1 실행 → 성공? → Action 2 실행 → 성공? → Action 3 실행 - ↓ ↓ - 실패 시 실패 시 - 중단 중단 -``` - ---- - -## ⚙️ 미들웨어: panelQueueMiddleware - -### 동작 원리 - -**파일**: `src/middleware/panelQueueMiddleware.js` - -```javascript -const panelQueueMiddleware = (store) => (next) => (action) => { - const result = next(action); - - // 큐에 액션이 추가되면 자동으로 처리 시작 - if (action.type === types.ENQUEUE_PANEL_ACTION) { - console.log('[panelQueueMiddleware] 🚀 ACTION_ENQUEUED', { - action: action.payload.action, - queueId: action.payload.id, - }); - - // setTimeout을 사용하여 현재 액션이 완전히 처리된 후에 큐 처리 시작 - setTimeout(() => { - const currentState = store.getState(); - - if (currentState.panels) { - // 이미 처리 중이 아니고 큐에 액션이 있으면 처리 시작 - if (!currentState.panels.isProcessingQueue && - currentState.panels.panelActionQueue.length > 0) { - console.log('[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS'); - store.dispatch({ type: types.PROCESS_PANEL_QUEUE }); - } - } - }, 0); - } - - // 큐 처리가 완료되고 남은 큐가 있으면 계속 처리 - if (action.type === types.PROCESS_PANEL_QUEUE) { - setTimeout(() => { - const currentState = store.getState(); - - if (currentState.panels) { - // 처리 중이 아니고 큐에 남은 액션이 있으면 계속 처리 - if (!currentState.panels.isProcessingQueue && - currentState.panels.panelActionQueue.length > 0) { - console.log('[panelQueueMiddleware] 🔄 CONTINUING_QUEUE_PROCESS'); - store.dispatch({ type: types.PROCESS_PANEL_QUEUE }); - } - } - }, 0); - } - - return result; -}; -``` - -### 주요 특징 - -1. ✅ **자동 시작**: 큐에 액션 추가 시 자동으로 처리 시작 -2. ✅ **연속 처리**: 한 액션 완료 후 자동으로 다음 액션 처리 -3. ✅ **중복 방지**: 이미 처리 중이면 새로 시작하지 않음 -4. ✅ **로깅**: 모든 단계에서 로그 출력 - ---- - -## 📊 리듀서 상태 구조 - -### panelReducer.js의 큐 관련 상태 - -```javascript -{ - panels: [], // 실제 패널 스택 - lastPanelAction: 'push', // 마지막 액션 타입 - - // 큐 관련 상태 - panelActionQueue: [ // 처리 대기 중인 큐 - { - id: 'queue_item_1_1699999999999', - action: 'PUSH_PANEL', - panel: { name: 'SEARCH_PANEL' }, - duplicatable: false, - timestamp: 1699999999999 - }, - // ... - ], - - isProcessingQueue: false, // 큐 처리 중 여부 - queueError: null, // 큐 처리 에러 - - queueStats: { // 큐 통계 - totalProcessed: 0, // 총 처리된 액션 수 - failedCount: 0, // 실패한 액션 수 - averageProcessingTime: 0 // 평균 처리 시간 (ms) - }, - - // 비동기 액션 상태 - asyncActions: { // 실행 중인 비동기 액션들 - 'async_action_1': { - id: 'async_action_1', - status: 'pending', // 'pending' | 'success' | 'failed' - timestamp: 1699999999999 - } - }, - - completedAsyncActions: [ // 완료된 액션 ID들 - 'async_action_1', - 'async_action_2' - ], - - failedAsyncActions: [ // 실패한 액션 ID들 - 'async_action_3' - ] -} -``` - ---- - -## 🎯 실제 사용 시나리오 - -### 시나리오 1: 검색 플로우 - -```javascript -export const performSearch = (keyword) => (dispatch) => { - // 1. 로딩 패널 열기 - dispatch(pushPanelQueued({ name: panel_names.LOADING })); - - // 2. 검색 API 호출 후 결과 표시 - dispatch(createApiWithPanelActions({ - apiCall: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'post', URLS.SEARCH, {}, { keyword }, onSuccess, onFail); - }, - panelActions: [ - popPanelQueued(panel_names.LOADING), - (response) => pushPanelQueued({ - name: panel_names.SEARCH_RESULT, - results: response.data.results - }) - ] - })); -}; -``` - -### 시나리오 2: 다단계 결제 프로세스 - -```javascript -export const processCheckout = (orderInfo) => - createAsyncPanelSequence([ - // 1단계: 주문 검증 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'post', URLS.VALIDATE_ORDER, {}, orderInfo, onSuccess, onFail); - }, - onSuccess: () => { - dispatch(updatePanelQueued({ - name: panel_names.CHECKOUT, - panelInfo: { step: 1, status: 'validated' } - })); - } - }, - - // 2단계: 결제 처리 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'post', URLS.PROCESS_PAYMENT, {}, orderInfo, onSuccess, onFail); - }, - onSuccess: (response) => { - dispatch(updatePanelQueued({ - name: panel_names.CHECKOUT, - panelInfo: { step: 2, paymentId: response.data.data.paymentId } - })); - } - }, - - // 3단계: 주문 확정 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - const state = getState(); - const paymentId = state.panels.panels.find(p => p.name === panel_names.CHECKOUT) - .panelInfo.paymentId; - - TAxios( - dispatch, - getState, - 'post', - URLS.CONFIRM_ORDER, - {}, - { ...orderInfo, paymentId }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch(popPanelQueued(panel_names.CHECKOUT)); - dispatch(pushPanelQueued({ - name: panel_names.ORDER_COMPLETE, - orderId: response.data.data.orderId - })); - } - } - ]); -``` - ---- - -## ✅ 장점 - -1. **완벽한 순서 보장**: 큐 시스템으로 100% 순서 보장 -2. **자동 처리**: 미들웨어가 자동으로 큐 처리 -3. **비동기 지원**: API 호출 등 비동기 작업 완벽 지원 -4. **타임아웃**: 응답 없는 작업 자동 처리 -5. **에러 복구**: 에러 발생 시에도 다음 액션 계속 처리 -6. **통계**: 큐 처리 통계 자동 수집 - -## ⚠️ 주의사항 - -1. **미들웨어 등록**: store에 panelQueueMiddleware 등록 필요 -2. **리듀서 확장**: panelReducer에 큐 관련 상태 추가 필요 -3. **기존 코드**: 기존 pushPanel 등과 병행 사용 가능 - ---- - -**다음**: [사용 패턴 및 예제 →](./05-usage-patterns.md) diff --git a/.docs/dispatch-async/05-usage-patterns.md b/.docs/dispatch-async/05-usage-patterns.md deleted file mode 100644 index 4d09d1c4..00000000 --- a/.docs/dispatch-async/05-usage-patterns.md +++ /dev/null @@ -1,804 +0,0 @@ -# 사용 패턴 및 예제 - -## 📋 목차 - -1. [어떤 솔루션을 선택할까?](#어떤-솔루션을-선택할까) -2. [공통 패턴](#공통-패턴) -3. [실전 예제](#실전-예제) -4. [마이그레이션 가이드](#마이그레이션-가이드) -5. [Best Practices](#best-practices) - ---- - -## 어떤 솔루션을 선택할까? - -### 의사결정 플로우차트 - -``` -패널 관련 액션인가? -├─ YES → 큐 기반 패널 액션 시스템 사용 -│ (queuedPanelActions.js) -│ -└─ NO → API 호출이 포함되어 있는가? - ├─ YES → API 패턴은? - │ ├─ API 후 여러 dispatch 필요 → createApiThunkWithChain - │ ├─ 로딩 상태 관리 필요 → withLoadingState - │ └─ Promise 기반 처리 필요 → asyncActionUtils - │ - └─ NO → 순차적 dispatch만 필요 - → createSequentialDispatch -``` - -### 솔루션 비교표 - -| 상황 | 추천 솔루션 | 파일 | -|------|------------|------| -| 패널 열기/닫기/업데이트 | `pushPanelQueued`, `popPanelQueued` | queuedPanelActions.js | -| API 호출 후 패널 업데이트 | `createApiWithPanelActions` | queuedPanelActions.js | -| 여러 API 순차 호출 | `createAsyncPanelSequence` | queuedPanelActions.js | -| API 후 여러 dispatch | `createApiThunkWithChain` | dispatchHelper.js | -| 로딩 상태 자동 관리 | `withLoadingState` | dispatchHelper.js | -| 단순 순차 dispatch | `createSequentialDispatch` | dispatchHelper.js | -| Promise 기반 API 호출 | `fetchApi`, `tAxiosToPromise` | asyncActionUtils.js | - ---- - -## 공통 패턴 - -### 패턴 1: API 후 State 업데이트 - -#### Before -```javascript -export const getProductDetail = (productId) => (dispatch, getState) => { - const onSuccess = (response) => { - dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); - dispatch(getRelatedProducts(productId)); - }; - - TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT, {}, { productId }, onSuccess, onFail); -}; -``` - -#### After (dispatchHelper) -```javascript -export const getProductDetail = (productId) => - createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, 'get', URLS.GET_PRODUCT, {}, { productId }, onS, onF), - [ - (response) => ({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }), - getRelatedProducts(productId) - ] - ); -``` - -#### After (asyncActionUtils - Chrome 68+) -```javascript -export const getProductDetail = (productId) => async (dispatch, getState) => { - const result = await tAxiosToPromise( - TAxios, dispatch, getState, 'get', URLS.GET_PRODUCT, {}, { productId } - ); - - if (result.success) { - dispatch({ type: types.GET_PRODUCT_DETAIL, payload: result.data.data }); - dispatch(getRelatedProducts(productId)); - } -}; -``` - -### 패턴 2: 로딩 상태 관리 - -#### Before -```javascript -export const fetchUserData = (userId) => (dispatch, getState) => { - dispatch(changeAppStatus({ showLoadingPanel: { show: true } })); - - const onSuccess = (response) => { - dispatch({ type: types.GET_USER_DATA, payload: response.data.data }); - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - }; - - const onFail = (error) => { - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - }; - - TAxios(dispatch, getState, 'get', URLS.GET_USER, {}, { userId }, onSuccess, onFail); -}; -``` - -#### After -```javascript -export const fetchUserData = (userId) => - withLoadingState( - (dispatch, getState) => { - return TAxiosPromise(dispatch, getState, 'get', URLS.GET_USER, {}, { userId }) - .then((response) => { - dispatch({ type: types.GET_USER_DATA, payload: response.data.data }); - }); - } - ); -``` - -### 패턴 3: 패널 순차 열기 - -#### Before -```javascript -dispatch(pushPanel({ name: panel_names.SEARCH })); -setTimeout(() => { - dispatch(updatePanel({ results: [...] })); - setTimeout(() => { - dispatch(popPanel(panel_names.LOADING)); - }, 0); -}, 0); -``` - -#### After -```javascript -dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: panel_names.SEARCH }), - updatePanelQueued({ results: [...] }), - popPanelQueued(panel_names.LOADING) -])); -``` - -### 패턴 4: 조건부 dispatch - -#### Before -```javascript -export const checkAndFetch = () => (dispatch, getState) => { - const state = getState(); - - if (state.user.isLoggedIn) { - dispatch(fetchUserProfile()); - dispatch(fetchUserCart()); - } else { - dispatch({ type: types.SHOW_LOGIN_POPUP }); - } -}; -``` - -#### After -```javascript -export const checkAndFetch = () => - createConditionalDispatch( - (state) => state.user.isLoggedIn, - [ - fetchUserProfile(), - fetchUserCart() - ], - [ - { type: types.SHOW_LOGIN_POPUP } - ] - ); -``` - ---- - -## 실전 예제 - -### 예제 1: 검색 기능 - -```javascript -// src/actions/searchActions.js -import { createApiWithPanelActions, pushPanelQueued, popPanelQueued, updatePanelQueued } - from './queuedPanelActions'; -import { panel_names } from '../constants/panelNames'; -import { URLS } from '../constants/urls'; - -export const performSearch = (keyword) => (dispatch) => { - // 1. 로딩 패널 열기 - dispatch(pushPanelQueued({ name: panel_names.LOADING })); - - // 2. 검색 API 호출 후 결과 처리 - dispatch(createApiWithPanelActions({ - apiCall: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.SEARCH_PRODUCTS, - {}, - { keyword, page: 1, size: 20 }, - onSuccess, - onFail - ); - }, - - panelActions: [ - // 1) 로딩 패널 닫기 - popPanelQueued(panel_names.LOADING), - - // 2) 검색 결과 패널 열기 - (response) => pushPanelQueued({ - name: panel_names.SEARCH_RESULT, - results: response.data.results, - totalCount: response.data.totalCount, - keyword - }), - - // 3) 검색 히스토리 업데이트 - (response) => updatePanelQueued({ - name: panel_names.SEARCH_HISTORY, - panelInfo: { - lastSearch: keyword, - resultCount: response.data.totalCount - } - }) - ], - - onApiSuccess: (response) => { - console.log(`"${keyword}" 검색 완료: ${response.data.totalCount}개`); - }, - - onApiFail: (error) => { - console.error('검색 실패:', error); - dispatch(popPanelQueued(panel_names.LOADING)); - dispatch(pushPanelQueued({ - name: panel_names.ERROR, - message: '검색에 실패했습니다' - })); - } - })); -}; -``` - -### 예제 2: 장바구니 추가 - -```javascript -// src/actions/cartActions.js -import { createApiThunkWithChain } from '../utils/dispatchHelper'; -import { types } from './actionTypes'; -import { URLS } from '../constants/urls'; - -export const addToCart = (productId, quantity) => - createApiThunkWithChain( - // API 호출 - (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.ADD_TO_CART, - {}, - { productId, quantity }, - onSuccess, - onFail - ); - }, - - // 성공 시 순차 dispatch - [ - // 1) 장바구니 추가 액션 - (response) => ({ - type: types.ADD_TO_CART, - payload: response.data.data - }), - - // 2) 장바구니 개수 업데이트 - (response) => ({ - type: types.UPDATE_CART_COUNT, - payload: response.data.data.cartCount - }), - - // 3) 장바구니 정보 재조회 - (response) => getMyCartInfo({ mbrNo: response.data.data.mbrNo }), - - // 4) 성공 메시지 표시 - () => ({ - type: types.SHOW_TOAST, - payload: { message: '장바구니에 담았습니다' } - }) - ], - - // 실패 시 dispatch - (error) => ({ - type: types.SHOW_ERROR, - payload: { message: error.message || '장바구니 담기에 실패했습니다' } - }) - ); -``` - -### 예제 3: 로그인 플로우 - -```javascript -// src/actions/authActions.js -import { createAsyncPanelSequence } from './queuedPanelActions'; -import { withLoadingState } from '../utils/dispatchHelper'; -import { panel_names } from '../constants/panelNames'; -import { types } from './actionTypes'; -import { URLS } from '../constants/urls'; - -export const performLogin = (userId, password) => - withLoadingState( - createAsyncPanelSequence([ - // 1단계: 로그인 API 호출 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.LOGIN, - {}, - { userId, password }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - // 로그인 성공 - 토큰 저장 - dispatch({ - type: types.LOGIN_SUCCESS, - payload: { - token: response.data.data.token, - userInfo: response.data.data.userInfo - } - }); - }, - onFail: (error) => { - dispatch({ - type: types.SHOW_ERROR, - payload: { message: '로그인에 실패했습니다' } - }); - } - }, - - // 2단계: 사용자 정보 조회 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - const state = getState(); - const mbrNo = state.auth.userInfo.mbrNo; - - TAxios( - dispatch, - getState, - 'get', - URLS.GET_USER_INFO, - {}, - { mbrNo }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch({ - type: types.GET_USER_INFO, - payload: response.data.data - }); - } - }, - - // 3단계: 장바구니 정보 조회 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - const state = getState(); - const mbrNo = state.auth.userInfo.mbrNo; - - TAxios( - dispatch, - getState, - 'get', - URLS.GET_CART, - {}, - { mbrNo }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch({ - type: types.GET_CART_INFO, - payload: response.data.data - }); - - // 로그인 완료 패널로 이동 - dispatch(pushPanelQueued({ - name: panel_names.LOGIN_COMPLETE - })); - } - } - ]), - { loadingType: 'wait' } - ); -``` - -### 예제 4: 다단계 폼 제출 - -```javascript -// src/actions/formActions.js -import { createAsyncPanelSequence } from './queuedPanelActions'; -import { tAxiosToPromise } from '../utils/asyncActionUtils'; -import { types } from './actionTypes'; -import { URLS } from '../constants/urls'; - -export const submitMultiStepForm = (formData) => - createAsyncPanelSequence([ - // Step 1: 입력 검증 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.VALIDATE_FORM, - {}, - formData, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch({ - type: types.UPDATE_FORM_STEP, - payload: { step: 1, status: 'validated' } - }); - dispatch(updatePanelQueued({ - name: panel_names.FORM_PANEL, - panelInfo: { step: 1, validated: true } - })); - }, - onFail: (error) => { - dispatch({ - type: types.SHOW_VALIDATION_ERROR, - payload: { errors: error.data?.errors || [] } - }); - } - }, - - // Step 2: 중복 체크 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.CHECK_DUPLICATE, - {}, - { email: formData.email }, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch({ - type: types.UPDATE_FORM_STEP, - payload: { step: 2, status: 'checked' } - }); - dispatch(updatePanelQueued({ - name: panel_names.FORM_PANEL, - panelInfo: { step: 2, duplicate: false } - })); - }, - onFail: (error) => { - dispatch({ - type: types.SHOW_ERROR, - payload: { message: '이미 사용 중인 이메일입니다' } - }); - } - }, - - // Step 3: 최종 제출 - { - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios( - dispatch, - getState, - 'post', - URLS.SUBMIT_FORM, - {}, - formData, - onSuccess, - onFail - ); - }, - onSuccess: (response) => { - dispatch({ - type: types.SUBMIT_FORM_SUCCESS, - payload: response.data.data - }); - - // 성공 패널로 이동 - dispatch(popPanelQueued(panel_names.FORM_PANEL)); - dispatch(pushPanelQueued({ - name: panel_names.SUCCESS_PANEL, - message: '가입이 완료되었습니다' - })); - }, - onFail: (error) => { - dispatch({ - type: types.SUBMIT_FORM_FAIL, - payload: { error: error.message } - }); - } - } - ]); -``` - -### 예제 5: 병렬 데이터 로딩 - -```javascript -// src/actions/dashboardActions.js -import { createParallelDispatch } from '../utils/dispatchHelper'; -import { executeParallelAsyncActions } from '../utils/asyncActionUtils'; -import { types } from './actionTypes'; -import { URLS } from '../constants/urls'; - -// 방법 1: dispatchHelper 사용 -export const loadDashboardData = () => - createParallelDispatch([ - fetchUserProfile(), - fetchRecentOrders(), - fetchRecommendations(), - fetchNotifications() - ], { withLoading: true }); - -// 방법 2: asyncActionUtils 사용 -export const loadDashboardDataAsync = () => async (dispatch, getState) => { - dispatch(changeAppStatus({ showLoadingPanel: { show: true } })); - - const results = await executeParallelAsyncActions([ - // 1. 사용자 프로필 - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_PROFILE, {}, {}, onSuccess, onFail); - }, - - // 2. 최근 주문 - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_RECENT_ORDERS, {}, {}, onSuccess, onFail); - }, - - // 3. 추천 상품 - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_RECOMMENDATIONS, {}, {}, onSuccess, onFail); - }, - - // 4. 알림 - (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URLS.GET_NOTIFICATIONS, {}, {}, onSuccess, onFail); - } - ], { dispatch, getState }); - - // 각 결과 처리 - const [profileResult, ordersResult, recoResult, notiResult] = results; - - if (profileResult.success) { - dispatch({ type: types.GET_PROFILE, payload: profileResult.data.data }); - } - - if (ordersResult.success) { - dispatch({ type: types.GET_RECENT_ORDERS, payload: ordersResult.data.data }); - } - - if (recoResult.success) { - dispatch({ type: types.GET_RECOMMENDATIONS, payload: recoResult.data.data }); - } - - if (notiResult.success) { - dispatch({ type: types.GET_NOTIFICATIONS, payload: notiResult.data.data }); - } - - dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); -}; -``` - ---- - -## 마이그레이션 가이드 - -### Step 1: 파일 import 변경 - -```javascript -// Before -import { pushPanel, popPanel, updatePanel } from '../actions/panelActions'; - -// After -import { pushPanelQueued, popPanelQueued, updatePanelQueued } - from '../actions/queuedPanelActions'; -import { createApiThunkWithChain, withLoadingState } - from '../utils/dispatchHelper'; -``` - -### Step 2: 기존 코드 점진적 마이그레이션 - -```javascript -// 1단계: 기존 코드 유지 -dispatch(pushPanel({ name: panel_names.SEARCH })); - -// 2단계: 큐 버전으로 변경 -dispatch(pushPanelQueued({ name: panel_names.SEARCH })); - -// 3단계: 여러 액션을 묶어서 처리 -dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: panel_names.SEARCH }), - updatePanelQueued({ results: [...] }) -])); -``` - -### Step 3: setTimeout 패턴 제거 - -```javascript -// Before -dispatch(action1()); -setTimeout(() => { - dispatch(action2()); - setTimeout(() => { - dispatch(action3()); - }, 0); -}, 0); - -// After -dispatch(createSequentialDispatch([ - action1(), - action2(), - action3() -])); -``` - -### Step 4: API 패턴 개선 - -```javascript -// Before -const onSuccess = (response) => { - dispatch({ type: types.ACTION_1, payload: response.data }); - dispatch(action2()); - dispatch(action3()); -}; - -TAxios(dispatch, getState, 'post', URL, {}, params, onSuccess, onFail); - -// After -dispatch(createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF), - [ - (response) => ({ type: types.ACTION_1, payload: response.data }), - action2(), - action3() - ] -)); -``` - ---- - -## Best Practices - -### 1. 명확한 에러 처리 - -```javascript -// ✅ Good -dispatch(createApiWithPanelActions({ - apiCall: (d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF), - panelActions: [...], - onApiSuccess: (response) => { - console.log('API 성공:', response); - }, - onApiFail: (error) => { - console.error('API 실패:', error); - dispatch(pushPanelQueued({ - name: panel_names.ERROR, - message: error.message || '작업에 실패했습니다' - })); - } -})); - -// ❌ Bad - 에러 처리 없음 -dispatch(createApiWithPanelActions({ - apiCall: (d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF), - panelActions: [...] -})); -``` - -### 2. 타임아웃 설정 - -```javascript -// ✅ Good -dispatch(enqueueAsyncPanelAction({ - asyncAction: (d, gs, onS, onF) => { - TAxios(d, gs, 'post', URL, {}, params, onS, onF); - }, - timeout: 10000, // 10초 - onFail: (error) => { - if (error.code === 'TIMEOUT') { - console.error('요청 시간 초과'); - } - } -})); - -// ❌ Bad - 타임아웃 없음 (무한 대기 가능) -dispatch(enqueueAsyncPanelAction({ - asyncAction: (d, gs, onS, onF) => { - TAxios(d, gs, 'post', URL, {}, params, onS, onF); - } -})); -``` - -### 3. 로깅 활용 - -```javascript -// ✅ Good - 상세한 로깅 -console.log('[SearchAction] 🔍 검색 시작:', keyword); - -dispatch(createApiWithPanelActions({ - apiCall: (d, gs, onS, onF) => { - TAxios(d, gs, 'post', URLS.SEARCH, {}, { keyword }, onS, onF); - }, - onApiSuccess: (response) => { - console.log('[SearchAction] ✅ 검색 성공:', response.data.totalCount, '개'); - }, - onApiFail: (error) => { - console.error('[SearchAction] ❌ 검색 실패:', error); - } -})); -``` - -### 4. 상태 검증 - -```javascript -// ✅ Good - 상태 검증 후 실행 -export const performAction = () => - createConditionalDispatch( - (state) => state.user.isLoggedIn && state.cart.items.length > 0, - [proceedToCheckout()], - [{ type: types.SHOW_LOGIN_POPUP }] - ); - -// ❌ Bad - 검증 없이 바로 실행 -export const performAction = () => proceedToCheckout(); -``` - -### 5. 재사용 가능한 액션 - -```javascript -// ✅ Good - 재사용 가능 -export const fetchDataWithLoading = (url, actionType) => - withLoadingState( - (dispatch, getState) => { - return TAxiosPromise(dispatch, getState, 'get', url, {}, {}) - .then((response) => { - dispatch({ type: actionType, payload: response.data.data }); - }); - } - ); - -// 사용 -dispatch(fetchDataWithLoading(URLS.GET_USER, types.GET_USER)); -dispatch(fetchDataWithLoading(URLS.GET_CART, types.GET_CART)); -``` - ---- - -## 체크리스트 - -### 초기 설정 확인사항 - -- [ ] **panelQueueMiddleware가 store.js에 등록되어 있는가?** (큐 시스템 사용 시 필수!) - ```javascript - // src/store/store.js - import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - - export const store = createStore( - rootReducer, - applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware) - ); - ``` -- [ ] TAxiosPromise가 import되어 있는가? (withLoadingState 사용 시) - -### 기능 구현 전 확인사항 - -- [ ] 패널 관련 액션인가? → 큐 시스템 사용 -- [ ] API 호출이 포함되어 있는가? → createApiThunkWithChain 또는 createApiWithPanelActions -- [ ] 로딩 상태 관리가 필요한가? → withLoadingState -- [ ] 순차 실행이 필요한가? → createSequentialDispatch 또는 createAsyncPanelSequence -- [ ] 타임아웃이 필요한가? → withTimeout 또는 timeout 옵션 설정 - -### 코드 리뷰 체크리스트 - -- [ ] setTimeout 사용 여부 확인 -- [ ] 에러 처리가 적절한가? -- [ ] 로깅이 충분한가? -- [ ] 타임아웃이 설정되어 있는가? -- [ ] 상태 검증이 필요한가? -- [ ] 재사용 가능한 구조인가? - ---- - -**이전**: [← 해결 방법 3: 큐 기반 패널 액션 시스템](./04-solution-queue-system.md) -**처음으로**: [← README](./README.md) diff --git a/.docs/dispatch-async/06-setup-guide.md b/.docs/dispatch-async/06-setup-guide.md deleted file mode 100644 index f7260ea0..00000000 --- a/.docs/dispatch-async/06-setup-guide.md +++ /dev/null @@ -1,396 +0,0 @@ -# 설정 가이드 - -## 📋 목차 - -1. [초기 설정](#초기-설정) -2. [파일 구조 확인](#파일-구조-확인) -3. [설정 순서](#설정-순서) -4. [검증 방법](#검증-방법) -5. [트러블슈팅](#트러블슈팅) - ---- - -## 초기 설정 - -### 1️⃣ 필수: panelQueueMiddleware 등록 - -큐 기반 패널 액션 시스템을 사용하려면 **반드시** Redux store에 미들웨어를 등록해야 합니다. - -#### 파일 위치 -`com.twin.app.shoptime/src/store/store.js` - -#### 수정 전 -```javascript -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware'; -import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware'; -// panelQueueMiddleware import 없음! - -// ... reducers ... - -export const store = createStore( - rootReducer, - applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware) - // panelQueueMiddleware 등록 없음! -); -``` - -#### 수정 후 -```javascript -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware'; -import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware'; -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; // ← 추가 - -// ... reducers ... - -export const store = createStore( - rootReducer, - applyMiddleware( - thunk, - panelHistoryMiddleware, - autoCloseMiddleware, - panelQueueMiddleware // ← 추가 (맨 마지막 위치) - ) -); -``` - -### 2️⃣ 미들웨어 등록 순서 - -미들웨어 등록 순서는 다음과 같습니다: - -```javascript -applyMiddleware( - thunk, // 1. Redux-thunk (비동기 액션 지원) - panelHistoryMiddleware, // 2. 패널 히스토리 관리 - autoCloseMiddleware, // 3. 자동 닫기 처리 - panelQueueMiddleware // 4. 패널 큐 처리 (맨 마지막) -) -``` - -**중요**: `panelQueueMiddleware`는 **맨 마지막**에 위치해야 합니다! -- 다른 미들웨어들이 먼저 액션을 처리한 후 -- 큐 미들웨어가 큐 관련 액션을 감지하고 처리합니다 - ---- - -## 파일 구조 확인 - -### 필수 파일들이 모두 존재하는지 확인 - -```bash -# 프로젝트 루트에서 실행 -ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js -ls -la com.twin.app.shoptime/src/actions/queuedPanelActions.js -ls -la com.twin.app.shoptime/src/utils/dispatchHelper.js -ls -la com.twin.app.shoptime/src/utils/asyncActionUtils.js -ls -la com.twin.app.shoptime/src/reducers/panelReducer.js -``` - -### 예상 출력 -``` --rw-r--r-- 1 user user 2063 Nov 10 06:32 .../panelQueueMiddleware.js --rw-r--r-- 1 user user 13845 Nov 06 10:15 .../queuedPanelActions.js --rw-r--r-- 1 user user 12345 Nov 05 14:20 .../dispatchHelper.js --rw-r--r-- 1 user user 10876 Nov 06 10:30 .../asyncActionUtils.js --rw-r--r-- 1 user user 25432 Nov 06 11:00 .../panelReducer.js -``` - -### 파일이 없다면? - -```bash -# 최신 코드를 pull 받으세요 -git fetch origin -git pull origin -``` - ---- - -## 설정 순서 - -### Step 1: 미들웨어 import 추가 - -**파일**: `src/store/store.js` - -```javascript -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; -``` - -### Step 2: applyMiddleware에 추가 - -```javascript -export const store = createStore( - rootReducer, - applyMiddleware( - thunk, - panelHistoryMiddleware, - autoCloseMiddleware, - panelQueueMiddleware // ← 추가 - ) -); -``` - -### Step 3: 저장 및 빌드 - -```bash -# 파일 저장 후 -npm run build -# 또는 개발 서버 재시작 -npm start -``` - -### Step 4: 브라우저 콘솔 확인 - -브라우저 개발자 도구(F12)를 열고 다음과 같은 로그가 보이는지 확인: - -``` -[panelQueueMiddleware] 🚀 ACTION_ENQUEUED -[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -[panelReducer] 🟡 PROCESSING_QUEUE_ITEM -[panelReducer] ✅ QUEUE_ITEM_PROCESSED -``` - ---- - -## 검증 방법 - -### 방법 1: 콘솔 로그 확인 - -큐 시스템을 사용하는 액션을 dispatch하면 다음과 같은 로그가 출력됩니다: - -```javascript -import { pushPanelQueued } from '../actions/queuedPanelActions'; -import { panel_names } from '../utils/Config'; - -dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL })); -``` - -**예상 로그**: -``` -[panelQueueMiddleware] 🚀 ACTION_ENQUEUED { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999' } -[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS -[panelReducer] 🟡 PROCESS_PANEL_QUEUE { isProcessing: false, queueLength: 1 } -[panelReducer] 🟡 PROCESSING_QUEUE_ITEM { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999', remainingQueueLength: 0 } -[panelReducer] 🔵 PUSH_PANEL START { newPanelName: 'SEARCH_PANEL', currentPanels: [...], duplicatable: false } -[panelReducer] 🔵 PUSH_PANEL END { resultPanels: [...], lastAction: 'push' } -[panelReducer] ✅ QUEUE_ITEM_PROCESSED { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999', processingTime: 2 } -``` - -### 방법 2: Redux DevTools 확인 - -Redux DevTools를 사용하여 액션 흐름을 확인: - -1. Chrome 확장 프로그램: Redux DevTools 설치 -2. 개발자 도구에서 "Redux" 탭 선택 -3. 다음 액션들이 순서대로 dispatch되는지 확인: - - `ENQUEUE_PANEL_ACTION` - - `PROCESS_PANEL_QUEUE` - - `PUSH_PANEL` (또는 다른 패널 액션) - -### 방법 3: State 확인 - -Redux state를 확인하여 큐 관련 상태가 정상적으로 업데이트되는지 확인: - -```javascript -// 콘솔에서 실행 -store.getState().panels -``` - -**예상 출력**: -```javascript -{ - panels: [...], // 실제 패널들 - lastPanelAction: 'push', - - // 큐 관련 상태 - panelActionQueue: [], // 처리 대기 중인 큐 (처리 후 비어있음) - isProcessingQueue: false, - queueError: null, - queueStats: { - totalProcessed: 1, - failedCount: 0, - averageProcessingTime: 2.5 - }, - - // 비동기 액션 관련 - asyncActions: {}, - completedAsyncActions: [], - failedAsyncActions: [] -} -``` - ---- - -## 트러블슈팅 - -### 문제 1: 큐가 처리되지 않음 - -#### 증상 -```javascript -dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL })); -// 아무 일도 일어나지 않음 -// 로그도 출력되지 않음 -``` - -#### 원인 -panelQueueMiddleware가 등록되지 않음 - -#### 해결 방법 -1. `store.js` 파일 확인: - ```javascript - // import가 있는지 확인 - import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - - // applyMiddleware에 추가되어 있는지 확인 - applyMiddleware(..., panelQueueMiddleware) - ``` - -2. 파일 저장 후 앱 재시작 -3. 브라우저 캐시 삭제 (Ctrl+Shift+R 또는 Cmd+Shift+R) - -### 문제 2: 미들웨어 파일을 찾을 수 없음 - -#### 증상 -``` -Error: Cannot find module '../middleware/panelQueueMiddleware' -``` - -#### 원인 -파일이 존재하지 않거나 경로가 잘못됨 - -#### 해결 방법 -1. 파일 존재 확인: - ```bash - ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js - ``` - -2. 파일이 없다면 최신 코드 pull: - ```bash - git fetch origin - git pull origin main - ``` - -3. 여전히 없다면 커밋 확인: - ```bash - git log --oneline --grep="panelQueueMiddleware" - # 5bd2774 [251106] feat: Queued Panel functions - ``` - -### 문제 3: 로그는 보이는데 패널이 열리지 않음 - -#### 증상 -``` -[panelQueueMiddleware] 🚀 ACTION_ENQUEUED -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -[panelReducer] ✅ QUEUE_ITEM_PROCESSED -// 하지만 패널이 화면에 표시되지 않음 -``` - -#### 원인 -UI 렌더링 문제 (Redux는 정상 작동) - -#### 해결 방법 -1. Redux state 확인: - ```javascript - console.log(store.getState().panels.panels); - // 패널이 배열에 추가되었는지 확인 - ``` - -2. 패널 컴포넌트 렌더링 로직 확인 -3. React DevTools로 컴포넌트 트리 확인 - -### 문제 4: 타입 에러 - -#### 증상 -``` -Error: Cannot read property 'type' of undefined -ReferenceError: types is not defined -``` - -#### 원인 -actionTypes.js에 필요한 타입이 정의되지 않음 - -#### 해결 방법 -1. `src/actions/actionTypes.js` 확인: - ```javascript - export const types = { - // ... 기존 타입들 ... - - // 큐 관련 타입들 - ENQUEUE_PANEL_ACTION: 'ENQUEUE_PANEL_ACTION', - PROCESS_PANEL_QUEUE: 'PROCESS_PANEL_QUEUE', - CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE', - SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING', - - // 비동기 액션 타입들 - ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION', - COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION', - FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION', - }; - ``` - -2. 없다면 추가 후 저장 - -### 문제 5: 순서가 여전히 보장되지 않음 - -#### 증상 -```javascript -dispatch(pushPanelQueued({ name: 'PANEL_1' })); -dispatch(pushPanelQueued({ name: 'PANEL_2' })); -// PANEL_2가 먼저 열림 -``` - -#### 원인 -일반 `pushPanel`과 `pushPanelQueued`를 혼용 - -#### 해결 방법 -순서를 보장하려면 **모두** queued 버전 사용: -```javascript -// ❌ 잘못된 사용 -dispatch(pushPanel({ name: 'PANEL_1' })); // 일반 버전 -dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐 버전 - -// ✅ 올바른 사용 -dispatch(pushPanelQueued({ name: 'PANEL_1' })); -dispatch(pushPanelQueued({ name: 'PANEL_2' })); - -// 또는 -dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: 'PANEL_1' }), - pushPanelQueued({ name: 'PANEL_2' }) -])); -``` - ---- - -## 빠른 체크리스트 - -설정이 완료되었는지 빠르게 확인: - -- [ ] `src/store/store.js`에 `import panelQueueMiddleware` 추가됨 -- [ ] `applyMiddleware`에 `panelQueueMiddleware` 추가됨 (맨 마지막 위치) -- [ ] 파일 저장 및 앱 재시작 -- [ ] 브라우저 콘솔에서 큐 관련 로그 확인 -- [ ] Redux DevTools에서 액션 흐름 확인 -- [ ] Redux state에서 큐 관련 상태 확인 - -모든 항목이 체크되었다면 설정 완료! 🎉 - ---- - -## 참고 자료 - -- [README.md](./README.md) - 전체 개요 -- [04-solution-queue-system.md](./04-solution-queue-system.md) - 큐 시스템 상세 설명 -- [05-usage-patterns.md](./05-usage-patterns.md) - 사용 패턴 및 예제 -- [07-changelog.md](./07-changelog.md) - 변경 이력 - ---- - -**작성일**: 2025-11-10 -**최종 수정일**: 2025-11-10 diff --git a/.docs/dispatch-async/07-changelog.md b/.docs/dispatch-async/07-changelog.md deleted file mode 100644 index 130ba5e2..00000000 --- a/.docs/dispatch-async/07-changelog.md +++ /dev/null @@ -1,314 +0,0 @@ -# 변경 이력 (Changelog) - -## [2025-11-10] - 미들웨어 등록 및 문서 개선 - -### 🔧 수정 (Fixed) - -#### store.js - panelQueueMiddleware 등록 -**커밋**: `c12cc91 [수정] panelQueueMiddleware 등록 및 문서 업데이트` - -**문제**: -- panelQueueMiddleware가 store.js에 등록되어 있지 않았음 -- 큐 시스템이 작동하지 않는 치명적인 문제 -- `ENQUEUE_PANEL_ACTION` dispatch 시 자동으로 `PROCESS_PANEL_QUEUE`가 실행되지 않음 - -**해결**: -```javascript -// src/store/store.js -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - -export const store = createStore( - rootReducer, - applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware) -); -``` - -**영향**: -- ✅ 큐 기반 패널 액션 시스템이 정상 작동 -- ✅ 패널 액션 순서 보장 -- ✅ 비동기 패널 액션 자동 처리 - -### 📝 문서 (Documentation) - -#### README.md -- "설치 및 설정" 섹션 추가 -- panelQueueMiddleware 등록 필수 사항 강조 -- 등록하지 않으면 큐 시스템이 작동하지 않는다는 경고 추가 - -#### 04-solution-queue-system.md -- "사전 요구사항" 섹션 추가 -- 미들웨어 등록 코드 예제 포함 -- `src/store/store.js`를 관련 파일에 추가 - -#### 05-usage-patterns.md -- "초기 설정 확인사항" 체크리스트 추가 -- panelQueueMiddleware 등록 여부를 최우선 확인 항목으로 배치 - ---- - -## [2025-11-10] - 초기 문서 작성 - -### ✨ 추가 (Added) - -#### 문서 작성 -**커밋**: `f75860c [문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성` - -dispatch 비동기 처리 순서 보장 문제와 해결 방법을 체계적으로 정리한 문서 세트: - -1. **README.md** - - 전체 개요 및 목차 - - 주요 솔루션 요약 - - 관련 파일 목록 - - 커밋 히스토리 - -2. **01-problem.md** - - 문제 상황 및 원인 분석 - - Redux-thunk에서 dispatch 순서가 보장되지 않는 이유 - - 실제 발생 가능한 버그 시나리오 - - 기존 해결 방법의 한계 - -3. **02-solution-dispatch-helper.md** - - dispatchHelper.js 솔루션 설명 - - 5가지 헬퍼 함수 상세 설명: - - `createSequentialDispatch` - - `createApiThunkWithChain` - - `withLoadingState` - - `createConditionalDispatch` - - `createParallelDispatch` - - Before/After 코드 비교 - - 실제 사용 예제 - -4. **03-solution-async-utils.md** - - asyncActionUtils.js 솔루션 설명 - - API 성공 기준 명확화 (HTTP 200-299 + retCode 0/'0') - - Promise 체인 보장 방법 (reject 없이 resolve만 사용) - - 주요 함수 설명: - - `isApiSuccess` - - `fetchApi` - - `tAxiosToPromise` - - `wrapAsyncAction` - - `withTimeout` - - `executeParallelAsyncActions` - -5. **04-solution-queue-system.md** - - 큐 기반 패널 액션 시스템 설명 - - 기본 패널 액션 (pushPanelQueued, popPanelQueued 등) - - 비동기 패널 액션 (enqueueAsyncPanelAction) - - API 호출 후 패널 액션 (createApiWithPanelActions) - - 비동기 액션 시퀀스 (createAsyncPanelSequence) - - panelQueueMiddleware 동작 원리 - - 리듀서 상태 구조 - -6. **05-usage-patterns.md** - - 솔루션 선택 가이드 (의사결정 플로우차트) - - 솔루션 비교표 - - 공통 패턴 Before/After 비교 - - 실전 예제 5가지: - - 검색 기능 - - 장바구니 추가 - - 로그인 플로우 - - 다단계 폼 제출 - - 병렬 데이터 로딩 - - 마이그레이션 가이드 - - Best Practices - - 체크리스트 - -**문서 통계**: -- 총 6개 마크다운 파일 -- 약 3,000줄 -- 50개 이상의 코드 예제 -- Before/After 비교 20개 이상 - ---- - -## [2025-11-06] - 큐 시스템 구현 - -### ✨ 추가 (Added) - -#### Dispatch Queue Implementation -**커밋**: `f9290a1 [251106] fix: Dispatch Queue implementation` - -- `asyncActionUtils.js` 추가 - - Promise 기반 비동기 액션 처리 - - API 성공 기준 명확화 - - 타임아웃 지원 - -- `queuedPanelActions.js` 확장 - - 비동기 패널 액션 지원 - - API 호출 후 패널 액션 자동 실행 - - 비동기 액션 시퀀스 - -- `panelReducer.js` 확장 - - 큐 상태 관리 - - 비동기 액션 상태 추적 - - 큐 처리 통계 - -#### Queued Panel Functions -**커밋**: `5bd2774 [251106] feat: Queued Panel functions` - -- `queuedPanelActions.js` 초기 구현 - - 기본 큐 액션 (pushPanelQueued, popPanelQueued 등) - - 여러 액션 일괄 큐 추가 - - 패널 시퀀스 생성 - -- `panelQueueMiddleware.js` 추가 - - 큐 액션 자동 감지 - - 순차 처리 자동 시작 - - 연속 처리 지원 - -- `panelReducer.js` 큐 기능 추가 - - 큐 상태 관리 - - 큐 처리 로직 - - 큐 통계 수집 - ---- - -## [2025-11-05] - dispatch 헬퍼 함수 - -### ✨ 추가 (Added) - -#### dispatchHelper.js -**커밋**: `9490d72 [251105] feat: dispatchHelper.js` - -Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음: - -- `createSequentialDispatch` - - 여러 dispatch를 순차적으로 실행 - - Promise 체인으로 순서 보장 - - delay 옵션 지원 - - stopOnError 옵션 지원 - -- `createApiThunkWithChain` - - API 호출 후 dispatch 자동 체이닝 - - TAxios onSuccess/onFail 패턴 호환 - - response를 각 action에 전달 - - 에러 처리 action 지원 - -- `withLoadingState` - - 로딩 상태 자동 관리 - - changeAppStatus 자동 on/off - - 성공/에러 시 추가 dispatch 지원 - - loadingType 옵션 - -- `createConditionalDispatch` - - 조건에 따라 다른 dispatch 실행 - - getState() 결과 기반 분기 - - 배열 또는 단일 action 지원 - -- `createParallelDispatch` - - 여러 API를 병렬로 실행 - - Promise.all 사용 - - 로딩 상태 관리 옵션 - ---- - -## 관련 커밋 전체 목록 - -```bash -c12cc91 [수정] panelQueueMiddleware 등록 및 문서 업데이트 -f75860c [문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성 -f9290a1 [251106] fix: Dispatch Queue implementation -5bd2774 [251106] feat: Queued Panel functions -9490d72 [251105] feat: dispatchHelper.js -``` - ---- - -## 마이그레이션 가이드 - -### 기존 코드에서 새 솔루션으로 전환 - -#### 1단계: setTimeout 패턴 제거 - -```javascript -// Before -dispatch(action1()); -setTimeout(() => { - dispatch(action2()); -}, 0); - -// After -dispatch(createSequentialDispatch([action1(), action2()])); -``` - -#### 2단계: API 패턴 개선 - -```javascript -// Before -const onSuccess = (response) => { - dispatch({ type: types.ACTION_1, payload: response.data }); - dispatch(action2()); -}; -TAxios(..., onSuccess, onFail); - -// After -dispatch(createApiThunkWithChain( - (d, gs, onS, onF) => TAxios(d, gs, ..., onS, onF), - [ - (response) => ({ type: types.ACTION_1, payload: response.data }), - action2() - ] -)); -``` - -#### 3단계: 패널 액션을 큐 버전으로 전환 - -```javascript -// Before -dispatch(pushPanel({ name: panel_names.SEARCH })); - -// After -dispatch(pushPanelQueued({ name: panel_names.SEARCH })); -``` - ---- - -## Breaking Changes - -### 없음 - -모든 새로운 기능은 기존 코드와 완전히 호환됩니다: -- 기존 `pushPanel`, `popPanel` 등은 그대로 동작 -- 새로운 큐 버전은 선택적으로 사용 가능 -- 점진적 마이그레이션 가능 - ---- - -## 알려진 이슈 - -### 해결됨 - -1. **panelQueueMiddleware 미등록 문제** (2025-11-10 해결) - - 문제: 큐 시스템이 작동하지 않음 - - 해결: store.js에 미들웨어 등록 - -### 현재 이슈 - -없음 - ---- - -## 향후 계획 - -### 예정된 개선사항 - -1. **성능 최적화** - - 큐 처리 성능 모니터링 - - 대량 액션 처리 최적화 - -2. **에러 처리 강화** - - 더 상세한 에러 메시지 - - 에러 복구 전략 - -3. **개발자 도구** - - 큐 상태 시각화 - - 디버깅 도구 - -4. **테스트 코드** - - 단위 테스트 추가 - - 통합 테스트 추가 - ---- - -**작성일**: 2025-11-10 -**최종 수정일**: 2025-11-10 diff --git a/.docs/dispatch-async/08-troubleshooting.md b/.docs/dispatch-async/08-troubleshooting.md deleted file mode 100644 index 5778af77..00000000 --- a/.docs/dispatch-async/08-troubleshooting.md +++ /dev/null @@ -1,606 +0,0 @@ -# 트러블슈팅 가이드 - -## 📋 목차 - -1. [일반적인 문제](#일반적인-문제) -2. [큐 시스템 문제](#큐-시스템-문제) -3. [API 호출 문제](#api-호출-문제) -4. [성능 문제](#성능-문제) -5. [디버깅 팁](#디버깅-팁) - ---- - -## 일반적인 문제 - -### 문제 1: dispatch 순서가 여전히 보장되지 않음 - -#### 증상 -```javascript -dispatch(action1()); -dispatch(action2()); -dispatch(action3()); -// 실행 순서: action2 → action3 → action1 -``` - -#### 가능한 원인 - -1. **일반 dispatch와 큐 dispatch 혼용** - ```javascript - // ❌ 잘못된 사용 - dispatch(pushPanel({ name: 'PANEL_1' })); // 일반 - dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐 - ``` - -2. **async/await 없이 비동기 처리** - ```javascript - // ❌ 잘못된 사용 - fetchData(); // Promise를 기다리지 않음 - dispatch(action()); - ``` - -3. **헬퍼 함수를 사용하지 않음** - ```javascript - // ❌ 잘못된 사용 - dispatch(asyncAction1()); - dispatch(asyncAction2()); // asyncAction1이 완료되기 전에 실행 - ``` - -#### 해결 방법 - -**방법 1: 큐 시스템 사용** (패널 액션인 경우) -```javascript -// ✅ 올바른 사용 -dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: 'PANEL_1' }), - pushPanelQueued({ name: 'PANEL_2' }), - pushPanelQueued({ name: 'PANEL_3' }) -])); -``` - -**방법 2: createSequentialDispatch 사용** -```javascript -// ✅ 올바른 사용 -dispatch(createSequentialDispatch([ - action1(), - action2(), - action3() -])); -``` - -**방법 3: async/await 사용** (Chrome 68+) -```javascript -// ✅ 올바른 사용 -export const myAction = () => async (dispatch, getState) => { - await dispatch(action1()); - await dispatch(action2()); - await dispatch(action3()); -}; -``` - ---- - -### 문제 2: "Cannot find module" 에러 - -#### 증상 -``` -Error: Cannot find module '../utils/dispatchHelper' -Error: Cannot find module '../middleware/panelQueueMiddleware' -``` - -#### 원인 -- 파일이 존재하지 않음 -- import 경로가 잘못됨 -- 빌드가 필요함 - -#### 해결 방법 - -**Step 1: 파일 존재 확인** -```bash -# 프로젝트 루트에서 실행 -ls -la com.twin.app.shoptime/src/utils/dispatchHelper.js -ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js -ls -la com.twin.app.shoptime/src/utils/asyncActionUtils.js -``` - -**Step 2: 최신 코드 pull** -```bash -git fetch origin -git pull origin -``` - -**Step 3: node_modules 재설치** -```bash -cd com.twin.app.shoptime -rm -rf node_modules package-lock.json -npm install -``` - -**Step 4: 빌드 재실행** -```bash -npm run build -# 또는 -npm start -``` - ---- - -### 문제 3: 타입 에러 (types is not defined) - -#### 증상 -``` -ReferenceError: types is not defined -TypeError: Cannot read property 'ENQUEUE_PANEL_ACTION' of undefined -``` - -#### 원인 -actionTypes.js에 필요한 타입이 정의되지 않음 - -#### 해결 방법 - -**Step 1: actionTypes.js 확인** -```javascript -// src/actions/actionTypes.js -export const types = { - // ... 기존 타입들 ... - - // 큐 관련 타입들 (필수!) - ENQUEUE_PANEL_ACTION: 'ENQUEUE_PANEL_ACTION', - PROCESS_PANEL_QUEUE: 'PROCESS_PANEL_QUEUE', - CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE', - SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING', - - // 비동기 액션 타입들 (필수!) - ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION', - COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION', - FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION', -}; -``` - -**Step 2: import 확인** -```javascript -import { types } from '../actions/actionTypes'; -``` - ---- - -## 큐 시스템 문제 - -### 문제 4: 큐가 처리되지 않음 - -#### 증상 -```javascript -dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL })); -// 아무 일도 일어나지 않음 -// 콘솔 로그도 없음 -``` - -#### 원인 -**panelQueueMiddleware가 등록되지 않음** (가장 흔한 문제!) - -#### 해결 방법 - -**Step 1: store.js 확인** -```javascript -// src/store/store.js -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - -export const store = createStore( - rootReducer, - applyMiddleware( - thunk, - panelHistoryMiddleware, - autoCloseMiddleware, - panelQueueMiddleware // ← 이것이 있는지 확인! - ) -); -``` - -**Step 2: import 경로 확인** -```javascript -// ✅ 올바른 import -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; - -// ❌ 잘못된 import -import { panelQueueMiddleware } from '../middleware/panelQueueMiddleware'; -// default export이므로 중괄호 없이 import해야 함 -``` - -**Step 3: 앱 재시작** -```bash -# 개발 서버 재시작 -npm start -``` - -**Step 4: 브라우저 캐시 삭제** -- Chrome: Ctrl+Shift+R (Windows) 또는 Cmd+Shift+R (Mac) - ---- - -### 문제 5: 큐가 무한 루프에 빠짐 - -#### 증상 -``` -[panelQueueMiddleware] 🚀 ACTION_ENQUEUED -[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -... (무한 반복) -``` - -#### 원인 -1. 큐 처리 중에 다시 큐에 액션 추가 -2. `isProcessingQueue` 플래그가 제대로 설정되지 않음 - -#### 해결 방법 - -**방법 1: 큐 액션 내부에서 일반 dispatch 사용** -```javascript -// ❌ 잘못된 사용 (무한 루프 발생) -export const myAction = () => (dispatch) => { - dispatch(pushPanelQueued({ name: 'PANEL_1' })); - dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐 처리 중 큐 추가 -}; - -// ✅ 올바른 사용 -export const myAction = () => (dispatch) => { - dispatch(enqueueMultiplePanelActions([ - pushPanelQueued({ name: 'PANEL_1' }), - pushPanelQueued({ name: 'PANEL_2' }) - ])); -}; -``` - -**방법 2: 리듀서 로직 확인** -```javascript -// panelReducer.js에서 확인 -case types.PROCESS_PANEL_QUEUE: { - // 이미 처리 중이면 무시 - if (state.isProcessingQueue || state.panelActionQueue.length === 0) { - return state; // ← 이 로직이 있는지 확인 - } - // ... -} -``` - ---- - -### 문제 6: 큐 통계가 업데이트되지 않음 - -#### 증상 -```javascript -store.getState().panels.queueStats -// { totalProcessed: 0, failedCount: 0, averageProcessingTime: 0 } -// 항상 0으로 유지됨 -``` - -#### 원인 -큐 처리가 정상적으로 완료되지 않음 - -#### 해결 방법 - -**Step 1: 콘솔 로그 확인** -``` -[panelReducer] ✅ QUEUE_ITEM_PROCESSED ← 이 로그가 보이는지 확인 -``` - -**Step 2: 에러 발생 확인** -```javascript -store.getState().panels.queueError -// null이어야 정상 -``` - -**Step 3: 큐 처리 완료 여부 확인** -```javascript -store.getState().panels.isProcessingQueue -// false여야 정상 (처리 완료) -``` - ---- - -## API 호출 문제 - -### 문제 7: API 성공인데 onFail이 호출됨 - -#### 증상 -```javascript -// API 호출 -// HTTP 200, retCode: 0 -// 그런데 onFail이 호출됨 -``` - -#### 원인 -프로젝트 성공 기준을 이해하지 못함 - -#### 프로젝트 성공 기준 -**HTTP 200-299 + retCode 0/'0' 둘 다 만족해야 성공!** - -```javascript -// ✅ 성공 케이스 -{ status: 200, data: { retCode: 0, data: {...} } } -{ status: 200, data: { retCode: '0', data: {...} } } - -// ❌ 실패 케이스 -{ status: 200, data: { retCode: 1, message: '에러' } } // retCode가 0이 아님 -{ status: 500, data: { retCode: 0, data: {...} } } // HTTP 에러 -``` - -#### 해결 방법 - -**방법 1: isApiSuccess 사용** -```javascript -import { isApiSuccess } from '../utils/asyncActionUtils'; - -const response = { status: 200 }; -const responseData = { retCode: 1, message: '에러' }; - -if (isApiSuccess(response, responseData)) { - // 성공 처리 -} else { - // 실패 처리 (retCode가 1이므로 실패!) -} -``` - -**방법 2: asyncActionUtils 사용** -```javascript -import { tAxiosToPromise } from '../utils/asyncActionUtils'; - -const result = await tAxiosToPromise(...); - -if (result.success) { - // HTTP 200-299 + retCode 0/'0' - console.log(result.data); -} else { - // 실패 - console.error(result.error); -} -``` - ---- - -### 문제 8: API 타임아웃이 작동하지 않음 - -#### 증상 -```javascript -dispatch(enqueueAsyncPanelAction({ - asyncAction: (d, gs, onS, onF) => { /* 느린 API */ }, - timeout: 5000 // 5초 -})); -// 10초가 지나도 타임아웃되지 않음 -``` - -#### 원인 -1. `withTimeout`이 적용되지 않음 -2. 타임아웃 값이 잘못 설정됨 - -#### 해결 방법 - -**방법 1: enqueueAsyncPanelAction 사용 시** -```javascript -// ✅ timeout 옵션 사용 -dispatch(enqueueAsyncPanelAction({ - asyncAction: (dispatch, getState, onSuccess, onFail) => { - TAxios(dispatch, getState, 'get', URL, {}, {}, onSuccess, onFail); - }, - timeout: 5000, // 5초 (ms 단위) - onFail: (error) => { - if (error.code === 'TIMEOUT') { - console.error('타임아웃 발생!'); - } - } -})); -``` - -**방법 2: withTimeout 직접 사용** -```javascript -import { withTimeout, fetchApi } from '../utils/asyncActionUtils'; - -const result = await withTimeout( - fetchApi('/api/slow-endpoint'), - 5000, // 5초 - '요청 시간이 초과되었습니다' -); - -if (result.error?.code === 'TIMEOUT') { - console.error('타임아웃!'); -} -``` - ---- - -## 성능 문제 - -### 문제 9: 큐 처리가 너무 느림 - -#### 증상 -```javascript -// 100개의 패널 액션을 큐에 추가 -// 처리하는데 10초 이상 소요 -``` - -#### 원인 -1. 각 액션이 복잡한 로직 수행 -2. 동기적으로 처리되어 병목 발생 - -#### 해결 방법 - -**방법 1: 불필요한 액션 제거** -```javascript -// ❌ 잘못된 사용 -for (let i = 0; i < 100; i++) { - dispatch(pushPanelQueued({ name: `PANEL_${i}` })); -} - -// ✅ 올바른 사용 - 필요한 것만 -dispatch(pushPanelQueued({ name: 'MAIN_PANEL' })); -``` - -**방법 2: 배치 처리** -```javascript -// 한 번에 여러 액션 추가 -dispatch(enqueueMultiplePanelActions( - panels.map(panel => pushPanelQueued(panel)) -)); -``` - -**방법 3: 병렬 처리가 필요하면 큐 사용 안함** -```javascript -// 순서가 중요하지 않은 경우 -dispatch(createParallelDispatch([ - fetchData1(), - fetchData2(), - fetchData3() -])); -``` - ---- - -### 문제 10: 메모리 누수 - -#### 증상 -```javascript -// 오랜 시간 앱 사용 후 -store.getState().panels.completedAsyncActions.length -// → 10000개 이상 -``` - -#### 원인 -완료된 비동기 액션 ID가 계속 누적됨 - -#### 해결 방법 - -**방법 1: 주기적으로 클리어** -```javascript -// 일정 시간마다 완료된 액션 정리 -setInterval(() => { - const state = store.getState().panels; - - if (state.completedAsyncActions.length > 1000) { - // 클리어 액션 dispatch - dispatch({ type: types.CLEAR_COMPLETED_ASYNC_ACTIONS }); - } -}, 60000); // 1분마다 -``` - -**방법 2: 리듀서에 최대 개수 제한 추가** -```javascript -// panelReducer.js -case types.COMPLETE_ASYNC_PANEL_ACTION: { - const newCompleted = [...state.completedAsyncActions, action.payload.actionId]; - - // 최근 100개만 유지 - const trimmed = newCompleted.slice(-100); - - return { - ...state, - completedAsyncActions: trimmed - }; -} -``` - ---- - -## 디버깅 팁 - -### Tip 1: 콘솔 로그 활용 - -모든 헬퍼 함수와 미들웨어는 상세한 로그를 출력합니다: - -```javascript -// 큐 관련 로그 -[panelQueueMiddleware] 🚀 ACTION_ENQUEUED -[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS -[panelReducer] 🟡 PROCESS_PANEL_QUEUE -[panelReducer] 🟡 PROCESSING_QUEUE_ITEM -[panelReducer] ✅ QUEUE_ITEM_PROCESSED - -// 비동기 액션 로그 -[queuedPanelActions] 🔄 ENQUEUE_ASYNC_PANEL_ACTION -[queuedPanelActions] ⚡ EXECUTING_ASYNC_ACTION -[queuedPanelActions] ✅ ASYNC_ACTION_SUCCESS - -// asyncActionUtils 로그 -[asyncActionUtils] 🌐 FETCH_API_START -[asyncActionUtils] 📊 API_RESPONSE -[asyncActionUtils] ✅ TAXIOS_SUCCESS -``` - -### Tip 2: Redux DevTools 사용 - -1. Chrome 확장 프로그램 설치: Redux DevTools -2. 개발자 도구 → Redux 탭 -3. 액션 히스토리 확인 -4. State diff 확인 - -### Tip 3: 브레이크포인트 설정 - -```javascript -// 디버깅용 브레이크포인트 -export const myAction = () => (dispatch, getState) => { - debugger; // ← 여기서 멈춤 - - const state = getState(); - console.log('Current state:', state); - - dispatch(action1()); - - debugger; // ← 여기서 다시 멈춤 -}; -``` - -### Tip 4: State 스냅샷 - -```javascript -// 콘솔에서 실행 -const snapshot = JSON.parse(JSON.stringify(store.getState())); -console.log(snapshot); - -// 특정 부분만 -const panelsSnapshot = JSON.parse(JSON.stringify(store.getState().panels)); -console.log(panelsSnapshot); -``` - -### Tip 5: 큐 상태 모니터링 - -```javascript -// 콘솔에서 실행 -window.monitorQueue = setInterval(() => { - const state = store.getState().panels; - console.log('Queue status:', { - queueLength: state.panelActionQueue.length, - isProcessing: state.isProcessingQueue, - stats: state.queueStats - }); -}, 1000); - -// 중지 -clearInterval(window.monitorQueue); -``` - ---- - -## 도움이 필요하신가요? - -### 체크리스트 - -문제 해결 전에 다음을 확인하세요: - -- [ ] panelQueueMiddleware가 store.js에 등록되어 있는가? -- [ ] 필요한 파일들이 모두 존재하는가? -- [ ] actionTypes.js에 필요한 타입들이 정의되어 있는가? -- [ ] 콘솔 로그를 확인했는가? -- [ ] Redux DevTools로 액션 흐름을 확인했는가? -- [ ] 앱을 재시작했는가? -- [ ] 브라우저 캐시를 삭제했는가? - -### 추가 리소스 - -- [README.md](./README.md) - 전체 개요 -- [06-setup-guide.md](./06-setup-guide.md) - 설정 가이드 -- [05-usage-patterns.md](./05-usage-patterns.md) - 사용 패턴 -- [07-changelog.md](./07-changelog.md) - 변경 이력 - ---- - -**작성일**: 2025-11-10 -**최종 수정일**: 2025-11-10 diff --git a/.docs/dispatch-async/README.md b/.docs/dispatch-async/README.md deleted file mode 100644 index 21572afc..00000000 --- a/.docs/dispatch-async/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Dispatch 비동기 처리 순서 보장 솔루션 - -## 📋 목차 - -1. [문제 상황](./01-problem.md) -2. [해결 방법 1: dispatchHelper.js](./02-solution-dispatch-helper.md) -3. [해결 방법 2: asyncActionUtils.js](./03-solution-async-utils.md) -4. [해결 방법 3: 큐 기반 패널 액션 시스템](./04-solution-queue-system.md) -5. [사용 패턴 및 예제](./05-usage-patterns.md) -6. [설정 가이드](./06-setup-guide.md) ⭐ -7. [변경 이력 (Changelog)](./07-changelog.md) -8. [트러블슈팅](./08-troubleshooting.md) ⭐ - -## 🎯 개요 - -이 문서는 Redux-thunk 환경에서 여러 개의 dispatch를 순차적으로 실행할 때 순서가 보장되지 않는 문제를 해결하기 위해 구현된 솔루션들을 정리한 문서입니다. - -## ⚙️ 설치 및 설정 - -### 필수: panelQueueMiddleware 등록 - -큐 기반 패널 액션 시스템을 사용하려면 **반드시** store에 미들웨어를 등록해야 합니다. - -**파일**: `src/store/store.js` - -```javascript -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; -import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware'; -import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware'; -import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; // ← 추가 - -// ... reducers ... - -export const store = createStore( - rootReducer, - applyMiddleware( - thunk, - panelHistoryMiddleware, - autoCloseMiddleware, - panelQueueMiddleware // ← 추가 (맨 마지막에 위치) - ) -); -``` - -**⚠️ 중요**: panelQueueMiddleware를 등록하지 않으면 큐 시스템이 작동하지 않습니다! - -## 🚀 주요 솔루션 - -### 1. dispatchHelper.js (2025-11-05) -Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음 - -- `createSequentialDispatch`: 순차적 dispatch 실행 -- `createApiThunkWithChain`: API 후 dispatch 자동 체이닝 -- `withLoadingState`: 로딩 상태 자동 관리 -- `createConditionalDispatch`: 조건부 dispatch -- `createParallelDispatch`: 병렬 dispatch - -### 2. asyncActionUtils.js (2025-11-06) -Promise 기반 비동기 액션 처리 및 성공/실패 기준 명확화 - -- API 성공 기준: HTTP 200-299 + retCode 0/'0' -- 모든 비동기 작업을 Promise로 래핑 -- reject 없이 resolve + success 플래그 사용 -- 타임아웃 지원 - -### 3. 큐 기반 패널 액션 시스템 (2025-11-06) -미들웨어 기반의 액션 큐 처리 시스템 - -- `queuedPanelActions.js`: 큐 기반 패널 액션 -- `panelQueueMiddleware.js`: 자동 큐 처리 미들웨어 -- `panelReducer.js`: 큐 상태 관리 - -## 📊 커밋 히스토리 - -``` -f9290a1 [251106] fix: Dispatch Queue implementation - - asyncActionUtils.js 추가 - - queuedPanelActions.js 확장 - - panelReducer.js 확장 - -5bd2774 [251106] feat: Queued Panel functions - - queuedPanelActions.js 초기 구현 - - panelQueueMiddleware.js 추가 - -9490d72 [251105] feat: dispatchHelper.js - - createSequentialDispatch - - createApiThunkWithChain - - withLoadingState - - createConditionalDispatch - - createParallelDispatch -``` - -## 📂 관련 파일 - -### Core Files -- `src/utils/dispatchHelper.js` -- `src/utils/asyncActionUtils.js` -- `src/actions/queuedPanelActions.js` -- `src/middleware/panelQueueMiddleware.js` -- `src/reducers/panelReducer.js` - -### Example Files -- `src/actions/homeActions.js` -- `src/actions/cartActions.js` - -## 🔑 핵심 개선 사항 - -1. ✅ **순서 보장**: Promise 체인과 큐 시스템으로 dispatch 순서 보장 -2. ✅ **에러 처리**: reject 대신 resolve + success 플래그로 체인 보장 -3. ✅ **성공 기준 명확화**: HTTP 상태 + retCode 둘 다 확인 -4. ✅ **타임아웃 지원**: withTimeout으로 응답 없는 API 처리 -5. ✅ **로깅**: 모든 단계에서 상세한 로그 출력 -6. ✅ **호환성**: 기존 코드와 완전 호환 (선택적 사용 가능) - -## 🎓 학습 자료 - -각 솔루션에 대한 자세한 설명은 개별 문서를 참고하세요. - -### 시작하기 -- **처음 시작한다면** → [06-setup-guide.md](./06-setup-guide.md) ⭐ -- **문제가 발생했다면** → [08-troubleshooting.md](./08-troubleshooting.md) ⭐ - -### 이해하기 -- 기존 코드의 문제점이 궁금하다면 → [01-problem.md](./01-problem.md) -- dispatchHelper 사용법이 궁금하다면 → [02-solution-dispatch-helper.md](./02-solution-dispatch-helper.md) -- asyncActionUtils 사용법이 궁금하다면 → [03-solution-async-utils.md](./03-solution-async-utils.md) -- 큐 시스템 사용법이 궁금하다면 → [04-solution-queue-system.md](./04-solution-queue-system.md) - -### 실전 적용 -- 실제 사용 예제가 궁금하다면 → [05-usage-patterns.md](./05-usage-patterns.md) -- 변경 이력을 확인하려면 → [07-changelog.md](./07-changelog.md) - ---- - -**작성일**: 2025-11-10 -**최종 수정일**: 2025-11-10 diff --git a/.docs/modal-transition-analysis.md b/.docs/modal-transition-analysis.md deleted file mode 100644 index 3707992d..00000000 --- a/.docs/modal-transition-analysis.md +++ /dev/null @@ -1,437 +0,0 @@ -# Modal 전환 기능 상세 분석 - -**작성일**: 2025-11-10 -**목적**: MediaPlayer.v2.jsx 설계를 위한 필수 기능 분석 - ---- - -## 📋 Modal 모드 전환 플로우 - -### 1. 시작: Modal 모드로 비디오 재생 - -```javascript -// actions/mediaActions.js - startMediaPlayer() -dispatch(startMediaPlayer({ - modal: true, - modalContainerId: 'some-product-id', - showUrl: 'video-url.mp4', - thumbnailUrl: 'thumb.jpg', - // ... -})); -``` - -**MediaPanel에서의 처리 (MediaPanel.jsx:114-161)**: -```javascript -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에 전달**: -```javascript - -``` - ---- - -### 2. 전환: Modal → Fullscreen - -**사용자 액션**: modal 비디오 클릭 - -```javascript -// MediaPanel.jsx:164-174 -const onVideoClick = useCallback(() => { - if (panelInfo.modal) { - dispatch(switchMediaToFullscreen()); - } -}, [dispatch, panelInfo.modal]); -``` - -**Redux Action (mediaActions.js:164-208)**: -```javascript -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 재렌더링**: -```javascript -// 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 변경 - -``` - ---- - -### 3. 복귀: Fullscreen → Modal (Back 버튼) - -```javascript -// 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 전환 시 패널을 새로 만들지 않음 -- **updatePanel**로 `panelInfo.modal`만 변경 -- **비디오 재생 상태 유지** (같은 컴포넌트 인스턴스) - -### 2. 스타일 동적 계산 -```javascript -// modal=true -style={{ - position: 'fixed', - top: '100px', - left: '200px', - width: '400px', - height: '300px' -}} - -// modal=false -style={{}} // 전체화면 (기본 CSS) -``` - -### 3. Pause/Resume 관리 -```javascript -// 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 (추가) - -```javascript -{ - // 기존 - 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 모드 스타일 적용 -```javascript -const containerStyle = useMemo(() => { - if (panelInfo?.modal && style) { - return style; // MediaPanel에서 계산한 fixed position - } - return {}; // 전체화면 -}, [panelInfo?.modal, style]); -``` - -#### 2. Modal 클릭 처리 -```javascript -const handleVideoClick = useCallback(() => { - if (panelInfo?.modal && onClick) { - onClick(); // switchMediaToFullscreen 호출 - return; - } - - // fullscreen이면 controls 토글 - toggleControls(); -}, [panelInfo?.modal, onClick]); -``` - -#### 3. isPaused 상태 동기화 -```javascript -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 전환 시 재생 복원 -```javascript -useEffect(() => { - // modal에서 fullscreen으로 전환되었을 때 - if (prevPanelInfo?.modal && !panelInfo?.modal) { - if (videoRef.current?.paused) { - videoRef.current.play(); - } - setControlsVisible(true); - } -}, [panelInfo?.modal]); -``` - -#### 5. Controls/Spotlight 비활성화 -```javascript -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개) - -```javascript -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개) - -```javascript -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에 있음 - -**→ 여전히 대폭 간소화 가능!** diff --git a/.docs/video-player-analysis-and-optimization-plan.md b/.docs/video-player-analysis-and-optimization-plan.md deleted file mode 100644 index f3db4859..00000000 --- a/.docs/video-player-analysis-and-optimization-plan.md +++ /dev/null @@ -1,214 +0,0 @@ -# 비디오 플레이어 분석 및 최적화 계획 - -**작성일**: 2025-11-10 -**대상**: MediaPlayer.v2.jsx 설계 - ---- - -## 📊 현재 구조 분석 - -### 1. 발견된 파일들 - -| 파일 | 경로 | 라인 수 | 타입 | -|------|------|---------|------| -| VideoPlayer.js | `src/components/VideoPlayer/VideoPlayer.js` | 2,658 | Class Component | -| MediaPlayer.jsx | `src/components/VideoPlayer/MediaPlayer.jsx` | 2,595 | Class Component | -| MediaPanel.jsx | `src/views/MediaPanel/MediaPanel.jsx` | 415 | Function Component | -| PlayerPanel.jsx | `src/views/PlayerPanel/PlayerPanel.jsx` | 25,146+ | (파일 읽기 실패) | - -### 2. 주요 문제점 - -#### 🔴 심각한 코드 비대화 -``` -VideoPlayer.js: 2,658 라인 (클래스 컴포넌트) -MediaPlayer.jsx: 2,595 라인 (거의 동일한 복사본) -PlayerPanel.jsx: 25,146+ 라인 -``` - -#### 🔴 과도한 Enact 프레임워크 의존성 -```javascript -// 7개 이상의 Decorator 래핑 -ApiDecorator -I18nContextDecorator -Slottable -FloatingLayerDecorator -Skinnable -SpotlightContainerDecorator -Spottable, Touchable -``` - -#### 🔴 복잡한 상태 관리 (20+ 상태 변수) -```javascript -state = { - // 미디어 상태 - currentTime, duration, paused, loading, error, - playbackRate, proportionLoaded, proportionPlayed, - - // UI 상태 - announce, feedbackVisible, feedbackAction, - mediaControlsVisible, mediaSliderVisible, miniFeedbackVisible, - titleVisible, infoVisible, bottomControlsRendered, - - // 기타 - sourceUnavailable, titleOffsetHeight, bottomOffsetHeight, - lastFocusedTarget, slider5WayPressed, thumbnailUrl -} -``` - -#### 🔴 메모리 점유 과다 - -**8개의 Job 인스턴스**: -- `autoCloseJob` - 자동 controls 숨김 -- `hideTitleJob` - 타이틀 숨김 -- `hideFeedbackJob` - 피드백 숨김 -- `hideMiniFeedbackJob` - 미니 피드백 숨김 -- `rewindJob` - 되감기 처리 -- `announceJob` - 접근성 알림 -- `renderBottomControl` - 하단 컨트롤 렌더링 -- `slider5WayPressJob` - 슬라이더 5-way 입력 - -**다수의 이벤트 리스너**: -- `mousemove`, `touchmove`, `keydown`, `wheel` -- 복잡한 Spotlight 포커스 시스템 - -#### 🔴 불필요한 기능들 (MediaPanel에서 미사용) -```javascript -// PlayerOverlayQRCode (QR코드 표시) -// VideoOverlayWithPhoneNumber (전화번호 오버레이) -// ThemeIndicatorArrow (테마 인디케이터) -// FeedbackTooltip, MediaTitle (주석 처리됨) -// 복잡한 TabContainerV2 동기화 -// Redux 통합 (updateVideoPlayState) -``` - ---- - -## 🔍 webOS 특정 기능 분석 - -### 필수 기능 - -#### 1. Spotlight 포커스 관리 -```javascript -// 리모컨 5-way 네비게이션 -SpotlightContainerDecorator -Spottable, Touchable -``` - -#### 2. Media 컴포넌트 (webOS 전용) -```javascript -videoComponent: window.PalmSystem ? Media : TReactPlayer -``` - -#### 3. playbackRate 네거티브 지원 -```javascript -if (platform.webos) { - this.video.playbackRate = pbNumber; // 음수 지원 (되감기) -} else { - // 브라우저: 수동 되감기 구현 - this.beginRewind(); -} -``` - -### 제거 가능한 기능 - -- FloatingLayer 시스템 -- 복잡한 announce/accessibility 시스템 -- Marquee 애니메이션 -- 다중 오버레이 시스템 -- Job 기반 타이머 → `setTimeout`으로 대체 가능 - ---- - -## 📐 MediaPlayer.v2.jsx 초기 설계 (수정 전) - -### 설계 원칙 -``` -1. 함수 컴포넌트 + React Hooks 사용 -2. 상태 최소화 (5~7개만) -3. Enact 의존성 최소화 (Spotlight 기본만) -4. 직접 video element 제어 -5. props 최소화 (15개 이하) -6. 단순한 controls UI -7. 메모리 효율성 우선 -``` - -### 최소 상태 (6개) -```javascript -const [currentTime, setCurrentTime] = useState(0); -const [duration, setDuration] = useState(0); -const [paused, setPaused] = useState(true); -const [loading, setLoading] = useState(true); -const [controlsVisible, setControlsVisible] = useState(false); -const [error, setError] = useState(null); -``` - -### 필수 Props (~12개) -```javascript -{ - src, // 비디오 URL - type, // 비디오 타입 - autoPlay, // 자동 재생 - loop, // 반복 재생 - disabled, // modal 상태 - onEnded, // 종료 콜백 - onError, // 에러 콜백 - onBackButton, // 뒤로가기 - thumbnailUrl, // 썸네일 - panelInfo, // 패널 정보 - spotlightId, // spotlight ID - videoComponent // Media or TReactPlayer -} -``` - -### 제거할 기능들 -``` -❌ QR코드 오버레이 -❌ 전화번호 오버레이 -❌ 테마 인디케이터 -❌ 복잡한 피드백 시스템 -❌ MediaSlider (seek bar) -❌ 자동 숨김/보임 Job 시스템 -❌ Announce/Accessibility 복잡계 -❌ FloatingLayer -❌ Redux 통합 -❌ TabContainer 동기화 -❌ 다중 overlay 시스템 -❌ MediaTitle, infoComponents -❌ jumpBy, fastForward, rewind -❌ playbackRate 조정 -``` - ---- - -## 📈 예상 개선 효과 - -| 항목 | 현재 | 개선 후 | 개선율 | -|------|------|---------|--------| -| **코드 라인** | 2,595 | ~500 | **80% 감소** | -| **상태 변수** | 20+ | 5~7 | **65% 감소** | -| **Props** | 70+ | ~12 | **83% 감소** | -| **타이머/Job** | 8 | 2~3 | **70% 감소** | -| **메모리 점유** | 높음 | 낮음 | **예상 50%+ 감소** | -| **렌더링 속도** | 느림 | 빠름 | **예상 2~3배 향상** | - ---- - -## 🚨 중요 요구사항 추가 - -### Modal 모드 전환 기능 (필수) - -사용자 피드백: -> "비디오 플레이어가 이렇게 복잡하게 된 데에는 다 이유가 있다. -> modal=true 모드에서 화면의 일부 크기로 재생이 되다가 -> 그 화면 그대로 키워서 modal=false로 전체화면으로 비디오를 재생하는 부분이 있어야 한다." - -**→ 이 기능은 반드시 유지되어야 함** - ---- - -## 📝 다음 단계 - -1. Modal 전환 기능 상세 분석 -2. 필수 기능 재정의 -3. MediaPlayer.v2.jsx 재설계 -4. 구현 우선순위 결정